spaceship/buildings/Building.gd

111 lines
4 KiB
GDScript3
Raw Normal View History

extends Node2D
#84: move 215 root files into a folder hierarchy Everything lived in one flat directory, so a diff gave no clue what area it touched and finding the file you wanted meant knowing its exact name. Twelve folders now, grouped by what a thing IS rather than what type it is - a vessel's definition, its component scenes and its upgrade resources sit together, because that is the set you read when you change one of them. buildings core controllers enemies logistics planets resources shaders surface ui vessels vfx world 332 files moved and every res:// reference rewritten - 306 files touched, including the autoload and main-scene paths in project.godot. Nothing renamed: this is purely where files sit, so the diff is a pile of pure moves and no behaviour is buried in it. ui_theme.tres, icon.svg and web_shell.html stay at the root, where Godot and the export preset expect them. Verified three ways, because a suite alone would not have caught all of it: the full 117 pass, the web export builds clean, and the game renders. The export is why that matters. The FIRST export after the move failed with "Attempt to open script 'res://GameState.gd'" - the old path - while project.godot on disk plainly said res://core/GameState.gd. That is this project's documented Android FUSE mtime trap: Godot served a cached copy of a file whose content had changed. A `touch` fixed it. Nothing in the test suite would have found that, because the suite runs from a copy on a native filesystem - the one place the bug cannot happen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 19:44:14 +02:00
const BUILDING_DEFINITIONS = preload("res://buildings/BuildingDefinitions.gd")
@export var definition_id: String = "mothership"
@export var building_id: int = 0
@export var interact_radius: float = 150.0
var definition: Resource
var visual: Node2D
var building_type: String:
get: return definition_id
set(value): definition_id = value
Skill tree, node-based building animation, bigger UI, and a 19x faster test suite Gameplay and UI - Research tree at the mothership gates which structures can be built and grants permanent fleet-capacity upgrades (GameState.SKILL_TREE, SkillTreeView). Node depth is derived from prerequisites, so the layout cannot drift out of step with the dependencies. - Building info popup before committing to a build, with per-building cost and description. - Ship crafter redrawn as a space factory with an assembly animation; mothership redrawn as a radial flagship; command centre as a plotting hub. - Keyboard and mouse support (WASD, Q/E pivot, wheel zoom) for the web build. - UI and font scaled up for phone use, via a custom Theme resource - gui/theme/default_font_size does not reach the default theme. - Asteroids no longer spawn on top of the fleet's start position. - Fleet formation origin derived from the layout's own offsets rather than the centroid, fixing the lurch when a lopsided formation started moving. - Fleet.stop() brakes from the ship's TRUE motion (velocity + fleet_velocity). Ship zeroes its velocity when seated in its slot even while the anchor drags that slot forward, so braking from velocity alone stopped the fleet dead instead of coasting - and whether it did depended on the frame rate. Animation architecture (issue #10) - Buildings, projectiles and explosion VFX no longer animate by redrawing. Static geometry is rasterised once in _draw(); moving parts are NeonPiece children animated through node properties, which reuse the cached geometry. Every NeonDraw call expands into four antialiased polylines, so redrawing to move something re-tessellated all of it. Test suite (issue #8): 1075s -> ~55s - Runs from a copy on Termux's own filesystem. The project lives on Android's FUSE bridge, read through proot, where load("res://Main.tscn") alone cost 8.5s per test against ~2s natively - and nearly every test loads it. - Runs 6 tests at a time, in a single proot-distro login, with output buffered per test and printed only on failure. - --fixed-fps 60 pins delta instead of measuring the wall clock, so a test's result no longer depends on how busy the machine is. Without it, parallelism made two tests fail on 2 of 3 runs. - A test passes only if it both exits 0 and prints ": PASS". - 38 tests, green on three consecutive runs. Bundles Carrois Gothic SC (Apache 2.0) with its licence text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:00:41 +02:00
# #99: drawn only once the structure has actually been hit. A full bar over
# every building would be permanent clutter saying nothing, and the fleet
# would be flying over a field of meters in peacetime.
# #215 made it bigger and brighter. It stays a DAMAGE-ONLY bar: the reason
# above still holds, and a field of full meters in peacetime is clutter, not
# visibility. What was actually hard to see was the bar when it was there -
# 74x7 of muted colour against a lit structure, at the zoom the game is
# usually played at.
const HEALTH_BAR_WIDTH: float = 104.0
const HEALTH_BAR_HEIGHT: float = 14.0
const HEALTH_BAR_OFFSET: float = -88.0
# The dark plate the bar sits on, drawn past its edges so the meter reads
# against the structure's own glow instead of competing with it.
const HEALTH_BAR_PAD: float = 3.0
const HEALTH_FULL_COLOR: Color = Color(0.5, 1.0, 0.55)
const HEALTH_HURT_COLOR: Color = Color(1.0, 0.28, 0.2)
const HEALTH_PLATE_COLOR: Color = Color(0.02, 0.02, 0.06, 0.92)
const HEALTH_FRAME_COLOR: Color = Color(0.85, 0.96, 1.0, 0.95)
const HEALTH_FRAME_WIDTH: float = 2.0
var _drawn_health: float = 1.0
func _ready() -> void:
add_to_group("buildings")
z_index = -1
definition = BUILDING_DEFINITIONS.get_definition(definition_id)
if definition == null or definition.visual_scene == null:
return
visual = definition.visual_scene.instantiate()
get_node("VisualHost").add_child(visual)
Skill tree, node-based building animation, bigger UI, and a 19x faster test suite Gameplay and UI - Research tree at the mothership gates which structures can be built and grants permanent fleet-capacity upgrades (GameState.SKILL_TREE, SkillTreeView). Node depth is derived from prerequisites, so the layout cannot drift out of step with the dependencies. - Building info popup before committing to a build, with per-building cost and description. - Ship crafter redrawn as a space factory with an assembly animation; mothership redrawn as a radial flagship; command centre as a plotting hub. - Keyboard and mouse support (WASD, Q/E pivot, wheel zoom) for the web build. - UI and font scaled up for phone use, via a custom Theme resource - gui/theme/default_font_size does not reach the default theme. - Asteroids no longer spawn on top of the fleet's start position. - Fleet formation origin derived from the layout's own offsets rather than the centroid, fixing the lurch when a lopsided formation started moving. - Fleet.stop() brakes from the ship's TRUE motion (velocity + fleet_velocity). Ship zeroes its velocity when seated in its slot even while the anchor drags that slot forward, so braking from velocity alone stopped the fleet dead instead of coasting - and whether it did depended on the frame rate. Animation architecture (issue #10) - Buildings, projectiles and explosion VFX no longer animate by redrawing. Static geometry is rasterised once in _draw(); moving parts are NeonPiece children animated through node properties, which reuse the cached geometry. Every NeonDraw call expands into four antialiased polylines, so redrawing to move something re-tessellated all of it. Test suite (issue #8): 1075s -> ~55s - Runs from a copy on Termux's own filesystem. The project lives on Android's FUSE bridge, read through proot, where load("res://Main.tscn") alone cost 8.5s per test against ~2s natively - and nearly every test loads it. - Runs 6 tests at a time, in a single proot-distro login, with output buffered per test and printed only on failure. - --fixed-fps 60 pins delta instead of measuring the wall clock, so a test's result no longer depends on how busy the machine is. Without it, parallelism made two tests fail on 2 of 3 runs. - A test passes only if it both exits 0 and prints ": PASS". - 38 tests, green on three consecutive runs. Bundles Carrois Gothic SC (Apache 2.0) with its licence text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 15:00:41 +02:00
func health_fraction() -> float:
return GameState.building_health_fraction(building_id) if building_id != 0 else 1.0
func is_damaged() -> bool:
return health_fraction() < 0.999
func _process(_delta: float) -> void:
# Redraw only when the number moves. A building that is not being shot at
# costs nothing, which is the same bargain Ship.gd's constant glow makes.
var health: float = health_fraction()
if is_equal_approx(health, _drawn_health):
return
_drawn_health = health
queue_redraw()
func _draw() -> void:
if not is_damaged():
return
# Above the structure rather than across it: the visual scenes all draw
# around their own origin, and a bar laid over one would be read as part
# of the building.
var health: float = health_fraction()
var left: float = -HEALTH_BAR_WIDTH * 0.5
var top: float = HEALTH_BAR_OFFSET
var fill_color: Color = HEALTH_HURT_COLOR.lerp(HEALTH_FULL_COLOR, health)
draw_rect(Rect2(left - HEALTH_BAR_PAD, top - HEALTH_BAR_PAD,
HEALTH_BAR_WIDTH + HEALTH_BAR_PAD * 2.0,
HEALTH_BAR_HEIGHT + HEALTH_BAR_PAD * 2.0), HEALTH_PLATE_COLOR)
# The empty part is not left blank: a dim wash of the fill colour shows
# how much has GONE, which is the thing being reported.
draw_rect(Rect2(left, top, HEALTH_BAR_WIDTH, HEALTH_BAR_HEIGHT),
Color(fill_color.r, fill_color.g, fill_color.b, 0.16))
draw_rect(Rect2(left, top, HEALTH_BAR_WIDTH * health, HEALTH_BAR_HEIGHT),
fill_color)
# A bright square cap on the fill's leading edge, so the level itself has
# an edge to catch rather than only the colour changing.
if health > 0.001:
draw_rect(Rect2(left + HEALTH_BAR_WIDTH * health - HEALTH_FRAME_WIDTH,
top, HEALTH_FRAME_WIDTH, HEALTH_BAR_HEIGHT), HEALTH_FRAME_COLOR)
draw_rect(Rect2(left, top, HEALTH_BAR_WIDTH, HEALTH_BAR_HEIGHT),
HEALTH_FRAME_COLOR, false, HEALTH_FRAME_WIDTH)
func available_actions() -> PackedStringArray:
if visual == null:
return PackedStringArray()
var provider: Node = visual.get_node_or_null("ActionProvider")
return provider.actions if provider != null else PackedStringArray()
func primary_action() -> String:
for action in available_actions():
if action != "transfer" and action != "hangar":
return action
return ""
func get_display_name() -> String:
return String(definition.display_name) if definition != null else definition_id.capitalize()