#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>
This commit is contained in:
parent
baac11ae8b
commit
2f161621c3
495 changed files with 571 additions and 567 deletions
170
ui/SkillTreeView.gd
Normal file
170
ui/SkillTreeView.gd
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
extends Control
|
||||
|
||||
# The mothership's research tree, drawn in the game's own neon style rather
|
||||
# than with Godot UI widgets, for the same reason everything else here is
|
||||
# hand-drawn: a themed Button would look nothing like the rest of the game.
|
||||
#
|
||||
# Nodes are laid out from the normalised `pos` in GameState.SKILL_TREE, so
|
||||
# the tree's shape is data next to the upgrades themselves instead of being
|
||||
# duplicated here.
|
||||
#
|
||||
# Input arrives through BuildController._unhandled_input, NOT _gui_input -
|
||||
# a Control that consumes touches through the GUI system swallows them
|
||||
# before the menu can act on them, which is the bug that made the formation
|
||||
# editor's drag silently do nothing. Everything in this menu must stay
|
||||
# mouse_filter = IGNORE.
|
||||
|
||||
signal node_picked(id: String)
|
||||
|
||||
const LOCKED_COLOR: Color = Color(0.36, 0.39, 0.5)
|
||||
const READY_COLOR: Color = Color(0.55, 0.6, 1.0)
|
||||
const DONE_COLOR: Color = Color(0.45, 0.9, 0.55)
|
||||
const SELECTED_RING: Color = Color(1.0, 0.85, 0.4)
|
||||
# A locked node needs a dark core as well as a dim glow. NeonDraw tops every
|
||||
# shape with a near-white core stroke, which at this size dominates the glow
|
||||
# entirely - so a "dim" colour alone still came out the brightest thing on
|
||||
# screen, making unreachable nodes look like the highlighted ones.
|
||||
const LOCKED_CORE: Color = Color(0.17, 0.19, 0.26)
|
||||
const LOCKED_PULSE: float = 0.5
|
||||
|
||||
@export var node_radius: float = 46.0
|
||||
@export var row_spacing: float = 250.0
|
||||
|
||||
# #83: a second tap on the node already selected buys it. The separate buy
|
||||
# button is gone - it sat at the bottom of the panel, a long way from the node
|
||||
# being chosen, and meant every purchase was look-down-tap-look-up. Tapping
|
||||
# the thing you want is the shorter sentence.
|
||||
#
|
||||
# Deliberately "tap the SAME node twice" rather than a timed double-tap: no
|
||||
# window to miss on a slow phone, no accidental purchase from a fast
|
||||
# double-tap while browsing, and the first tap always does something visible
|
||||
# (it selects, and the panel fills in with the cost and what it unlocks) so
|
||||
# the second one is never a blind commitment.
|
||||
signal node_confirmed(id: String)
|
||||
|
||||
var selected_id: String = ""
|
||||
var scroll_y: float = 0.0
|
||||
var _dragging: bool = false
|
||||
|
||||
|
||||
func _content_height() -> float:
|
||||
# The canvas deliberately exceeds the viewport. Four depths at this
|
||||
# spacing leave clear breathing room around 92px-wide nodes and make each
|
||||
# research branch read as a route rising away from Shipyards.
|
||||
return maxf(size.y, GameState.max_skill_depth() * row_spacing + node_radius * 3.0)
|
||||
|
||||
|
||||
func _max_scroll() -> float:
|
||||
return maxf(0.0, _content_height() - size.y)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
call_deferred("reset_scroll")
|
||||
|
||||
|
||||
func reset_scroll() -> void:
|
||||
# Start at Shipyards, the base of the tree, whenever the menu opens.
|
||||
# The player then swipes up to travel along the branches.
|
||||
scroll_y = _max_scroll()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func node_screen_pos(node: Dictionary) -> Vector2:
|
||||
# Every dependency depth gets its own widely-spaced horizontal row. The
|
||||
# x lane is stable within a branch (set beside SKILL_TREE), so Logistics,
|
||||
# Freight and Freight Yards rise vertically instead of zig-zagging across
|
||||
# the display. scroll_y exposes the part of this tall canvas in view.
|
||||
var margin: float = node_radius * 1.4
|
||||
return Vector2(
|
||||
lerpf(margin, size.x - margin, float(node.x)),
|
||||
_content_height() - margin - float(GameState.skill_depth(node.id)) * row_spacing - scroll_y
|
||||
)
|
||||
|
||||
|
||||
func _state_color(node: Dictionary) -> Color:
|
||||
if GameState.is_unlocked(node.id):
|
||||
return DONE_COLOR
|
||||
if GameState.prerequisite_met(node.id):
|
||||
return READY_COLOR
|
||||
return LOCKED_COLOR
|
||||
|
||||
|
||||
# Returns true when the press landed on a node, so the caller knows the
|
||||
# touch was consumed.
|
||||
func handle_touch(event: InputEventScreenTouch) -> bool:
|
||||
if not visible:
|
||||
return false
|
||||
if not event.pressed:
|
||||
_dragging = false
|
||||
return true
|
||||
_dragging = true
|
||||
var local: Vector2 = event.position - global_position
|
||||
for node in GameState.SKILL_TREE:
|
||||
if local.distance_to(node_screen_pos(node)) <= node_radius * 1.25:
|
||||
var confirming: bool = selected_id == node.id
|
||||
selected_id = node.id
|
||||
queue_redraw()
|
||||
node_picked.emit(node.id)
|
||||
if confirming:
|
||||
node_confirmed.emit(node.id)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func handle_drag(event: InputEventScreenDrag) -> bool:
|
||||
if not visible:
|
||||
return false
|
||||
# Dragging a finger upward reveals higher research; dragging downward
|
||||
# returns to the root. Clamp hard so the canvas never leaves an empty gap.
|
||||
scroll_y = clampf(scroll_y - event.relative.y, 0.0, _max_scroll())
|
||||
queue_redraw()
|
||||
return true
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
# Links first, so nodes sit on top of them.
|
||||
for node in GameState.SKILL_TREE:
|
||||
if node.requires == "":
|
||||
continue
|
||||
var parent: Dictionary = GameState.skill_node(node.requires)
|
||||
if parent.is_empty():
|
||||
continue
|
||||
var from: Vector2 = node_screen_pos(parent)
|
||||
var to: Vector2 = node_screen_pos(node)
|
||||
# A link reads as "live" only once the step it leads to is actually
|
||||
# reachable, so the eye follows the researched path outwards.
|
||||
var lit: bool = GameState.is_unlocked(node.id)
|
||||
var reachable: bool = GameState.prerequisite_met(node.id)
|
||||
var link_color: Color = DONE_COLOR if lit else (READY_COLOR if reachable else LOCKED_COLOR)
|
||||
var trim: Vector2 = (to - from).normalized() * node_radius
|
||||
NeonDraw.draw_glowing_line(self, from + trim, to - trim, link_color,
|
||||
1.0 if reachable else LOCKED_PULSE, 0.4,
|
||||
NeonDraw.DEFAULT_CORE if reachable else LOCKED_CORE)
|
||||
|
||||
for node in GameState.SKILL_TREE:
|
||||
var centre: Vector2 = node_screen_pos(node)
|
||||
var colour: Color = _state_color(node)
|
||||
var reach: bool = GameState.prerequisite_met(node.id)
|
||||
var pulse: float = 1.0 if reach else LOCKED_PULSE
|
||||
var core_stroke: Color = NeonDraw.DEFAULT_CORE if reach else LOCKED_CORE
|
||||
var hexagon: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(6):
|
||||
var a: float = (float(i) / 6.0) * TAU + PI / 6.0
|
||||
hexagon.append(centre + Vector2(cos(a), sin(a)) * node_radius)
|
||||
NeonDraw.draw_glowing_polygon(self, hexagon, colour, pulse, core_stroke)
|
||||
|
||||
# Researched nodes get a filled-in core; the rest stay hollow.
|
||||
if GameState.is_unlocked(node.id):
|
||||
var core: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(6):
|
||||
var a: float = (float(i) / 6.0) * TAU + PI / 6.0
|
||||
core.append(centre + Vector2(cos(a), sin(a)) * node_radius * 0.42)
|
||||
NeonDraw.draw_glowing_polygon(self, core, colour, 1.0)
|
||||
|
||||
if node.id == selected_id:
|
||||
var ring: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(6):
|
||||
var a: float = (float(i) / 6.0) * TAU
|
||||
ring.append(centre + Vector2(cos(a), sin(a)) * node_radius * 1.32)
|
||||
NeonDraw.draw_glowing_polygon(self, ring, SELECTED_RING, 0.9)
|
||||
Loading…
Reference in a new issue