Resolve pending gameplay tickets

This commit is contained in:
Loïc Wisniewski 2026-09-02 13:43:56 +02:00
commit c2ebbfaa2d
28 changed files with 618 additions and 188 deletions

View file

@ -61,6 +61,8 @@ enum Icon {
# inserting anywhere earlier renumbers every icon int already stored in
# every scene.
QUEST,
# #179: a fleet-wide thrust burst, distinct from the Propulser vessel icon.
DASH,
}
# Size the glyphs were originally drawn against. Every glyph dimension is
@ -319,6 +321,24 @@ func _draw() -> void:
_draw_repair_drone_icon(center, pulse, s)
Icon.QUEST:
_draw_quest_icon(center, pulse, s)
Icon.DASH:
_draw_dash_icon(center, pulse, s)
func _draw_dash_icon(center: Vector2, pulse: float, s: float) -> void:
var tip: Vector2 = center + Vector2(12.0, 0.0) * s
var tail: Vector2 = center + Vector2(-5.0, 0.0) * s
NeonDraw.draw_glowing_line(self, tail, tip, _active_color(), pulse,
0.7 * s, core_color)
NeonDraw.draw_glowing_path(self, PackedVector2Array([
tip + Vector2(-7.0, -7.0) * s, tip,
tip + Vector2(-7.0, 7.0) * s,
]), _active_color(), pulse, 0.7 * s, core_color)
for y in [-7.0, 0.0, 7.0]:
NeonDraw.draw_glowing_line(self,
center + Vector2(-13.0, y) * s,
center + Vector2(-7.0, y) * s,
_active_color(), pulse * 0.7, 0.45 * s, core_color)
# #140: a checklist - a page with a tick against the top line. Reads as

View file

@ -25,7 +25,7 @@ extends Control
# with nothing to author, and building it here keeps MainMenu.tscn - which the
# player's own editor touches - out of the change.
const BACKDROP: Color = Color(0.01, 0.02, 0.06, 0.94)
const BACKDROP: Color = Color(0.01, 0.02, 0.06, 1.0)
const GLOW: Color = Color(0.42, 0.86, 0.95)
const TEXT: Color = Color(0.82, 0.94, 1.0)
const MUTED: Color = Color(0.52, 0.66, 0.76)
@ -33,7 +33,6 @@ const TITLE_SIZE: int = 44
const SUBTITLE_SIZE: int = 24
const BAR_WIDTH: float = 520.0
const BAR_HEIGHT: float = 16.0
const BAR_GAP: float = 46.0
# The travelling band inside the bar, and how much of the bar it covers.
const SWEEP_SECONDS: float = 1.2
const SWEEP_WIDTH: float = 0.22
@ -55,12 +54,22 @@ func _ready() -> void:
# MainMenu refuses a second press while a load is running anyway.
mouse_filter = Control.MOUSE_FILTER_IGNORE
set_anchors_preset(Control.PRESET_FULL_RECT)
_fit_viewport()
get_viewport().size_changed.connect(_fit_viewport)
title_label = _make_label("LOADING", TITLE_SIZE, TEXT)
subtitle_label = _make_label("", SUBTITLE_SIZE, MUTED)
resized.connect(_layout)
_layout()
# A Control parent would resolve full-rect anchors for us. This node sits
# directly under a CanvasLayer, so its first layout pass can otherwise be
# 0x0 - exactly the frame that is supposed to cover the menu before loading.
func _fit_viewport() -> void:
position = Vector2.ZERO
size = get_viewport_rect().size
func _make_label(text_value: String, font_size: int, colour: Color) -> Label:
var label := Label.new()
label.text = text_value
@ -83,16 +92,17 @@ func begin(what: String) -> void:
func _bar_rect() -> Rect2:
var width: float = minf(BAR_WIDTH, maxf(size.x - 120.0, 40.0))
return Rect2(Vector2((size.x - width) * 0.5, size.y * 0.5 + BAR_GAP),
return Rect2(Vector2((size.x - width) * 0.5, (size.y - BAR_HEIGHT) * 0.5),
Vector2(width, BAR_HEIGHT))
func _layout() -> void:
if title_label == null:
return
title_label.position = Vector2(0.0, size.y * 0.5 - float(TITLE_SIZE) * 1.4)
var bar: Rect2 = _bar_rect()
title_label.position = Vector2(0.0, bar.position.y - float(TITLE_SIZE) * 1.8)
title_label.size = Vector2(size.x, float(TITLE_SIZE) * 1.4)
subtitle_label.position = Vector2(0.0, size.y * 0.5)
subtitle_label.position = Vector2(0.0, bar.end.y + 18.0)
subtitle_label.size = Vector2(size.x, float(SUBTITLE_SIZE) * 1.6)
queue_redraw()

View file

@ -36,9 +36,9 @@ extends Control
# lines never sees the second one at all, and the whole point of #127 was
# that lines wait for the reader.
#
# #168 says the same rule out loud - still arriving, show it all; already
# whole, move on - and extends it to the KEYBOARD, which had no way in at
# all. See _is_advance_press.
# A dialog is advanced deliberately with a tap or click. Keyboard presses are
# ignored while it is open: movement keys are too easy to hold when a line
# arrives, and must never turn into an accidental skip (#175).
signal dismissed
@ -139,7 +139,9 @@ func _ensure_label() -> void:
label.add_theme_color_override("font_color", TEXT_COLOR)
label.add_theme_color_override("font_outline_color", Color(0.01, 0.02, 0.06, 0.95))
label.add_theme_constant_override("outline_size", 4)
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
# #178: decoded text grows from a stable left edge. Centring a partially
# revealed line made it appear in the middle and expand in both directions.
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
add_child(label)
@ -268,28 +270,18 @@ func _process(delta: float) -> void:
queue_redraw()
# #168: what counts as "go on". A tap, a left click, or a key.
# What counts as "go on": a deliberate tap or left click.
#
# The rule the ticket asks for was already the rule for a click (#149) - a
# press finishes a line still arriving, a press on a finished line moves to
# the next - but the game is played on a desktop with WASD and a mouse, and
# a keyboard press reached nothing at all. Sitting behind a dialog pressing
# space is the most obvious thing a player can do, so the keyboard now goes
# through the same one door every other press does (dismiss()).
#
# Modifiers alone are not a press: holding shift before typing must not eat
# a line. Neither is an auto-repeat echo - a held key would otherwise run
# the whole queue out in a second.
# A press finishes a line still arriving; a press on a finished line moves to
# the next (#149). #175 deliberately limits that gesture to pointer input: the
# player may still be holding a
# movement key when a transmission arrives, and that must leave the line
# untouched rather than finishing or dismissing it.
func _is_advance_press(event: InputEvent) -> bool:
if event is InputEventScreenTouch:
return event.pressed
if event is InputEventMouseButton:
return event.pressed and event.button_index == MOUSE_BUTTON_LEFT
if event is InputEventKey:
if not event.pressed or event.echo:
return false
return not (event.keycode in [KEY_SHIFT, KEY_CTRL, KEY_ALT, KEY_META,
KEY_CAPSLOCK, KEY_NUMLOCK, KEY_SCROLLLOCK])
return false

View file

@ -67,6 +67,11 @@ var _entries: Array = []
var _shown_costs: Dictionary = {}
var _shown_points: int = 0
var _built: bool = false
# Optional site whose purchase this tag describes. With one, the displayed
# "have" includes fleet cargo only while the fleet is close enough to hand it
# over. Most tags describe construction at the fleet itself and leave this
# null, retaining the ordinary combined inventory readout.
var payment_position: Variant = null
func _ready() -> void:
@ -89,7 +94,7 @@ func show_price(costs: Dictionary, points: int = 0) -> void:
if amount <= 0:
continue
_add_entry(String(resource_type), "", amount,
GameState.available_resource(String(resource_type)),
_available(String(resource_type)),
GameState.resource_color(resource_type))
if points > 0:
_add_entry("", POINT_MARK, points, GameState.research_points, POINT_COLOR)
@ -102,12 +107,22 @@ func set_costs(costs: Dictionary) -> void:
show_price(costs)
func set_payment_position(at: Variant) -> void:
payment_position = at
_refresh_held()
func _available(resource_type: String) -> int:
return GameState.available_resource(resource_type) if payment_position == null \
else GameState.available_resource_at(resource_type, payment_position)
# What the player holds, re-read. The price does not move; the wallet does.
func _refresh_held() -> void:
var relayout: bool = false
for entry in _entries:
var have: int = GameState.research_points if String(entry.resource) == "" \
else GameState.available_resource(String(entry.resource))
else _available(String(entry.resource))
if have == int(entry.have):
continue
entry.have = have

View file

@ -8,6 +8,8 @@ signal research_requested(id: String)
@onready var status: Label = $StatusLabel
@onready var resource_bar: Control = $Bar
var building_id: int = 0
func _ready() -> void:
super._ready()
@ -15,13 +17,22 @@ func _ready() -> void:
tree.node_confirmed.connect(func(id: String): research_requested.emit(id))
func open_for(building_id: int) -> void:
func open_for(next_building_id: int) -> void:
building_id = next_building_id
resource_bar.building_id = building_id
var building: Dictionary = GameState.find_building(building_id)
cost.set_payment_position(building.get("position", null))
open_menu()
tree.reset_scroll()
refresh()
func close_menu() -> void:
super.close_menu()
building_id = 0
cost.set_payment_position(null)
func selected_id() -> String:
return String(tree.selected_id)
@ -31,6 +42,10 @@ func research_completed(id: String) -> void:
refresh(id)
func set_status(message: String) -> void:
status.text = message
func refresh(id: String = "") -> void:
if id == "":
id = String(tree.selected_id)