Blend generated terrain textures
@@ -36,6 +36,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr
|
|||||||
- Pre-battle briefing shows the campaign chapter, chapter battle number, location, victory/defeat summary, and first-clear reward preview.
|
- Pre-battle briefing shows the campaign chapter, chapter battle number, location, victory/defeat summary, and first-clear reward preview.
|
||||||
- Pre-battle prep commands use compact icon buttons with active selection state and Korean hover labels for chapters, shop, talks, armory, roster, formation, and save records.
|
- Pre-battle prep commands use compact icon buttons with active selection state and Korean hover labels for chapters, shop, talks, armory, roster, formation, and save records.
|
||||||
- Battle, top-toolbar, and pre-battle command buttons now prefer generated high-resolution bronze/parchment icon art under `art/ui/icons`, with procedural icon drawing kept only as a fallback.
|
- Battle, top-toolbar, and pre-battle command buttons now prefer generated high-resolution bronze/parchment icon art under `art/ui/icons`, with procedural icon drawing kept only as a fallback.
|
||||||
|
- Battle maps blend generated terrain texture tiles for plains, forests, hills, wasteland, roads, water, villages, and castles over the high-resolution battlefield backdrop.
|
||||||
- Core UI panels now use a calmer ink/wood/jade palette with softer rounded corners and lighter button frames instead of the earlier yellow, hard-edged frame treatment.
|
- Core UI panels now use a calmer ink/wood/jade palette with softer rounded corners and lighter button frames instead of the earlier yellow, hard-edged frame treatment.
|
||||||
- Pre-battle Chapters overview shows story-arc progress with map thumbnail rows, compact status badges, hover details, and can open completed or current battles.
|
- Pre-battle Chapters overview shows story-arc progress with map thumbnail rows, compact status badges, hover details, and can open completed or current battles.
|
||||||
- Pre-battle Save menu can write and load one manual campaign checkpoint separate from the automatic save, using compact seal-style slot rows and hover checkpoint details.
|
- Pre-battle Save menu can write and load one manual campaign checkpoint separate from the automatic save, using compact seal-style slot rows and hover checkpoint details.
|
||||||
@@ -129,7 +130,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr
|
|||||||
- Victory and defeat result overlay.
|
- Victory and defeat result overlay.
|
||||||
- Dialogue portrait slot stays fixed across speaker and narration lines, with officer default image paths, optional per-line overrides, and speaker-initial or record fallback.
|
- Dialogue portrait slot stays fixed across speaker and narration lines, with officer default image paths, optional per-line overrides, and speaker-initial or record fallback.
|
||||||
- Initial AI-generated photorealistic officer portraits for Cao Cao, Xiahou Dun, Xiahou Yuan, Cao Ren, Dian Wei, Guo Jia, and Zhang He.
|
- Initial AI-generated photorealistic officer portraits for Cao Cao, Xiahou Dun, Xiahou Yuan, Cao Ren, Dian Wei, Guo Jia, and Zhang He.
|
||||||
- AI-generated scenario battlefield backgrounds, opening story panels, transparent class units, transparent generic-enemy units, item icons, and toolbar UI icons live under `art/backgrounds`, `art/story`, `art/units`, `art/items`, and `art/ui/icons`.
|
- AI-generated scenario battlefield backgrounds, opening story panels, terrain textures, transparent class units, transparent generic-enemy units, item icons, and toolbar UI icons live under `art/backgrounds`, `art/story`, `art/terrain`, `art/units`, `art/items`, and `art/ui/icons`.
|
||||||
- Battle HUD portrait thumbnails show the selected unit, or a hovered unit when nothing is selected.
|
- Battle HUD portrait thumbnails show the selected unit, or a hovered unit when nothing is selected.
|
||||||
- Selected and hovered unit focus text shows combat role, movement type, current terrain move cost, terrain bonuses, core stats, and status effects.
|
- Selected and hovered unit focus text shows combat role, movement type, current terrain move cost, terrain bonuses, core stats, and status effects.
|
||||||
- Dialogue lines can mark speaking side for text alignment inside a fixed visual-novel-style dialogue panel with stable portrait, progress, and previous-line controls.
|
- Dialogue lines can mark speaking side for text alignment inside a fixed visual-novel-style dialogue panel with stable portrait, progress, and previous-line controls.
|
||||||
@@ -149,7 +150,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr
|
|||||||
2. Open this folder as a Godot project.
|
2. Open this folder as a Godot project.
|
||||||
3. Run the project. The main scene is `res://scenes/battle_scene.tscn`.
|
3. Run the project. The main scene is `res://scenes/battle_scene.tscn`.
|
||||||
|
|
||||||
Campaign order and chapter ranges live at `res://data/campaign/campaign.json`. Scenarios live under `res://data/scenarios/`, with reusable officers, classes, items, skills, and terrain definitions under `res://data/defs/`. Generated visual assets live under `res://art/portraits/`, `res://art/units/`, `res://art/items/`, and `res://art/backgrounds/`. Placeholder BGM and SFX live under `res://audio/`.
|
Campaign order and chapter ranges live at `res://data/campaign/campaign.json`. Scenarios live under `res://data/scenarios/`, with reusable officers, classes, items, skills, and terrain definitions under `res://data/defs/`. Generated visual assets live under `res://art/portraits/`, `res://art/units/`, `res://art/items/`, `res://art/terrain/`, `res://art/ui/`, and `res://art/backgrounds/`. Placeholder BGM and SFX live under `res://audio/`.
|
||||||
|
|
||||||
Data files and item icon cutouts can be checked with `powershell -NoProfile -ExecutionPolicy Bypass -File tools\validate_data.ps1`.
|
Data files and item icon cutouts can be checked with `powershell -NoProfile -ExecutionPolicy Bypass -File tools\validate_data.ps1`.
|
||||||
|
|
||||||
|
|||||||
BIN
art/terrain/terrain_c.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
art/terrain/terrain_d.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
art/terrain/terrain_f.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
art/terrain/terrain_g.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
art/terrain/terrain_h.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
art/terrain/terrain_r.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
art/terrain/terrain_t.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
art/terrain/terrain_w.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
@@ -29,6 +29,7 @@ const BGM_BATTLE_PATH := "res://audio/bgm/battle_loop_placeholder.wav"
|
|||||||
const TITLE_BACKGROUND_PATH := "res://art/backgrounds/title_war_camp_dawn.png"
|
const TITLE_BACKGROUND_PATH := "res://art/backgrounds/title_war_camp_dawn.png"
|
||||||
const TITLE_PORTRAIT_PATH := "res://art/portraits/cao_cao.png"
|
const TITLE_PORTRAIT_PATH := "res://art/portraits/cao_cao.png"
|
||||||
const TOOLBAR_ICON_PATH_TEMPLATE := "res://art/ui/icons/toolbar_%s.png"
|
const TOOLBAR_ICON_PATH_TEMPLATE := "res://art/ui/icons/toolbar_%s.png"
|
||||||
|
const TERRAIN_TEXTURE_PATH_TEMPLATE := "res://art/terrain/terrain_%s.png"
|
||||||
const OPENING_PROLOGUE_REBELLION_PATH := "res://art/story/opening/yellow_turban_rebellion.png"
|
const OPENING_PROLOGUE_REBELLION_PATH := "res://art/story/opening/yellow_turban_rebellion.png"
|
||||||
const OPENING_PROLOGUE_RESOLVE_PATH := "res://art/story/opening/cao_cao_resolves.png"
|
const OPENING_PROLOGUE_RESOLVE_PATH := "res://art/story/opening/cao_cao_resolves.png"
|
||||||
const OPENING_PROLOGUE_XIAHOU_DUN_PATH := "res://art/story/opening/xiahou_dun_joins.png"
|
const OPENING_PROLOGUE_XIAHOU_DUN_PATH := "res://art/story/opening/xiahou_dun_joins.png"
|
||||||
@@ -4366,6 +4367,7 @@ func _draw_map() -> void:
|
|||||||
var rect := _rect_for_cell(cell)
|
var rect := _rect_for_cell(cell)
|
||||||
var terrain_key := state.get_terrain_key(cell)
|
var terrain_key := state.get_terrain_key(cell)
|
||||||
draw_rect(rect, _terrain_fill_color(cell, terrain_key, has_background))
|
draw_rect(rect, _terrain_fill_color(cell, terrain_key, has_background))
|
||||||
|
_draw_terrain_texture(cell, rect, terrain_key, has_background)
|
||||||
_draw_terrain_detail(cell, rect, terrain_key)
|
_draw_terrain_detail(cell, rect, terrain_key)
|
||||||
_draw_terrain_edge_blend(cell, rect, terrain_key)
|
_draw_terrain_edge_blend(cell, rect, terrain_key)
|
||||||
draw_rect(rect, _map_grid_color(has_background), false, 1.0)
|
draw_rect(rect, _map_grid_color(has_background), false, 1.0)
|
||||||
@@ -4755,6 +4757,35 @@ func _map_grid_color(has_background: bool) -> Color:
|
|||||||
return color
|
return color
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_terrain_texture(cell: Vector2i, rect: Rect2, terrain_key: String, has_background: bool) -> void:
|
||||||
|
var texture := _load_terrain_texture(terrain_key)
|
||||||
|
if texture == null:
|
||||||
|
return
|
||||||
|
var modulate := _terrain_texture_modulate(cell, terrain_key, has_background)
|
||||||
|
draw_texture_rect(texture, rect.grow(-1.0), false, modulate)
|
||||||
|
|
||||||
|
|
||||||
|
func _load_terrain_texture(terrain_key: String) -> Texture2D:
|
||||||
|
var normalized_key := terrain_key.strip_edges().to_lower()
|
||||||
|
if normalized_key.is_empty():
|
||||||
|
return null
|
||||||
|
return _load_art_texture(TERRAIN_TEXTURE_PATH_TEMPLATE % normalized_key)
|
||||||
|
|
||||||
|
|
||||||
|
func _terrain_texture_modulate(cell: Vector2i, terrain_key: String, has_background: bool) -> Color:
|
||||||
|
var alpha := 0.17 if has_background else 0.58
|
||||||
|
if terrain_key == "W":
|
||||||
|
alpha += 0.07 if has_background else 0.10
|
||||||
|
elif terrain_key == "C" or terrain_key == "T":
|
||||||
|
alpha += 0.08 if has_background else 0.12
|
||||||
|
elif terrain_key == "R":
|
||||||
|
alpha += 0.05 if has_background else 0.08
|
||||||
|
elif terrain_key == "F" or terrain_key == "H" or terrain_key == "D":
|
||||||
|
alpha += 0.03 if has_background else 0.06
|
||||||
|
alpha *= 0.94 + _cell_noise(cell, 137) * 0.12
|
||||||
|
return Color(1.0, 1.0, 1.0, clampf(alpha, 0.0, 0.82))
|
||||||
|
|
||||||
|
|
||||||
func _draw_terrain_detail(cell: Vector2i, rect: Rect2, terrain_key: String) -> void:
|
func _draw_terrain_detail(cell: Vector2i, rect: Rect2, terrain_key: String) -> void:
|
||||||
if terrain_key == "G":
|
if terrain_key == "G":
|
||||||
for index in range(3):
|
for index in range(3):
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ func _init() -> void:
|
|||||||
var failures: Array[String] = []
|
var failures: Array[String] = []
|
||||||
_check_battle_visual_data(failures)
|
_check_battle_visual_data(failures)
|
||||||
_check_generated_toolbar_icons(failures)
|
_check_generated_toolbar_icons(failures)
|
||||||
|
_check_generated_terrain_textures(failures)
|
||||||
_check_scene_texture_loading(failures)
|
_check_scene_texture_loading(failures)
|
||||||
_check_hud_focus_text(failures)
|
_check_hud_focus_text(failures)
|
||||||
_check_ancient_ui_theme(failures)
|
_check_ancient_ui_theme(failures)
|
||||||
@@ -2976,6 +2977,12 @@ func _check_generated_toolbar_icons(failures: Array[String]) -> void:
|
|||||||
_check_toolbar_icon_path(failures, "res://art/ui/icons/toolbar_%s.png" % icon_kind, "toolbar icon %s" % icon_kind)
|
_check_toolbar_icon_path(failures, "res://art/ui/icons/toolbar_%s.png" % icon_kind, "toolbar icon %s" % icon_kind)
|
||||||
|
|
||||||
|
|
||||||
|
func _check_generated_terrain_textures(failures: Array[String]) -> void:
|
||||||
|
var terrain_keys := ["g", "f", "h", "d", "r", "w", "t", "c"]
|
||||||
|
for terrain_key in terrain_keys:
|
||||||
|
_check_terrain_texture_path(failures, "res://art/terrain/terrain_%s.png" % terrain_key, "terrain texture %s" % terrain_key)
|
||||||
|
|
||||||
|
|
||||||
func _check_scene_texture_loading(failures: Array[String]) -> void:
|
func _check_scene_texture_loading(failures: Array[String]) -> void:
|
||||||
var scene = BattleSceneScript.new()
|
var scene = BattleSceneScript.new()
|
||||||
if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
|
if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
|
||||||
@@ -4293,6 +4300,17 @@ func _check_terrain_and_unit_presentation(failures: Array[String]) -> void:
|
|||||||
var wasteland_fill: Color = scene._terrain_fill_color(Vector2i(0, 0), "D", true)
|
var wasteland_fill: Color = scene._terrain_fill_color(Vector2i(0, 0), "D", true)
|
||||||
if wasteland_fill.a <= background_fill.a:
|
if wasteland_fill.a <= background_fill.a:
|
||||||
failures.append("wasteland fill should sit slightly above grass over background art")
|
failures.append("wasteland fill should sit slightly above grass over background art")
|
||||||
|
if scene._load_terrain_texture("G") == null or scene._load_terrain_texture("T") == null or scene._load_terrain_texture("C") == null:
|
||||||
|
failures.append("battle map should load generated terrain texture assets")
|
||||||
|
var grass_texture_modulate := scene._terrain_texture_modulate(Vector2i(0, 0), "G", true)
|
||||||
|
var grass_fallback_modulate := scene._terrain_texture_modulate(Vector2i(0, 0), "G", false)
|
||||||
|
var castle_texture_modulate := scene._terrain_texture_modulate(Vector2i(20, 1), "C", true)
|
||||||
|
if grass_texture_modulate.a <= 0.0 or grass_texture_modulate.a >= 0.35:
|
||||||
|
failures.append("background terrain texture should stay subtle over high-res art: %s" % str(grass_texture_modulate))
|
||||||
|
if grass_fallback_modulate.a <= grass_texture_modulate.a:
|
||||||
|
failures.append("fallback terrain texture should be stronger without map background")
|
||||||
|
if castle_texture_modulate.a <= grass_texture_modulate.a:
|
||||||
|
failures.append("castle terrain texture should read stronger than plain grass")
|
||||||
if scene._terrain_edge_color("D", "G").a <= 0.0:
|
if scene._terrain_edge_color("D", "G").a <= 0.0:
|
||||||
failures.append("wasteland edge blend should be visible")
|
failures.append("wasteland edge blend should be visible")
|
||||||
if scene._terrain_edge_priority("D") <= scene._terrain_edge_priority("R"):
|
if scene._terrain_edge_priority("D") <= scene._terrain_edge_priority("R"):
|
||||||
@@ -4825,6 +4843,36 @@ func _check_toolbar_icon_path(failures: Array[String], path: String, context: St
|
|||||||
failures.append("%s icon should not keep chroma-key green pixels: %s (%d sampled)" % [context, path, green_fringe_samples])
|
failures.append("%s icon should not keep chroma-key green pixels: %s (%d sampled)" % [context, path, green_fringe_samples])
|
||||||
|
|
||||||
|
|
||||||
|
func _check_terrain_texture_path(failures: Array[String], path: String, context: String) -> void:
|
||||||
|
if path.is_empty():
|
||||||
|
failures.append("%s has an empty texture path" % context)
|
||||||
|
return
|
||||||
|
if not path.begins_with("res://"):
|
||||||
|
failures.append("%s must use a res:// path: %s" % [context, path])
|
||||||
|
return
|
||||||
|
if not FileAccess.file_exists(path):
|
||||||
|
failures.append("%s references missing texture: %s" % [context, path])
|
||||||
|
return
|
||||||
|
var image := Image.new()
|
||||||
|
var err := image.load(path)
|
||||||
|
if err != OK:
|
||||||
|
failures.append("%s references unreadable texture: %s" % [context, path])
|
||||||
|
return
|
||||||
|
if image.get_width() < 128 or image.get_height() < 128:
|
||||||
|
failures.append("%s should keep high-resolution terrain source pixels: %s (%dx%d)" % [context, path, image.get_width(), image.get_height()])
|
||||||
|
return
|
||||||
|
var min_luma := 999.0
|
||||||
|
var max_luma := -999.0
|
||||||
|
for y in range(0, image.get_height(), 16):
|
||||||
|
for x in range(0, image.get_width(), 16):
|
||||||
|
var pixel := image.get_pixel(x, y)
|
||||||
|
var luma := pixel.r * 0.299 + pixel.g * 0.587 + pixel.b * 0.114
|
||||||
|
min_luma = minf(min_luma, luma)
|
||||||
|
max_luma = maxf(max_luma, luma)
|
||||||
|
if max_luma - min_luma < 0.08:
|
||||||
|
failures.append("%s should contain visible generated terrain texture variation: %s" % [context, path])
|
||||||
|
|
||||||
|
|
||||||
func _check_image_path(failures: Array[String], path: String, context: String) -> void:
|
func _check_image_path(failures: Array[String], path: String, context: String) -> void:
|
||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
failures.append("%s has an empty image path" % context)
|
failures.append("%s has an empty image path" % context)
|
||||||
|
|||||||