Standoff sniper enemy #45

Closed
opened 2026-08-15 11:16:20 +00:00 by metaheavens · 1 comment
Owner

Part of #40. Depends on the enemy-foundation ticket.

one that stays a bit far from the fleet when detected, and shot them projectiles (not too fast) toward them

Scope

  1. EnemyStandoffComponent.tscn + .gd (capability_id = "standoff") - holds a preferred distance from the nearest player_ships node instead of ramming:
    • preferred_range (suggest 620), advances when farther than that, retreats when closer than preferred_range * 0.75, drifts laterally in the band between. Do not put a pursuit component on the same enemy.
    • Exported/definition-driven, not hardcoded per enemy.
  2. EnemyRangedWeaponComponent.tscn + .gd (capability_id = "ranged_weapon") - fires an EnemyProjectile at the nearest ship's current position (deliberately not leading the target, so it is dodgeable):
    • fire_range (suggest 700), fire_cooldown (suggest 2.2 s), projectile_speed (suggest 260 - well under the interceptor's 460 max_speed, so a moving ship can evade), damage 1.
    • Same shape as VesselWeaponComponent.gd, which is the reference implementation.
  3. SniperEnemyDefinition.tres - enemy_id = "sniper", max_hp 4, speed ~90, an elongated hull clearly different from the chaser's diamond, and its own glow colour distinct from the chaser red and the gas-bomb pink (a cold cyan or violet reads well against both surfaces).
  4. Register it in EnemyDefinitions so it is placeable from a planet setup by enemy_id.

Acceptance criteria

  • The sniper closes to roughly preferred_range and stays in that band - it never reaches contact range with a stationary fleet.
  • It fires only within fire_range, on cooldown.
  • A ship moving perpendicular at max_speed is not guaranteed to be hit - assert the projectile is slow enough that the ship clears the impact point.
  • A hit destroys an unshielded ship and is absorbed by a shielded one.
  • No type switch: everything comes from the definition and its components.

Tests

tests/test_sniper_enemy.gd (bash tests/run_tests.sh sniper), plus a 720x1280 screenshot showing it beside a chaser so the two are visually distinguishable.

Repo notes (read before starting)

  • Godot 4.7.1, portrait 720x1280, touch UI. Project dir: /storage/emulated/0/GodotProjects/SpaceshipTest.
  • After editing any project file, run touch on 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.
  • Headless tests: bash tests/run_tests.sh <substring>; full suite with no argument. Tests are SceneTree scripts printing TEST_NAME: PASS / TEST_NAME: FAIL (reason) and calling quit(0|1) - copy the shape of tests/test_gas_bomber_cooldowns.gd.
  • Real screenshots: bash tests/screenshot.sh <res://Scene.tscn> <out.png> <frames> 720x1280. Use few frames (~10-20); the software rasterizer is slow. --headless renders nothing at all, so it cannot validate visuals.
  • Drawing goes through NeonDraw / NeonPiece; do not introduce a different visual idiom.
  • The repo deliberately has no type switches left (tickets #31-#35): behaviour comes from *Definition resources plus capability component scenes. Do not add if type == "..." branches.
  • There is no persistence layer (no FileAccess, no user:// anywhere outside this ticket's explicit scope). GameState is an in-memory autoload. Do not invent a save system.
  • Commit each ticket separately and run the full suite before pushing.
Part of #40. Depends on the enemy-foundation ticket. > one that stays a bit far from the fleet when detected, and shot them projectiles (not too fast) toward them ### Scope 1. `EnemyStandoffComponent.tscn` + `.gd` (`capability_id = "standoff"`) - holds a preferred distance from the nearest `player_ships` node instead of ramming: - `preferred_range` (suggest 620), advances when farther than that, retreats when closer than `preferred_range * 0.75`, drifts laterally in the band between. Do not put a pursuit component on the same enemy. - Exported/definition-driven, not hardcoded per enemy. 2. `EnemyRangedWeaponComponent.tscn` + `.gd` (`capability_id = "ranged_weapon"`) - fires an `EnemyProjectile` at the nearest ship's **current position** (deliberately not leading the target, so it is dodgeable): - `fire_range` (suggest 700), `fire_cooldown` (suggest 2.2 s), `projectile_speed` (suggest 260 - well under the interceptor's 460 `max_speed`, so a moving ship can evade), `damage` 1. - Same shape as `VesselWeaponComponent.gd`, which is the reference implementation. 3. `SniperEnemyDefinition.tres` - `enemy_id = "sniper"`, `max_hp` 4, `speed` ~90, an elongated hull clearly different from the chaser's diamond, and its own glow colour distinct from the chaser red and the gas-bomb pink (a cold cyan or violet reads well against both surfaces). 4. Register it in `EnemyDefinitions` so it is placeable from a planet setup by `enemy_id`. ### Acceptance criteria - The sniper closes to roughly `preferred_range` and stays in that band - it never reaches contact range with a stationary fleet. - It fires only within `fire_range`, on cooldown. - A ship moving perpendicular at `max_speed` is not guaranteed to be hit - assert the projectile is slow enough that the ship clears the impact point. - A hit destroys an unshielded ship and is absorbed by a shielded one. - No type switch: everything comes from the definition and its components. ### Tests `tests/test_sniper_enemy.gd` (`bash tests/run_tests.sh sniper`), plus a 720x1280 screenshot showing it beside a chaser so the two are visually distinguishable. ### Repo notes (read before starting) - Godot 4.7.1, portrait 720x1280, touch UI. Project dir: `/storage/emulated/0/GodotProjects/SpaceshipTest`. - **After editing any project file, run `touch` on 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. - Headless tests: `bash tests/run_tests.sh <substring>`; full suite with no argument. Tests are `SceneTree` scripts printing `TEST_NAME: PASS` / `TEST_NAME: FAIL (reason)` and calling `quit(0|1)` - copy the shape of `tests/test_gas_bomber_cooldowns.gd`. - Real screenshots: `bash tests/screenshot.sh <res://Scene.tscn> <out.png> <frames> 720x1280`. Use few frames (~10-20); the software rasterizer is slow. `--headless` renders nothing at all, so it cannot validate visuals. - Drawing goes through `NeonDraw` / `NeonPiece`; do not introduce a different visual idiom. - The repo deliberately has **no type switches** left (tickets #31-#35): behaviour comes from `*Definition` resources plus capability component scenes. Do not add `if type == "..."` branches. - There is **no persistence layer** (no `FileAccess`, no `user://` anywhere outside this ticket's explicit scope). `GameState` is an in-memory autoload. Do not invent a save system. - Commit each ticket separately and run the full suite before pushing.
Author
Owner

Implemented and pushed as fc2b5ca.

  • EnemyStandoffComponent holds a preferred distance from the nearest player ship instead of ramming: advances beyond preferred_range, retreats inside retreat_range_factor of it, drifts laterally in the band between. Never paired with a pursuit component.
  • EnemyRangedWeaponComponent mirrors VesselWeaponComponent for an enemy: fires an EnemyProjectile at the target's current position, not led, so a moving ship can dodge it (projectile_speed 260 vs. the interceptor's 460 max_speed).
  • Registered in EnemyDefinitions - placeable from a planet setup by enemy_id, no other code touched.

New test tests/test_sniper_enemy.gd covers standoff distance-holding (never reaches contact range), fire-range gating, cooldown, dodgeability, and hit/shield-absorption. This one took a few iterations: physics ticks can run ahead of a SceneTree test's own _process() clock under this device's load, so the cooldown-violation check ended up reading the component's own fire_timer directly (a rising value means it refired) instead of comparing projectile counts against a wall-clock wait - and manually repositioned ships need their move_target re-pinned, or Ship's own steering silently drives them back to the stale target. Verified visually beside a chaser at 720x1280 for a clear hull/colour contrast.

Part of the #40 conquest-mechanic breakdown, alongside #46.

Implemented and pushed as `fc2b5ca`. - `EnemyStandoffComponent` holds a preferred distance from the nearest player ship instead of ramming: advances beyond `preferred_range`, retreats inside `retreat_range_factor` of it, drifts laterally in the band between. Never paired with a pursuit component. - `EnemyRangedWeaponComponent` mirrors `VesselWeaponComponent` for an enemy: fires an `EnemyProjectile` at the target's **current** position, not led, so a moving ship can dodge it (`projectile_speed` 260 vs. the interceptor's 460 `max_speed`). - Registered in `EnemyDefinitions` - placeable from a planet setup by `enemy_id`, no other code touched. New test `tests/test_sniper_enemy.gd` covers standoff distance-holding (never reaches contact range), fire-range gating, cooldown, dodgeability, and hit/shield-absorption. This one took a few iterations: physics ticks can run ahead of a SceneTree test's own `_process()` clock under this device's load, so the cooldown-violation check ended up reading the component's own `fire_timer` directly (a rising value means it refired) instead of comparing projectile counts against a wall-clock wait - and manually repositioned ships need their `move_target` re-pinned, or `Ship`'s own steering silently drives them back to the stale target. Verified visually beside a chaser at 720x1280 for a clear hull/colour contrast. Part of the #40 conquest-mechanic breakdown, alongside #46.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
metaheavens/spaceship#45
No description provided.