diff --git a/client/assets/theme/fonts/ArchitectsDaughter-Regular.ttf b/client/assets/theme/fonts/ArchitectsDaughter-Regular.ttf new file mode 100644 index 0000000..d0d88ba Binary files /dev/null and b/client/assets/theme/fonts/ArchitectsDaughter-Regular.ttf differ diff --git a/client/assets/theme/fonts/ArchitectsDaughter-Regular.ttf.import b/client/assets/theme/fonts/ArchitectsDaughter-Regular.ttf.import new file mode 100644 index 0000000..a937419 --- /dev/null +++ b/client/assets/theme/fonts/ArchitectsDaughter-Regular.ttf.import @@ -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={} diff --git a/client/assets/theme/fonts/EBGaramond-Italic-VariableFont_wght.ttf b/client/assets/theme/fonts/EBGaramond-Italic-VariableFont_wght.ttf new file mode 100644 index 0000000..f9ad996 Binary files /dev/null and b/client/assets/theme/fonts/EBGaramond-Italic-VariableFont_wght.ttf differ diff --git a/client/assets/theme/fonts/EBGaramond-Italic-VariableFont_wght.ttf.import b/client/assets/theme/fonts/EBGaramond-Italic-VariableFont_wght.ttf.import new file mode 100644 index 0000000..8393484 --- /dev/null +++ b/client/assets/theme/fonts/EBGaramond-Italic-VariableFont_wght.ttf.import @@ -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={} diff --git a/client/assets/theme/fonts/EBGaramond-VariableFont_wght.ttf b/client/assets/theme/fonts/EBGaramond-VariableFont_wght.ttf new file mode 100644 index 0000000..52d34b8 Binary files /dev/null and b/client/assets/theme/fonts/EBGaramond-VariableFont_wght.ttf differ diff --git a/client/assets/theme/fonts/EBGaramond-VariableFont_wght.ttf.import b/client/assets/theme/fonts/EBGaramond-VariableFont_wght.ttf.import new file mode 100644 index 0000000..903ee32 --- /dev/null +++ b/client/assets/theme/fonts/EBGaramond-VariableFont_wght.ttf.import @@ -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={} diff --git a/client/assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf b/client/assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf new file mode 100644 index 0000000..aa310be Binary files /dev/null and b/client/assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf differ diff --git a/client/assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf.import b/client/assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf.import new file mode 100644 index 0000000..5809796 --- /dev/null +++ b/client/assets/theme/fonts/JetBrainsMono-VariableFont_wght.ttf.import @@ -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={} diff --git a/client/assets/theme/game_theme.tres b/client/assets/theme/game_theme.tres new file mode 100644 index 0000000..8eef967 --- /dev/null +++ b/client/assets/theme/game_theme.tres @@ -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") diff --git a/client/assets/theme/shaders/dark_bay.gdshader b/client/assets/theme/shaders/dark_bay.gdshader new file mode 100644 index 0000000..94c36fb --- /dev/null +++ b/client/assets/theme/shaders/dark_bay.gdshader @@ -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; +} diff --git a/client/assets/theme/shaders/dark_bay.gdshader.uid b/client/assets/theme/shaders/dark_bay.gdshader.uid new file mode 100644 index 0000000..d10aef5 --- /dev/null +++ b/client/assets/theme/shaders/dark_bay.gdshader.uid @@ -0,0 +1 @@ +uid://jb8clyworwr1 diff --git a/client/assets/theme/shaders/parchment.gdshader b/client/assets/theme/shaders/parchment.gdshader new file mode 100644 index 0000000..8532d89 --- /dev/null +++ b/client/assets/theme/shaders/parchment.gdshader @@ -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); +} diff --git a/client/assets/theme/shaders/parchment.gdshader.uid b/client/assets/theme/shaders/parchment.gdshader.uid new file mode 100644 index 0000000..4aef44d --- /dev/null +++ b/client/assets/theme/shaders/parchment.gdshader.uid @@ -0,0 +1 @@ +uid://b2heb6jhvwxxl diff --git a/client/assets/theme/shaders/vignette.gdshader b/client/assets/theme/shaders/vignette.gdshader new file mode 100644 index 0000000..ec1cb54 --- /dev/null +++ b/client/assets/theme/shaders/vignette.gdshader @@ -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)); +} diff --git a/client/assets/theme/shaders/vignette.gdshader.uid b/client/assets/theme/shaders/vignette.gdshader.uid new file mode 100644 index 0000000..5334880 --- /dev/null +++ b/client/assets/theme/shaders/vignette.gdshader.uid @@ -0,0 +1 @@ +uid://b4tami4peyxaw diff --git a/client/project.godot b/client/project.godot index 9c87665..3b737ae 100644 --- a/client/project.godot +++ b/client/project.godot @@ -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" diff --git a/client/scenes/theme/surfaces/DarkBay.tscn b/client/scenes/theme/surfaces/DarkBay.tscn new file mode 100644 index 0000000..0ada4c5 --- /dev/null +++ b/client/scenes/theme/surfaces/DarkBay.tscn @@ -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") diff --git a/client/scenes/theme/surfaces/ParchmentPanel.tscn b/client/scenes/theme/surfaces/ParchmentPanel.tscn new file mode 100644 index 0000000..9ca9029 --- /dev/null +++ b/client/scenes/theme/surfaces/ParchmentPanel.tscn @@ -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") diff --git a/client/scenes/theme/surfaces/VignetteOverlay.tscn b/client/scenes/theme/surfaces/VignetteOverlay.tscn new file mode 100644 index 0000000..4e22b68 --- /dev/null +++ b/client/scenes/theme/surfaces/VignetteOverlay.tscn @@ -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") diff --git a/client/scenes/theme/theme_showcase.tscn b/client/scenes/theme/theme_showcase.tscn new file mode 100644 index 0000000..531fed8 --- /dev/null +++ b/client/scenes/theme/theme_showcase.tscn @@ -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") diff --git a/client/scripts/theme/build_game_theme.gd b/client/scripts/theme/build_game_theme.gd new file mode 100644 index 0000000..96b08f6 --- /dev/null +++ b/client/scripts/theme/build_game_theme.gd @@ -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)) diff --git a/client/scripts/theme/build_game_theme.gd.uid b/client/scripts/theme/build_game_theme.gd.uid new file mode 100644 index 0000000..c01e0a5 --- /dev/null +++ b/client/scripts/theme/build_game_theme.gd.uid @@ -0,0 +1 @@ +uid://drualc64s2x3p diff --git a/client/scripts/theme/palette.gd b/client/scripts/theme/palette.gd new file mode 100644 index 0000000..401ef83 --- /dev/null +++ b/client/scripts/theme/palette.gd @@ -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] diff --git a/client/scripts/theme/palette.gd.uid b/client/scripts/theme/palette.gd.uid new file mode 100644 index 0000000..c635ce2 --- /dev/null +++ b/client/scripts/theme/palette.gd.uid @@ -0,0 +1 @@ +uid://d4bext4g7reql diff --git a/client/scripts/theme/surfaces/dark_bay.gd b/client/scripts/theme/surfaces/dark_bay.gd new file mode 100644 index 0000000..a48abd9 --- /dev/null +++ b/client/scripts/theme/surfaces/dark_bay.gd @@ -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 diff --git a/client/scripts/theme/surfaces/dark_bay.gd.uid b/client/scripts/theme/surfaces/dark_bay.gd.uid new file mode 100644 index 0000000..fc4fabf --- /dev/null +++ b/client/scripts/theme/surfaces/dark_bay.gd.uid @@ -0,0 +1 @@ +uid://24c8pl0yx8wu diff --git a/client/scripts/theme/surfaces/parchment_panel.gd b/client/scripts/theme/surfaces/parchment_panel.gd new file mode 100644 index 0000000..7f1c83e --- /dev/null +++ b/client/scripts/theme/surfaces/parchment_panel.gd @@ -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 diff --git a/client/scripts/theme/surfaces/parchment_panel.gd.uid b/client/scripts/theme/surfaces/parchment_panel.gd.uid new file mode 100644 index 0000000..6be5eb2 --- /dev/null +++ b/client/scripts/theme/surfaces/parchment_panel.gd.uid @@ -0,0 +1 @@ +uid://cnv1r00ane2la diff --git a/client/scripts/theme/surfaces/vignette_overlay.gd b/client/scripts/theme/surfaces/vignette_overlay.gd new file mode 100644 index 0000000..ea172c8 --- /dev/null +++ b/client/scripts/theme/surfaces/vignette_overlay.gd @@ -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 diff --git a/client/scripts/theme/surfaces/vignette_overlay.gd.uid b/client/scripts/theme/surfaces/vignette_overlay.gd.uid new file mode 100644 index 0000000..181a2f4 --- /dev/null +++ b/client/scripts/theme/surfaces/vignette_overlay.gd.uid @@ -0,0 +1 @@ +uid://7bc7agrl6oi0 diff --git a/client/scripts/theme/theme_keys.gd b/client/scripts/theme/theme_keys.gd new file mode 100644 index 0000000..0a11113 --- /dev/null +++ b/client/scripts/theme/theme_keys.gd @@ -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", +} diff --git a/client/scripts/theme/theme_keys.gd.uid b/client/scripts/theme/theme_keys.gd.uid new file mode 100644 index 0000000..e0f33e1 --- /dev/null +++ b/client/scripts/theme/theme_keys.gd.uid @@ -0,0 +1 @@ +uid://upoh5yt3dd4i diff --git a/client/scripts/theme/theme_showcase.gd b/client/scripts/theme/theme_showcase.gd new file mode 100644 index 0000000..48fe2fb --- /dev/null +++ b/client/scripts/theme/theme_showcase.gd @@ -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 diff --git a/client/scripts/theme/theme_showcase.gd.uid b/client/scripts/theme/theme_showcase.gd.uid new file mode 100644 index 0000000..bde8f3e --- /dev/null +++ b/client/scripts/theme/theme_showcase.gd.uid @@ -0,0 +1 @@ +uid://ciqf5caopnsh3 diff --git a/client/tests/unit/test_fonts.gd b/client/tests/unit/test_fonts.gd new file mode 100644 index 0000000..b038c62 --- /dev/null +++ b/client/tests/unit/test_fonts.gd @@ -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) diff --git a/client/tests/unit/test_fonts.gd.uid b/client/tests/unit/test_fonts.gd.uid new file mode 100644 index 0000000..5a1e3a5 --- /dev/null +++ b/client/tests/unit/test_fonts.gd.uid @@ -0,0 +1 @@ +uid://ck81t1k5xlvgu diff --git a/client/tests/unit/test_palette.gd b/client/tests/unit/test_palette.gd new file mode 100644 index 0000000..1cb204f --- /dev/null +++ b/client/tests/unit/test_palette.gd @@ -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) diff --git a/client/tests/unit/test_palette.gd.uid b/client/tests/unit/test_palette.gd.uid new file mode 100644 index 0000000..484d2a2 --- /dev/null +++ b/client/tests/unit/test_palette.gd.uid @@ -0,0 +1 @@ +uid://w5blbshxn630 diff --git a/client/tests/unit/test_surfaces.gd b/client/tests/unit/test_surfaces.gd new file mode 100644 index 0000000..7edb200 --- /dev/null +++ b/client/tests/unit/test_surfaces.gd @@ -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) diff --git a/client/tests/unit/test_surfaces.gd.uid b/client/tests/unit/test_surfaces.gd.uid new file mode 100644 index 0000000..a1976d9 --- /dev/null +++ b/client/tests/unit/test_surfaces.gd.uid @@ -0,0 +1 @@ +uid://csc3r7lqoe83d diff --git a/client/tests/unit/test_theme_resource.gd b/client/tests/unit/test_theme_resource.gd new file mode 100644 index 0000000..212c651 --- /dev/null +++ b/client/tests/unit/test_theme_resource.gd @@ -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]) diff --git a/client/tests/unit/test_theme_resource.gd.uid b/client/tests/unit/test_theme_resource.gd.uid new file mode 100644 index 0000000..7a8e942 --- /dev/null +++ b/client/tests/unit/test_theme_resource.gd.uid @@ -0,0 +1 @@ +uid://bjwiotv7oid2t diff --git a/client/tests/unit/test_theme_showcase.gd b/client/tests/unit/test_theme_showcase.gd new file mode 100644 index 0000000..c05b017 --- /dev/null +++ b/client/tests/unit/test_theme_showcase.gd @@ -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") diff --git a/client/tests/unit/test_theme_showcase.gd.uid b/client/tests/unit/test_theme_showcase.gd.uid new file mode 100644 index 0000000..5feaeb2 --- /dev/null +++ b/client/tests/unit/test_theme_showcase.gd.uid @@ -0,0 +1 @@ +uid://e4p6b65e14mw