Removed unused parts of Godot for export
This commit is contained in:
@@ -23,6 +23,10 @@ let
|
||||
# disable SIMD on some CPUs (e.g. Firefox on x86/x64 without SSE4.1) and then
|
||||
# reject the module with "v128 not enabled", so compile a matching template
|
||||
# without it and ship that instead.
|
||||
#
|
||||
# The template is also stripped down to what this 2D game needs: no 3D, 3D
|
||||
# physics, navigation, XR or any engine module it never touches. 2D physics is
|
||||
# kept because `scenes/units/unit.tscn` uses an `Area2D` for unit hover.
|
||||
webTemplates = stdenv.mkDerivation (finalAttrs: {
|
||||
pname = "godot-web-nosimd-template";
|
||||
inherit (godot_4_6) version;
|
||||
@@ -54,8 +58,25 @@ let
|
||||
# archive the template) rejects because it predates 1980.
|
||||
find . -exec touch -h {} +
|
||||
|
||||
# Only the modules this game actually uses are compiled in; everything
|
||||
# else (3D and its importers, compressed/extra image formats, audio
|
||||
# codecs, ENet/WebRTC/UPnP, regex, SVG rasterisation, texture
|
||||
# compressors, ...) is left out. `modules_enabled_by_default=no` keeps a
|
||||
# future upstream module out of the build until it is enabled here.
|
||||
#
|
||||
# `force_png` (project.godot) makes the importer store lossless textures
|
||||
# as PNG, so the WebP module is not needed at runtime.
|
||||
scons platform=web target=template_release \
|
||||
wasm_simd=no threads=no dlink_enabled=no \
|
||||
disable_3d=yes disable_physics_3d=yes \
|
||||
disable_navigation_2d=yes disable_navigation_3d=yes disable_xr=yes \
|
||||
modules_enabled_by_default=no \
|
||||
module_freetype_enabled=yes \
|
||||
module_gdscript_enabled=yes \
|
||||
module_multiplayer_enabled=yes \
|
||||
module_noise_enabled=yes \
|
||||
module_text_server_adv_enabled=yes \
|
||||
module_websocket_enabled=yes \
|
||||
-j$NIX_BUILD_CORES
|
||||
|
||||
runHook postBuild
|
||||
|
||||
@@ -62,3 +62,4 @@ camera_pan_down={
|
||||
rendering_device/driver.windows="d3d12"
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
renderer/rendering_method.mobile="gl_compatibility"
|
||||
textures/lossless_compression/force_png=true
|
||||
|
||||
@@ -74,7 +74,14 @@ class _MinHeap:
|
||||
var parent := (index - 1) >> 1
|
||||
if _keys[parent] <= _keys[index]:
|
||||
break
|
||||
_swap(parent, index)
|
||||
# Swaps are inlined: the heap is the hottest loop in A*, and a
|
||||
# GDScript call per level dominated the profile.
|
||||
var key_swap := _keys[parent]
|
||||
_keys[parent] = _keys[index]
|
||||
_keys[index] = key_swap
|
||||
var value_swap := _values[parent]
|
||||
_values[parent] = _values[index]
|
||||
_values[index] = value_swap
|
||||
index = parent
|
||||
|
||||
func pop() -> Vector2i:
|
||||
@@ -84,11 +91,7 @@ class _MinHeap:
|
||||
_values[0] = _values[last]
|
||||
_keys.resize(last)
|
||||
_values.resize(last)
|
||||
if not _keys.is_empty():
|
||||
_sift_down(0)
|
||||
return value
|
||||
|
||||
func _sift_down(index: int) -> void:
|
||||
var index := 0
|
||||
var count := _keys.size()
|
||||
while true:
|
||||
var smallest := index
|
||||
@@ -100,13 +103,11 @@ class _MinHeap:
|
||||
smallest = right
|
||||
if smallest == index:
|
||||
break
|
||||
_swap(index, smallest)
|
||||
var key_swap := _keys[index]
|
||||
_keys[index] = _keys[smallest]
|
||||
_keys[smallest] = key_swap
|
||||
var value_swap := _values[index]
|
||||
_values[index] = _values[smallest]
|
||||
_values[smallest] = value_swap
|
||||
index = smallest
|
||||
|
||||
func _swap(a: int, b: int) -> void:
|
||||
var key := _keys[a]
|
||||
_keys[a] = _keys[b]
|
||||
_keys[b] = key
|
||||
var value := _values[a]
|
||||
_values[a] = _values[b]
|
||||
_values[b] = value
|
||||
return value
|
||||
|
||||
@@ -39,6 +39,10 @@ var identity_civ: Dictionary = {}
|
||||
|
||||
var _configured: bool = false
|
||||
var _profile_elapsed: float = 0.0
|
||||
## Set when the state changed since the last broadcast. Snapshots are expensive,
|
||||
## so several changes in one frame are coalesced into a single broadcast in
|
||||
## _process instead of one per changed.emit().
|
||||
var _state_dirty: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -51,6 +55,9 @@ func _ready() -> void:
|
||||
func _process(delta: float) -> void:
|
||||
if _configured and state != null:
|
||||
state.advance_movement(delta / GameClock.SECONDS_PER_HOUR)
|
||||
if _state_dirty:
|
||||
_state_dirty = false
|
||||
_broadcast_state()
|
||||
if profile_interval <= 0.0:
|
||||
return
|
||||
_profile_elapsed += delta
|
||||
@@ -139,7 +146,7 @@ func configure_game(setup: Dictionary) -> void:
|
||||
state = GameState.new()
|
||||
state.map_config = map_config
|
||||
state.configure(_load_civs(setup.get("civilisations", [])), int(setup.get("seed", 0)))
|
||||
state.changed.connect(_broadcast_state)
|
||||
state.changed.connect(_on_state_changed)
|
||||
_configured = true
|
||||
for peer_id in Network.get_peer_ids():
|
||||
_assign_civ(int(peer_id), str(peer_names.get(int(peer_id), "")))
|
||||
@@ -284,12 +291,23 @@ func _on_hour() -> void:
|
||||
state.tick_hour()
|
||||
|
||||
|
||||
func _on_state_changed() -> void:
|
||||
_state_dirty = true
|
||||
|
||||
|
||||
func _broadcast_state() -> void:
|
||||
if not Network.is_server() or state == null:
|
||||
return
|
||||
Profiler.begin("server.broadcast_state")
|
||||
# The world data is identical for everyone, so serialise it once and only
|
||||
# add each viewer's explored/visible/stats on top.
|
||||
var shared := state.serialize_shared()
|
||||
var stats_cache := {}
|
||||
for peer_id in Network.get_peer_ids():
|
||||
Network.send_game_state(peer_id, state.snapshot(int(peer_civ.get(peer_id, -1))))
|
||||
var civ := int(peer_civ.get(peer_id, -1))
|
||||
if not stats_cache.has(civ):
|
||||
stats_cache[civ] = state.viewer_stats(civ)
|
||||
Network.send_game_state(peer_id, state.viewer_snapshot(shared, civ, stats_cache[civ]))
|
||||
Profiler.end("server.broadcast_state")
|
||||
|
||||
|
||||
|
||||
+118
-40
@@ -112,11 +112,28 @@ var _max_step_length: float = 1.0
|
||||
var _next_unit_id: int = 1
|
||||
var _next_city_id: int = 1
|
||||
var _city_names_used: Dictionary = {}
|
||||
## Id -> unit/city dictionaries, so orders and training resolve in constant time
|
||||
## instead of scanning the arrays (which grow as units are trained).
|
||||
var _unit_by_id: Dictionary = {}
|
||||
var _city_by_id: Dictionary = {}
|
||||
## City lookup keyed by tile, and the tiles adjacent to a city keyed by the
|
||||
## owning civilisation. Both are derived caches rebuilt when cities change;
|
||||
## they keep per-tile economy queries off the O(cities) path.
|
||||
var _city_by_coords: Dictionary = {}
|
||||
var _city_adjacent: Dictionary = {}
|
||||
## Territory grouped by owner, so per-civ economy and visibility queries only
|
||||
## walk their own tiles instead of scanning every claimed tile. Rebuilt
|
||||
## alongside `territory` in _recompute_territory.
|
||||
var _territory_by_civ: Dictionary = {}
|
||||
## A tile's GDP per capita depends only on terrain, city placement and
|
||||
## ownership, all fixed once the world is generated, so it is memoised instead
|
||||
## of being recomputed on every economy query.
|
||||
var _gdp_per_capita_cache: Dictionary = {}
|
||||
## Serialised territory and per-viewer explored sets. Both change rarely (only
|
||||
## on configure/claiming, and when a unit reveals new ground), so the wire
|
||||
## arrays are built once and reused instead of rebuilt for every snapshot.
|
||||
var _serialized_territory: Array = []
|
||||
var _serialized_explored: Dictionary = {}
|
||||
## Terrain neighbourhoods, memoised because pathfinding asks for each tile's
|
||||
## neighbours dozens of times. Cleared on configure; never mutated by callers.
|
||||
var _neighbour_cache: Dictionary = {}
|
||||
@@ -177,9 +194,15 @@ func configure(civs: Array, world_seed: int = 0) -> void:
|
||||
visible.clear()
|
||||
_next_unit_id = 1
|
||||
_next_city_id = 1
|
||||
_unit_by_id.clear()
|
||||
_city_by_id.clear()
|
||||
_city_names_used.clear()
|
||||
_city_by_coords.clear()
|
||||
_city_adjacent.clear()
|
||||
_territory_by_civ.clear()
|
||||
_gdp_per_capita_cache.clear()
|
||||
_serialized_territory = []
|
||||
_serialized_explored.clear()
|
||||
_neighbour_cache.clear()
|
||||
total_hours = 0
|
||||
_configured = true
|
||||
@@ -377,17 +400,11 @@ func tick_hour() -> void:
|
||||
|
||||
|
||||
func find_unit(unit_id: int) -> Dictionary:
|
||||
for unit in units:
|
||||
if int(unit["id"]) == unit_id:
|
||||
return unit
|
||||
return {}
|
||||
return _unit_by_id.get(unit_id, {})
|
||||
|
||||
|
||||
func find_city(city_id: int) -> Dictionary:
|
||||
for city in cities:
|
||||
if int(city["id"]) == city_id:
|
||||
return city
|
||||
return {}
|
||||
return _city_by_id.get(city_id, {})
|
||||
|
||||
|
||||
func unit_proto(unit: Dictionary) -> ProtoUnit:
|
||||
@@ -434,14 +451,26 @@ func get_tile_info(coords: Vector2i) -> Dictionary:
|
||||
|
||||
|
||||
func get_player_population(civ: int) -> int:
|
||||
var total := 0.0
|
||||
for coords in territory.keys():
|
||||
if int(territory[coords]) == civ:
|
||||
total += float(tile_population.get(coords, 0.0))
|
||||
return roundi(total)
|
||||
return int(_player_aggregates(civ)["population"])
|
||||
|
||||
|
||||
## Population and GDP of a civilisation in a single pass over its tiles. The
|
||||
## snapshot viewer stats need both, and two separate scans doubled the cost.
|
||||
func _player_aggregates(civ: int) -> Dictionary:
|
||||
var population := 0.0
|
||||
var base_gdp := 0.0
|
||||
for coords in _territory_by_civ.get(civ, []):
|
||||
var pop := float(tile_population.get(coords, 0.0))
|
||||
population += pop
|
||||
base_gdp += pop * get_tile_gdp_per_capita(coords)
|
||||
var modifiers := get_civ_modifiers(civ)
|
||||
var gdp := base_gdp * (1.0 + float(modifiers.get(ProtoBuilding.EFFECT_GDP, 0.0)))
|
||||
return {"population": roundi(population), "gdp": gdp}
|
||||
|
||||
|
||||
func get_tile_gdp_per_capita(coords: Vector2i) -> float:
|
||||
if _gdp_per_capita_cache.has(coords):
|
||||
return _gdp_per_capita_cache[coords]
|
||||
var owner := civ_at(coords)
|
||||
if owner < 0:
|
||||
return 0.0
|
||||
@@ -451,6 +480,7 @@ func get_tile_gdp_per_capita(coords: Vector2i) -> float:
|
||||
per_capita *= economy.capital_tile_gdp_multiplier if city["is_capital"] else economy.city_tile_gdp_multiplier
|
||||
elif _is_next_to_city(coords, owner):
|
||||
per_capita *= 1.0 + economy.city_surrounding_gdp_bonus
|
||||
_gdp_per_capita_cache[coords] = per_capita
|
||||
return per_capita
|
||||
|
||||
|
||||
@@ -460,18 +490,15 @@ func get_tile_gdp(coords: Vector2i) -> float:
|
||||
|
||||
func get_player_gdp(civ: int) -> float:
|
||||
Profiler.begin("state.player_gdp")
|
||||
var total := 0.0
|
||||
for coords in territory.keys():
|
||||
if int(territory[coords]) == civ:
|
||||
total += get_tile_gdp(coords)
|
||||
var modifiers := get_civ_modifiers(civ)
|
||||
total *= 1.0 + float(modifiers.get(ProtoBuilding.EFFECT_GDP, 0.0))
|
||||
var total := float(_player_aggregates(civ)["gdp"])
|
||||
Profiler.end("state.player_gdp")
|
||||
return total
|
||||
|
||||
|
||||
func get_player_upkeep(civ: int) -> float:
|
||||
var total := get_building_upkeep(civ)
|
||||
## Total upkeep per game hour. Callers that already know the civilisation's GDP
|
||||
## (the economy tick and viewer stats) pass it in to avoid a second full scan.
|
||||
func get_player_upkeep(civ: int, gdp: float = -1.0) -> float:
|
||||
var total := get_building_upkeep(civ, gdp)
|
||||
for unit in units:
|
||||
if int(unit["civ"]) != civ:
|
||||
continue
|
||||
@@ -483,9 +510,10 @@ func get_player_upkeep(civ: int) -> float:
|
||||
|
||||
## Budget upkeep of every building a civilisation owns, per game hour. Upkeep
|
||||
## is a fraction of GDP so it keeps pace with the economy as it grows.
|
||||
func get_building_upkeep(civ: int) -> float:
|
||||
func get_building_upkeep(civ: int, gdp: float = -1.0) -> float:
|
||||
var total := 0.0
|
||||
var gdp := get_player_gdp(civ)
|
||||
if gdp < 0.0:
|
||||
gdp = get_player_gdp(civ)
|
||||
for city in cities:
|
||||
if int(city["civ"]) != civ:
|
||||
continue
|
||||
@@ -691,7 +719,7 @@ func _spawn_city(site: Dictionary) -> void:
|
||||
var coords: Vector2i = site["coords"]
|
||||
var is_capital: bool = site["is_capital"]
|
||||
var description := _city_description_for(civ, is_capital, coords)
|
||||
cities.append({
|
||||
var city := {
|
||||
"id": _next_city_id,
|
||||
"civ": civ_index,
|
||||
"coords": coords,
|
||||
@@ -700,7 +728,9 @@ func _spawn_city(site: Dictionary) -> void:
|
||||
"population": description.population,
|
||||
"improvements": description.improvements.duplicate(),
|
||||
"buildings": {},
|
||||
})
|
||||
}
|
||||
cities.append(city)
|
||||
_city_by_id[_next_city_id] = city
|
||||
_next_city_id += 1
|
||||
|
||||
|
||||
@@ -744,7 +774,7 @@ func _spawn_unit(center: Vector2i, civ_index: int, proto: ProtoUnit) -> void:
|
||||
if index < 0:
|
||||
proto_units.append(proto)
|
||||
index = proto_units.size() - 1
|
||||
units.append({
|
||||
var unit := {
|
||||
"id": _next_unit_id,
|
||||
"civ": civ_index,
|
||||
"proto": index,
|
||||
@@ -754,7 +784,9 @@ func _spawn_unit(center: Vector2i, civ_index: int, proto: ProtoUnit) -> void:
|
||||
"path": [],
|
||||
"path_index": 0,
|
||||
"progress_hours": 0.0,
|
||||
})
|
||||
}
|
||||
units.append(unit)
|
||||
_unit_by_id[_next_unit_id] = unit
|
||||
_next_unit_id += 1
|
||||
|
||||
|
||||
@@ -762,6 +794,7 @@ func _spawn_unit(center: Vector2i, civ_index: int, proto: ProtoUnit) -> void:
|
||||
## landmass, via a multi-source breadth-first search seeded from every city.
|
||||
func _recompute_territory() -> void:
|
||||
territory.clear()
|
||||
_gdp_per_capita_cache.clear()
|
||||
var owner_by_cell: Dictionary = {}
|
||||
var frontier: Array[Vector2i] = []
|
||||
for city in cities:
|
||||
@@ -781,6 +814,13 @@ func _recompute_territory() -> void:
|
||||
territory[neighbour] = owner
|
||||
next.append(neighbour)
|
||||
frontier = next
|
||||
_territory_by_civ.clear()
|
||||
for coords in territory.keys():
|
||||
var owner := int(territory[coords])
|
||||
if not _territory_by_civ.has(owner):
|
||||
_territory_by_civ[owner] = []
|
||||
_territory_by_civ[owner].append(coords)
|
||||
_serialized_territory = _serialize_cells(territory)
|
||||
|
||||
|
||||
func _generate_population() -> void:
|
||||
@@ -828,7 +868,7 @@ func _tick_economy() -> void:
|
||||
var modifiers := get_civ_modifiers(civ)
|
||||
var income := economy.budget_income(gdp, hours) \
|
||||
* (1.0 + float(modifiers.get(ProtoBuilding.EFFECT_BUDGET_INCOME, 0.0)))
|
||||
var upkeep := get_player_upkeep(civ) * hours
|
||||
var upkeep := get_player_upkeep(civ, gdp) * hours
|
||||
budgets[civ] = float(budgets[civ]) + income - upkeep
|
||||
|
||||
|
||||
@@ -867,8 +907,9 @@ func _refresh_visibility() -> void:
|
||||
visible.clear()
|
||||
for i in civilisations.size():
|
||||
visible[i] = {}
|
||||
for coords in territory.keys():
|
||||
_reveal(int(territory[coords]), coords, TERRITORY_VISION)
|
||||
for civ in _territory_by_civ.keys():
|
||||
for coords in _territory_by_civ[civ]:
|
||||
_reveal(int(civ), coords, TERRITORY_VISION)
|
||||
for unit in units:
|
||||
_reveal(int(unit["civ"]), unit["coords"], UNIT_VISION)
|
||||
Profiler.end("state.refresh_visibility")
|
||||
@@ -888,9 +929,26 @@ func _reveal(civ: int, center: Vector2i, radius: int) -> void:
|
||||
explored[civ] = {}
|
||||
if not visible.has(civ):
|
||||
visible[civ] = {}
|
||||
var explored_cells: Dictionary = explored[civ]
|
||||
var visible_cells: Dictionary = visible[civ]
|
||||
var discovered := false
|
||||
for coords in _cells_in_radius(center, radius):
|
||||
explored[civ][coords] = true
|
||||
visible[civ][coords] = true
|
||||
if not explored_cells.has(coords):
|
||||
explored_cells[coords] = true
|
||||
discovered = true
|
||||
visible_cells[coords] = true
|
||||
# Only new ground changes the serialised explored set; visibility is not
|
||||
# cached because it changes every time a unit moves.
|
||||
if discovered:
|
||||
_serialized_explored.erase(civ)
|
||||
|
||||
|
||||
## Serialised explored set for one viewer, built once and reused until a unit
|
||||
## reveals a tile the civilisation had not seen before.
|
||||
func _cached_explored(civ: int) -> Array:
|
||||
if not _serialized_explored.has(civ):
|
||||
_serialized_explored[civ] = _serialize_cells(explored.get(civ, {}))
|
||||
return _serialized_explored[civ]
|
||||
|
||||
|
||||
func _cells_in_radius(center: Vector2i, radius: int) -> Array[Vector2i]:
|
||||
@@ -961,10 +1019,16 @@ func _compute_max_step_length() -> float:
|
||||
## that cannot travel over RPC is included. When a viewer civilisation is given,
|
||||
## its explored/visible sets are included.
|
||||
func snapshot(viewer_civ: int = -1) -> Dictionary:
|
||||
return viewer_snapshot(serialize_shared(), viewer_civ)
|
||||
|
||||
|
||||
## Everything in a snapshot that is identical for every viewer. The server
|
||||
## builds this once per broadcast and layers the cheap per-viewer fields on top,
|
||||
## instead of reserialising the whole world for every connected peer.
|
||||
func serialize_shared() -> Dictionary:
|
||||
Profiler.begin("state.snapshot")
|
||||
var result := {
|
||||
"seed": seed,
|
||||
"viewer": viewer_civ,
|
||||
"total_hours": total_hours,
|
||||
"civs": _serialize_civs(),
|
||||
"protos": _serialize_protos(),
|
||||
@@ -973,22 +1037,35 @@ func snapshot(viewer_civ: int = -1) -> Dictionary:
|
||||
"technologies": _serialize_technology_defs(),
|
||||
"cities": _serialize_cities(),
|
||||
"units": _serialize_units(),
|
||||
"territory": _serialize_cells(territory),
|
||||
"territory": _serialized_territory,
|
||||
"population": _serialize_population(),
|
||||
"budgets": _serialize_budgets(),
|
||||
"research": _serialize_civ_floats(research),
|
||||
"culture": _serialize_civ_floats(culture),
|
||||
"government": _serialize_civ_ints(government),
|
||||
"researched": _serialize_civ_researched(researched),
|
||||
"viewer_stats": _viewer_stats(viewer_civ),
|
||||
"training": training.duplicate(true),
|
||||
"explored": _serialize_cells(explored.get(viewer_civ, {})),
|
||||
"visible": _serialize_cells(visible.get(viewer_civ, {})),
|
||||
}
|
||||
Profiler.end("state.snapshot")
|
||||
return result
|
||||
|
||||
|
||||
## Adds the fields that differ per viewer to a shared snapshot. `stats` may be
|
||||
## supplied by the caller to reuse a viewer_stats computed for another peer on
|
||||
## the same civilisation.
|
||||
func viewer_snapshot(shared: Dictionary, viewer_civ: int, stats: Dictionary = {}) -> Dictionary:
|
||||
var result := shared.duplicate()
|
||||
result["viewer"] = viewer_civ
|
||||
result["viewer_stats"] = stats if not stats.is_empty() else viewer_stats(viewer_civ)
|
||||
result["explored"] = _cached_explored(viewer_civ)
|
||||
result["visible"] = _serialize_cells(visible.get(viewer_civ, {}))
|
||||
return result
|
||||
|
||||
|
||||
func viewer_stats(viewer_civ: int) -> Dictionary:
|
||||
return _viewer_stats(viewer_civ)
|
||||
|
||||
|
||||
func _viewer_stats(viewer_civ: int) -> Dictionary:
|
||||
Profiler.begin("state.viewer_stats")
|
||||
if viewer_civ < 0:
|
||||
@@ -997,14 +1074,15 @@ func _viewer_stats(viewer_civ: int) -> Dictionary:
|
||||
"population": 0, "gdp": 0.0, "gdp_per_capita": 0.0, "budget": 0.0,
|
||||
"upkeep": 0.0, "research": 0.0, "culture": 0.0, "government": 0,
|
||||
}
|
||||
var population := get_player_population(viewer_civ)
|
||||
var gdp := get_player_gdp(viewer_civ)
|
||||
var aggregates := _player_aggregates(viewer_civ)
|
||||
var population := int(aggregates["population"])
|
||||
var gdp := float(aggregates["gdp"])
|
||||
var result := {
|
||||
"population": population,
|
||||
"gdp": gdp,
|
||||
"gdp_per_capita": gdp / population if population > 0 else 0.0,
|
||||
"budget": get_budget(viewer_civ),
|
||||
"upkeep": get_player_upkeep(viewer_civ),
|
||||
"upkeep": get_player_upkeep(viewer_civ, gdp),
|
||||
"research": get_research(viewer_civ),
|
||||
"culture": get_culture(viewer_civ),
|
||||
"government": get_government(viewer_civ),
|
||||
|
||||
@@ -81,6 +81,18 @@ func test_server_process_advances_movement() -> void:
|
||||
server.free()
|
||||
|
||||
|
||||
## State changes only mark the server dirty; the broadcast is deferred to the
|
||||
## next frame so a burst of orders cannot trigger several full snapshots.
|
||||
func test_state_changes_coalesce_into_one_broadcast() -> void:
|
||||
var server := _server()
|
||||
server._on_state_changed()
|
||||
server._on_state_changed()
|
||||
assert_true(server._state_dirty)
|
||||
server._process(1.0 / 60.0)
|
||||
assert_false(server._state_dirty, "the pending change was flushed")
|
||||
server.free()
|
||||
|
||||
|
||||
func test_move_order_for_enemy_unit_is_rejected() -> void:
|
||||
var server := _server()
|
||||
server.peer_civ[1] = 0
|
||||
|
||||
@@ -245,6 +245,49 @@ func test_snapshot_visibility_is_per_viewer() -> void:
|
||||
assert_ne(first["visible"], second["visible"])
|
||||
|
||||
|
||||
## The server serialises the viewer-independent half of a snapshot once and
|
||||
## layers each viewer's fog on top, so shared fields must not differ per viewer.
|
||||
func test_snapshot_shared_fields_are_viewer_independent() -> void:
|
||||
var state := _state()
|
||||
var first := state.snapshot(0)
|
||||
var second := state.snapshot(1)
|
||||
for key in ["seed", "total_hours", "civs", "protos", "buildings", "cities", "units",
|
||||
"territory", "population", "budgets", "research", "culture", "government",
|
||||
"researched", "training"]:
|
||||
assert_eq(first[key], second[key], "key %s is shared" % key)
|
||||
assert_ne(first["visible"], second["visible"], "visibility stays per viewer")
|
||||
|
||||
|
||||
## The explored-set serialisation is cached until new ground is revealed, so the
|
||||
## cache must be invalidated by a reveal that discovers a tile.
|
||||
func test_explored_serialisation_cache_invalidates_on_discovery() -> void:
|
||||
var state := _state()
|
||||
state.explored[0].clear()
|
||||
assert_empty(state.snapshot(0)["explored"], "cache reflects the emptied set")
|
||||
state._reveal(0, state.land_cells[0], 1)
|
||||
assert_true(state.snapshot(0)["explored"].size() > 0, "new ground is serialised")
|
||||
|
||||
|
||||
func test_territory_index_matches_territory() -> void:
|
||||
var state := _state()
|
||||
var total := 0
|
||||
for civ in state._territory_by_civ.keys():
|
||||
for coords in state._territory_by_civ[civ]:
|
||||
assert_eq(state.civ_at(coords), int(civ), "tile %s" % coords)
|
||||
total += 1
|
||||
assert_eq(total, state.territory.size())
|
||||
|
||||
|
||||
func test_find_unit_and_city_use_the_id_index() -> void:
|
||||
var state := _state()
|
||||
for unit in state.units:
|
||||
assert_eq(state.find_unit(int(unit["id"])), unit)
|
||||
assert_true(state.find_unit(999).is_empty())
|
||||
for city in state.cities:
|
||||
assert_eq(state.find_city(int(city["id"])), city)
|
||||
assert_true(state.find_city(999).is_empty())
|
||||
|
||||
|
||||
## Friendly territory must be at least as fast as foreign/unclaimed land.
|
||||
func test_units_move_faster_in_their_own_territory() -> void:
|
||||
var state := _state()
|
||||
|
||||
Reference in New Issue
Block a user