Turret vessel spawner enemy #52
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A stationary, high-health enemy placeable from the map editor that continuously spawns basic vessels which chase the fleet and destroy it on contact.
Enemy behaviour is composed from capability components (see #44) and the editor palette is generated from
EnemyDefinitions.all()(see #48), so this needs no editor or palette change - registering the definition is enough.Scope
EnemySpawnerComponent.tscn+.gd,capability_id = "spawner":@export var spawn_interval: float = 4.0@export var spawned_enemy_id: String = "chaser"@export var max_alive: int = 6@export var spawn_radius: float = 90.0tick(delta)past the interval, instantiateres://Enemy.tscnwithenemy_id = spawned_enemy_idat a random point onspawn_radiusaround the parent enemy, added to the spawner's own parent (so a surface-hosted spawner keeps its spawns in the same host - see #44's note on why../Fleet-style assumptions break).Arrayfiltered withis_instance_valid()each tick, so destroyed spawns free their slot andmax_aliveis respected.get_tree().get_first_node_in_group("player_fleet")null or itsshipsempty) - otherwise a preview or an unattended scene accumulates enemies forever.TurretSpawnerEnemyDefinition.tres:enemy_id = "turret_spawner",max_hp = 20(the "decent amount of life" - roughly 7x a chaser),speed = 0.0(stationary; no pursuit or standoff component), a bulky distinct hull, and its own glow colour distinct from chaser red, sniper blue, artillery orange and gas-bomb pink.EnemyDefinitions.ORDERandDEFINITIONS.Out of scope
The enemy mothership (separate ticket) - though it reuses this component, so keep
EnemySpawnerComponentfree of anything turret-specific.Acceptance criteria
spawn_interval, stops atmax_alive, resumes when a spawn dies.Tests
tests/test_turret_spawner_enemy.gd(bash tests/run_tests.sh turret), plus a 720x1280 screenshot beside a chaser showing the two are visually distinguishable. Freeze the scene for the screenshot the waytests/_sniper_vs_chaser_preview.gddoes.Repo notes (read before starting)
/storage/emulated/0/GodotProjects/SpaceshipTest.touchon it. Android FUSE does not bump mtime under~/storage/shared, and Godot's import cache will silently keep serving the old version, making a correct fix look like it did nothing.bash tests/run_tests.sh <substring>; full suite with no argument. Tests areSceneTreescripts printingTEST_NAME: PASS/TEST_NAME: FAIL (reason)and callingquit(0|1)- copy the shape oftests/test_capture_points.gd.bash tests/screenshot.sh <res://Scene.tscn> <out.png> <frames> 720x1280. Use few frames (~15); the software rasterizer is slow.--headlessrenders nothing at all, so it cannot validate visuals.NeonDraw/NeonPiece; do not introduce a different visual idiom.*Definitionresources plus capability component scenes. Do not addif type == "..."branches.FileAccess, nouser://).GameStateis an in-memory autoload. Do not invent a save system.Traps that have already cost hours on this codebase
const X = preload(...)in a test file that is itself the-sentry point. It evaluates during that file's own early compilation, which can run before autoloads are registered; any script it pulls in that referencesGameStateinside a function body then fails to compile withIdentifier not found: GameState. Usevar X: Scriptplusload()inside_initialize()instead. See the header comment intests/test_capture_points.gd.[]to a typedArray[Resource]export from another script fails at runtime - the compiler only sees the target asResourceand cannot confirm the element type. Leave the declared default alone, or build the typed array inside the owning script.Fleet.spawn_ships(0, ...)does not empty the fleet - it falls back to one default interceptor by design. Clearfleet.shipsexplicitly if a test or preview needs genuinely zero ships.ship.positionwithout also settingship.move_targetletsShip._physics_processsteer it straight back toward the stale target within a few frames.SceneTreetest's own_process()clock under device load, even with--fixed-fps 60. Gate waits on the system's own state (a component's timer or flag), not onelapsed >= duration.queue_free()s itself (projectiles, shells) must not be re-read inside a per-frame wait condition - cache the scalar you need before the wait._screenshot_tool.gd's_initialize(), so their_ready()races the game scene's own_ready(). Do setup on the first_process()tick, andget_tree().paused = trueonce everything is placed if the scene must hold still.Done in commit
1bd25c6, pushed to main.New
EnemySpawnerComponent.gd(capability_id"spawner"): continuously instantiates another enemy type beside its host at a fixed interval, capped atmax_aliveconcurrently, refilling as spawns die. Spawned enemies are parented under the spawner's OWN PARENT rather than the spawner itself, so they're siblings, not descendants - killing the spawner can't free them, and a surface-hosted spawner (underPlanetSetupHost) keeps its spawns under the same host instead of leaking them to a hardcoded path, the same hazard #44 already closed for the pursuit/standoff/ranged-weapon components.TurretSpawnerEnemyDefinition.tres: stationary (speed 0, no pursuit or standoff component),max_hp20 (~7x a chaser), a bulky octagon hull, and its own acid-green glow distinct from chaser/sniper/artillery. Registered inEnemyDefinitions.ORDER/DEFINITIONS- the map editor palette (#48) picks it up with no editor code change.Tests: new
tests/test_turret_spawner_enemy.gdcovers registration and the real definition defaults, no spawns without a fleet, spawning capped atmax_alive, a dead spawn's slot refilling, never moving, surviving 19 damage and dying on the 20th, and its spawns surviving its death with their pursuit component intact. Screenshot beside a chaser (shots/turret_vs_chaser.png) confirms the two read as visually distinct enemies - small red diamond vs. bulky green octagon. Full suite: 79/79.Three things worth keeping in mind for later tickets:
set_process(false)call made before it has fully finished entering the tree doesn't reliably stick (already documented intests/test_planet_atmosphere_transition.gd, and it bit this test too):BuildController._check_fleet_wipe()kept rebuilding a ship during an intended "empty fleet" test window even after disabling it in_initialize(). Do it in_process()'s first real tick instead, once_ready()cascades are confirmed complete.@exportinterval (likespawn_interval) on an already-running component does NOT reset its own in-progress countdown timer - only changes what it resets TO on its next expiry. If the countdown already started at the OLD value before your override lands, reset the timer variable itself too, or the first real interval after the override silently uses the stale value.