611 lines
17 KiB
GDScript
611 lines
17 KiB
GDScript
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
|
|
var dealt: float = 0.0 ## ausgeteilter Schaden, für den Rückblick
|
|
var healed: 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 survivor_stars: int = 0 ## Sternsumme der Überlebenden, bestimmt den Schaden
|
|
var events: Array = [] ## kurze Textmeldungen für das Log
|
|
## Treffer, Heilungen und Zauber für Anzeige und Sound. Nur im Spielerkampf
|
|
## aufgezeichnet, Bot-Kämpfe laufen still durch.
|
|
var record_hits: bool = false
|
|
var hits: Array = []
|
|
|
|
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"] + s.get("crit", 0.0)
|
|
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
|
|
if record_hits:
|
|
hits.append({"kind": "attack", "pos": t.pos, "from": u.pos, "team": u.team,
|
|
"ranged": u.atk_range > 1, "element": u.avatar.primary_element(), "amount": 0, "t": time})
|
|
_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
|
|
# Schere-Stein-Papier: Hauptelement und Rolle des Angreifers gegen das Ziel.
|
|
var counter := 1.0
|
|
if src != null and not is_true:
|
|
counter = Data.counter_multiplier(src.avatar.primary_element(), src.avatar.role,
|
|
tgt.avatar.primary_element(), tgt.avatar.role)
|
|
amount *= counter
|
|
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
|
|
if src != null:
|
|
src.dealt += dmg
|
|
if record_hits and src != null:
|
|
var kind := "dmg"
|
|
if counter > 1.0:
|
|
kind = "counter"
|
|
elif amount > src.atk * 1.4 and not is_true:
|
|
kind = "crit"
|
|
_record(kind, tgt, dmg)
|
|
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 not u.alive:
|
|
return
|
|
var real := minf(u.max_hp, u.hp + amount) - u.hp
|
|
u.hp += real
|
|
u.healed += real
|
|
if record_hits and real >= 1.0:
|
|
_record("heal", u, real)
|
|
|
|
|
|
func _record(kind: String, u: CUnit, amount: float) -> void:
|
|
hits.append({"kind": kind, "pos": u.pos, "team": u.team, "amount": int(round(amount)), "t": time})
|
|
|
|
|
|
# --- 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)
|
|
if record_hits:
|
|
hits.append({"kind": "cast", "pos": u.pos, "team": u.team, "amount": 0, "t": time,
|
|
"role": u.avatar.role, "element": u.avatar.primary_element(),
|
|
"target": main_target.pos if main_target != null else u.pos})
|
|
|
|
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, u)
|
|
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)
|
|
|
|
if not elems.is_empty():
|
|
_apply_form(u, elems[0], main_target, hit_enemies)
|
|
for i in range(1, elems.size()):
|
|
var scale := 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)
|
|
|
|
|
|
## Verbündete in Heilreichweite, schwächste zuerst. Mindestens der Heiler selbst.
|
|
## Die Form des Hauptelements wirkt für jede Rolle gleich: um das Ziel (Gegner)
|
|
## oder um den Zaubernden (Verbündete).
|
|
func _apply_form(u: CUnit, element: int, main_target, hit_enemies: Array) -> void:
|
|
match element:
|
|
Data.Element.FEUER:
|
|
if main_target == null:
|
|
return
|
|
for n in Hex.neighbors(main_target.pos):
|
|
var o = _occ.get(n)
|
|
if o != null and o.alive and o.team != u.team and not hit_enemies.has(o):
|
|
_apply_damage(u, o, u.atk * 0.6, false)
|
|
hit_enemies.append(o)
|
|
for o in hit_enemies:
|
|
if o.alive:
|
|
o.burn_dps = maxf(o.burn_dps, u.atk * 0.6)
|
|
o.burn_timer = maxf(o.burn_timer, 3.0)
|
|
Data.Element.WASSER:
|
|
for o in units:
|
|
if o.alive and o.team == u.team and o != u and Hex.distance(o.pos, u.pos) <= 2:
|
|
o.mana = minf(o.max_mana - 1.0, o.mana + 20.0)
|
|
u.mana += 10.0
|
|
Data.Element.WIND:
|
|
for o in units:
|
|
if o.alive and o.team == u.team and Hex.distance(o.pos, u.pos) <= 2:
|
|
o.haste = maxf(o.haste, 0.25)
|
|
o.haste_timer = maxf(o.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.6, false)
|
|
hit_enemies.append(o)
|
|
chained += 1
|
|
Data.Element.ERDE:
|
|
if main_target == null:
|
|
return
|
|
main_target.stun_timer = maxf(main_target.stun_timer, 0.6)
|
|
for n in Hex.neighbors(main_target.pos):
|
|
var o = _occ.get(n)
|
|
if o != null and o.alive and o.team != u.team:
|
|
o.stun_timer = maxf(o.stun_timer, 0.6)
|
|
Data.Element.EIS:
|
|
if main_target == null:
|
|
return
|
|
for o in units:
|
|
if o.alive and o.team != u.team and Hex.distance(o.pos, main_target.pos) <= 1:
|
|
o.slow = maxf(o.slow, 0.30)
|
|
o.slow_timer = maxf(o.slow_timer, 3.0)
|
|
|
|
|
|
func _allies_by_missing_hp(team: int, healer: CUnit) -> Array:
|
|
var list := []
|
|
for o in units:
|
|
if o.alive and o.team == team and Hex.distance(o.pos, healer.pos) <= Data.HEAL_RANGE:
|
|
list.append(o)
|
|
if list.is_empty():
|
|
list.append(healer)
|
|
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
|
|
|
|
|
|
## Kurzer Rückblick für ein Team: Überlebende, stärkste Einheit, Gegnerspitze.
|
|
func recap(team: int) -> Dictionary:
|
|
var total := 0
|
|
var alive := 0
|
|
var best = null
|
|
var best_enemy = null
|
|
for u in units:
|
|
if u.team == team:
|
|
total += 1
|
|
if u.alive:
|
|
alive += 1
|
|
if best == null or u.dealt + u.healed > best.dealt + best.healed:
|
|
best = u
|
|
elif best_enemy == null or u.dealt > best_enemy.dealt:
|
|
best_enemy = u
|
|
return {
|
|
"total": total, "alive": alive,
|
|
"best_name": best.avatar.display_name() if best else "-",
|
|
"best_dealt": int(best.dealt) if best else 0,
|
|
"best_healed": int(best.healed) if best else 0,
|
|
"enemy_name": best_enemy.avatar.display_name() if best_enemy else "-",
|
|
"enemy_dealt": int(best_enemy.dealt) if best_enemy else 0,
|
|
"duration": time,
|
|
}
|
|
|
|
|
|
func _star_sum(team: int) -> int:
|
|
var n := 0
|
|
for u in units:
|
|
if u.alive and u.team == team:
|
|
n += u.avatar.star
|
|
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
|
|
survivor_stars = 0
|
|
else:
|
|
winner = 0 if a > b else 1
|
|
survivors = maxi(a, b)
|
|
survivor_stars = _star_sum(winner)
|
|
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
|
|
survivor_stars = 0
|
|
else:
|
|
winner = 0 if frac[0] > frac[1] else 1
|
|
survivors = alive_count(winner)
|
|
survivor_stars = _star_sum(winner)
|