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,71 @@
|
||||
extends Control
|
||||
class_name BoltRule
|
||||
|
||||
## A lightning bolt struck through a horizontal rule.
|
||||
##
|
||||
## The motif the whole UI is built around, in one reusable widget: menus use it
|
||||
## as a section divider, the HUD uses it to underline a heading. It is drawn
|
||||
## rather than shipped as an image so it takes the theme's colours and scales to
|
||||
## any width without a second asset.
|
||||
##
|
||||
## The zigzag is deliberately not centred. A bolt centred in its rule reads as an
|
||||
## ornament; struck a third of the way along, it reads as something that HIT the
|
||||
## line — which is the difference between decorative and energetic.
|
||||
|
||||
@export var line_color: Color = Color(1.0, 0.47, 0.10)
|
||||
@export var bolt_color: Color = Color(1.0, 0.93, 0.22)
|
||||
@export var ink: Color = Color(0.045, 0.040, 0.075)
|
||||
## Where along the rule the bolt strikes, 0..1.
|
||||
@export var strike_at: float = 0.34
|
||||
@export var thickness: float = 3.0
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
custom_minimum_size = Vector2(0, 22)
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var w := size.x
|
||||
var h := size.y
|
||||
if w <= 1.0:
|
||||
return
|
||||
var mid := h * 0.5
|
||||
var strike := w * clampf(strike_at, 0.05, 0.95)
|
||||
var half := h * 0.42
|
||||
|
||||
# The rule, broken where the bolt lands so the bolt reads as passing
|
||||
# THROUGH it rather than sitting on top.
|
||||
var gap := h * 0.55
|
||||
draw_line(Vector2(0, mid), Vector2(maxf(0.0, strike - gap), mid),
|
||||
line_color, thickness)
|
||||
draw_line(Vector2(minf(w, strike + gap), mid), Vector2(w, mid),
|
||||
line_color, thickness)
|
||||
|
||||
# Bolt: down-right, kick back left, down-right again.
|
||||
var pts := PackedVector2Array([
|
||||
Vector2(strike + half * 0.55, mid - half),
|
||||
Vector2(strike - half * 0.10, mid + half * 0.12),
|
||||
Vector2(strike + half * 0.30, mid + half * 0.12),
|
||||
Vector2(strike - half * 0.55, mid + half),
|
||||
Vector2(strike + half * 0.12, mid - half * 0.10),
|
||||
Vector2(strike - half * 0.28, mid - half * 0.10),
|
||||
])
|
||||
# Ink first, one step out, so the bolt keeps a drawn edge against any
|
||||
# background it is placed on.
|
||||
draw_colored_polygon(_grown(pts, 2.0), ink)
|
||||
draw_colored_polygon(pts, bolt_color)
|
||||
|
||||
|
||||
## The same polygon pushed out from its own centre — a cheap outline that needs
|
||||
## no second point list to maintain.
|
||||
func _grown(pts: PackedVector2Array, by: float) -> PackedVector2Array:
|
||||
var centre := Vector2.ZERO
|
||||
for p in pts:
|
||||
centre += p
|
||||
centre /= float(pts.size())
|
||||
var out := PackedVector2Array()
|
||||
for p in pts:
|
||||
var dir := (p - centre)
|
||||
out.append(p + (dir.normalized() * by if dir.length() > 0.001 else Vector2.ZERO))
|
||||
return out
|
||||
Reference in New Issue
Block a user