update
This commit is contained in:
@@ -70,13 +70,16 @@ func base_stats() -> Dictionary:
|
||||
var b: Dictionary = Data.ROLE_STATS[role]
|
||||
var mult: float = Data.STAR_MULT[star - 1]
|
||||
var ebonus := 1.0 + Data.ELEMENT_STAT_BONUS * float(maxi(0, elements.size() - 1))
|
||||
var prof: Dictionary = Data.ELEMENT_PROFILE.get(primary_element(),
|
||||
{"hp": 1.0, "atk": 1.0, "armor": 0.0, "aspd": 1.0, "mana": 1.0, "crit": 0.0})
|
||||
return {
|
||||
"hp": b["hp"] * mult * ebonus,
|
||||
"atk": b["atk"] * mult * ebonus,
|
||||
"armor": b["armor"],
|
||||
"aspd": b["aspd"],
|
||||
"hp": b["hp"] * mult * ebonus * prof["hp"],
|
||||
"atk": b["atk"] * mult * ebonus * prof["atk"],
|
||||
"armor": minf(0.75, b["armor"] + prof["armor"]),
|
||||
"aspd": b["aspd"] * prof["aspd"],
|
||||
"range": b["range"],
|
||||
"mana": b["mana"],
|
||||
"mana": b["mana"] * prof["mana"],
|
||||
"crit": prof["crit"],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -22,31 +22,111 @@ func setup(rand: RandomNumberGenerator) -> void:
|
||||
greed = rand.randf_range(0.3, 0.8)
|
||||
|
||||
|
||||
func take_turn(p: PlayerState, pool: Pool, rand: RandomNumberGenerator, round_no: int) -> void:
|
||||
func take_turn(p: PlayerState, pool: Pool, rand: RandomNumberGenerator) -> void:
|
||||
_do_upgrades(p, pool)
|
||||
_do_buys(p, pool, rand, round_no)
|
||||
_do_placements(p, pool, round_no)
|
||||
_do_buys(p, pool)
|
||||
_do_placements(p)
|
||||
_do_upgrades(p, pool)
|
||||
var keep := int(lerpf(2.0, 18.0, greed))
|
||||
if p.gold > keep + Data.REROLL_COST and rand.randf() < 0.6:
|
||||
p.reroll(pool, rand)
|
||||
_do_buys(p, pool, rand, round_no)
|
||||
_do_placements(p, pool, round_no)
|
||||
_do_upgrades(p, pool)
|
||||
# Wer kurz vor einem Upgrade steht, würfelt gezielt nach Futter.
|
||||
var hunting := _closest_upgrade_gap(p) <= 3
|
||||
var rolls := 3 if hunting else 1
|
||||
for i in rolls:
|
||||
if p.gold > keep + Data.REROLL_COST and (hunting or rand.randf() < 0.6):
|
||||
p.reroll(pool, rand)
|
||||
_do_buys(p, pool)
|
||||
_do_placements(p)
|
||||
_do_upgrades(p, pool)
|
||||
# Level kaufen, wenn das Brett voll ist und Nachwuchs auf der Bank wartet.
|
||||
while p.gold > keep + Data.XP_BUY_COST and not p.can_place() \
|
||||
and _pick_bench_for_board(p) >= 0 and p.buy_xp():
|
||||
_do_placements(p)
|
||||
_free_bench(p, pool)
|
||||
|
||||
|
||||
## Stellt das Brett gegen den bekannten Gegner um: Tanks vor seine Schützen,
|
||||
## eigene Schützen weg von seinen Tanks. Ohne Gegner gilt die Standardaufstellung.
|
||||
func arrange(p: PlayerState, opp: PlayerState) -> void:
|
||||
if p.board.is_empty():
|
||||
return
|
||||
var units: Array = p.board_units()
|
||||
p.board.clear()
|
||||
var threat := [] # Spalten (gespiegelt), in denen gegnerische Schützen stehen
|
||||
var enemy_tanks := [] # Spalten (gespiegelt) gegnerischer Tanks
|
||||
if opp != null:
|
||||
for hex in opp.board:
|
||||
var col: int = Data.BOARD_COLS - 1 - hex.x
|
||||
if opp.board[hex].role == Data.Role.TANK:
|
||||
enemy_tanks.append(col)
|
||||
else:
|
||||
threat.append(col)
|
||||
var center := [3, 2, 4, 1, 5, 0, 6]
|
||||
var tank_cols := threat.duplicate()
|
||||
for c in center:
|
||||
if not tank_cols.has(c):
|
||||
tank_cols.append(c)
|
||||
# Schützen möglichst weit weg von gegnerischen Tanks.
|
||||
var far_cols := center.duplicate()
|
||||
far_cols.sort_custom(func(a, b):
|
||||
return _min_dist(a, enemy_tanks) > _min_dist(b, enemy_tanks))
|
||||
units.sort_custom(func(a, b): return a.role == Data.Role.TANK and b.role != Data.Role.TANK)
|
||||
for u in units:
|
||||
var rows: Array = [0, 1, 2, 3] if u.role == Data.Role.TANK else [3, 2, 1, 0]
|
||||
var cols: Array = tank_cols if u.role == Data.Role.TANK else far_cols
|
||||
var placed := false
|
||||
for r in rows:
|
||||
for c in cols:
|
||||
var h := Vector2i(c, r)
|
||||
if not p.board.has(h):
|
||||
p.board[h] = u
|
||||
placed = true
|
||||
break
|
||||
if placed:
|
||||
break
|
||||
|
||||
|
||||
func _min_dist(col: int, cols: Array) -> int:
|
||||
if cols.is_empty():
|
||||
return 0
|
||||
var best := 99
|
||||
for c in cols:
|
||||
best = mini(best, absi(col - c))
|
||||
return best
|
||||
|
||||
|
||||
## Kleinste Zahl fehlender Futter-Avatare über alle Board-Einheiten.
|
||||
func _closest_upgrade_gap(p: PlayerState) -> int:
|
||||
var best := 99
|
||||
for hex in p.board:
|
||||
var miss := p.upgrade_missing(hex)
|
||||
if p.board[hex].upgrade_cost().is_empty():
|
||||
continue
|
||||
var total := 0
|
||||
for e in miss:
|
||||
total += miss[e]
|
||||
best = mini(best, total)
|
||||
return best
|
||||
|
||||
|
||||
## Elemente, die irgendeine Board-Einheit für ihr nächstes Upgrade braucht.
|
||||
func _needed_elements(p: PlayerState) -> Dictionary:
|
||||
var need := {}
|
||||
for hex in p.board:
|
||||
for e in p.board[hex].upgrade_cost():
|
||||
need[e] = true
|
||||
return need
|
||||
|
||||
|
||||
func _wants(entry: ShopEntry, p: PlayerState) -> bool:
|
||||
if entry.element < 0:
|
||||
# Neutral wird beim Kauf spezialisiert, also immer brauchbar.
|
||||
return true
|
||||
if pref.has(entry.element):
|
||||
if _needed_elements(p).has(entry.element):
|
||||
return true
|
||||
# Elemente, die schon auf dem Board sind, sind immer Futter.
|
||||
return p.trait_counts().has(entry.element)
|
||||
return pref.has(entry.element)
|
||||
|
||||
|
||||
func _do_buys(p: PlayerState, pool: Pool, rand: RandomNumberGenerator, round_no: int) -> void:
|
||||
func _do_buys(p: PlayerState, pool: Pool) -> void:
|
||||
for i in p.shop.size():
|
||||
var e: ShopEntry = p.shop[i]
|
||||
if e.sold:
|
||||
@@ -60,8 +140,8 @@ func _do_buys(p: PlayerState, pool: Pool, rand: RandomNumberGenerator, round_no:
|
||||
p.buy(i, pool, _pick_element(p) if e.element < 0 else e.element)
|
||||
|
||||
|
||||
func _do_placements(p: PlayerState, pool: Pool, round_no: int) -> void:
|
||||
while p.can_place(round_no):
|
||||
func _do_placements(p: PlayerState) -> void:
|
||||
while p.can_place():
|
||||
var idx := _pick_bench_for_board(p)
|
||||
if idx < 0:
|
||||
return
|
||||
@@ -69,7 +149,7 @@ func _do_placements(p: PlayerState, pool: Pool, round_no: int) -> void:
|
||||
var hex := _pick_hex(p, a.role)
|
||||
if hex.x < 0:
|
||||
return
|
||||
if not p.place(idx, hex, round_no):
|
||||
if not p.place(idx, hex):
|
||||
return
|
||||
|
||||
|
||||
@@ -77,6 +157,7 @@ func _do_placements(p: PlayerState, pool: Pool, round_no: int) -> void:
|
||||
## Alles Weitere bleibt als Futter auf der Bank.
|
||||
func _pick_bench_for_board(p: PlayerState) -> int:
|
||||
var counts := p.trait_counts()
|
||||
var need := _needed_elements(p)
|
||||
var best := -1
|
||||
var best_score := -1
|
||||
for i in p.bench.size():
|
||||
@@ -84,7 +165,9 @@ func _pick_bench_for_board(p: PlayerState) -> int:
|
||||
if a == null:
|
||||
continue
|
||||
var score := 0
|
||||
if pref.has(a.elements[0]):
|
||||
if need.has(a.elements[0]) and counts.get(a.elements[0], 0) > 0:
|
||||
score = 0 # Futter für ein laufendes Upgrade bleibt auf der Bank
|
||||
elif pref.has(a.elements[0]):
|
||||
score = 3 if counts.get(a.elements[0], 0) == 0 else 1
|
||||
else:
|
||||
score = 2 if counts.get(a.elements[0], 0) == 0 else 0
|
||||
@@ -144,12 +227,16 @@ func _do_upgrades(p: PlayerState, pool: Pool) -> void:
|
||||
func _free_bench(p: PlayerState, pool: Pool) -> void:
|
||||
if p.free_bench_slot() >= 0:
|
||||
return
|
||||
var counts := p.trait_counts()
|
||||
var need := _needed_elements(p)
|
||||
# Erst verkaufen, was weder Futter noch Lieblingselement ist.
|
||||
for i in p.bench.size():
|
||||
var a = p.bench[i]
|
||||
if a == null:
|
||||
continue
|
||||
if counts.get(a.elements[0], 0) == 0 and not pref.has(a.elements[0]):
|
||||
if a != null and not need.has(a.elements[0]) and not pref.has(a.elements[0]):
|
||||
p.sell_bench(i, pool)
|
||||
return
|
||||
for i in p.bench.size():
|
||||
var a = p.bench[i]
|
||||
if a != null and not need.has(a.elements[0]):
|
||||
p.sell_bench(i, pool)
|
||||
return
|
||||
p.sell_bench(0, pool)
|
||||
|
||||
@@ -47,6 +47,8 @@ class CUnit extends RefCounted:
|
||||
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))
|
||||
@@ -57,7 +59,12 @@ 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 := {}
|
||||
@@ -116,7 +123,7 @@ func _spawn(p: PlayerState, team: int) -> void:
|
||||
u.max_mana = s["mana"]
|
||||
u.mana = 0.0
|
||||
u.mana_mult = m["mana"]
|
||||
u.crit = m["crit"]
|
||||
u.crit = m["crit"] + s.get("crit", 0.0)
|
||||
if _occ.has(u.pos):
|
||||
u.pos = _free_near(u.pos)
|
||||
_occ[u.pos] = u
|
||||
@@ -266,6 +273,9 @@ 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:
|
||||
@@ -291,6 +301,10 @@ func _apply_damage(src, tgt: CUnit, amount: float, is_true: bool) -> void:
|
||||
if dmg > 0.0:
|
||||
tgt.hp -= dmg
|
||||
tgt.hit_flash = 0.12
|
||||
if src != null:
|
||||
src.dealt += dmg
|
||||
if record_hits and src != null:
|
||||
_record("crit" if amount > src.atk * 1.4 and not is_true else "dmg", 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)
|
||||
@@ -319,8 +333,17 @@ func _kill(u: CUnit) -> void:
|
||||
|
||||
|
||||
func _heal(u: CUnit, amount: float) -> void:
|
||||
if u.alive:
|
||||
u.hp = minf(u.max_hp, u.hp + amount)
|
||||
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 ----------------------------------------------------------
|
||||
@@ -328,6 +351,8 @@ func _heal(u: CUnit, amount: float) -> void:
|
||||
func _cast(u: CUnit) -> void:
|
||||
u.mana = 0.0
|
||||
u.cast_flash = 0.35
|
||||
if record_hits:
|
||||
_record("cast", u, 0.0)
|
||||
var elems: Array = u.avatar.elements
|
||||
var heal_bonus := 0.0
|
||||
for i in elems.size():
|
||||
@@ -373,7 +398,7 @@ func _cast(u: CUnit) -> void:
|
||||
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 allies := _allies_by_missing_hp(u.team, u)
|
||||
var count := 1
|
||||
if u.avatar.star == 2:
|
||||
count = 3
|
||||
@@ -428,11 +453,14 @@ func _apply_element_effects(u: CUnit, main_target, hit_enemies: Array) -> void:
|
||||
main_target.slow_timer = maxf(main_target.slow_timer, 3.0)
|
||||
|
||||
|
||||
func _allies_by_missing_hp(team: int) -> Array:
|
||||
## Verbündete in Heilreichweite, schwächste zuerst. Mindestens der Heiler selbst.
|
||||
func _allies_by_missing_hp(team: int, healer: CUnit) -> Array:
|
||||
var list := []
|
||||
for o in units:
|
||||
if o.alive and o.team == team:
|
||||
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
|
||||
|
||||
@@ -447,6 +475,40 @@ func alive_count(team: int) -> int:
|
||||
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)
|
||||
@@ -455,9 +517,11 @@ func _check_end() -> void:
|
||||
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()
|
||||
@@ -472,6 +536,8 @@ func _resolve_by_hp() -> void:
|
||||
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)
|
||||
|
||||
@@ -28,6 +28,21 @@ const ROLE_STATS := {
|
||||
Role.HEILER: {"hp": 520.0, "atk": 38.0, "armor": 0.22, "aspd": 0.70, "range": 3, "mana": 90.0},
|
||||
}
|
||||
|
||||
# Das Hauptelement prägt die Werte: Feuer schlägt hart, Erde hält aus, Wind ist
|
||||
# schnell, Blitz trifft kritisch, Wasser zaubert öfter, Eis ist zäh und langsam.
|
||||
const ELEMENT_PROFILE := {
|
||||
Element.FEUER: {"hp": 0.90, "atk": 1.18, "armor": 0.0, "aspd": 1.0, "mana": 1.0, "crit": 0.0},
|
||||
Element.WASSER: {"hp": 1.0, "atk": 0.95, "armor": 0.0, "aspd": 1.0, "mana": 0.75, "crit": 0.0},
|
||||
Element.WIND: {"hp": 0.92, "atk": 0.95, "armor": 0.0, "aspd": 1.20, "mana": 1.0, "crit": 0.0},
|
||||
Element.BLITZ: {"hp": 0.95, "atk": 1.0, "armor": 0.0, "aspd": 1.05, "mana": 1.0, "crit": 0.15},
|
||||
Element.ERDE: {"hp": 1.22, "atk": 0.92, "armor": 0.08, "aspd": 0.90, "mana": 1.0, "crit": 0.0},
|
||||
Element.EIS: {"hp": 1.08, "atk": 0.95, "armor": 0.05, "aspd": 0.95, "mana": 0.9, "crit": 0.0},
|
||||
}
|
||||
const PROFILE_HINT := {
|
||||
Element.FEUER: "hart, aber weich", Element.WASSER: "zaubert oft", Element.WIND: "schnell",
|
||||
Element.BLITZ: "kritisch", Element.ERDE: "zäh und langsam", Element.EIS: "robust",
|
||||
}
|
||||
|
||||
# Multiplikator pro Sternstufe (Index = star - 1).
|
||||
const STAR_MULT := [1.0, 1.8, 3.24]
|
||||
const MAX_STAR := 3
|
||||
@@ -55,13 +70,29 @@ const START_GOLD := 4
|
||||
# --- Geteilter Pool --------------------------------------------------------
|
||||
## Alle 8 Spieler teilen sich diesen Vorrat pro Element. Ein 3-Star bindet
|
||||
## allein 9 Stück, dazu kommt was auf Bänken und Brettern liegt.
|
||||
## Bei 180 hält der Vorrat eine ganze Partie und kann im Endspiel trotzdem knapp werden.
|
||||
const POOL_PER_ELEMENT := 180
|
||||
## Bei 100 hält der Vorrat eine ganze Partie, geht im Endspiel aber auf rund ein Fünftel.
|
||||
const POOL_PER_ELEMENT := 100
|
||||
|
||||
# --- Board -----------------------------------------------------------------
|
||||
const BOARD_COLS := 7
|
||||
const BOARD_ROWS := 4
|
||||
const MAX_TEAM_SLOTS := 9
|
||||
|
||||
# --- Level und Erfahrung ---------------------------------------------------
|
||||
## Das Spielerlevel ist die Anzahl der Kaderplätze. Jede Runde gibt XP,
|
||||
## weitere XP kauft man. Level gegen Würfeln ist die TFT-Kernentscheidung.
|
||||
const START_LEVEL := 3
|
||||
const MAX_LEVEL := 9
|
||||
const XP_PER_ROUND := 2
|
||||
const XP_BUY_COST := 4
|
||||
const XP_BUY_AMOUNT := 4
|
||||
const XP_TABLE := {3: 4, 4: 8, 5: 12, 6: 20, 7: 32, 8: 48}
|
||||
|
||||
# --- Planung ---------------------------------------------------------------
|
||||
const PLAN_SECONDS := 30.0
|
||||
|
||||
# --- Kampf -----------------------------------------------------------------
|
||||
## Heiler heilen nur Verbündete in dieser Hex-Reichweite.
|
||||
const HEAL_RANGE := 2
|
||||
|
||||
# --- Element-Traits --------------------------------------------------------
|
||||
# steps = Anzahl aufgestellter Einheiten mit diesem Element.
|
||||
@@ -133,9 +164,14 @@ static func trait_tier(element: int, count: int) -> int:
|
||||
return tier
|
||||
|
||||
|
||||
## Teamgröße wächst mit der Runde.
|
||||
static func team_slots(round_no: int) -> int:
|
||||
return mini(3 + int(float(round_no - 1) / 2.0), MAX_TEAM_SLOTS)
|
||||
## XP bis zum nächsten Level, 0 auf Maximalstufe.
|
||||
static func xp_needed(level: int) -> int:
|
||||
return XP_TABLE.get(level, 0)
|
||||
|
||||
|
||||
## Grundschaden einer verlorenen Runde, steigt alle vier Runden.
|
||||
static func round_base_damage(round_no: int) -> int:
|
||||
return 2 + int(float(round_no - 1) / 4.0)
|
||||
|
||||
|
||||
## Wahrscheinlichkeit, dass ein Shop-Platz einen neutralen Avatar zeigt.
|
||||
|
||||
@@ -20,6 +20,11 @@ var pairings: Array = [] ## [{a, b, ghost}]
|
||||
var player_combat: Combat = null
|
||||
var pending: Array = [] ## Ergebnisse der Bot-Kämpfe
|
||||
var last_round_log: Array = []
|
||||
var plan_left: float = Data.PLAN_SECONDS
|
||||
var timer_enabled: bool = true
|
||||
var human_recap: Dictionary = {} ## Rückblick auf den letzten eigenen Kampf
|
||||
## Zählt, wie oft der Mensch gewonnen bzw. verloren hat (für Sound und Anzeige).
|
||||
var human_last_result: int = 0 ## 1 Sieg, -1 Niederlage, 0 unentschieden/keine
|
||||
|
||||
|
||||
func start(seed_value: int = 0) -> void:
|
||||
@@ -37,7 +42,9 @@ func start(seed_value: int = 0) -> void:
|
||||
round_no = 1
|
||||
for p in players:
|
||||
p.roll_shop(pool, rand)
|
||||
pairings = _make_pairings()
|
||||
_run_bot_turns()
|
||||
plan_left = Data.PLAN_SECONDS
|
||||
phase = Phase.PLANNING
|
||||
|
||||
|
||||
@@ -54,7 +61,21 @@ func alive_players() -> Array:
|
||||
func _run_bot_turns() -> void:
|
||||
for p in players:
|
||||
if p.is_bot and p.alive:
|
||||
bots[p.pid].take_turn(p, pool, rand, round_no)
|
||||
bots[p.pid].take_turn(p, pool, rand)
|
||||
bots[p.pid].arrange(p, opponent_of(p))
|
||||
|
||||
|
||||
## Planungsuhr. Läuft sie ab, startet der Kampf von selbst.
|
||||
## Gibt true zurück, wenn dadurch die Phase gewechselt hat.
|
||||
func tick_planning(dt: float) -> bool:
|
||||
if phase != Phase.PLANNING or not timer_enabled:
|
||||
return false
|
||||
plan_left -= dt
|
||||
if plan_left <= 0.0:
|
||||
plan_left = 0.0
|
||||
start_combat()
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
# --- Kampfphase ------------------------------------------------------------
|
||||
@@ -62,12 +83,16 @@ func _run_bot_turns() -> void:
|
||||
func start_combat() -> void:
|
||||
if phase != Phase.PLANNING:
|
||||
return
|
||||
pairings = _make_pairings()
|
||||
# Abbilder erst jetzt kopieren, damit sie den aktuellen Stand zeigen.
|
||||
for pr in pairings:
|
||||
if pr["ghost"]:
|
||||
pr["b"] = _make_ghost(pr["ghost_src"])
|
||||
pending.clear()
|
||||
player_combat = null
|
||||
for pr in pairings:
|
||||
if pr["a"] == human or pr["b"] == human:
|
||||
player_combat = Combat.new(pr["a"], pr["b"], rand.randi())
|
||||
player_combat.record_hits = true
|
||||
pending.append({"pair": pr, "combat": player_combat})
|
||||
else:
|
||||
var c := Combat.new(pr["a"], pr["b"], rand.randi())
|
||||
@@ -107,7 +132,7 @@ func _make_pairings() -> Array:
|
||||
if others.is_empty():
|
||||
return out
|
||||
var src: PlayerState = others[rand.randi_range(0, others.size() - 1)]
|
||||
out.append({"a": lone, "b": _make_ghost(src), "ghost": true})
|
||||
out.append({"a": lone, "b": _make_ghost(src), "ghost": true, "ghost_src": src})
|
||||
return out
|
||||
|
||||
|
||||
@@ -129,18 +154,25 @@ func resolve_round() -> void:
|
||||
c.run_to_end()
|
||||
var a: PlayerState = pr["a"]
|
||||
var b: PlayerState = pr["b"]
|
||||
var dmg := 3 + c.survivors
|
||||
var base := Data.round_base_damage(round_no)
|
||||
var dmg := base + c.survivor_stars
|
||||
if a == human or b == human:
|
||||
human_last_result = 0 if c.winner < 0 else (1 if pr["a" if c.winner == 0 else "b"] == human else -1)
|
||||
human_recap = c.recap(0 if a == human else 1)
|
||||
human_recap["opponent"] = (b if a == human else a).pname
|
||||
human_recap["result"] = human_last_result
|
||||
human_recap["damage"] = dmg if c.winner >= 0 else base
|
||||
if c.winner == 0:
|
||||
_apply_result(a, b, dmg, pr["ghost"])
|
||||
elif c.winner == 1:
|
||||
_apply_result(b, a, dmg, pr["ghost"])
|
||||
else:
|
||||
a.register_result(false)
|
||||
a.take_damage(3)
|
||||
a.take_damage(base)
|
||||
if not pr["ghost"]:
|
||||
b.register_result(false)
|
||||
b.take_damage(3)
|
||||
last_round_log.append("%s vs %s: unentschieden" % [a.pname, b.pname])
|
||||
b.take_damage(base)
|
||||
last_round_log.append("%s vs %s: unentschieden (-%d)" % [a.pname, b.pname, base])
|
||||
pending.clear()
|
||||
player_combat = null
|
||||
_check_eliminations()
|
||||
@@ -176,13 +208,26 @@ func _next_round() -> void:
|
||||
if not p.alive:
|
||||
continue
|
||||
p.gold += p.round_income()
|
||||
p.roll_shop(pool, rand)
|
||||
p.gain_xp(Data.XP_PER_ROUND)
|
||||
p.new_round_shop(pool, rand)
|
||||
pairings = _make_pairings()
|
||||
_run_bot_turns()
|
||||
plan_left = Data.PLAN_SECONDS
|
||||
phase = Phase.PLANNING
|
||||
|
||||
|
||||
# --- Hilfen für die UI ----------------------------------------------------
|
||||
|
||||
## Gegner eines Spielers in der aktuellen Paarung (Abbilder liefern ihre Quelle).
|
||||
func opponent_of(p: PlayerState) -> PlayerState:
|
||||
for pr in pairings:
|
||||
if pr["a"] == p:
|
||||
return pr["ghost_src"] if pr["ghost"] else pr["b"]
|
||||
if pr["b"] == p:
|
||||
return pr["a"]
|
||||
return null
|
||||
|
||||
|
||||
func human_opponent() -> PlayerState:
|
||||
for pr in pairings:
|
||||
if pr["a"] == human:
|
||||
|
||||
@@ -12,6 +12,10 @@ var streak: int = 0 ## >0 Siegserie, <0 Niederlagenserie
|
||||
var alive: bool = true
|
||||
var last_won: bool = false
|
||||
var placement: int = 0 ## Endplatzierung, 0 = noch im Spiel
|
||||
var level: int = Data.START_LEVEL
|
||||
var xp: int = 0
|
||||
var shop_locked: bool = false ## eingefrorener Shop bleibt über die Runde
|
||||
var last_element: int = -1 ## zuletzt gewählte Spezialisierung für Schnellkauf
|
||||
|
||||
var bench: Array = [] ## Avatar oder null
|
||||
var board: Dictionary = {} ## Vector2i -> Avatar
|
||||
@@ -72,10 +76,18 @@ func reroll(pool: Pool, rng: RandomNumberGenerator) -> bool:
|
||||
if gold < Data.REROLL_COST:
|
||||
return false
|
||||
gold -= Data.REROLL_COST
|
||||
shop_locked = false
|
||||
roll_shop(pool, rng)
|
||||
return true
|
||||
|
||||
|
||||
## Neue Runde: der Shop wird nur neu gefüllt, wenn er nicht eingefroren ist.
|
||||
func new_round_shop(pool: Pool, rng: RandomNumberGenerator) -> void:
|
||||
if shop_locked:
|
||||
return
|
||||
roll_shop(pool, rng)
|
||||
|
||||
|
||||
func free_bench_slot() -> int:
|
||||
for i in bench.size():
|
||||
if bench[i] == null:
|
||||
@@ -101,6 +113,8 @@ func buy(index: int, pool: Pool, chosen_element: int = -1) -> bool:
|
||||
gold -= entry.price()
|
||||
bench[slot] = entry.to_avatar(element)
|
||||
entry.sold = true
|
||||
if entry.element < 0:
|
||||
last_element = element
|
||||
return true
|
||||
|
||||
|
||||
@@ -126,21 +140,42 @@ func sell_board(hex: Vector2i, pool: Pool) -> bool:
|
||||
|
||||
# --- Aufstellen ------------------------------------------------------------
|
||||
|
||||
func team_slots(round_no: int) -> int:
|
||||
return Data.team_slots(round_no)
|
||||
func team_slots() -> int:
|
||||
return level
|
||||
|
||||
|
||||
func can_place(round_no: int) -> bool:
|
||||
return board.size() < team_slots(round_no)
|
||||
func can_place() -> bool:
|
||||
return board.size() < level
|
||||
|
||||
|
||||
func gain_xp(amount: int) -> void:
|
||||
xp += amount
|
||||
while level < Data.MAX_LEVEL and xp >= Data.xp_needed(level):
|
||||
xp -= Data.xp_needed(level)
|
||||
level += 1
|
||||
if level >= Data.MAX_LEVEL:
|
||||
xp = 0
|
||||
|
||||
|
||||
func can_buy_xp() -> bool:
|
||||
return level < Data.MAX_LEVEL and gold >= Data.XP_BUY_COST
|
||||
|
||||
|
||||
func buy_xp() -> bool:
|
||||
if not can_buy_xp():
|
||||
return false
|
||||
gold -= Data.XP_BUY_COST
|
||||
gain_xp(Data.XP_BUY_AMOUNT)
|
||||
return true
|
||||
|
||||
|
||||
## Setzt einen Avatar von der Bank aufs Board.
|
||||
func place(bench_index: int, hex: Vector2i, round_no: int) -> bool:
|
||||
func place(bench_index: int, hex: Vector2i) -> bool:
|
||||
if bench_index < 0 or bench_index >= bench.size() or bench[bench_index] == null:
|
||||
return false
|
||||
if board.has(hex) or not _valid_hex(hex):
|
||||
return false
|
||||
if not can_place(round_no):
|
||||
if not can_place():
|
||||
return false
|
||||
var a: Avatar = bench[bench_index]
|
||||
board[hex] = a
|
||||
@@ -175,7 +210,7 @@ func move_on_board(from_hex: Vector2i, to_hex: Vector2i) -> bool:
|
||||
## Verschiebt einen Avatar zwischen Bank und Board, in beide Richtungen.
|
||||
## Ist das Ziel belegt, tauschen die beiden Avatare die Plätze.
|
||||
## src und dst sind {"kind": "board"/"bench", "hex": Vector2i, "index": int}.
|
||||
func move_slot(src: Dictionary, dst: Dictionary, round_no: int) -> bool:
|
||||
func move_slot(src: Dictionary, dst: Dictionary) -> bool:
|
||||
if src.is_empty() or dst.is_empty() or src == dst:
|
||||
return false
|
||||
var sk: String = src["kind"]
|
||||
@@ -212,7 +247,7 @@ func move_slot(src: Dictionary, dst: Dictionary, round_no: int) -> bool:
|
||||
var hex3: Vector2i = dst["hex"]
|
||||
if bench[i3] == null or not _valid_hex(hex3):
|
||||
return false
|
||||
if not board.has(hex3) and not can_place(round_no):
|
||||
if not board.has(hex3) and not can_place():
|
||||
return false
|
||||
var unit3: Avatar = bench[i3]
|
||||
if board.has(hex3):
|
||||
|
||||
Reference in New Issue
Block a user