Improve readable text surfaces

This commit is contained in:
2026-06-20 16:59:26 +09:00
parent c5ee403fef
commit 11242cded7
5 changed files with 182 additions and 71 deletions

View File

@@ -26,6 +26,10 @@ const THREAT_OVERLAY_COLOR := Color(0.76, 0.060, 0.035, 0.20)
const THREAT_BORDER_COLOR := Color(0.86, 0.18, 0.10, 0.48)
const BGM_MENU_PATH := "res://audio/bgm/menu_camp_dawn_theme.wav"
const BGM_BATTLE_PATH := "res://audio/bgm/battle_yingchuan_march.wav"
const BGM_TRACK_LABELS := {
"menu": "군막의 새벽",
"battle": "영천 진군"
}
const TITLE_BACKGROUND_PATH := "res://art/backgrounds/title_war_camp_dawn.png"
const TITLE_PORTRAIT_PATH := "res://art/portraits/cao_cao.png"
const TITLE_MENU_SURFACE_PATH := "res://art/ui/panels/panel_title_command.png"
@@ -92,6 +96,7 @@ const DEFAULT_AUDIO_VOLUME_PERCENT := 75
const AUDIO_MUTED_DB := -80.0
const MAX_RECENT_SFX_KEYS := 32
const AUDIO_PREVIEW_SFX_KEYS := ["menu_confirm", "footstep", "slash"]
const AUDIO_PREVIEW_LABEL := "확인음 · 발소리 · 타격음"
const FLOATING_TEXT_LIFETIME := 1.0
const FLOATING_TEXT_RISE := 42.0
const OBJECTIVE_NOTICE_DURATION := 2.6
@@ -136,17 +141,17 @@ const DIALOGUE_PANEL_POSITION := Vector2(40, 324)
const DIALOGUE_PANEL_SIZE := Vector2(1200, 356)
const DIALOGUE_PORTRAIT_SIZE := Vector2(220, 310)
const DIALOGUE_COLUMN_SIZE := Vector2(850, 318)
const DIALOGUE_TEXT_SIZE := Vector2(830, 164)
const OBJECTIVE_HUD_FONT_SIZE := 13
const OBJECTIVE_HUD_MIN_FONT_SIZE := 11
const OBJECTIVE_NOTICE_FONT_SIZE := 15
const OBJECTIVE_NOTICE_MIN_FONT_SIZE := 12
const BRIEFING_OBJECTIVE_FONT_SIZE := 15
const BRIEFING_OBJECTIVE_MIN_FONT_SIZE := 12
const DIALOGUE_SPEAKER_FONT_SIZE := 18
const DIALOGUE_SPEAKER_MIN_FONT_SIZE := 15
const DIALOGUE_TEXT_FONT_SIZE := 17
const DIALOGUE_TEXT_MIN_FONT_SIZE := 14
const DIALOGUE_TEXT_SIZE := Vector2(830, 188)
const OBJECTIVE_HUD_FONT_SIZE := 14
const OBJECTIVE_HUD_MIN_FONT_SIZE := 12
const OBJECTIVE_NOTICE_FONT_SIZE := 16
const OBJECTIVE_NOTICE_MIN_FONT_SIZE := 14
const BRIEFING_OBJECTIVE_FONT_SIZE := 17
const BRIEFING_OBJECTIVE_MIN_FONT_SIZE := 14
const DIALOGUE_SPEAKER_FONT_SIZE := 20
const DIALOGUE_SPEAKER_MIN_FONT_SIZE := 16
const DIALOGUE_TEXT_FONT_SIZE := 19
const DIALOGUE_TEXT_MIN_FONT_SIZE := 16
const LOCAL_COMMAND_TITLE_FONT_SIZE := 14
const LOCAL_COMMAND_TITLE_MIN_FONT_SIZE := 10
const LOCAL_COMMAND_DETAIL_FONT_SIZE := 12
@@ -212,7 +217,7 @@ const BATTLE_UNIT_LIST_CLASS_CREST_ICON_SIZE := Vector2(17, 17)
const BATTLE_UNIT_LIST_BADGE_SIZE := Vector2(72, 22)
const BATTLE_UNIT_LIST_BADGE_ICON_SIZE := Vector2(15, 15)
const BATTLE_UNIT_LIST_HP_BAR_SIZE := Vector2(40, 7)
const SETTINGS_PANEL_SIZE := Vector2(420, 368)
const SETTINGS_PANEL_SIZE := Vector2(420, 414)
const LOCAL_COMMAND_BUTTON_SIZE := Vector2(92, 38)
const LOCAL_COMMAND_CANCEL_BUTTON_SIZE := Vector2(196, 30)
const LOCAL_COMMAND_HEADER_ICON_SIZE := Vector2(24, 24)
@@ -234,6 +239,9 @@ const UI_RICE_PAPER_DARK := Color(0.18, 0.17, 0.14, 0.98)
const UI_READABLE_PAPER := Color(0.66, 0.63, 0.53, 0.998)
const UI_READABLE_CARD := Color(0.58, 0.55, 0.47, 0.998)
const UI_HUD_READABLE := Color(0.060, 0.056, 0.048, 0.982)
const UI_TEXT_PAPER_SURFACE := Color(0.72, 0.69, 0.58, 0.998)
const UI_TEXT_DARK_SURFACE := Color(0.030, 0.028, 0.024, 0.996)
const UI_TEXT_NOTICE_SURFACE := Color(0.68, 0.65, 0.55, 0.998)
const UI_AGED_INK := Color(0.055, 0.060, 0.055, 1.0)
const UI_DARK_LACQUER := Color(0.045, 0.038, 0.034, 0.988)
const UI_LACQUER_EDGE := Color(0.025, 0.023, 0.020, 1.0)
@@ -597,6 +605,7 @@ var settings_bgm_volume_slider: HSlider
var settings_sfx_volume_slider: HSlider
var settings_bgm_volume_value_label: Label
var settings_sfx_volume_value_label: Label
var settings_audio_status_label: Label
var settings_audio_reset_button: Button
var restart_button: Button
var new_campaign_button: Button
@@ -635,6 +644,7 @@ var title_bgm_volume_slider: HSlider
var title_sfx_volume_slider: HSlider
var title_bgm_volume_value_label: Label
var title_sfx_volume_value_label: Label
var title_audio_status_label: Label
var title_audio_reset_button: Button
var title_settings_back_button: Button
var title_screen_visible := false
@@ -838,6 +848,16 @@ func _panel_texture_spec(variant: String) -> Dictionary:
return {}
func _panel_variant_prefers_flat_text_surface(variant: String) -> bool:
return variant in [
"dialogue",
"speech_scroll",
"notice_edict",
"cinematic_caption",
"story_caption"
]
func _age_panel_style(style: StyleBoxFlat, variant: String, border_width: int) -> void:
if style == null:
return
@@ -922,11 +942,11 @@ func _apply_panel_style(panel: PanelContainer, variant: String) -> void:
margin = 6
shadow_size = 4
"notice_edict":
fill = Color(0.42, 0.40, 0.34, 0.998)
border = UI_LACQUER_EDGE
border_width = 7
radius = 11
margin = 14
fill = UI_TEXT_NOTICE_SURFACE
border = Color(0.10, 0.085, 0.060, 1.0)
border_width = 5
radius = 10
margin = 18
shadow_size = 14
"seal":
fill = Color(0.080, 0.030, 0.022, 1.0)
@@ -947,18 +967,18 @@ func _apply_panel_style(panel: PanelContainer, variant: String) -> void:
radius = 10
margin = 3
"dialogue":
fill = Color(0.045, 0.045, 0.040, 0.982)
border = Color(0.18, 0.17, 0.14, 1.0)
border_width = 8
radius = 12
margin = 20
fill = UI_TEXT_DARK_SURFACE
border = Color(0.28, 0.23, 0.16, 1.0)
border_width = 7
radius = 10
margin = 22
shadow_size = 18
"speech_scroll":
fill = Color(0.58, 0.55, 0.48, 0.996)
border = Color(0.13, 0.12, 0.10, 0.98)
border_width = 5
radius = 11
margin = 15
fill = UI_TEXT_PAPER_SURFACE
border = Color(0.12, 0.10, 0.072, 1.0)
border_width = 4
radius = 9
margin = 18
shadow_size = 6
"hud_info":
fill = Color(0.045, 0.050, 0.048, 0.90)
@@ -1015,14 +1035,14 @@ func _apply_panel_style(panel: PanelContainer, variant: String) -> void:
margin = 5
shadow_size = 2
"cinematic_caption":
fill = Color(0.010, 0.010, 0.009, 0.996)
fill = Color(0.008, 0.008, 0.007, 0.998)
border = UI_TARNISHED_BRONZE
border_width = 4
radius = 10
margin = 14
shadow_size = 22
"story_caption":
fill = Color(0.15, 0.13, 0.10, 0.94)
fill = Color(0.030, 0.026, 0.020, 0.988)
border = UI_TARNISHED_BRONZE
border_width = 3
radius = 10
@@ -1067,7 +1087,7 @@ func _apply_panel_style(panel: PanelContainer, variant: String) -> void:
pass
var style := _make_panel_style(fill, border, border_width, radius, margin, shadow_size)
_age_panel_style(style, variant, border_width)
var texture_style := _make_panel_texture_style(variant, margin)
var texture_style := _make_panel_texture_style(variant, margin) if not _panel_variant_prefers_flat_text_surface(variant) else null
if texture_style != null:
panel.add_theme_stylebox_override("panel", texture_style)
panel.set_meta("generated_panel_texture", true)
@@ -1280,12 +1300,14 @@ func _apply_text_legibility(control: Control, font_color: Color, font_size: int
var resolved_size := font_size
if resolved_size <= 0:
resolved_size = control.get_theme_font_size("font_size")
var outline_size := 2 if resolved_size >= 13 else 1
var outline_size := 3 if resolved_size >= 16 else (2 if resolved_size >= 12 else 1)
control.add_theme_color_override("font_outline_color", _text_outline_color_for(font_color))
control.add_theme_constant_override("outline_size", outline_size)
control.add_theme_color_override("font_shadow_color", UI_TEXT_SHADOW)
control.add_theme_constant_override("shadow_offset_x", 1)
control.add_theme_constant_override("shadow_offset_y", 1)
if control is Label:
control.add_theme_constant_override("line_spacing", 4 if resolved_size >= 16 else 3)
func _apply_strong_text_legibility(control: Control, font_color: Color, font_size: int = 0) -> void:
@@ -2366,7 +2388,7 @@ func _estimated_wrapped_line_count(text: String, width: float, font_size: int) -
func _estimated_line_height(font_size: int) -> float:
return ceilf(float(font_size) * 1.36)
return ceilf(float(font_size) * 1.46 + 3.0)
func _weighted_text_length(text: String) -> float:
@@ -2895,7 +2917,7 @@ func _make_map_lattice_line(position: Vector2, size: Vector2, alpha: float) -> C
func _make_section_caption(text: String, width: float = 0.0) -> Control:
var panel := PanelContainer.new()
panel.custom_minimum_size = Vector2(width, 24)
panel.custom_minimum_size = Vector2(width, 30)
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_apply_panel_style(panel, "caption")
var row := HBoxContainer.new()
@@ -2904,11 +2926,11 @@ func _make_section_caption(text: String, width: float = 0.0) -> Control:
row.add_child(_make_seal_tick())
var label := Label.new()
label.text = text
label.custom_minimum_size = Vector2(maxf(0.0, width - 68.0), 18)
label.custom_minimum_size = Vector2(maxf(0.0, width - 68.0), 22)
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_apply_label_style(label, UI_PARCHMENT_TEXT, 13)
_apply_label_style(label, UI_PARCHMENT_TEXT, 15)
row.add_child(label)
row.add_child(_make_seal_tick())
return panel
@@ -3049,7 +3071,7 @@ func _create_hud() -> void:
top_objective_chip = PanelContainer.new()
top_objective_chip.name = "TopObjectiveChip"
top_objective_chip.custom_minimum_size = Vector2(396, 54)
_apply_panel_style(top_objective_chip, "edict_compact")
_apply_panel_style(top_objective_chip, "hud_readable")
top_row.add_child(top_objective_chip)
var objective_top_row := HBoxContainer.new()
@@ -3062,7 +3084,7 @@ func _create_hud() -> void:
objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
objective_label.custom_minimum_size = Vector2(274, 36)
objective_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_apply_label_style(objective_label, UI_AGED_INK, OBJECTIVE_HUD_FONT_SIZE)
_apply_label_style(objective_label, UI_PARCHMENT_TEXT, OBJECTIVE_HUD_FONT_SIZE)
objective_top_row.add_child(objective_label)
objective_top_row.add_child(_make_bamboo_gutter(10, 36))
@@ -3314,6 +3336,13 @@ func _create_hud() -> void:
_apply_label_style(settings_sfx_volume_value_label, UI_PARCHMENT_TEXT, 13)
settings_sfx_volume_row.add_child(settings_sfx_volume_value_label)
settings_audio_status_label = Label.new()
settings_audio_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
settings_audio_status_label.custom_minimum_size = Vector2(374, 42)
settings_audio_status_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_apply_label_style(settings_audio_status_label, UI_PARCHMENT_TEXT, 12)
settings_column.add_child(settings_audio_status_label)
settings_audio_reset_button = Button.new()
settings_audio_reset_button.text = "기본값 듣기"
settings_audio_reset_button.custom_minimum_size = Vector2(374, 32)
@@ -3338,7 +3367,8 @@ func _create_hud() -> void:
objective_notice_panel = PanelContainer.new()
objective_notice_panel.visible = false
objective_notice_panel.position = Vector2(338, 88)
objective_notice_panel.size = Vector2(604, 170)
objective_notice_panel.size = Vector2(604, 190)
objective_notice_panel.custom_minimum_size = Vector2(604, 190)
objective_notice_panel.mouse_filter = Control.MOUSE_FILTER_PASS
_apply_panel_style(objective_notice_panel, "notice_edict")
root.add_child(objective_notice_panel)
@@ -3361,7 +3391,7 @@ func _create_hud() -> void:
objective_notice_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
objective_notice_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
objective_notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
objective_notice_label.custom_minimum_size = Vector2(548, 62)
objective_notice_label.custom_minimum_size = Vector2(548, 72)
_apply_label_style(objective_notice_label, UI_AGED_INK, OBJECTIVE_NOTICE_FONT_SIZE)
objective_notice_column.add_child(objective_notice_label)
objective_notice_column.add_child(_make_scroll_rod(548, 3))
@@ -3373,7 +3403,7 @@ func _create_hud() -> void:
auto_end_turn_prompt_panel.position = Vector2(408, 112)
auto_end_turn_prompt_panel.size = Vector2(464, 154)
auto_end_turn_prompt_panel.custom_minimum_size = Vector2(464, 154)
_apply_panel_style(auto_end_turn_prompt_panel, "command_seal")
_apply_panel_style(auto_end_turn_prompt_panel, "dialogue")
root.add_child(auto_end_turn_prompt_panel)
var auto_end_column := VBoxContainer.new()
@@ -3955,7 +3985,7 @@ func _create_hud() -> void:
var opening_caption_readability_mat := ColorRect.new()
opening_caption_readability_mat.name = "OpeningCaptionTextBacking"
opening_caption_readability_mat.color = Color(0.0, 0.0, 0.0, 0.62)
opening_caption_readability_mat.color = Color(0.0, 0.0, 0.0, 0.74)
opening_caption_readability_mat.mouse_filter = Control.MOUSE_FILTER_IGNORE
opening_caption_readability_mat.position = Vector2(30, 488)
opening_caption_readability_mat.size = Vector2(1220, 198)
@@ -4117,7 +4147,7 @@ func _create_hud() -> void:
briefing_objective_header.add_child(briefing_objective_toggle_button)
briefing_objective_panel = PanelContainer.new()
briefing_objective_panel.custom_minimum_size = Vector2(680, 124)
briefing_objective_panel.custom_minimum_size = Vector2(680, 148)
_apply_panel_style(briefing_objective_panel, "briefing_card")
briefing_column.add_child(briefing_objective_panel)
@@ -4127,9 +4157,9 @@ func _create_hud() -> void:
briefing_objective_label = Label.new()
briefing_objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
briefing_objective_label.custom_minimum_size = Vector2(648, 96)
briefing_objective_label.custom_minimum_size = Vector2(648, 120)
briefing_objective_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_apply_label_style(briefing_objective_label, UI_AGED_INK, 17)
_apply_label_style(briefing_objective_label, UI_AGED_INK, BRIEFING_OBJECTIVE_FONT_SIZE)
briefing_objective_row.add_child(briefing_objective_label)
briefing_column.add_child(_make_section_caption("전장도와 군막 보고", 680))
@@ -4185,7 +4215,7 @@ func _create_hud() -> void:
briefing_camp_overview_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
briefing_camp_overview_label.custom_minimum_size = Vector2(286, 86)
briefing_camp_overview_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_apply_label_style(briefing_camp_overview_label, UI_AGED_INK, 15)
_apply_label_style(briefing_camp_overview_label, UI_AGED_INK, 16)
briefing_camp_overview_row.add_child(briefing_camp_overview_label)
briefing_tactical_marker_list = VBoxContainer.new()
@@ -4196,13 +4226,13 @@ func _create_hud() -> void:
briefing_camp_overview_row.add_child(briefing_tactical_marker_list)
var briefing_scroll := ScrollContainer.new()
briefing_scroll.custom_minimum_size = Vector2(680, 84)
briefing_scroll.custom_minimum_size = Vector2(680, 96)
briefing_column.add_child(briefing_scroll)
briefing_label = Label.new()
briefing_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
briefing_label.custom_minimum_size = Vector2(656, 0)
_apply_label_style(briefing_label, UI_AGED_INK, 16)
_apply_label_style(briefing_label, UI_AGED_INK, 17)
briefing_scroll.add_child(briefing_label)
var prep_button_row := HBoxContainer.new()
@@ -4498,13 +4528,17 @@ func _create_hud() -> void:
dialogue_previous_button = Button.new()
dialogue_previous_button.text = "이전"
dialogue_previous_button.disabled = true
dialogue_previous_button.custom_minimum_size = Vector2(88, 32)
dialogue_previous_button.custom_minimum_size = Vector2(96, 38)
dialogue_previous_button.add_theme_font_size_override("font_size", 15)
dialogue_previous_button.set_meta("flat_readable_button", true)
dialogue_previous_button.pressed.connect(_retreat_dialogue)
dialogue_control_row.add_child(dialogue_previous_button)
dialogue_continue_button = Button.new()
dialogue_continue_button.text = "다음"
dialogue_continue_button.custom_minimum_size = Vector2(120, 32)
dialogue_continue_button.custom_minimum_size = Vector2(128, 38)
dialogue_continue_button.add_theme_font_size_override("font_size", 16)
dialogue_continue_button.set_meta("flat_readable_button", true)
dialogue_continue_button.pressed.connect(_advance_dialogue)
dialogue_control_row.add_child(dialogue_continue_button)
@@ -4809,7 +4843,7 @@ func _create_hud() -> void:
title_settings_panel = PanelContainer.new()
title_settings_panel.visible = false
title_settings_panel.custom_minimum_size = Vector2(390, 314)
title_settings_panel.custom_minimum_size = Vector2(390, 362)
_apply_panel_style(title_settings_panel, "hud_info")
title_menu_column.add_child(title_settings_panel)
@@ -4901,6 +4935,13 @@ func _create_hud() -> void:
_apply_label_style(title_sfx_volume_value_label, UI_PARCHMENT_TEXT, 13)
title_sfx_volume_row.add_child(title_sfx_volume_value_label)
title_audio_status_label = Label.new()
title_audio_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
title_audio_status_label.custom_minimum_size = Vector2(350, 42)
title_audio_status_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_apply_label_style(title_audio_status_label, UI_PARCHMENT_TEXT, 12)
title_settings_column.add_child(title_audio_status_label)
title_audio_reset_button = Button.new()
_configure_title_menu_button(
title_audio_reset_button,
@@ -5129,6 +5170,35 @@ func _refresh_title_volume_controls() -> void:
title_sfx_volume_value_label.text = "%d%%" % title_sfx_volume_percent
if settings_sfx_volume_value_label != null:
settings_sfx_volume_value_label.text = "%d%%" % title_sfx_volume_percent
var audio_status := _format_audio_status_text()
var audio_tooltip := _format_audio_status_tooltip()
if title_audio_status_label != null:
title_audio_status_label.text = audio_status
title_audio_status_label.tooltip_text = audio_tooltip
if settings_audio_status_label != null:
settings_audio_status_label.text = audio_status
settings_audio_status_label.tooltip_text = audio_tooltip
func _format_audio_status_text() -> String:
var bgm_text := "음악 꺼짐"
if title_bgm_enabled:
bgm_text = "현재 음악: %s %d%%" % [_current_bgm_display_name(), title_bgm_volume_percent]
var sfx_text := "효과음 꺼짐"
if title_sfx_enabled:
sfx_text = "효과음 %d%%" % title_sfx_volume_percent
return "%s\n%s · 미리듣기: %s" % [bgm_text, sfx_text, AUDIO_PREVIEW_LABEL]
func _format_audio_status_tooltip() -> String:
return "현재 재생되는 음악과 기본값 듣기에서 확인할 효과음입니다.\n%s" % _format_audio_status_text()
func _current_bgm_display_name() -> String:
var key := current_bgm_key
if key.is_empty():
key = "menu"
return str(BGM_TRACK_LABELS.get(key, key))
func _on_bgm_finished() -> void:
@@ -5145,6 +5215,7 @@ func _play_bgm(key: String) -> void:
if not title_bgm_enabled:
current_bgm_key = key
bgm_player.stop()
_refresh_title_volume_controls()
return
bgm_player.volume_db = _bgm_volume_db()
if current_bgm_key == key and bgm_player.playing:
@@ -5153,6 +5224,7 @@ func _play_bgm(key: String) -> void:
if stream == null:
return
current_bgm_key = key
_refresh_title_volume_controls()
bgm_player.stop()
bgm_player.stream = stream
if not bgm_player.is_inside_tree():