Rework the UI on Crusenho's pack; credit it in the game, not just the repo
ci / verify (push) Successful in 48s
ci / verify (push) Successful in 48s
Crusenho's Complete UI Essential Pack is CC BY 4.0 -- redistributable and
commercial-friendly, confirmed from the License.txt the pack itself ships --
so unlike the two bdragon packs a subset is committed: twelve PNGs, 48 KB,
under assets/sprites/ui/. Only what is used, because each committed PNG costs
a Godot .import sidecar and a directory nothing references is one nobody
prunes.
UiTheme builds a Theme in code from it -- button states, panels, line edits --
and every screen roots itself through UiTheme.themed_root(). The HUD's bars are
the pack's frame with a tinted fill, drawn as three horizontal slices because
Godot's nine-patch lives on nodes and the HUD is drawn rather than built from
controls. Inventory slots use the pack's slot art at exactly twice the source
size; a non-integer scale on a 1px border reads as a wobble along every edge.
The credits screen is the other half of the request and it is a licence
obligation, not a nicety: two packs are now CC BY, which asks for attribution
"in any reasonable manner", and a markdown file in a source repo is not
reasonable for someone who downloaded a build. Settings -> Credits shows every
source with its terms and a link to the licence text. test_credits.gd asserts
CREDITS.md and docs/ASSETS.md name every entry, so the three cannot drift.
Two things found by actually looking at the screen, which is the point:
- The FIRST version of this styled nothing. A Control inherits its theme from
Control ANCESTORS only, and the chain breaks at the first plain Node or
CanvasLayer -- which is every screen here. get_window().theme set the
property, changed nothing, and read as correct. check.sh, 458 tests and a
clean smoke run all passed with the entire interface unstyled. The theme
test now instantiates every screen and asks what its buttons resolve.
- The settings screen showed Fire bound to the right mouse button, because
the test suite was writing the player's real user://settings.cfg --
rebinding calls save() and nothing had redirected the path. Settings.path
is now redirectable, the fixture points it at a scratch file, and a test
asserts the default is still the player's own.
tools/screenshot.tscn is what found both. It boots the client windowed and
saves the menus, the HUD, settings and credits. Manual, needs a display, and
the only thing in the project that can tell you the interface rendered.
check.sh clean, 460 tests, SMOKE PASS, all four diagnostics green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
872922e9e2
commit
132646f6c3
54 files changed
+1319
-50
No files matched your search
@@ -10,6 +10,7 @@ extends Node2D
|
||||
@onready var characters: CanvasLayer = $CharacterSelect
|
||||
@onready var upgrades: CanvasLayer = $UpgradeScreen
|
||||
@onready var settings: CanvasLayer = $SettingsScreen
|
||||
@onready var credits: CanvasLayer = $CreditsScreen
|
||||
|
||||
var _bound: ClientRuntime = null
|
||||
## Opened deliberately from the menu, as opposed to forced open by having no
|
||||
@@ -37,6 +38,8 @@ func _ready() -> void:
|
||||
upgrades.closed.connect(func() -> void: _upgrades_open = false)
|
||||
menu.settings_requested.connect(func() -> void: settings.open())
|
||||
settings.closed.connect(func() -> void: settings.visible = false)
|
||||
settings.credits_requested.connect(func() -> void: credits.open())
|
||||
credits.closed.connect(func() -> void: credits.visible = false)
|
||||
upgrades.choose_requested.connect(func(i: int) -> void: Net.choose_upgrade(i))
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
class_name UiTheme
|
||||
extends RefCounted
|
||||
## The game's control theme, built in code from Crusenho's UI pack.
|
||||
##
|
||||
## Built rather than authored as a .tres for the same reason [Content] is code:
|
||||
## it is a readable diff, there are no resource UIDs churning in version
|
||||
## control, and a test can build it without the editor.
|
||||
##
|
||||
## Applied to each screen's root Control ([method apply_to]). It cannot be
|
||||
## applied once to the Window instead: Godot's theme lookup walks a Control's
|
||||
## ancestors, and the chain BREAKS at the first plain Node -- which for every
|
||||
## screen here is `main.gd` or a CanvasLayer. Setting `window.theme` looked like
|
||||
## it worked and changed nothing.
|
||||
|
||||
const DIR := "res://assets/sprites/ui/"
|
||||
|
||||
## Every texture the theme needs. Named so a test can assert they all load: a
|
||||
## missing file gives a null texture and a silently unstyled control, which
|
||||
## looks like the theme never being applied at all.
|
||||
const TEXTURES := {
|
||||
"button": DIR + "Button01a_1.png",
|
||||
"button_hover": DIR + "Button01a_2.png",
|
||||
"button_pressed": DIR + "Button01a_3.png",
|
||||
"button_disabled": DIR + "Button01a_4.png",
|
||||
"panel": DIR + "Frame01a.png",
|
||||
"panel_accent": DIR + "Frame02a.png",
|
||||
"slot": DIR + "FrameSlot01a.png",
|
||||
"slot_active": DIR + "FrameSlot01b.png",
|
||||
"bar": DIR + "Bar01a.png",
|
||||
"bar_fill": DIR + "BarFill01f.png",
|
||||
"field": DIR + "InputField01a.png",
|
||||
"banner": DIR + "Banner01a.png",
|
||||
}
|
||||
|
||||
## The art is cream with a black outline, so text on it has to be dark. Every
|
||||
## label the game draws over the dark world stays light; only controls that sit
|
||||
## on this pack's own surfaces flip.
|
||||
const INK := Color(0.12, 0.11, 0.13)
|
||||
const INK_DIM := Color(0.42, 0.40, 0.44)
|
||||
|
||||
static var _theme: Theme = null
|
||||
|
||||
|
||||
static func texture(key: String) -> Texture2D:
|
||||
return load(TEXTURES[key]) as Texture2D
|
||||
|
||||
|
||||
## One shared instance. Themes are pure data and every control only reads them,
|
||||
## so building a second would be waste rather than isolation.
|
||||
static func get_theme() -> Theme:
|
||||
if _theme != null:
|
||||
return _theme
|
||||
var t := Theme.new()
|
||||
t.default_font_size = 14
|
||||
|
||||
t.set_stylebox("normal", "Button", _box("button", 8, 12, 7))
|
||||
t.set_stylebox("hover", "Button", _box("button_hover", 8, 12, 7))
|
||||
t.set_stylebox("pressed", "Button", _box("button_pressed", 8, 12, 7))
|
||||
t.set_stylebox("disabled", "Button", _box("button_disabled", 8, 12, 7))
|
||||
# Focus deliberately reuses the hover art rather than adding an outline:
|
||||
# the pack has no focus state, and an invented one would be the only thing
|
||||
# on screen that is not from the set.
|
||||
t.set_stylebox("focus", "Button", _box("button_hover", 8, 12, 7))
|
||||
t.set_color("font_color", "Button", INK)
|
||||
t.set_color("font_hover_color", "Button", INK)
|
||||
t.set_color("font_pressed_color", "Button", INK)
|
||||
t.set_color("font_disabled_color", "Button", INK_DIM)
|
||||
t.set_color("font_focus_color", "Button", INK)
|
||||
|
||||
t.set_stylebox("panel", "Panel", _box("panel", 8, 10, 10))
|
||||
t.set_stylebox("panel", "PanelContainer", _box("panel", 8, 10, 10))
|
||||
|
||||
t.set_stylebox("normal", "LineEdit", _box("field", 8, 10, 6))
|
||||
t.set_stylebox("focus", "LineEdit", _box("field", 8, 10, 6))
|
||||
t.set_color("font_color", "LineEdit", INK)
|
||||
t.set_color("font_placeholder_color", "LineEdit", INK_DIM)
|
||||
t.set_color("caret_color", "LineEdit", INK)
|
||||
|
||||
_theme = t
|
||||
return _theme
|
||||
|
||||
|
||||
## Give [param root] the theme. Everything under it inherits, so one call per
|
||||
## screen covers that screen.
|
||||
##
|
||||
## Per-screen rather than once on the Window, because theme inheritance only
|
||||
## travels Control-to-Control: a Control whose parent is a plain Node or a
|
||||
## CanvasLayer resolves against the default theme however the Window is set.
|
||||
## Every screen in this game hangs off one or the other.
|
||||
static func apply_to(root: Control) -> void:
|
||||
if root != null:
|
||||
root.theme = get_theme()
|
||||
|
||||
|
||||
## A full-rect Control with the theme already on it -- the root every screen
|
||||
## built in code starts from. Using this rather than a bare Control is what
|
||||
## stops a new screen quietly rendering unstyled.
|
||||
static func themed_root() -> Control:
|
||||
var root := Control.new()
|
||||
root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
apply_to(root)
|
||||
return root
|
||||
|
||||
|
||||
## A nine-patch stylebox. [param margin] is the corner size in source pixels;
|
||||
## the middle is what stretches.
|
||||
static func _box(key: String, margin: int, pad_x: int, pad_y: int) -> StyleBoxTexture:
|
||||
var sb := StyleBoxTexture.new()
|
||||
sb.texture = texture(key)
|
||||
for side in [SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM]:
|
||||
sb.set_texture_margin(side, float(margin))
|
||||
sb.set_content_margin(SIDE_LEFT, float(pad_x))
|
||||
sb.set_content_margin(SIDE_RIGHT, float(pad_x))
|
||||
sb.set_content_margin(SIDE_TOP, float(pad_y))
|
||||
sb.set_content_margin(SIDE_BOTTOM, float(pad_y))
|
||||
return sb
|
||||
@@ -0,0 +1 @@
|
||||
uid://ddaj0c8xfjitx
|
||||
Reference in new issue
Block a user