#105 (#97g): garrison vessels patrol and defend their outpost
A garrisoned vessel circles its post, engages what comes into range, and goes back to circling. It steers by writing move_target and lets Ship's own steering fly it, so it accelerates, banks and brakes exactly like a vessel in the formation. It is not in Fleet.ships. It does not follow the formation, does not count against max_fleet_size(), and does not move when the player flies away - which is the whole point of stationing it. A garrison that ate a fleet slot would mean stationing a vessel cost the player a place in their own wedge. The leash is the load-bearing part. Enemies are judged from the POST rather than from the vessel, so a garrison that has drifted out to its leash cannot see further still and be walked away from its outpost one enemy at a time, and an engagement is clamped to a circle around the post rather than to a straight line, so it still manoeuvres while pinned. The orbit starts from where the vessel actually is rather than a random bearing. A random one makes the first move_target a point on the far side of the circle, and the vessel flies straight across the middle - through the outpost it is guarding. EnemyTargets.nearest_ship now searches the player_ships GROUP instead of Fleet.ships: an enemy that ignored the guns shooting at it, because they were not in the player's formation, would let a garrison hold a position without ever being fought back. The laser vessel is deliberately not garrisonable. Firing the laser lives in LaserController, a fleet ability driven by the laser button, rather than in VesselLaserComponent - so a garrisoned laser would hold its position and never shoot, and offering a vessel that cannot fight is worse than a short list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8122e280b7
commit
bd0cc842ff
5 changed files with 444 additions and 6 deletions
|
|
@ -19,16 +19,17 @@ extends RefCounted
|
|||
const SHIP_PRIORITY_RANGE: float = 520.0
|
||||
|
||||
|
||||
# The GROUP, not Fleet.ships. #105 puts vessels at outposts that are not in
|
||||
# the fleet at all, and an enemy that ignored the guns actually shooting at
|
||||
# it - because they were not in the player's formation - would let a
|
||||
# garrison hold a position without ever being fought back.
|
||||
static func nearest_ship(node: Node, from: Vector2) -> Node2D:
|
||||
var fleet: Node2D = node.get_tree().get_first_node_in_group("player_fleet")
|
||||
if fleet == null:
|
||||
return null
|
||||
var nearest: Node2D = null
|
||||
var best: float = INF
|
||||
for ship in fleet.ships:
|
||||
if not is_instance_valid(ship):
|
||||
for ship in node.get_tree().get_nodes_in_group("player_ships"):
|
||||
if not is_instance_valid(ship) or ship.is_queued_for_deletion():
|
||||
continue
|
||||
var d: float = from.distance_to(ship.global_position)
|
||||
var d: float = from.distance_to((ship as Node2D).global_position)
|
||||
if d < best:
|
||||
best = d
|
||||
nearest = ship
|
||||
|
|
|
|||
217
tests/test_outpost_garrison_patrol.gd
Normal file
217
tests/test_outpost_garrison_patrol.gd
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
extends SceneTree
|
||||
|
||||
# Headless gameplay test: run with
|
||||
# godot --headless -s res://tests/test_outpost_garrison_patrol.gd
|
||||
# Forgejo #105 (#97g): "When a vessel is assigned to an outpost, it
|
||||
# navigates around the outpost by itself, attacking nearby ennemies if any
|
||||
# are detected."
|
||||
#
|
||||
# Covers:
|
||||
# A. a crewed outpost puts a real vessel in the world, and that vessel is
|
||||
# NOT in the fleet - it must not follow the formation, must not eat a
|
||||
# fleet slot, and must not fly off when the player leaves
|
||||
# B. left alone it circles its post rather than sitting on it
|
||||
# C. an enemy inside its detection range is engaged and shot at
|
||||
# D. it will not be walked away: an enemy beyond the leash is ignored, and
|
||||
# an engagement never drags it further than the leash from its post
|
||||
# E. it goes back to patrolling once the enemy is gone
|
||||
# F. losing a garrison vessel frees the berth, so it gets crewed again
|
||||
# G. enemies fight back - a garrison vessel is a target like any other
|
||||
|
||||
var game_state: Node
|
||||
var main: Node
|
||||
var fleet: Node
|
||||
var field: Node
|
||||
var outpost_id: int = 0
|
||||
var guard: Node2D
|
||||
var component: Node
|
||||
var enemy: Node2D
|
||||
var stage: int = 0
|
||||
var elapsed: float = 0.0
|
||||
var orbit_min: float = INF
|
||||
var orbit_max: float = 0.0
|
||||
var max_from_post: float = 0.0
|
||||
var dummy_hp_before: int = 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
# GameState is an autoload: a bare GameState.<instance member> does not
|
||||
# compile in a SceneTree script, so the node is fetched by name instead.
|
||||
game_state = root.get_node("GameState")
|
||||
game_state.buildings.clear()
|
||||
game_state.extractors.clear()
|
||||
game_state.transporters.clear()
|
||||
game_state.stored_vessels.clear()
|
||||
game_state.vessel_deliveries.clear()
|
||||
game_state.reset_progression()
|
||||
game_state.reset_waves()
|
||||
game_state.unlocked.append("forward_bases")
|
||||
main = load("res://world/Main.tscn").instantiate()
|
||||
root.add_child(main)
|
||||
current_scene = main
|
||||
fleet = main.get_node("Fleet")
|
||||
field = main.get_node("GarrisonField")
|
||||
|
||||
|
||||
func _fail(message: String) -> bool:
|
||||
print("TEST_OUTPOST_GARRISON_PATROL: FAIL (%s)" % message)
|
||||
quit(1)
|
||||
return true
|
||||
|
||||
|
||||
# A pinned target that neither shoots back nor dies. The point of these
|
||||
# stages is where the GARRISON goes and whether it fires, and a sniper that
|
||||
# killed the guard mid-check, or a chaser that died to it, would end the
|
||||
# observation before the thing being measured had happened.
|
||||
func _dummy(at: Vector2) -> Node2D:
|
||||
var dummy: Node2D = load("res://enemies/Enemy.tscn").instantiate()
|
||||
dummy.enemy_id = "chaser"
|
||||
dummy.position = at
|
||||
main.add_child(dummy)
|
||||
dummy.speed = 0.0
|
||||
dummy.hp = 100000
|
||||
return dummy
|
||||
|
||||
|
||||
func _process(delta: float) -> bool:
|
||||
elapsed += delta
|
||||
match stage:
|
||||
0:
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
main.get_node("EnemySpawner").set_process(false)
|
||||
main.get_node("AsteroidField").set_process(false)
|
||||
for hazard in get_nodes_in_group("asteroids") + get_nodes_in_group("enemies"):
|
||||
if is_instance_valid(hazard):
|
||||
hazard.queue_free()
|
||||
# The fleet is parked a long way off, so nothing here is the
|
||||
# fleet's doing.
|
||||
fleet.spawn_ships(1, Vector2(-9000.0, 0.0),
|
||||
PackedStringArray([GameState.VESSEL_INTERCEPTOR]))
|
||||
fleet.ships[0].move_target = fleet.ships[0].position
|
||||
|
||||
main.get_node("BuildingField").place(GameState.BUILDING_OUTPOST, Vector2.ZERO)
|
||||
outpost_id = int(game_state.buildings[-1].id)
|
||||
game_state.set_garrison_desired(outpost_id, GameState.VESSEL_INTERCEPTOR, 1)
|
||||
game_state.garrison_crew(outpost_id).append(GameState.VESSEL_INTERCEPTOR)
|
||||
elapsed = 0.0
|
||||
stage = 1
|
||||
return false
|
||||
1:
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
# --- A. a real vessel, and not a fleet one -------------------
|
||||
var here: Array = field.ships_at(outpost_id)
|
||||
if here.size() != 1:
|
||||
return _fail("a crewed outpost put %d vessels in the world" % here.size())
|
||||
guard = here[0]
|
||||
component = guard.get_component("garrison")
|
||||
if component == null:
|
||||
return _fail("the garrison vessel has no garrison component steering it")
|
||||
if guard in fleet.ships:
|
||||
return _fail("the garrison vessel joined the fleet")
|
||||
if fleet.ships.size() != 1:
|
||||
return _fail("stationing a vessel changed the fleet to %d ships" % fleet.ships.size())
|
||||
print("outpost crewed with 1 vessel, fleet still %d and untouched" % fleet.ships.size())
|
||||
elapsed = 0.0
|
||||
stage = 2
|
||||
return false
|
||||
2:
|
||||
# --- B. it circles the post ---------------------------------
|
||||
var from_post: float = guard.global_position.length()
|
||||
orbit_min = minf(orbit_min, from_post)
|
||||
orbit_max = maxf(orbit_max, from_post)
|
||||
if elapsed < 4.0:
|
||||
return false
|
||||
if component.is_engaging():
|
||||
return _fail("the garrison thinks it is fighting with no enemy on the map")
|
||||
if orbit_min < 40.0:
|
||||
return _fail("the garrison sat on top of its outpost (closest %.0fpx)" % orbit_min)
|
||||
if orbit_max > component.leash_range:
|
||||
return _fail("patrolling took it %.0fpx from its post, past the %.0fpx leash" \
|
||||
% [orbit_max, component.leash_range])
|
||||
print("patrolled between %.0f and %.0fpx of its post" % [orbit_min, orbit_max])
|
||||
|
||||
# --- D (first half). an enemy beyond the leash is ignored ----
|
||||
enemy = _dummy(Vector2(component.leash_range + component.engage_distance + 900.0, 0.0))
|
||||
elapsed = 0.0
|
||||
stage = 3
|
||||
return false
|
||||
3:
|
||||
if elapsed < 1.0:
|
||||
return false
|
||||
if component.is_engaging():
|
||||
return _fail("the garrison went after an enemy well beyond its leash")
|
||||
print("an enemy %.0fpx out is left alone" % enemy.global_position.length())
|
||||
enemy.queue_free()
|
||||
|
||||
# --- C. an enemy in reach is engaged and shot at -------------
|
||||
enemy = _dummy(Vector2(300.0, 0.0))
|
||||
dummy_hp_before = enemy.hp
|
||||
max_from_post = 0.0
|
||||
elapsed = 0.0
|
||||
stage = 4
|
||||
return false
|
||||
4:
|
||||
if not is_instance_valid(guard):
|
||||
return _fail("the garrison vessel was destroyed mid-engagement")
|
||||
max_from_post = maxf(max_from_post, guard.global_position.length())
|
||||
if elapsed < 2.5:
|
||||
return false
|
||||
if not component.is_engaging():
|
||||
return _fail("an enemy 300px from the outpost was not engaged")
|
||||
# Damage taken, not projectiles in flight: a bolt that has already
|
||||
# connected has freed itself, so counting live ones at an instant
|
||||
# reads zero exactly when the gun is working best.
|
||||
if enemy.hp >= dummy_hp_before:
|
||||
return _fail("the garrison engaged but the enemy took no damage")
|
||||
# --- D (second half). the leash holds during a fight ---------
|
||||
if max_from_post > component.leash_range + 60.0:
|
||||
return _fail("the fight pulled it %.0fpx from its post, past a %.0fpx leash" \
|
||||
% [max_from_post, component.leash_range])
|
||||
print("engaged, took %d hp off it, and stayed within %.0fpx of its post" \
|
||||
% [dummy_hp_before - enemy.hp, max_from_post])
|
||||
|
||||
# --- G. and it can be fought back ---------------------------
|
||||
var targets: Script = load("res://enemies/EnemyTargets.gd")
|
||||
var picked: Node2D = targets.nearest_ship(main, enemy.global_position)
|
||||
if picked != guard:
|
||||
return _fail("an enemy beside the garrison picks %s instead of it - it cannot be fought back" \
|
||||
% ("nothing" if picked == null else str(picked.name)))
|
||||
print("enemies see the garrison as a target too")
|
||||
|
||||
if is_instance_valid(enemy):
|
||||
enemy.queue_free()
|
||||
elapsed = 0.0
|
||||
stage = 5
|
||||
return false
|
||||
5:
|
||||
# --- E. back to the patrol ----------------------------------
|
||||
if elapsed < 1.5:
|
||||
return false
|
||||
if component.is_engaging():
|
||||
return _fail("the garrison is still fighting an enemy that is gone")
|
||||
print("stood down and went back to its patrol")
|
||||
|
||||
# --- F. a lost vessel frees its berth ------------------------
|
||||
if game_state.garrison_crew(outpost_id).size() != 1:
|
||||
return _fail("the crew is not 1 before the kill")
|
||||
guard.take_damage(1)
|
||||
elapsed = 0.0
|
||||
stage = 6
|
||||
return false
|
||||
6:
|
||||
if elapsed < 0.3:
|
||||
return false
|
||||
if not game_state.garrison_crew(outpost_id).is_empty():
|
||||
return _fail("a destroyed garrison vessel still holds its berth")
|
||||
if game_state.garrison_desired_of(outpost_id, GameState.VESSEL_INTERCEPTOR) != 1:
|
||||
return _fail("losing the vessel cancelled the order as well")
|
||||
if field.ships_at(outpost_id).size() != 0:
|
||||
return _fail("the dead vessel is still stationed")
|
||||
print("losing the vessel frees the berth and leaves the order standing")
|
||||
|
||||
print("TEST_OUTPOST_GARRISON_PATROL: PASS")
|
||||
quit(0)
|
||||
return true
|
||||
return false
|
||||
96
vessels/GarrisonField.gd
Normal file
96
vessels/GarrisonField.gd
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
extends Node2D
|
||||
|
||||
# #105 (#97g): keeps the vessels stationed at outposts in step with what
|
||||
# GameState says is stationed there.
|
||||
#
|
||||
# Garrison vessels are NOT in Fleet.ships. They are spawned here, under a
|
||||
# node of their own, so the formation, the fleet's speed fader and
|
||||
# max_fleet_size() all continue to be about the fleet - a garrison that
|
||||
# counted against the fleet's slots would mean stationing a vessel cost the
|
||||
# player a place in their own wedge, which is the opposite of what an outpost
|
||||
# is for.
|
||||
#
|
||||
# The record is the truth, as everywhere else: this node is rebuilt from
|
||||
# GameState.garrison_crew() when the space scene loads, so a garrison
|
||||
# survives a trip to a planet surface.
|
||||
|
||||
var ship_scene: PackedScene = preload("res://vessels/Ship.tscn")
|
||||
const GARRISON_COMPONENT = preload("res://vessels/VesselGarrisonComponent.gd")
|
||||
|
||||
# {outpost_id: [ship, ...]}
|
||||
var stationed: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("garrison_field")
|
||||
sync()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
sync()
|
||||
|
||||
|
||||
func ships_at(outpost_id: int) -> Array:
|
||||
var out: Array = []
|
||||
for ship in stationed.get(outpost_id, []):
|
||||
if is_instance_valid(ship) and not ship.is_queued_for_deletion():
|
||||
out.append(ship)
|
||||
return out
|
||||
|
||||
|
||||
func all_ships() -> Array:
|
||||
var out: Array = []
|
||||
for outpost_id in stationed:
|
||||
out.append_array(ships_at(outpost_id))
|
||||
return out
|
||||
|
||||
|
||||
func sync() -> void:
|
||||
# Retire whole outposts that no longer exist, then reconcile the rest.
|
||||
for outpost_id in stationed.keys():
|
||||
if GameState.find_building(outpost_id).is_empty():
|
||||
for ship in ships_at(outpost_id):
|
||||
ship.queue_free()
|
||||
stationed.erase(outpost_id)
|
||||
|
||||
for outpost_id in GameState.outpost_ids():
|
||||
var record: Dictionary = GameState.find_building(outpost_id)
|
||||
var crew: Array = GameState.garrison_crew(outpost_id)
|
||||
var here: Array = ships_at(outpost_id)
|
||||
stationed[outpost_id] = here
|
||||
if here.size() == crew.size():
|
||||
continue
|
||||
if here.size() > crew.size():
|
||||
# The record shrank under us - a vessel was released or lost
|
||||
# while this node was not looking.
|
||||
for i in range(crew.size(), here.size()):
|
||||
here[i].queue_free()
|
||||
stationed[outpost_id] = here.slice(0, crew.size())
|
||||
continue
|
||||
for i in range(here.size(), crew.size()):
|
||||
here.append(_spawn(outpost_id, String(crew[i]), record.position, i))
|
||||
|
||||
|
||||
func _spawn(outpost_id: int, vessel_type: String, at: Vector2, index: int) -> Node2D:
|
||||
var ship: Area2D = ship_scene.instantiate()
|
||||
# Before add_child, so Ship._ready() builds the right vessel - the same
|
||||
# order Fleet.add_ship() relies on.
|
||||
ship.vessel_type = vessel_type
|
||||
var spread: float = TAU * float(index) / 4.0
|
||||
ship.position = at + Vector2(cos(spread), sin(spread)) * 170.0
|
||||
add_child(ship)
|
||||
var component: Node = GARRISON_COMPONENT.new()
|
||||
component.outpost_id = outpost_id
|
||||
component.station = at
|
||||
ship.add_child(component)
|
||||
component.setup()
|
||||
# A garrison vessel that dies frees its berth, so #104 sends another.
|
||||
ship.destroyed.connect(_on_garrison_lost.bind(outpost_id, vessel_type))
|
||||
return ship
|
||||
|
||||
|
||||
func _on_garrison_lost(outpost_id: int, vessel_type: String) -> void:
|
||||
var crew: Array = GameState.garrison_crew(outpost_id)
|
||||
var index: int = crew.find(vessel_type)
|
||||
if index >= 0:
|
||||
crew.remove_at(index)
|
||||
120
vessels/VesselGarrisonComponent.gd
Normal file
120
vessels/VesselGarrisonComponent.gd
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
class_name VesselGarrisonComponent
|
||||
extends Node
|
||||
|
||||
# #105 (#97g): "When a vessel is assigned to an outpost, it navigates around
|
||||
# the outpost by itself, attacking nearby ennemies if any are detected."
|
||||
#
|
||||
# Attached to a Ship at spawn by GarrisonField rather than declared on the
|
||||
# vessel definition: garrisoning is a ROLE a hull is put into, not a property
|
||||
# of the hull. The same interceptor is a fleet vessel one minute and a
|
||||
# garrison the next, and nothing about it changes but who is steering.
|
||||
#
|
||||
# It steers by writing move_target and lets Ship's own steering do the
|
||||
# flying, so a garrison vessel accelerates, banks and brakes exactly like one
|
||||
# in the formation. It is not in Fleet.ships, so it does not follow the
|
||||
# formation, does not count against max_fleet_size(), and does not move when
|
||||
# the player flies away - which is the whole point of stationing it.
|
||||
|
||||
var capability_id: String = "garrison"
|
||||
|
||||
# Which outpost this belongs to, and where that outpost is. The position is
|
||||
# copied rather than looked up every tick: the record is the truth, and if
|
||||
# the outpost is levelled the vessel should hold the ground it was left on
|
||||
# rather than snap to the origin.
|
||||
var outpost_id: int = 0
|
||||
var station: Vector2 = Vector2.ZERO
|
||||
|
||||
@export var orbit_radius: float = 170.0
|
||||
@export var orbit_speed: float = 0.85
|
||||
@export var detection_range: float = 950.0
|
||||
# How far from its post it will go. Without a leash a garrison would be
|
||||
# drawn off by anything that flew past, which is exactly what stationing it
|
||||
# was meant to stop - and the outpost would be left open behind it.
|
||||
@export var leash_range: float = 620.0
|
||||
# Held off the target rather than ramming it: these are gunships, and a
|
||||
# vessel that closed to zero would trade itself for a chaser every time.
|
||||
@export var engage_distance: float = 300.0
|
||||
|
||||
var _angle: float = 0.0
|
||||
var _engaging: bool = false
|
||||
|
||||
|
||||
func setup() -> void:
|
||||
# Start the orbit from where the vessel actually IS, not from a random
|
||||
# bearing. A random one makes the first move_target a point on the far
|
||||
# side of the circle, and the vessel flies straight across the middle to
|
||||
# reach it - through the outpost it is supposed to be guarding.
|
||||
var ship: Node2D = get_parent()
|
||||
var offset: Vector2 = ship.global_position - station
|
||||
_angle = offset.angle() if offset.length() > 1.0 else randf() * TAU
|
||||
|
||||
|
||||
func is_engaging() -> bool:
|
||||
return _engaging
|
||||
|
||||
|
||||
func tick(delta: float) -> void:
|
||||
var ship: Node2D = get_parent()
|
||||
var target: Node2D = _nearest_enemy(ship)
|
||||
_engaging = target != null
|
||||
if target == null:
|
||||
_patrol(ship, delta)
|
||||
_hold_fire(ship)
|
||||
return
|
||||
_engage(ship, target, delta)
|
||||
|
||||
|
||||
func _patrol(ship: Node2D, delta: float) -> void:
|
||||
_angle += orbit_speed * delta
|
||||
ship.move_target = station + Vector2(cos(_angle), sin(_angle)) * orbit_radius
|
||||
|
||||
|
||||
func _engage(ship: Node2D, target: Node2D, delta: float) -> void:
|
||||
# Keep station off the target, but never further from the post than the
|
||||
# leash allows - the approach is clamped to a circle around the outpost
|
||||
# rather than to a straight line, so it still manoeuvres while pinned.
|
||||
var to_target: Vector2 = target.global_position - ship.global_position
|
||||
var distance: float = to_target.length()
|
||||
var wanted: Vector2 = target.global_position
|
||||
if distance > 0.001:
|
||||
wanted = target.global_position - to_target.normalized() * engage_distance
|
||||
var from_station: Vector2 = wanted - station
|
||||
if from_station.length() > leash_range:
|
||||
wanted = station + from_station.normalized() * leash_range
|
||||
ship.move_target = wanted
|
||||
# The orbit angle keeps advancing while engaged, so breaking off does not
|
||||
# snap the vessel back to whatever bearing it left on.
|
||||
_angle += orbit_speed * delta
|
||||
|
||||
var weapon: Node = ship.get_component("weapon") if ship.has_method("get_component") else null
|
||||
if weapon == null:
|
||||
return
|
||||
if distance <= weapon.fire_range:
|
||||
weapon.set_fire_direction(to_target)
|
||||
else:
|
||||
weapon.set_fire_direction(Vector2.ZERO)
|
||||
|
||||
|
||||
func _hold_fire(ship: Node2D) -> void:
|
||||
var weapon: Node = ship.get_component("weapon") if ship.has_method("get_component") else null
|
||||
if weapon != null:
|
||||
weapon.set_fire_direction(Vector2.ZERO)
|
||||
|
||||
|
||||
func _nearest_enemy(ship: Node2D) -> Node2D:
|
||||
var nearest: Node2D = null
|
||||
var best: float = detection_range
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
if not is_instance_valid(enemy) or enemy.is_queued_for_deletion():
|
||||
continue
|
||||
var at: Vector2 = (enemy as Node2D).global_position
|
||||
# Judged from the POST, not from the vessel: otherwise a garrison
|
||||
# that had drifted out to its leash would see further still, and
|
||||
# could be walked away from its outpost one enemy at a time.
|
||||
if station.distance_to(at) > leash_range + engage_distance:
|
||||
continue
|
||||
var d: float = ship.global_position.distance_to(at)
|
||||
if d < best:
|
||||
best = d
|
||||
nearest = enemy
|
||||
return nearest
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
[ext_resource type="Script" path="res://ui/SkillTreeView.gd" id="21"]
|
||||
[ext_resource type="Script" path="res://ui/WaveAlert.gd" id="32"]
|
||||
[ext_resource type="Script" path="res://ui/GarrisonPanel.gd" id="33"]
|
||||
[ext_resource type="Script" path="res://vessels/GarrisonField.gd" id="34"]
|
||||
[ext_resource type="Script" path="res://logistics/TransporterField.gd" id="23"]
|
||||
[ext_resource type="Script" path="res://ui/ResourceBar.gd" id="24"]
|
||||
[ext_resource type="Script" path="res://logistics/VesselDeliveryField.gd" id="26"]
|
||||
|
|
@ -90,6 +91,9 @@ script = ExtResource("11")
|
|||
[node name="BuildingField" type="Node2D" parent="."]
|
||||
script = ExtResource("14")
|
||||
|
||||
[node name="GarrisonField" type="Node2D" parent="."]
|
||||
script = ExtResource("34")
|
||||
|
||||
[node name="TransporterField" type="Node2D" parent="."]
|
||||
script = ExtResource("23")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue