diff --git a/README.md b/README.md index dd90b5a..2f86735 100644 --- a/README.md +++ b/README.md @@ -36,7 +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 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` and generated lacquer/jade/cinnabar button surfaces under `art/ui/buttons`, with procedural icon drawing and flat button styles kept only as fallbacks. -- Battle maps blend generated terrain texture tiles for plains, forests, hills, wasteland, roads, water, villages, and castles over the high-resolution battlefield backdrop. +- Battle maps blend generated terrain texture tiles for plains, forests, hills, wasteland, roads, water, villages, and castles over the high-resolution battlefield backdrop, with generated transparent feature overlays for road connections, water, villages, castles, and wasteland accents. - Core UI panels now prefer generated high-resolution lacquer, scroll, jade, and command-seal frame textures under `art/ui/panels`, with the calmer ink/wood/jade flat styles kept as fallbacks 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 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. @@ -130,7 +130,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - 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. - 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, terrain textures, transparent class units, transparent generic-enemy units, item icons, toolbar UI icons, generated UI panel frames, generated button surfaces, tactical tile markers, class crest icons, and battlefield map badges live under `art/backgrounds`, `art/story`, `art/terrain`, `art/units`, `art/items`, `art/ui/icons`, `art/ui/panels`, `art/ui/buttons`, `art/ui/tiles`, `art/ui/class_icons`, and `art/ui/map_badges`. +- AI-generated scenario battlefield backgrounds, opening story panels, terrain textures and terrain feature overlays, transparent class units, transparent generic-enemy units, item icons, toolbar UI icons, generated UI panel frames, generated button surfaces, tactical tile markers, class crest icons, and battlefield map badges live under `art/backgrounds`, `art/story`, `art/terrain`, `art/terrain/features`, `art/units`, `art/items`, `art/ui/icons`, `art/ui/panels`, `art/ui/buttons`, `art/ui/tiles`, `art/ui/class_icons`, and `art/ui/map_badges`. - 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. - 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. diff --git a/art/terrain/features/terrain_feature_castle.png b/art/terrain/features/terrain_feature_castle.png new file mode 100644 index 0000000..97d5e0b Binary files /dev/null and b/art/terrain/features/terrain_feature_castle.png differ diff --git a/art/terrain/features/terrain_feature_road_e.png b/art/terrain/features/terrain_feature_road_e.png new file mode 100644 index 0000000..88e306d Binary files /dev/null and b/art/terrain/features/terrain_feature_road_e.png differ diff --git a/art/terrain/features/terrain_feature_road_es.png b/art/terrain/features/terrain_feature_road_es.png new file mode 100644 index 0000000..dff4606 Binary files /dev/null and b/art/terrain/features/terrain_feature_road_es.png differ diff --git a/art/terrain/features/terrain_feature_road_esw.png b/art/terrain/features/terrain_feature_road_esw.png new file mode 100644 index 0000000..14491a9 Binary files /dev/null and b/art/terrain/features/terrain_feature_road_esw.png differ diff --git a/art/terrain/features/terrain_feature_road_ew.png b/art/terrain/features/terrain_feature_road_ew.png new file mode 100644 index 0000000..825c4f7 Binary files /dev/null and b/art/terrain/features/terrain_feature_road_ew.png differ diff --git a/art/terrain/features/terrain_feature_road_n.png b/art/terrain/features/terrain_feature_road_n.png new file mode 100644 index 0000000..00b9ce9 Binary files /dev/null and b/art/terrain/features/terrain_feature_road_n.png differ diff --git a/art/terrain/features/terrain_feature_road_ne.png b/art/terrain/features/terrain_feature_road_ne.png new file mode 100644 index 0000000..f6c6267 Binary files /dev/null and b/art/terrain/features/terrain_feature_road_ne.png differ diff --git a/art/terrain/features/terrain_feature_road_nesw.png b/art/terrain/features/terrain_feature_road_nesw.png new file mode 100644 index 0000000..0031df3 Binary files /dev/null and b/art/terrain/features/terrain_feature_road_nesw.png differ diff --git a/art/terrain/features/terrain_feature_road_new.png b/art/terrain/features/terrain_feature_road_new.png new file mode 100644 index 0000000..f930402 Binary files /dev/null and b/art/terrain/features/terrain_feature_road_new.png differ diff --git a/art/terrain/features/terrain_feature_road_node.png b/art/terrain/features/terrain_feature_road_node.png new file mode 100644 index 0000000..18f4e2f Binary files /dev/null and b/art/terrain/features/terrain_feature_road_node.png differ diff --git a/art/terrain/features/terrain_feature_road_ns.png b/art/terrain/features/terrain_feature_road_ns.png new file mode 100644 index 0000000..baef70f Binary files /dev/null and b/art/terrain/features/terrain_feature_road_ns.png differ diff --git a/art/terrain/features/terrain_feature_road_nse.png b/art/terrain/features/terrain_feature_road_nse.png new file mode 100644 index 0000000..82a8c68 Binary files /dev/null and b/art/terrain/features/terrain_feature_road_nse.png differ diff --git a/art/terrain/features/terrain_feature_road_nsw.png b/art/terrain/features/terrain_feature_road_nsw.png new file mode 100644 index 0000000..2063655 Binary files /dev/null and b/art/terrain/features/terrain_feature_road_nsw.png differ diff --git a/art/terrain/features/terrain_feature_road_s.png b/art/terrain/features/terrain_feature_road_s.png new file mode 100644 index 0000000..7b08adb Binary files /dev/null and b/art/terrain/features/terrain_feature_road_s.png differ diff --git a/art/terrain/features/terrain_feature_road_sw.png b/art/terrain/features/terrain_feature_road_sw.png new file mode 100644 index 0000000..0a6c2d4 Binary files /dev/null and b/art/terrain/features/terrain_feature_road_sw.png differ diff --git a/art/terrain/features/terrain_feature_road_w.png b/art/terrain/features/terrain_feature_road_w.png new file mode 100644 index 0000000..c8ef60d Binary files /dev/null and b/art/terrain/features/terrain_feature_road_w.png differ diff --git a/art/terrain/features/terrain_feature_road_wn.png b/art/terrain/features/terrain_feature_road_wn.png new file mode 100644 index 0000000..7a30891 Binary files /dev/null and b/art/terrain/features/terrain_feature_road_wn.png differ diff --git a/art/terrain/features/terrain_feature_town.png b/art/terrain/features/terrain_feature_town.png new file mode 100644 index 0000000..0aa4981 Binary files /dev/null and b/art/terrain/features/terrain_feature_town.png differ diff --git a/art/terrain/features/terrain_feature_wasteland.png b/art/terrain/features/terrain_feature_wasteland.png new file mode 100644 index 0000000..3828237 Binary files /dev/null and b/art/terrain/features/terrain_feature_wasteland.png differ diff --git a/art/terrain/features/terrain_feature_water.png b/art/terrain/features/terrain_feature_water.png new file mode 100644 index 0000000..0fd7f5b Binary files /dev/null and b/art/terrain/features/terrain_feature_water.png differ diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 103f2d5..36c6621 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -62,7 +62,7 @@ Joined officers gate whether `requires_joined` player deployments are loaded at Inventory and campaign flags are copied from `CampaignState` into `BattleState` when a scenario starts. The pre-battle Chapters overview reads `CampaignState` chapter progress and can load completed or current battles without mutating the save. Briefings preview first-clear rewards, can expose `briefing.camp_conversations` through a selectable Talk menu with `briefing.camp_dialogue` as a fallback, can filter those conversations by saved campaign flags, can claim saved one-time camp conversation supply effects through `CampaignState` and refresh the loaded battle inventory copy, and show completed replay rewards as already claimed. The pre-battle Save menu can write the current campaign state to `user://campaign_manual_save.json`, and loading that checkpoint restores it into `user://campaign_save.json` before re-entering the same pending-choice, completion, or current-briefing branch as startup. Pre-battle shop stock comes from the loaded scenario's `shop.items` list plus matching `shop.conditional_items` blocks, with optional finite limits from item entries or `stock` maps, and `shop.merchant` lines are presentation-only camp flavor. Shop purchases and 50% sell-back are campaign transactions: they update saved gold, unequipped inventory, and finite-stock purchase counts immediately, write the save file, and refresh the already-loaded battle inventory before the player begins the battle. Camp conversation supply claims are also campaign transactions: they save a per-scenario conversation claim ledger and inventory immediately, then refresh the already-loaded battle inventory. Pre-battle Armory equipment changes use the same equipment rules as battle HUD equipment changes, then immediately save merged roster equipment/stat snapshots and inventory stock. Pre-battle Roster uses scenario `roster.max_units`, `roster.required_officers`, and `roster.required_units` to mark optional player deployments as sortie or reserve; reserve units remain in the candidate list but are excluded from board occupancy, drawing, selection, AI targeting, and living-unit victory checks. Non-controllable player units can act as protected escort targets: enemies can attack them and conditions can reference them, but they are skipped by player selection, Armory, Formation, counterattacks, and campaign roster progression snapshots when `persist_progression` is false. Scenario `ai_target_priority` nudges enemy movement and damage-skill scoring toward important targets such as envoys without overriding defeat bonuses or range checks, and `set_ai_target_priority` events can retune that battle-only pressure after an ambush, gate closure, or escort exposure. Pre-battle Formation uses the loaded scenario's `formation.cells` and only mutates current battle unit positions before battle-begin events fire. Destination victory cells from `unit_reaches_tile` conditions are exposed to the scene for objective overlays and tile info, and can be expressed as one `pos` or a multi-cell `cells` list; gated victory conditions can also expose the `unit_reaches_tile` event that unlocks them as the next visible objective marker. Rewarding movement events that grant items or gold can expose optional battlefield side markers and Korean tile-info labels through `when.label`, without being treated as victory objectives. Movement-triggered `unit_reaches_tile` events receive the unit that just moved, so ambushes and pickup events fire on entry rather than from units already standing on a marker; `unit_defeated` events receive the fallen unit before final victory/defeat checks, allowing named-officer reactions and last-moment reinforcements. Briefing data can append matching `briefing.conditional_lines`, and scenario events can use `when.campaign_flags` to branch dialogue, objective changes, item and gold pickups, AI target-priority shifts, and reinforcements from saved campaign choices or `when.after_event` to wait for earlier scenario beats. Consumable use can restore HP or MP, cure matching poison/seal/snare/disarm statuses, or cleanse negative stat debuffs, and in-battle item use, event pickup grants, event gold pickups, plus equipment swaps mutate only the battle copy until victory; defeats and restarts do not spend saved items, keep picked-up items or gold, or preserve in-battle gear changes. Post-battle choices write campaign `flags` only after the result-panel button is pressed; if the player reloads after rewards are saved but before selecting, the pending scenario id restores the victory choice panel without replaying rewards. The next battle button stays locked until the campaign choice save succeeds. Equipment rewards share the same `item_id -> count` inventory stock, cover weapon, armor, and accessory slots, can be equipped from the side HUD before the selected unit moves or acts, and do not appear in the consumable action menu. Named equipment currently uses the same stock model with a presentation-only rarity tag rather than unique item instances. Already completed scenarios can be replayed without granting duplicate rewards, choices, replay inventory consumption, event gold pickups, save writes, current-scenario advancement, finite-stock purchases, or camp conversation supply claims; pre-battle Shop, Armory, Roster, Formation, and Save are disabled on completed-scenario replays to avoid side effects. -The battle scene owns presentation feedback: briefing and result states use menu BGM, briefing headers combine `CampaignState` chapter ranges, campaign order, and title data with `BattleState` briefing, objective text, condition-progress text, and defeat-risk text, active battles use battle BGM and show an objective/progress/risk panel fed by `BattleState`, objective-update signals show a short HUD notice and refresh the objective panel immediately, core panels prefer generated lacquer, scroll, jade, and command-seal `StyleBoxTexture` frames with flat ink/wood/jade styles as fallbacks, core buttons prefer generated lacquer, jade-hover, cinnabar-pressed, and disabled-ink `StyleBoxTexture` surfaces with flat styles as fallbacks, dialogue lines can show optional cached portrait textures or speaker-initial fallback panels in an expanded visual-novel-style panel, keep the portrait slot fixed even for narration lines, mark speaking side through text alignment, and expose progress plus previous-line controls while a sequence is open, the side HUD reuses cached officer portrait textures or class sprites for the selected unit or hovered unit, uses generated class crest icons for role badges, and formats focus text with role, movement type, facing, current terrain move cost, defense/avoid bonuses, core stats, status effects, guard-zone AI hints, and gear, board maps can draw a scenario background texture under translucent terrain overlays, generated terrain texture tiles, generated tactical tile markers for move, attack, selection, target, objective, and recovery overlays, connected road segments, terrain detail patterns, shoreline/castle/forest/hill edge blends, generated map badge icons for objective, recovery, supply, gold, tactic, and side-event markers, and move-attack target badges for enemies that can be struck after movement, board units render class sprite tokens or generic-enemy transparent class cutouts with generated class crest markers, team rings, facing markers, generic-enemy marks, officer pins, low-HP warning rings, HP bar color states, active support/debuff/poison/seal/snare/disarm status pips, and a scene-clocked idle stance whose intensity reacts to selected, acted, and low-HP unit state, item buttons can show optional cached item icons, hover previews are rendered as target badges from existing `BattleState` forecast APIs and include compact side/rear attack bonuses when facing matters, equipment option menus show stat/range/effect deltas, result and inventory summaries show named equipment with compact rarity tags, selected area tactics highlight their affected cells, the Threat toggle overlays enemy physical and hostile tactic reach while tile info names threat sources, estimates physical damage, and summarizes hostile tactic damage or status effects against occupied cells, log/result hooks trigger placeholder SFX, support, debuff, poison, seal, snare, disarm, objective updates, AI target-focus changes, and item/gold pickup effects use distinct feedback, `BattleState.unit_motion_requested` asks the scene to interpolate a unit's draw position during movement, `BattleState.unit_action_motion_requested` keeps its rules-facing signal shape while the scene maps attacker class ids to infantry, archer, cavalry, command, and heavy attack presentation profiles, and `BattleState.combat_feedback_requested` asks the scene to draw transient floating combat text without changing battle rules. +The battle scene owns presentation feedback: briefing and result states use menu BGM, briefing headers combine `CampaignState` chapter ranges, campaign order, and title data with `BattleState` briefing, objective text, condition-progress text, and defeat-risk text, active battles use battle BGM and show an objective/progress/risk panel fed by `BattleState`, objective-update signals show a short HUD notice and refresh the objective panel immediately, core panels prefer generated lacquer, scroll, jade, and command-seal `StyleBoxTexture` frames with flat ink/wood/jade styles as fallbacks, core buttons prefer generated lacquer, jade-hover, cinnabar-pressed, and disabled-ink `StyleBoxTexture` surfaces with flat styles as fallbacks, dialogue lines can show optional cached portrait textures or speaker-initial fallback panels in an expanded visual-novel-style panel, keep the portrait slot fixed even for narration lines, mark speaking side through text alignment, and expose progress plus previous-line controls while a sequence is open, the side HUD reuses cached officer portrait textures or class sprites for the selected unit or hovered unit, uses generated class crest icons for role badges, and formats focus text with role, movement type, facing, current terrain move cost, defense/avoid bonuses, core stats, status effects, guard-zone AI hints, and gear, board maps can draw a scenario background texture under translucent terrain overlays, generated terrain texture tiles, generated terrain feature overlays for connected roads, water, villages, castles, and wasteland accents, generated tactical tile markers for move, attack, selection, target, objective, and recovery overlays, connected edge blends, generated map badge icons for objective, recovery, supply, gold, tactic, and side-event markers, and move-attack target badges for enemies that can be struck after movement, board units render class sprite tokens or generic-enemy transparent class cutouts with generated class crest markers, team rings, facing markers, generic-enemy marks, officer pins, low-HP warning rings, HP bar color states, active support/debuff/poison/seal/snare/disarm status pips, and a scene-clocked idle stance whose intensity reacts to selected, acted, and low-HP unit state, item buttons can show optional cached item icons, hover previews are rendered as target badges from existing `BattleState` forecast APIs and include compact side/rear attack bonuses when facing matters, equipment option menus show stat/range/effect deltas, result and inventory summaries show named equipment with compact rarity tags, selected area tactics highlight their affected cells, the Threat toggle overlays enemy physical and hostile tactic reach while tile info names threat sources, estimates physical damage, and summarizes hostile tactic damage or status effects against occupied cells, log/result hooks trigger placeholder SFX, support, debuff, poison, seal, snare, disarm, objective updates, AI target-focus changes, and item/gold pickup effects use distinct feedback, `BattleState.unit_motion_requested` asks the scene to interpolate a unit's draw position during movement, `BattleState.unit_action_motion_requested` keeps its rules-facing signal shape while the scene maps attacker class ids to infantry, archer, cavalry, command, and heavy attack presentation profiles, and `BattleState.combat_feedback_requested` asks the scene to draw transient floating combat text without changing battle rules. Large tactical maps are presented through a fixed visible map window rather than by shrinking the whole board, including the opening chapter-one battle. `BattleScene` keeps a clamped `board_scroll_offset`, scrolls the board while the mouse is near or slightly outside the visible map edge, brightens the scrollable frame edge under the cursor, ignores board clicks outside the visible map window, and draws a small minimap plate only when the board actually exceeds the visible map view; the minimap shows terrain, unit dots, objective seals, and side-event markers, and clicking or dragging it recenters the tactical viewport, while compact maps keep the lower-right tiles unobstructed. Board-attached command panels are repositioned after map scrolling so post-move choices stay beside the moved unit. Battle start focuses the board on the deployed player group, pre-battle Formation can focus the board on the selected unit even before the battle opens, and action feedback can focus the board on off-screen cells before motion or combat effects play. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 712574a..6a45b8a 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -35,6 +35,7 @@ const TILE_MARKER_TEXTURE_PATH_TEMPLATE := "res://art/ui/tiles/tile_marker_%s.pn const CLASS_ICON_TEXTURE_PATH_TEMPLATE := "res://art/ui/class_icons/class_%s.png" const MAP_BADGE_TEXTURE_PATH_TEMPLATE := "res://art/ui/map_badges/map_badge_%s.png" const TERRAIN_TEXTURE_PATH_TEMPLATE := "res://art/terrain/terrain_%s.png" +const TERRAIN_FEATURE_TEXTURE_PATH_TEMPLATE := "res://art/terrain/features/terrain_feature_%s.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_XIAHOU_DUN_PATH := "res://art/story/opening/xiahou_dun_joins.png" @@ -4890,6 +4891,13 @@ func _load_terrain_texture(terrain_key: String) -> Texture2D: return _load_art_texture(TERRAIN_TEXTURE_PATH_TEMPLATE % normalized_key) +func _load_terrain_feature_texture(feature_key: String) -> Texture2D: + var normalized_key := feature_key.strip_edges().to_lower() + if normalized_key.is_empty(): + return null + return _load_art_texture(TERRAIN_FEATURE_TEXTURE_PATH_TEMPLATE % normalized_key) + + func _load_tile_marker_texture(marker_key: String) -> Texture2D: var normalized_key := marker_key.strip_edges().to_lower() if normalized_key.is_empty(): @@ -4948,6 +4956,17 @@ func _draw_map_badge(badge_key: String, rect: Rect2, modulate: Color = Color.WHI return true +func _draw_terrain_feature(feature_key: String, rect: Rect2, modulate: Color = Color.WHITE, inset: float = 0.0) -> bool: + var texture := _load_terrain_feature_texture(feature_key) + if texture == null: + return false + var target_rect := rect.grow(-inset) + if target_rect.size.x <= 0.0 or target_rect.size.y <= 0.0: + return false + draw_texture_rect(texture, target_rect, false, modulate) + return true + + 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": @@ -4982,18 +5001,22 @@ func _draw_terrain_detail(cell: Vector2i, rect: Rect2, terrain_key: String) -> v draw_line(peak, point + Vector2(12, 9), Color(0.20, 0.18, 0.14, 0.42), 2.2) draw_line(point + Vector2(-5, 5), point + Vector2(5, 6), Color(0.10, 0.09, 0.07, 0.20), 1.1) elif terrain_key == "D": - _draw_wasteland_detail(cell, rect) + if not _draw_terrain_feature("wasteland", rect, Color(1.0, 1.0, 1.0, 0.68)): + _draw_wasteland_detail(cell, rect) elif terrain_key == "R": _draw_road_detail(cell, rect) elif terrain_key == "W": - for index in range(3): - var y := rect.position.y + 15.0 + float(index) * 14.0 - draw_line(rect.position + Vector2(10, y - rect.position.y), rect.position + Vector2(rect.size.x - 10, y - rect.position.y + 4), Color(0.70, 0.88, 1.0, 0.32), 1.6) - _draw_water_shoreline(cell, rect) + if not _draw_terrain_feature("water", rect, Color(1.0, 1.0, 1.0, 0.78)): + for index in range(3): + var y := rect.position.y + 15.0 + float(index) * 14.0 + draw_line(rect.position + Vector2(10, y - rect.position.y), rect.position + Vector2(rect.size.x - 10, y - rect.position.y + 4), Color(0.70, 0.88, 1.0, 0.32), 1.6) + _draw_water_shoreline(cell, rect) elif terrain_key == "C": - _draw_castle_detail(rect) + if not _draw_terrain_feature("castle", rect, Color(1.0, 1.0, 1.0, 0.74)): + _draw_castle_detail(rect) elif terrain_key == "T": - _draw_town_detail(rect) + if not _draw_terrain_feature("town", rect, Color(1.0, 1.0, 1.0, 0.74)): + _draw_town_detail(rect) else: for index in range(2): var point := _terrain_detail_point(cell, rect, index + 15) @@ -5001,6 +5024,8 @@ func _draw_terrain_detail(cell: Vector2i, rect: Rect2, terrain_key: String) -> v func _draw_road_detail(cell: Vector2i, rect: Rect2) -> void: + if _draw_terrain_feature(_road_feature_key(cell), rect, Color(1.0, 1.0, 1.0, 0.82)): + return var road_color := Color(0.82, 0.69, 0.48, 0.34) var rut_color := Color(0.36, 0.28, 0.18, 0.24) var center := rect.position + rect.size * 0.5 @@ -5024,6 +5049,45 @@ func _draw_road_detail(cell: Vector2i, rect: Rect2) -> void: draw_circle(center, 5.0, Color(0.84, 0.70, 0.48, 0.24)) +func _road_feature_key(cell: Vector2i) -> String: + var flags := _terrain_connection_flags(cell, "R") + var north := bool(flags.get("north", false)) + var east := bool(flags.get("east", false)) + var south := bool(flags.get("south", false)) + var west := bool(flags.get("west", false)) + if north and east and south and west: + return "road_nesw" + if north and south and east: + return "road_nse" + if east and south and west: + return "road_esw" + if north and south and west: + return "road_nsw" + if north and east and west: + return "road_new" + if north and south: + return "road_ns" + if east and west: + return "road_ew" + if north and east: + return "road_ne" + if east and south: + return "road_es" + if south and west: + return "road_sw" + if west and north: + return "road_wn" + if north: + return "road_n" + if east: + return "road_e" + if south: + return "road_s" + if west: + return "road_w" + return "road_node" + + func _draw_water_shoreline(cell: Vector2i, rect: Rect2) -> void: var shore := Color(0.82, 0.88, 0.72, 0.24) if _terrain_key_at(cell + Vector2i(0, -1)) != "W": diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 3ae06be..42b934d 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -98,6 +98,7 @@ func _init() -> void: _check_generated_class_icon_textures(failures) _check_generated_map_badge_textures(failures) _check_generated_terrain_textures(failures) + _check_generated_terrain_feature_textures(failures) _check_scene_texture_loading(failures) _check_hud_focus_text(failures) _check_ancient_ui_theme(failures) @@ -3018,6 +3019,19 @@ func _check_generated_terrain_textures(failures: Array[String]) -> void: _check_terrain_texture_path(failures, "res://art/terrain/terrain_%s.png" % terrain_key, "terrain texture %s" % terrain_key) +func _check_generated_terrain_feature_textures(failures: Array[String]) -> void: + var feature_keys := [ + "road_node", + "road_n", "road_s", "road_e", "road_w", + "road_ns", "road_ew", + "road_ne", "road_es", "road_sw", "road_wn", + "road_nse", "road_esw", "road_nsw", "road_new", "road_nesw", + "town", "castle", "water", "wasteland" + ] + for feature_key in feature_keys: + _check_terrain_feature_texture_path(failures, "res://art/terrain/features/terrain_feature_%s.png" % feature_key, "terrain feature %s" % feature_key) + + func _check_scene_texture_loading(failures: Array[String]) -> void: var scene = BattleSceneScript.new() if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"): @@ -3038,6 +3052,8 @@ func _check_scene_texture_loading(failures: Array[String]) -> void: failures.append("battle scene should load generated class icon textures") if scene._load_map_badge_texture("objective") == null or scene._load_map_badge_texture("recover") == null: failures.append("battle scene should load generated map badge textures") + if scene._load_terrain_feature_texture("road_ew") == null or scene._load_terrain_feature_texture("town") == null or scene._load_terrain_feature_texture("castle") == null: + failures.append("battle scene should load generated terrain feature overlays") var conversations: Array = scene.state.get_briefing().get("camp_conversations", []) if not conversations.is_empty(): var first_lines: Array = (conversations[0] as Dictionary).get("lines", []) @@ -4451,6 +4467,8 @@ func _check_terrain_and_unit_presentation(failures: Array[String]) -> void: failures.append("Sishui Gate road should connect west/east at 3,5: %s" % str(road_flags)) if bool(road_flags.get("north", false)) or bool(road_flags.get("south", false)): failures.append("Sishui Gate road should not connect north/south at 3,5: %s" % str(road_flags)) + if scene._road_feature_key(Vector2i(2, 4)) != "road_ew": + failures.append("Sishui Gate road should use generated east-west road feature: %s" % scene._road_feature_key(Vector2i(2, 4))) if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("could not reload opening battle for unit presentation helpers") scene.free() @@ -5269,6 +5287,57 @@ func _check_terrain_texture_path(failures: Array[String], path: String, context: failures.append("%s should contain visible generated terrain texture variation: %s" % [context, path]) +func _check_terrain_feature_texture_path(failures: Array[String], path: String, context: String) -> void: + if path.is_empty(): + failures.append("%s has an empty feature 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 terrain feature texture: %s" % [context, path]) + return + var image := Image.new() + var err := image.load(path) + if err != OK: + failures.append("%s references unreadable terrain feature texture: %s" % [context, path]) + return + if image.get_width() < 512 or image.get_height() < 512: + failures.append("%s should keep high-resolution overlay pixels: %s (%dx%d)" % [context, path, image.get_width(), image.get_height()]) + return + if image.get_width() != image.get_height(): + failures.append("%s should be square: %s (%dx%d)" % [context, path, image.get_width(), image.get_height()]) + return + var transparent_samples := 0 + var opaque_samples := 0 + var green_fringe_samples := 0 + var min_luma := 999.0 + var max_luma := -999.0 + var total_samples := 0 + for y in range(0, image.get_height(), 8): + for x in range(0, image.get_width(), 8): + total_samples += 1 + var pixel := image.get_pixel(x, y) + if pixel.a <= 0.01: + transparent_samples += 1 + continue + if pixel.a >= 0.32: + opaque_samples += 1 + if pixel.a > 0.05 and pixel.g > 0.92 and pixel.r < 0.08 and pixel.b < 0.08: + green_fringe_samples += 1 + 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 transparent_samples < int(total_samples * 0.18): + failures.append("%s should keep transparent padding so the map art shows through: %s (%d/%d sampled pixels)" % [context, path, transparent_samples, total_samples]) + if opaque_samples < int(total_samples * 0.04): + failures.append("%s should contain readable generated feature artwork: %s (%d/%d sampled pixels)" % [context, path, opaque_samples, total_samples]) + if green_fringe_samples > 0: + failures.append("%s should not keep chroma-key green pixels: %s (%d sampled)" % [context, path, green_fringe_samples]) + if max_luma - min_luma < 0.06: + failures.append("%s should contain visible generated feature variation: %s" % [context, path]) + + func _check_image_path(failures: Array[String], path: String, context: String) -> void: if path.is_empty(): failures.append("%s has an empty image path" % context)