Improve tactical attack flow
This commit is contained in:
@@ -27,6 +27,11 @@ const EXP_DEFEAT_BONUS := 30
|
||||
const EXP_LEVEL := 100
|
||||
const BASE_HIT_CHANCE := 90
|
||||
const MIN_HIT_CHANCE := 25
|
||||
const DIRECTIONAL_FRONT_ATTACK := "front"
|
||||
const DIRECTIONAL_SIDE_ATTACK := "side"
|
||||
const DIRECTIONAL_BACK_ATTACK := "back"
|
||||
const DIRECTIONAL_SIDE_DAMAGE_BONUS := 2
|
||||
const DIRECTIONAL_BACK_DAMAGE_BONUS := 5
|
||||
|
||||
const CARDINAL_DIRECTIONS := [
|
||||
Vector2i(1, 0),
|
||||
@@ -569,6 +574,7 @@ func _prepare_unit(source_unit: Dictionary) -> Dictionary:
|
||||
unit["agi"] = int(unit.get("agi", 0))
|
||||
unit["move"] = int(unit.get("move", 3))
|
||||
unit["move_type"] = str(unit.get("move_type", "foot"))
|
||||
unit["facing_x"] = _normalize_facing_x(unit.get("facing_x", _default_unit_facing_x(unit)))
|
||||
if not unit.has("skills") or typeof(unit["skills"]) != TYPE_ARRAY:
|
||||
unit["skills"] = []
|
||||
else:
|
||||
@@ -695,13 +701,16 @@ func try_move_selected(cell: Vector2i, defer_reach_events := false) -> bool:
|
||||
return false
|
||||
|
||||
var from_cell: Vector2i = unit["pos"]
|
||||
unit["_pending_move_facing_x"] = unit_facing_x(unit)
|
||||
unit["pos"] = cell
|
||||
unit["moved"] = true
|
||||
_face_unit_from_to(unit, from_cell, cell)
|
||||
unit_motion_requested.emit(str(unit.get("id", "")), from_cell, cell)
|
||||
_emit_log("%s, %s으로 진군." % [unit["name"], _format_cell(cell)])
|
||||
if not defer_reach_events:
|
||||
_run_events("unit_reaches_tile", str(unit.get("team", "")), turn_number, unit)
|
||||
_check_battle_status()
|
||||
unit.erase("_pending_move_facing_x")
|
||||
_notify_changed()
|
||||
return true
|
||||
|
||||
@@ -710,6 +719,7 @@ func commit_pending_move_events(unit_id: String) -> bool:
|
||||
var unit := get_unit(unit_id)
|
||||
if unit.is_empty() or battle_status != STATUS_ACTIVE:
|
||||
return false
|
||||
unit.erase("_pending_move_facing_x")
|
||||
_run_events("unit_reaches_tile", str(unit.get("team", "")), turn_number, unit)
|
||||
_check_battle_status()
|
||||
_notify_changed()
|
||||
@@ -730,6 +740,8 @@ func cancel_pending_move(unit_id: String, from_cell: Vector2i, to_cell: Vector2i
|
||||
|
||||
unit["pos"] = from_cell
|
||||
unit["moved"] = false
|
||||
unit["facing_x"] = _normalize_facing_x(unit.get("_pending_move_facing_x", unit.get("facing_x", _default_unit_facing_x(unit))))
|
||||
unit.erase("_pending_move_facing_x")
|
||||
unit_motion_requested.emit(unit_id, to_cell, from_cell)
|
||||
_emit_log("%s의 행군을 거두었다." % unit["name"])
|
||||
_notify_changed()
|
||||
@@ -793,6 +805,7 @@ func try_cast_selected_skill(skill_id: String, target_cell: Vector2i) -> bool:
|
||||
if skill_kind == "support" and not _skill_has_support_effects(skill):
|
||||
return false
|
||||
|
||||
_face_unit_toward_cell(caster, target_cell)
|
||||
_emit_skill_motion(caster, target_cell, skill_kind)
|
||||
caster["mp"] = max(0, int(caster.get("mp", 0)) - int(skill.get("mp_cost", 0)))
|
||||
if skill_kind == "heal":
|
||||
@@ -847,6 +860,7 @@ func try_use_selected_item_on_cell(item_id: String, target_cell: Vector2i) -> bo
|
||||
if not applied:
|
||||
return false
|
||||
|
||||
_face_unit_toward_cell(user, target_cell)
|
||||
battle_inventory[item_id] = max(0, int(battle_inventory.get(item_id, 0)) - 1)
|
||||
user["acted"] = true
|
||||
user["moved"] = true
|
||||
@@ -868,21 +882,32 @@ func get_damage_preview(attacker_id: String, target_id: String) -> Dictionary:
|
||||
return {}
|
||||
|
||||
var attack_locked := _unit_has_action_lock(attacker, "attack")
|
||||
var effective_bonus := 0 if attack_locked else _weapon_effectiveness_bonus(attacker, target)
|
||||
var damage := 0 if attack_locked else calculate_damage(attacker, target)
|
||||
var preview_attacker := attacker.duplicate(true)
|
||||
var preview_target := target.duplicate(true)
|
||||
if not attack_locked:
|
||||
_face_unit_toward_cell(preview_attacker, preview_target["pos"])
|
||||
var effective_bonus := 0 if attack_locked else _weapon_effectiveness_bonus(preview_attacker, preview_target)
|
||||
var directional_info := {} if attack_locked else get_directional_attack_info(preview_attacker, preview_target)
|
||||
var directional_bonus := int(directional_info.get("bonus", 0))
|
||||
var damage := 0 if attack_locked else calculate_damage(preview_attacker, preview_target)
|
||||
var target_hp_after: int = maxi(0, int(target["hp"]) - damage)
|
||||
var counter_damage := 0
|
||||
var attacker_hp_after := int(attacker["hp"])
|
||||
var hit_chance := 0 if attack_locked else calculate_hit_chance(attacker, target)
|
||||
var hit_chance := 0 if attack_locked else calculate_hit_chance(preview_attacker, preview_target)
|
||||
var counter_hit_chance := 0
|
||||
var counter_effective_bonus := 0
|
||||
var counter_directional_info := {}
|
||||
var counter_directional_bonus := 0
|
||||
var counter_in_range := false
|
||||
if not attack_locked and (target_hp_after > 0 or hit_chance < 100):
|
||||
counter_in_range = _is_in_attack_range(target, attacker["pos"])
|
||||
counter_in_range = _is_in_attack_range(preview_target, preview_attacker["pos"])
|
||||
if counter_in_range:
|
||||
counter_effective_bonus = _weapon_effectiveness_bonus(target, attacker)
|
||||
counter_damage = calculate_damage(target, attacker)
|
||||
counter_hit_chance = calculate_hit_chance(target, attacker)
|
||||
_face_unit_toward_cell(preview_target, preview_attacker["pos"])
|
||||
counter_effective_bonus = _weapon_effectiveness_bonus(preview_target, preview_attacker)
|
||||
counter_directional_info = get_directional_attack_info(preview_target, preview_attacker)
|
||||
counter_directional_bonus = int(counter_directional_info.get("bonus", 0))
|
||||
counter_damage = calculate_damage(preview_target, preview_attacker)
|
||||
counter_hit_chance = calculate_hit_chance(preview_target, preview_attacker)
|
||||
attacker_hp_after = maxi(0, int(attacker["hp"]) - counter_damage)
|
||||
return {
|
||||
"attacker_id": attacker_id,
|
||||
@@ -890,6 +915,9 @@ func get_damage_preview(attacker_id: String, target_id: String) -> Dictionary:
|
||||
"damage": damage,
|
||||
"effective_bonus": effective_bonus,
|
||||
"effective_target_type": _weapon_effectiveness_target_type(target) if effective_bonus > 0 else "",
|
||||
"directional_attack": str(directional_info.get("kind", DIRECTIONAL_FRONT_ATTACK)),
|
||||
"directional_bonus": directional_bonus,
|
||||
"directional_label": str(directional_info.get("label", _directional_attack_label(DIRECTIONAL_FRONT_ATTACK))),
|
||||
"hit_chance": hit_chance,
|
||||
"target_hp_after": target_hp_after,
|
||||
"would_defeat": int(target["hp"]) - damage <= 0,
|
||||
@@ -899,6 +927,9 @@ func get_damage_preview(attacker_id: String, target_id: String) -> Dictionary:
|
||||
"counter_damage": counter_damage,
|
||||
"counter_effective_bonus": counter_effective_bonus,
|
||||
"counter_effective_target_type": _weapon_effectiveness_target_type(attacker) if counter_effective_bonus > 0 else "",
|
||||
"counter_directional_attack": str(counter_directional_info.get("kind", DIRECTIONAL_FRONT_ATTACK)),
|
||||
"counter_directional_bonus": counter_directional_bonus,
|
||||
"counter_directional_label": str(counter_directional_info.get("label", _directional_attack_label(DIRECTIONAL_FRONT_ATTACK))),
|
||||
"counter_hit_chance": counter_hit_chance,
|
||||
"attacker_hp_after": attacker_hp_after,
|
||||
"counter_would_defeat": attacker_hp_after <= 0
|
||||
@@ -997,7 +1028,87 @@ func get_item_preview(user_id: String, item_id: String, target_cell: Vector2i) -
|
||||
func calculate_damage(attacker: Dictionary, target: Dictionary) -> int:
|
||||
var terrain_defense := get_terrain_defense(target["pos"])
|
||||
var effective_bonus := _weapon_effectiveness_bonus(attacker, target)
|
||||
return max(1, _effective_stat(attacker, "atk") + effective_bonus - _effective_stat(target, "def") - terrain_defense)
|
||||
var directional_bonus := _directional_damage_bonus(attacker, target)
|
||||
return max(1, _effective_stat(attacker, "atk") + effective_bonus + directional_bonus - _effective_stat(target, "def") - terrain_defense)
|
||||
|
||||
|
||||
func get_directional_attack_info(attacker: Dictionary, target: Dictionary) -> Dictionary:
|
||||
var attack_kind := _directional_attack_kind(attacker, target)
|
||||
return {
|
||||
"kind": attack_kind,
|
||||
"bonus": _directional_damage_bonus_for_kind(attack_kind),
|
||||
"label": _directional_attack_label(attack_kind)
|
||||
}
|
||||
|
||||
|
||||
func unit_facing_x(unit: Dictionary) -> int:
|
||||
return _normalize_facing_x(unit.get("facing_x", _default_unit_facing_x(unit)))
|
||||
|
||||
|
||||
func _default_unit_facing_x(unit: Dictionary) -> int:
|
||||
return -1 if str(unit.get("team", "")) == TEAM_ENEMY else 1
|
||||
|
||||
|
||||
func _normalize_facing_x(value) -> int:
|
||||
if typeof(value) == TYPE_VECTOR2I:
|
||||
var vector_i: Vector2i = value
|
||||
return -1 if vector_i.x < 0 else 1
|
||||
if typeof(value) == TYPE_VECTOR2:
|
||||
var vector: Vector2 = value
|
||||
return -1 if vector.x < 0.0 else 1
|
||||
if typeof(value) == TYPE_STRING:
|
||||
var text := str(value).strip_edges().to_lower()
|
||||
if text == "left" or text == "west" or text == "w" or text == "-1":
|
||||
return -1
|
||||
if text == "right" or text == "east" or text == "e" or text == "1":
|
||||
return 1
|
||||
return -1 if int(value) < 0 else 1
|
||||
|
||||
|
||||
func _face_unit_from_to(unit: Dictionary, from_cell: Vector2i, to_cell: Vector2i) -> void:
|
||||
if unit.is_empty() or from_cell.x == to_cell.x:
|
||||
return
|
||||
unit["facing_x"] = -1 if to_cell.x < from_cell.x else 1
|
||||
|
||||
|
||||
func _face_unit_toward_cell(unit: Dictionary, target_cell: Vector2i) -> void:
|
||||
if unit.is_empty() or not unit.has("pos"):
|
||||
return
|
||||
_face_unit_from_to(unit, unit.get("pos", target_cell), target_cell)
|
||||
|
||||
|
||||
func _directional_attack_kind(attacker: Dictionary, target: Dictionary) -> String:
|
||||
if attacker.is_empty() or target.is_empty():
|
||||
return DIRECTIONAL_FRONT_ATTACK
|
||||
var attacker_cell: Vector2i = attacker.get("pos", Vector2i.ZERO)
|
||||
var target_cell: Vector2i = target.get("pos", Vector2i.ZERO)
|
||||
var delta_x := attacker_cell.x - target_cell.x
|
||||
if delta_x == 0:
|
||||
return DIRECTIONAL_SIDE_ATTACK
|
||||
var approach_x := -1 if delta_x < 0 else 1
|
||||
if approach_x == unit_facing_x(target):
|
||||
return DIRECTIONAL_FRONT_ATTACK
|
||||
return DIRECTIONAL_BACK_ATTACK
|
||||
|
||||
|
||||
func _directional_damage_bonus(attacker: Dictionary, target: Dictionary) -> int:
|
||||
return _directional_damage_bonus_for_kind(_directional_attack_kind(attacker, target))
|
||||
|
||||
|
||||
func _directional_damage_bonus_for_kind(attack_kind: String) -> int:
|
||||
if attack_kind == DIRECTIONAL_BACK_ATTACK:
|
||||
return DIRECTIONAL_BACK_DAMAGE_BONUS
|
||||
if attack_kind == DIRECTIONAL_SIDE_ATTACK:
|
||||
return DIRECTIONAL_SIDE_DAMAGE_BONUS
|
||||
return 0
|
||||
|
||||
|
||||
func _directional_attack_label(attack_kind: String) -> String:
|
||||
if attack_kind == DIRECTIONAL_BACK_ATTACK:
|
||||
return "후방"
|
||||
if attack_kind == DIRECTIONAL_SIDE_ATTACK:
|
||||
return "측면"
|
||||
return "정면"
|
||||
|
||||
|
||||
func calculate_hit_chance(attacker: Dictionary, target: Dictionary) -> int:
|
||||
@@ -1553,6 +1664,7 @@ func get_physical_threat_previews(cell: Vector2i, source_team := TEAM_ENEMY) ->
|
||||
var damage := calculate_damage(unit, target)
|
||||
var hit_chance := calculate_hit_chance(unit, target)
|
||||
var effective_bonus := _weapon_effectiveness_bonus(unit, target)
|
||||
var directional_info := get_directional_attack_info(unit, target)
|
||||
result.append({
|
||||
"source_id": str(unit.get("id", "")),
|
||||
"source_name": str(unit.get("name", unit.get("id", "Unit"))),
|
||||
@@ -1561,7 +1673,10 @@ func get_physical_threat_previews(cell: Vector2i, source_team := TEAM_ENEMY) ->
|
||||
"target_hp_after": max(0, int(target.get("hp", 0)) - damage),
|
||||
"would_defeat": int(target.get("hp", 0)) - damage <= 0,
|
||||
"effective_bonus": effective_bonus,
|
||||
"effective_target_type": _weapon_effectiveness_target_type(target) if effective_bonus > 0 else ""
|
||||
"effective_target_type": _weapon_effectiveness_target_type(target) if effective_bonus > 0 else "",
|
||||
"directional_attack": str(directional_info.get("kind", DIRECTIONAL_FRONT_ATTACK)),
|
||||
"directional_bonus": int(directional_info.get("bonus", 0)),
|
||||
"directional_label": str(directional_info.get("label", _directional_attack_label(DIRECTIONAL_FRONT_ATTACK)))
|
||||
})
|
||||
result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
if bool(a.get("would_defeat", false)) != bool(b.get("would_defeat", false)):
|
||||
@@ -2958,6 +3073,7 @@ func _enemy_take_action(enemy: Dictionary) -> void:
|
||||
var from_cell: Vector2i = enemy["pos"]
|
||||
enemy["pos"] = best_cell
|
||||
enemy["moved"] = true
|
||||
_face_unit_from_to(enemy, from_cell, best_cell)
|
||||
unit_motion_requested.emit(str(enemy.get("id", "")), from_cell, best_cell)
|
||||
_emit_log("%s, %s으로 압박." % [enemy["name"], _format_cell(best_cell)])
|
||||
_run_events("unit_reaches_tile", str(enemy.get("team", "")), turn_number, enemy)
|
||||
@@ -2985,7 +3101,12 @@ func _best_ai_physical_attack_action(attacker: Dictionary, origins: Array[Vector
|
||||
for target in get_living_units(target_team):
|
||||
if not _attack_cells_from(attacker, origin).has(target["pos"]):
|
||||
continue
|
||||
var damage := calculate_damage(attacker, target)
|
||||
var scoring_attacker := attacker
|
||||
if attacker.get("pos", Vector2i(-1, -1)) != origin:
|
||||
scoring_attacker = attacker.duplicate(true)
|
||||
scoring_attacker["pos"] = origin
|
||||
_face_unit_from_to(scoring_attacker, attacker.get("pos", origin), origin)
|
||||
var damage := calculate_damage(scoring_attacker, target)
|
||||
var score := _physical_attack_ai_score(attacker, target, damage)
|
||||
score -= _manhattan(attacker["pos"], origin) * 2
|
||||
if score > best_score:
|
||||
@@ -3346,6 +3467,7 @@ func _resolve_combat(attacker: Dictionary, target: Dictionary) -> void:
|
||||
|
||||
|
||||
func _resolve_attack(attacker: Dictionary, target: Dictionary, is_counter := false) -> Dictionary:
|
||||
_face_unit_toward_cell(attacker, target.get("pos", attacker.get("pos", Vector2i.ZERO)))
|
||||
var hit_chance := calculate_hit_chance(attacker, target)
|
||||
var verb := "반격" if is_counter else "공격"
|
||||
unit_action_motion_requested.emit(
|
||||
@@ -3358,6 +3480,7 @@ func _resolve_attack(attacker: Dictionary, target: Dictionary, is_counter := fal
|
||||
_emit_log("%s %s %s, 헛쳤다. 명중 %d%%." % [attacker["name"], verb, target["name"], hit_chance])
|
||||
_emit_combat_feedback(target, "헛침", "miss")
|
||||
return {"hit": false, "defeated": false}
|
||||
var directional_info := get_directional_attack_info(attacker, target)
|
||||
var damage := calculate_damage(attacker, target)
|
||||
target["hp"] = max(0, int(target["hp"]) - damage)
|
||||
var effective_bonus := _weapon_effectiveness_bonus(attacker, target)
|
||||
@@ -3365,6 +3488,9 @@ func _resolve_attack(attacker: Dictionary, target: Dictionary, is_counter := fal
|
||||
if effective_bonus > 0:
|
||||
effective_text = " %s 특효 +%d." % [_format_move_type(_weapon_effectiveness_target_type(target)), effective_bonus]
|
||||
_emit_log("%s %s %s, %d 피해. 명중 %d%%.%s" % [attacker["name"], verb, target["name"], damage, hit_chance, effective_text])
|
||||
var directional_bonus := int(directional_info.get("bonus", 0))
|
||||
if directional_bonus > 0:
|
||||
_emit_log("%s %s +%d." % [target["name"], str(directional_info.get("label", "")), directional_bonus])
|
||||
_emit_combat_feedback(target, "-%d" % damage, "damage")
|
||||
|
||||
if int(target["hp"]) <= 0:
|
||||
|
||||
Reference in New Issue
Block a user