Files
Papay-Shooter/debug/ui_capture.gd
T
Nicholas ButzkeandClaude Opus 5 26f7c2c622 feat(ui): one high-voltage theme, and pick your character from the escape menu
The theme was a comic one — cream paper, ink borders, papaya. It is now a
charged one: near-black violet, hot papaya, and a lightning yellow spent
nowhere except the instant a button is pressed. Chips are cut with two sharp
corners and two round ones on a diagonal, which is as close to a skew as a
StyleBoxFlat gets and is the difference between a button that reads calm and
one that reads fast. BoltRule draws the motif itself, struck a third of the way
along its rule rather than centred, so it reads as something that HIT the line.

The pause menu was 900 lines of hand-rolled UI that never referenced UITheme at
all, so it rendered in Godot's default grey. It now applies the theme — and
applies it to its own root Control, not only to the Window, because a Control
inherits from its nearest Control ANCESTOR and this screen hangs off a
CanvasLayer, which is not one. That was invisible at first: the parts built with
UITheme.title() carry their own overrides and looked right next to a list and a
button that did not.

And it now has a Character screen. The roster on the left, the character
themselves on the right, turning — a name in a dropdown is not a character
selection screen. The preview is a real SkinnedPlayerModel in its own world, so
it shows exactly what will spawn: the same cel look, the same per-class
outlines, the same cloth and hair on springs. Selecting applies immediately;
there is nothing destructive to confirm, and applying on selection means the
character behind the menu changes as you arrow the list, which IS the
comparison.

PlayerMovementController.set_skin() is the supported way in. Both halves of a
skin change are easy to do by halves — `synced_skin_id` is what REMOTE peers
rebuild from, and only their _process watches it, so setting the property alone
would change everyone else's view of you and not your own.

Checked rather than asserted. debug/character_picker_check.gd walks the whole
roster and proves each entry loads a skeleton, animations, a surface table and
body surfaces — and that the skeleton is MOVING, because the clip name is a
variable this class sets on itself and reads "Idle" just as happily when nothing
is ticking. debug/ui_capture.gd and debug/roster_capture.gd photograph the
screens and every character, which is how three things were found that no
assertion could see: the theme break above, a preview showing the back of the
character's head, and a turntable that carried on from the last character so the
third one you looked at was side-on.

Known and not fixed here: momo's idle pose is wrong — arms overhead and a
pinched waist. Her rig, not the picker; every other character is correct.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-27 14:46:14 -04:00

69 lines
1.9 KiB
GDScript

extends SceneTree
## Screenshot every UI screen, so the theme can be LOOKED at rather than
## asserted about.
##
## A theme is the one kind of change no headless check can validate: every
## contrast mistake, every panel that turns out to be invisible against the
## thing behind it, and every button whose label sits outside its own chip
## compiles perfectly. So this drives the real screens with rendering on and
## writes a PNG of each.
##
## godot --path . -s res://debug/ui_capture.gd -- <output_dir>
##
## Writes shot_menu.png, shot_pause.png, shot_character.png and
## shot_settings.png.
var _frames := 0
var _out_dir := "."
func _initialize() -> void:
var args := OS.get_cmdline_user_args()
if args.size() > 0:
_out_dir = args[0]
change_scene_to_file("res://ui/main_menu/main_menu.tscn")
func _process(_delta: float) -> bool:
_frames += 1
match _frames:
50:
_shot("menu")
# Into a real match. The pause menu deliberately refuses to open over
# the main menu, and it is the in-game background — a lit 3D level
# rather than a flat colour — that its legibility has to survive.
var nm = root.get_node_or_null("NetworkManager")
if nm and nm.has_method("start_singleplayer_match"):
nm.start_singleplayer_match("Deathmatch")
change_scene_to_file("res://scenes/maps/test_level/test_level.tscn")
200:
_open("_show_main_menu")
215:
_shot("pause")
_open("_show_character")
320:
# Long enough for the model to load and the springs to settle.
_shot("character")
_open("_show_settings")
340:
_shot("settings")
quit()
return false
func _open(screen: String) -> void:
var menu = root.get_node_or_null("PauseMenu")
if menu == null:
push_warning("ui_capture: no PauseMenu autoload")
return
menu.visible = true
menu.call(screen)
func _shot(name: String) -> void:
var img := root.get_texture().get_image()
var path := "%s/shot_%s.png" % [_out_dir, name]
img.save_png(path)
print("wrote %s" % path)