Add enemy threat overlay
This commit is contained in:
@@ -1223,6 +1223,28 @@ func get_item_target_cells(unit_id: String, item_id: String) -> Array[Vector2i]:
|
||||
return _item_target_cells_from(unit, item)
|
||||
|
||||
|
||||
func get_threat_cells(source_team := TEAM_ENEMY) -> Array[Vector2i]:
|
||||
var result: Array[Vector2i] = []
|
||||
for unit in get_living_units(source_team):
|
||||
for cell in _unit_threat_cells(unit):
|
||||
if not result.has(cell):
|
||||
result.append(cell)
|
||||
return result
|
||||
|
||||
|
||||
func get_threatening_unit_names(cell: Vector2i, source_team := TEAM_ENEMY) -> Array[String]:
|
||||
var result: Array[String] = []
|
||||
if not is_inside(cell):
|
||||
return result
|
||||
for unit in get_living_units(source_team):
|
||||
if not _unit_threat_cells(unit).has(cell):
|
||||
continue
|
||||
var unit_name := str(unit.get("name", unit.get("id", "Unit")))
|
||||
if not result.has(unit_name):
|
||||
result.append(unit_name)
|
||||
return result
|
||||
|
||||
|
||||
func get_objective_cells() -> Array[Vector2i]:
|
||||
var result: Array[Vector2i] = []
|
||||
_collect_objective_cells(battle_conditions.get("victory", {}), result)
|
||||
@@ -1685,6 +1707,93 @@ func _movement_range_for_unit(unit: Dictionary) -> Array[Vector2i]:
|
||||
return result
|
||||
|
||||
|
||||
func _unit_threat_cells(unit: Dictionary) -> Array[Vector2i]:
|
||||
var result: Array[Vector2i] = []
|
||||
if unit.is_empty() or not unit.get("alive", false) or not unit.get("deployed", true):
|
||||
return result
|
||||
if not bool(unit.get("controllable", true)):
|
||||
return result
|
||||
|
||||
for cell in _unit_physical_threat_cells(unit):
|
||||
if not result.has(cell):
|
||||
result.append(cell)
|
||||
for cell in _unit_skill_threat_cells(unit):
|
||||
if not result.has(cell):
|
||||
result.append(cell)
|
||||
return result
|
||||
|
||||
|
||||
func _unit_threat_origins(unit: Dictionary) -> Array[Vector2i]:
|
||||
var result: Array[Vector2i] = []
|
||||
var origin: Vector2i = unit.get("pos", Vector2i(-1, -1))
|
||||
if not is_inside(origin):
|
||||
return result
|
||||
result.append(origin)
|
||||
for cell in _movement_range_for_unit(unit):
|
||||
if not result.has(cell):
|
||||
result.append(cell)
|
||||
return result
|
||||
|
||||
|
||||
func _unit_physical_threat_cells(unit: Dictionary) -> Array[Vector2i]:
|
||||
var result: Array[Vector2i] = []
|
||||
if _unit_has_action_lock(unit, "attack"):
|
||||
return result
|
||||
for origin in _unit_threat_origins(unit):
|
||||
for cell in _attack_cells_from(unit, origin):
|
||||
if not result.has(cell):
|
||||
result.append(cell)
|
||||
return result
|
||||
|
||||
|
||||
func _unit_skill_threat_cells(unit: Dictionary) -> Array[Vector2i]:
|
||||
var result: Array[Vector2i] = []
|
||||
if _unit_has_action_lock(unit, "skill"):
|
||||
return result
|
||||
if typeof(unit.get("skills", [])) != TYPE_ARRAY:
|
||||
return result
|
||||
for skill_id_value in unit.get("skills", []):
|
||||
var skill_id := str(skill_id_value)
|
||||
var skill := get_skill_def(skill_id)
|
||||
if skill.is_empty():
|
||||
continue
|
||||
if int(unit.get("mp", 0)) < int(skill.get("mp_cost", 0)):
|
||||
continue
|
||||
if not _skill_threatens_opponents(skill):
|
||||
continue
|
||||
for origin in _unit_threat_origins(unit):
|
||||
for cell in _skill_cells_from_origin(unit, skill, origin):
|
||||
if not result.has(cell):
|
||||
result.append(cell)
|
||||
return result
|
||||
|
||||
|
||||
func _skill_threatens_opponents(skill: Dictionary) -> bool:
|
||||
var target_rule := str(skill.get("target", "enemy"))
|
||||
if target_rule != "enemy" and target_rule != "any":
|
||||
return false
|
||||
var skill_kind := str(skill.get("kind", "damage"))
|
||||
if skill_kind == "damage":
|
||||
return true
|
||||
if skill_kind == "support":
|
||||
return _skill_has_harmful_support_effects(skill)
|
||||
return false
|
||||
|
||||
|
||||
func _skill_has_harmful_support_effects(skill: Dictionary) -> bool:
|
||||
for effect in skill.get("effects", []):
|
||||
if typeof(effect) != TYPE_DICTIONARY:
|
||||
continue
|
||||
var effect_type := str(effect.get("type", ""))
|
||||
if effect_type == "damage_over_time" and int(effect.get("amount", 0)) > 0:
|
||||
return true
|
||||
if effect_type == "action_lock" and not str(effect.get("action", "")).strip_edges().is_empty():
|
||||
return true
|
||||
if effect_type == "stat_bonus" and int(effect.get("amount", 0)) < 0:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _attack_cells_from(unit: Dictionary, origin: Vector2i) -> Array[Vector2i]:
|
||||
var result: Array[Vector2i] = []
|
||||
var attack_range := int(unit.get("range", 1))
|
||||
@@ -1707,11 +1816,14 @@ func _is_in_attack_range(attacker: Dictionary, target_cell: Vector2i) -> bool:
|
||||
|
||||
|
||||
func _skill_cells_from(unit: Dictionary, skill: Dictionary) -> Array[Vector2i]:
|
||||
return _skill_cells_from_origin(unit, skill, unit["pos"])
|
||||
|
||||
|
||||
func _skill_cells_from_origin(_unit: Dictionary, skill: Dictionary, origin: Vector2i) -> Array[Vector2i]:
|
||||
var result: Array[Vector2i] = []
|
||||
var range_pair := _skill_range_pair(skill)
|
||||
var max_range := int(range_pair["max"])
|
||||
var min_range := int(range_pair["min"])
|
||||
var origin: Vector2i = unit["pos"]
|
||||
for y in range(origin.y - max_range, origin.y + max_range + 1):
|
||||
for x in range(origin.x - max_range, origin.x + max_range + 1):
|
||||
var cell := Vector2i(x, y)
|
||||
|
||||
@@ -13,6 +13,8 @@ const SKILL_OVERLAY_COLOR := Color(0.56, 0.28, 0.95, 0.28)
|
||||
const ITEM_OVERLAY_COLOR := Color(0.24, 0.74, 0.34, 0.30)
|
||||
const FORMATION_OVERLAY_COLOR := Color(0.95, 0.76, 0.25, 0.34)
|
||||
const OBJECTIVE_OVERLAY_COLOR := Color(1.0, 0.78, 0.18, 0.30)
|
||||
const THREAT_OVERLAY_COLOR := Color(1.0, 0.12, 0.10, 0.18)
|
||||
const THREAT_BORDER_COLOR := Color(1.0, 0.16, 0.12, 0.44)
|
||||
const BGM_MENU_PATH := "res://audio/bgm/menu_theme_placeholder.wav"
|
||||
const BGM_BATTLE_PATH := "res://audio/bgm/battle_loop_placeholder.wav"
|
||||
const SFX_BOW_RELEASE_PATH := "res://audio/sfx/bow_release_01.wav"
|
||||
@@ -48,8 +50,10 @@ var move_cells: Array[Vector2i] = []
|
||||
var attack_cells: Array[Vector2i] = []
|
||||
var skill_cells: Array[Vector2i] = []
|
||||
var item_cells: Array[Vector2i] = []
|
||||
var threat_cells: Array[Vector2i] = []
|
||||
var selected_skill_id := ""
|
||||
var selected_item_id := ""
|
||||
var show_threat_overlay := false
|
||||
var active_scenario_id := ""
|
||||
var battle_started := false
|
||||
var battle_result_applied := false
|
||||
@@ -124,6 +128,7 @@ var item_list: VBoxContainer
|
||||
var equip_button: Button
|
||||
var equip_menu: VBoxContainer
|
||||
var equip_list: VBoxContainer
|
||||
var threat_button: Button
|
||||
var restart_button: Button
|
||||
var new_campaign_button: Button
|
||||
var log_box: RichTextLabel
|
||||
@@ -280,6 +285,12 @@ func _create_hud() -> void:
|
||||
equip_button.pressed.connect(_on_equip_pressed)
|
||||
button_row.add_child(equip_button)
|
||||
|
||||
threat_button = Button.new()
|
||||
threat_button.text = "Threat"
|
||||
threat_button.toggle_mode = true
|
||||
threat_button.pressed.connect(_on_threat_pressed)
|
||||
button_row.add_child(threat_button)
|
||||
|
||||
end_turn_button = Button.new()
|
||||
end_turn_button.text = "End Turn"
|
||||
end_turn_button.pressed.connect(_on_end_turn_pressed)
|
||||
@@ -875,6 +886,8 @@ func _handle_key(event: InputEventKey) -> void:
|
||||
_on_item_pressed()
|
||||
elif event.keycode == KEY_E and not _is_input_locked():
|
||||
_on_equip_pressed()
|
||||
elif event.keycode == KEY_Y and not _is_prep_menu_visible():
|
||||
_on_threat_pressed()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
@@ -932,6 +945,11 @@ func _draw_overlays() -> void:
|
||||
draw_rect(objective_rect, OBJECTIVE_OVERLAY_COLOR)
|
||||
draw_rect(objective_rect.grow(-5.0), Color(1.0, 0.88, 0.30, 0.82), false, 2.0)
|
||||
|
||||
for cell in threat_cells:
|
||||
var threat_rect := _rect_for_cell(cell)
|
||||
draw_rect(threat_rect, THREAT_OVERLAY_COLOR)
|
||||
draw_rect(threat_rect.grow(-6.0), THREAT_BORDER_COLOR, false, 1.5)
|
||||
|
||||
for cell in move_cells:
|
||||
draw_rect(_rect_for_cell(cell), Color(0.16, 0.55, 0.95, 0.28))
|
||||
|
||||
@@ -1328,6 +1346,9 @@ func _refresh_ranges() -> void:
|
||||
attack_cells.clear()
|
||||
skill_cells.clear()
|
||||
item_cells.clear()
|
||||
threat_cells.clear()
|
||||
if show_threat_overlay and battle_started and state.battle_status == BattleState.STATUS_ACTIVE:
|
||||
threat_cells = state.get_threat_cells(BattleState.TEAM_ENEMY)
|
||||
var selected := state.get_selected_unit()
|
||||
if selected.is_empty():
|
||||
selected_skill_id = ""
|
||||
@@ -1403,6 +1424,9 @@ func _update_hud() -> void:
|
||||
|
||||
end_turn_button.disabled = not state.can_player_act() or _is_input_locked()
|
||||
restart_button.disabled = campaign_complete_screen
|
||||
if threat_button != null:
|
||||
threat_button.button_pressed = show_threat_overlay
|
||||
threat_button.disabled = not battle_started or state.battle_status != BattleState.STATUS_ACTIVE or campaign_complete_screen
|
||||
|
||||
|
||||
func _update_cell_info() -> void:
|
||||
@@ -1422,6 +1446,10 @@ func _update_cell_info() -> void:
|
||||
]
|
||||
if state.get_objective_cells().has(cell):
|
||||
text += "\nObjective marker"
|
||||
if show_threat_overlay:
|
||||
var threat_names := state.get_threatening_unit_names(cell, BattleState.TEAM_ENEMY)
|
||||
if not threat_names.is_empty():
|
||||
text += "\nThreatened by %s" % _compact_name_list(threat_names, 3)
|
||||
|
||||
var unit: Dictionary = summary["unit"]
|
||||
if not unit.is_empty():
|
||||
@@ -1448,6 +1476,16 @@ func _update_cell_info() -> void:
|
||||
cell_info_label.text = text
|
||||
|
||||
|
||||
func _compact_name_list(names: Array[String], limit: int) -> String:
|
||||
var visible_names: Array[String] = []
|
||||
var safe_limit: int = maxi(1, limit)
|
||||
for index in range(mini(names.size(), safe_limit)):
|
||||
visible_names.append(names[index])
|
||||
if names.size() > safe_limit:
|
||||
visible_names.append("+%d" % (names.size() - safe_limit))
|
||||
return _join_strings(visible_names, ", ")
|
||||
|
||||
|
||||
func _update_forecast() -> void:
|
||||
var selected := state.get_selected_unit()
|
||||
if selected.is_empty() or not state.is_inside(hover_cell):
|
||||
@@ -1963,6 +2001,17 @@ func _on_equip_pressed() -> void:
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _on_threat_pressed() -> void:
|
||||
if not battle_started or state.battle_status != BattleState.STATUS_ACTIVE or campaign_complete_screen:
|
||||
if threat_button != null:
|
||||
threat_button.button_pressed = show_threat_overlay
|
||||
return
|
||||
show_threat_overlay = not show_threat_overlay
|
||||
_refresh_ranges()
|
||||
_update_hud()
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _on_restart_pressed() -> void:
|
||||
if campaign_complete_screen:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user