Enemy mothership #53
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 large, slow, editor-placeable capital enemy that periodically launches smaller enemies and fires a heavily telegraphed beam capable of killing several player vessels at once.
Depends on #52 (turret vessel spawner) - it reuses that ticket's
EnemySpawnerComponentrather than reimplementing spawning.Scope
EnemySpawnerComponentinstances on the definition rather than adding multi-type support to the component - one withspawned_enemy_id = "chaser"(short interval, e.g. 5s), one withspawned_enemy_id = "artillery"(long interval, e.g. 18s,max_alive = 2). Two instances of a single-purpose component is the composition the codebase already uses everywhere; aspawned_enemy_idsarray would be the type-switch-shaped answer.artilleryenemy (slow telegraphed AOE), rather than inventing a fourth enemy type. Revisit if a distinct bomber is wanted.EnemyBigLaserComponent.tscn+.gd,capability_id = "big_laser":@export var charge_time: float = 2.5,@export var fire_cooldown: float = 9.0,@export var beam_range: float = 1200.0,@export var beam_half_width: float = 35.0,@export var damage: int = 3Fleet.get_centroid()) and hold it - it must not track the fleet while charging, or it cannot be dodged. Draw a widening beam-line telegraph along that direction for the wholecharge_time, in the mothership's own colour, using the ring/arc idiom fromArtilleryShell._draw_ringadapted to a line.player_shipswhose perpendicular distance to the segment is<= beam_half_width(and whose projection falls within the segment) takesShip.take_damage(damage)- so a line of vessels dies together. Shield charges absorb per-ship, as everywhere else.MothershipEnemyDefinition.tres:enemy_id = "enemy_mothership",max_hp = 60,speed = 35.0(relatively slow),accelerationlow, a large hull roughly 90-100 units across, distinct glow colour. Components: one pursuit (so it closes slowly), two spawners, one big laser.EnemyDefinitions.ORDERandDEFINITIONS.Acceptance criteria
charge_timebefore any damage lands, and the aim does not re-track the fleet once charging starts.beam_half_widthbefore the shot lands survives.Tests
tests/test_enemy_mothership.gd(bash tests/run_tests.sh mothership), plus a 720x1280 screenshot of the charge telegraph mid-countdown. Note trap 6 in the repo notes: cache any scalar you need from a node that frees itself.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
472de1d, pushed to main.Spawning reuses #52's
EnemySpawnerComponentas-is via two preset scenes (EnemySpawnerComponentChaser.tscn: 5s interval/chaser,EnemySpawnerComponentArtillery.tscn: 18s/max 2/artillery) rather than adding multi-type support to the component - two instances of a single-purpose component, matching the composition this codebase already uses everywhere. "Enemy bomber" in the original request is read here as the existingartilleryenemy's slow telegraphed AOE rather than a new enemy type, per the judgment call flagged in the ticket.New
EnemyBigLaserComponent.gd(big_laser) fires a standaloneEnemyBigLaserBeamon a long cooldown - the beam owns its whole charge/fire/flash sequence once launched, so the mothership dying mid-charge doesn't cancel an already-telegraphed shot, same design asEnemyArtilleryComponent/ArtilleryShell. The aim direction is locked once inlaunch()and never re-read - nothing in the charging phase touches the fleet's position again, which is what makes it dodgeable. Damage is a line-segment check (projection + perpendicular distance) against everyplayer_shipsmember;Ship.take_damage()already handles shield absorption per-ship, so no new shield logic was needed.MothershipEnemyDefinition.tres:max_hp60,speed35 (slow), a ~96-unit hull, pursuit + two spawners + big_laser components, no type switch anywhere - every stat from the definition, every behaviour from a component.Tests: new
tests/test_enemy_mothership.gd, split into beam mechanics (tested directly, not through the component's own cooldown timing, which would make the geometry assertions timing-fragile) and the composed definition (tested through a realEnemy.tscninstance). Covers: telegraph holds with no damage for most of the charge; the locked aim direction survives the fleet moving mid-charge; three ships in a line all die from one shot while one that dodges out survives; a shielded ship survives losing exactly one charge; both chaser and artillery spawn on their own intervals; it closes on the fleet without ever exceeding its own defined speed. Screenshot of the charge telegraph mid-countdown (shots/mothership_beam_charge.png). Full suite: 80/80.Found and fixed a real bug along the way, in
EnemySpawnerComponent.gd(#52): itsenemy_scene: PackedScene = preload("res://Enemy.tscn")member-level preload could get permanently baked in as a broken, emptyPackedScenewhen a definition'scomponent_sceneschain led back toEnemy.tscnwhile it was still mid-load - confirmed empirically viaResourceLoader.has_cached()being true (a good copy existed in the cache) while the preloaded reference's ownresource_pathwas empty. This only surfaces when a SINGLE definition composes two instances of a script carrying such a preload (the mothership's chaser/artillery spawner presets; #52's single-spawner turret never hit it, which is why it shipped unnoticed). Fixed by resolvingenemy_scenelazily viaload()on first use instead of at compile time. Also made a fresh spawner count down from a full interval before its first spawn rather than firing on tick one - reads more naturally, and incidentally keeps two sibling spawners with different intervals from ever landing their first attempt on the exact same frame.