update
This commit is contained in:
@@ -113,16 +113,25 @@ func ability_text() -> String:
|
||||
s = "heilt 3 Verbündete um %d" % amount
|
||||
else:
|
||||
s = "heilt alle um %d und schildet" % amount
|
||||
if not elements.is_empty():
|
||||
var f: Dictionary = Data.ELEMENT_FORM[elements[0]]
|
||||
s += "\nForm %s: %s" % [f["name"], f["desc"]]
|
||||
var extra := []
|
||||
for i in elements.size():
|
||||
var scale := "" if i == 0 else " (halb)"
|
||||
match elements[i]:
|
||||
Data.Element.FEUER: extra.append("Brand" + scale)
|
||||
Data.Element.WASSER: extra.append("Mana + stärkere Heilung" + scale)
|
||||
Data.Element.WIND: extra.append("Tempo-Schub" + scale)
|
||||
Data.Element.BLITZ: extra.append("Kettenblitz" + scale)
|
||||
Data.Element.ERDE: extra.append("Betäubung" + scale)
|
||||
Data.Element.EIS: extra.append("Verlangsamung" + scale)
|
||||
for i in range(1, elements.size()):
|
||||
extra.append(Data.element_effect(elements[i]) + " (halb)")
|
||||
if not extra.is_empty():
|
||||
s += "\nElemente: " + ", ".join(extra)
|
||||
s += "\nDazu: " + ", ".join(extra)
|
||||
return s
|
||||
|
||||
|
||||
## „Stark gegen … · Schwach gegen …" aus Hauptelement und Rolle.
|
||||
func counter_text() -> String:
|
||||
var e := primary_element()
|
||||
var strong := []
|
||||
var weak := []
|
||||
if e >= 0:
|
||||
strong.append(Data.ELEMENT_NAMES[Data.ELEMENT_BEATS[e]])
|
||||
weak.append(Data.ELEMENT_NAMES[Data.beaten_by(e)])
|
||||
strong.append(Data.ROLE_NAMES[Data.ROLE_BEATS[role]])
|
||||
weak.append(Data.ROLE_NAMES[Data.role_beaten_by(role)])
|
||||
return "Stark gegen %s · Schwach gegen %s" % [", ".join(strong), ", ".join(weak)]
|
||||
|
||||
@@ -5,9 +5,12 @@ extends RefCounted
|
||||
|
||||
var pref: Array[int] = []
|
||||
var greed: float = 0.5 ## wie stark der Bot Gold hortet
|
||||
var difficulty: int = Data.Difficulty.NORMAL
|
||||
var opponent: PlayerState = null ## bekannter Gegner der Runde, für Konterwahl
|
||||
|
||||
|
||||
func setup(rand: RandomNumberGenerator) -> void:
|
||||
func setup(rand: RandomNumberGenerator, p_difficulty: int = Data.Difficulty.NORMAL) -> void:
|
||||
difficulty = p_difficulty
|
||||
var all: Array[int] = []
|
||||
for e in Data.ELEMENT_COUNT:
|
||||
all.append(e)
|
||||
@@ -19,18 +22,23 @@ func setup(rand: RandomNumberGenerator) -> void:
|
||||
all[i] = all[j]
|
||||
all[j] = tmp
|
||||
pref = [all[0], all[1], all[2]]
|
||||
greed = rand.randf_range(0.3, 0.8)
|
||||
var g: Array = Data.BOT_GREED[difficulty]
|
||||
greed = rand.randf_range(g[0], g[1])
|
||||
|
||||
|
||||
func take_turn(p: PlayerState, pool: Pool, rand: RandomNumberGenerator) -> void:
|
||||
func take_turn(p: PlayerState, pool: Pool, rand: RandomNumberGenerator, opp: PlayerState = null) -> void:
|
||||
opponent = opp
|
||||
_do_upgrades(p, pool)
|
||||
_do_buys(p, pool)
|
||||
_do_placements(p)
|
||||
_do_upgrades(p, pool)
|
||||
var keep := int(lerpf(2.0, 18.0, greed))
|
||||
# Wer kurz vor einem Upgrade steht, würfelt gezielt nach Futter.
|
||||
var hunting := _closest_upgrade_gap(p) <= 3
|
||||
# Leichte Bots jagen nicht, schwere immer.
|
||||
var hunting := difficulty != Data.Difficulty.LEICHT and _closest_upgrade_gap(p) <= 3
|
||||
var rolls := 3 if hunting else 1
|
||||
if difficulty == Data.Difficulty.SCHWER:
|
||||
rolls += 1
|
||||
for i in rolls:
|
||||
if p.gold > keep + Data.REROLL_COST and (hunting or rand.randf() < 0.6):
|
||||
p.reroll(pool, rand)
|
||||
@@ -71,8 +79,13 @@ func arrange(p: PlayerState, opp: PlayerState) -> void:
|
||||
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
|
||||
# Heiler direkt hinter die Tanks, sonst reicht ihre Heilung (2 Felder) nicht.
|
||||
var rows: Array
|
||||
match u.role:
|
||||
Data.Role.TANK: rows = [0, 1, 2, 3]
|
||||
Data.Role.HEILER: rows = [1, 2, 0, 3]
|
||||
_: rows = [3, 2, 1, 0]
|
||||
var cols: Array = tank_cols if u.role != Data.Role.DPS else far_cols
|
||||
var placed := false
|
||||
for r in rows:
|
||||
for c in cols:
|
||||
@@ -179,6 +192,13 @@ func _pick_bench_for_board(p: PlayerState) -> int:
|
||||
|
||||
func _pick_element(p: PlayerState) -> int:
|
||||
var counts := p.trait_counts()
|
||||
# Konter: schlägt eines meiner Lieblingselemente das häufigste Element des Gegners,
|
||||
# nehme ich das. Schwere Bots kontern auch außerhalb ihrer Vorlieben.
|
||||
var dominant := _dominant_element(opponent)
|
||||
if dominant >= 0:
|
||||
var counter := Data.beaten_by(dominant)
|
||||
if pref.has(counter) or difficulty == Data.Difficulty.SCHWER:
|
||||
return counter
|
||||
var best := pref[0]
|
||||
var best_c := -1
|
||||
for e in pref:
|
||||
@@ -189,6 +209,23 @@ func _pick_element(p: PlayerState) -> int:
|
||||
return best
|
||||
|
||||
|
||||
## Häufigstes Hauptelement auf dem Brett eines Spielers, -1 wenn leer.
|
||||
func _dominant_element(p: PlayerState) -> int:
|
||||
if p == null or p.board.is_empty():
|
||||
return -1
|
||||
var counts := {}
|
||||
for u in p.board_units():
|
||||
var e: int = u.primary_element()
|
||||
counts[e] = counts.get(e, 0) + 1
|
||||
var best := -1
|
||||
var best_n := 1 # ab zwei Einheiten lohnt das Kontern
|
||||
for e in counts:
|
||||
if counts[e] > best_n:
|
||||
best_n = counts[e]
|
||||
best = e
|
||||
return best
|
||||
|
||||
|
||||
func _pick_hex(p: PlayerState, role: int) -> Vector2i:
|
||||
var rows: Array
|
||||
match role:
|
||||
|
||||
@@ -293,6 +293,12 @@ func _basic_attack(u: CUnit, t: CUnit) -> void:
|
||||
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)
|
||||
@@ -304,7 +310,12 @@ func _apply_damage(src, tgt: CUnit, amount: float, is_true: bool) -> void:
|
||||
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)
|
||||
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)
|
||||
@@ -351,8 +362,6 @@ func _record(kind: String, 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():
|
||||
@@ -361,6 +370,10 @@ func _cast(u: CUnit) -> void:
|
||||
|
||||
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:
|
||||
@@ -423,8 +436,10 @@ func _apply_element_effects(u: CUnit, main_target, hit_enemies: Array) -> void:
|
||||
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
|
||||
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:
|
||||
@@ -454,6 +469,58 @@ func _apply_element_effects(u: CUnit, main_target, hit_enemies: Array) -> void:
|
||||
|
||||
|
||||
## 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:
|
||||
|
||||
@@ -43,6 +43,42 @@ const PROFILE_HINT := {
|
||||
Element.BLITZ: "kritisch", Element.ERDE: "zäh und langsam", Element.EIS: "robust",
|
||||
}
|
||||
|
||||
# Das Hauptelement gibt der Fähigkeit ihre Form. Gilt für jede Rolle:
|
||||
# ein Feuer-Heiler brennt, ein Erde-Tank erschüttert, ein Eis-Schütze friert.
|
||||
const ELEMENT_FORM := {
|
||||
Element.FEUER: {"name": "Flächenbrand", "desc": "trifft auch Nachbarn des Ziels (60%) und entzündet sie"},
|
||||
Element.WASSER: {"name": "Welle", "desc": "gibt Verbündeten im Umkreis 2 je 20 Mana"},
|
||||
Element.WIND: {"name": "Sturmschritt", "desc": "Verbündete im Umkreis 2 werden 3 s um 25% schneller"},
|
||||
Element.BLITZ: {"name": "Kette", "desc": "springt auf 2 weitere Gegner (60%)"},
|
||||
Element.ERDE: {"name": "Erschütterung","desc": "betäubt Gegner neben dem Ziel 0,6 s"},
|
||||
Element.EIS: {"name": "Frostfeld", "desc": "verlangsamt Gegner um das Ziel 3 s um 30%"},
|
||||
}
|
||||
|
||||
# Bots: wie gierig sie horten und wie gezielt sie jagen.
|
||||
enum Difficulty { LEICHT, NORMAL, SCHWER }
|
||||
const DIFFICULTY_NAMES := ["Leicht", "Normal", "Schwer"]
|
||||
const BOT_GREED := {
|
||||
Difficulty.LEICHT: [0.6, 0.9], Difficulty.NORMAL: [0.3, 0.8], Difficulty.SCHWER: [0.15, 0.4],
|
||||
}
|
||||
|
||||
# Schere-Stein-Papier. Elemente im Ring: jedes schlägt genau eines und verliert
|
||||
# gegen genau eines. Rollen als Dreieck. Nur das Hauptelement zählt.
|
||||
const ELEMENT_BEATS := {
|
||||
Element.WASSER: Element.FEUER, # Wasser löscht Feuer
|
||||
Element.FEUER: Element.EIS, # Feuer schmilzt Eis
|
||||
Element.EIS: Element.WIND, # Eis lähmt Wind
|
||||
Element.WIND: Element.ERDE, # Wind trägt Erde ab
|
||||
Element.ERDE: Element.BLITZ, # Erde erdet Blitz
|
||||
Element.BLITZ: Element.WASSER, # Blitz leitet durch Wasser
|
||||
}
|
||||
const ROLE_BEATS := {
|
||||
Role.TANK: Role.DPS, # Tank hält Schützen aus
|
||||
Role.DPS: Role.HEILER, # Schütze bricht Heiler
|
||||
Role.HEILER: Role.TANK, # Heiler überdauert Tank
|
||||
}
|
||||
const ELEMENT_COUNTER := 0.20
|
||||
const ROLE_COUNTER := 0.15
|
||||
|
||||
# Multiplikator pro Sternstufe (Index = star - 1).
|
||||
const STAR_MULT := [1.0, 1.8, 3.24]
|
||||
const MAX_STAR := 3
|
||||
@@ -89,6 +125,9 @@ const XP_TABLE := {3: 4, 4: 8, 5: 12, 6: 20, 7: 32, 8: 48}
|
||||
|
||||
# --- Planung ---------------------------------------------------------------
|
||||
const PLAN_SECONDS := 30.0
|
||||
## Die ersten Runden gehen gegen die Wildnis, damit jeder in Ruhe aufbaut.
|
||||
const PVE_ROUNDS := 2
|
||||
const SAVE_PATH := "user://spielstand.json"
|
||||
|
||||
# --- Kampf -----------------------------------------------------------------
|
||||
## Heiler heilen nur Verbündete in dieser Hex-Reichweite.
|
||||
@@ -164,6 +203,65 @@ static func trait_tier(element: int, count: int) -> int:
|
||||
return tier
|
||||
|
||||
|
||||
static func beats_element(attacker: int, defender: int) -> bool:
|
||||
return attacker >= 0 and defender >= 0 and ELEMENT_BEATS.get(attacker, -1) == defender
|
||||
|
||||
|
||||
static func beats_role(attacker: int, defender: int) -> bool:
|
||||
return ROLE_BEATS.get(attacker, -1) == defender
|
||||
|
||||
|
||||
## Schadensfaktor eines Angreifers gegen einen Verteidiger (Hauptelement und Rolle).
|
||||
static func counter_multiplier(att_el: int, att_role: int, def_el: int, def_role: int) -> float:
|
||||
var m := 1.0
|
||||
if beats_element(att_el, def_el):
|
||||
m += ELEMENT_COUNTER
|
||||
elif beats_element(def_el, att_el):
|
||||
m -= ELEMENT_COUNTER
|
||||
if beats_role(att_role, def_role):
|
||||
m += ROLE_COUNTER
|
||||
elif beats_role(def_role, att_role):
|
||||
m -= ROLE_COUNTER
|
||||
return m
|
||||
|
||||
|
||||
## Element, das dieses Element schlägt (das Gegenstück zu ELEMENT_BEATS).
|
||||
static func beaten_by(element: int) -> int:
|
||||
for e in ELEMENT_BEATS:
|
||||
if ELEMENT_BEATS[e] == element:
|
||||
return e
|
||||
return -1
|
||||
|
||||
|
||||
static func role_beaten_by(role: int) -> int:
|
||||
for r in ROLE_BEATS:
|
||||
if ROLE_BEATS[r] == role:
|
||||
return r
|
||||
return -1
|
||||
|
||||
|
||||
## Was die Fähigkeit einer Rolle auf einer Sternstufe tut.
|
||||
static func star_effect(role: int, star: int) -> String:
|
||||
match role:
|
||||
Role.TANK:
|
||||
return "Schild und spottet 3 Gegner" if star == 2 else "Schild, Spott und 25% Reflexion"
|
||||
Role.DPS:
|
||||
return "Ziel plus Splash auf Nachbarn" if star == 2 else "Ziel plus Fläche im Radius 2"
|
||||
_:
|
||||
return "heilt 3 Verbündete" if star == 2 else "heilt alle und schildet sie"
|
||||
|
||||
|
||||
## Zusatzwirkung eines Nebenelements.
|
||||
static func element_effect(element: int) -> String:
|
||||
match element:
|
||||
Element.FEUER: return "Brand"
|
||||
Element.WASSER: return "Mana und stärkere Heilung"
|
||||
Element.WIND: return "Tempo-Schub"
|
||||
Element.BLITZ: return "Kettenblitz"
|
||||
Element.ERDE: return "Betäubung"
|
||||
_: return "Verlangsamung"
|
||||
|
||||
|
||||
## XP bis zum nächsten Level, 0 auf Maximalstufe.
|
||||
static func xp_needed(level: int) -> int:
|
||||
return XP_TABLE.get(level, 0)
|
||||
|
||||
@@ -27,7 +27,11 @@ var human_recap: Dictionary = {} ## Rückblick auf den letzten eigenen Kampf
|
||||
var human_last_result: int = 0 ## 1 Sieg, -1 Niederlage, 0 unentschieden/keine
|
||||
|
||||
|
||||
func start(seed_value: int = 0) -> void:
|
||||
var difficulty: int = Data.Difficulty.NORMAL
|
||||
|
||||
|
||||
func start(seed_value: int = 0, p_difficulty: int = Data.Difficulty.NORMAL) -> void:
|
||||
difficulty = p_difficulty
|
||||
rand.seed = seed_value if seed_value != 0 else randi()
|
||||
pool = Pool.new()
|
||||
players.clear()
|
||||
@@ -37,7 +41,7 @@ func start(seed_value: int = 0) -> void:
|
||||
var b := PlayerState.new(i + 1, BOT_NAMES[i], true)
|
||||
players.append(b)
|
||||
var ai := BotAI.new()
|
||||
ai.setup(rand)
|
||||
ai.setup(rand, difficulty)
|
||||
bots[b.pid] = ai
|
||||
round_no = 1
|
||||
for p in players:
|
||||
@@ -61,8 +65,9 @@ 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)
|
||||
bots[p.pid].arrange(p, opponent_of(p))
|
||||
var opp := opponent_of(p)
|
||||
bots[p.pid].take_turn(p, pool, rand, opp)
|
||||
bots[p.pid].arrange(p, opp)
|
||||
|
||||
|
||||
## Planungsuhr. Läuft sie ab, startet der Kampf von selbst.
|
||||
@@ -85,7 +90,7 @@ func start_combat() -> void:
|
||||
return
|
||||
# Abbilder erst jetzt kopieren, damit sie den aktuellen Stand zeigen.
|
||||
for pr in pairings:
|
||||
if pr["ghost"]:
|
||||
if pr["ghost"] and pr["ghost_src"] != null:
|
||||
pr["b"] = _make_ghost(pr["ghost_src"])
|
||||
pending.clear()
|
||||
player_combat = null
|
||||
@@ -117,10 +122,28 @@ func skip_combat() -> void:
|
||||
resolve_round()
|
||||
|
||||
|
||||
func is_pve_round() -> bool:
|
||||
return round_no <= Data.PVE_ROUNDS
|
||||
|
||||
|
||||
## Wildnis-Gegner: klein, ohne Leben, jeder Spieler bekommt seinen eigenen.
|
||||
func _make_creeps() -> PlayerState:
|
||||
var c := PlayerState.new(-2, "Wildnis", true)
|
||||
c.level = 9
|
||||
c.board[Vector2i(3, 0)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE] as Array[int], 1)
|
||||
if round_no >= 2:
|
||||
c.board[Vector2i(2, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.WIND] as Array[int], 1)
|
||||
return c
|
||||
|
||||
|
||||
func _make_pairings() -> Array:
|
||||
var alive := alive_players()
|
||||
alive.shuffle()
|
||||
var out := []
|
||||
if is_pve_round():
|
||||
for p in alive:
|
||||
out.append({"a": p, "b": _make_creeps(), "ghost": true, "ghost_src": null})
|
||||
return out
|
||||
alive.shuffle()
|
||||
var i := 0
|
||||
while i + 1 < alive.size():
|
||||
out.append({"a": alive[i], "b": alive[i + 1], "ghost": false})
|
||||
@@ -207,7 +230,9 @@ func _next_round() -> void:
|
||||
for p in players:
|
||||
if not p.alive:
|
||||
continue
|
||||
p.gold += p.round_income()
|
||||
var income: int = p.round_income()
|
||||
p.gold += income
|
||||
p.stats["gold_earned"] += income
|
||||
p.gain_xp(Data.XP_PER_ROUND)
|
||||
p.new_round_shop(pool, rand)
|
||||
pairings = _make_pairings()
|
||||
@@ -216,13 +241,96 @@ func _next_round() -> void:
|
||||
phase = Phase.PLANNING
|
||||
|
||||
|
||||
# --- Speichern -------------------------------------------------------------
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
var ps := []
|
||||
for p in players:
|
||||
ps.append(p.to_dict())
|
||||
var ai := {}
|
||||
for pid in bots:
|
||||
ai[str(pid)] = {"pref": Array(bots[pid].pref), "greed": bots[pid].greed}
|
||||
return {
|
||||
"version": 1, "round": round_no, "difficulty": difficulty, "rng_state": str(rand.state),
|
||||
"pool": Array(pool.element_left), "players": ps, "bots": ai,
|
||||
"human_recap": human_recap, "log": last_round_log,
|
||||
}
|
||||
|
||||
|
||||
func from_dict(d: Dictionary) -> void:
|
||||
difficulty = int(d["difficulty"])
|
||||
rand.state = int(str(d["rng_state"]))
|
||||
pool = Pool.new()
|
||||
for i in mini(pool.element_left.size(), d["pool"].size()):
|
||||
pool.element_left[i] = int(d["pool"][i])
|
||||
players.clear()
|
||||
bots.clear()
|
||||
for pd in d["players"]:
|
||||
var p := PlayerState.from_dict(pd)
|
||||
players.append(p)
|
||||
if not p.is_bot:
|
||||
human = p
|
||||
else:
|
||||
var ai := BotAI.new()
|
||||
ai.setup(rand, difficulty)
|
||||
var bd: Dictionary = d["bots"].get(str(p.pid), {})
|
||||
if bd.has("pref"):
|
||||
ai.pref.clear()
|
||||
for e in bd["pref"]:
|
||||
ai.pref.append(int(e))
|
||||
ai.greed = float(bd["greed"])
|
||||
bots[p.pid] = ai
|
||||
round_no = int(d["round"])
|
||||
human_recap = d.get("human_recap", {})
|
||||
last_round_log = d.get("log", [])
|
||||
pairings = _make_pairings()
|
||||
for p in players:
|
||||
if p.is_bot and p.alive:
|
||||
bots[p.pid].arrange(p, opponent_of(p))
|
||||
plan_left = Data.PLAN_SECONDS
|
||||
phase = Phase.PLANNING
|
||||
|
||||
|
||||
func save(path: String = Data.SAVE_PATH) -> bool:
|
||||
if phase != Phase.PLANNING:
|
||||
return false
|
||||
var f := FileAccess.open(path, FileAccess.WRITE)
|
||||
if f == null:
|
||||
return false
|
||||
f.store_string(JSON.stringify(to_dict()))
|
||||
return true
|
||||
|
||||
|
||||
static func has_save(path: String = Data.SAVE_PATH) -> bool:
|
||||
return FileAccess.file_exists(path)
|
||||
|
||||
|
||||
static func delete_save(path: String = Data.SAVE_PATH) -> void:
|
||||
if FileAccess.file_exists(path):
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(path))
|
||||
|
||||
|
||||
static func load_from(path: String = Data.SAVE_PATH) -> Game:
|
||||
if not FileAccess.file_exists(path):
|
||||
return null
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
if f == null:
|
||||
return null
|
||||
var parsed = JSON.parse_string(f.get_as_text())
|
||||
if not (parsed is Dictionary):
|
||||
return null
|
||||
var g := Game.new()
|
||||
g.from_dict(parsed)
|
||||
return g
|
||||
|
||||
|
||||
# --- 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"]
|
||||
return pr["ghost_src"] if (pr["ghost"] and pr["ghost_src"] != null) else pr["b"]
|
||||
if pr["b"] == p:
|
||||
return pr["a"]
|
||||
return null
|
||||
|
||||
@@ -16,6 +16,9 @@ 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 last_sale: Dictionary = {} ## letzter Verkauf, für Rückgängig
|
||||
## Statistik für den Endbildschirm.
|
||||
var stats := {"wins": 0, "losses": 0, "gold_earned": 0, "spent": 0, "max_star": 1}
|
||||
|
||||
var bench: Array = [] ## Avatar oder null
|
||||
var board: Dictionary = {} ## Vector2i -> Avatar
|
||||
@@ -111,6 +114,7 @@ func buy(index: int, pool: Pool, chosen_element: int = -1) -> bool:
|
||||
if not pool.take(element):
|
||||
return false
|
||||
gold -= entry.price()
|
||||
stats["spent"] += entry.price()
|
||||
bench[slot] = entry.to_avatar(element)
|
||||
entry.sold = true
|
||||
if entry.element < 0:
|
||||
@@ -123,8 +127,39 @@ func sell_bench(index: int, pool: Pool) -> bool:
|
||||
return false
|
||||
var a: Avatar = bench[index]
|
||||
pool.return_avatar(a)
|
||||
gold += maxi(1, a.invested)
|
||||
var refund := maxi(1, a.invested)
|
||||
gold += refund
|
||||
bench[index] = null
|
||||
last_sale = {"avatar": a, "refund": refund, "index": index}
|
||||
return true
|
||||
|
||||
|
||||
## Macht den letzten Bankverkauf rückgängig, solange Gold und Platz da sind.
|
||||
func undo_sell(pool: Pool) -> bool:
|
||||
if last_sale.is_empty():
|
||||
return false
|
||||
var a: Avatar = last_sale["avatar"]
|
||||
var refund: int = last_sale["refund"]
|
||||
if gold < refund:
|
||||
return false
|
||||
var idx: int = last_sale["index"]
|
||||
if bench[idx] != null:
|
||||
idx = free_bench_slot()
|
||||
if idx < 0:
|
||||
return false
|
||||
# Bausteine wieder aus dem Vorrat nehmen; ist er leer, geht es nicht.
|
||||
if not pool.take(a.elements[0]):
|
||||
return false
|
||||
if a.star >= 2:
|
||||
for i in 2:
|
||||
pool.take(a.elements[0])
|
||||
if a.star >= 3:
|
||||
for e in a.elements:
|
||||
for i in 3:
|
||||
pool.take(e)
|
||||
gold -= refund
|
||||
bench[idx] = a
|
||||
last_sale = {}
|
||||
return true
|
||||
|
||||
|
||||
@@ -338,6 +373,7 @@ func upgrade(hex: Vector2i, new_element: int, pool: Pool) -> bool:
|
||||
a.star += 1
|
||||
a.elements.append(new_element)
|
||||
unlock(new_element)
|
||||
stats["max_star"] = maxi(stats["max_star"], a.star)
|
||||
return true
|
||||
|
||||
|
||||
@@ -377,8 +413,10 @@ func register_result(won: bool) -> void:
|
||||
last_won = won
|
||||
if won:
|
||||
streak = maxi(1, streak + 1)
|
||||
stats["wins"] += 1
|
||||
else:
|
||||
streak = mini(-1, streak - 1)
|
||||
stats["losses"] += 1
|
||||
|
||||
|
||||
## Nur Leben abziehen. Das Ausscheiden setzt Game._check_eliminations,
|
||||
@@ -392,3 +430,63 @@ func board_units() -> Array:
|
||||
for hex in board:
|
||||
out.append(board[hex])
|
||||
return out
|
||||
|
||||
|
||||
# --- Speichern -------------------------------------------------------------
|
||||
|
||||
static func _avatar_to_dict(a: Avatar) -> Dictionary:
|
||||
return {"role": a.role, "elements": Array(a.elements), "star": a.star, "invested": a.invested}
|
||||
|
||||
|
||||
static func _avatar_from_dict(d: Dictionary) -> Avatar:
|
||||
var els: Array[int] = []
|
||||
for e in d["elements"]:
|
||||
els.append(int(e))
|
||||
var a := Avatar.new(int(d["role"]), els, int(d["star"]))
|
||||
a.invested = int(d["invested"])
|
||||
return a
|
||||
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
var b := []
|
||||
for a in bench:
|
||||
b.append(null if a == null else _avatar_to_dict(a))
|
||||
var brd := []
|
||||
for hex in board:
|
||||
brd.append({"x": hex.x, "y": hex.y, "a": _avatar_to_dict(board[hex])})
|
||||
var sh := []
|
||||
for e in shop:
|
||||
sh.append({"role": e.role, "element": e.element, "sold": e.sold})
|
||||
return {
|
||||
"pid": pid, "pname": pname, "is_bot": is_bot, "gold": gold, "hp": hp, "streak": streak,
|
||||
"alive": alive, "last_won": last_won, "placement": placement, "level": level, "xp": xp,
|
||||
"shop_locked": shop_locked, "last_element": last_element, "unlocked": Array(unlocked),
|
||||
"bench": b, "board": brd, "shop": sh, "stats": stats.duplicate(),
|
||||
}
|
||||
|
||||
|
||||
static func from_dict(d: Dictionary) -> PlayerState:
|
||||
var p := PlayerState.new(int(d["pid"]), d["pname"], d["is_bot"])
|
||||
p.gold = int(d["gold"])
|
||||
p.hp = int(d["hp"])
|
||||
p.streak = int(d["streak"])
|
||||
p.alive = d["alive"]
|
||||
p.last_won = d["last_won"]
|
||||
p.placement = int(d["placement"])
|
||||
p.level = int(d["level"])
|
||||
p.xp = int(d["xp"])
|
||||
p.shop_locked = d["shop_locked"]
|
||||
p.last_element = int(d["last_element"])
|
||||
for i in mini(p.unlocked.size(), d["unlocked"].size()):
|
||||
p.unlocked[i] = d["unlocked"][i]
|
||||
for i in mini(p.bench.size(), d["bench"].size()):
|
||||
p.bench[i] = null if d["bench"][i] == null else _avatar_from_dict(d["bench"][i])
|
||||
for entry in d["board"]:
|
||||
p.board[Vector2i(int(entry["x"]), int(entry["y"]))] = _avatar_from_dict(entry["a"])
|
||||
for entry in d["shop"]:
|
||||
var se := ShopEntry.new(int(entry["role"]), int(entry["element"]))
|
||||
se.sold = entry["sold"]
|
||||
p.shop.append(se)
|
||||
for k in d["stats"]:
|
||||
p.stats[k] = int(d["stats"][k])
|
||||
return p
|
||||
|
||||
24
scripts/core/Settings.gd
Normal file
24
scripts/core/Settings.gd
Normal file
@@ -0,0 +1,24 @@
|
||||
class_name Settings
|
||||
extends RefCounted
|
||||
|
||||
## Lautstärke und Schwierigkeit in user://settings.cfg.
|
||||
|
||||
const PATH := "user://settings.cfg"
|
||||
|
||||
var volume: float = 0.8
|
||||
var difficulty: int = Data.Difficulty.NORMAL
|
||||
|
||||
|
||||
func load_from_disk(path: String = PATH) -> void:
|
||||
var cfg := ConfigFile.new()
|
||||
if cfg.load(path) != OK:
|
||||
return
|
||||
volume = clampf(float(cfg.get_value("audio", "volume", volume)), 0.0, 1.0)
|
||||
difficulty = clampi(int(cfg.get_value("game", "difficulty", difficulty)), 0, 2)
|
||||
|
||||
|
||||
func save_to_disk(path: String = PATH) -> void:
|
||||
var cfg := ConfigFile.new()
|
||||
cfg.set_value("audio", "volume", volume)
|
||||
cfg.set_value("game", "difficulty", difficulty)
|
||||
cfg.save(path)
|
||||
1
scripts/core/Settings.gd.uid
Normal file
1
scripts/core/Settings.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://glddwd7k5wml
|
||||
Reference in New Issue
Block a user