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]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
53f175ed6d
commit
26f7c2c622
@@ -0,0 +1,84 @@
|
||||
extends SceneTree
|
||||
|
||||
## Photograph every character in the picker, one shot each.
|
||||
##
|
||||
## The picker builds a real SkinnedPlayerModel per character, so this is the
|
||||
## cheapest full-roster visual check there is: it catches a rig whose idle pose
|
||||
## came out wrong, a skin that lost its textures, cloth that never settles, and
|
||||
## a character standing the wrong way round — none of which any assertion in
|
||||
## debug/character_picker_check.gd can see.
|
||||
##
|
||||
## godot --path . -s res://debug/roster_capture.gd -- <output_dir>
|
||||
|
||||
var _out_dir := "."
|
||||
var _frames := 0
|
||||
var _index := -1
|
||||
var _menu: Node = null
|
||||
## The character currently posing. Tracked explicitly rather than recomputed
|
||||
## from the list index at shutter time — _next() skips the colour-tint skins,
|
||||
## so the index when the photo is taken is not the index it was selected at,
|
||||
## and every file came out labelled with the wrong character.
|
||||
var _posing: String = ""
|
||||
## Frames to let a freshly-built character load, blend into idle and let its
|
||||
## hair and skirt stop swinging before the shutter opens.
|
||||
const SETTLE := 90
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
if args.size() > 0:
|
||||
_out_dir = args[0]
|
||||
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")
|
||||
|
||||
|
||||
func _process(_delta: float) -> bool:
|
||||
_frames += 1
|
||||
if _frames < 150:
|
||||
return false
|
||||
if _menu == null:
|
||||
_menu = root.get_node_or_null("PauseMenu")
|
||||
if _menu == null:
|
||||
print("no PauseMenu autoload")
|
||||
quit(1)
|
||||
return true
|
||||
_menu.visible = true
|
||||
_menu._show_character()
|
||||
_next()
|
||||
return false
|
||||
if (_frames - 150) % SETTLE == 0:
|
||||
_shoot()
|
||||
_next()
|
||||
return false
|
||||
|
||||
|
||||
func _next() -> void:
|
||||
_index += 1
|
||||
while _index < _menu.character_list.item_count:
|
||||
var id: String = _menu.character_list.get_item_metadata(_index)
|
||||
# Colour-tint skins have no model; there is nothing to photograph.
|
||||
_menu.character_list.select(_index)
|
||||
_menu._on_character_selected(_index)
|
||||
if _menu._preview_model != null:
|
||||
_posing = id
|
||||
return
|
||||
_index += 1
|
||||
_posing = ""
|
||||
quit()
|
||||
|
||||
|
||||
func _shoot() -> void:
|
||||
if _posing == "":
|
||||
return
|
||||
var id := _posing
|
||||
var img := root.get_texture().get_image()
|
||||
# Crop to the preview panel — the rest of the frame is the level behind it.
|
||||
var w := img.get_width()
|
||||
var h := img.get_height()
|
||||
img = img.get_region(Rect2i(int(w * 0.44), int(h * 0.15),
|
||||
int(w * 0.32), int(h * 0.68)))
|
||||
var path := "%s/roster_%s.png" % [_out_dir, id]
|
||||
img.save_png(path)
|
||||
print("wrote %s" % path)
|
||||
Reference in New Issue
Block a user