Deckstärke, Schwierigkeitsgrade mit Archetypen, Gegnerdecks entlang der Energiekurve, Starter mit zwei seltenen Karten
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPgKvy5W9WRNFHjkm5QsHx
This commit is contained in:
@@ -2,34 +2,44 @@ class_name Campaign
|
||||
## Kampagne: zehn benannte Magier, jede Stufe mit mehr Gegnern am Tisch.
|
||||
|
||||
const STAGES := [
|
||||
{"name": "Fenn", "title": "Kobold-Zähmer", "elements": ["Feuer"], "extra": 0, "bias": 1.0, "reward": 100, "perk": "feuer_angriff"},
|
||||
{"name": "Mara", "title": "Gezeitenhexe", "elements": ["Wasser"], "extra": 0, "bias": 1.0, "reward": 100, "perk": "leben_35"},
|
||||
{"name": "Borin", "title": "Steinfaust", "elements": ["Erde"], "extra": 1, "bias": 1.2, "reward": 150, "perk": "waechter_leben"},
|
||||
{"name": "Liska", "title": "Windläuferin", "elements": ["Luft"], "extra": 1, "bias": 1.2, "reward": 150, "perk": "flink_angriff"},
|
||||
{"name": "Vulkar", "title": "Herr der Lava", "elements": ["Feuer", "Erde"], "extra": 2, "bias": 1.5, "reward": 200, "perk": "zauber_schaden"},
|
||||
{"name": "Nyx", "title": "Sturmruferin", "elements": ["Wasser", "Luft"], "extra": 2, "bias": 1.5, "reward": 200, "perk": "extra_karten"},
|
||||
{"name": "Ignis", "title": "Inferno-Priester", "elements": ["Feuer"], "extra": 3, "bias": 2.0, "reward": 250, "perk": "resonanz_doppelt"},
|
||||
{"name": "Thalassa", "title": "Tiefseekönigin", "elements": ["Wasser"], "extra": 3, "bias": 2.0, "reward": 250, "perk": "schild_wesen"},
|
||||
{"name": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "extra": 4, "bias": 2.5, "reward": 300, "perk": "alle_flink"},
|
||||
{"name": "Archimagier", "title": "Meister der Elemente", "elements": ["Erde", "Luft"], "extra": 4, "bias": 3.0, "reward": 500, "perk": "start_energie"},
|
||||
{"name": "Fenn", "title": "Kobold-Zähmer", "elements": ["Feuer"], "extra": 0, "reward": 100, "perk": "feuer_angriff"},
|
||||
{"name": "Mara", "title": "Gezeitenhexe", "elements": ["Wasser"], "extra": 0, "reward": 100, "perk": "leben_35"},
|
||||
{"name": "Borin", "title": "Steinfaust", "elements": ["Erde"], "extra": 1, "reward": 150, "perk": "waechter_leben"},
|
||||
{"name": "Liska", "title": "Windläuferin", "elements": ["Luft"], "extra": 1, "reward": 150, "perk": "flink_angriff"},
|
||||
{"name": "Vulkar", "title": "Herr der Lava", "elements": ["Feuer", "Erde"], "extra": 2, "reward": 200, "perk": "zauber_schaden"},
|
||||
{"name": "Nyx", "title": "Sturmruferin", "elements": ["Wasser", "Luft"], "extra": 2, "reward": 200, "perk": "extra_karten"},
|
||||
{"name": "Ignis", "title": "Inferno-Priester", "elements": ["Feuer"], "extra": 3, "reward": 250, "perk": "resonanz_doppelt"},
|
||||
{"name": "Thalassa", "title": "Tiefseekönigin", "elements": ["Wasser"], "extra": 3, "reward": 250, "perk": "schild_wesen"},
|
||||
{"name": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "extra": 4, "reward": 300, "perk": "alle_flink"},
|
||||
{"name": "Archimagier", "title": "Meister der Elemente", "elements": ["Erde", "Luft"], "extra": 4, "reward": 500, "perk": "start_energie"},
|
||||
]
|
||||
|
||||
|
||||
## Kapitel 2: die Magier kehren mit zwei Merkmalen und mehr Gefolge zurück.
|
||||
const STAGES2 := [
|
||||
{"name": "Fenn", "title": "Flammenzähmer", "elements": ["Feuer"], "extra": 1, "bias": 2.0, "reward": 300, "perks": ["feuer_angriff", "start_energie"]},
|
||||
{"name": "Mara", "title": "Herrin der Gezeiten", "elements": ["Wasser"], "extra": 1, "bias": 2.0, "reward": 300, "perks": ["leben_35", "extra_karten"]},
|
||||
{"name": "Borin", "title": "Fels der Ewigkeit", "elements": ["Erde"], "extra": 2, "bias": 2.2, "reward": 350, "perks": ["waechter_leben", "schild_wesen"]},
|
||||
{"name": "Liska", "title": "Sturmläuferin", "elements": ["Luft"], "extra": 2, "bias": 2.2, "reward": 350, "perks": ["flink_angriff", "alle_flink"]},
|
||||
{"name": "Vulkar", "title": "Aschekönig", "elements": ["Feuer", "Erde"], "extra": 3, "bias": 2.5, "reward": 400, "perks": ["zauber_schaden", "feuer_angriff"]},
|
||||
{"name": "Nyx", "title": "Gewitterruferin", "elements": ["Wasser", "Luft"], "extra": 3, "bias": 2.5, "reward": 400, "perks": ["extra_karten", "flink_angriff"]},
|
||||
{"name": "Ignis", "title": "Inferno-Erzpriester", "elements": ["Feuer"], "extra": 4, "bias": 3.0, "reward": 450, "perks": ["resonanz_doppelt", "zauber_schaden"]},
|
||||
{"name": "Thalassa", "title": "Kaiserin der Tiefe", "elements": ["Wasser"], "extra": 4, "bias": 3.0, "reward": 450, "perks": ["schild_wesen", "leben_35"]},
|
||||
{"name": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "extra": 5, "bias": 3.5, "reward": 500, "perks": ["alle_flink", "start_energie"]},
|
||||
{"name": "Archimagier", "title": "Herr aller Elemente", "elements": ["Erde", "Luft"], "extra": 5, "bias": 4.0, "reward": 800, "perks": ["start_energie", "resonanz_doppelt", "extra_karten"]},
|
||||
{"name": "Fenn", "title": "Flammenzähmer", "elements": ["Feuer"], "extra": 1, "reward": 300, "perks": ["feuer_angriff", "start_energie"]},
|
||||
{"name": "Mara", "title": "Herrin der Gezeiten", "elements": ["Wasser"], "extra": 1, "reward": 300, "perks": ["leben_35", "extra_karten"]},
|
||||
{"name": "Borin", "title": "Fels der Ewigkeit", "elements": ["Erde"], "extra": 2, "reward": 350, "perks": ["waechter_leben", "schild_wesen"]},
|
||||
{"name": "Liska", "title": "Sturmläuferin", "elements": ["Luft"], "extra": 2, "reward": 350, "perks": ["flink_angriff", "alle_flink"]},
|
||||
{"name": "Vulkar", "title": "Aschekönig", "elements": ["Feuer", "Erde"], "extra": 3, "reward": 400, "perks": ["zauber_schaden", "feuer_angriff"]},
|
||||
{"name": "Nyx", "title": "Gewitterruferin", "elements": ["Wasser", "Luft"], "extra": 3, "reward": 400, "perks": ["extra_karten", "flink_angriff"]},
|
||||
{"name": "Ignis", "title": "Inferno-Erzpriester", "elements": ["Feuer"], "extra": 4, "reward": 450, "perks": ["resonanz_doppelt", "zauber_schaden"]},
|
||||
{"name": "Thalassa", "title": "Kaiserin der Tiefe", "elements": ["Wasser"], "extra": 4, "reward": 450, "perks": ["schild_wesen", "leben_35"]},
|
||||
{"name": "Zephyra", "title": "Blitzkaiserin", "elements": ["Feuer", "Luft"], "extra": 5, "reward": 500, "perks": ["alle_flink", "start_energie"]},
|
||||
{"name": "Archimagier", "title": "Herr aller Elemente", "elements": ["Erde", "Luft"], "extra": 5, "reward": 800, "perks": ["start_energie", "resonanz_doppelt", "extra_karten"]},
|
||||
]
|
||||
|
||||
|
||||
# Mindeststärke der Gegnerdecks je Stufe; nie unter der Stärke des eigenen Decks.
|
||||
const MIN_RATING := [1.2, 1.3, 1.5, 1.6, 1.8, 1.9, 2.1, 2.2, 2.4, 2.6]
|
||||
const MIN_RATING2 := [2.2, 2.3, 2.5, 2.6, 2.8, 2.9, 3.0, 3.1, 3.2, 3.4]
|
||||
|
||||
|
||||
static func target_rating(stage_index: int, player_rating: float) -> float:
|
||||
var base: float = MIN_RATING[stage_index] if stage_index < STAGES.size() else MIN_RATING2[stage_index - STAGES.size()]
|
||||
return maxf(base, player_rating)
|
||||
|
||||
|
||||
static func total() -> int:
|
||||
return STAGES.size() + STAGES2.size()
|
||||
|
||||
@@ -47,13 +57,15 @@ static func stage(i: int) -> Dictionary:
|
||||
return STAGES2[i - STAGES.size()]
|
||||
|
||||
|
||||
static func opponents(stage_index: int, r: RandomNumberGenerator) -> Array:
|
||||
## Gegner einer Stufe, Decks auf die Zielstärke (Stufe oder eigenes Deck, was höher ist).
|
||||
static func opponents(stage_index: int, r: RandomNumberGenerator, player_rating: float = 0.0) -> Array:
|
||||
var st := stage(stage_index)
|
||||
var target := target_rating(stage_index, player_rating)
|
||||
var life: int = 35 if "leben_35" in st.perks else Game.START_LIFE
|
||||
var boss := Game.Player.new(st.name, Game.build_opponent_deck(r, st.elements, st.bias), false, life, st.title)
|
||||
var boss := Game.Player.new(st.name, Game.build_opponent_deck_rated(r, st.elements, target), false, life, st.title)
|
||||
boss.perks = Array(st.perks)
|
||||
var out := [boss]
|
||||
var extra: int = mini(st.extra, Game.MAX_OPPONENTS - 1)
|
||||
for k in extra:
|
||||
out.append(Game.random_opponent(r, k + 1))
|
||||
out.append(Game.random_opponent(r, k + 1, Game.START_LIFE, target))
|
||||
return out
|
||||
|
||||
@@ -52,6 +52,8 @@ const KEYWORD_HELP := {
|
||||
"Doppelschlag": "Greift zweimal pro Zug an.",
|
||||
}
|
||||
const CHOSEN_TARGETS: Array[String] = ["enemy_creature", "enemy_any", "friendly_creature"]
|
||||
# Näherung der Kartenstärke: Seltenheit plus Zuschläge für Schlüsselwörter und Effekte.
|
||||
const RARITY_POINTS := {"gewöhnlich": 1.0, "selten": 2.0, "episch": 3.0, "legendär": 4.0, "mythisch": 5.0}
|
||||
const DATA_PATH := "res://data/karten.json"
|
||||
|
||||
|
||||
@@ -274,6 +276,10 @@ static func has_id(id) -> bool:
|
||||
return _by_id.has(id)
|
||||
|
||||
|
||||
static func card_value(c: Card) -> float:
|
||||
return RARITY_POINTS[c.rarity] + 0.3 * c.keywords.size() + 0.3 * c.effects.size()
|
||||
|
||||
|
||||
static func of_rarity(r: String) -> Array:
|
||||
return all().filter(func(c): return c.rarity == r)
|
||||
|
||||
|
||||
114
scripts/game.gd
114
scripts/game.gd
@@ -778,43 +778,115 @@ static func validate_deck(cards: Array, owned = null) -> Array:
|
||||
return [true, ""]
|
||||
|
||||
|
||||
## Gegnerdeck aus den Elementen. bias > 1 bevorzugt seltene Karten (Kampagne).
|
||||
## Deckstärke: Durchschnitt der Kartenwerte (1 = nur gewöhnliche Karten, bis etwa 3,5).
|
||||
static func deck_rating(cards: Array) -> float:
|
||||
if cards.is_empty():
|
||||
return 0.0
|
||||
var sum := 0.0
|
||||
for c in cards:
|
||||
sum += Cards.card_value(c)
|
||||
return sum / cards.size()
|
||||
|
||||
|
||||
## Gegnerdeck auf eine Zielstärke: die Gewichtung wird angepasst, bis das Deck nahe am Ziel liegt.
|
||||
static func build_opponent_deck_rated(r: RandomNumberGenerator, elements: Array, target: float, tolerance: float = 0.12) -> Array:
|
||||
var bias := 1.0
|
||||
var best := []
|
||||
var best_diff := 99.0
|
||||
for attempt in 24:
|
||||
var deck := build_opponent_deck(r, elements, bias)
|
||||
var diff := deck_rating(deck) - target
|
||||
if absf(diff) < best_diff:
|
||||
best = deck
|
||||
best_diff = absf(diff)
|
||||
if absf(diff) <= tolerance:
|
||||
break
|
||||
bias = clampf(bias * (1.35 if diff < 0 else 1.0 / 1.35), 0.05, 20.0)
|
||||
return best
|
||||
|
||||
|
||||
# Gemessene Stärke der Elementkombinationen (Siegquote gegen das Feld, KI gegen KI, tests/strength_probe.gd).
|
||||
const ARCHETYPE_STRENGTH := {
|
||||
"Feuer": 33, "Wasser": 20, "Erde": 53, "Luft": 46,
|
||||
"Feuer+Wasser": 46, "Feuer+Erde": 60, "Feuer+Luft": 60,
|
||||
"Wasser+Erde": 23, "Wasser+Luft": 36, "Erde+Luft": 66,
|
||||
}
|
||||
|
||||
|
||||
## Elementkombinationen einer Stufe: "leicht" die schwächere Hälfte, "schwer" die stärkere, sonst alle.
|
||||
static func archetypes(tier: String = "") -> Array:
|
||||
var keys: Array = ARCHETYPE_STRENGTH.keys()
|
||||
keys.sort_custom(func(a, b): return ARCHETYPE_STRENGTH[a] < ARCHETYPE_STRENGTH[b])
|
||||
var half: int = keys.size() / 2
|
||||
match tier:
|
||||
"leicht":
|
||||
return keys.slice(0, half)
|
||||
"schwer":
|
||||
return keys.slice(half)
|
||||
return keys
|
||||
|
||||
|
||||
# Energiekurve der Gegnerdecks: Anzahl Karten je Kosten (6 = sechs und mehr).
|
||||
const CURVE := {1: 3, 2: 5, 3: 4, 4: 3, 5: 2, 6: 3}
|
||||
|
||||
|
||||
## Gegnerdeck aus den Elementen entlang der Energiekurve. bias > 1 bevorzugt seltene Karten, bias < 1 gewöhnliche.
|
||||
static func build_opponent_deck(r: RandomNumberGenerator, elements: Array, bias: float = 1.0) -> Array:
|
||||
var pool := Cards.all().filter(func(c): return c.elements.all(func(e): return e in elements))
|
||||
var weights := PackedFloat32Array()
|
||||
for c in pool:
|
||||
var w: float = OPPONENT_RARITY_WEIGHT[c.rarity]
|
||||
weights.append(w / bias if c.rarity == "gewöhnlich" else w * bias)
|
||||
var deck := []
|
||||
var counts := {}
|
||||
for bucket in CURVE:
|
||||
for i in CURVE[bucket]:
|
||||
var c = _pick_for_cost(r, pool, counts, bucket, bias)
|
||||
if c != null:
|
||||
counts[c.id] = counts.get(c.id, 0) + 1
|
||||
deck.append(c)
|
||||
while deck.size() < DECK_SIZE:
|
||||
var c = pool[r.rand_weighted(weights)]
|
||||
if counts.get(c.id, 0) < MAX_COPIES:
|
||||
counts[c.id] = counts.get(c.id, 0) + 1
|
||||
deck.append(c)
|
||||
var c = _pick_for_cost(r, pool, counts, r.randi_range(1, 6), bias)
|
||||
if c == null:
|
||||
break
|
||||
counts[c.id] = counts.get(c.id, 0) + 1
|
||||
deck.append(c)
|
||||
return deck
|
||||
|
||||
|
||||
static func random_opponent(r: RandomNumberGenerator, index: int, life: int = START_LIFE) -> Player:
|
||||
var elements: Array
|
||||
if r.randf() < 0.4:
|
||||
elements = [Cards.ELEMENTS[r.randi_range(0, Cards.ELEMENTS.size() - 1)]]
|
||||
else:
|
||||
var pool: Array = Cards.ELEMENTS.duplicate()
|
||||
shuffle(pool, r)
|
||||
elements = [pool[0], pool[1]]
|
||||
return Player.new("Gegner %d" % index, build_opponent_deck(r, elements), false, life,
|
||||
Cards.combo_name(elements) + "-Magier")
|
||||
## Karte nahe den gewünschten Kosten, gewichtet nach Seltenheit (bias) und Kostenabstand.
|
||||
## Bei extremer Seltenheitsvorgabe darf die Kurve nachgeben, sonst wäre das Ziel unerreichbar.
|
||||
static func _pick_for_cost(r: RandomNumberGenerator, pool: Array, counts: Dictionary, cost: int, bias: float):
|
||||
var candidates := []
|
||||
var weights := PackedFloat32Array()
|
||||
for c in pool:
|
||||
if counts.get(c.id, 0) >= MAX_COPIES:
|
||||
continue
|
||||
var spread: int = absi(mini(c.cost, 6) - cost)
|
||||
var w: float = OPPONENT_RARITY_WEIGHT[c.rarity]
|
||||
w = w / bias if c.rarity == "gewöhnlich" else w * bias
|
||||
w *= pow(0.3, spread)
|
||||
candidates.append(c)
|
||||
weights.append(w)
|
||||
if candidates.is_empty():
|
||||
return null
|
||||
return candidates[r.rand_weighted(weights)]
|
||||
|
||||
|
||||
## rules: {"human_perks": [...], "everyone_perk": String, "start_life": int}
|
||||
## Zufälliger Gegner. target > 0: Deck auf diese Stärke gebaut. tier: Archetypen der Schwierigkeit.
|
||||
static func random_opponent(r: RandomNumberGenerator, index: int, life: int = START_LIFE, target: float = 0.0, tier: String = "") -> Player:
|
||||
var options := archetypes(tier)
|
||||
var elements: Array = options[r.randi_range(0, options.size() - 1)].split("+")
|
||||
var deck := build_opponent_deck_rated(r, elements, target) if target > 0.0 else build_opponent_deck(r, elements)
|
||||
return Player.new("Gegner %d" % index, deck, false, life, Cards.combo_name(elements) + "-Magier")
|
||||
|
||||
|
||||
## rules: {"human_perks": [...], "everyone_perk": String, "start_life": int, "target_rating": float, "tier": String}
|
||||
static func new_battle(deck: Array, n_opp: int, r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game:
|
||||
assert(n_opp >= 1 and n_opp <= MAX_OPPONENTS, "1 bis 5 Gegner")
|
||||
if r == null:
|
||||
r = make_rng()
|
||||
var target: float = float(rules.get("target_rating", 0.0))
|
||||
var tier: String = str(rules.get("tier", ""))
|
||||
var opps := []
|
||||
for i in n_opp:
|
||||
opps.append(random_opponent(r, i + 1))
|
||||
opps.append(random_opponent(r, i + 1, START_LIFE, target, tier))
|
||||
return new_battle_with(deck, opps, r, false, rules)
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,12 @@ const START_COINS := 150
|
||||
const REWARD_WIN := {1: 100, 2: 150, 3: 200, 4: 250, 5: 300}
|
||||
const REWARD_LOSS := 50
|
||||
const DECK_SLOTS := 5
|
||||
# Schwierigkeit: Faktor auf die Zielstärke der Gegnerdecks, Belohnung und KI-Tiefe.
|
||||
const DIFFICULTIES := {
|
||||
"leicht": {"name": "Leicht", "factor": 0.9, "reward": 0.75, "smart": false, "tier": "leicht"},
|
||||
"normal": {"name": "Normal", "factor": 1.0, "reward": 1.0, "smart": true, "tier": ""},
|
||||
"schwer": {"name": "Schwer", "factor": 1.2, "reward": 1.5, "smart": true, "tier": "schwer"},
|
||||
}
|
||||
# Werkstatt: Zerlegen bringt Staub, Herstellen kostet Staub.
|
||||
const DUST_DISMANTLE := {"gewöhnlich": 5, "selten": 20, "episch": 100, "legendär": 400, "mythisch": 1600}
|
||||
const DUST_CRAFT := {"gewöhnlich": 40, "selten": 100, "episch": 400, "legendär": 1600, "mythisch": 3200}
|
||||
@@ -21,7 +27,7 @@ const ACHIEVEMENTS := [
|
||||
{"id": "duellant", "name": "Duellant", "text": "Gewinne ein Duell.", "reward": 50},
|
||||
{"id": "tischherr", "name": "Herr des Tisches", "text": "Gewinne gegen 3 oder mehr Gegner.", "reward": 100},
|
||||
{"id": "chaosmeister", "name": "Chaosmeister", "text": "Gewinne gegen 5 Gegner.", "reward": 200},
|
||||
{"id": "sammler", "name": "Sammler", "text": "Besitze 40 verschiedene Karten.", "reward": 100},
|
||||
{"id": "sammler", "name": "Sammler", "text": "Besitze 50 verschiedene Karten.", "reward": 100},
|
||||
{"id": "vollstaendig", "name": "Vollständig", "text": "Besitze jede Karte.", "reward": 500},
|
||||
{"id": "boosterfreund", "name": "Boosterfreund", "text": "Öffne 10 Booster.", "reward": 100},
|
||||
{"id": "elementarist", "name": "Elementarist", "text": "Gewinne mit einem Deck aus nur einem Element.", "reward": 100},
|
||||
@@ -58,7 +64,7 @@ var stats := {"games": 0, "wins": 0, "boosters": 0, "wins_by_opp": {}}
|
||||
var achievements: Array = []
|
||||
var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen
|
||||
var daily := {"date": "", "task": "", "progress": 0, "done": false}
|
||||
var settings := {"volume": 0.8, "music": 0.5, "ai_speed": 1.0, "animations": true, "language": "de"}
|
||||
var settings := {"volume": 0.8, "music": 0.5, "ai_speed": 1.0, "animations": true, "language": "de", "difficulty": "normal"}
|
||||
var notices: Array[String] = [] # Meldungen für die Oberfläche (Erfolge, Tagesaufgabe)
|
||||
var card_stats := {} # id -> {"played", "games", "wins"}
|
||||
var booster_since_mythic := 0
|
||||
@@ -86,11 +92,16 @@ static func starter() -> Profile:
|
||||
for c in Cards.of_rarity("gewöhnlich"):
|
||||
if c.id.begins_with("f_") or c.id.begins_with("w_") or c.id.begins_with("e_") or c.id.begins_with("l_"):
|
||||
p.collection[c.id] = 2
|
||||
# Zwei seltene Karten zum Start, damit es etwas zum Aufbauen gibt
|
||||
p.collection["f_drache"] = 1
|
||||
p.collection["e_baum"] = 1
|
||||
var starter_ids := ["f_funke", "f_kobold", "f_wolf", "f_feuerball", "f_asche",
|
||||
"e_wall", "e_wurzel", "e_haerten", "e_golem", "e_troll"]
|
||||
for cid in starter_ids:
|
||||
p.deck.append(cid)
|
||||
p.deck.append(cid)
|
||||
p.deck[p.deck.find("f_asche")] = "f_drache"
|
||||
p.deck[p.deck.find("e_troll")] = "e_baum"
|
||||
return p
|
||||
|
||||
|
||||
@@ -147,7 +158,9 @@ static func from_dict(d: Dictionary) -> Profile:
|
||||
var se = d.get("settings", {})
|
||||
p.settings = {"volume": float(se.get("volume", 0.8)), "music": float(se.get("music", 0.5)),
|
||||
"ai_speed": float(se.get("ai_speed", 1.0)), "animations": bool(se.get("animations", true)),
|
||||
"language": str(se.get("language", "de"))}
|
||||
"language": str(se.get("language", "de")), "difficulty": str(se.get("difficulty", "normal"))}
|
||||
if not DIFFICULTIES.has(p.settings.difficulty):
|
||||
p.settings.difficulty = "normal"
|
||||
if version >= 4:
|
||||
for cid in d.get("card_stats", {}):
|
||||
if Cards.has_id(cid):
|
||||
@@ -363,7 +376,7 @@ func open_booster(r: RandomNumberGenerator = null) -> Array:
|
||||
## stage >= 0: Kampagnenstufe; ein erster Sieg schaltet die nächste Stufe frei.
|
||||
## turns: Dauer der Partie für die Bestenliste. challenge: Herausforderung dieser Woche.
|
||||
func record_battle(won: bool, n_opp: int, deck_used: Array, stage: int = -1, played_ids: Array = [],
|
||||
turns: int = 0, challenge: Dictionary = {}) -> int:
|
||||
turns: int = 0, challenge: Dictionary = {}, reward_factor: float = 1.0) -> int:
|
||||
stats.games += 1
|
||||
if won:
|
||||
records.streak += 1
|
||||
@@ -386,7 +399,7 @@ func record_battle(won: bool, n_opp: int, deck_used: Array, stage: int = -1, pla
|
||||
stats.wins += 1
|
||||
var key := str(n_opp)
|
||||
stats.wins_by_opp[key] = int(stats.wins_by_opp.get(key, 0)) + 1
|
||||
reward = REWARD_WIN[n_opp]
|
||||
reward = int(round(REWARD_WIN[n_opp] * reward_factor))
|
||||
if stage >= 0 and stage == campaign_stage:
|
||||
campaign_stage += 1
|
||||
reward = Campaign.stage(stage).reward
|
||||
@@ -440,7 +453,7 @@ func _achieved(id: String, ctx: Dictionary) -> bool:
|
||||
"chaosmeister":
|
||||
return won and n == 5
|
||||
"sammler":
|
||||
return distinct_owned() >= 40
|
||||
return distinct_owned() >= 50
|
||||
"vollstaendig":
|
||||
return distinct_owned() >= Cards.all().size()
|
||||
"boosterfreund":
|
||||
@@ -460,6 +473,16 @@ func _achieved(id: String, ctx: Dictionary) -> bool:
|
||||
return false
|
||||
|
||||
|
||||
func difficulty() -> Dictionary:
|
||||
return DIFFICULTIES.get(settings.get("difficulty", "normal"), DIFFICULTIES.normal)
|
||||
|
||||
|
||||
## Zielstärke der Gegnerdecks: eigene Deckstärke mal Schwierigkeitsfaktor.
|
||||
func target_rating(cards: Array = []) -> float:
|
||||
var deck_used: Array = cards if not cards.is_empty() else deck_cards()
|
||||
return Game.deck_rating(deck_used) * float(difficulty().factor)
|
||||
|
||||
|
||||
func perk_unlocked() -> bool:
|
||||
return campaign_stage >= Campaign.STAGES.size()
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ var mulligan_selected: Array = []
|
||||
var tutorial_step := 0
|
||||
var challenge := {} # wöchentliche Herausforderung oder leer
|
||||
var skip_ai := false # Gegnerzüge im Schnelldurchlauf
|
||||
var smart_ai := true # Vorausschau-KI (Leicht: einfache KI)
|
||||
var undo_state: Game = null # Zustand vor der letzten rückgängig machbaren Aktion
|
||||
var undo_log_len := 0
|
||||
var undo_t := 0.0
|
||||
@@ -170,25 +171,29 @@ func _ready() -> void:
|
||||
game = Game.new_hotseat(hotseat_decks, hotseat_ai)
|
||||
elif tutorial:
|
||||
var r := Game.make_rng()
|
||||
var opps := Campaign.opponents(0, r)
|
||||
var opps := Campaign.opponents(0, r, Game.deck_rating(app.profile.deck_cards()))
|
||||
opps[0].perks = []
|
||||
game = Game.new_battle_with(app.profile.deck_cards(), opps, r, true)
|
||||
elif not challenge.is_empty():
|
||||
game = Game.new_battle(app.profile.deck_cards(), n_opp, null, challenge.rules)
|
||||
var rules: Dictionary = challenge.rules.duplicate()
|
||||
rules["target_rating"] = Game.deck_rating(app.profile.deck_cards())
|
||||
game = Game.new_battle(app.profile.deck_cards(), n_opp, null, rules)
|
||||
elif stage >= 0:
|
||||
var r := Game.make_rng()
|
||||
game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r), r, false, own_rules())
|
||||
game = Game.new_battle_with(app.profile.deck_cards(), Campaign.opponents(stage, r, app.profile.target_rating()), r, false, own_rules())
|
||||
else:
|
||||
game = Game.new_battle(app.profile.deck_cards(), n_opp, null, own_rules())
|
||||
smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile()
|
||||
_begin()
|
||||
|
||||
|
||||
## Eigenes Merkmal des Spielers (nach Kapitel 1 wählbar).
|
||||
## Eigenes Merkmal des Spielers (nach Kapitel 1 wählbar) und Zielstärke der Gegner.
|
||||
func own_rules() -> Dictionary:
|
||||
var p: Profile = app.profile
|
||||
var rules := {"target_rating": p.target_rating(), "tier": str(p.difficulty().tier)}
|
||||
if p.perk != "" and p.perk_unlocked():
|
||||
return {"human_perks": [p.perk]}
|
||||
return {}
|
||||
rules["human_perks"] = [p.perk]
|
||||
return rules
|
||||
|
||||
|
||||
func _begin() -> void:
|
||||
@@ -491,7 +496,7 @@ func _process(delta: float) -> void:
|
||||
ai_timer -= delta
|
||||
if ai_timer > 0:
|
||||
return
|
||||
var action := AI.choose_action(game, cur)
|
||||
var action := AI.choose_action(game, cur) if smart_ai else AI.choose_action_greedy(game, cur)
|
||||
AI.apply_action(game, cur, action)
|
||||
ai_timer = ai_delay * (0.6 if action.type == "end" else 1.0)
|
||||
if game.current().is_human:
|
||||
@@ -502,7 +507,8 @@ func _process(delta: float) -> void:
|
||||
func _finish() -> void:
|
||||
var won := game.winner == "human"
|
||||
if counts_for_profile():
|
||||
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, challenge)
|
||||
var factor: float = float(app.profile.difficulty().reward) if stage < 0 and challenge.is_empty() and not tutorial else 1.0
|
||||
reward = app.profile.record_battle(won, n_opp, app.profile.deck_cards(), stage, played_ids, game.turn, challenge, factor)
|
||||
if tutorial:
|
||||
app.profile.tutorial_done = true
|
||||
app.save()
|
||||
|
||||
@@ -340,10 +340,13 @@ func refresh() -> void:
|
||||
if not name_box.has_focus():
|
||||
name_box.text = p.deck_name(slot)
|
||||
deck_label.text = "%d/%d" % [deck.size(), Game.DECK_SIZE]
|
||||
deck_label.tooltip_text = tr("Deckstärke: Durchschnitt aus Seltenheit, Schlüsselwörtern und Effekten")
|
||||
deck_label.add_theme_color_override("font_color", Style.GREEN if deck.size() == Game.DECK_SIZE else Style.TEXT)
|
||||
var elems := Game.deck_elements(dc)
|
||||
elems.sort_custom(func(a, b): return Cards.ELEMENTS.find(a) < Cards.ELEMENTS.find(b))
|
||||
elems_label.text = "Elemente: " + (", ".join(elems) if not elems.is_empty() else "–")
|
||||
if not dc.is_empty():
|
||||
elems_label.text += " · " + tr("Stärke %.1f") % Game.deck_rating(dc)
|
||||
var ok: Array = Game.validate_deck(dc, p.collection)
|
||||
valid_label.text = "spielbar" if ok[0] else ok[1]
|
||||
valid_label.add_theme_color_override("font_color", Style.GREEN if ok[0] else Style.ORANGE)
|
||||
|
||||
@@ -1,27 +1,56 @@
|
||||
class_name OpponentSelectScreen
|
||||
extends Control
|
||||
## Einzelspieler: Auswahl, gegen wie viele Gegner gekämpft wird.
|
||||
## Einzelspieler: Schwierigkeit und Gegnerzahl. Gegnerdecks werden auf die eigene Deckstärke gebaut.
|
||||
|
||||
var app
|
||||
var rating_label: Label
|
||||
var diff_buttons := {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var col := Style.column(self, 900, 50, 14)
|
||||
var col := Style.column(self, 900, 40, 12)
|
||||
Style.clabel(col, "Einzelspieler", 36, Style.GOLD, "center", true)
|
||||
Style.clabel(col, "Wie viele Gegner? Jeder kämpft gegen jeden, alle nach denselben Regeln mit %d Leben." % Game.START_LIFE, 18, Style.MUTED)
|
||||
Style.clabel(col, "Du gewinnst, wenn alle anderen besiegt sind. Mehr Gegner, mehr Chaos, mehr Münzen.", 16, Style.MUTED)
|
||||
Style.spacer(col, 10)
|
||||
Style.clabel(col, "Jeder kämpft gegen jeden, alle nach denselben Regeln mit %d Leben. Du gewinnst, wenn alle anderen besiegt sind." % Game.START_LIFE, 16, Style.MUTED)
|
||||
Style.spacer(col, 4)
|
||||
Style.clabel(col, "Schwierigkeit", 20, Style.TEXT, "center", true)
|
||||
var dr := Style.row(col, 10)
|
||||
dr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
for key in ["leicht", "normal", "schwer"]:
|
||||
var k: String = key
|
||||
diff_buttons[k] = Style.cbutton(dr, Profile.DIFFICULTIES[k].name, Vector2(150, 42), func(): set_difficulty(k))
|
||||
rating_label = Style.clabel(col, "", 15, Style.MUTED)
|
||||
Style.spacer(col, 4)
|
||||
for i in range(1, Game.MAX_OPPONENTS + 1):
|
||||
var n := i
|
||||
var r := Style.row(col, 20)
|
||||
r.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(r, "%d Gegner" % n, Vector2(200, 56), func(): app.show_screen(BattleScreen.new(n)), Color("285a3c"))
|
||||
Style.cbutton(r, "%d Gegner" % n, Vector2(200, 50), func(): app.show_screen(BattleScreen.new(n)), Color("285a3c"))
|
||||
var info := ("Duell" if n == 1 else "%d Spieler am Tisch" % (n + 1)) + " · Sieg: %d Münzen" % Profile.REWARD_WIN[n]
|
||||
Style.clabel(r, info, 16, Style.MUTED, "left", false, 420)
|
||||
Style.spacer(col, 20)
|
||||
Style.spacer(col, 10)
|
||||
var br := Style.row(col)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
Style.cbutton(br, "Zurück", Vector2(160, 44), func(): app.show_screen(MenuScreen.new()))
|
||||
refresh()
|
||||
|
||||
|
||||
func set_difficulty(key: String) -> void:
|
||||
app.profile.settings.difficulty = key
|
||||
app.save()
|
||||
refresh()
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
var d := p.difficulty()
|
||||
for key in diff_buttons:
|
||||
var b: Button = diff_buttons[key]
|
||||
var active: bool = key == p.settings.difficulty
|
||||
b.add_theme_stylebox_override("normal", Style.box(Color("285a3c") if active else Style.PANEL_LIGHT, 8, Style.GOLD if active else Style.MUTED, 2))
|
||||
b.add_theme_stylebox_override("hover", Style.box(Style.lighter(Color("285a3c") if active else Style.PANEL_LIGHT), 8, Style.GOLD if active else Style.MUTED, 2))
|
||||
var arche: String = tr("schwache Archetypen") if d.tier == "leicht" else (tr("starke Archetypen") if d.tier == "schwer" else tr("alle Archetypen"))
|
||||
rating_label.text = tr("Deine Deckstärke %.1f · Gegnerdecks %.1f · %s · Münzen x%.2f · KI: %s") % [
|
||||
Game.deck_rating(p.deck_cards()), p.target_rating(), arche, d.reward, tr("Vorausschau") if d.smart else tr("einfach")]
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
|
||||
@@ -268,6 +268,8 @@ static func rules_text() -> String:
|
||||
lines.append("Werkstatt: Karten zerlegen bringt Staub, aus Staub entstehen neue Karten. Fünf Deck-Slots.")
|
||||
lines.append("Lokal zu mehreren: bis zu vier Menschen am selben Rechner, dazu KI, ohne Münzen.")
|
||||
lines.append("Mulligan: Im ersten eigenen Zug darfst du einmal Handkarten zurücklegen und nachziehen.")
|
||||
lines.append("Deckstärke: Seltenheit, Schlüsselwörter und Effekte ergeben je Karte einen Wert. Gegnerdecks werden auf deine")
|
||||
lines.append(" Stärke gebaut (Leicht 0,9-fach, Normal 1-fach, Schwer 1,2-fach mit mehr Münzen). Die Kampagne liegt nie darunter.")
|
||||
lines.append("Kampagnen-Bosse haben ein Merkmal (★), zum Beispiel stärkere Feuerwesen. Netzwerkspiel im lokalen Netz.")
|
||||
lines.append("Steuerung: Linksklick spielt Karten und wählt Ziele, Rechtsklick oder Esc bricht ab, Leertaste beendet den Zug, 1 bis 9 spielen Handkarten.")
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -138,8 +138,8 @@ func refresh() -> void:
|
||||
preview_holder.add_child(v)
|
||||
var owned: int = p.collection.get(selected.id, 0)
|
||||
var st: Dictionary = p.card_stats.get(selected.id, {"played": 0, "games": 0, "wins": 0})
|
||||
info_label.text = "%s · %s\nIm Besitz: %dx · Gespielt %dx · %d Partien, %d Siege" % [
|
||||
selected.combo(), selected.rarity, owned, st.played, st.games, st.wins]
|
||||
info_label.text = "%s · %s · Wert %.1f\nIm Besitz: %dx · Gespielt %dx · %d Partien, %d Siege" % [
|
||||
selected.combo(), selected.rarity, Cards.card_value(selected), owned, st.played, st.games, st.wins]
|
||||
b_dismantle.text = "Zerlegen (+%d Staub)" % Profile.DUST_DISMANTLE[selected.rarity]
|
||||
b_craft.text = "Herstellen (%d Staub)" % Profile.DUST_CRAFT[selected.rarity]
|
||||
b_dismantle.disabled = owned == 0
|
||||
|
||||
Reference in New Issue
Block a user