Show side event markers

This commit is contained in:
2026-06-19 19:42:00 +09:00
parent b01995db00
commit 6d95f13794
7 changed files with 220 additions and 4 deletions

View File

@@ -602,6 +602,7 @@
"when": { "when": {
"type": "unit_reaches_tile", "type": "unit_reaches_tile",
"team": "player", "team": "player",
"label": "북숲 보급고",
"pos": [ "pos": [
4, 4,
2 2
@@ -641,6 +642,7 @@
"when": { "when": {
"type": "unit_reaches_tile", "type": "unit_reaches_tile",
"team": "player", "team": "player",
"label": "마을 보급",
"cells": [ "cells": [
[ [
6, 6,

File diff suppressed because one or more lines are too long

View File

@@ -613,7 +613,7 @@ Movement-triggered pickups can use `grant_item` for one item or `grant_items` fo
{ {
"id": "northern_woods_cache", "id": "northern_woods_cache",
"once": true, "once": true,
"when": { "type": "unit_reaches_tile", "team": "player", "pos": [3, 2] }, "when": { "type": "unit_reaches_tile", "team": "player", "label": "북숲 보급고", "pos": [3, 2] },
"actions": [ "actions": [
{ "type": "log", "text": "The northern woods cache is recovered." }, { "type": "log", "text": "The northern woods cache is recovered." },
{ "type": "grant_items", "items": ["bean", { "item_id": "antidote", "count": 2 }] } { "type": "grant_items", "items": ["bean", { "item_id": "antidote", "count": 2 }] }
@@ -621,6 +621,8 @@ Movement-triggered pickups can use `grant_item` for one item or `grant_items` fo
} }
``` ```
Rewarding `unit_reaches_tile` events are also exposed as optional side-event markers on the battlefield when they are not already objective cells. Use `when.label` to keep the map marker and tile-info text readable in Korean; otherwise the marker falls back to a short supply or gold label derived from its reward action.
Movement-triggered gold caches can use `grant_gold` with a positive `amount`. The gold is shown immediately through log and floating feedback, but it stays battle-only until victory commits the battle result. Movement-triggered gold caches can use `grant_gold` with a positive `amount`. The gold is shown immediately through log and floating feedback, but it stays battle-only until victory commits the battle result.
```json ```json

View File

@@ -1749,6 +1749,67 @@ func get_objective_cell_label(cell: Vector2i) -> String:
return _objective_cell_label_for_condition(battle_conditions.get("victory", {}), cell) return _objective_cell_label_for_condition(battle_conditions.get("victory", {}), cell)
func get_event_markers() -> Array[Dictionary]:
var result: Array[Dictionary] = []
var objective_cells := get_objective_cells()
for event in battle_events:
var event_id := str(event.get("id", ""))
if bool(event.get("once", false)) and fired_event_ids.has(event_id):
continue
var when: Dictionary = event.get("when", {})
if str(when.get("type", "")) != "unit_reaches_tile":
continue
if not _event_gate_open(when) or not _campaign_flags_match(when.get("campaign_flags", {})):
continue
if not _event_has_reward_action(event):
continue
var cells: Array[Vector2i] = []
for cell in _condition_cells(when):
if not objective_cells.has(cell) and not cells.has(cell):
cells.append(cell)
if cells.is_empty():
continue
result.append({
"id": event_id,
"label": _event_marker_label(event),
"kind": _event_marker_kind(event),
"cells": cells
})
return result
func get_event_marker_cells() -> Array[Vector2i]:
var result: Array[Vector2i] = []
for marker in get_event_markers():
var cells = marker.get("cells", [])
if typeof(cells) != TYPE_ARRAY:
continue
for cell in cells:
if typeof(cell) == TYPE_VECTOR2I and not result.has(cell):
result.append(cell)
return result
func get_event_marker_label(cell: Vector2i) -> String:
if not is_inside(cell):
return ""
for marker in get_event_markers():
var cells = marker.get("cells", [])
if typeof(cells) == TYPE_ARRAY and cells.has(cell):
return str(marker.get("label", "전장 표식"))
return ""
func get_event_marker_kind(cell: Vector2i) -> String:
if not is_inside(cell):
return ""
for marker in get_event_markers():
var cells = marker.get("cells", [])
if typeof(cells) == TYPE_ARRAY and cells.has(cell):
return str(marker.get("kind", "event"))
return ""
func get_objective_progress_lines(include_defeat_risks := false, include_turn_limit := true) -> Array[String]: func get_objective_progress_lines(include_defeat_risks := false, include_turn_limit := true) -> Array[String]:
var result: Array[String] = [] var result: Array[String] = []
_append_condition_progress_lines(battle_conditions.get("victory", {}), result, "victory") _append_condition_progress_lines(battle_conditions.get("victory", {}), result, "victory")
@@ -4260,6 +4321,47 @@ func _objective_cell_label_for_after_event(event_id: String, cell: Vector2i) ->
return "" return ""
func _event_has_reward_action(event: Dictionary) -> bool:
for action in event.get("actions", []):
if typeof(action) != TYPE_DICTIONARY:
continue
var action_type := str((action as Dictionary).get("type", ""))
if action_type == "grant_item" or action_type == "grant_items" or action_type == "grant_gold":
return true
return false
func _event_marker_label(event: Dictionary) -> String:
var when: Dictionary = event.get("when", {})
var label := _condition_destination_label(when)
if label != "전장 표식":
return label
var kind := _event_marker_kind(event)
if kind == "gold":
return "군자금"
if kind == "supply":
return "보급"
return "전장 표식"
func _event_marker_kind(event: Dictionary) -> String:
var has_item := false
var has_gold := false
for action in event.get("actions", []):
if typeof(action) != TYPE_DICTIONARY:
continue
var action_type := str((action as Dictionary).get("type", ""))
if action_type == "grant_item" or action_type == "grant_items":
has_item = true
elif action_type == "grant_gold":
has_gold = true
if has_gold and not has_item:
return "gold"
if has_item:
return "supply"
return "event"
func _find_turn_limit(condition_group) -> int: func _find_turn_limit(condition_group) -> int:
if typeof(condition_group) == TYPE_ARRAY: if typeof(condition_group) == TYPE_ARRAY:
var best_limit := 0 var best_limit := 0

View File

@@ -505,6 +505,7 @@ func _draw() -> void:
_draw_recovery_terrain_markers(_visible_cell_bounds()) _draw_recovery_terrain_markers(_visible_cell_bounds())
_draw_units() _draw_units()
_draw_objective_foreground_markers() _draw_objective_foreground_markers()
_draw_event_foreground_markers()
_draw_target_selection_markers() _draw_target_selection_markers()
_draw_attack_effects() _draw_attack_effects()
_draw_target_preview_badge() _draw_target_preview_badge()
@@ -3275,7 +3276,7 @@ func _draw_objective_seal_marker(rect: Rect2) -> void:
func _draw_objective_foreground_markers() -> void: func _draw_objective_foreground_markers() -> void:
var font := _draw_ui_font(true) var font := _draw_ui_font(true)
for cell in state.get_objective_cells(): for cell in state.get_objective_cells():
if not state.is_inside(cell): if not state.is_inside(cell) or not _cell_is_in_visible_bounds(cell):
continue continue
var rect := _rect_for_cell(cell) var rect := _rect_for_cell(cell)
_draw_objective_corner_brackets(rect) _draw_objective_corner_brackets(rect)
@@ -3285,6 +3286,58 @@ func _draw_objective_foreground_markers() -> void:
_draw_ink_text(font, label_rect.position + Vector2(0, 12), "", HORIZONTAL_ALIGNMENT_CENTER, label_rect.size.x, 11, UI_PARCHMENT_TEXT) _draw_ink_text(font, label_rect.position + Vector2(0, 12), "", HORIZONTAL_ALIGNMENT_CENTER, label_rect.size.x, 11, UI_PARCHMENT_TEXT)
func _draw_event_foreground_markers() -> void:
var font := _draw_ui_font(true)
for marker in state.get_event_markers():
var cells = marker.get("cells", [])
if typeof(cells) != TYPE_ARRAY:
continue
for cell in cells:
if typeof(cell) != TYPE_VECTOR2I or not state.is_inside(cell) or not _cell_is_in_visible_bounds(cell):
continue
_draw_event_foreground_marker(_rect_for_cell(cell), str(marker.get("label", "보급")), str(marker.get("kind", "event")), font)
func _draw_event_foreground_marker(rect: Rect2, label: String, kind: String, font: Font) -> void:
var color := _event_marker_color(kind)
var marker_rect := _event_marker_rect(rect)
draw_rect(marker_rect, Color(0.035, 0.022, 0.012, 0.76))
draw_rect(marker_rect.grow(-2.0), Color(color.r, color.g, color.b, 0.72))
draw_rect(marker_rect, Color(UI_OLD_BRONZE.r, UI_OLD_BRONZE.g, UI_OLD_BRONZE.b, 0.88), false, 1.2)
var text := _event_marker_abbrev(label, kind)
_draw_ink_text(font, marker_rect.position + Vector2(0, 12), text, HORIZONTAL_ALIGNMENT_CENTER, marker_rect.size.x, 11, UI_PARCHMENT_TEXT)
var knot := marker_rect.get_center() + Vector2(0.0, -8.0)
draw_line(knot + Vector2(-5.0, 0.0), knot + Vector2(5.0, 0.0), Color(0.10, 0.05, 0.02, 0.70), 1.2)
draw_line(knot + Vector2(0.0, -4.0), knot + Vector2(0.0, 4.0), Color(0.10, 0.05, 0.02, 0.56), 1.0)
func _event_marker_rect(cell_rect: Rect2) -> Rect2:
return Rect2(cell_rect.position + Vector2(4.0, 26.0), Vector2(23.0, 17.0))
func _event_marker_color(kind: String) -> Color:
if kind == "gold":
return Color(0.92, 0.62, 0.18, 1.0)
if kind == "supply":
return Color(0.38, 0.62, 0.34, 1.0)
return Color(UI_MUTED_JADE.r, UI_MUTED_JADE.g, UI_MUTED_JADE.b, 1.0)
func _event_marker_abbrev(label: String, kind: String) -> String:
if kind == "gold":
return ""
var text := label.strip_edges()
if text.is_empty():
return "" if kind == "supply" else ""
if text.contains("마을"):
return ""
if text.contains(""):
return ""
if text.contains("보급"):
return ""
return text.substr(0, 1)
func _draw_objective_corner_brackets(rect: Rect2) -> void: func _draw_objective_corner_brackets(rect: Rect2) -> void:
var bracket := 12.0 var bracket := 12.0
var inset := 4.0 var inset := 4.0
@@ -3380,6 +3433,8 @@ func _draw_units() -> void:
continue continue
var rect := _visual_rect_for_unit(unit) var rect := _visual_rect_for_unit(unit)
if not rect.intersects(_map_view_rect().grow(TILE_SIZE)):
continue
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 acted := bool(unit.get("acted", false)) var acted := bool(unit.get("acted", false))
@@ -5543,6 +5598,9 @@ func _update_cell_info() -> void:
var objective_cell_label := state.get_objective_cell_label(cell) var objective_cell_label := state.get_objective_cell_label(cell)
if not objective_cell_label.is_empty(): if not objective_cell_label.is_empty():
text += "\n목표: %s" % objective_cell_label text += "\n목표: %s" % objective_cell_label
var event_marker_label := state.get_event_marker_label(cell)
if not event_marker_label.is_empty():
text += "\n표식: %s" % event_marker_label
if show_threat_overlay: if show_threat_overlay:
var threat_names := state.get_threatening_unit_names(cell, BattleState.TEAM_ENEMY) var threat_names := state.get_threatening_unit_names(cell, BattleState.TEAM_ENEMY)
if not threat_names.is_empty(): if not threat_names.is_empty():

View File

@@ -172,6 +172,7 @@ func _init() -> void:
_check_event_gated_objective_markers(failures) _check_event_gated_objective_markers(failures)
_check_event_gated_multi_cell_objective_markers(failures) _check_event_gated_multi_cell_objective_markers(failures)
_check_turn_gated_objective_does_not_create_marker(failures) _check_turn_gated_objective_does_not_create_marker(failures)
_check_opening_battle_side_event_markers(failures)
_check_opening_battle_requires_castle_capture(failures) _check_opening_battle_requires_castle_capture(failures)
_check_opening_battle_capture_events(failures) _check_opening_battle_capture_events(failures)
_check_sishui_gate_extended_pressure(failures) _check_sishui_gate_extended_pressure(failures)
@@ -327,6 +328,42 @@ func _check_turn_gated_objective_does_not_create_marker(failures: Array[String])
failures.append("turn-gated objective should not create map markers: %s" % str(marker_cells)) failures.append("turn-gated objective should not create map markers: %s" % str(marker_cells))
func _check_opening_battle_side_event_markers(failures: Array[String]) -> void:
var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
failures.append("opening side event marker smoke could not load scenario")
return
var cache_cell := Vector2i(4, 2)
var village_cell := Vector2i(6, 5)
var initial_cells := state.get_event_marker_cells()
if not initial_cells.has(cache_cell):
failures.append("opening battle should mark the northern woods supply cache: %s" % str(initial_cells))
if not initial_cells.has(village_cell):
failures.append("opening battle should mark village supply side event cells: %s" % str(initial_cells))
if state.get_event_marker_label(cache_cell) != "북숲 보급고":
failures.append("northern woods cache marker should expose Korean label: %s" % state.get_event_marker_label(cache_cell))
if state.get_event_marker_kind(cache_cell) != "supply":
failures.append("northern woods cache marker should be a supply marker")
if state.get_event_marker_label(village_cell) != "마을 보급":
failures.append("village supply marker should expose Korean label: %s" % state.get_event_marker_label(village_cell))
if state.get_event_marker_kind(village_cell) != "supply":
failures.append("village supply marker should be a supply marker")
var cao_cao: Dictionary = state.get_unit("cao_cao")
cao_cao["pos"] = cache_cell
state._run_events("unit_reaches_tile", BattleStateScript.TEAM_PLAYER, 1, cao_cao)
if state.get_event_marker_cells().has(cache_cell):
failures.append("northern woods supply marker should clear after pickup")
if int(state.get_inventory_snapshot().get("bean", 0)) < 1:
failures.append("northern woods supply marker event should grant a Bean")
cao_cao["pos"] = village_cell
state._run_events("unit_reaches_tile", BattleStateScript.TEAM_PLAYER, 2, cao_cao)
if state.get_event_marker_cells().has(village_cell):
failures.append("village supply marker should clear after securing the village")
func _check_opening_battle_requires_castle_capture(failures: Array[String]) -> void: func _check_opening_battle_requires_castle_capture(failures: Array[String]) -> void:
var state = BattleStateScript.new() var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"):

View File

@@ -3696,8 +3696,23 @@ func _check_terrain_and_unit_presentation(failures: Array[String]) -> void:
scene._update_cell_info() scene._update_cell_info()
if scene.cell_info_label == null or not scene.cell_info_label.text.contains("목표: 성채 장악") or not scene.cell_info_label.text.contains("회복 +8"): if scene.cell_info_label == null or not scene.cell_info_label.text.contains("목표: 성채 장악") or not scene.cell_info_label.text.contains("회복 +8"):
failures.append("castle objective cell info should expose capture label and healing: %s" % scene.cell_info_label.text) failures.append("castle objective cell info should expose capture label and healing: %s" % scene.cell_info_label.text)
scene.hover_cell = Vector2i(4, 2)
scene._update_cell_info()
if scene.cell_info_label == null or not scene.cell_info_label.text.contains("표식: 북숲 보급고"):
failures.append("side event marker cell info should expose the supply label: %s" % scene.cell_info_label.text)
scene.cell_info_label.free() scene.cell_info_label.free()
scene.cell_info_label = null scene.cell_info_label = null
var event_marker_rect: Rect2 = scene._event_marker_rect(scene._rect_for_cell(Vector2i(4, 2)))
if not scene._rect_for_cell(Vector2i(4, 2)).encloses(event_marker_rect):
failures.append("Side event marker should stay inside its tile: %s" % str(event_marker_rect))
if scene._event_marker_abbrev("북숲 보급고", "supply") != "":
failures.append("Northern woods marker should use a compact forest glyph")
if scene._event_marker_abbrev("마을 보급", "supply") != "":
failures.append("Village supply marker should use a compact village glyph")
if scene._event_marker_abbrev("", "supply") != "":
failures.append("Supply marker fallback should stay concise")
if scene._event_marker_abbrev("군자금", "gold") != "":
failures.append("Gold marker should use a compact gold glyph")
var background_fill: Color = scene._terrain_fill_color(Vector2i(0, 0), "G", true) var background_fill: Color = scene._terrain_fill_color(Vector2i(0, 0), "G", true)
var fallback_fill: Color = scene._terrain_fill_color(Vector2i(0, 0), "G", false) var fallback_fill: Color = scene._terrain_fill_color(Vector2i(0, 0), "G", false)
if background_fill.a >= 0.20: if background_fill.a >= 0.20: