Polish chapter one battle presentation

This commit is contained in:
2026-06-19 15:27:54 +09:00
parent ed23b2bdf0
commit fb04cbda44
2 changed files with 418 additions and 39 deletions

View File

@@ -47,6 +47,8 @@ const UI_SFX_VOLUME_DB := -8.0
const FLOATING_TEXT_LIFETIME := 1.0 const FLOATING_TEXT_LIFETIME := 1.0
const FLOATING_TEXT_RISE := 42.0 const FLOATING_TEXT_RISE := 42.0
const OBJECTIVE_NOTICE_DURATION := 2.6 const OBJECTIVE_NOTICE_DURATION := 2.6
const EDGE_SCROLL_MARGIN := 34.0
const EDGE_SCROLL_SPEED := 360.0
const UNIT_MOVE_ANIMATION_DURATION := 0.18 const UNIT_MOVE_ANIMATION_DURATION := 0.18
const TARGET_PREVIEW_BADGE_SIZE := Vector2(104, 22) const TARGET_PREVIEW_BADGE_SIZE := Vector2(104, 22)
const LOW_HP_WARNING_RATIO := 0.35 const LOW_HP_WARNING_RATIO := 0.35
@@ -62,11 +64,11 @@ const UNIT_ARROW_ATTACK_DURATION := 0.36
const UNIT_COMMAND_ATTACK_DURATION := 0.38 const UNIT_COMMAND_ATTACK_DURATION := 0.38
const UNIT_HEAVY_ATTACK_DURATION := 0.34 const UNIT_HEAVY_ATTACK_DURATION := 0.34
const UNIT_TACTIC_CAST_DURATION := 0.42 const UNIT_TACTIC_CAST_DURATION := 0.42
const DIALOGUE_PANEL_POSITION := Vector2(40, 356) const DIALOGUE_PANEL_POSITION := Vector2(40, 324)
const DIALOGUE_PANEL_SIZE := Vector2(1200, 304) const DIALOGUE_PANEL_SIZE := Vector2(1200, 356)
const DIALOGUE_PORTRAIT_SIZE := Vector2(220, 262) const DIALOGUE_PORTRAIT_SIZE := Vector2(220, 310)
const DIALOGUE_COLUMN_SIZE := Vector2(850, 266) const DIALOGUE_COLUMN_SIZE := Vector2(850, 318)
const DIALOGUE_TEXT_SIZE := Vector2(830, 112) const DIALOGUE_TEXT_SIZE := Vector2(830, 164)
const BRIEFING_CAMP_THUMBNAIL_SIZE := Vector2(188, 86) const BRIEFING_CAMP_THUMBNAIL_SIZE := Vector2(188, 86)
const CAMP_TALK_PORTRAIT_SIZE := Vector2(58, 58) const CAMP_TALK_PORTRAIT_SIZE := Vector2(58, 58)
const SHOP_MERCHANT_BADGE_SIZE := Vector2(50, 50) const SHOP_MERCHANT_BADGE_SIZE := Vector2(50, 50)
@@ -121,9 +123,12 @@ const SPEAKER_DISPLAY_NAMES := {
"Zhang Xiu": "장수", "Zhang Xiu": "장수",
"Jia Xu": "가후", "Jia Xu": "가후",
"Cao Ang": "조앙", "Cao Ang": "조앙",
"Xu Rong": "서영",
"Li Jue": "이각",
"Chen Gong": "진궁", "Chen Gong": "진궁",
"Gao Shun": "고순", "Gao Shun": "고순",
"Lu Bu": "여포", "Lu Bu": "여포",
"Qingzhou Chief": "청주 두령",
"Yan Liang": "안량", "Yan Liang": "안량",
"Wen Chou": "문추", "Wen Chou": "문추",
"Yuan Shao": "원소", "Yuan Shao": "원소",
@@ -166,9 +171,12 @@ const SPEAKER_CANONICAL_NAMES := {
"장수": "Zhang Xiu", "장수": "Zhang Xiu",
"가후": "Jia Xu", "가후": "Jia Xu",
"조앙": "Cao Ang", "조앙": "Cao Ang",
"서영": "Xu Rong",
"이각": "Li Jue",
"진궁": "Chen Gong", "진궁": "Chen Gong",
"고순": "Gao Shun", "고순": "Gao Shun",
"여포": "Lu Bu", "여포": "Lu Bu",
"청주 두령": "Qingzhou Chief",
"안량": "Yan Liang", "안량": "Yan Liang",
"문추": "Wen Chou", "문추": "Wen Chou",
"원소": "Yuan Shao", "원소": "Yuan Shao",
@@ -188,6 +196,46 @@ const SPEAKER_CANONICAL_NAMES := {
"천자 금위": "Imperial Guard", "천자 금위": "Imperial Guard",
"완성 척후": "Wan Scout" "완성 척후": "Wan Scout"
} }
const DIALOGUE_RIGHT_SIDE_SPEAKERS := {
"Camp Merchant": true,
"Coalition Merchant": true,
"Coalition Quartermaster": true,
"Dingtao Field Sutler": true,
"Escort Road Sutler": true,
"Puyang Night Trader": true,
"Wan Castle Sutler": true,
"Xiapi Flood-Gate Sutler": true,
"White Horse Road Sutler": true,
"Scout": true,
"Wan Scout": true,
"Yuan Scout": true,
"Imperial Envoy": true,
"Imperial Guard": true,
"Xu Rong": true,
"Li Jue": true,
"Zhang Xiu": true,
"Jia Xu": true,
"Zhang Liao": true,
"Chen Gong": true,
"Gao Shun": true,
"Lu Bu": true,
"Qingzhou Chief": true,
"Yuan Shao": true,
"Yan Liang": true,
"Wen Chou": true,
"Ju Shou": true,
"Chunyu Qiong": true,
"Yuan Guard": true,
"Yuan Field Advisor": true,
"Yuan Rally Advisor": true,
"Yuan Tan Guard": true,
"Yuan Shang Guard": true,
"Ye Guard": true,
"Yuan Shang Inner Guard": true,
"Inner City Advisor": true,
"Ye Last Loyalist": true,
"Palace Advisor": true
}
var state: BattleState = BattleStateScript.new() var state: BattleState = BattleStateScript.new()
var campaign_state: CampaignState = CampaignStateScript.new() var campaign_state: CampaignState = CampaignStateScript.new()
@@ -352,6 +400,7 @@ var battle_background_path := ""
var battle_background_texture: Texture2D var battle_background_texture: Texture2D
var ui_regular_font: Font var ui_regular_font: Font
var ui_bold_font: Font var ui_bold_font: Font
var board_scroll_offset := Vector2.ZERO
func _ready() -> void: func _ready() -> void:
@@ -1522,7 +1571,7 @@ func _create_hud() -> void:
briefing_objective_header.add_child(briefing_objective_toggle_button) briefing_objective_header.add_child(briefing_objective_toggle_button)
briefing_objective_panel = PanelContainer.new() briefing_objective_panel = PanelContainer.new()
briefing_objective_panel.custom_minimum_size = Vector2(680, 104) briefing_objective_panel.custom_minimum_size = Vector2(680, 124)
_apply_panel_style(briefing_objective_panel, "edict") _apply_panel_style(briefing_objective_panel, "edict")
briefing_column.add_child(briefing_objective_panel) briefing_column.add_child(briefing_objective_panel)
@@ -1530,17 +1579,17 @@ func _create_hud() -> void:
briefing_objective_row.add_theme_constant_override("separation", 8) briefing_objective_row.add_theme_constant_override("separation", 8)
briefing_objective_panel.add_child(briefing_objective_row) briefing_objective_panel.add_child(briefing_objective_row)
briefing_objective_row.add_child(_make_bamboo_gutter(18, 84)) briefing_objective_row.add_child(_make_bamboo_gutter(18, 104))
briefing_objective_row.add_child(_make_edict_marker_stack(["", "", ""], 84)) briefing_objective_row.add_child(_make_edict_marker_stack(["", "", ""], 104))
briefing_objective_label = Label.new() briefing_objective_label = Label.new()
briefing_objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART briefing_objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
briefing_objective_label.custom_minimum_size = Vector2(500, 84) briefing_objective_label.custom_minimum_size = Vector2(500, 104)
briefing_objective_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL briefing_objective_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_apply_label_style(briefing_objective_label, UI_AGED_INK, 15) _apply_label_style(briefing_objective_label, UI_AGED_INK, 15)
briefing_objective_row.add_child(briefing_objective_label) briefing_objective_row.add_child(briefing_objective_label)
briefing_objective_row.add_child(_make_edict_marker_stack(["", "", ""], 84)) briefing_objective_row.add_child(_make_edict_marker_stack(["", "", ""], 104))
briefing_objective_row.add_child(_make_bamboo_gutter(18, 84)) briefing_objective_row.add_child(_make_bamboo_gutter(18, 104))
briefing_column.add_child(_make_ink_wash_rule(680, 8)) briefing_column.add_child(_make_ink_wash_rule(680, 8))
briefing_column.add_child(_make_section_caption("전장도와 군막 보고", 680)) briefing_column.add_child(_make_section_caption("전장도와 군막 보고", 680))
@@ -1879,7 +1928,7 @@ func _create_hud() -> void:
dialogue_speaker_row.add_child(_make_seal_tile("", 24)) dialogue_speaker_row.add_child(_make_seal_tile("", 24))
dialogue_text_panel = PanelContainer.new() dialogue_text_panel = PanelContainer.new()
dialogue_text_panel.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x, 104) dialogue_text_panel.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x, DIALOGUE_TEXT_SIZE.y)
_apply_panel_style(dialogue_text_panel, "speech_scroll") _apply_panel_style(dialogue_text_panel, "speech_scroll")
dialogue_column.add_child(dialogue_text_panel) dialogue_column.add_child(dialogue_text_panel)
@@ -2426,6 +2475,10 @@ func _handle_key(event: InputEventKey) -> void:
func _process(delta: float) -> void: func _process(delta: float) -> void:
var needs_redraw := false var needs_redraw := false
if battle_started and state.battle_status == BattleState.STATUS_ACTIVE and not _is_dialogue_visible():
needs_redraw = true
if _update_edge_scroll(delta):
needs_redraw = true
if objective_notice_timer > 0.0: if objective_notice_timer > 0.0:
objective_notice_timer = maxf(0.0, objective_notice_timer - delta) objective_notice_timer = maxf(0.0, objective_notice_timer - delta)
if objective_notice_timer <= 0.0 and objective_notice_panel != null: if objective_notice_timer <= 0.0 and objective_notice_panel != null:
@@ -2478,7 +2531,7 @@ func _can_select(unit: Dictionary) -> bool:
func _draw_map() -> void: func _draw_map() -> void:
var board_rect := Rect2(BOARD_OFFSET, Vector2(state.map_size.x, state.map_size.y) * TILE_SIZE) var board_rect := _board_rect()
var background := _current_battle_background_texture() var background := _current_battle_background_texture()
var has_background := background != null var has_background := background != null
if background != null: if background != null:
@@ -2976,7 +3029,6 @@ func _draw_overlays() -> void:
func _draw_units() -> void: func _draw_units() -> void:
var font := _draw_ui_font()
for unit in state.units: for unit in state.units:
if not unit.get("alive", false) or not unit.get("deployed", true): if not unit.get("alive", false) or not unit.get("deployed", true):
continue continue
@@ -2984,25 +3036,11 @@ func _draw_units() -> void:
var rect := _visual_rect_for_unit(unit) var rect := _visual_rect_for_unit(unit)
var center := rect.position + rect.size * 0.5 var center := rect.position + rect.size * 0.5
var team_color := PLAYER_COLOR if unit.get("team", "") == BattleState.TEAM_PLAYER else ENEMY_COLOR var team_color := PLAYER_COLOR if unit.get("team", "") == BattleState.TEAM_PLAYER else ENEMY_COLOR
var token_back := Color(0.02, 0.022, 0.026, 0.78)
var acted := bool(unit.get("acted", false)) var acted := bool(unit.get("acted", false))
var sprite_modulate := _unit_sprite_modulate(unit, acted) var sprite_modulate := _unit_sprite_modulate(unit, acted)
draw_circle(center + Vector2(0, 3), 29, Color(0.0, 0.0, 0.0, 0.38)) draw_circle(center + Vector2(0, 17), 20, Color(0.0, 0.0, 0.0, 0.30))
draw_circle(center, 28, token_back) _draw_pixel_unit_sprite(rect, unit, team_color, sprite_modulate)
var sprite_texture := _load_art_texture(str(unit.get("sprite", "")))
if sprite_texture != null:
var sprite_target := Rect2(rect.position + Vector2(5, 1), Vector2(TILE_SIZE - 10, TILE_SIZE - 16))
var sprite_rect := _fit_texture_rect(sprite_texture, sprite_target)
draw_texture_rect(sprite_texture, sprite_rect, false, sprite_modulate)
else:
var body_color := team_color.darkened(0.35) if acted else team_color
draw_circle(center, 22, body_color)
draw_arc(center, 28, 0.0, TAU, 64, Color(0.03, 0.035, 0.04), 3.0)
draw_arc(center, 28, -PI * 0.65, PI * 0.35, 36, team_color.lightened(0.18), 3.0)
_draw_unit_identity_marks(rect, center, unit, team_color)
_draw_unit_class_emblem(rect, center, unit, team_color)
_draw_unit_class_badge(rect, unit, team_color)
var hp_ratio := float(unit["hp"]) / float(max(1, int(unit["max_hp"]))) var hp_ratio := float(unit["hp"]) / float(max(1, int(unit["max_hp"])))
var hp_back := Rect2(rect.position + Vector2(8, TILE_SIZE - 8), Vector2(TILE_SIZE - 16, 5)) var hp_back := Rect2(rect.position + Vector2(8, TILE_SIZE - 8), Vector2(TILE_SIZE - 16, 5))
@@ -3154,6 +3192,124 @@ func _unit_sprite_modulate(unit: Dictionary, acted: bool) -> Color:
return Color.WHITE return Color.WHITE
func _draw_pixel_unit_sprite(rect: Rect2, unit: Dictionary, team_color: Color, modulate: Color) -> void:
var family := _unit_class_family(unit)
var phase := _unit_idle_phase(unit)
var bob := roundf(sin(phase) * 1.25)
var step := 1.0 if sin(phase * 1.65) >= 0.0 else -1.0
var pixel := 3.0
var origin := Vector2(roundf(rect.position.x + 12.0), roundf(rect.position.y + 5.0 + bob))
var outline := _unit_pixel_color(Color(0.035, 0.026, 0.018, 1.0), modulate)
var skin := _unit_pixel_color(Color(0.86, 0.62, 0.38, 1.0), modulate)
var cloth := _unit_pixel_color(team_color.lightened(0.05), modulate)
var cloth_dark := _unit_pixel_color(team_color.darkened(0.34), modulate)
var class_color := _unit_pixel_color(_unit_class_badge_color(unit, team_color), modulate)
var accent := _unit_pixel_color(Color(0.98, 0.76, 0.28, 1.0) if not str(unit.get("officer_id", "")).is_empty() else class_color.lightened(0.25), modulate)
_draw_pixel_rect(origin, 2, 11, 8, 2, pixel, Color(0.0, 0.0, 0.0, 0.22 * modulate.a))
if family == "cavalry":
_draw_pixel_cavalry(origin, pixel, outline, skin, cloth, cloth_dark, class_color, accent, step)
elif family == "ranged":
_draw_pixel_archer(origin, pixel, outline, skin, cloth, cloth_dark, class_color, accent, step)
elif family == "tactic":
_draw_pixel_strategist(origin, pixel, outline, skin, cloth, cloth_dark, class_color, accent, step)
elif family == "heavy":
_draw_pixel_heavy(origin, pixel, outline, skin, cloth, cloth_dark, class_color, accent, step)
else:
_draw_pixel_infantry(origin, pixel, outline, skin, cloth, cloth_dark, class_color, accent, step)
func _unit_idle_phase(unit: Dictionary) -> float:
var seed := 0
var unit_id := str(unit.get("id", "unit"))
for index in range(unit_id.length()):
seed += unit_id.unicode_at(index) * (index + 1)
return float(Time.get_ticks_msec()) * 0.0024 + float(seed % 360) * 0.01745
func _unit_pixel_color(color: Color, modulate: Color) -> Color:
return Color(
color.r * modulate.r,
color.g * modulate.g,
color.b * modulate.b,
color.a * modulate.a
)
func _draw_pixel_rect(origin: Vector2, x: float, y: float, width: float, height: float, pixel: float, color: Color) -> void:
draw_rect(Rect2(origin + Vector2(x, y) * pixel, Vector2(width, height) * pixel), color)
func _draw_pixel_infantry(origin: Vector2, pixel: float, outline: Color, skin: Color, cloth: Color, cloth_dark: Color, class_color: Color, accent: Color, step: float) -> void:
_draw_pixel_rect(origin, 4, 1, 4, 2, pixel, outline)
_draw_pixel_rect(origin, 5, 2, 2, 2, pixel, skin)
_draw_pixel_rect(origin, 3, 4, 6, 5, pixel, outline)
_draw_pixel_rect(origin, 4, 4, 4, 4, pixel, cloth)
_draw_pixel_rect(origin, 3, 6, 2, 3, pixel, class_color)
_draw_pixel_rect(origin, 7, 6, 2, 3, pixel, cloth_dark)
_draw_pixel_rect(origin, 4 + step, 9, 2, 3, pixel, outline)
_draw_pixel_rect(origin, 7 - step, 9, 2, 3, pixel, outline)
_draw_pixel_rect(origin, 1, 4, 1, 8, pixel, accent.lightened(0.15))
_draw_pixel_rect(origin, 0, 3, 3, 1, pixel, accent)
func _draw_pixel_archer(origin: Vector2, pixel: float, outline: Color, skin: Color, cloth: Color, cloth_dark: Color, class_color: Color, accent: Color, step: float) -> void:
_draw_pixel_rect(origin, 4, 1, 4, 2, pixel, outline)
_draw_pixel_rect(origin, 5, 2, 2, 2, pixel, skin)
_draw_pixel_rect(origin, 3, 4, 6, 5, pixel, outline)
_draw_pixel_rect(origin, 4, 4, 4, 4, pixel, cloth)
_draw_pixel_rect(origin, 5, 5, 2, 4, pixel, class_color)
_draw_pixel_rect(origin, 4 + step, 9, 2, 3, pixel, outline)
_draw_pixel_rect(origin, 7 - step, 9, 2, 3, pixel, outline)
draw_line(origin + Vector2(9, 3) * pixel, origin + Vector2(11, 7) * pixel, accent, 2.0)
draw_line(origin + Vector2(11, 7) * pixel, origin + Vector2(9, 11) * pixel, accent, 2.0)
draw_line(origin + Vector2(4, 6) * pixel, origin + Vector2(11, 7) * pixel, accent.lightened(0.15), 1.3)
_draw_pixel_rect(origin, 2, 5, 2, 1, pixel, cloth_dark)
func _draw_pixel_cavalry(origin: Vector2, pixel: float, outline: Color, skin: Color, cloth: Color, cloth_dark: Color, class_color: Color, accent: Color, step: float) -> void:
var horse := _unit_pixel_color(Color(0.33, 0.20, 0.11, 1.0), Color(1.0, 1.0, 1.0, outline.a))
var mane := _unit_pixel_color(Color(0.12, 0.075, 0.04, 1.0), Color(1.0, 1.0, 1.0, outline.a))
_draw_pixel_rect(origin, 1, 7, 9, 3, pixel, outline)
_draw_pixel_rect(origin, 2, 7, 7, 2, pixel, horse)
_draw_pixel_rect(origin, 8, 5, 3, 3, pixel, outline)
_draw_pixel_rect(origin, 8, 6, 2, 2, pixel, horse)
_draw_pixel_rect(origin, 7, 5, 1, 3, pixel, mane)
_draw_pixel_rect(origin, 2 + step, 10, 2, 3, pixel, outline)
_draw_pixel_rect(origin, 7 - step, 10, 2, 3, pixel, outline)
_draw_pixel_rect(origin, 4, 2, 4, 2, pixel, outline)
_draw_pixel_rect(origin, 5, 3, 2, 2, pixel, skin)
_draw_pixel_rect(origin, 4, 5, 4, 3, pixel, cloth)
_draw_pixel_rect(origin, 5, 5, 2, 2, pixel, class_color)
draw_line(origin + Vector2(4, 4) * pixel, origin + Vector2(2, 1) * pixel, accent, 2.0)
func _draw_pixel_strategist(origin: Vector2, pixel: float, outline: Color, skin: Color, cloth: Color, cloth_dark: Color, class_color: Color, accent: Color, step: float) -> void:
_draw_pixel_rect(origin, 4, 1, 4, 2, pixel, outline)
_draw_pixel_rect(origin, 5, 2, 2, 2, pixel, skin)
_draw_pixel_rect(origin, 3, 4, 6, 7, pixel, outline)
_draw_pixel_rect(origin, 4, 4, 4, 6, pixel, cloth)
_draw_pixel_rect(origin, 3, 6, 2, 3, pixel, class_color)
_draw_pixel_rect(origin, 7, 6, 2, 3, pixel, class_color)
_draw_pixel_rect(origin, 4 + step, 11, 2, 2, pixel, outline)
_draw_pixel_rect(origin, 7 - step, 11, 2, 2, pixel, outline)
_draw_pixel_rect(origin, 8, 3, 3, 1, pixel, accent)
_draw_pixel_rect(origin, 9, 4, 2, 1, pixel, accent)
_draw_pixel_rect(origin, 2, 5, 1, 5, pixel, cloth_dark)
func _draw_pixel_heavy(origin: Vector2, pixel: float, outline: Color, skin: Color, cloth: Color, cloth_dark: Color, class_color: Color, accent: Color, step: float) -> void:
_draw_pixel_rect(origin, 4, 1, 4, 2, pixel, outline)
_draw_pixel_rect(origin, 5, 2, 2, 2, pixel, skin)
_draw_pixel_rect(origin, 2, 4, 8, 6, pixel, outline)
_draw_pixel_rect(origin, 3, 4, 6, 5, pixel, cloth)
_draw_pixel_rect(origin, 4, 5, 4, 4, pixel, class_color)
_draw_pixel_rect(origin, 3 + step, 10, 2, 3, pixel, outline)
_draw_pixel_rect(origin, 8 - step, 10, 2, 3, pixel, outline)
draw_line(origin + Vector2(9, 2) * pixel, origin + Vector2(2, 10) * pixel, accent, 3.0)
_draw_pixel_rect(origin, 8, 1, 3, 2, pixel, accent.lightened(0.20))
func _draw_unit_identity_marks(rect: Rect2, center: Vector2, unit: Dictionary, team_color: Color) -> void: func _draw_unit_identity_marks(rect: Rect2, center: Vector2, unit: Dictionary, team_color: Color) -> void:
if _is_generic_enemy_unit(unit): if _is_generic_enemy_unit(unit):
var sash := ENEMY_COLOR.lightened(0.22) var sash := ENEMY_COLOR.lightened(0.22)
@@ -3960,18 +4116,86 @@ func _target_preview_badge_color(kind: String) -> Color:
func _cell_from_screen(screen_position: Vector2) -> Vector2i: func _cell_from_screen(screen_position: Vector2) -> Vector2i:
var local := screen_position - BOARD_OFFSET var local := screen_position - _board_origin()
if local.x < 0 or local.y < 0: if local.x < 0 or local.y < 0:
return Vector2i(-1, -1) return Vector2i(-1, -1)
return Vector2i(int(floor(local.x / TILE_SIZE)), int(floor(local.y / TILE_SIZE))) return Vector2i(int(floor(local.x / TILE_SIZE)), int(floor(local.y / TILE_SIZE)))
func _rect_for_cell(cell: Vector2i) -> Rect2: func _rect_for_cell(cell: Vector2i) -> Rect2:
return Rect2(BOARD_OFFSET + Vector2(cell.x, cell.y) * TILE_SIZE, Vector2(TILE_SIZE, TILE_SIZE)) return Rect2(_board_origin() + Vector2(cell.x, cell.y) * TILE_SIZE, Vector2(TILE_SIZE, TILE_SIZE))
func _board_rect() -> Rect2: func _board_rect() -> Rect2:
return Rect2(BOARD_OFFSET, Vector2(state.map_size.x, state.map_size.y) * TILE_SIZE) return Rect2(_board_origin(), _board_size())
func _board_origin() -> Vector2:
return BOARD_OFFSET + board_scroll_offset
func _board_size() -> Vector2:
return Vector2(state.map_size.x, state.map_size.y) * TILE_SIZE
func _map_view_rect() -> Rect2:
var viewport_size := _viewport_size_for_layout()
var right_edge := minf(SIDE_PANEL_POSITION.x - 18.0, viewport_size.x - 18.0)
var bottom_edge := viewport_size.y - 20.0
return Rect2(
BOARD_OFFSET,
Vector2(maxf(160.0, right_edge - BOARD_OFFSET.x), maxf(160.0, bottom_edge - BOARD_OFFSET.y))
)
func _viewport_size_for_layout() -> Vector2:
if is_inside_tree():
return get_viewport_rect().size
var window_size := Vector2(DisplayServer.window_get_size())
if window_size.x > 0.0 and window_size.y > 0.0:
return window_size
return Vector2(1280, 720)
func _reset_board_scroll() -> void:
board_scroll_offset = _clamped_board_scroll_offset(Vector2.ZERO)
func _clamped_board_scroll_offset(offset: Vector2) -> Vector2:
var view_rect := _map_view_rect()
var board_size := _board_size()
var min_x := minf(0.0, view_rect.size.x - board_size.x)
var min_y := minf(0.0, view_rect.size.y - board_size.y)
return Vector2(clampf(offset.x, min_x, 0.0), clampf(offset.y, min_y, 0.0))
func _update_edge_scroll(delta: float) -> bool:
if _is_input_locked():
return false
var view_rect := _map_view_rect()
var mouse_position := get_viewport().get_mouse_position()
if not view_rect.has_point(mouse_position):
return false
var velocity := Vector2.ZERO
if mouse_position.x <= view_rect.position.x + EDGE_SCROLL_MARGIN:
velocity.x += EDGE_SCROLL_SPEED
elif mouse_position.x >= view_rect.end.x - EDGE_SCROLL_MARGIN:
velocity.x -= EDGE_SCROLL_SPEED
if mouse_position.y <= view_rect.position.y + EDGE_SCROLL_MARGIN:
velocity.y += EDGE_SCROLL_SPEED
elif mouse_position.y >= view_rect.end.y - EDGE_SCROLL_MARGIN:
velocity.y -= EDGE_SCROLL_SPEED
if velocity == Vector2.ZERO:
return false
var previous_offset := board_scroll_offset
board_scroll_offset = _clamped_board_scroll_offset(board_scroll_offset + velocity * delta)
if previous_offset.distance_squared_to(board_scroll_offset) <= 0.01:
return false
var next_hover := _cell_from_screen(mouse_position)
if next_hover != hover_cell:
hover_cell = next_hover
_update_hud()
return true
func _short_name(unit_name: String) -> String: func _short_name(unit_name: String) -> String:
@@ -5041,7 +5265,7 @@ func _normalized_dialogue_lines(lines: Array) -> Array:
"speaker": display_speaker, "speaker": display_speaker,
"text": text, "text": text,
"portrait": portrait, "portrait": portrait,
"side": _normalized_dialogue_side(line.get("side", "left")) "side": _dialogue_side_for_line(line)
}) })
elif typeof(line) == TYPE_STRING: elif typeof(line) == TYPE_STRING:
var text := str(line) var text := str(line)
@@ -5058,6 +5282,16 @@ func _normalized_dialogue_side(value) -> String:
return "left" return "left"
func _dialogue_side_for_line(line: Dictionary) -> String:
var explicit_side := str(line.get("side", "")).strip_edges()
if not explicit_side.is_empty():
return _normalized_dialogue_side(explicit_side)
var speaker := _canonical_speaker_name(str(line.get("speaker", line.get("display_speaker", ""))))
if DIALOGUE_RIGHT_SIDE_SPEAKERS.has(speaker):
return "right"
return "left"
func _can_show_dialogue_now() -> bool: func _can_show_dialogue_now() -> bool:
if not battle_started or campaign_complete_screen: if not battle_started or campaign_complete_screen:
return false return false
@@ -5095,7 +5329,7 @@ func _render_dialogue_line() -> void:
var line: Dictionary = active_dialogue_lines[active_dialogue_index] var line: Dictionary = active_dialogue_lines[active_dialogue_index]
var speaker := str(line.get("speaker", "")) var speaker := str(line.get("speaker", ""))
var display_speaker := _localized_speaker_name(str(line.get("display_speaker", speaker))) var display_speaker := _localized_speaker_name(str(line.get("display_speaker", speaker)))
var side := _normalized_dialogue_side(line.get("side", "left")) var side := _dialogue_side_for_line(line)
_apply_dialogue_side(side) _apply_dialogue_side(side)
dialogue_speaker_label.text = display_speaker if not display_speaker.is_empty() else "" dialogue_speaker_label.text = display_speaker if not display_speaker.is_empty() else ""
dialogue_speaker_label.visible = not display_speaker.is_empty() dialogue_speaker_label.visible = not display_speaker.is_empty()
@@ -5467,6 +5701,7 @@ func _load_scenario(scenario_id: String) -> void:
campaign_state.get_joined_officers_snapshot(), campaign_state.get_joined_officers_snapshot(),
campaign_state.get_scenario_title(active_scenario_id) campaign_state.get_scenario_title(active_scenario_id)
) )
_reset_board_scroll()
func _format_briefing_title(briefing: Dictionary) -> String: func _format_briefing_title(briefing: Dictionary) -> String:
@@ -7832,7 +8067,7 @@ func _update_post_move_menu() -> void:
func _local_command_panel_position_for_cell(cell: Vector2i, panel_size: Vector2) -> Vector2: func _local_command_panel_position_for_cell(cell: Vector2i, panel_size: Vector2) -> Vector2:
if not state.is_inside(cell): if not state.is_inside(cell):
return BOARD_OFFSET + Vector2(LOCAL_COMMAND_PANEL_BOARD_PADDING, LOCAL_COMMAND_PANEL_BOARD_PADDING) return _board_origin() + Vector2(LOCAL_COMMAND_PANEL_BOARD_PADDING, LOCAL_COMMAND_PANEL_BOARD_PADDING)
var cell_rect := _rect_for_cell(cell) var cell_rect := _rect_for_cell(cell)
var centered_y := cell_rect.position.y + (TILE_SIZE - panel_size.y) * 0.5 var centered_y := cell_rect.position.y + (TILE_SIZE - panel_size.y) * 0.5
var centered_x := cell_rect.position.x + (TILE_SIZE - panel_size.x) * 0.5 var centered_x := cell_rect.position.x + (TILE_SIZE - panel_size.x) * 0.5
@@ -7860,9 +8095,16 @@ func _local_command_panel_position_for_cell(cell: Vector2i, panel_size: Vector2)
func _clamped_local_command_panel_position(position: Vector2, panel_size: Vector2) -> Vector2: func _clamped_local_command_panel_position(position: Vector2, panel_size: Vector2) -> Vector2:
var board_size := Vector2(state.map_size.x, state.map_size.y) * TILE_SIZE var board_rect := _board_rect()
var board_min := BOARD_OFFSET + Vector2(LOCAL_COMMAND_PANEL_BOARD_PADDING, LOCAL_COMMAND_PANEL_BOARD_PADDING) var view_rect := _map_view_rect()
var board_max := BOARD_OFFSET + board_size - panel_size - Vector2(LOCAL_COMMAND_PANEL_BOARD_PADDING, LOCAL_COMMAND_PANEL_BOARD_PADDING) var board_min := Vector2(
maxf(board_rect.position.x, view_rect.position.x),
maxf(board_rect.position.y, view_rect.position.y)
) + Vector2(LOCAL_COMMAND_PANEL_BOARD_PADDING, LOCAL_COMMAND_PANEL_BOARD_PADDING)
var board_max := Vector2(
minf(board_rect.end.x, view_rect.end.x),
minf(board_rect.end.y, view_rect.end.y)
) - panel_size - Vector2(LOCAL_COMMAND_PANEL_BOARD_PADDING, LOCAL_COMMAND_PANEL_BOARD_PADDING)
board_max.x = maxf(board_min.x, board_max.x) board_max.x = maxf(board_min.x, board_max.x)
board_max.y = maxf(board_min.y, board_max.y) board_max.y = maxf(board_min.y, board_max.y)
return Vector2( return Vector2(

View File

@@ -0,0 +1,137 @@
extends SceneTree
const BattleSceneScript := preload("res://scripts/scenes/battle_scene.gd")
const CHAPTER_ONE_SCENARIOS := [
"res://data/scenarios/001_yellow_turbans.json",
"res://data/scenarios/002_sishui_gate.json",
"res://data/scenarios/003_xingyang_ambush.json",
"res://data/scenarios/004_qingzhou_campaign.json",
"res://data/scenarios/005_puyang_raid.json",
"res://data/scenarios/006_dingtao_counterattack.json",
"res://data/scenarios/007_xian_emperor_escort.json",
"res://data/scenarios/008_wan_castle_escape.json",
"res://data/scenarios/009_xiapi_siege.json"
]
func _init() -> void:
var failures: Array[String] = []
_check_chapter_one_scenarios_have_visual_grounding(failures)
_check_edge_scroll_layout_contract(failures)
_check_readability_contract(failures)
_check_dialogue_localization_and_side(failures)
_check_pixel_unit_helpers(failures)
if failures.is_empty():
print("chapter one polish smoke ok")
quit(0)
return
for failure in failures:
push_error(failure)
quit(1)
func _check_chapter_one_scenarios_have_visual_grounding(failures: Array[String]) -> void:
var scene = BattleSceneScript.new()
for scenario_path in CHAPTER_ONE_SCENARIOS:
if not scene.state.load_battle(scenario_path):
failures.append("could not load chapter one scenario: %s" % scenario_path)
continue
var background_path: String = scene.state.get_map_background_path()
if background_path.is_empty():
failures.append("scenario is missing a map background: %s" % scenario_path)
continue
if not FileAccess.file_exists(background_path):
failures.append("scenario background does not exist: %s -> %s" % [scenario_path, background_path])
scene.free()
func _check_edge_scroll_layout_contract(failures: Array[String]) -> void:
var scene = BattleSceneScript.new()
if not scene.state.load_battle("res://data/scenarios/009_xiapi_siege.json"):
failures.append("could not load large chapter one map for edge scrolling")
scene.free()
return
var view_rect := scene._map_view_rect()
var board_size := scene._board_size()
if view_rect.size.x < 160.0 or view_rect.size.y < 160.0:
failures.append("map view rect should keep a usable minimum size")
if board_size.x <= view_rect.size.x and board_size.y <= view_rect.size.y:
failures.append("chapter one should include at least one map large enough to exercise edge scrolling")
scene._reset_board_scroll()
if scene.board_scroll_offset.x > 0.0 or scene.board_scroll_offset.y > 0.0:
failures.append("reset board scroll should never push the map past the top-left origin")
var far_offset := scene._clamped_board_scroll_offset(Vector2(-9999.0, -9999.0))
var min_x := minf(0.0, view_rect.size.x - board_size.x)
var min_y := minf(0.0, view_rect.size.y - board_size.y)
if far_offset.x < min_x - 0.01 or far_offset.y < min_y - 0.01:
failures.append("edge scroll clamp should stay inside the visible map bounds")
if far_offset.x > 0.01 or far_offset.y > 0.01:
failures.append("edge scroll clamp should not create positive board offsets")
scene.board_scroll_offset = far_offset
var first_cell_screen := scene._rect_for_cell(Vector2i(0, 0)).position + Vector2(4.0, 4.0)
if scene._cell_from_screen(first_cell_screen) != Vector2i(0, 0):
failures.append("screen-to-cell mapping should stay stable after scrolling")
scene.free()
func _check_readability_contract(failures: Array[String]) -> void:
var scene = BattleSceneScript.new()
scene._create_hud()
if scene.dialogue_text_panel.custom_minimum_size.y < 160.0:
failures.append("dialogue text panel should be tall enough for multi-line story text")
if scene.dialogue_text_label.custom_minimum_size.y < 130.0:
failures.append("dialogue label should reserve enough height to avoid clipping")
if scene.dialogue_text_label.autowrap_mode != TextServer.AUTOWRAP_WORD_SMART:
failures.append("dialogue text should use smart Korean-friendly wrapping")
if scene.briefing_objective_panel.custom_minimum_size.y < 120.0:
failures.append("briefing objective panel should reserve enough height for victory and defeat text")
if scene.briefing_objective_toggle_button == null:
failures.append("briefing objective section should have a close/open button")
elif scene.briefing_objective_toggle_button.text != "닫기":
failures.append("briefing objective button should start with a clear close label")
scene.free()
func _check_dialogue_localization_and_side(failures: Array[String]) -> void:
var scene = BattleSceneScript.new()
var lines := scene._normalized_dialogue_lines([
{"speaker": "Cao Cao", "text": "전열을 세워라."},
{"speaker": "Lu Bu", "text": "조조를 짓밟아라."},
{"speaker": "Qingzhou Chief", "text": "마을 곡식을 내놓아라."},
{"speaker": "Camp Merchant", "text": "군막에 물자가 남았습니다."}
])
if lines.size() != 4:
failures.append("dialogue normalization should keep all valid chapter one lines")
scene.free()
return
if lines[0].get("speaker", "") != "조조" or lines[0].get("side", "") != "left":
failures.append("Cao Cao dialogue should localize to Korean on the left")
if lines[1].get("speaker", "") != "여포" or lines[1].get("side", "") != "right":
failures.append("enemy officer dialogue should localize to Korean on the right")
if lines[2].get("speaker", "") != "청주 두령" or lines[2].get("side", "") != "right":
failures.append("chapter one named enemy dialogue should be localized and right-sided")
if lines[3].get("speaker", "") != "군막 상인" or lines[3].get("side", "") != "right":
failures.append("camp merchant dialogue should be localized and right-sided by default")
scene.free()
func _check_pixel_unit_helpers(failures: Array[String]) -> void:
var scene = BattleSceneScript.new()
for method_name in [
"_draw_pixel_unit_sprite",
"_draw_pixel_infantry",
"_draw_pixel_archer",
"_draw_pixel_cavalry",
"_draw_pixel_strategist",
"_draw_pixel_heavy"
]:
if not scene.has_method(method_name):
failures.append("missing pixel-map unit helper: %s" % method_name)
scene.free()