From 1fe0554b1fe9ed940dce624db6534094e15862e0 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sat, 20 Jun 2026 06:35:06 +0900 Subject: [PATCH] Play footstep SFX from movement motions --- docs/ARCHITECTURE.md | 2 ++ scripts/scenes/battle_scene.gd | 11 +++++++++-- tools/smoke_visual_assets.gd | 8 ++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e71a999..614ee5e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -64,6 +64,8 @@ Inventory and campaign flags are copied from `CampaignState` into `BattleState` 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. +Movement audio is keyed from `BattleState.unit_motion_requested` rather than movement log text, so future log cleanup does not mute board feedback. + 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. The `New Campaign` button clears `user://campaign_save.json`, resets campaign state to the first scenario, and reloads the opening briefing. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 91dc961..504e12e 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -4381,8 +4381,6 @@ func _play_log_sfx(message: String) -> void: _play_sfx("skill_cast") else: _play_sfx("fire_skill") - elif message.contains(" moved to ") or message.contains(" advances to ") or message.contains(" takes formation at ") or message.contains(" arrives at ") or message.contains("진군") or message.contains("진형을 잡았다") or message.contains("압박") or message.contains("당도"): - _play_sfx("footstep") elif message.contains(" uses ") or message.contains(" equips ") or message.contains(" 사용:") or message.contains("장착") or message.contains("해제") or message.contains(" 매입:") or message.contains(" 매각:") or message.contains("군자금") or message.begins_with("Received ") or message.begins_with("전리품:"): _play_sfx("item_pickup") elif message.contains("공적") or message.contains("품계") or message.contains("승급"): @@ -9930,6 +9928,7 @@ func _on_unit_motion_requested(unit_id: String, from_cell: Vector2i, to_cell: Ve battle_presentation_feedback_delay_by_unit[unit_id] = delay + duration + BATTLE_PRESENTATION_FEEDBACK_LAG if delay <= 0.0: _focus_board_on_cell(to_cell) + var sfx_key := _movement_motion_sfx(unit_id, from_cell, to_cell) unit_motion_by_unit[unit_id] = { "from": from_cell, "to": to_cell, @@ -9938,8 +9937,12 @@ func _on_unit_motion_requested(unit_id: String, from_cell: Vector2i, to_cell: Ve "focus_cell": to_cell, "force_focus": false, "focused": delay <= 0.0, + "sfx": sfx_key, + "sfx_played": delay <= 0.0, "sequence": battle_presentation_sequence_active or delay > 0.0 } + if delay <= 0.0 and is_inside_tree(): + _play_sfx(sfx_key) queue_redraw() @@ -10168,6 +10171,10 @@ func _action_motion_sfx(profile: String) -> String: return "slash" +func _movement_motion_sfx(_unit_id: String, _from_cell: Vector2i, _to_cell: Vector2i) -> String: + return "footstep" + + func _floating_text_origin(cell: Vector2i) -> Vector2: var rect := _rect_for_cell(cell) return rect.position + Vector2(TILE_SIZE * 0.5, 18.0) diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 95ea632..b26b363 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -4799,6 +4799,14 @@ func _check_attack_motion_profiles(failures: Array[String]) -> void: failures.append("heavy animation should use swing SFX before hit resolution") if scene._action_motion_sfx("tactic_damage") != "skill_cast": failures.append("tactic animation should use skill cast SFX") + if scene._movement_motion_sfx("cao_cao", Vector2i(1, 6), Vector2i(2, 6)) != "footstep": + failures.append("unit movement animation should use footstep SFX") + scene._on_unit_motion_requested("cao_cao", Vector2i(1, 6), Vector2i(2, 6)) + var move_motion: Dictionary = scene.unit_motion_by_unit.get("cao_cao", {}) + if str(move_motion.get("sfx", "")) != "footstep": + failures.append("stored movement motion should queue footstep SFX: %s" % str(move_motion)) + if not bool(move_motion.get("sfx_played", false)): + failures.append("immediate movement motion should mark SFX as played: %s" % str(move_motion)) _check_attack_motion_curves(failures, scene) scene.free()