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,123 @@
|
||||
extends SceneTree
|
||||
|
||||
## Does the escape menu's character picker actually work?
|
||||
##
|
||||
## It is built entirely in code, in an autoload, over a paused tree — three
|
||||
## things that each hide their own class of mistake and none of which a compile
|
||||
## check catches. So: open it, walk every entry, and assert that each one
|
||||
## selects, describes itself, and builds a real model with a real skeleton.
|
||||
##
|
||||
## godot --headless --path . -s res://debug/character_picker_check.gd
|
||||
|
||||
var _fails: int = 0
|
||||
|
||||
func _init() -> void:
|
||||
root.call_deferred("add_child", Node.new()) # let autoloads finish _ready
|
||||
await process_frame
|
||||
await process_frame
|
||||
|
||||
var menu = root.get_node_or_null("PauseMenu")
|
||||
_check(menu != null, "PauseMenu autoload exists")
|
||||
if menu == null:
|
||||
_done()
|
||||
return
|
||||
|
||||
_check(menu.character_btn != null, "Character button exists on the pause menu")
|
||||
_check(menu.character_list != null, "Character list exists")
|
||||
_check(menu.character_editor != null, "Character screen exists")
|
||||
|
||||
menu._show_character()
|
||||
await process_frame
|
||||
_check(menu.character_editor.visible, "Character screen shows")
|
||||
_check(not menu.main_vbox.visible, "Main pause list hides behind it")
|
||||
|
||||
# Autoload singletons are not resolvable as identifiers from a `-s` SceneTree
|
||||
# script — it is compiled before they register — so reach it by path.
|
||||
var skin_mgr = root.get_node("SkinManager")
|
||||
|
||||
var count: int = menu.character_list.item_count
|
||||
_check(count > 0, "Roster is not empty (%d entries)" % count)
|
||||
|
||||
var seen_glb := 0
|
||||
for i in count:
|
||||
var id: String = menu.character_list.get_item_metadata(i)
|
||||
menu._on_character_selected(i)
|
||||
await process_frame
|
||||
await process_frame
|
||||
_check(menu.character_desc.text != "", "'%s' has a description line" % id)
|
||||
|
||||
var skin = skin_mgr.get_skin(id)
|
||||
var expects_model: bool = skin.model_path != "" \
|
||||
and ResourceLoader.exists(skin.model_path)
|
||||
if not expects_model:
|
||||
# A colour-tint skin has no GLB. The preview must be EMPTY, not the
|
||||
# previously selected character left standing there.
|
||||
_check(menu._preview_model == null,
|
||||
"'%s' is a colour skin and clears the preview" % id)
|
||||
continue
|
||||
seen_glb += 1
|
||||
var model = menu._preview_model
|
||||
_check(model != null, "'%s' builds a preview model" % id)
|
||||
if model == null:
|
||||
continue
|
||||
_check(model.loaded, "'%s' preview finished loading" % id)
|
||||
_check(model.skeleton != null, "'%s' preview has a skeleton" % id)
|
||||
_check(model.animation_player != null, "'%s' preview has animations" % id)
|
||||
_check(model.surface_table() != null and not model.surface_table().is_empty(),
|
||||
"'%s' preview knows its surface classes" % id)
|
||||
var body: Array = model.surfaces_of(SkinSurfaces.BODY)
|
||||
_check(not body.is_empty(), "'%s' preview reports body surfaces" % id)
|
||||
# A preview that is not ANIMATING is a preview of the bind pose, which
|
||||
# is the one pose the character will never be in during play. The clip
|
||||
# NAME is not evidence of that — it is a variable this class sets on
|
||||
# itself, and it reads "Idle" just as happily when the animation tree
|
||||
# is not ticking at all. So watch the skeleton move.
|
||||
_check(model.current_clip_debug() == "Idle",
|
||||
"'%s' preview selected Idle (got '%s')"
|
||||
% [id, model.current_clip_debug()])
|
||||
_check(await _pose_moves(model),
|
||||
"'%s' preview skeleton is actually animating" % id)
|
||||
|
||||
_check(seen_glb >= 6, "every shipping GLB skin previewed (%d)" % seen_glb)
|
||||
|
||||
# Back out, and make sure the turntable stops costing frames.
|
||||
menu._show_main_menu()
|
||||
await process_frame
|
||||
_check(not menu.character_editor.visible, "Back returns to the pause list")
|
||||
_check(menu.main_vbox.visible, "Pause list is showing again")
|
||||
|
||||
_done()
|
||||
|
||||
|
||||
## Does the skeleton's pose change over a handful of frames?
|
||||
##
|
||||
## Sampled from INSIDE the modifier pass would be better, but the question here
|
||||
## is only "is anything driving this at all", and for that the animated pose is
|
||||
## the right thing to read: if the AnimationTree is not ticking, every bone
|
||||
## holds still and this returns false.
|
||||
func _pose_moves(model) -> bool:
|
||||
var skel: Skeleton3D = model.skeleton
|
||||
if skel == null or skel.get_bone_count() == 0:
|
||||
return false
|
||||
var before: Array = []
|
||||
for b in skel.get_bone_count():
|
||||
before.append(skel.get_bone_pose_rotation(b))
|
||||
for _i in 12:
|
||||
await process_frame
|
||||
for b in skel.get_bone_count():
|
||||
if not skel.get_bone_pose_rotation(b).is_equal_approx(before[b]):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _check(ok: bool, what: String) -> void:
|
||||
if ok:
|
||||
print(" OK: %s" % what)
|
||||
else:
|
||||
print(" FAIL: %s" % what)
|
||||
_fails += 1
|
||||
|
||||
|
||||
func _done() -> void:
|
||||
print("\n=== CHARACTER PICKER ===\nFailures: %d" % _fails)
|
||||
quit(1 if _fails > 0 else 0)
|
||||
Reference in New Issue
Block a user