spaceship/MothershipEnemyDefinition.tres

21 lines
1.1 KiB
Text
Raw Normal View History

Enemy mothership (#53) A large, slow capital enemy that periodically launches chasers and artillery on their own intervals and fires a heavily telegraphed line beam capable of killing several player vessels at once. Spawning reuses #52's EnemySpawnerComponent as-is via two preset scenes (EnemySpawnerComponentChaser.tscn: 5s/chaser, EnemySpawnerComponentArtillery.tscn: 18s/max 2/artillery) rather than adding multi-type support to the component - two instances of a single-purpose component is the composition this codebase already uses everywhere ("enemy bomber" in the original request is read here as the existing artillery enemy's slow telegraphed AOE, not a new enemy type - flagged as a judgment call in the ticket). New EnemyBigLaserComponent.gd ("big_laser") fires a standalone EnemyBigLaserBeam on 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 as EnemyArtilleryComponent/ArtilleryShell. The aim direction is locked once in launch() 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 every player_ships member; Ship.take_damage() already handles shield absorption per-ship. MothershipEnemyDefinition.tres: max_hp 60, speed 35 (slow), a ~96-unit hull, pursuit + two spawners + big_laser, 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 real Enemy.tscn instance). Screenshot of the charge telegraph mid-countdown. Full suite: 80/80. Found and fixed a real bug along the way, in EnemySpawnerComponent.gd (#52): its `enemy_scene: PackedScene = preload("res://Enemy.tscn")` member-level preload could get permanently baked in as a broken, empty PackedScene when a definition's component_scenes chain led back to Enemy.tscn while it was still mid-load - confirmed empirically via ResourceLoader.has_cached() being true (a good copy existed) while the preloaded reference's own resource_path was 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). Fixed by resolving enemy_scene lazily via load() on first use instead of at compile time, and additionally made a fresh spawner count down from a full interval before its first spawn rather than firing on tick one, which also happens to keep two sibling spawners with different intervals from ever landing their first attempt on the same frame. 🤖 Generated with Claude Code Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 10:19:02 +02:00
[gd_resource type="Resource" script_class="EnemyDefinition" load_steps=6 format=3]
[ext_resource type="Script" path="res://EnemyDefinition.gd" id="1"]
[ext_resource type="PackedScene" path="res://EnemyPursuitComponentMothership.tscn" id="2"]
Enemy mothership (#53) A large, slow capital enemy that periodically launches chasers and artillery on their own intervals and fires a heavily telegraphed line beam capable of killing several player vessels at once. Spawning reuses #52's EnemySpawnerComponent as-is via two preset scenes (EnemySpawnerComponentChaser.tscn: 5s/chaser, EnemySpawnerComponentArtillery.tscn: 18s/max 2/artillery) rather than adding multi-type support to the component - two instances of a single-purpose component is the composition this codebase already uses everywhere ("enemy bomber" in the original request is read here as the existing artillery enemy's slow telegraphed AOE, not a new enemy type - flagged as a judgment call in the ticket). New EnemyBigLaserComponent.gd ("big_laser") fires a standalone EnemyBigLaserBeam on 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 as EnemyArtilleryComponent/ArtilleryShell. The aim direction is locked once in launch() 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 every player_ships member; Ship.take_damage() already handles shield absorption per-ship. MothershipEnemyDefinition.tres: max_hp 60, speed 35 (slow), a ~96-unit hull, pursuit + two spawners + big_laser, 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 real Enemy.tscn instance). Screenshot of the charge telegraph mid-countdown. Full suite: 80/80. Found and fixed a real bug along the way, in EnemySpawnerComponent.gd (#52): its `enemy_scene: PackedScene = preload("res://Enemy.tscn")` member-level preload could get permanently baked in as a broken, empty PackedScene when a definition's component_scenes chain led back to Enemy.tscn while it was still mid-load - confirmed empirically via ResourceLoader.has_cached() being true (a good copy existed) while the preloaded reference's own resource_path was 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). Fixed by resolving enemy_scene lazily via load() on first use instead of at compile time, and additionally made a fresh spawner count down from a full interval before its first spawn rather than firing on tick one, which also happens to keep two sibling spawners with different intervals from ever landing their first attempt on the same frame. 🤖 Generated with Claude Code Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 10:19:02 +02:00
[ext_resource type="PackedScene" path="res://EnemySpawnerComponentChaser.tscn" id="3"]
[ext_resource type="PackedScene" path="res://EnemySpawnerComponentArtillery.tscn" id="4"]
[ext_resource type="PackedScene" path="res://EnemyBigLaserComponent.tscn" id="5"]
[resource]
script = ExtResource("1")
enemy_id = "enemy_mothership"
display_name = "Mothership"
max_hp = 60
speed = 35.0
acceleration = 45.0
rotation_speed = 1.5
hull_points = PackedVector2Array(0, -50, 35, -25, 48, 10, 20, 48, -20, 48, -48, 10, -35, -25)
glow_color = Color(0.55, 0.05, 0.85, 1)
Stacking distorders, menu layouts, lab rewards, enemy detail, artillery cross DISTORDER STACKING. Each space-time distorder vessel past the first raises the fleet's speed ceiling by 1.0, so a second one is worth crewing instead of being dead weight. GameState gains max_fleet_speed_multiplier, pushed by Fleet whenever composition changes - the same direction as every other fleet-derived figure, since GameState does not reach into the scene tree to count ships. The energy drain is normalised against the CURRENT ceiling, so full boost costs the same per second however wide it is: extra vessels buy more speed, not cheaper speed. Losing one re-clamps an over-ceiling boost rather than leaving the fleet travelling faster than its crew can sustain. TRANSFER MENU. Rows overlapped, and the last one ran into the hint text. row_height is divided from the available space, but each row's contents used a fixed pixel ladder - glyph at +10, buttons at +88, selector at +142 - which needs about 244px. Three resources fit; the fourth cut rows to 180 and every one spilled 64px into the next. Every offset is now a fraction of the row, so it cannot overflow. Also dropped the per-row AMOUNT caption (four copies of a word the footer hint already explains) and moved each glyph and name to the TOP of its row, where they read as its heading rather than the next row's. HANGAR. Six vessel types in one row gave each about 100px for a 155px label, so the names ran together. Now a 3x2 grid per section, sized from the space between the first heading and the status line, using the empty lower half. LAB RESEARCH REWARD. A harder enemy lab is worth more research points, and how hard a lab is depends on the layout authored for it - how many capture points, how many defenders - which the planet TYPE cannot know. PlanetSetup carries research_point_reward, set in the map editor, overriding the planet type's flat rate. 0 means "no opinion", so every map authored before this keeps its old value. RESEARCH LAB BACKGROUND. Its own shader instead of the crater field with a red tint, which read as "rocky planet, tinted" rather than someone's facility: plated floor, seams, service conduits, lit junctions. Loop-free by construction (#68's lesson applied up front) - conduits stay continuous across cells by keying a horizontal run to cell.y and a vertical to cell.x, so neighbouring cells agree without any cell scanning its neighbours. ENEMY DETAIL. Every enemy was a single flat outline, which left the artillery and the turret spawner as the same octagon at two sizes - the silhouette carried no information about what the thing did. EnemyDefinition gains detail_lines and detail_rings as DATA, so adding an enemy still means adding a definition rather than editing Enemy.gd, and the four non-default hulls are redrawn: a long-barrelled sniper, a squat mortar, a hive with launch bays, a capital ship with spine and ribs. Three things drew an enemy from its definition - the enemy, the map editor's markers, and the palette buttons - each reimplementing it from hull_points alone, so the new detail appeared in the game and nowhere else and an editor marker stopped matching what it placed. They now share EnemyDefinitions.draw_definition(). ARTILLERY. The 1.8s charge-up before the tower fired arrived long before the threat was legible and mostly read as the tower being stuck. It fires immediately now; the warning is the shell you can see in the air plus a small cross on the ground at the impact point. Deliberately not a blast-radius ring - a large translucent area was easy to read as the explosion having already happened, where the cross says "move off this spot". MAIN MENU. No Quit in any build (a web build has nothing to quit to, and Android's back gesture already does it), a plain black background, and the game is "Spaceship" - in the title and in config/name, which is what drives the browser tab for the web export. change_scene_to_file() returns an error and otherwise fails silently, leaving the menu up, which is indistinguishable from an unpressed button; it is now checked and reported on screen. Full suite green at 99/99. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 16:54:46 +02:00
detail_lines = PackedVector2Array(0, -44, 0, 40, -28, -18, 28, -18, -34, 4, 34, 4, -20, 28, 20, 28, -12, -40, 12, -40)
detail_rings = PackedVector2Array(0, -28, 9, 0, 0, 18, 6, 0)
Enemy mothership (#53) A large, slow capital enemy that periodically launches chasers and artillery on their own intervals and fires a heavily telegraphed line beam capable of killing several player vessels at once. Spawning reuses #52's EnemySpawnerComponent as-is via two preset scenes (EnemySpawnerComponentChaser.tscn: 5s/chaser, EnemySpawnerComponentArtillery.tscn: 18s/max 2/artillery) rather than adding multi-type support to the component - two instances of a single-purpose component is the composition this codebase already uses everywhere ("enemy bomber" in the original request is read here as the existing artillery enemy's slow telegraphed AOE, not a new enemy type - flagged as a judgment call in the ticket). New EnemyBigLaserComponent.gd ("big_laser") fires a standalone EnemyBigLaserBeam on 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 as EnemyArtilleryComponent/ArtilleryShell. The aim direction is locked once in launch() 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 every player_ships member; Ship.take_damage() already handles shield absorption per-ship. MothershipEnemyDefinition.tres: max_hp 60, speed 35 (slow), a ~96-unit hull, pursuit + two spawners + big_laser, 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 real Enemy.tscn instance). Screenshot of the charge telegraph mid-countdown. Full suite: 80/80. Found and fixed a real bug along the way, in EnemySpawnerComponent.gd (#52): its `enemy_scene: PackedScene = preload("res://Enemy.tscn")` member-level preload could get permanently baked in as a broken, empty PackedScene when a definition's component_scenes chain led back to Enemy.tscn while it was still mid-load - confirmed empirically via ResourceLoader.has_cached() being true (a good copy existed) while the preloaded reference's own resource_path was 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). Fixed by resolving enemy_scene lazily via load() on first use instead of at compile time, and additionally made a fresh spawner count down from a full interval before its first spawn rather than firing on tick one, which also happens to keep two sibling spawners with different intervals from ever landing their first attempt on the same frame. 🤖 Generated with Claude Code Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-16 10:19:02 +02:00
component_scenes = Array[PackedScene]([ExtResource("2"), ExtResource("3"), ExtResource("4"), ExtResource("5")])