Compare commits
13 Commits
29474e5751
...
322aae4a33
| Author | SHA1 | Date | |
|---|---|---|---|
| 322aae4a33 | |||
| 7bbb66827b | |||
| 4a32e7df8a | |||
| 0c12e13ac1 | |||
| 6530625f4d | |||
| 0082765ee7 | |||
| 7954c579d3 | |||
| 73fcf2d096 | |||
| 9091707535 | |||
| 3981047ec6 | |||
| 888b2b32f5 | |||
| 233b1088cf | |||
| 2d7ed469bf |
BIN
client/assets/theme/fonts/ArchitectsDaughter-Regular.ttf
Normal file
BIN
client/assets/theme/fonts/ArchitectsDaughter-Regular.ttf
Normal file
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
[remap]
|
||||
|
||||
importer="font_data_dynamic"
|
||||
type="FontFile"
|
||||
uid="uid://dgftorjwjkcx6"
|
||||
path="res://.godot/imported/ArchitectsDaughter-Regular.ttf-39fe9a11a65cddcd9ceecf51a839f0c5.fontdata"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/theme/fonts/ArchitectsDaughter-Regular.ttf"
|
||||
dest_files=["res://.godot/imported/ArchitectsDaughter-Regular.ttf-39fe9a11a65cddcd9ceecf51a839f0c5.fontdata"]
|
||||
|
||||
[params]
|
||||
|
||||
Rendering=null
|
||||
antialiasing=1
|
||||
generate_mipmaps=false
|
||||
disable_embedded_bitmaps=true
|
||||
multichannel_signed_distance_field=false
|
||||
msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=3
|
||||
subpixel_positioning=4
|
||||
keep_rounding_remainders=true
|
||||
oversampling=0.0
|
||||
Fallbacks=null
|
||||
fallbacks=[]
|
||||
Compress=null
|
||||
compress=true
|
||||
preload=[]
|
||||
language_support={}
|
||||
script_support={}
|
||||
opentype_features={}
|
||||
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
[remap]
|
||||
|
||||
importer="font_data_dynamic"
|
||||
type="FontFile"
|
||||
uid="uid://bosx65t7inqbs"
|
||||
path="res://.godot/imported/EBGaramond-Italic-VariableFont_wght.ttf-8a9f89c45d1261f74ac5e087dfe5ec85.fontdata"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/theme/fonts/EBGaramond-Italic-VariableFont_wght.ttf"
|
||||
dest_files=["res://.godot/imported/EBGaramond-Italic-VariableFont_wght.ttf-8a9f89c45d1261f74ac5e087dfe5ec85.fontdata"]
|
||||
|
||||
[params]
|
||||
|
||||
Rendering=null
|
||||
antialiasing=1
|
||||
generate_mipmaps=false
|
||||
disable_embedded_bitmaps=true
|
||||
multichannel_signed_distance_field=false
|
||||
msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=3
|
||||
subpixel_positioning=4
|
||||
keep_rounding_remainders=true
|
||||
oversampling=0.0
|
||||
Fallbacks=null
|
||||
fallbacks=[]
|
||||
Compress=null
|
||||
compress=true
|
||||
preload=[]
|
||||
language_support={}
|
||||
script_support={}
|
||||
opentype_features={}
|
||||
BIN
client/assets/theme/fonts/EBGaramond-VariableFont_wght.ttf
Normal file
BIN
client/assets/theme/fonts/EBGaramond-VariableFont_wght.ttf
Normal file
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
[remap]
|
||||
|
||||
importer="font_data_dynamic"
|
||||
type="FontFile"
|
||||
uid="uid://b00hfuuyxctc6"
|
||||
path="res://.godot/imported/EBGaramond-VariableFont_wght.ttf-32cc8b7204b14062fe5657f6ee3ae93b.fontdata"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/theme/fonts/EBGaramond-VariableFont_wght.ttf"
|
||||
dest_files=["res://.godot/imported/EBGaramond-VariableFont_wght.ttf-32cc8b7204b14062fe5657f6ee3ae93b.fontdata"]
|
||||
|
||||
[params]
|
||||
|
||||
Rendering=null
|
||||
antialiasing=1
|
||||
generate_mipmaps=false
|
||||
disable_embedded_bitmaps=true
|
||||
multichannel_signed_distance_field=false
|
||||
msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=3
|
||||
subpixel_positioning=4
|
||||
keep_rounding_remainders=true
|
||||
oversampling=0.0
|
||||
Fallbacks=null
|
||||
fallbacks=[]
|
||||
Compress=null
|
||||
compress=true
|
||||
preload=[]
|
||||
language_support={}
|
||||
script_support={}
|
||||
opentype_features={}
|
||||
BIN
client/assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf
Normal file
BIN
client/assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf
Normal file
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
[remap]
|
||||
|
||||
importer="font_data_dynamic"
|
||||
type="FontFile"
|
||||
uid="uid://1ge2menmepj8"
|
||||
path="res://.godot/imported/JetBrainsMono-VariableFont_wght.ttf-9b8836092a0b124cd7b65994f8da4002.fontdata"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf"
|
||||
dest_files=["res://.godot/imported/JetBrainsMono-VariableFont_wght.ttf-9b8836092a0b124cd7b65994f8da4002.fontdata"]
|
||||
|
||||
[params]
|
||||
|
||||
Rendering=null
|
||||
antialiasing=1
|
||||
generate_mipmaps=false
|
||||
disable_embedded_bitmaps=true
|
||||
multichannel_signed_distance_field=false
|
||||
msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=3
|
||||
subpixel_positioning=4
|
||||
keep_rounding_remainders=true
|
||||
oversampling=0.0
|
||||
Fallbacks=null
|
||||
fallbacks=[]
|
||||
Compress=null
|
||||
compress=true
|
||||
preload=[]
|
||||
language_support={}
|
||||
script_support={}
|
||||
opentype_features={}
|
||||
353
client/assets/theme/game_theme.tres
Normal file
353
client/assets/theme/game_theme.tres
Normal file
@@ -0,0 +1,353 @@
|
||||
[gd_resource type="Theme" format=3]
|
||||
|
||||
[ext_resource type="FontFile" path="res://assets/theme/fonts/ArchitectsDaughter-Regular.ttf" id="1_cu4xe"]
|
||||
[ext_resource type="FontFile" path="res://assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf" id="2_605w4"]
|
||||
[ext_resource type="FontFile" path="res://assets/theme/fonts/EBGaramond-VariableFont_wght.ttf" id="3_4lwr2"]
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_eckwq"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_i0b23"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.6039216, 0.56078434, 0.47058824, 0.22)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.6039216, 0.56078434, 0.47058824, 1)
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_cqbvn"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.6039216, 0.56078434, 0.47058824, 0.12)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.6039216, 0.56078434, 0.47058824, 1)
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q5gpk"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.6039216, 0.56078434, 0.47058824, 0.32)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.6039216, 0.56078434, 0.47058824, 1)
|
||||
corner_radius_top_left = 3
|
||||
corner_radius_top_right = 3
|
||||
corner_radius_bottom_right = 3
|
||||
corner_radius_bottom_left = 3
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bbpmc"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.07058824, 0.05882353, 0.043137256, 0.7)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.22745098, 0.20784314, 0.18039216, 1)
|
||||
corner_radius_top_left = 4
|
||||
corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_73lme"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.9019608, 0.8509804, 0.72156864, 1)
|
||||
border_width_left = 2
|
||||
border_width_top = 2
|
||||
border_width_right = 2
|
||||
border_width_bottom = 2
|
||||
border_color = Color(0.7490196, 0.65882355, 0.47058824, 1)
|
||||
corner_radius_top_left = 6
|
||||
corner_radius_top_right = 6
|
||||
corner_radius_bottom_right = 6
|
||||
corner_radius_bottom_left = 6
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_js3tr"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.8509804, 0.7882353, 0.63529414, 1)
|
||||
draw_center = false
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.6627451, 0.5803922, 0.39215687, 1)
|
||||
corner_radius_top_left = 6
|
||||
corner_radius_top_right = 6
|
||||
corner_radius_bottom_right = 6
|
||||
corner_radius_bottom_left = 6
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ube3r"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.94509804, 0.9098039, 0.8235294, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.7490196, 0.65882355, 0.47058824, 1)
|
||||
corner_radius_top_left = 6
|
||||
corner_radius_top_right = 6
|
||||
corner_radius_bottom_right = 6
|
||||
corner_radius_bottom_left = 6
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2tooq"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.9019608, 0.8509804, 0.72156864, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.6627451, 0.5803922, 0.39215687, 1)
|
||||
corner_radius_top_left = 4
|
||||
corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_4kten"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.47843137, 0.4392157, 0.3764706, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.47843137, 0.4392157, 0.3764706, 1)
|
||||
corner_radius_top_left = 4
|
||||
corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_p6wtm"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.72156864, 0.32156864, 0.2901961, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.56078434, 0.22745098, 0.20392157, 1)
|
||||
corner_radius_top_left = 4
|
||||
corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_0fjdt"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.56078434, 0.22745098, 0.20392157, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.56078434, 0.22745098, 0.20392157, 1)
|
||||
corner_radius_top_left = 4
|
||||
corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ns3kg"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.47843137, 0.18431373, 0.16078432, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.56078434, 0.22745098, 0.20392157, 1)
|
||||
corner_radius_top_left = 4
|
||||
corner_radius_top_right = 4
|
||||
corner_radius_bottom_right = 4
|
||||
corner_radius_bottom_left = 4
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_jly1r"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_lw8lk"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.94509804, 0.9098039, 0.8235294, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.79607844, 0.7137255, 0.5176471, 1)
|
||||
corner_radius_top_left = 14
|
||||
corner_radius_top_right = 14
|
||||
corner_radius_bottom_right = 14
|
||||
corner_radius_bottom_left = 14
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_nh0e7"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.9372549, 0.9019608, 0.8117647, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.79607844, 0.7137255, 0.5176471, 1)
|
||||
corner_radius_top_left = 14
|
||||
corner_radius_top_right = 14
|
||||
corner_radius_bottom_right = 14
|
||||
corner_radius_bottom_left = 14
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dtr07"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.9019608, 0.8509804, 0.72156864, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.7490196, 0.65882355, 0.47058824, 1)
|
||||
corner_radius_top_left = 14
|
||||
corner_radius_top_right = 14
|
||||
corner_radius_bottom_right = 14
|
||||
corner_radius_bottom_left = 14
|
||||
|
||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_nmowx"]
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_oi067"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.72156864, 0.32156864, 0.2901961, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.72156864, 0.32156864, 0.2901961, 1)
|
||||
corner_radius_top_left = 14
|
||||
corner_radius_top_right = 14
|
||||
corner_radius_bottom_right = 14
|
||||
corner_radius_bottom_left = 14
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ukblj"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.56078434, 0.22745098, 0.20392157, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.56078434, 0.22745098, 0.20392157, 1)
|
||||
corner_radius_top_left = 14
|
||||
corner_radius_top_right = 14
|
||||
corner_radius_bottom_right = 14
|
||||
corner_radius_bottom_left = 14
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_utt73"]
|
||||
content_margin_left = 12.0
|
||||
content_margin_top = 6.0
|
||||
content_margin_right = 12.0
|
||||
content_margin_bottom = 6.0
|
||||
bg_color = Color(0.47843137, 0.18431373, 0.16078432, 1)
|
||||
border_width_left = 1
|
||||
border_width_top = 1
|
||||
border_width_right = 1
|
||||
border_width_bottom = 1
|
||||
border_color = Color(0.56078434, 0.22745098, 0.20392157, 1)
|
||||
corner_radius_top_left = 14
|
||||
corner_radius_top_right = 14
|
||||
corner_radius_bottom_right = 14
|
||||
corner_radius_bottom_left = 14
|
||||
|
||||
[resource]
|
||||
default_font = ExtResource("3_4lwr2")
|
||||
default_font_size = 18
|
||||
Accent/base_type = &"Label"
|
||||
Accent/colors/font_color = Color(0.56078434, 0.41568628, 0.16470589, 1)
|
||||
Accent/font_sizes/font_size = 24
|
||||
Accent/fonts/font = ExtResource("1_cu4xe")
|
||||
Chip/base_type = &"Button"
|
||||
Chip/colors/font_color = Color(0.6039216, 0.56078434, 0.47058824, 1)
|
||||
Chip/font_sizes/font_size = 11
|
||||
Chip/fonts/font = ExtResource("2_605w4")
|
||||
Chip/styles/focus = SubResource("StyleBoxEmpty_eckwq")
|
||||
Chip/styles/hover = SubResource("StyleBoxFlat_i0b23")
|
||||
Chip/styles/normal = SubResource("StyleBoxFlat_cqbvn")
|
||||
Chip/styles/pressed = SubResource("StyleBoxFlat_q5gpk")
|
||||
DarkPanel/base_type = &"PanelContainer"
|
||||
DarkPanel/styles/panel = SubResource("StyleBoxFlat_bbpmc")
|
||||
Heading/base_type = &"Label"
|
||||
Heading/colors/font_color = Color(0.22745098, 0.18431373, 0.10980392, 1)
|
||||
Heading/font_sizes/font_size = 34
|
||||
Heading/fonts/font = ExtResource("3_4lwr2")
|
||||
ItemTile/base_type = &"PanelContainer"
|
||||
ItemTile/styles/panel = SubResource("StyleBoxFlat_73lme")
|
||||
ItemTileEmpty/base_type = &"PanelContainer"
|
||||
ItemTileEmpty/styles/panel = SubResource("StyleBoxFlat_js3tr")
|
||||
Mono/base_type = &"Label"
|
||||
Mono/colors/font_color = Color(0.6039216, 0.56078434, 0.47058824, 1)
|
||||
Mono/font_sizes/font_size = 13
|
||||
Mono/fonts/font = ExtResource("2_605w4")
|
||||
ParchmentCard/base_type = &"PanelContainer"
|
||||
ParchmentCard/styles/panel = SubResource("StyleBoxFlat_ube3r")
|
||||
ParchmentInset/base_type = &"PanelContainer"
|
||||
ParchmentInset/styles/panel = SubResource("StyleBoxFlat_2tooq")
|
||||
PrimaryCTA/base_type = &"Button"
|
||||
PrimaryCTA/colors/font_color = Color(0.9411765, 0.9019608, 0.8156863, 1)
|
||||
PrimaryCTA/colors/font_disabled_color = Color(0.6039216, 0.56078434, 0.47058824, 1)
|
||||
PrimaryCTA/styles/disabled = SubResource("StyleBoxFlat_4kten")
|
||||
PrimaryCTA/styles/hover = SubResource("StyleBoxFlat_p6wtm")
|
||||
PrimaryCTA/styles/normal = SubResource("StyleBoxFlat_0fjdt")
|
||||
PrimaryCTA/styles/pressed = SubResource("StyleBoxFlat_ns3kg")
|
||||
Tab/base_type = &"Button"
|
||||
Tab/colors/font_color = Color(0.41568628, 0.3529412, 0.22745098, 1)
|
||||
Tab/styles/focus = SubResource("StyleBoxEmpty_jly1r")
|
||||
Tab/styles/hover = SubResource("StyleBoxFlat_lw8lk")
|
||||
Tab/styles/normal = SubResource("StyleBoxFlat_nh0e7")
|
||||
Tab/styles/pressed = SubResource("StyleBoxFlat_dtr07")
|
||||
TabActive/base_type = &"Button"
|
||||
TabActive/colors/font_color = Color(0.9411765, 0.9019608, 0.8156863, 1)
|
||||
TabActive/styles/focus = SubResource("StyleBoxEmpty_nmowx")
|
||||
TabActive/styles/hover = SubResource("StyleBoxFlat_oi067")
|
||||
TabActive/styles/normal = SubResource("StyleBoxFlat_ukblj")
|
||||
TabActive/styles/pressed = SubResource("StyleBoxFlat_utt73")
|
||||
21
client/assets/theme/shaders/dark_bay.gdshader
Normal file
21
client/assets/theme/shaders/dark_bay.gdshader
Normal file
@@ -0,0 +1,21 @@
|
||||
shader_type canvas_item;
|
||||
uniform vec4 stripe_a : source_color;
|
||||
uniform vec4 stripe_b : source_color;
|
||||
uniform float band_px = 16.0; // 16px bands per README
|
||||
uniform float vignette = 0.55; // edge darkening strength
|
||||
void fragment() {
|
||||
// Coordinate spaces differ on purpose: the stripe uses FRAGCOORD (screen-space,
|
||||
// not node-local), while the vignette below uses UV (node-local, 0..1 across
|
||||
// this node's rect). An offset DarkBay instance therefore keeps its stripes
|
||||
// anchored to the screen rather than restarting at the node's own origin —
|
||||
// intended for full-bleed use.
|
||||
// 135deg diagonal stripe: sum of pixel coords, banded.
|
||||
vec2 px = FRAGCOORD.xy;
|
||||
float diag = px.x + px.y;
|
||||
float band = mod(floor(diag / band_px), 2.0);
|
||||
vec4 base = mix(stripe_a, stripe_b, band);
|
||||
// radial vignette centred slightly high (README: 50% 38%).
|
||||
float d = distance(UV, vec2(0.5, 0.38));
|
||||
base.rgb *= 1.0 - clamp(d * vignette, 0.0, 1.0);
|
||||
COLOR = base;
|
||||
}
|
||||
1
client/assets/theme/shaders/dark_bay.gdshader.uid
Normal file
1
client/assets/theme/shaders/dark_bay.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://jb8clyworwr1
|
||||
6
client/assets/theme/shaders/parchment.gdshader
Normal file
6
client/assets/theme/shaders/parchment.gdshader
Normal file
@@ -0,0 +1,6 @@
|
||||
shader_type canvas_item;
|
||||
uniform vec4 top : source_color;
|
||||
uniform vec4 bottom : source_color;
|
||||
void fragment() {
|
||||
COLOR = mix(top, bottom, UV.y);
|
||||
}
|
||||
1
client/assets/theme/shaders/parchment.gdshader.uid
Normal file
1
client/assets/theme/shaders/parchment.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b2heb6jhvwxxl
|
||||
6
client/assets/theme/shaders/vignette.gdshader
Normal file
6
client/assets/theme/shaders/vignette.gdshader
Normal file
@@ -0,0 +1,6 @@
|
||||
shader_type canvas_item;
|
||||
uniform float strength = 0.62;
|
||||
void fragment() {
|
||||
float d = distance(UV, vec2(0.5));
|
||||
COLOR = vec4(0.0, 0.0, 0.0, clamp(d * strength, 0.0, 1.0));
|
||||
}
|
||||
1
client/assets/theme/shaders/vignette.gdshader.uid
Normal file
1
client/assets/theme/shaders/vignette.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b4tami4peyxaw
|
||||
@@ -13,3 +13,15 @@ config_version=5
|
||||
config/name="coc-rpg"
|
||||
config/description="AI-driven single-player party RPG. Code owns state. AI owns text."
|
||||
config/features=PackedStringArray("4.7")
|
||||
|
||||
[display]
|
||||
|
||||
; Design/logical canvas stays 1920x1080 (charter §16, mockups authored here).
|
||||
window/size/viewport_width=1920
|
||||
window/size/viewport_height=1080
|
||||
; Open a smaller 1600x900 window that scales the 1920x1080 canvas down, so it
|
||||
; doesn't fill the whole screen. canvas_items + keep aspect does the scaling.
|
||||
window/size/window_width_override=1600
|
||||
window/size/window_height_override=900
|
||||
window/stretch/mode="canvas_items"
|
||||
window/stretch/aspect="keep"
|
||||
|
||||
5
client/scenes/theme/surfaces/DarkBay.tscn
Normal file
5
client/scenes/theme/surfaces/DarkBay.tscn
Normal file
@@ -0,0 +1,5 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://scripts/theme/surfaces/dark_bay.gd" id="1"]
|
||||
[node name="DarkBay" type="Panel"]
|
||||
anchors_preset = 15
|
||||
script = ExtResource("1")
|
||||
5
client/scenes/theme/surfaces/ParchmentPanel.tscn
Normal file
5
client/scenes/theme/surfaces/ParchmentPanel.tscn
Normal file
@@ -0,0 +1,5 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://scripts/theme/surfaces/parchment_panel.gd" id="1"]
|
||||
[node name="ParchmentPanel" type="Panel"]
|
||||
anchors_preset = 15
|
||||
script = ExtResource("1")
|
||||
5
client/scenes/theme/surfaces/VignetteOverlay.tscn
Normal file
5
client/scenes/theme/surfaces/VignetteOverlay.tscn
Normal file
@@ -0,0 +1,5 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://scripts/theme/surfaces/vignette_overlay.gd" id="1"]
|
||||
[node name="VignetteOverlay" type="ColorRect"]
|
||||
anchors_preset = 15
|
||||
script = ExtResource("1")
|
||||
5
client/scenes/theme/theme_showcase.tscn
Normal file
5
client/scenes/theme/theme_showcase.tscn
Normal file
@@ -0,0 +1,5 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://scripts/theme/theme_showcase.gd" id="1"]
|
||||
[node name="ThemeShowcase" type="Control"]
|
||||
anchors_preset = 15
|
||||
script = ExtResource("1")
|
||||
138
client/scripts/theme/build_game_theme.gd
Normal file
138
client/scripts/theme/build_game_theme.gd
Normal file
@@ -0,0 +1,138 @@
|
||||
# client/scripts/theme/build_game_theme.gd
|
||||
@tool
|
||||
extends SceneTree
|
||||
## One-shot Theme builder. Run headless:
|
||||
## godot --headless -s res://scripts/theme/build_game_theme.gd
|
||||
## Constructs game_theme.tres from Palette + the bundled fonts and saves it.
|
||||
## The builder is the source of truth; the .tres is a committed artifact.
|
||||
##
|
||||
## build_theme() is a static func so it is reachable without saving/quitting —
|
||||
## tests preload this script and call Builder.build_theme() directly to compare
|
||||
## a fresh in-memory Theme against the committed .tres (drift guard). Preloading
|
||||
## a script and calling a static func does not run _init() — only `.new()` does
|
||||
## — so this is safe to call from a test without side effects.
|
||||
## §2: presentation.
|
||||
|
||||
|
||||
const OUT := "res://assets/theme/game_theme.tres"
|
||||
const SERIF := "res://assets/theme/fonts/EBGaramond-VariableFont_wght.ttf"
|
||||
const ACCENT := "res://assets/theme/fonts/ArchitectsDaughter-Regular.ttf"
|
||||
const MONO := "res://assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf"
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var t := build_theme()
|
||||
var err := ResourceSaver.save(t, OUT)
|
||||
if err != OK:
|
||||
push_error("theme save failed: %d" % err)
|
||||
else:
|
||||
print("wrote ", OUT)
|
||||
quit()
|
||||
|
||||
|
||||
static func build_theme() -> Theme:
|
||||
var theme := Theme.new()
|
||||
_fonts(theme)
|
||||
_primary_cta(theme)
|
||||
_tabs(theme)
|
||||
_chip(theme)
|
||||
_tiles(theme)
|
||||
_cards_and_panels(theme)
|
||||
return theme
|
||||
|
||||
|
||||
static func _flat(bg: Color, border: Color, width := 1, radius := 4) -> StyleBoxFlat:
|
||||
var s := StyleBoxFlat.new()
|
||||
s.bg_color = bg
|
||||
s.border_color = border
|
||||
s.set_border_width_all(width)
|
||||
s.set_corner_radius_all(radius)
|
||||
s.content_margin_left = 12
|
||||
s.content_margin_right = 12
|
||||
s.content_margin_top = 6
|
||||
s.content_margin_bottom = 6
|
||||
return s
|
||||
|
||||
|
||||
static func _fonts(theme: Theme) -> void:
|
||||
var serif: FontFile = load(SERIF)
|
||||
# Global defaults: prose serif at body size.
|
||||
theme.default_font = serif
|
||||
theme.default_font_size = 18
|
||||
# Heading role via the Label "Heading" variation.
|
||||
theme.set_type_variation(ThemeKeys.HEADING, "Label")
|
||||
theme.set_font(&"font", ThemeKeys.HEADING, serif)
|
||||
theme.set_font_size(&"font_size", ThemeKeys.HEADING, 34)
|
||||
theme.set_color(&"font_color", ThemeKeys.HEADING, Palette.INK_HEADING)
|
||||
# Accent (hand-inked) role.
|
||||
theme.set_type_variation(ThemeKeys.ACCENT, "Label")
|
||||
theme.set_font(&"font", ThemeKeys.ACCENT, load(ACCENT))
|
||||
theme.set_font_size(&"font_size", ThemeKeys.ACCENT, 24)
|
||||
theme.set_color(&"font_color", ThemeKeys.ACCENT, Palette.INK_GOLD)
|
||||
# Mono chrome role.
|
||||
theme.set_type_variation(ThemeKeys.MONO, "Label")
|
||||
theme.set_font(&"font", ThemeKeys.MONO, load(MONO))
|
||||
theme.set_font_size(&"font_size", ThemeKeys.MONO, 13)
|
||||
theme.set_color(&"font_color", ThemeKeys.MONO, Palette.MUTED_MONO)
|
||||
|
||||
|
||||
static func _primary_cta(theme: Theme) -> void:
|
||||
var k := ThemeKeys.PRIMARY_CTA
|
||||
theme.set_type_variation(k, "Button")
|
||||
theme.set_stylebox(&"normal", k, _flat(Palette.BLOOD, Palette.BLOOD, 1, 4))
|
||||
theme.set_stylebox(&"hover", k, _flat(Palette.BLOOD_BRIGHT, Palette.BLOOD, 1, 4))
|
||||
theme.set_stylebox(&"pressed", k, _flat(Palette.BLOOD_DARK, Palette.BLOOD, 1, 4))
|
||||
# Unaffordable/disabled CTA state (2a shell needs this) — desaturated/greyed.
|
||||
theme.set_stylebox(&"disabled", k, _flat(Palette.MUTED_MONO_DIM, Palette.MUTED_MONO_DIM, 1, 4))
|
||||
theme.set_color(&"font_color", k, Palette.CREAM_BRIGHT)
|
||||
theme.set_color(&"font_disabled_color", k, Palette.MUTED_MONO)
|
||||
|
||||
|
||||
static func _tabs(theme: Theme) -> void:
|
||||
var tab := ThemeKeys.TAB
|
||||
theme.set_type_variation(tab, "Button")
|
||||
theme.set_stylebox(&"normal", tab, _flat(Palette.TAB_INACTIVE_BG, Palette.PARCHMENT_BORDER_2, 1, 14))
|
||||
theme.set_stylebox(&"hover", tab, _flat(Palette.PARCHMENT_CARD, Palette.PARCHMENT_BORDER_2, 1, 14))
|
||||
theme.set_stylebox(&"pressed", tab, _flat(Palette.PARCHMENT_INSET, Palette.PARCHMENT_BORDER, 1, 14))
|
||||
theme.set_stylebox(&"focus", tab, StyleBoxEmpty.new())
|
||||
theme.set_color(&"font_color", tab, Palette.INK_MUTED)
|
||||
|
||||
var tab_active := ThemeKeys.TAB_ACTIVE
|
||||
theme.set_type_variation(tab_active, "Button")
|
||||
theme.set_stylebox(&"normal", tab_active, _flat(Palette.BLOOD, Palette.BLOOD, 1, 14))
|
||||
theme.set_stylebox(&"hover", tab_active, _flat(Palette.BLOOD_BRIGHT, Palette.BLOOD_BRIGHT, 1, 14))
|
||||
theme.set_stylebox(&"pressed", tab_active, _flat(Palette.BLOOD_DARK, Palette.BLOOD, 1, 14))
|
||||
theme.set_stylebox(&"focus", tab_active, StyleBoxEmpty.new())
|
||||
theme.set_color(&"font_color", tab_active, Palette.CREAM_BRIGHT)
|
||||
|
||||
|
||||
static func _chip(theme: Theme) -> void:
|
||||
# Base chip; the semantic colour (red/gold/grey) is applied per-use by script.
|
||||
var k := ThemeKeys.CHIP
|
||||
theme.set_type_variation(k, "Button")
|
||||
theme.set_stylebox(&"normal", k, _flat(Color(Palette.MUTED_MONO, 0.12), Palette.MUTED_MONO, 1, 3))
|
||||
theme.set_stylebox(&"hover", k, _flat(Color(Palette.MUTED_MONO, 0.22), Palette.MUTED_MONO, 1, 3))
|
||||
theme.set_stylebox(&"pressed", k, _flat(Color(Palette.MUTED_MONO, 0.32), Palette.MUTED_MONO, 1, 3))
|
||||
theme.set_stylebox(&"focus", k, StyleBoxEmpty.new())
|
||||
theme.set_font(&"font", k, load(MONO))
|
||||
theme.set_font_size(&"font_size", k, 11)
|
||||
theme.set_color(&"font_color", k, Palette.MUTED_MONO)
|
||||
|
||||
|
||||
static func _tiles(theme: Theme) -> void:
|
||||
var filled := _flat(Palette.PARCHMENT_INSET, Palette.PARCHMENT_BORDER, 2, 6)
|
||||
theme.set_type_variation(ThemeKeys.ITEM_TILE, "PanelContainer")
|
||||
theme.set_stylebox(&"panel", ThemeKeys.ITEM_TILE, filled)
|
||||
var empty := _flat(Palette.PARCHMENT_INSET_2, Palette.PARCHMENT_BORDER_3, 1, 6)
|
||||
empty.draw_center = false
|
||||
theme.set_type_variation(ThemeKeys.ITEM_TILE_EMPTY, "PanelContainer")
|
||||
theme.set_stylebox(&"panel", ThemeKeys.ITEM_TILE_EMPTY, empty)
|
||||
|
||||
|
||||
static func _cards_and_panels(theme: Theme) -> void:
|
||||
theme.set_type_variation(ThemeKeys.PARCHMENT_CARD, "PanelContainer")
|
||||
theme.set_stylebox(&"panel", ThemeKeys.PARCHMENT_CARD, _flat(Palette.PARCHMENT_CARD, Palette.PARCHMENT_BORDER, 1, 6))
|
||||
theme.set_type_variation(ThemeKeys.PARCHMENT_INSET, "PanelContainer")
|
||||
theme.set_stylebox(&"panel", ThemeKeys.PARCHMENT_INSET, _flat(Palette.PARCHMENT_INSET, Palette.PARCHMENT_BORDER_3, 1, 4))
|
||||
theme.set_type_variation(ThemeKeys.DARK_PANEL, "PanelContainer")
|
||||
theme.set_stylebox(&"panel", ThemeKeys.DARK_PANEL, _flat(Palette.DARK_PANEL_70, Palette.BORDER_4, 1, 4))
|
||||
1
client/scripts/theme/build_game_theme.gd.uid
Normal file
1
client/scripts/theme/build_game_theme.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://drualc64s2x3p
|
||||
89
client/scripts/theme/palette.gd
Normal file
89
client/scripts/theme/palette.gd
Normal file
@@ -0,0 +1,89 @@
|
||||
class_name Palette
|
||||
extends RefCounted
|
||||
## The single source of truth for every colour token (mockups README, "Color
|
||||
## tokens"). §2: presentation — owns no state. No hex literal may live anywhere
|
||||
## else in the client; every script, shader, and the Theme builder reads here.
|
||||
|
||||
# --- Dark backgrounds ---
|
||||
const STAGE_1 := Color("0a0806")
|
||||
const STAGE_2 := Color("0f0d0b")
|
||||
const BAY_STRIPE_A := Color("26221c")
|
||||
const BAY_STRIPE_B := Color("221e18")
|
||||
const DARK_PANEL_70 := Color("120f0b", 0.7)
|
||||
const DARK_PANEL_85 := Color("0e0b09", 0.85)
|
||||
const DARK_PANEL_90 := Color("0a0806", 0.9)
|
||||
# border ladder, light -> dark
|
||||
const BORDER_1 := Color("6b6152")
|
||||
const BORDER_2 := Color("5a5348")
|
||||
const BORDER_3 := Color("4a4238")
|
||||
const BORDER_4 := Color("3a352e")
|
||||
const BORDER_5 := Color("1a1512")
|
||||
|
||||
# --- Parchment ---
|
||||
const SHEET_TOP := Color("ece2ca")
|
||||
const SHEET_BOTTOM := Color("e4d8bd")
|
||||
const LOG_TOP := Color("e7dcc4")
|
||||
const LOG_BOTTOM := Color("ddd0b2")
|
||||
const PARCHMENT_CARD := Color("f1e8d2")
|
||||
const PARCHMENT_INSET := Color("e6d9b8")
|
||||
const PARCHMENT_INSET_2 := Color("d9c9a2")
|
||||
const PARCHMENT_BORDER := Color("bfa878")
|
||||
const PARCHMENT_BORDER_2 := Color("cbb684")
|
||||
const PARCHMENT_BORDER_3 := Color("a99464")
|
||||
const TAB_INACTIVE_BG := Color("efe6cf")
|
||||
|
||||
# --- Ink (text on parchment) ---
|
||||
const INK_HEADING := Color("3a2f1c")
|
||||
const INK_HEADING_2 := Color("4a3520")
|
||||
const INK_BODY := Color("4a3f2c")
|
||||
const INK_MUTED := Color("6a5a3a")
|
||||
const INK_LABEL := Color("5a4326")
|
||||
const INK_GOLD := Color("8f6a2a")
|
||||
|
||||
# --- Text on dark ---
|
||||
const CREAM := Color("e8ddc8")
|
||||
const CREAM_BRIGHT := Color("f0e6d0")
|
||||
const CREAM_SECONDARY := Color("c9bda3")
|
||||
const MUTED_MONO := Color("9a8f78")
|
||||
const MUTED_MONO_DIM := Color("7a7060")
|
||||
|
||||
# --- Accents (muted for grit) ---
|
||||
const BLOOD := Color("8f3a34") # core CTA / danger / selection / enemies
|
||||
const BLOOD_BRIGHT := Color("b8524a") # hover
|
||||
const BLOOD_DARK := Color("7a2f29") # CTA gradient bottom
|
||||
const GOLD := Color("a9843f") # highlight / currency / current-turn
|
||||
const GOLD_BRIGHT := Color("c8963f")
|
||||
const STEEL := Color("5f8a9a") # allies / mana
|
||||
const STEEL_BLUE := Color("4f7ab8")
|
||||
const STEEL_DEEP := Color("243f6f")
|
||||
const GREEN := Color("5f8a4f") # stamina / success / cleared
|
||||
|
||||
# --- Stat bars ---
|
||||
const HP_A := Color("b8524a")
|
||||
const HP_B := Color("8f3a34")
|
||||
const MP_A := Color("4f7ab8")
|
||||
const MP_B := Color("243f6f")
|
||||
const STAMINA := Color("8f9a4f")
|
||||
const BAR_TRACK := Color("2a251f")
|
||||
const BAR_BORDER := Color("4a4238")
|
||||
|
||||
# --- Rarity (ordered common..legendary) ---
|
||||
const RARITY: Array[Color] = [
|
||||
Color("8a8378"), # 0 common
|
||||
Color("5f8a4f"), # 1 uncommon
|
||||
Color("4f7ab8"), # 2 rare
|
||||
Color("8a5fb8"), # 3 epic
|
||||
Color("c8963f"), # 4 legendary
|
||||
]
|
||||
|
||||
|
||||
static func rarity_color(rarity: int) -> Color:
|
||||
return RARITY[clampi(rarity, 0, RARITY.size() - 1)]
|
||||
|
||||
|
||||
static func stat_bar(kind: StringName) -> Array:
|
||||
match kind:
|
||||
&"hp": return [HP_A, HP_B]
|
||||
&"mp": return [MP_A, MP_B]
|
||||
&"stamina": return [STAMINA, STAMINA]
|
||||
_: return [MUTED_MONO, MUTED_MONO_DIM]
|
||||
1
client/scripts/theme/palette.gd.uid
Normal file
1
client/scripts/theme/palette.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://d4bext4g7reql
|
||||
14
client/scripts/theme/surfaces/dark_bay.gd
Normal file
14
client/scripts/theme/surfaces/dark_bay.gd
Normal file
@@ -0,0 +1,14 @@
|
||||
class_name DarkBay
|
||||
extends Panel
|
||||
## Drop-in dark stripe+vignette bay backdrop. §2: presentation.
|
||||
## Uniforms are set at runtime from Palette (deliberate deviation from spec §5.4's
|
||||
## "baked into the .tscn/material" approach) — keeps Palette the single source of
|
||||
## truth at the cost of an editor preview; the surface renders once the scene runs.
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var m := ShaderMaterial.new()
|
||||
m.shader = load("res://assets/theme/shaders/dark_bay.gdshader")
|
||||
m.set_shader_parameter("stripe_a", Palette.BAY_STRIPE_A)
|
||||
m.set_shader_parameter("stripe_b", Palette.BAY_STRIPE_B)
|
||||
material = m
|
||||
1
client/scripts/theme/surfaces/dark_bay.gd.uid
Normal file
1
client/scripts/theme/surfaces/dark_bay.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://24c8pl0yx8wu
|
||||
14
client/scripts/theme/surfaces/parchment_panel.gd
Normal file
14
client/scripts/theme/surfaces/parchment_panel.gd
Normal file
@@ -0,0 +1,14 @@
|
||||
class_name ParchmentPanel
|
||||
extends Panel
|
||||
## Drop-in parchment backdrop (mockups "Dark Bay + Parchment" motif). §2: presentation.
|
||||
## Uniforms are set at runtime from Palette (deliberate deviation from spec §5.4's
|
||||
## "baked into the .tscn/material" approach) — keeps Palette the single source of
|
||||
## truth at the cost of an editor preview; the surface renders once the scene runs.
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var m := ShaderMaterial.new()
|
||||
m.shader = load("res://assets/theme/shaders/parchment.gdshader")
|
||||
m.set_shader_parameter("top", Palette.SHEET_TOP)
|
||||
m.set_shader_parameter("bottom", Palette.SHEET_BOTTOM)
|
||||
material = m
|
||||
1
client/scripts/theme/surfaces/parchment_panel.gd.uid
Normal file
1
client/scripts/theme/surfaces/parchment_panel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cnv1r00ane2la
|
||||
16
client/scripts/theme/surfaces/vignette_overlay.gd
Normal file
16
client/scripts/theme/surfaces/vignette_overlay.gd
Normal file
@@ -0,0 +1,16 @@
|
||||
class_name VignetteOverlay
|
||||
extends ColorRect
|
||||
## Drop-in edge-dim overlay for pause/dialogue dim. §2: presentation.
|
||||
## The ShaderMaterial (and, for the other two surfaces, its Palette-sourced
|
||||
## uniforms) is built at runtime in _ready() rather than baked into the .tscn
|
||||
## (deliberate deviation from spec §5.4's "baked" approach) — keeps Palette the
|
||||
## single source of truth at the cost of an editor preview; the surface renders
|
||||
## once the scene runs.
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
color = Color(0, 0, 0, 0) # base transparent; shader draws the dim
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var m := ShaderMaterial.new()
|
||||
m.shader = load("res://assets/theme/shaders/vignette.gdshader")
|
||||
material = m
|
||||
1
client/scripts/theme/surfaces/vignette_overlay.gd.uid
Normal file
1
client/scripts/theme/surfaces/vignette_overlay.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://7bc7agrl6oi0
|
||||
36
client/scripts/theme/theme_keys.gd
Normal file
36
client/scripts/theme/theme_keys.gd
Normal file
@@ -0,0 +1,36 @@
|
||||
# client/scripts/theme/theme_keys.gd
|
||||
class_name ThemeKeys
|
||||
extends RefCounted
|
||||
## Type-variation names for game_theme.tres. Scripts set `theme_type_variation`
|
||||
## to one of these so a typo is a compile-time symbol error, not a silent miss.
|
||||
## §2: presentation.
|
||||
|
||||
const PRIMARY_CTA := &"PrimaryCTA"
|
||||
const TAB := &"Tab"
|
||||
const TAB_ACTIVE := &"TabActive"
|
||||
const CHIP := &"Chip"
|
||||
const ITEM_TILE := &"ItemTile"
|
||||
const ITEM_TILE_EMPTY := &"ItemTileEmpty"
|
||||
const PARCHMENT_CARD := &"ParchmentCard"
|
||||
const PARCHMENT_INSET := &"ParchmentInset"
|
||||
const DARK_PANEL := &"DarkPanel"
|
||||
|
||||
## Font roles (Label type-variations carrying a font, not a stylebox). Kept
|
||||
## separate from ALL below, which is stylebox variations + their base type only.
|
||||
const HEADING := &"Heading"
|
||||
const ACCENT := &"Accent"
|
||||
const MONO := &"Mono"
|
||||
|
||||
## Every variation name + the base Control type it decorates. The builder and the
|
||||
## test both iterate this so they can never drift apart.
|
||||
const ALL := {
|
||||
PRIMARY_CTA: "Button",
|
||||
TAB: "Button",
|
||||
TAB_ACTIVE: "Button",
|
||||
CHIP: "Button",
|
||||
ITEM_TILE: "PanelContainer",
|
||||
ITEM_TILE_EMPTY: "PanelContainer",
|
||||
PARCHMENT_CARD: "PanelContainer",
|
||||
PARCHMENT_INSET: "PanelContainer",
|
||||
DARK_PANEL: "PanelContainer",
|
||||
}
|
||||
1
client/scripts/theme/theme_keys.gd.uid
Normal file
1
client/scripts/theme/theme_keys.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://upoh5yt3dd4i
|
||||
214
client/scripts/theme/theme_showcase.gd
Normal file
214
client/scripts/theme/theme_showcase.gd
Normal file
@@ -0,0 +1,214 @@
|
||||
# client/scripts/theme/theme_showcase.gd
|
||||
extends Control
|
||||
## The eyeball proof for the shared Theme (spec §7). Builds every token, stylebox,
|
||||
## font, surface, and bar on one scrollable screen. No AI, no state. §2: presentation.
|
||||
const GAME_THEME := "res://assets/theme/game_theme.tres"
|
||||
const SERIF_ITALIC := "res://assets/theme/fonts/EBGaramond-Italic-VariableFont_wght.ttf"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
theme = load(GAME_THEME)
|
||||
# A root Control run directly (F6) is not reliably sized to the viewport by
|
||||
# full-rect anchors — it collapses to (0,0), clipping the ScrollContainer and
|
||||
# hiding everything. Size to the viewport explicitly (no full-rect anchors here:
|
||||
# they conflict with setting `size` directly) and track resizes.
|
||||
_fit_to_viewport()
|
||||
get_viewport().size_changed.connect(_fit_to_viewport)
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_child(scroll)
|
||||
var col := VBoxContainer.new()
|
||||
col.custom_minimum_size = Vector2(1900, 0)
|
||||
col.add_theme_constant_override("separation", 16)
|
||||
scroll.add_child(col)
|
||||
|
||||
col.add_child(_heading("SHARED THEME — SHOWCASE"))
|
||||
col.add_child(_swatches())
|
||||
col.add_child(_variations())
|
||||
col.add_child(_fonts())
|
||||
col.add_child(_prose())
|
||||
col.add_child(_rarity())
|
||||
col.add_child(_bars())
|
||||
col.add_child(_surfaces())
|
||||
|
||||
|
||||
func _fit_to_viewport() -> void:
|
||||
size = get_viewport_rect().size
|
||||
|
||||
|
||||
func _heading(text: String) -> Label:
|
||||
var l := Label.new()
|
||||
l.text = text
|
||||
l.theme_type_variation = ThemeKeys.HEADING
|
||||
return l
|
||||
|
||||
|
||||
func _swatches() -> Control:
|
||||
var grid := GridContainer.new()
|
||||
grid.columns = 8
|
||||
var tokens := [
|
||||
["STAGE_1", Palette.STAGE_1], ["BAY_STRIPE_A", Palette.BAY_STRIPE_A],
|
||||
["DARK_PANEL_70", Palette.DARK_PANEL_70], ["SHEET_TOP", Palette.SHEET_TOP],
|
||||
["PARCHMENT_CARD", Palette.PARCHMENT_CARD], ["BLOOD", Palette.BLOOD],
|
||||
["GOLD", Palette.GOLD], ["STEEL", Palette.STEEL], ["GREEN", Palette.GREEN],
|
||||
["INK_BODY", Palette.INK_BODY], ["CREAM", Palette.CREAM],
|
||||
]
|
||||
for entry in tokens:
|
||||
var col := VBoxContainer.new()
|
||||
var r := ColorRect.new()
|
||||
r.color = entry[1]
|
||||
r.custom_minimum_size = Vector2(120, 60)
|
||||
col.add_child(r)
|
||||
var caption := Label.new()
|
||||
caption.text = entry[0]
|
||||
caption.theme_type_variation = ThemeKeys.MONO
|
||||
col.add_child(caption)
|
||||
grid.add_child(col)
|
||||
return grid
|
||||
|
||||
|
||||
func _variations() -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 12)
|
||||
var cta := Button.new(); cta.text = "ENTER THE WORLD"; cta.theme_type_variation = ThemeKeys.PRIMARY_CTA
|
||||
var tab := Button.new(); tab.text = "INACTIVE"; tab.theme_type_variation = ThemeKeys.TAB
|
||||
var tab_a := Button.new(); tab_a.text = "ACTIVE"; tab_a.theme_type_variation = ThemeKeys.TAB_ACTIVE
|
||||
# Neutral chip (grey) plus the semantic colours a script applies per-use
|
||||
# (spec §5.3): red for aggressive/combat actions, gold for pay/faith.
|
||||
var chip := Button.new(); chip.text = "STEALTH · DEX"; chip.theme_type_variation = ThemeKeys.CHIP
|
||||
var chip_red := _semantic_chip("INTIMIDATE · STR", Palette.BLOOD)
|
||||
var chip_gold := _semantic_chip("PAY ◈40", Palette.GOLD)
|
||||
for b in [cta, tab, tab_a, chip, chip_red, chip_gold]:
|
||||
row.add_child(b)
|
||||
var card := PanelContainer.new(); card.theme_type_variation = ThemeKeys.PARCHMENT_CARD
|
||||
card.custom_minimum_size = Vector2(160, 80)
|
||||
row.add_child(card)
|
||||
|
||||
# Filled vs empty item tile, side by side for comparison.
|
||||
var tile_filled := PanelContainer.new()
|
||||
tile_filled.theme_type_variation = ThemeKeys.ITEM_TILE
|
||||
tile_filled.custom_minimum_size = Vector2(64, 64)
|
||||
row.add_child(tile_filled)
|
||||
var tile_empty := PanelContainer.new()
|
||||
tile_empty.theme_type_variation = ThemeKeys.ITEM_TILE_EMPTY
|
||||
tile_empty.custom_minimum_size = Vector2(64, 64)
|
||||
row.add_child(tile_empty)
|
||||
|
||||
var inset := PanelContainer.new()
|
||||
inset.theme_type_variation = ThemeKeys.PARCHMENT_INSET
|
||||
inset.custom_minimum_size = Vector2(160, 80)
|
||||
row.add_child(inset)
|
||||
|
||||
var dark := PanelContainer.new()
|
||||
dark.theme_type_variation = ThemeKeys.DARK_PANEL
|
||||
dark.custom_minimum_size = Vector2(160, 80)
|
||||
row.add_child(dark)
|
||||
|
||||
return row
|
||||
|
||||
|
||||
func _semantic_chip(text: String, color: Color) -> Button:
|
||||
# Chip base is neutral (ThemeKeys.CHIP); the semantic colour (§5.3) is set
|
||||
# by the consuming script, here via font-colour + border overrides.
|
||||
var chip := Button.new()
|
||||
chip.text = text
|
||||
chip.theme_type_variation = ThemeKeys.CHIP
|
||||
chip.add_theme_color_override("font_color", color)
|
||||
var tinted := StyleBoxFlat.new()
|
||||
tinted.bg_color = Color(color, 0.15)
|
||||
tinted.border_color = color
|
||||
tinted.set_border_width_all(1)
|
||||
tinted.set_corner_radius_all(3)
|
||||
tinted.content_margin_left = 12
|
||||
tinted.content_margin_right = 12
|
||||
tinted.content_margin_top = 6
|
||||
tinted.content_margin_bottom = 6
|
||||
chip.add_theme_stylebox_override("normal", tinted)
|
||||
return chip
|
||||
|
||||
|
||||
func _prose() -> Control:
|
||||
# Narrative italic (spec §6) — the bundled EB Garamond italic face, shown
|
||||
# nowhere else on this screen. RichTextLabel is required for bbcode italics;
|
||||
# a plain Label cannot render [i].
|
||||
var rtl := RichTextLabel.new()
|
||||
rtl.bbcode_enabled = true
|
||||
rtl.fit_content = true
|
||||
rtl.custom_minimum_size = Vector2(900, 0)
|
||||
rtl.add_theme_font_override("normal_font", load(GAME_THEME).default_font)
|
||||
rtl.add_theme_font_override("italics_font", load(SERIF_ITALIC))
|
||||
rtl.text = "[i]The chamber is cold. Something waits in the dark.[/i]"
|
||||
return rtl
|
||||
|
||||
|
||||
func _fonts() -> Control:
|
||||
# All three bundled font families, at their real Theme sizes, plus the
|
||||
# default serif body face (not otherwise shown elsewhere on this screen).
|
||||
var col := VBoxContainer.new()
|
||||
col.add_theme_constant_override("separation", 6)
|
||||
|
||||
var body := Label.new()
|
||||
body.text = "The road to the Ashmarch is longer than the maps admit."
|
||||
col.add_child(body)
|
||||
|
||||
var accent := Label.new()
|
||||
accent.text = "Vexcca of the Ashmarch"
|
||||
accent.theme_type_variation = ThemeKeys.ACCENT
|
||||
col.add_child(accent)
|
||||
|
||||
var mono := Label.new()
|
||||
mono.text = "STR 14 · INITIATIVE +2 · AC 16"
|
||||
mono.theme_type_variation = ThemeKeys.MONO
|
||||
col.add_child(mono)
|
||||
|
||||
return col
|
||||
|
||||
|
||||
func _rarity() -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
for i in range(5):
|
||||
var tile := PanelContainer.new()
|
||||
tile.theme_type_variation = ThemeKeys.ITEM_TILE
|
||||
tile.custom_minimum_size = Vector2(72, 72)
|
||||
var bar := ColorRect.new()
|
||||
bar.color = Palette.rarity_color(i)
|
||||
bar.custom_minimum_size = Vector2(60, 4)
|
||||
tile.add_child(bar)
|
||||
row.add_child(tile)
|
||||
return row
|
||||
|
||||
|
||||
func _bars() -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
for kind in [&"hp", &"mp", &"stamina"]:
|
||||
var pair = Palette.stat_bar(kind)
|
||||
var bar := ColorRect.new()
|
||||
bar.color = pair[0]
|
||||
bar.custom_minimum_size = Vector2(200, 18)
|
||||
row.add_child(bar)
|
||||
return row
|
||||
|
||||
|
||||
func _surfaces() -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
row.custom_minimum_size = Vector2(0, 240)
|
||||
for path in ["res://scenes/theme/surfaces/ParchmentPanel.tscn",
|
||||
"res://scenes/theme/surfaces/DarkBay.tscn"]:
|
||||
var s = load(path).instantiate()
|
||||
s.custom_minimum_size = Vector2(600, 220)
|
||||
row.add_child(s)
|
||||
|
||||
# VignetteOverlay draws a black-with-alpha dim; give it a mid-tone backing
|
||||
# so the dim is visibly rendering rather than reading as empty space.
|
||||
var vignette_slot := Control.new()
|
||||
vignette_slot.custom_minimum_size = Vector2(600, 220)
|
||||
var backing := ColorRect.new()
|
||||
backing.color = Palette.STEEL
|
||||
backing.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
vignette_slot.add_child(backing)
|
||||
var vignette = load("res://scenes/theme/surfaces/VignetteOverlay.tscn").instantiate()
|
||||
vignette.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
vignette_slot.add_child(vignette)
|
||||
row.add_child(vignette_slot)
|
||||
|
||||
return row
|
||||
1
client/scripts/theme/theme_showcase.gd.uid
Normal file
1
client/scripts/theme/theme_showcase.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ciqf5caopnsh3
|
||||
17
client/tests/unit/test_fonts.gd
Normal file
17
client/tests/unit/test_fonts.gd
Normal file
@@ -0,0 +1,17 @@
|
||||
# client/tests/unit/test_fonts.gd
|
||||
extends "res://addons/gut/test.gd"
|
||||
|
||||
const FONTS := {
|
||||
"serif": "res://assets/theme/fonts/EBGaramond-VariableFont_wght.ttf",
|
||||
"serif_italic": "res://assets/theme/fonts/EBGaramond-Italic-VariableFont_wght.ttf",
|
||||
"accent": "res://assets/theme/fonts/ArchitectsDaughter-Regular.ttf",
|
||||
"mono": "res://assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf",
|
||||
}
|
||||
|
||||
|
||||
func test_all_fonts_exist_and_load_as_fontfile():
|
||||
for key in FONTS:
|
||||
var path: String = FONTS[key]
|
||||
assert_true(FileAccess.file_exists(path), "missing font: %s" % path)
|
||||
var f = load(path)
|
||||
assert_true(f is FontFile, "%s did not load as FontFile" % key)
|
||||
1
client/tests/unit/test_fonts.gd.uid
Normal file
1
client/tests/unit/test_fonts.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ck81t1k5xlvgu
|
||||
43
client/tests/unit/test_palette.gd
Normal file
43
client/tests/unit/test_palette.gd
Normal file
@@ -0,0 +1,43 @@
|
||||
extends "res://addons/gut/test.gd"
|
||||
|
||||
|
||||
func test_core_tokens_are_colors():
|
||||
assert_true(Palette.BLOOD is Color)
|
||||
assert_true(Palette.GOLD is Color)
|
||||
assert_true(Palette.PARCHMENT_CARD is Color)
|
||||
assert_true(Palette.CREAM is Color)
|
||||
|
||||
|
||||
func test_blood_core_matches_readme_hex():
|
||||
# #8f3a34 — the primary CTA / danger / selection red.
|
||||
assert_eq(Palette.BLOOD, Color("8f3a34"))
|
||||
|
||||
|
||||
func test_dark_panel_is_translucent():
|
||||
assert_almost_eq(Palette.DARK_PANEL_70.a, 0.7, 0.01)
|
||||
|
||||
|
||||
func test_rarity_ladder_has_five_ordered_colors():
|
||||
assert_eq(Palette.RARITY.size(), 5)
|
||||
assert_eq(Palette.rarity_color(0), Color("8a8378")) # common
|
||||
assert_eq(Palette.rarity_color(4), Color("c8963f")) # legendary
|
||||
|
||||
|
||||
func test_rarity_color_clamps_out_of_range():
|
||||
# Out-of-range must NOT index-error (would emit an engine error -> fail).
|
||||
assert_eq(Palette.rarity_color(-3), Palette.RARITY[0])
|
||||
assert_eq(Palette.rarity_color(99), Palette.RARITY[4])
|
||||
|
||||
|
||||
func test_stat_bar_returns_from_to_pair():
|
||||
var hp = Palette.stat_bar(&"hp")
|
||||
assert_eq(hp.size(), 2)
|
||||
assert_eq(hp[0], Color("b8524a"))
|
||||
assert_eq(hp[1], Color("8f3a34"))
|
||||
|
||||
|
||||
func test_stat_bar_unknown_kind_is_safe():
|
||||
# Unknown kind returns a grey pair, never null / error.
|
||||
var pair = Palette.stat_bar(&"nonsense")
|
||||
assert_eq(pair.size(), 2)
|
||||
assert_true(pair[0] is Color)
|
||||
1
client/tests/unit/test_palette.gd.uid
Normal file
1
client/tests/unit/test_palette.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://w5blbshxn630
|
||||
17
client/tests/unit/test_surfaces.gd
Normal file
17
client/tests/unit/test_surfaces.gd
Normal file
@@ -0,0 +1,17 @@
|
||||
extends "res://addons/gut/test.gd"
|
||||
|
||||
const SCENES := [
|
||||
"res://scenes/theme/surfaces/ParchmentPanel.tscn",
|
||||
"res://scenes/theme/surfaces/DarkBay.tscn",
|
||||
"res://scenes/theme/surfaces/VignetteOverlay.tscn",
|
||||
]
|
||||
|
||||
|
||||
func test_surfaces_instance_with_shader_material():
|
||||
for path in SCENES:
|
||||
var packed = load(path)
|
||||
assert_true(packed is PackedScene, "not a scene: %s" % path)
|
||||
var node = packed.instantiate()
|
||||
add_child_autofree(node) # triggers _ready()
|
||||
assert_true(node.material is ShaderMaterial, "%s has no ShaderMaterial" % path)
|
||||
assert_true(node.material.shader is Shader, "%s shader missing" % path)
|
||||
1
client/tests/unit/test_surfaces.gd.uid
Normal file
1
client/tests/unit/test_surfaces.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://csc3r7lqoe83d
|
||||
50
client/tests/unit/test_theme_resource.gd
Normal file
50
client/tests/unit/test_theme_resource.gd
Normal file
@@ -0,0 +1,50 @@
|
||||
# client/tests/unit/test_theme_resource.gd
|
||||
extends "res://addons/gut/test.gd"
|
||||
|
||||
const Builder = preload("res://scripts/theme/build_game_theme.gd")
|
||||
const THEME_PATH := "res://assets/theme/game_theme.tres"
|
||||
|
||||
|
||||
func test_theme_loads():
|
||||
assert_true(FileAccess.file_exists(THEME_PATH), "theme not built — run build_game_theme.gd")
|
||||
var t = load(THEME_PATH)
|
||||
assert_true(t is Theme)
|
||||
|
||||
|
||||
func test_every_variation_resolves():
|
||||
var t: Theme = load(THEME_PATH)
|
||||
for key in ThemeKeys.ALL:
|
||||
var base: String = ThemeKeys.ALL[key]
|
||||
assert_eq(t.get_type_variation_base(key), StringName(base),
|
||||
"variation %s missing or wrong base" % key)
|
||||
|
||||
|
||||
func test_cta_has_normal_stylebox():
|
||||
var t: Theme = load(THEME_PATH)
|
||||
assert_true(t.has_stylebox(&"normal", ThemeKeys.PRIMARY_CTA))
|
||||
|
||||
|
||||
func test_default_font_is_set():
|
||||
var t: Theme = load(THEME_PATH)
|
||||
assert_true(t.default_font is FontFile)
|
||||
|
||||
|
||||
func test_committed_tres_matches_builder():
|
||||
# Catches "palette/builder changed but nobody regenerated game_theme.tres."
|
||||
# Preloading the builder script and calling its static build_theme() does
|
||||
# NOT run _init() (that only fires on .new()), so this is side-effect-free.
|
||||
var fresh: Theme = Builder.build_theme()
|
||||
var committed: Theme = load(THEME_PATH)
|
||||
|
||||
var checks := [
|
||||
[ThemeKeys.PRIMARY_CTA, &"normal"],
|
||||
[ThemeKeys.PARCHMENT_CARD, &"panel"],
|
||||
[ThemeKeys.DARK_PANEL, &"panel"],
|
||||
]
|
||||
for pair in checks:
|
||||
var variation: StringName = pair[0]
|
||||
var state: StringName = pair[1]
|
||||
var fresh_box: StyleBoxFlat = fresh.get_stylebox(state, variation)
|
||||
var committed_box: StyleBoxFlat = committed.get_stylebox(state, variation)
|
||||
assert_eq(fresh_box.bg_color, committed_box.bg_color,
|
||||
"stale game_theme.tres — %s/%s bg_color drifted from the builder; re-run build_game_theme.gd" % [variation, state])
|
||||
1
client/tests/unit/test_theme_resource.gd.uid
Normal file
1
client/tests/unit/test_theme_resource.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bjwiotv7oid2t
|
||||
11
client/tests/unit/test_theme_showcase.gd
Normal file
11
client/tests/unit/test_theme_showcase.gd
Normal file
@@ -0,0 +1,11 @@
|
||||
# client/tests/unit/test_theme_showcase.gd
|
||||
extends "res://addons/gut/test.gd"
|
||||
|
||||
|
||||
func test_showcase_instances_without_error():
|
||||
var packed = load("res://scenes/theme/theme_showcase.tscn")
|
||||
assert_true(packed is PackedScene)
|
||||
var node = packed.instantiate()
|
||||
add_child_autofree(node) # runs _ready(); any engine error here fails the suite
|
||||
assert_true(node is Control)
|
||||
assert_true(node.theme is Theme, "showcase did not apply game_theme")
|
||||
1
client/tests/unit/test_theme_showcase.gd.uid
Normal file
1
client/tests/unit/test_theme_showcase.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://e4p6b65e14mw
|
||||
835
docs/superpowers/plans/2026-07-10-shared-theme.md
Normal file
835
docs/superpowers/plans/2026-07-10-shared-theme.md
Normal file
@@ -0,0 +1,835 @@
|
||||
# Shared Theme Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Build the one shared visual system (palette, fonts, styleboxes, signature surfaces) every later Code of Conquest screen inherits — as reusable Godot resources with a human-eyeballed showcase proof.
|
||||
|
||||
**Architecture:** Three consumers of one palette. `palette.gd` holds every colour as `const Color` (the single source of truth). `game_theme.tres` — a `Theme` resource built by a committed GDScript builder — carries fonts + StyleBoxFlat type-variations for crisp bordered UI. Three `.gdshader`-backed `Control` subclasses render the parchment / dark-stripe-bay / vignette surfaces (a StyleBox can't run a shader). No game state, no AI, no network.
|
||||
|
||||
**Tech Stack:** Godot 4.7, GDScript, GUT (headless), OFL fonts, Godot shading language.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **Engine:** Godot **4.7**, **GDScript** only (charter §16).
|
||||
- **Tests:** GUT headless via `./run_tests.sh`. `.gutconfig.json` sets `failure_error_types: ["engine","gut"]` — **any `push_error`, failed `load()`, or engine warning fails the suite.** Load resources defensively; never let a missing file emit an engine error in a passing path.
|
||||
- **Test idiom:** every test file `extends "res://addons/gut/test.gd"`, `preload`s its subject, uses `assert_eq`/`assert_ne`/`assert_true`.
|
||||
- **Script idiom:** `class_name X extends RefCounted` (or the needed Control type), `##` doc comment at top stating purpose + §2 side.
|
||||
- **§2 side:** presentation — **owns no state, makes no call.** (charter §2)
|
||||
- **Palette is the single source of truth:** no raw hex literal may appear in any script, `.tres`, or shader *except* inside `palette.gd`. Everything else references it.
|
||||
- **Fonts:** OFL-licensed only. Georgia is proprietary and must never ship (spec §3).
|
||||
- **No real art:** portraits / key-art / iso views stay dashed art slots.
|
||||
- **Canvas:** 1920×1080 reference (charter §16); do not tune stretch here.
|
||||
- **Spec:** `docs/superpowers/specs/2026-07-10-shared-theme-design.md`.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Palette tokens (`palette.gd`)
|
||||
|
||||
The single source of truth for every colour. Pure `const Color`s grouped by role, plus the two helpers the code-driven cases need.
|
||||
|
||||
**Files:**
|
||||
- Create: `client/scripts/theme/palette.gd`
|
||||
- Test: `client/tests/unit/test_palette.gd`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `class_name Palette` (a `RefCounted` used only as a namespace for `const`s + `static func`s). Key consts used by later tasks: `BLOOD`, `GOLD`, `STEEL`, `GREEN`, `PARCHMENT_CARD`, `PARCHMENT_BORDER`, `DARK_PANEL_70`, `CREAM`, `INK_BODY`, `INK_HEADING`, `STAGE_1`, `SHEET_TOP`, `SHEET_BOTTOM`, `BAY_STRIPE_A`, `BAY_STRIPE_B`, `HP_A`/`HP_B`, `MP_A`/`MP_B`, `STAMINA`, `RARITY` (Array). Statics: `rarity_color(rarity: int) -> Color`, `stat_bar(kind: StringName) -> Array` (returns `[from, to]`).
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```gdscript
|
||||
# client/tests/unit/test_palette.gd
|
||||
extends "res://addons/gut/test.gd"
|
||||
|
||||
const Palette = preload("res://scripts/theme/palette.gd")
|
||||
|
||||
|
||||
func test_core_tokens_are_colors():
|
||||
assert_true(Palette.BLOOD is Color)
|
||||
assert_true(Palette.GOLD is Color)
|
||||
assert_true(Palette.PARCHMENT_CARD is Color)
|
||||
assert_true(Palette.CREAM is Color)
|
||||
|
||||
|
||||
func test_blood_core_matches_readme_hex():
|
||||
# #8f3a34 — the primary CTA / danger / selection red.
|
||||
assert_eq(Palette.BLOOD, Color("8f3a34"))
|
||||
|
||||
|
||||
func test_dark_panel_is_translucent():
|
||||
assert_almost_eq(Palette.DARK_PANEL_70.a, 0.7, 0.01)
|
||||
|
||||
|
||||
func test_rarity_ladder_has_five_ordered_colors():
|
||||
assert_eq(Palette.RARITY.size(), 5)
|
||||
assert_eq(Palette.rarity_color(0), Color("8a8378")) # common
|
||||
assert_eq(Palette.rarity_color(4), Color("c8963f")) # legendary
|
||||
|
||||
|
||||
func test_rarity_color_clamps_out_of_range():
|
||||
# Out-of-range must NOT index-error (would emit an engine error -> fail).
|
||||
assert_eq(Palette.rarity_color(-3), Palette.RARITY[0])
|
||||
assert_eq(Palette.rarity_color(99), Palette.RARITY[4])
|
||||
|
||||
|
||||
func test_stat_bar_returns_from_to_pair():
|
||||
var hp = Palette.stat_bar(&"hp")
|
||||
assert_eq(hp.size(), 2)
|
||||
assert_eq(hp[0], Color("b8524a"))
|
||||
assert_eq(hp[1], Color("8f3a34"))
|
||||
|
||||
|
||||
func test_stat_bar_unknown_kind_is_safe():
|
||||
# Unknown kind returns a grey pair, never null / error.
|
||||
var pair = Palette.stat_bar(&"nonsense")
|
||||
assert_eq(pair.size(), 2)
|
||||
assert_true(pair[0] is Color)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `cd client && ./run_tests.sh -gtest=res://tests/unit/test_palette.gd`
|
||||
Expected: FAIL — `Palette` script not found / consts undefined.
|
||||
|
||||
- [ ] **Step 3: Write the implementation**
|
||||
|
||||
```gdscript
|
||||
# client/scripts/theme/palette.gd
|
||||
class_name Palette
|
||||
extends RefCounted
|
||||
## The single source of truth for every colour token (mockups README, "Color
|
||||
## tokens"). §2: presentation — owns no state. No hex literal may live anywhere
|
||||
## else in the client; every script, shader, and the Theme builder reads here.
|
||||
|
||||
# --- Dark backgrounds ---
|
||||
const STAGE_1 := Color("0a0806")
|
||||
const STAGE_2 := Color("0f0d0b")
|
||||
const BAY_STRIPE_A := Color("26221c")
|
||||
const BAY_STRIPE_B := Color("221e18")
|
||||
const DARK_PANEL_70 := Color("120f0b", 0.7)
|
||||
const DARK_PANEL_85 := Color("0e0b09", 0.85)
|
||||
const DARK_PANEL_90 := Color("0a0806", 0.9)
|
||||
# border ladder, light -> dark
|
||||
const BORDER_1 := Color("6b6152")
|
||||
const BORDER_2 := Color("5a5348")
|
||||
const BORDER_3 := Color("4a4238")
|
||||
const BORDER_4 := Color("3a352e")
|
||||
const BORDER_5 := Color("1a1512")
|
||||
|
||||
# --- Parchment ---
|
||||
const SHEET_TOP := Color("ece2ca")
|
||||
const SHEET_BOTTOM := Color("e4d8bd")
|
||||
const LOG_TOP := Color("e7dcc4")
|
||||
const LOG_BOTTOM := Color("ddd0b2")
|
||||
const PARCHMENT_CARD := Color("f1e8d2")
|
||||
const PARCHMENT_INSET := Color("e6d9b8")
|
||||
const PARCHMENT_INSET_2 := Color("d9c9a2")
|
||||
const PARCHMENT_BORDER := Color("bfa878")
|
||||
const PARCHMENT_BORDER_2 := Color("cbb684")
|
||||
const PARCHMENT_BORDER_3 := Color("a99464")
|
||||
const TAB_INACTIVE_BG := Color("efe6cf")
|
||||
|
||||
# --- Ink (text on parchment) ---
|
||||
const INK_HEADING := Color("3a2f1c")
|
||||
const INK_HEADING_2 := Color("4a3520")
|
||||
const INK_BODY := Color("4a3f2c")
|
||||
const INK_MUTED := Color("6a5a3a")
|
||||
const INK_LABEL := Color("5a4326")
|
||||
const INK_GOLD := Color("8f6a2a")
|
||||
|
||||
# --- Text on dark ---
|
||||
const CREAM := Color("e8ddc8")
|
||||
const CREAM_BRIGHT := Color("f0e6d0")
|
||||
const CREAM_SECONDARY := Color("c9bda3")
|
||||
const MUTED_MONO := Color("9a8f78")
|
||||
const MUTED_MONO_DIM := Color("7a7060")
|
||||
|
||||
# --- Accents (muted for grit) ---
|
||||
const BLOOD := Color("8f3a34") # core CTA / danger / selection / enemies
|
||||
const BLOOD_BRIGHT := Color("b8524a") # hover
|
||||
const BLOOD_DARK := Color("7a2f29") # CTA gradient bottom
|
||||
const GOLD := Color("a9843f") # highlight / currency / current-turn
|
||||
const GOLD_BRIGHT := Color("c8963f")
|
||||
const STEEL := Color("5f8a9a") # allies / mana
|
||||
const STEEL_BLUE := Color("4f7ab8")
|
||||
const STEEL_DEEP := Color("243f6f")
|
||||
const GREEN := Color("5f8a4f") # stamina / success / cleared
|
||||
|
||||
# --- Stat bars ---
|
||||
const HP_A := Color("b8524a")
|
||||
const HP_B := Color("8f3a34")
|
||||
const MP_A := Color("4f7ab8")
|
||||
const MP_B := Color("243f6f")
|
||||
const STAMINA := Color("8f9a4f")
|
||||
const BAR_TRACK := Color("2a251f")
|
||||
const BAR_BORDER := Color("4a4238")
|
||||
|
||||
# --- Rarity (ordered common..legendary) ---
|
||||
const RARITY: Array[Color] = [
|
||||
Color("8a8378"), # 0 common
|
||||
Color("5f8a4f"), # 1 uncommon
|
||||
Color("4f7ab8"), # 2 rare
|
||||
Color("8a5fb8"), # 3 epic
|
||||
Color("c8963f"), # 4 legendary
|
||||
]
|
||||
|
||||
|
||||
static func rarity_color(rarity: int) -> Color:
|
||||
return RARITY[clampi(rarity, 0, RARITY.size() - 1)]
|
||||
|
||||
|
||||
static func stat_bar(kind: StringName) -> Array:
|
||||
match kind:
|
||||
&"hp": return [HP_A, HP_B]
|
||||
&"mp": return [MP_A, MP_B]
|
||||
&"stamina": return [STAMINA, STAMINA]
|
||||
_: return [MUTED_MONO, MUTED_MONO_DIM]
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd client && ./run_tests.sh -gtest=res://tests/unit/test_palette.gd`
|
||||
Expected: PASS — all 7 tests green, 0 engine errors.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add client/scripts/theme/palette.gd client/tests/unit/test_palette.gd
|
||||
git commit -m "feat(theme): palette tokens as the single colour source"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Bundle the fonts
|
||||
|
||||
Acquire the three OFL font families and verify Godot imports them. Deliverable: font files committed + a test proving each loads as a `FontFile`.
|
||||
|
||||
**Files:**
|
||||
- Create: `client/assets/theme/fonts/EBGaramond-VariableFont_wght.ttf`
|
||||
- Create: `client/assets/theme/fonts/EBGaramond-Italic-VariableFont_wght.ttf`
|
||||
- Create: `client/assets/theme/fonts/ArchitectsDaughter-Regular.ttf`
|
||||
- Create: `client/assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf`
|
||||
- Test: `client/tests/unit/test_fonts.gd`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: four font resource paths under `res://assets/theme/fonts/`, consumed by Task 3's Theme builder.
|
||||
|
||||
- [ ] **Step 1: Download the fonts (all OFL, from the official google/fonts repo)**
|
||||
|
||||
```bash
|
||||
cd client/assets/theme/fonts
|
||||
BASE="https://github.com/google/fonts/raw/main/ofl"
|
||||
curl -fL -o EBGaramond-VariableFont_wght.ttf "$BASE/ebgaramond/EBGaramond%5Bwght%5D.ttf"
|
||||
curl -fL -o EBGaramond-Italic-VariableFont_wght.ttf "$BASE/ebgaramond/EBGaramond-Italic%5Bwght%5D.ttf"
|
||||
curl -fL -o ArchitectsDaughter-Regular.ttf "$BASE/architectsdaughter/ArchitectsDaughter-Regular.ttf"
|
||||
curl -fL -o JetBrainsMono-VariableFont_wght.ttf "$BASE/jetbrainsmono/JetBrainsMono%5Bwght%5D.ttf"
|
||||
ls -la
|
||||
```
|
||||
|
||||
Expected: four `.ttf` files, each non-empty (tens–hundreds of KB). If a URL 404s (repo layout changed), find the file under https://github.com/google/fonts/tree/main/ofl and adjust the path — the licences are OFL either way.
|
||||
|
||||
- [ ] **Step 2: Import them into Godot**
|
||||
|
||||
Run: `cd client && godot --headless --import >/dev/null 2>&1 || true`
|
||||
This generates the `.import` sidecars so `load()` resolves the fonts.
|
||||
|
||||
- [ ] **Step 3: Write the failing test**
|
||||
|
||||
```gdscript
|
||||
# client/tests/unit/test_fonts.gd
|
||||
extends "res://addons/gut/test.gd"
|
||||
|
||||
const FONTS := {
|
||||
"serif": "res://assets/theme/fonts/EBGaramond-VariableFont_wght.ttf",
|
||||
"serif_italic": "res://assets/theme/fonts/EBGaramond-Italic-VariableFont_wght.ttf",
|
||||
"accent": "res://assets/theme/fonts/ArchitectsDaughter-Regular.ttf",
|
||||
"mono": "res://assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf",
|
||||
}
|
||||
|
||||
|
||||
func test_all_fonts_exist_and_load_as_fontfile():
|
||||
for key in FONTS:
|
||||
var path: String = FONTS[key]
|
||||
assert_true(FileAccess.file_exists(path), "missing font: %s" % path)
|
||||
var f = load(path)
|
||||
assert_true(f is FontFile, "%s did not load as FontFile" % key)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `cd client && ./run_tests.sh -gtest=res://tests/unit/test_fonts.gd`
|
||||
Expected: PASS. (This test only fails before the files are present; after download+import it is green. If it fails on `load()`, re-run Step 2.)
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add client/assets/theme/fonts/ client/tests/unit/test_fonts.gd
|
||||
git commit -m "feat(theme): bundle EB Garamond, Architects Daughter, JetBrains Mono (OFL)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Theme keys + `game_theme.tres` (built by a committed script)
|
||||
|
||||
`theme_keys.gd` names every type-variation. `build_game_theme.gd` constructs the `Theme` programmatically (deterministic + reviewable — a `.tres` can't be hand-authored reliably headless) and saves it. The committed `.tres` is the artifact the game loads; the builder is its source of truth.
|
||||
|
||||
**Files:**
|
||||
- Create: `client/scripts/theme/theme_keys.gd`
|
||||
- Create: `client/scripts/theme/build_game_theme.gd`
|
||||
- Create (generated by the builder, then committed): `client/assets/theme/game_theme.tres`
|
||||
- Test: `client/tests/unit/test_theme_resource.gd`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Palette` (Task 1); the four fonts (Task 2).
|
||||
- Produces: `class_name ThemeKeys` with `const StringName` variation names: `PRIMARY_CTA`, `TAB`, `TAB_ACTIVE`, `CHIP`, `ITEM_TILE`, `ITEM_TILE_EMPTY`, `PARCHMENT_CARD`, `PARCHMENT_INSET`, `DARK_PANEL`. Produces `res://assets/theme/game_theme.tres` — a `Theme` whose base type for each variation is `PanelContainer` (cards/panels/tiles) or `Button` (CTA/tab/chip).
|
||||
|
||||
- [ ] **Step 1: Write `theme_keys.gd`**
|
||||
|
||||
```gdscript
|
||||
# client/scripts/theme/theme_keys.gd
|
||||
class_name ThemeKeys
|
||||
extends RefCounted
|
||||
## Type-variation names for game_theme.tres. Scripts set `theme_type_variation`
|
||||
## to one of these so a typo is a compile-time symbol error, not a silent miss.
|
||||
## §2: presentation.
|
||||
|
||||
const PRIMARY_CTA := &"PrimaryCTA"
|
||||
const TAB := &"Tab"
|
||||
const TAB_ACTIVE := &"TabActive"
|
||||
const CHIP := &"Chip"
|
||||
const ITEM_TILE := &"ItemTile"
|
||||
const ITEM_TILE_EMPTY := &"ItemTileEmpty"
|
||||
const PARCHMENT_CARD := &"ParchmentCard"
|
||||
const PARCHMENT_INSET := &"ParchmentInset"
|
||||
const DARK_PANEL := &"DarkPanel"
|
||||
|
||||
## Every variation name + the base Control type it decorates. The builder and the
|
||||
## test both iterate this so they can never drift apart.
|
||||
const ALL := {
|
||||
PRIMARY_CTA: "Button",
|
||||
TAB: "Button",
|
||||
TAB_ACTIVE: "Button",
|
||||
CHIP: "Button",
|
||||
ITEM_TILE: "PanelContainer",
|
||||
ITEM_TILE_EMPTY: "PanelContainer",
|
||||
PARCHMENT_CARD: "PanelContainer",
|
||||
PARCHMENT_INSET: "PanelContainer",
|
||||
DARK_PANEL: "PanelContainer",
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write the builder**
|
||||
|
||||
```gdscript
|
||||
# client/scripts/theme/build_game_theme.gd
|
||||
@tool
|
||||
extends SceneTree
|
||||
## One-shot Theme builder. Run headless:
|
||||
## godot --headless -s res://scripts/theme/build_game_theme.gd
|
||||
## Constructs game_theme.tres from Palette + the bundled fonts and saves it.
|
||||
## The builder is the source of truth; the .tres is a committed artifact.
|
||||
## §2: presentation.
|
||||
|
||||
const Palette = preload("res://scripts/theme/palette.gd")
|
||||
const ThemeKeys = preload("res://scripts/theme/theme_keys.gd")
|
||||
|
||||
const OUT := "res://assets/theme/game_theme.tres"
|
||||
const SERIF := "res://assets/theme/fonts/EBGaramond-VariableFont_wght.ttf"
|
||||
const ACCENT := "res://assets/theme/fonts/ArchitectsDaughter-Regular.ttf"
|
||||
const MONO := "res://assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf"
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var theme := Theme.new()
|
||||
_fonts(theme)
|
||||
_primary_cta(theme)
|
||||
_tabs(theme)
|
||||
_chip(theme)
|
||||
_tiles(theme)
|
||||
_cards_and_panels(theme)
|
||||
var err := ResourceSaver.save(theme, OUT)
|
||||
if err != OK:
|
||||
push_error("theme save failed: %d" % err)
|
||||
else:
|
||||
print("wrote ", OUT)
|
||||
quit()
|
||||
|
||||
|
||||
func _flat(bg: Color, border: Color, width := 1, radius := 4) -> StyleBoxFlat:
|
||||
var s := StyleBoxFlat.new()
|
||||
s.bg_color = bg
|
||||
s.border_color = border
|
||||
s.set_border_width_all(width)
|
||||
s.set_corner_radius_all(radius)
|
||||
s.content_margin_left = 12
|
||||
s.content_margin_right = 12
|
||||
s.content_margin_top = 6
|
||||
s.content_margin_bottom = 6
|
||||
return s
|
||||
|
||||
|
||||
func _fonts(theme: Theme) -> void:
|
||||
var serif: FontFile = load(SERIF)
|
||||
# Global defaults: prose serif at body size.
|
||||
theme.default_font = serif
|
||||
theme.default_font_size = 18
|
||||
# Heading role via the Label "Heading" variation.
|
||||
theme.set_type_variation(&"Heading", "Label")
|
||||
theme.set_font(&"font", &"Heading", serif)
|
||||
theme.set_font_size(&"font_size", &"Heading", 34)
|
||||
theme.set_color(&"font_color", &"Heading", Palette.INK_HEADING)
|
||||
# Accent (hand-inked) role.
|
||||
theme.set_type_variation(&"Accent", "Label")
|
||||
theme.set_font(&"font", &"Accent", load(ACCENT))
|
||||
theme.set_font_size(&"font_size", &"Accent", 24)
|
||||
theme.set_color(&"font_color", &"Accent", Palette.INK_GOLD)
|
||||
# Mono chrome role.
|
||||
theme.set_type_variation(&"Mono", "Label")
|
||||
theme.set_font(&"font", &"Mono", load(MONO))
|
||||
theme.set_font_size(&"font_size", &"Mono", 13)
|
||||
theme.set_color(&"font_color", &"Mono", Palette.MUTED_MONO)
|
||||
|
||||
|
||||
func _primary_cta(theme: Theme) -> void:
|
||||
var k := ThemeKeys.PRIMARY_CTA
|
||||
theme.set_type_variation(k, "Button")
|
||||
theme.set_stylebox(&"normal", k, _flat(Palette.BLOOD, Palette.BLOOD, 1, 4))
|
||||
theme.set_stylebox(&"hover", k, _flat(Palette.BLOOD_BRIGHT, Palette.BLOOD, 1, 4))
|
||||
theme.set_stylebox(&"pressed", k, _flat(Palette.BLOOD_DARK, Palette.BLOOD, 1, 4))
|
||||
theme.set_color(&"font_color", k, Palette.CREAM_BRIGHT)
|
||||
|
||||
|
||||
func _tabs(theme: Theme) -> void:
|
||||
theme.set_type_variation(ThemeKeys.TAB, "Button")
|
||||
theme.set_stylebox(&"normal", ThemeKeys.TAB, _flat(Palette.TAB_INACTIVE_BG, Palette.PARCHMENT_BORDER_2, 1, 14))
|
||||
theme.set_color(&"font_color", ThemeKeys.TAB, Palette.INK_MUTED)
|
||||
theme.set_type_variation(ThemeKeys.TAB_ACTIVE, "Button")
|
||||
theme.set_stylebox(&"normal", ThemeKeys.TAB_ACTIVE, _flat(Palette.BLOOD, Palette.BLOOD, 1, 14))
|
||||
theme.set_color(&"font_color", ThemeKeys.TAB_ACTIVE, Palette.CREAM_BRIGHT)
|
||||
|
||||
|
||||
func _chip(theme: Theme) -> void:
|
||||
# Base chip; the semantic colour (red/gold/grey) is applied per-use by script.
|
||||
var k := ThemeKeys.CHIP
|
||||
theme.set_type_variation(k, "Button")
|
||||
var s := _flat(Color(Palette.MUTED_MONO, 0.12), Palette.MUTED_MONO, 1, 3)
|
||||
theme.set_stylebox(&"normal", k, s)
|
||||
theme.set_font(&"font", k, load(MONO))
|
||||
theme.set_font_size(&"font_size", k, 11)
|
||||
theme.set_color(&"font_color", k, Palette.MUTED_MONO)
|
||||
|
||||
|
||||
func _tiles(theme: Theme) -> void:
|
||||
var filled := _flat(Palette.PARCHMENT_INSET, Palette.PARCHMENT_BORDER, 2, 6)
|
||||
theme.set_type_variation(ThemeKeys.ITEM_TILE, "PanelContainer")
|
||||
theme.set_stylebox(&"panel", ThemeKeys.ITEM_TILE, filled)
|
||||
var empty := _flat(Palette.PARCHMENT_INSET_2, Palette.PARCHMENT_BORDER_3, 1, 6)
|
||||
empty.draw_center = false
|
||||
theme.set_type_variation(ThemeKeys.ITEM_TILE_EMPTY, "PanelContainer")
|
||||
theme.set_stylebox(&"panel", ThemeKeys.ITEM_TILE_EMPTY, empty)
|
||||
|
||||
|
||||
func _cards_and_panels(theme: Theme) -> void:
|
||||
theme.set_type_variation(ThemeKeys.PARCHMENT_CARD, "PanelContainer")
|
||||
theme.set_stylebox(&"panel", ThemeKeys.PARCHMENT_CARD, _flat(Palette.PARCHMENT_CARD, Palette.PARCHMENT_BORDER, 1, 6))
|
||||
theme.set_type_variation(ThemeKeys.PARCHMENT_INSET, "PanelContainer")
|
||||
theme.set_stylebox(&"panel", ThemeKeys.PARCHMENT_INSET, _flat(Palette.PARCHMENT_INSET, Palette.PARCHMENT_BORDER_3, 1, 4))
|
||||
theme.set_type_variation(ThemeKeys.DARK_PANEL, "PanelContainer")
|
||||
theme.set_stylebox(&"panel", ThemeKeys.DARK_PANEL, _flat(Palette.DARK_PANEL_70, Palette.BORDER_4, 1, 4))
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run the builder to generate the `.tres`**
|
||||
|
||||
Run: `cd client && godot --headless --import >/dev/null 2>&1 || true && godot --headless -s res://scripts/theme/build_game_theme.gd`
|
||||
Expected: prints `wrote res://assets/theme/game_theme.tres`; the file now exists. No `theme save failed`.
|
||||
|
||||
- [ ] **Step 4: Write the failing test**
|
||||
|
||||
```gdscript
|
||||
# client/tests/unit/test_theme_resource.gd
|
||||
extends "res://addons/gut/test.gd"
|
||||
|
||||
const ThemeKeys = preload("res://scripts/theme/theme_keys.gd")
|
||||
const THEME_PATH := "res://assets/theme/game_theme.tres"
|
||||
|
||||
|
||||
func test_theme_loads():
|
||||
assert_true(FileAccess.file_exists(THEME_PATH), "theme not built — run build_game_theme.gd")
|
||||
var t = load(THEME_PATH)
|
||||
assert_true(t is Theme)
|
||||
|
||||
|
||||
func test_every_variation_resolves():
|
||||
var t: Theme = load(THEME_PATH)
|
||||
for key in ThemeKeys.ALL:
|
||||
var base: String = ThemeKeys.ALL[key]
|
||||
assert_eq(t.get_type_variation_base(key), StringName(base),
|
||||
"variation %s missing or wrong base" % key)
|
||||
|
||||
|
||||
func test_cta_has_normal_stylebox():
|
||||
var t: Theme = load(THEME_PATH)
|
||||
assert_true(t.has_stylebox(&"normal", ThemeKeys.PRIMARY_CTA))
|
||||
|
||||
|
||||
func test_default_font_is_set():
|
||||
var t: Theme = load(THEME_PATH)
|
||||
assert_true(t.default_font is FontFile)
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run tests to verify they pass**
|
||||
|
||||
Run: `cd client && ./run_tests.sh -gtest=res://tests/unit/test_theme_resource.gd`
|
||||
Expected: PASS — 4 tests green.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add client/scripts/theme/theme_keys.gd client/scripts/theme/build_game_theme.gd \
|
||||
client/assets/theme/game_theme.tres client/tests/unit/test_theme_resource.gd
|
||||
git commit -m "feat(theme): game_theme.tres (fonts + stylebox variations) via builder"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: The three signature surfaces (shaders + drop-in scripts)
|
||||
|
||||
Each surface = one `.gdshader` + one `class_name` `Control` subclass that assigns a `ShaderMaterial` with palette-derived uniforms. A trivial `.tscn` per surface makes it an instanceable drop-in. (A `StyleBox` can't run a shader — spec §2.)
|
||||
|
||||
**Files:**
|
||||
- Create: `client/assets/theme/shaders/parchment.gdshader`
|
||||
- Create: `client/assets/theme/shaders/dark_bay.gdshader`
|
||||
- Create: `client/assets/theme/shaders/vignette.gdshader`
|
||||
- Create: `client/scripts/theme/surfaces/parchment_panel.gd`
|
||||
- Create: `client/scripts/theme/surfaces/dark_bay.gd`
|
||||
- Create: `client/scripts/theme/surfaces/vignette_overlay.gd`
|
||||
- Create: `client/scenes/theme/surfaces/ParchmentPanel.tscn`
|
||||
- Create: `client/scenes/theme/surfaces/DarkBay.tscn`
|
||||
- Create: `client/scenes/theme/surfaces/VignetteOverlay.tscn`
|
||||
- Test: `client/tests/unit/test_surfaces.gd`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Palette` (Task 1).
|
||||
- Produces: `class_name ParchmentPanel extends Panel`, `class_name DarkBay extends Panel`, `class_name VignetteOverlay extends ColorRect` — each sets `self.material` to a `ShaderMaterial` in `_ready()`, uniforms fed from `Palette`. Instanceable via their `.tscn`.
|
||||
|
||||
- [ ] **Step 1: Write the three shaders**
|
||||
|
||||
```glsl
|
||||
// client/assets/theme/shaders/parchment.gdshader
|
||||
shader_type canvas_item;
|
||||
uniform vec4 top : source_color;
|
||||
uniform vec4 bottom : source_color;
|
||||
void fragment() {
|
||||
COLOR = mix(top, bottom, UV.y);
|
||||
}
|
||||
```
|
||||
|
||||
```glsl
|
||||
// client/assets/theme/shaders/dark_bay.gdshader
|
||||
shader_type canvas_item;
|
||||
uniform vec4 stripe_a : source_color;
|
||||
uniform vec4 stripe_b : source_color;
|
||||
uniform float band_px = 16.0; // 16px bands per README
|
||||
uniform float vignette = 0.55; // edge darkening strength
|
||||
void fragment() {
|
||||
// 135deg diagonal stripe: sum of pixel coords, banded.
|
||||
vec2 px = FRAGCOORD.xy;
|
||||
float diag = px.x + px.y;
|
||||
float band = mod(floor(diag / band_px), 2.0);
|
||||
vec4 base = mix(stripe_a, stripe_b, band);
|
||||
// radial vignette centred slightly high (README: 50% 38%).
|
||||
float d = distance(UV, vec2(0.5, 0.38));
|
||||
base.rgb *= 1.0 - clamp(d * vignette, 0.0, 1.0);
|
||||
COLOR = base;
|
||||
}
|
||||
```
|
||||
|
||||
```glsl
|
||||
// client/assets/theme/shaders/vignette.gdshader
|
||||
shader_type canvas_item;
|
||||
uniform float strength = 0.62;
|
||||
void fragment() {
|
||||
float d = distance(UV, vec2(0.5));
|
||||
COLOR = vec4(0.0, 0.0, 0.0, clamp(d * strength, 0.0, 1.0));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write the three surface scripts**
|
||||
|
||||
```gdscript
|
||||
# client/scripts/theme/surfaces/parchment_panel.gd
|
||||
class_name ParchmentPanel
|
||||
extends Panel
|
||||
## Drop-in parchment backdrop (mockups "Dark Bay + Parchment" motif). §2: presentation.
|
||||
const Palette = preload("res://scripts/theme/palette.gd")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var m := ShaderMaterial.new()
|
||||
m.shader = load("res://assets/theme/shaders/parchment.gdshader")
|
||||
m.set_shader_parameter("top", Palette.SHEET_TOP)
|
||||
m.set_shader_parameter("bottom", Palette.SHEET_BOTTOM)
|
||||
material = m
|
||||
```
|
||||
|
||||
```gdscript
|
||||
# client/scripts/theme/surfaces/dark_bay.gd
|
||||
class_name DarkBay
|
||||
extends Panel
|
||||
## Drop-in dark stripe+vignette bay backdrop. §2: presentation.
|
||||
const Palette = preload("res://scripts/theme/palette.gd")
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var m := ShaderMaterial.new()
|
||||
m.shader = load("res://assets/theme/shaders/dark_bay.gdshader")
|
||||
m.set_shader_parameter("stripe_a", Palette.BAY_STRIPE_A)
|
||||
m.set_shader_parameter("stripe_b", Palette.BAY_STRIPE_B)
|
||||
material = m
|
||||
```
|
||||
|
||||
```gdscript
|
||||
# client/scripts/theme/surfaces/vignette_overlay.gd
|
||||
class_name VignetteOverlay
|
||||
extends ColorRect
|
||||
## Drop-in edge-dim overlay for pause/dialogue dim. §2: presentation.
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
color = Color(0, 0, 0, 0) # base transparent; shader draws the dim
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var m := ShaderMaterial.new()
|
||||
m.shader = load("res://assets/theme/shaders/vignette.gdshader")
|
||||
material = m
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Author the three trivial scenes**
|
||||
|
||||
Each `.tscn` is one root node with the script attached, anchored full-rect. Create `ParchmentPanel.tscn`:
|
||||
|
||||
```
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://scripts/theme/surfaces/parchment_panel.gd" id="1"]
|
||||
[node name="ParchmentPanel" type="Panel"]
|
||||
anchors_preset = 15
|
||||
script = ExtResource("1")
|
||||
```
|
||||
|
||||
Create `DarkBay.tscn` (same shape, `type="Panel"`, script `dark_bay.gd`, node name `DarkBay`). Create `VignetteOverlay.tscn` (`type="ColorRect"`, script `vignette_overlay.gd`, node name `VignetteOverlay`).
|
||||
|
||||
- [ ] **Step 4: Write the failing test**
|
||||
|
||||
```gdscript
|
||||
# client/tests/unit/test_surfaces.gd
|
||||
extends "res://addons/gut/test.gd"
|
||||
|
||||
const SCENES := [
|
||||
"res://scenes/theme/surfaces/ParchmentPanel.tscn",
|
||||
"res://scenes/theme/surfaces/DarkBay.tscn",
|
||||
"res://scenes/theme/surfaces/VignetteOverlay.tscn",
|
||||
]
|
||||
|
||||
|
||||
func test_surfaces_instance_with_shader_material():
|
||||
for path in SCENES:
|
||||
var packed = load(path)
|
||||
assert_true(packed is PackedScene, "not a scene: %s" % path)
|
||||
var node = packed.instantiate()
|
||||
add_child_autofree(node) # triggers _ready()
|
||||
assert_true(node.material is ShaderMaterial, "%s has no ShaderMaterial" % path)
|
||||
assert_true(node.material.shader is Shader, "%s shader missing" % path)
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Import, then run the test**
|
||||
|
||||
Run: `cd client && godot --headless --import >/dev/null 2>&1 || true && ./run_tests.sh -gtest=res://tests/unit/test_surfaces.gd`
|
||||
Expected: PASS — all three instance and carry a compiled `ShaderMaterial`.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add client/assets/theme/shaders/ client/scripts/theme/surfaces/ \
|
||||
client/scenes/theme/surfaces/ client/tests/unit/test_surfaces.gd
|
||||
git commit -m "feat(theme): parchment/dark-bay/vignette shader surfaces as drop-in scenes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: The showcase proof (`theme_showcase.tscn`) + human confirm
|
||||
|
||||
The one screen a human opens to confirm "the look is one system." Renders the palette, every stylebox variation, all three fonts, the rarity ladder, sample stat bars, and the three surfaces. GUT can only assert it instances cleanly; the real proof is the human eyeball (spec §7).
|
||||
|
||||
**Files:**
|
||||
- Create: `client/scripts/theme/theme_showcase.gd`
|
||||
- Create: `client/scenes/theme/theme_showcase.tscn`
|
||||
- Test: `client/tests/unit/test_theme_showcase.gd`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `Palette`, `ThemeKeys`, `game_theme.tres`, the three surface scenes.
|
||||
- Produces: a runnable scene; no API for later tasks.
|
||||
|
||||
- [ ] **Step 1: Write the showcase script**
|
||||
|
||||
```gdscript
|
||||
# client/scripts/theme/theme_showcase.gd
|
||||
extends Control
|
||||
## The eyeball proof for the shared Theme (spec §7). Builds every token, stylebox,
|
||||
## font, surface, and bar on one scrollable screen. No AI, no state. §2: presentation.
|
||||
const Palette = preload("res://scripts/theme/palette.gd")
|
||||
const ThemeKeys = preload("res://scripts/theme/theme_keys.gd")
|
||||
const GAME_THEME := "res://assets/theme/game_theme.tres"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
theme = load(GAME_THEME)
|
||||
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_child(scroll)
|
||||
var col := VBoxContainer.new()
|
||||
col.custom_minimum_size = Vector2(1900, 0)
|
||||
col.add_theme_constant_override("separation", 16)
|
||||
scroll.add_child(col)
|
||||
|
||||
col.add_child(_heading("SHARED THEME — SHOWCASE"))
|
||||
col.add_child(_swatches())
|
||||
col.add_child(_variations())
|
||||
col.add_child(_rarity())
|
||||
col.add_child(_bars())
|
||||
col.add_child(_surfaces())
|
||||
|
||||
|
||||
func _heading(text: String) -> Label:
|
||||
var l := Label.new()
|
||||
l.text = text
|
||||
l.theme_type_variation = &"Heading"
|
||||
return l
|
||||
|
||||
|
||||
func _swatches() -> Control:
|
||||
var grid := GridContainer.new()
|
||||
grid.columns = 8
|
||||
for c in [Palette.STAGE_1, Palette.BAY_STRIPE_A, Palette.DARK_PANEL_70,
|
||||
Palette.SHEET_TOP, Palette.PARCHMENT_CARD, Palette.BLOOD, Palette.GOLD,
|
||||
Palette.STEEL, Palette.GREEN, Palette.INK_BODY, Palette.CREAM]:
|
||||
var r := ColorRect.new()
|
||||
r.color = c
|
||||
r.custom_minimum_size = Vector2(120, 60)
|
||||
grid.add_child(r)
|
||||
return grid
|
||||
|
||||
|
||||
func _variations() -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 12)
|
||||
var cta := Button.new(); cta.text = "ENTER THE WORLD"; cta.theme_type_variation = ThemeKeys.PRIMARY_CTA
|
||||
var tab := Button.new(); tab.text = "INACTIVE"; tab.theme_type_variation = ThemeKeys.TAB
|
||||
var tab_a := Button.new(); tab_a.text = "ACTIVE"; tab_a.theme_type_variation = ThemeKeys.TAB_ACTIVE
|
||||
var chip := Button.new(); chip.text = "INTIMIDATE · STR"; chip.theme_type_variation = ThemeKeys.CHIP
|
||||
for b in [cta, tab, tab_a, chip]:
|
||||
row.add_child(b)
|
||||
var card := PanelContainer.new(); card.theme_type_variation = ThemeKeys.PARCHMENT_CARD
|
||||
card.custom_minimum_size = Vector2(160, 80)
|
||||
row.add_child(card)
|
||||
return row
|
||||
|
||||
|
||||
func _rarity() -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
for i in range(5):
|
||||
var tile := PanelContainer.new()
|
||||
tile.theme_type_variation = ThemeKeys.ITEM_TILE
|
||||
tile.custom_minimum_size = Vector2(72, 72)
|
||||
var bar := ColorRect.new()
|
||||
bar.color = Palette.rarity_color(i)
|
||||
bar.custom_minimum_size = Vector2(60, 4)
|
||||
tile.add_child(bar)
|
||||
row.add_child(tile)
|
||||
return row
|
||||
|
||||
|
||||
func _bars() -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
for kind in [&"hp", &"mp", &"stamina"]:
|
||||
var pair = Palette.stat_bar(kind)
|
||||
var bar := ColorRect.new()
|
||||
bar.color = pair[0]
|
||||
bar.custom_minimum_size = Vector2(200, 18)
|
||||
row.add_child(bar)
|
||||
return row
|
||||
|
||||
|
||||
func _surfaces() -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
row.custom_minimum_size = Vector2(0, 240)
|
||||
for path in ["res://scenes/theme/surfaces/ParchmentPanel.tscn",
|
||||
"res://scenes/theme/surfaces/DarkBay.tscn"]:
|
||||
var s = load(path).instantiate()
|
||||
s.custom_minimum_size = Vector2(600, 220)
|
||||
row.add_child(s)
|
||||
return row
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Author the scene**
|
||||
|
||||
Create `client/scenes/theme/theme_showcase.tscn`:
|
||||
|
||||
```
|
||||
[gd_scene load_steps=2 format=3]
|
||||
[ext_resource type="Script" path="res://scripts/theme/theme_showcase.gd" id="1"]
|
||||
[node name="ThemeShowcase" type="Control"]
|
||||
anchors_preset = 15
|
||||
script = ExtResource("1")
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Write the smoke test**
|
||||
|
||||
```gdscript
|
||||
# client/tests/unit/test_theme_showcase.gd
|
||||
extends "res://addons/gut/test.gd"
|
||||
|
||||
|
||||
func test_showcase_instances_without_error():
|
||||
var packed = load("res://scenes/theme/theme_showcase.tscn")
|
||||
assert_true(packed is PackedScene)
|
||||
var node = packed.instantiate()
|
||||
add_child_autofree(node) # runs _ready(); any engine error here fails the suite
|
||||
assert_true(node is Control)
|
||||
assert_true(node.theme is Theme, "showcase did not apply game_theme")
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Import, then run the test**
|
||||
|
||||
Run: `cd client && godot --headless --import >/dev/null 2>&1 || true && ./run_tests.sh -gtest=res://tests/unit/test_theme_showcase.gd`
|
||||
Expected: PASS — instances cleanly, theme applied, 0 engine errors.
|
||||
|
||||
- [ ] **Step 5: Run the FULL suite (nothing regressed)**
|
||||
|
||||
Run: `cd client && ./run_tests.sh`
|
||||
Expected: all prior tests (110+) plus the new theme tests PASS, 0 failures, 0 engine errors.
|
||||
|
||||
- [ ] **Step 6: HUMAN CONFIRM — the eyeball proof (spec §7, cannot be skipped)**
|
||||
|
||||
Ask the human to open the showcase in the editor and confirm it reads as the mockups' system:
|
||||
|
||||
```
|
||||
godot res://scenes/theme/theme_showcase.tscn
|
||||
```
|
||||
|
||||
Human checks: parchment reads as warm aged paper; the dark bay shows the diagonal stripe + vignette; EB Garamond looks like a bookish serif; the CTA is blood-red with a cream label; tabs, chip, card, rarity tiles, and stat bars match the mock palette. **Do not mark the milestone done until the human signs off.** Record tweaks (uniform values, sizes) as follow-up edits to `palette.gd` / the builder, then re-run the builder + full suite.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add client/scripts/theme/theme_showcase.gd client/scenes/theme/theme_showcase.tscn \
|
||||
client/tests/unit/test_theme_showcase.gd
|
||||
git commit -m "feat(theme): showcase harness — the human-eyeball proof"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes for the executor
|
||||
|
||||
- **Branch:** this is non-doc code — work on a `feature/shared-theme` branch off `dev`, never commit to `dev`/`master` directly (charter §18). The human does the `--no-ff` merge after the eyeball sign-off.
|
||||
- **`.uid` files:** Godot 4.7 writes a `.gd.uid` beside each new script on import. Add them with their script (the repo already tracks `.uid`s).
|
||||
- **If a font URL 404s:** the google/fonts repo occasionally re-lays-out; find the OFL dir in-browser and adjust. Any of the three families can be swapped for another OFL face of the same character without a design change (spec §3) — but update `palette.gd`'s comment and the builder paths together.
|
||||
- **Builder re-runs:** any palette/stylebox change means re-running `build_game_theme.gd` to regenerate `game_theme.tres`, then re-running the suite. The builder is the source of truth; never hand-edit the `.tres`.
|
||||
196
docs/superpowers/specs/2026-07-10-shared-theme-design.md
Normal file
196
docs/superpowers/specs/2026-07-10-shared-theme-design.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# Shared Theme — design
|
||||
|
||||
**Milestone:** M3 (Visual foundation & shell), item **M3-a** — the first of three
|
||||
cycles (Theme → Main Window shell 2a → Title). See `docs/roadmap.md` and
|
||||
`CLAUDE.md` §16.
|
||||
|
||||
**§2 side:** n/a — pure presentation. Owns no game state, makes no AI/network
|
||||
call. It is the visual system every later screen inherits.
|
||||
|
||||
**Depends on:** nothing (foundation). **Unblocks:** every M3+ screen.
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Deliver the one shared visual system the mockups describe (`/mockups/README.md`,
|
||||
"Global Design System") as reusable Godot resources, so every screen built after
|
||||
this pulls the same palette, fonts, styleboxes, and signature surfaces — "the
|
||||
look is one system." Nothing renders a real screen yet; this is the toolbox the
|
||||
screens are built from.
|
||||
|
||||
Because a Theme has no behavior, its **proof is a showcase harness** a human
|
||||
eyeballs in the editor — the same "live-proven / human-confirmed" bar M0–M2 held.
|
||||
|
||||
## 2. The core architectural split
|
||||
|
||||
A Godot `StyleBox` **cannot run a shader** — the engine draws styleboxes itself.
|
||||
So the deliverable splits in two, each half using the right tool:
|
||||
|
||||
| Concern | Mechanism | Covers |
|
||||
|---|---|---|
|
||||
| **Crisp bordered UI** | `game_theme.tres` (StyleBoxFlat + fonts) | buttons/CTA, tabs active/inactive, chips/tags, item tiles, cards, insets, dark panels; all three font families at their sizes |
|
||||
| **The 3 signature surfaces** | `ShaderMaterial` on `Panel`/`ColorRect`, wrapped as drop-in mini-scenes | parchment sheet, dark diagonal-stripe bay, radial vignette overlay |
|
||||
| **Code-driven color** | `palette.gd` consts | rarity bars, HP/MP/stamina gradients, disposition/turn-order colour coding |
|
||||
|
||||
The "procedural shaders" decision applies to the **three big backdrops only**. A
|
||||
chip or button is flat-bordered and fits `StyleBoxFlat` natively; a shader on
|
||||
every chip would be wasted work.
|
||||
|
||||
`palette.gd` is the **single source of truth** for every hex value. The
|
||||
`game_theme.tres` styleboxes are authored to those same values; the shaders take
|
||||
them as uniforms. **One palette, three consumers.**
|
||||
|
||||
## 3. Fonts (redistribution constraint)
|
||||
|
||||
Godot exports must **bundle** their fonts. The mock's three families become:
|
||||
|
||||
| Mock role | Bundled face | Licence | Notes |
|
||||
|---|---|---|---|
|
||||
| Georgia (prose / headings / logo) | **EB Garamond** (Regular, Italic, Bold) | OFL | Redistributable. Georgia is Microsoft-proprietary and **cannot** ship. EB Garamond is a warm old-style serif — the "aged tome" DM-prose voice. Its strong italic carries the narrative-body *italic* the mock uses. |
|
||||
| Architects Daughter (hand-inked accent) | **Architects Daughter** (Regular) | OFL | Ships as-is; the mock already used the Google face. Nameplates, big stat numbers, DM-header labels. |
|
||||
| Monospace (UI chrome) | **JetBrains Mono** (Regular) | OFL | Default pick (clean, wide, reads well UPPERCASE with letter-spacing); swappable for another OFL mono without design change. Labels, keybinds, numeric readouts, tags. |
|
||||
|
||||
## 4. File layout
|
||||
|
||||
```
|
||||
client/
|
||||
scripts/theme/
|
||||
palette.gd # const Color tokens — the source of truth
|
||||
theme_keys.gd # const StringNames for type-variations, so scripts never
|
||||
# hardcode Theme string keys
|
||||
assets/theme/
|
||||
fonts/
|
||||
EBGaramond-Regular.ttf EBGaramond-Italic.ttf EBGaramond-Bold.ttf
|
||||
ArchitectsDaughter-Regular.ttf
|
||||
JetBrainsMono-Regular.ttf
|
||||
shaders/
|
||||
parchment.gdshader # vertical gradient #ece2ca -> #e4d8bd
|
||||
dark_bay.gdshader # 135deg repeating stripe #26221c/#221e18 + radial vignette
|
||||
vignette.gdshader # standalone dim/blur-edge overlay (pause, dialogue)
|
||||
game_theme.tres # the Theme resource — fonts, sizes, StyleBoxFlat variations
|
||||
scenes/theme/
|
||||
surfaces/
|
||||
ParchmentPanel.tscn # Panel + parchment material (drop-in backdrop)
|
||||
DarkBay.tscn # Panel + dark_bay material
|
||||
VignetteOverlay.tscn # ColorRect + vignette material
|
||||
theme_showcase.tscn # the proof: every token, stylebox, font, surface, one screen
|
||||
theme_showcase.gd
|
||||
tests/unit/
|
||||
test_palette.gd
|
||||
test_theme_resource.gd
|
||||
```
|
||||
|
||||
Follows existing conventions: `scripts/<domain>/`, harness scenes under
|
||||
`scenes/`, `tests/unit/test_*.gd`.
|
||||
|
||||
## 5. Contents
|
||||
|
||||
### 5.1 `palette.gd`
|
||||
|
||||
Every hex from the README's "Color tokens" section as `const Color`, grouped by
|
||||
role:
|
||||
|
||||
- **Dark backgrounds** — near-black stage, bay stripe pair, translucent panels,
|
||||
the dark-border ladder (`#6b6152`…`#1a1512`).
|
||||
- **Parchment** — sheet gradient stops, card fills + insets, parchment borders.
|
||||
- **Ink** — heading / body / muted / label / gold-accent inks.
|
||||
- **Text-on-dark** — bone/cream primaries, secondaries, muted monos.
|
||||
- **Accents** — blood-red set, gold/brass set, steel blue-teal set, green set.
|
||||
- **Stat bars** — HP / MP / stamina gradient endpoints + track/border.
|
||||
- **Rarity** — the five rarity colours as an ordered array.
|
||||
|
||||
Plus the small helpers the code-driven cases need:
|
||||
|
||||
- `rarity_color(rarity) -> Color`
|
||||
- stat-bar gradient stops for HP / MP / stamina (returned as a pair, or two
|
||||
named consts each).
|
||||
|
||||
This is the file the whole game reads from for any colour a Theme can't express.
|
||||
|
||||
### 5.2 `theme_keys.gd`
|
||||
|
||||
`const StringName`s for every type-variation name (`PrimaryCTA`, `Tab`,
|
||||
`TabActive`, `Chip`, `ItemTile`, `ParchmentCard`, `DarkPanel`, …), so scripts
|
||||
that set `theme_type_variation` reference a constant, never a raw string that can
|
||||
typo silently.
|
||||
|
||||
### 5.3 `game_theme.tres`
|
||||
|
||||
The six reusable pieces from the README, each authored as a Godot **type
|
||||
variation** (applied via `theme_type_variation` on any Control):
|
||||
|
||||
- **PrimaryCTA** — blood-red CTA button (gradient-look `StyleBoxFlat`, `#8f3a34`
|
||||
border, cream text, brightened hover).
|
||||
- **Tab** / **TabActive** — inactive (parchment `#efe6cf` + `#cbb684` border,
|
||||
muted text) vs active (solid `#8f3a34`, cream text).
|
||||
- **Chip** — mono-uppercase tag base; the *semantic* colour (red/gold/grey) is
|
||||
set by the consuming script from `palette.gd`, since it varies per use.
|
||||
- **ItemTile** — rounded square, 2px border (rarity colour applied by script),
|
||||
plus a recessed dashed **empty** variant.
|
||||
- **ParchmentCard** — `#f1e8d2` fill, `#bfa878` border, and a darker inset
|
||||
variant.
|
||||
- **DarkPanel** — translucent `rgba(18,15,11,.7)`.
|
||||
|
||||
Plus the three font families wired into the Theme's default / heading / mono
|
||||
font roles at the README's sizes and letter-spacing.
|
||||
|
||||
### 5.4 The three surface scenes
|
||||
|
||||
Each wraps one shader as a **one-line drop-in**: later screens instance
|
||||
`DarkBay.tscn` / `ParchmentPanel.tscn` / `VignetteOverlay.tscn` instead of
|
||||
rebuilding a backdrop. Shader uniforms default to the `palette.gd` values (baked
|
||||
into the `.tscn`/material), so the surfaces match the tokens without a runtime
|
||||
dependency on the script.
|
||||
|
||||
- `parchment.gdshader` — vertical gradient `#ece2ca → #e4d8bd`.
|
||||
- `dark_bay.gdshader` — 135° repeating stripe `#26221c` / `#221e18` (16px bands)
|
||||
plus the radial vignette overlay.
|
||||
- `vignette.gdshader` — standalone edge-dim overlay for the pause/dialogue dim.
|
||||
|
||||
## 6. The proof — `theme_showcase.tscn`
|
||||
|
||||
A single scrollable screen rendering, for a human to eyeball in the editor:
|
||||
|
||||
- the full palette as labelled swatches,
|
||||
- every stylebox type-variation (CTA, both tabs, chips in each semantic colour,
|
||||
filled + empty item tiles, parchment card + inset, dark panel),
|
||||
- all three fonts at their real sizes (heading, prose body italic, hand-inked
|
||||
accent, mono chrome),
|
||||
- the rarity ladder,
|
||||
- a sample HP / MP / stamina bar,
|
||||
- the three shader surfaces side by side.
|
||||
|
||||
Pure presentation — no AI, no network, no game state. This is what confirms "the
|
||||
look is one system" before any real screen is built.
|
||||
|
||||
## 7. Testing
|
||||
|
||||
- **`test_palette.gd`** — asserts the documented tokens exist and are `Color`;
|
||||
`rarity_color()` maps each rarity to the right colour and handles out-of-range
|
||||
safely; the stat-bar stop helpers return the documented values.
|
||||
- **`test_theme_resource.gd`** — `game_theme.tres` loads without error, and every
|
||||
named type-variation in `theme_keys.gd` resolves on the Theme (no dangling
|
||||
key).
|
||||
|
||||
**Visual correctness is not GUT-testable** — it is an explicit **human-confirm**
|
||||
step against `theme_showcase.tscn`, exactly as M2's live model proofs were
|
||||
human-confirmed. The spec is not "done" until a human has opened the showcase and
|
||||
signed off that it reads as the mockups' system.
|
||||
|
||||
## 8. Explicitly out of scope (this cycle)
|
||||
|
||||
- Any real screen (the Main Window shell 2a and Title are the next two M3
|
||||
cycles).
|
||||
- Real art of any kind — portraits, key art, iso/battlefield renders, item/ability
|
||||
icons remain the mocks' dashed **art slots**, filled later.
|
||||
- Responsive/stretch tuning beyond the project's existing 1920×1080 canvas
|
||||
assumption (`canvas_items` stretch is a shell-cycle concern, not a Theme one).
|
||||
- Baked-PNG texture variants — the three surfaces are shader-only this cycle.
|
||||
- Animation (ember particles, pulse/`cturn` keyframes) — those belong to the
|
||||
screens that use them, not the Theme.
|
||||
|
||||
## 9. Reconciliations touched
|
||||
|
||||
None. The two flagged reconciliations (damage determinism → M5, class names →
|
||||
M4) do not touch presentation. This cycle introduces no charter conflict.
|
||||
Reference in New Issue
Block a user