diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index b78ee30..62bd4cf 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -50,9 +50,9 @@ const OBJECTIVE_NOTICE_DURATION := 2.6 const UNIT_MOVE_ANIMATION_DURATION := 0.18 const TARGET_PREVIEW_BADGE_SIZE := Vector2(92, 22) const LOW_HP_WARNING_RATIO := 0.35 -const UNIT_STATUS_MARKER_RADIUS := 4.0 -const UNIT_STATUS_MARKER_GAP := 9.0 -const UNIT_STATUS_MARKER_OFFSET_Y := 14.0 +const UNIT_STATUS_MARKER_RADIUS := 6.0 +const UNIT_STATUS_MARKER_GAP := 14.0 +const UNIT_STATUS_MARKER_OFFSET_Y := 13.0 const MAX_UNIT_STATUS_MARKERS := 4 const UNIT_ATTACK_ANIMATION_DURATION := 0.30 const UNIT_ATTACK_LUNGE_DISTANCE := 15.0 @@ -401,6 +401,7 @@ func _draw() -> void: _draw_map() _draw_overlays() _draw_units() + _draw_objective_foreground_markers() _draw_target_selection_markers() _draw_attack_effects() _draw_target_preview_badge() @@ -629,6 +630,14 @@ func _draw_ui_font(bold: bool = false) -> Font: return ThemeDB.fallback_font +func _draw_ink_text(font: Font, position: Vector2, text: String, alignment: HorizontalAlignment, width: float, font_size: int, color: Color, outline_color := Color(0.02, 0.012, 0.006, 0.95)) -> void: + if text.is_empty(): + return + for offset in [Vector2(-1, 0), Vector2(1, 0), Vector2(0, -1), Vector2(0, 1), Vector2(1, 1)]: + draw_string(font, position + offset, text, alignment, width, font_size, outline_color) + draw_string(font, position, text, alignment, width, font_size, color) + + func _apply_control_font(control: Control, bold: bool = false) -> void: if control == null: return @@ -2831,6 +2840,45 @@ func _draw_objective_seal_marker(rect: Rect2) -> void: draw_line(seal_rect.position + Vector2(seal_rect.size.x * 0.5, 8), seal_rect.end - Vector2(seal_rect.size.x * 0.5, 8), Color(UI_OLD_BRONZE.r, UI_OLD_BRONZE.g, UI_OLD_BRONZE.b, 0.34), 1.2) +func _draw_objective_foreground_markers() -> void: + var font := _draw_ui_font(true) + for cell in state.get_objective_cells(): + if not state.is_inside(cell): + continue + var rect := _rect_for_cell(cell) + _draw_objective_corner_brackets(rect) + var label_rect := Rect2(rect.position + Vector2(rect.size.x - 20.0, 3.0), Vector2(17.0, 16.0)) + draw_rect(label_rect, Color(UI_SEAL_RED_DARK.r, UI_SEAL_RED_DARK.g, UI_SEAL_RED_DARK.b, 0.86)) + draw_rect(label_rect, Color(UI_OLD_BRONZE.r, UI_OLD_BRONZE.g, UI_OLD_BRONZE.b, 0.88), false, 1.2) + _draw_ink_text(font, label_rect.position + Vector2(0, 12), "목", HORIZONTAL_ALIGNMENT_CENTER, label_rect.size.x, 11, UI_PARCHMENT_TEXT) + + +func _draw_objective_corner_brackets(rect: Rect2) -> void: + var bracket := 12.0 + var inset := 4.0 + var line_color := Color(UI_OLD_BRONZE.r, UI_OLD_BRONZE.g, UI_OLD_BRONZE.b, 0.95) + var shadow := Color(0.02, 0.012, 0.006, 0.88) + var corners := [ + rect.position + Vector2(inset, inset), + Vector2(rect.end.x - inset, rect.position.y + inset), + rect.end - Vector2(inset, inset), + Vector2(rect.position.x + inset, rect.end.y - inset) + ] + _draw_objective_corner(corners[0], Vector2.RIGHT, Vector2.DOWN, bracket, shadow, 4.0) + _draw_objective_corner(corners[1], Vector2.LEFT, Vector2.DOWN, bracket, shadow, 4.0) + _draw_objective_corner(corners[2], Vector2.LEFT, Vector2.UP, bracket, shadow, 4.0) + _draw_objective_corner(corners[3], Vector2.RIGHT, Vector2.UP, bracket, shadow, 4.0) + _draw_objective_corner(corners[0], Vector2.RIGHT, Vector2.DOWN, bracket, line_color, 2.0) + _draw_objective_corner(corners[1], Vector2.LEFT, Vector2.DOWN, bracket, line_color, 2.0) + _draw_objective_corner(corners[2], Vector2.LEFT, Vector2.UP, bracket, line_color, 2.0) + _draw_objective_corner(corners[3], Vector2.RIGHT, Vector2.UP, bracket, line_color, 2.0) + + +func _draw_objective_corner(origin: Vector2, x_dir: Vector2, y_dir: Vector2, length: float, color: Color, width: float) -> void: + draw_line(origin, origin + x_dir * length, color, width) + draw_line(origin, origin + y_dir * length, color, width) + + func _draw_overlays() -> void: for cell in state.get_objective_cells(): _draw_objective_seal_marker(_rect_for_cell(cell)) @@ -3192,10 +3240,22 @@ func _draw_unit_status_markers(center: Vector2, hp_ratio: float, unit: Dictionar return var start_x := -float(marker_kinds.size() - 1) * UNIT_STATUS_MARKER_GAP * 0.5 + var font := _draw_ui_font(true) for index in range(marker_kinds.size()): + var marker_kind := marker_kinds[index] var marker_center := center + Vector2(start_x + float(index) * UNIT_STATUS_MARKER_GAP, UNIT_STATUS_MARKER_OFFSET_Y) draw_circle(marker_center, UNIT_STATUS_MARKER_RADIUS + 1.5, Color(0.02, 0.022, 0.028, 0.88)) - draw_circle(marker_center, UNIT_STATUS_MARKER_RADIUS, _unit_status_marker_color(marker_kinds[index])) + draw_circle(marker_center, UNIT_STATUS_MARKER_RADIUS, _unit_status_marker_color(marker_kind)) + _draw_ink_text( + font, + marker_center + Vector2(-UNIT_STATUS_MARKER_RADIUS, 3.0), + _unit_status_marker_glyph(marker_kind), + HORIZONTAL_ALIGNMENT_CENTER, + UNIT_STATUS_MARKER_RADIUS * 2.0, + 8, + _unit_status_marker_text_color(marker_kind), + Color(0.02, 0.012, 0.006, 0.80) + ) func _unit_status_marker_kinds(unit: Dictionary) -> Array[String]: @@ -3252,6 +3312,28 @@ func _unit_status_marker_color(marker_kind: String) -> Color: return Color(0.72, 0.86, 1.0) +func _unit_status_marker_glyph(marker_kind: String) -> String: + if marker_kind == "support": + return "강" + if marker_kind == "debuff": + return "약" + if marker_kind == "poison": + return "독" + if marker_kind == "seal": + return "봉" + if marker_kind == "snare": + return "묶" + if marker_kind == "disarm": + return "해" + return "상" + + +func _unit_status_marker_text_color(marker_kind: String) -> Color: + if marker_kind == "poison" or marker_kind == "support": + return UI_AGED_INK + return UI_PARCHMENT_TEXT + + func _visual_rect_for_unit(unit: Dictionary) -> Rect2: var cell: Vector2i = unit.get("pos", Vector2i(-1, -1)) var rect := _rect_for_cell(cell) diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 75ee87b..f463092 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -97,6 +97,7 @@ func _init() -> void: _check_hover_intent_badges(failures) _check_action_button_disabled_reasons(failures) _check_terrain_and_unit_presentation(failures) + _check_objective_and_status_marker_helpers(failures) _check_attack_motion_profiles(failures) _check_attack_motion_signal(failures) @@ -3596,6 +3597,40 @@ func _check_terrain_and_unit_presentation(failures: Array[String]) -> void: scene.free() +func _check_objective_and_status_marker_helpers(failures: Array[String]) -> void: + var scene = BattleSceneScript.new() + if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"): + failures.append("could not load battle scene state for objective/status markers") + scene.free() + return + if scene.state.get_objective_cells().is_empty(): + failures.append("opening battle should expose objective cells for foreground markers") + var glyphs := { + "support": "강", + "debuff": "약", + "poison": "독", + "seal": "봉", + "snare": "묶", + "disarm": "해" + } + for marker_kind in glyphs.keys(): + if scene._unit_status_marker_glyph(str(marker_kind)) != str(glyphs[marker_kind]): + failures.append("status marker glyph mismatch for %s" % str(marker_kind)) + var unit := { + "status_effects": [ + {"type": "stat_bonus", "stat": "def", "amount": 2, "remaining_phases": 2}, + {"type": "stat_bonus", "stat": "def", "amount": -2, "remaining_phases": 2}, + {"type": "damage_over_time", "status": "poison", "amount": 4, "remaining_phases": 2}, + {"type": "action_lock", "status": "seal", "action": "skill", "remaining_phases": 2} + ] + } + var marker_kinds := scene._unit_status_marker_kinds(unit) + for expected in ["support", "debuff", "poison", "seal"]: + if not marker_kinds.has(expected): + failures.append("status marker kinds should include %s: %s" % [expected, str(marker_kinds)]) + scene.free() + + func _check_attack_motion_profiles(failures: Array[String]) -> void: var scene = BattleSceneScript.new() if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"):