first commit
This commit is contained in:
477
scripts/core/Combat.gd
Normal file
477
scripts/core/Combat.gd
Normal file
@@ -0,0 +1,477 @@
|
||||
class_name Combat
|
||||
extends RefCounted
|
||||
|
||||
## Auto-Kampf zweier Aufstellungen auf einem 7x8-Hexfeld.
|
||||
## Rein logisch und schrittweise: Bot-Kämpfe laufen sofort durch,
|
||||
## der Kampf des Spielers wird pro Frame gesteppt.
|
||||
|
||||
const ARENA_COLS := 7
|
||||
const ARENA_ROWS := 8
|
||||
const MOVE_TIME := 0.32
|
||||
const TIME_LIMIT := 35.0
|
||||
const CRIT_MULT := 1.5
|
||||
const MANA_PER_ATTACK := 10.0
|
||||
const MANA_PER_HIT := 5.0
|
||||
|
||||
|
||||
class CUnit extends RefCounted:
|
||||
var avatar: Avatar
|
||||
var team: int
|
||||
var pos: Vector2i
|
||||
var hp: float
|
||||
var max_hp: float
|
||||
var atk: float
|
||||
var armor: float
|
||||
var aspd: float
|
||||
var atk_range: int
|
||||
var mana: float
|
||||
var max_mana: float
|
||||
var mana_mult: float = 1.0
|
||||
var crit: float = 0.0
|
||||
var attack_timer: float = 0.0
|
||||
var move_timer: float = 0.0
|
||||
var alive: bool = true
|
||||
var shield: float = 0.0
|
||||
var shield_timer: float = 0.0
|
||||
var burn_dps: float = 0.0
|
||||
var burn_timer: float = 0.0
|
||||
var slow: float = 0.0
|
||||
var slow_timer: float = 0.0
|
||||
var haste: float = 0.0
|
||||
var haste_timer: float = 0.0
|
||||
var stun_timer: float = 0.0
|
||||
var reflect: float = 0.0
|
||||
var reflect_timer: float = 0.0
|
||||
var taunt_by = null
|
||||
var taunt_timer: float = 0.0
|
||||
var target = null
|
||||
var cast_flash: float = 0.0
|
||||
var hit_flash: float = 0.0
|
||||
|
||||
func effective_aspd() -> float:
|
||||
return maxf(0.1, aspd * (1.0 + haste) * (1.0 - slow))
|
||||
|
||||
|
||||
var units: Array = []
|
||||
var time: float = 0.0
|
||||
var finished: bool = false
|
||||
var winner: int = -1 ## 0 = Team A, 1 = Team B, -1 = unentschieden
|
||||
var survivors: int = 0
|
||||
var events: Array = [] ## kurze Textmeldungen für das Log
|
||||
|
||||
var _mods := [{}, {}]
|
||||
var _occ := {}
|
||||
var _rand := RandomNumberGenerator.new()
|
||||
|
||||
|
||||
func _init(a: PlayerState, b: PlayerState, seed_value: int = 0) -> void:
|
||||
_rand.seed = seed_value if seed_value != 0 else randi()
|
||||
_mods[0] = _build_mods(a)
|
||||
_mods[1] = _build_mods(b)
|
||||
_spawn(a, 0)
|
||||
_spawn(b, 1)
|
||||
_apply_start_effects()
|
||||
|
||||
|
||||
# --- Aufbau ----------------------------------------------------------------
|
||||
|
||||
func _build_mods(p: PlayerState) -> Dictionary:
|
||||
var counts := p.trait_counts()
|
||||
var m := {
|
||||
"atk": 1.0, "hp": 1.0, "aspd": 1.0, "mana": 1.0,
|
||||
"crit": 0.0, "ice": 0.0, "combos": {},
|
||||
}
|
||||
for e in counts:
|
||||
var tier := Data.trait_tier(e, counts[e])
|
||||
if tier <= 0:
|
||||
continue
|
||||
var v: float = Data.ELEMENT_TRAITS[e]["values"][tier - 1]
|
||||
match e:
|
||||
Data.Element.FEUER: m["atk"] += v
|
||||
Data.Element.WASSER: m["mana"] += v
|
||||
Data.Element.WIND: m["aspd"] += v
|
||||
Data.Element.BLITZ: m["crit"] += v
|
||||
Data.Element.ERDE: m["hp"] += v
|
||||
Data.Element.EIS: m["ice"] = maxf(m["ice"], v)
|
||||
for c in p.active_combos():
|
||||
m["combos"][c["key"]] = true
|
||||
return m
|
||||
|
||||
|
||||
func _spawn(p: PlayerState, team: int) -> void:
|
||||
var m: Dictionary = _mods[team]
|
||||
for hex in p.board:
|
||||
var a: Avatar = p.board[hex]
|
||||
var s := a.base_stats()
|
||||
var u := CUnit.new()
|
||||
u.avatar = a
|
||||
u.team = team
|
||||
u.pos = _arena_pos(hex, team)
|
||||
u.max_hp = s["hp"] * m["hp"]
|
||||
u.hp = u.max_hp
|
||||
u.atk = s["atk"] * m["atk"]
|
||||
u.armor = s["armor"]
|
||||
u.aspd = s["aspd"] * m["aspd"]
|
||||
u.atk_range = s["range"]
|
||||
u.max_mana = s["mana"]
|
||||
u.mana = 0.0
|
||||
u.mana_mult = m["mana"]
|
||||
u.crit = m["crit"]
|
||||
if _occ.has(u.pos):
|
||||
u.pos = _free_near(u.pos)
|
||||
_occ[u.pos] = u
|
||||
units.append(u)
|
||||
|
||||
|
||||
static func _arena_pos(hex: Vector2i, team: int) -> Vector2i:
|
||||
if team == 0:
|
||||
return Vector2i(hex.x, 4 + hex.y)
|
||||
return Vector2i(ARENA_COLS - 1 - hex.x, 3 - hex.y)
|
||||
|
||||
|
||||
func _free_near(p: Vector2i) -> Vector2i:
|
||||
for n in Hex.neighbors(p):
|
||||
if _in_arena(n) and not _occ.has(n):
|
||||
return n
|
||||
return p
|
||||
|
||||
|
||||
static func _in_arena(p: Vector2i) -> bool:
|
||||
return p.x >= 0 and p.x < ARENA_COLS and p.y >= 0 and p.y < ARENA_ROWS
|
||||
|
||||
|
||||
func _apply_start_effects() -> void:
|
||||
for team in 2:
|
||||
if _mods[team]["combos"].has("frost"):
|
||||
for u in units:
|
||||
if u.team != team:
|
||||
u.slow = maxf(u.slow, 0.15)
|
||||
u.slow_timer = 999.0
|
||||
|
||||
|
||||
# --- Simulation ------------------------------------------------------------
|
||||
|
||||
func step(dt: float) -> void:
|
||||
if finished:
|
||||
return
|
||||
time += dt
|
||||
for u in units:
|
||||
if u.alive:
|
||||
_tick_status(u, dt)
|
||||
for u in units:
|
||||
if u.alive and u.stun_timer <= 0.0:
|
||||
_act(u, dt)
|
||||
_check_end()
|
||||
|
||||
|
||||
func run_to_end(dt: float = 0.05) -> void:
|
||||
var guard := 0
|
||||
while not finished and guard < 5000:
|
||||
step(dt)
|
||||
guard += 1
|
||||
if not finished:
|
||||
_resolve_by_hp()
|
||||
|
||||
|
||||
func _tick_status(u: CUnit, dt: float) -> void:
|
||||
if u.shield_timer > 0.0:
|
||||
u.shield_timer -= dt
|
||||
if u.shield_timer <= 0.0:
|
||||
u.shield = 0.0
|
||||
if u.burn_timer > 0.0:
|
||||
u.burn_timer -= dt
|
||||
_apply_damage(null, u, u.burn_dps * dt, true)
|
||||
if u.burn_timer <= 0.0:
|
||||
u.burn_dps = 0.0
|
||||
if u.slow_timer > 0.0:
|
||||
u.slow_timer -= dt
|
||||
if u.slow_timer <= 0.0:
|
||||
u.slow = 0.0
|
||||
if u.haste_timer > 0.0:
|
||||
u.haste_timer -= dt
|
||||
if u.haste_timer <= 0.0:
|
||||
u.haste = 0.0
|
||||
if u.reflect_timer > 0.0:
|
||||
u.reflect_timer -= dt
|
||||
if u.reflect_timer <= 0.0:
|
||||
u.reflect = 0.0
|
||||
if u.stun_timer > 0.0:
|
||||
u.stun_timer -= dt
|
||||
if u.taunt_timer > 0.0:
|
||||
u.taunt_timer -= dt
|
||||
if u.taunt_timer <= 0.0:
|
||||
u.taunt_by = null
|
||||
u.cast_flash = maxf(0.0, u.cast_flash - dt)
|
||||
u.hit_flash = maxf(0.0, u.hit_flash - dt)
|
||||
|
||||
|
||||
func _act(u: CUnit, dt: float) -> void:
|
||||
var t = _pick_target(u)
|
||||
if t == null:
|
||||
return
|
||||
u.target = t
|
||||
var d := Hex.distance(u.pos, t.pos)
|
||||
if d <= u.atk_range:
|
||||
u.attack_timer -= dt * u.effective_aspd()
|
||||
if u.attack_timer <= 0.0:
|
||||
u.attack_timer += 1.0
|
||||
_basic_attack(u, t)
|
||||
else:
|
||||
u.move_timer -= dt
|
||||
if u.move_timer <= 0.0:
|
||||
u.move_timer = MOVE_TIME
|
||||
_step_towards(u, t)
|
||||
|
||||
|
||||
func _pick_target(u: CUnit):
|
||||
if u.taunt_by != null and u.taunt_by.alive and u.taunt_timer > 0.0:
|
||||
return u.taunt_by
|
||||
if u.target != null and u.target.alive:
|
||||
return u.target
|
||||
return _nearest_enemy(u)
|
||||
|
||||
|
||||
func _nearest_enemy(u: CUnit, exclude = null):
|
||||
var best = null
|
||||
var best_d := 999
|
||||
for o in units:
|
||||
if not o.alive or o.team == u.team or o == exclude:
|
||||
continue
|
||||
var d := Hex.distance(u.pos, o.pos)
|
||||
if d < best_d:
|
||||
best_d = d
|
||||
best = o
|
||||
return best
|
||||
|
||||
|
||||
func _step_towards(u: CUnit, t: CUnit) -> void:
|
||||
var best := u.pos
|
||||
var best_d := Hex.distance(u.pos, t.pos)
|
||||
for n in Hex.neighbors(u.pos):
|
||||
if not _in_arena(n) or _occ.has(n):
|
||||
continue
|
||||
var d := Hex.distance(n, t.pos)
|
||||
if d < best_d:
|
||||
best_d = d
|
||||
best = n
|
||||
if best != u.pos:
|
||||
_occ.erase(u.pos)
|
||||
u.pos = best
|
||||
_occ[u.pos] = u
|
||||
|
||||
|
||||
# --- Angriff und Schaden ---------------------------------------------------
|
||||
|
||||
func _basic_attack(u: CUnit, t: CUnit) -> void:
|
||||
var dmg := u.atk
|
||||
if u.crit > 0.0 and _rand.randf() < u.crit:
|
||||
dmg *= CRIT_MULT
|
||||
_apply_damage(u, t, dmg, false)
|
||||
var ice: float = _mods[u.team]["ice"]
|
||||
if ice > 0.0:
|
||||
t.slow = maxf(t.slow, ice)
|
||||
t.slow_timer = maxf(t.slow_timer, 2.0)
|
||||
if _mods[u.team]["combos"].has("sturmfront"):
|
||||
var second = _nearest_enemy(u, t)
|
||||
if second != null:
|
||||
_apply_damage(u, second, dmg * 0.5, false)
|
||||
u.mana += MANA_PER_ATTACK * u.mana_mult
|
||||
if u.mana >= u.max_mana:
|
||||
_cast(u)
|
||||
|
||||
|
||||
func _apply_damage(src, tgt: CUnit, amount: float, is_true: bool) -> void:
|
||||
if not tgt.alive or amount <= 0.0:
|
||||
return
|
||||
var dmg := amount if is_true else amount * (1.0 - tgt.armor)
|
||||
if tgt.shield > 0.0:
|
||||
var absorbed: float = minf(tgt.shield, dmg)
|
||||
tgt.shield -= absorbed
|
||||
dmg -= absorbed
|
||||
if dmg > 0.0:
|
||||
tgt.hp -= dmg
|
||||
tgt.hit_flash = 0.12
|
||||
tgt.mana += MANA_PER_HIT * tgt.mana_mult
|
||||
if tgt.mana >= tgt.max_mana and tgt.alive and tgt.hp > 0.0:
|
||||
_cast(tgt)
|
||||
if src != null and dmg > 0.0:
|
||||
if tgt.reflect > 0.0:
|
||||
src.hp -= dmg * tgt.reflect
|
||||
if src.hp <= 0.0:
|
||||
_kill(src)
|
||||
if _mods[tgt.team]["combos"].has("magma"):
|
||||
src.hp -= dmg * 0.20
|
||||
if src.hp <= 0.0:
|
||||
_kill(src)
|
||||
if _mods[src.team]["combos"].has("dampf"):
|
||||
src.hp = minf(src.max_hp, src.hp + dmg * 0.18)
|
||||
if tgt.hp <= 0.0:
|
||||
_kill(tgt)
|
||||
|
||||
|
||||
func _kill(u: CUnit) -> void:
|
||||
if not u.alive:
|
||||
return
|
||||
u.alive = false
|
||||
u.hp = 0.0
|
||||
_occ.erase(u.pos)
|
||||
events.append(u.avatar.display_name() + " fällt")
|
||||
|
||||
|
||||
func _heal(u: CUnit, amount: float) -> void:
|
||||
if u.alive:
|
||||
u.hp = minf(u.max_hp, u.hp + amount)
|
||||
|
||||
|
||||
# --- Fähigkeiten ----------------------------------------------------------
|
||||
|
||||
func _cast(u: CUnit) -> void:
|
||||
u.mana = 0.0
|
||||
u.cast_flash = 0.35
|
||||
var elems: Array = u.avatar.elements
|
||||
var heal_bonus := 0.0
|
||||
for i in elems.size():
|
||||
if elems[i] == Data.Element.WASSER:
|
||||
heal_bonus += 0.20 * (1.0 if i == 0 else 0.5)
|
||||
|
||||
var hit_enemies: Array = []
|
||||
var main_target = u.target if (u.target != null and u.target.alive) else _nearest_enemy(u)
|
||||
|
||||
match u.avatar.role:
|
||||
Data.Role.TANK:
|
||||
var sh := u.atk * (3.0 + 0.5 * float(u.avatar.star - 1)) * (1.0 + heal_bonus)
|
||||
u.shield = maxf(u.shield, sh)
|
||||
u.shield_timer = 4.0
|
||||
if u.avatar.star >= 2:
|
||||
var n := 0
|
||||
for o in units:
|
||||
if o.alive and o.team != u.team and Hex.distance(o.pos, u.pos) <= 2 and n < 3:
|
||||
o.taunt_by = u
|
||||
o.taunt_timer = 2.0
|
||||
o.target = u
|
||||
n += 1
|
||||
if u.avatar.star >= 3:
|
||||
u.reflect = 0.25
|
||||
u.reflect_timer = 4.0
|
||||
Data.Role.DPS:
|
||||
if main_target == null:
|
||||
return
|
||||
var base := u.atk * (3.0 + 0.3 * float(u.avatar.star - 1))
|
||||
_apply_damage(u, main_target, base, false)
|
||||
hit_enemies.append(main_target)
|
||||
if u.avatar.star == 2:
|
||||
for n in Hex.neighbors(main_target.pos):
|
||||
var o = _occ.get(n)
|
||||
if o != null and o.alive and o.team != u.team:
|
||||
_apply_damage(u, o, u.atk * 1.5, false)
|
||||
hit_enemies.append(o)
|
||||
elif u.avatar.star >= 3:
|
||||
for o in units:
|
||||
if o.alive and o.team != u.team and o != main_target \
|
||||
and Hex.distance(o.pos, main_target.pos) <= 2:
|
||||
_apply_damage(u, o, u.atk * 2.0, false)
|
||||
hit_enemies.append(o)
|
||||
Data.Role.HEILER:
|
||||
var amount := u.atk * (3.0 - 0.25 * float(u.avatar.star - 1)) * (1.0 + heal_bonus)
|
||||
var allies := _allies_by_missing_hp(u.team)
|
||||
var count := 1
|
||||
if u.avatar.star == 2:
|
||||
count = 3
|
||||
elif u.avatar.star >= 3:
|
||||
count = allies.size()
|
||||
for i in mini(count, allies.size()):
|
||||
_heal(allies[i], amount)
|
||||
if u.avatar.star >= 3:
|
||||
allies[i].shield = maxf(allies[i].shield, u.atk * 1.5 * (1.0 + heal_bonus))
|
||||
allies[i].shield_timer = 3.0
|
||||
|
||||
_apply_element_effects(u, main_target, hit_enemies)
|
||||
|
||||
|
||||
func _apply_element_effects(u: CUnit, main_target, hit_enemies: Array) -> void:
|
||||
var elems: Array = u.avatar.elements
|
||||
var combos: Dictionary = _mods[u.team]["combos"]
|
||||
if combos.has("feuersturm"):
|
||||
for o in hit_enemies:
|
||||
o.burn_dps = maxf(o.burn_dps, u.atk * 0.4)
|
||||
o.burn_timer = maxf(o.burn_timer, 3.0)
|
||||
if combos.has("magnetsturm") and main_target != null and main_target.alive:
|
||||
main_target.stun_timer = maxf(main_target.stun_timer, 0.6)
|
||||
|
||||
for i in elems.size():
|
||||
var scale := 1.0 if i == 0 else 0.5
|
||||
match elems[i]:
|
||||
Data.Element.FEUER:
|
||||
for o in hit_enemies:
|
||||
if o.alive:
|
||||
o.burn_dps = maxf(o.burn_dps, u.atk * 0.6 * scale)
|
||||
o.burn_timer = maxf(o.burn_timer, 3.0)
|
||||
Data.Element.WASSER:
|
||||
u.mana += 15.0 * scale
|
||||
Data.Element.WIND:
|
||||
u.haste = maxf(u.haste, 0.30 * scale)
|
||||
u.haste_timer = 3.0
|
||||
Data.Element.BLITZ:
|
||||
var chained := 0
|
||||
for o in units:
|
||||
if chained >= 2:
|
||||
break
|
||||
if o.alive and o.team != u.team and o != main_target:
|
||||
_apply_damage(u, o, u.atk * 0.8 * scale, false)
|
||||
chained += 1
|
||||
Data.Element.ERDE:
|
||||
if main_target != null and main_target.alive:
|
||||
main_target.stun_timer = maxf(main_target.stun_timer, 0.75 * scale)
|
||||
Data.Element.EIS:
|
||||
if main_target != null and main_target.alive:
|
||||
main_target.slow = maxf(main_target.slow, 0.30 * scale)
|
||||
main_target.slow_timer = maxf(main_target.slow_timer, 3.0)
|
||||
|
||||
|
||||
func _allies_by_missing_hp(team: int) -> Array:
|
||||
var list := []
|
||||
for o in units:
|
||||
if o.alive and o.team == team:
|
||||
list.append(o)
|
||||
list.sort_custom(func(a, b): return (a.hp / a.max_hp) < (b.hp / b.max_hp))
|
||||
return list
|
||||
|
||||
|
||||
# --- Ende ------------------------------------------------------------------
|
||||
|
||||
func alive_count(team: int) -> int:
|
||||
var n := 0
|
||||
for u in units:
|
||||
if u.alive and u.team == team:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
func _check_end() -> void:
|
||||
var a := alive_count(0)
|
||||
var b := alive_count(1)
|
||||
if a == 0 or b == 0:
|
||||
finished = true
|
||||
if a == b:
|
||||
winner = -1
|
||||
survivors = 0
|
||||
else:
|
||||
winner = 0 if a > b else 1
|
||||
survivors = maxi(a, b)
|
||||
return
|
||||
if time >= TIME_LIMIT:
|
||||
_resolve_by_hp()
|
||||
|
||||
|
||||
func _resolve_by_hp() -> void:
|
||||
finished = true
|
||||
var frac := [0.0, 0.0]
|
||||
for u in units:
|
||||
if u.alive:
|
||||
frac[u.team] += u.hp / u.max_hp
|
||||
if absf(frac[0] - frac[1]) < 0.25:
|
||||
winner = -1
|
||||
survivors = 0
|
||||
else:
|
||||
winner = 0 if frac[0] > frac[1] else 1
|
||||
survivors = alive_count(winner)
|
||||
Reference in New Issue
Block a user