Files
claude 61680d00d7
ci / verify (push) Successful in 48s
Style the rest of the interface: panels, sliders, scrollbars, cards
Buttons were the easy half. This is everything else.

Dialogs sit on panels now rather than being text over a dimmed world -- the
pause menu, character select, upgrades, settings and credits all root through
UiTheme.dialog_panel(). The panel stylebox is the pack's frame MODULATED DARK.
The set is cream throughout, which is right for buttons and wrong for a dialog
over a dark dungeon; tinting keeps the pixel border and the corner shape and
lets every label the game already draws in light colours stay readable, instead
of recolouring every label in five screens to suit the art.

Sliders and scrollbars are the pack's too, section headings sit on its banner
ribbon, and an upgrade card's rarity is now its frame rather than a word on it
-- three choices are compared at a glance and a colour reads faster than a
label.

Two bugs of the same shape, and neither was findable without looking at the
screen. A Slider and a ScrollBar take their THICKNESS from the stylebox's
minimum size, which for a StyleBoxTexture is its content margins. Mine were
zero, so both resolved the correct stylebox, reported the correct texture, and
drew a groove zero pixels tall. A probe confirmed the theme was resolving
perfectly while the track was invisible. Tests now assert every slider and
scrollbar stylebox has a non-zero minimum, and it is gotcha 7 in CLAUDE.md.

The first attempt at the slider groove also used the pack's HOLLOW bar sprite,
whose middle is transparent -- tinting it dark left an outline and nothing
else. It uses the solid one.

tools/screenshot.tscn gained the upgrade screen, which it has to stage: the
game scene owns that screen's visibility and re-asserts it every frame, so
setting `visible` lasted exactly one frame, and standing the player at the NPC
client-side lasted until reconciliation pulled them back. It now moves the
player on both sides and holds it until the shot. That tool has caught four
bugs the automated gates all passed.

check.sh clean, 468 tests, SMOKE PASS, all four diagnostics green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 11:45:46 +02:00

123 lines
4.0 KiB
GDScript

extends Node
## Boot the client windowed, capture a few screens, and quit.
##
## SHOT_DIR=/tmp/shots godot --path . res://tools/screenshot.tscn
##
## This exists because UI is the one part of the game the rest of the tooling
## cannot check. `check.sh` proves it parses, the suite proves the theme
## resolves, the smoke test proves nothing errors -- and all three passed
## while the entire interface rendered unstyled, because Godot's theme
## inheritance breaks at the first non-Control parent and nothing had ever
## looked at the screen.
##
## Needs a real display, so it is not part of any automated gate. Run it after
## touching anything under src/ui/ and open the files.
const GAME := preload("res://src/game.tscn")
## Frame numbers, not seconds: the point is a deterministic sequence, and a
## windowed client at 60fps gets there in about four seconds.
const AT_MENU := 30
const ENTER_GAME := 34
const AT_GAME := 200
const OPEN_MENU := 210
const AT_INGAME_MENU := 240
const OPEN_SETTINGS := 250
const AT_SETTINGS := 280
const OPEN_CREDITS := 290
const AT_CREDITS := 320
const OPEN_UPGRADES := 330
const AT_UPGRADES := 360
const DONE := 370
var _frame: int = 0
var _game: Node = null
var _menu: Control = null
var _dir: String = "user://"
func _ready() -> void:
GameOpts.parse()
Engine.physics_ticks_per_second = SimConfig.TICK_RATE
Settings.load_and_apply()
var from_env := OS.get_environment("SHOT_DIR")
if not from_env.is_empty():
_dir = from_env
get_window().size = Vector2i(1280, 720)
_menu = preload("res://src/ui/main_menu.gd").new()
add_child(_menu)
## Put the player at the quartermaster on BOTH sides. Setting only the client's
## predicted position does not hold: reconciliation pulls it back to whatever
## the server says within a tick or two, the game scene notices it is no longer
## at the NPC, and closes the screen again.
func _stand_at_the_npc() -> void:
var npc: Vector2 = Net.client.upgrade_npc
Net.client.predicted_pos = npc
var inst := Net.server.instance_of(Net.LOCAL_PEER)
if inst != null:
var p: SimPlayer = inst.world.players.get(Net.LOCAL_PEER)
if p != null:
p.pos = npc
_game.set("_upgrades_open", true)
func _process(_delta: float) -> void:
_frame += 1
# Held until the shot is taken; one frame of it is not enough.
if _frame > OPEN_UPGRADES and _frame <= AT_UPGRADES:
_stand_at_the_npc()
match _frame:
AT_MENU:
_grab("menu")
ENTER_GAME:
_menu.queue_free()
Net.host(27455)
Net.start_local_client()
_game = GAME.instantiate()
add_child(_game)
AT_GAME:
_grab("game")
OPEN_MENU:
_game.get_node("GameMenu").open()
AT_INGAME_MENU:
_grab("menu_ingame")
OPEN_SETTINGS:
_game.get_node("GameMenu").close()
_game.get_node("SettingsScreen").open()
AT_SETTINGS:
_grab("settings")
OPEN_CREDITS:
_game.get_node("SettingsScreen").visible = false
_game.get_node("CreditsScreen").open()
AT_CREDITS:
_grab("credits")
OPEN_UPGRADES:
# Driven with a made-up offer rather than by earning one: the point
# here is the card layout, and levelling a character up first would
# make the shot depend on a dungeon run.
_game.get_node("CreditsScreen").visible = false
# The game scene owns this screen's visibility and re-asserts it
# every frame, closing it unless the player is at the NPC -- so
# setting `visible` directly lasted exactly one frame. Stand the
# player at the quartermaster and open it the way the game does.
_stand_at_the_npc()
var offer: Array[StringName] = [
Upgrades.SPLIT_SHOT, Upgrades.POISON, Upgrades.ERASER]
var taken: Array[StringName] = [Upgrades.SNIPER, Upgrades.SPREAD]
_game.get_node("UpgradeScreen").refresh(2, offer, taken)
AT_UPGRADES:
_grab("upgrades")
DONE:
Net.shutdown()
get_tree().quit(0)
func _grab(shot_name: String) -> void:
var path := "%s/shot_%s.png" % [_dir, shot_name]
var err := get_viewport().get_texture().get_image().save_png(path)
if err != OK:
printerr("could not write %s (error %d)" % [path, err])
return
print("saved ", path)