update
This commit is contained in:
@@ -201,7 +201,7 @@ static func choose_action(game: Game, p: Game.Player) -> Dictionary:
|
||||
best = firsts[i].action
|
||||
if best.type == "end":
|
||||
if game.can_draw(p)[0]:
|
||||
return {"type": "draw", "index": pick_index(game, p)} # Mana übrig und Platz auf der Hand: lieber ziehen als passen
|
||||
return {"type": "draw"} # Mana übrig und Platz auf der Hand: lieber ziehen als passen
|
||||
return best
|
||||
return map_action(game, view, best)
|
||||
|
||||
@@ -258,24 +258,10 @@ static func choose_action_greedy(game: Game, p: Game.Player) -> Dictionary:
|
||||
if t != null:
|
||||
return {"type": "attack", "creature": cr, "target": t}
|
||||
if game.can_draw(p)[0]:
|
||||
return {"type": "draw", "index": pick_index(game, p)}
|
||||
return {"type": "draw"}
|
||||
return {"type": "end"}
|
||||
|
||||
|
||||
## Welche der offenen Karten: die wertvollste, die nach dem Ziehen noch bezahlbar wäre, sonst die billigste.
|
||||
static func pick_index(game: Game, p: Game.Player) -> int:
|
||||
var left: int = p.energy - game.draw_cost(p)
|
||||
var best := 0
|
||||
var best_score := -INF
|
||||
for i in p.offer.size():
|
||||
var c: Cards.Card = p.offer[i]
|
||||
var score := float(c.attack + c.health) if c.cost <= left else -10.0 - c.cost
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best = i
|
||||
return best
|
||||
|
||||
|
||||
## Ziel eines Angriffs: sichere Abtausche, dann gute Abtausche, dann das schwächste Wesen;
|
||||
## der Held nur, wenn kein Wesen mehr steht.
|
||||
static func pick_attack_target(game: Game, p: Game.Player, cr: Game.Creature):
|
||||
@@ -302,7 +288,7 @@ static func apply_action(game: Game, p: Game.Player, action: Dictionary) -> void
|
||||
"play":
|
||||
game.play_card(p, action.card, bool(action.get("face_down", false)))
|
||||
"draw":
|
||||
game.draw(p, int(action.get("index", 0)))
|
||||
game.draw(p)
|
||||
"reveal":
|
||||
game.reveal(p, action.creature)
|
||||
"attack":
|
||||
|
||||
@@ -64,9 +64,16 @@ static func stage(i: int) -> Dictionary:
|
||||
return CHAPTERS[chapter(i) - 1][i % STAGES.size()]
|
||||
|
||||
|
||||
## Boss einer Stufe mit dem Leben und den Merkmalen der Stufe.
|
||||
static func opponent(stage_index: int) -> Game.Player:
|
||||
## Boss einer Stufe mit dem Leben, den Merkmalen und einem Stapel der Stufe; ab Kapitel 2 sind
|
||||
## seine Karten eine Seltenheitsstufe höher (+1/+1), ab Kapitel 3 zwei.
|
||||
static func opponent(stage_index: int, r: RandomNumberGenerator = null) -> Game.Player:
|
||||
var st := stage(stage_index)
|
||||
var boss := Game.Player.new(st.name, false, int(st.life), st.title)
|
||||
boss.perks = Array(st.perks)
|
||||
if r == null:
|
||||
r = Game.make_rng()
|
||||
var ch := chapter(stage_index)
|
||||
boss.deck = DeckGen.opponent_deck(r, ch)
|
||||
for id in boss.deck:
|
||||
boss.looks[id] = ch - 1
|
||||
return boss
|
||||
|
||||
@@ -93,9 +93,9 @@ static func best_element(own: Array, other: Array) -> String:
|
||||
return best
|
||||
|
||||
|
||||
## Kosten einer Karte: zwei Drittel Angriff plus ein Drittel Leben, abgerundet (0/2 kostet nichts).
|
||||
## Kosten einer Karte: zwei Drittel Angriff plus ein Drittel Leben, abgerundet, höchstens 10 (0/2 kostet nichts).
|
||||
static func cost_of(attack: int, health: int) -> int:
|
||||
return (2 * attack + health) / 3
|
||||
return mini(10, (2 * attack + health) / 3)
|
||||
|
||||
|
||||
## Wert eines verschmolzenen Wesens: der bessere Wert plus ein Drittel des schwächeren (mindestens 1),
|
||||
@@ -132,8 +132,7 @@ static func fused(a: Card, b: Card, attack: int = -1, health: int = -1) -> Card:
|
||||
for k in KEYWORDS:
|
||||
if k in a.keywords or k in b.keywords:
|
||||
c.keywords.append(k)
|
||||
c.fusions = maxi(a.fusions, b.fusions) + 1
|
||||
c.look = maxi(a.look, b.look) # die höhere Stufe gewinnt; Game.fuse rechnet die Sammlung ein
|
||||
c.look = maxi(a.look, b.look) # die höhere Stufe gewinnt
|
||||
return c
|
||||
|
||||
|
||||
@@ -157,19 +156,18 @@ class Card extends RefCounted:
|
||||
var elements: Array
|
||||
var attack: int = 0
|
||||
var health: int = 0
|
||||
var cost: int = 0 # abgeleitet: (2 · Angriff + Leben) / 3, abgerundet
|
||||
var cost: int = 0 # abgeleitet: (2 · Angriff + Leben) / 3, abgerundet, höchstens 10 (aus den Grundwerten)
|
||||
var keywords: Array = [] # feste Eigenschaften der Karte (Elementpaare geben ihre zur Laufzeit)
|
||||
var fusions := 0 # wie oft verschmolzen (Verschmelzen kostet 1, 2, 4, 8 …)
|
||||
var look := -1 # Seltenheitsstufe verschmolzener Karten (die höhere der Eltern); -1: aus der Sammlung des Besitzers
|
||||
var look := -1 # Seltenheitsstufe des Exemplars (gibt +1/+1 je Stufe); -1: Grundkarte aus dem Pool
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
var d := {"id": id, "name": name, "name_en": name_en, "elements": elements, "attack": attack, "health": health}
|
||||
if not keywords.is_empty():
|
||||
d["keywords"] = keywords
|
||||
if fusions > 0:
|
||||
d["fusions"] = fusions
|
||||
if look >= 0:
|
||||
d["look"] = look
|
||||
if cost != Cards.cost_of(attack, health):
|
||||
d["cost"] = cost
|
||||
return d
|
||||
|
||||
## Stufe dieser Karte für einen Besitzer mit dieser Sammlung (Game.Player.looks).
|
||||
@@ -184,9 +182,8 @@ class Card extends RefCounted:
|
||||
c.elements = Array(d.elements)
|
||||
c.attack = int(d.get("attack", 0))
|
||||
c.health = int(d.get("health", 0))
|
||||
c.cost = Cards.cost_of(c.attack, c.health)
|
||||
c.cost = int(d.get("cost", Cards.cost_of(c.attack, c.health)))
|
||||
c.keywords = Array(d.get("keywords", []))
|
||||
c.fusions = int(d.get("fusions", 0))
|
||||
c.look = int(d.get("look", -1))
|
||||
return c
|
||||
|
||||
@@ -194,6 +191,15 @@ class Card extends RefCounted:
|
||||
func copy() -> Card:
|
||||
return Card.from_dict(to_dict())
|
||||
|
||||
## Exemplar in einer Seltenheitsstufe: +1/+1 je Stufe, die Kosten bleiben die der Grundkarte.
|
||||
func leveled(level: int) -> Card:
|
||||
var c := copy()
|
||||
c.look = level
|
||||
c.attack += level
|
||||
c.health += level
|
||||
c.cost = cost
|
||||
return c
|
||||
|
||||
## Anzeigename in der aktuellen Sprache.
|
||||
func title() -> String:
|
||||
if name_en != "" and TranslationServer.get_locale().begins_with("en"):
|
||||
@@ -206,10 +212,6 @@ class Card extends RefCounted:
|
||||
func combo() -> String:
|
||||
return Cards.combo_name(elements)
|
||||
|
||||
## Mana für die nächste Verschmelzung mit dieser Karte.
|
||||
func fusion_cost() -> int:
|
||||
return 1 << fusions
|
||||
|
||||
## Kartentext: die festen Eigenschaften.
|
||||
func text() -> String:
|
||||
if keywords.is_empty():
|
||||
|
||||
@@ -1,16 +1,54 @@
|
||||
class_name DeckGen
|
||||
## Der gemeinsame Stapel (jede Karte dreimal) und Gegner.
|
||||
## Stapel: die vier Startdecks, Auffüllen aus der Sammlung, zufällige Stapel für Gegner.
|
||||
|
||||
const COPIES := 3
|
||||
const CHEAP_IN_RANDOM := 12 # so viele im ersten Zug bezahlbare Karten hat ein zufälliger Stapel mindestens
|
||||
|
||||
|
||||
## Frisch gemischter Stapel; das Ende des Arrays ist oben.
|
||||
static func deck(r: RandomNumberGenerator) -> Array:
|
||||
## Zufälliger Stapel aus DECK_MIN verschiedenen Karten bis max_cost, mit genug billigen für den Anfang.
|
||||
static func random_deck(r: RandomNumberGenerator, max_cost: int = 99) -> Array:
|
||||
var cheap := Cards.all().filter(func(c): return c.cost <= Game.START_MANA)
|
||||
var rest := Cards.all().filter(func(c): return c.cost > Game.START_MANA and c.cost <= max_cost)
|
||||
Game.shuffle(cheap, r)
|
||||
Game.shuffle(rest, r)
|
||||
var out := []
|
||||
for c in cheap.slice(0, CHEAP_IN_RANDOM) + rest + cheap.slice(CHEAP_IN_RANDOM):
|
||||
if out.size() >= Game.DECK_MIN:
|
||||
break
|
||||
out.append(c.id)
|
||||
return out
|
||||
|
||||
|
||||
## Stapel eines Kampagnengegners: Kapitel 1 bis Kosten 3, Kapitel 2 bis 5, Kapitel 3 alles.
|
||||
static func opponent_deck(r: RandomNumberGenerator, chapter: int) -> Array:
|
||||
return random_deck(r, [3, 5, 99][clampi(chapter - 1, 0, 2)])
|
||||
|
||||
|
||||
## Startdeck für ein Element: die 25 Karten des Elements bis Angriff 4, dazu je fünf billige der
|
||||
## anderen Elemente (DECK_MIN Karten).
|
||||
static func start_deck(element: String) -> Array:
|
||||
var out := []
|
||||
for c in Cards.all():
|
||||
for i in COPIES:
|
||||
out.append(c)
|
||||
Game.shuffle(out, r)
|
||||
if c.elements[0] == element and c.attack <= 4:
|
||||
out.append(c.id)
|
||||
for e in Cards.ELEMENTS:
|
||||
if e == element:
|
||||
continue
|
||||
var cheap := Cards.all().filter(func(c): return c.elements[0] == e and c.cost <= Game.START_MANA)
|
||||
cheap.sort_custom(func(a, b): return a.cost < b.cost if a.cost != b.cost else a.attack > b.attack)
|
||||
for c in cheap.slice(0, 5):
|
||||
out.append(c.id)
|
||||
return out
|
||||
|
||||
|
||||
## Füllt deck aus pool (Karten-IDs) bis limit auf, die teuersten zuerst; nichts doppelt.
|
||||
static func fill(deck: Array, pool: Array, limit: int) -> Array:
|
||||
var out := deck.duplicate()
|
||||
var cards := pool.filter(func(id): return not (id in out)).map(func(id): return Cards.by_id(id))
|
||||
cards.sort_custom(func(a, b): return a.cost > b.cost if a.cost != b.cost else a.id < b.id)
|
||||
for c in cards:
|
||||
if out.size() >= limit:
|
||||
break
|
||||
out.append(c.id)
|
||||
return out
|
||||
|
||||
|
||||
|
||||
149
scripts/game.gd
149
scripts/game.gd
@@ -7,11 +7,11 @@ const MAX_BOARD := 5
|
||||
const MAX_HAND := 5
|
||||
const START_HAND := 5 # Startkarten, alle im ersten Zug bezahlbar
|
||||
const START_MANA := 1 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Rest verfällt
|
||||
const DRAW_COST := 2 # Ziehen: eine bezahlbare Karte vom Stapel
|
||||
const DECK_MIN := 40 # Karten je Stapel: mindestens so viele (Gegner und Startdecks genau so viele)
|
||||
const DECK_MAX := 60 # und höchstens so viele
|
||||
const START_LIFE := 30
|
||||
const ELEMENT_PAIR := 2 # so viele eigene offene Wesen eines Elements: Sondereffekt des Elements
|
||||
const ELEMENT_MANA := 10 # je so viel Mana an ausgespielten Karten eines Elements: +1 Angriff für die eigenen Wesen des Elements
|
||||
const OFFER := 3 # offene Karten auf dem Stapel, höchstens so teuer wie das Mana des Zugs; beim Ziehen wählt man eine, eine neue rückt nach
|
||||
const ELEMENT_COUNT := 5 # je so viele ausgespielte Karten eines Elements: +1/+1 für die eigenen Wesen des Elements
|
||||
const DISCARD_GAIN := 1 # Abwerfen einer Handkarte bringt so viel Mana
|
||||
|
||||
# Merkmale der Kampagnen-Bosse (Player.perks).
|
||||
@@ -129,17 +129,18 @@ class Player extends RefCounted:
|
||||
var is_human: bool
|
||||
var life: int
|
||||
var max_life: int
|
||||
var hand: Array = [] # eigene Exemplare (Cards.Card.copy)
|
||||
var hand: Array = [] # eigene Exemplare (Cards.Card, in der eigenen Seltenheitsstufe)
|
||||
var board: Array = []
|
||||
var deck: Array = [] # eigener Stapel: Karten-IDs, das Ende ist oben
|
||||
var energy := 0 # Mana in diesem Zug; der Rest verfällt
|
||||
var max_energy := 0 # Mana zu Zugbeginn (START_MANA + eigene Züge)
|
||||
var order := 0 # Platz in der Zugreihenfolge
|
||||
var perks: Array = [] # Boss-Merkmale (siehe PERKS)
|
||||
var looks := {} # Karten-ID -> Seltenheitsstufe, reine Optik (siehe Profile)
|
||||
var turns := 0 # begonnene eigene Züge
|
||||
var played := {} # Element -> Mana der bisher ausgespielten Karten (je ELEMENT_MANA: +1 Angriff)
|
||||
var offer: Array = [] # die OFFER offenen Karten auf dem Stapel (nur der Besitzer sieht sie)
|
||||
var drew := false # in diesem Zug schon gezogen? Das erste Ziehen ist frei
|
||||
var played := {} # Element -> bisher ausgespielte Karten (je ELEMENT_COUNT: +1/+1)
|
||||
var draws := 0 # in diesem Zug gezogene Karten; die n-te kostet n-1 Mana
|
||||
var fatigue := 0 # Züge ohne Karten im Stapel: so viel Leben kostet der nächste
|
||||
|
||||
func _init(n: String, h: bool, l: int, t: String = "") -> void:
|
||||
name = n
|
||||
@@ -153,6 +154,25 @@ class Player extends RefCounted:
|
||||
func alive() -> bool:
|
||||
return life > 0
|
||||
|
||||
## Eine Karte vom eigenen Stapel als Exemplar in der eigenen Stufe; null, wenn er leer ist.
|
||||
## Mit max_cost kommt die oberste bezahlbare, zu teure wandern nach unten (Starthand).
|
||||
func draw_from_deck(max_cost: int = 99) -> Cards.Card:
|
||||
var skipped := []
|
||||
var id := ""
|
||||
while not deck.is_empty():
|
||||
var top: String = deck.pop_back()
|
||||
if Cards.by_id(top).cost <= max_cost:
|
||||
id = top
|
||||
break
|
||||
skipped.append(top)
|
||||
if id == "" and not skipped.is_empty(): # nichts Bezahlbares: dann eben die oberste
|
||||
id = skipped.pop_front()
|
||||
for sk in skipped:
|
||||
deck.insert(0, sk)
|
||||
if id == "":
|
||||
return null
|
||||
return Cards.by_id(id).leveled(int(looks.get(id, 0)))
|
||||
|
||||
## Offene eigene Wesen mit diesem Element.
|
||||
func element_count(e: String) -> int:
|
||||
var n := 0
|
||||
@@ -172,20 +192,19 @@ class Player extends RefCounted:
|
||||
p.looks = looks
|
||||
p.turns = turns
|
||||
p.played = played.duplicate()
|
||||
p.offer = offer.duplicate()
|
||||
p.drew = drew
|
||||
p.deck = deck.duplicate()
|
||||
p.draws = draws
|
||||
p.fatigue = fatigue
|
||||
for c in board:
|
||||
p.board.append(c.clone(p))
|
||||
return p
|
||||
|
||||
|
||||
var human: Player # der (erste) menschliche Spieler
|
||||
var humans: Array = [] # alle menschlichen Spieler (im Netz zwei)
|
||||
var human: Player # der menschliche Spieler
|
||||
var opponent: Player # der andere am Tisch
|
||||
var players: Array = [] # in Zugreihenfolge
|
||||
var winner_player: Player = null
|
||||
var rng: RandomNumberGenerator
|
||||
var deck: Array = [] # gemeinsamer Stapel, das Ende ist oben (nur Host und Einzelspieler)
|
||||
var turn := 0
|
||||
var current_idx := 0
|
||||
var log: Array[String] = []
|
||||
@@ -199,7 +218,6 @@ func _init(h: Player, o: Player, r: RandomNumberGenerator = null) -> void:
|
||||
human = h
|
||||
opponent = o
|
||||
players = [h, o]
|
||||
humans = players.filter(func(p): return p.is_human)
|
||||
if r == null:
|
||||
r = make_rng()
|
||||
rng = r
|
||||
@@ -250,7 +268,6 @@ func clone() -> Game:
|
||||
var idx := players.find(human)
|
||||
var g := Game.new(copies[idx], copies[1 - idx], RandomNumberGenerator.new())
|
||||
g.players = copies # gleiche Zugreihenfolge wie das Original
|
||||
g.deck = deck.duplicate()
|
||||
g.turn = turn
|
||||
g.current_idx = current_idx
|
||||
g.winner = winner
|
||||
@@ -278,16 +295,21 @@ func masked_clone(viewer: Player) -> Game:
|
||||
|
||||
# --- Zugablauf ---------------------------------------------------------------
|
||||
|
||||
## Jeder spielt mit seinem eigenen Stapel (leer: ein zufälliger); die Starthand sind fünf im
|
||||
## ersten Zug bezahlbare Karten daraus.
|
||||
func start(fixed_order: bool = false) -> void:
|
||||
if not fixed_order:
|
||||
shuffle(players, rng) # zufällige Zugreihenfolge
|
||||
deck = DeckGen.deck(rng)
|
||||
for i in players.size():
|
||||
var p: Player = players[i]
|
||||
p.order = i
|
||||
for c in draw_cards(START_HAND, START_MANA):
|
||||
p.hand.append(c.copy())
|
||||
p.offer = draw_cards(OFFER, START_MANA)
|
||||
if p.deck.is_empty():
|
||||
p.deck = DeckGen.random_deck(rng)
|
||||
shuffle(p.deck, rng)
|
||||
for k in START_HAND:
|
||||
var c := p.draw_from_deck(START_MANA)
|
||||
if c != null:
|
||||
p.hand.append(c)
|
||||
turn = 1
|
||||
current_idx = 0
|
||||
say("Zugreihenfolge: " + ", ".join(players.map(func(p): return p.name)))
|
||||
@@ -297,54 +319,31 @@ func start(fixed_order: bool = false) -> void:
|
||||
begin_turn(current())
|
||||
|
||||
|
||||
## Nimmt n Karten vom Stapel, die höchstens max_cost kosten; zu teure wandern unter den Stapel.
|
||||
## Ist der Stapel leer, kommt ein frischer.
|
||||
func draw_cards(n: int, max_cost: int = 999) -> Array:
|
||||
var out := []
|
||||
var skipped := []
|
||||
var looked := 0
|
||||
while out.size() < n:
|
||||
if deck.is_empty():
|
||||
deck = DeckGen.deck(rng)
|
||||
say("Der Stapel wird neu gemischt.")
|
||||
var c: Cards.Card = deck.pop_back()
|
||||
looked += 1
|
||||
if c.cost <= max_cost or looked > deck.size() + skipped.size() + n:
|
||||
out.append(c)
|
||||
else:
|
||||
skipped.append(c)
|
||||
for c in skipped:
|
||||
deck.insert(0, c)
|
||||
return out
|
||||
|
||||
|
||||
## Ziehen: eine der offenen Karten kommt auf die Hand, eine neue rückt nach. Das erste Ziehen
|
||||
## im Zug ist frei, jedes weitere kostet DRAW_COST.
|
||||
## Ziehen: eine Karte vom eigenen Stapel. Die erste im Zug ist frei, jede weitere kostet eins
|
||||
## mehr (0, 1, 2 …).
|
||||
func can_draw(p: Player) -> Array:
|
||||
if winner != "" or p != current():
|
||||
return [false, "Nicht dein Zug"]
|
||||
if p.hand.size() >= MAX_HAND:
|
||||
return [false, "Deine Hand ist voll"]
|
||||
if p.deck.is_empty():
|
||||
return [false, "Dein Stapel ist leer"]
|
||||
if p.energy < draw_cost(p):
|
||||
return [false, "Nicht genug Mana"]
|
||||
return [true, ""]
|
||||
|
||||
|
||||
func draw_cost(p: Player) -> int:
|
||||
return 0 if not p.drew or "ziehen_gratis" in p.perks else DRAW_COST
|
||||
return maxi(0, p.draws - (1 if "ziehen_gratis" in p.perks else 0))
|
||||
|
||||
|
||||
func draw(p: Player, index: int = 0) -> String:
|
||||
func draw(p: Player) -> String:
|
||||
var ok := can_draw(p)
|
||||
if not ok[0]:
|
||||
return ok[1]
|
||||
if index < 0 or index >= p.offer.size():
|
||||
return "Keine Karte gewählt"
|
||||
p.energy -= draw_cost(p)
|
||||
p.drew = true
|
||||
var card: Cards.Card = p.offer[index]
|
||||
p.hand.append(card.copy())
|
||||
p.offer[index] = draw_cards(1, maxi(p.max_energy, START_MANA))[0] # nie teurer als das Mana dieses Zugs
|
||||
p.draws += 1
|
||||
p.hand.append(p.draw_from_deck())
|
||||
say("%s zieht." % p.name)
|
||||
emit({"type": "draw", "player": players.find(p)})
|
||||
return ""
|
||||
@@ -359,9 +358,15 @@ func begin_turn(p: Player) -> void:
|
||||
c.ready = true # lag eine Runde: greift jetzt an, Verdecktes darf aufgedeckt werden
|
||||
c.attacks_done = 0
|
||||
p.turns += 1
|
||||
p.drew = false # das erste Ziehen im Zug ist wieder frei
|
||||
p.draws = 0 # das erste Ziehen im Zug ist wieder frei
|
||||
say("--- Zug %d: %s ---" % [turn, p.name])
|
||||
emit({"type": "turn", "player": players.find(p)})
|
||||
if p.deck.is_empty(): # Erschöpfung: ohne Stapel kostet jeder Zug mehr Leben, damit es ein Ende gibt
|
||||
p.fatigue += 1
|
||||
p.life -= p.fatigue
|
||||
say("%s hat keine Karten mehr und verliert %d Leben." % [p.name, p.fatigue])
|
||||
emit({"type": "damage", "target": ref_of(p), "amount": p.fatigue})
|
||||
check_end()
|
||||
|
||||
|
||||
## Zugende: Wesen mit Regeneration heilen ganz, dann ist der andere dran.
|
||||
@@ -417,7 +422,7 @@ func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
|
||||
cr.max_health += 1
|
||||
p.board.append(cr)
|
||||
for e in card.elements:
|
||||
p.played[e] = int(p.played.get(e, 0)) + card.cost
|
||||
p.played[e] = int(p.played.get(e, 0)) + 1
|
||||
if face_down:
|
||||
say("%s legt ein Wesen verdeckt." % p.name)
|
||||
else:
|
||||
@@ -492,9 +497,9 @@ static func look_of(p: Player, c: Cards.Card) -> int:
|
||||
return c.look if c.look >= 0 else int(p.looks.get(c.id, 0))
|
||||
|
||||
|
||||
## Kosten: 1 Mana, verdoppelt je Verschmelzung, die schon in einer der beiden steckt.
|
||||
## Kosten: die Hälfte der Manasumme beider Karten, aufgerundet.
|
||||
static func fusion_cost(a, b) -> int:
|
||||
return 1 << maxi(card_of(a).fusions, card_of(b).fusions)
|
||||
return ceili((card_of(a).cost + card_of(b).cost) / 2.0)
|
||||
|
||||
|
||||
## a und b: zwei eigene Wesen auf dem Brett oder zwei eigene Handkarten.
|
||||
@@ -539,11 +544,11 @@ func fuse(p: Player, a, b) -> String:
|
||||
## das Ergebnis ist nur angriffsbereit, wenn beide es waren.
|
||||
func _fuse_board(p: Player, a: Creature, b: Creature) -> String:
|
||||
var card := Cards.fused(a.card, b.card, Cards.fusion_value(a.attack, b.attack),
|
||||
Cards.fusion_value(a.max_health, b.max_health))
|
||||
Cards.fusion_value(a.max_health - a.aura, b.max_health - b.aura))
|
||||
card.look = maxi(look_of(p, a.card), look_of(p, b.card))
|
||||
var cr := Creature.new(card, p, next_uid)
|
||||
next_uid += 1
|
||||
cr.health = mini(card.health, Cards.fusion_value(a.health, b.health))
|
||||
cr.health = mini(card.health, Cards.fusion_value(mini(a.health, a.max_health - a.aura), mini(b.health, b.max_health - b.aura)))
|
||||
cr.ready = a.ready and b.ready
|
||||
cr.attacks_done = maxi(a.attacks_done, b.attacks_done)
|
||||
var at := p.board.find(a)
|
||||
@@ -560,11 +565,11 @@ func _fuse_board(p: Player, a: Creature, b: Creature) -> String:
|
||||
|
||||
# --- Elementboni ---------------------------------------------------------------
|
||||
|
||||
## +1 Angriff je volle ELEMENT_MANA an ausgespielten Karten eines Elements des Spielers; mehrere Elemente zählen alle.
|
||||
## +1/+1 je volle ELEMENT_COUNT ausgespielte Karten eines Elements des Spielers; mehrere Elemente zählen alle.
|
||||
static func aura_of(elements: Array, played: Dictionary) -> int:
|
||||
var a := 0
|
||||
for e in elements:
|
||||
a += int(played.get(e, 0)) / ELEMENT_MANA
|
||||
a += int(played.get(e, 0)) / ELEMENT_COUNT
|
||||
return a
|
||||
|
||||
|
||||
@@ -579,18 +584,24 @@ func aura_for(c: Creature) -> int:
|
||||
return a
|
||||
|
||||
|
||||
## Rechnet die Elementboni neu; sie wirken nur auf den Angriff (Creature.power).
|
||||
## Rechnet die Elementboni neu. Kommt ein Bonus, steigen Leben und Höchstleben; geht er
|
||||
## (beim Verdecken), sinkt nur das Höchstleben, das Leben wird gekappt.
|
||||
func apply_auras() -> void:
|
||||
for p in players:
|
||||
for c in p.board:
|
||||
c.aura = aura_for(c)
|
||||
var a := aura_for(c)
|
||||
if a != c.aura:
|
||||
var delta: int = a - c.aura
|
||||
c.max_health += delta
|
||||
c.health = mini(c.health + maxi(delta, 0), c.max_health)
|
||||
c.aura = a
|
||||
|
||||
|
||||
## Vorschau fürs Ausspielen: der Elementbonus, den die Karte offen gelegt bekäme (sie zählt selbst mit).
|
||||
func forecast(p: Player, card: Cards.Card) -> int:
|
||||
var played := p.played.duplicate()
|
||||
for e in card.elements:
|
||||
played[e] = int(played.get(e, 0)) + card.cost
|
||||
played[e] = int(played.get(e, 0)) + 1
|
||||
return aura_of(card.elements, played) * (2 if "element_doppelt" in p.perks else 1)
|
||||
|
||||
|
||||
@@ -685,20 +696,7 @@ func check_end() -> void:
|
||||
return
|
||||
winner_player = alive[0]
|
||||
winner = "human" if winner_player.is_human else "opponents"
|
||||
if humans.size() == 1:
|
||||
say("Der Gegner ist besiegt!" if winner == "human" else "Du wurdest besiegt.")
|
||||
else:
|
||||
say("%s gewinnt!" % winner_player.name)
|
||||
|
||||
|
||||
# --- Zustand für das Netzwerk (siehe NetState) ------------------------------------
|
||||
|
||||
func snapshot(viewer: int) -> Dictionary:
|
||||
return NetState.snapshot(self, viewer)
|
||||
|
||||
|
||||
static func from_snapshot(d: Dictionary) -> Game:
|
||||
return NetState.from_snapshot(d)
|
||||
say("Der Gegner ist besiegt!" if winner == "human" else "Du wurdest besiegt.")
|
||||
|
||||
|
||||
# --- Gegner und Kampfstart ---------------------------------------------------
|
||||
@@ -711,7 +709,7 @@ static func shuffle(arr: Array, r: RandomNumberGenerator) -> void:
|
||||
arr[j] = tmp
|
||||
|
||||
|
||||
## Duell gegen einen zufälligen Gegner. rules: {"human_perks": [...], "opponent_life": int, "looks": {...}}
|
||||
## Duell gegen einen zufälligen Gegner. rules: {"human_perks": [...], "opponent_life": int, "looks": {...}, "deck": [ids]}
|
||||
static func new_battle(r: RandomNumberGenerator = null, rules: Dictionary = {}) -> Game:
|
||||
if r == null:
|
||||
r = make_rng()
|
||||
@@ -724,6 +722,7 @@ static func new_battle_with(opp: Player, r: RandomNumberGenerator = null, fixed_
|
||||
var human := Player.new("Spieler", true, START_LIFE)
|
||||
human.perks = Array(rules.get("human_perks", []))
|
||||
human.looks = rules.get("looks", {})
|
||||
human.deck = rules.get("deck", []).duplicate()
|
||||
var g := Game.new(human, opp, r)
|
||||
for p in g.players:
|
||||
if "leben_35" in p.perks:
|
||||
|
||||
443
scripts/net.gd
443
scripts/net.gd
@@ -1,443 +0,0 @@
|
||||
class_name Net
|
||||
extends Node
|
||||
## LAN-Mehrspieler: der Host führt das Spiel, Mitspieler senden Aktionen und erhalten
|
||||
## Zustandsbilder aus ihrer Sicht (fremde Hände bleiben verborgen).
|
||||
|
||||
signal lobby_changed
|
||||
signal started(seat: int)
|
||||
signal snapshot_received(snap: Dictionary)
|
||||
signal action_applied
|
||||
signal ended(reason: String)
|
||||
signal migrated # nach einem Host-Ausfall: dieser Rechner führt das Spiel jetzt
|
||||
signal status_changed
|
||||
|
||||
const PORT := 24455
|
||||
|
||||
var peer: ENetMultiplayerPeer
|
||||
var is_host := false
|
||||
var connected := false
|
||||
var my_name := "Spieler"
|
||||
var my_pick := {"looks": {}} # eigene Kartenoptik (Seltenheit), die der Gegner sieht
|
||||
var names := {} # peer_id -> Name (nur im Host vollständig, sonst Kopie)
|
||||
var picks := {} # peer_id -> {"looks": {...}} (Host)
|
||||
var seats := {} # peer_id -> Spielerindex (Host)
|
||||
var my_seat := -1
|
||||
var game: Game # Host
|
||||
var status := ""
|
||||
var waiting := {} # Name -> {"seat": int, "since": float}: Platz nach Verbindungsabbruch freihalten
|
||||
var rejoin_seconds := 60.0
|
||||
var spectator := false # eigene Rolle: nur zuschauen
|
||||
var spectators := {} # Host: peer_id -> Name
|
||||
var successor_ip := "" # Mitspieler: Adresse des Nachfolge-Hosts
|
||||
var i_am_successor := false
|
||||
var backup := {} # Nachfolger: voller Spielstand nach jeder Aktion
|
||||
var backup_names := {} # Nachfolger: Platz -> Name aller Menschen
|
||||
var last_winner := "" # Mitspieler: Sieger laut letztem Zustandsbild
|
||||
var reconnecting := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||
multiplayer.connected_to_server.connect(_on_connected)
|
||||
multiplayer.connection_failed.connect(func():
|
||||
if not reconnecting:
|
||||
_fail("Verbindung fehlgeschlagen"))
|
||||
multiplayer.server_disconnected.connect(_on_server_lost)
|
||||
|
||||
|
||||
func host(port: int = PORT) -> Error:
|
||||
peer = ENetMultiplayerPeer.new()
|
||||
var err := peer.create_server(port, 8) # ein Mitspieler, der Rest Zuschauer
|
||||
if err != OK:
|
||||
status = "Kann nicht hosten (Port %d belegt?)" % port
|
||||
return err
|
||||
multiplayer.multiplayer_peer = peer
|
||||
is_host = true
|
||||
connected = true
|
||||
names = {1: my_name}
|
||||
picks = {1: my_pick}
|
||||
spectators = {}
|
||||
status = "Warte auf Mitspieler …"
|
||||
lobby_changed.emit()
|
||||
return OK
|
||||
|
||||
|
||||
func join(ip: String, port: int = PORT) -> Error:
|
||||
peer = ENetMultiplayerPeer.new()
|
||||
var err := peer.create_client(ip, port)
|
||||
if err != OK:
|
||||
status = "Adresse ungültig"
|
||||
return err
|
||||
multiplayer.multiplayer_peer = peer
|
||||
is_host = false
|
||||
status = "Verbinde mit %s …" % ip
|
||||
return OK
|
||||
|
||||
|
||||
func leave() -> void:
|
||||
if multiplayer.multiplayer_peer != null:
|
||||
multiplayer.multiplayer_peer.close()
|
||||
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
||||
peer = null
|
||||
is_host = false
|
||||
connected = false
|
||||
names = {}
|
||||
picks = {}
|
||||
seats = {}
|
||||
waiting = {}
|
||||
my_seat = -1
|
||||
game = null
|
||||
status = ""
|
||||
spectator = false
|
||||
spectators = {}
|
||||
successor_ip = ""
|
||||
i_am_successor = false
|
||||
backup = {}
|
||||
backup_names = {}
|
||||
last_winner = ""
|
||||
|
||||
|
||||
func local_addresses() -> Array:
|
||||
return Array(IP.get_local_addresses()).filter(func(a): return "." in a and not a.begins_with("127."))
|
||||
|
||||
|
||||
func _fail(reason: String) -> void:
|
||||
status = reason
|
||||
ended.emit(reason)
|
||||
leave()
|
||||
|
||||
|
||||
func _on_connected() -> void:
|
||||
connected = true
|
||||
status = "Verbunden"
|
||||
register.rpc_id(1, my_name, my_pick, spectator)
|
||||
|
||||
|
||||
# --- Host-Wechsel ---------------------------------------------------------------
|
||||
# Der Host schickt dem Mitspieler mit der kleinsten Kennung nach jeder Aktion den vollen
|
||||
# Spielstand und allen die Adresse dieses Nachfolgers. Fällt der Host aus, eröffnet der
|
||||
# Nachfolger das Spiel neu, die anderen verbinden sich mit ihm und kehren auf ihre Plätze zurück.
|
||||
|
||||
func _successor_id() -> int:
|
||||
var ids: Array = seats.keys().filter(func(id): return id != 1)
|
||||
ids.sort()
|
||||
return ids[0] if not ids.is_empty() else -1
|
||||
|
||||
|
||||
func _update_successor() -> void:
|
||||
if not is_host or game == null:
|
||||
return
|
||||
var succ := _successor_id()
|
||||
var ip := ""
|
||||
if succ > 0 and peer != null and peer.get_peer(succ) != null:
|
||||
ip = peer.get_peer(succ).get_remote_address()
|
||||
for id in seats:
|
||||
if id != 1:
|
||||
set_successor.rpc_id(id, ip, id == succ)
|
||||
|
||||
|
||||
## Platz -> Name aller Menschen (auch der gerade fehlenden), für die Rückkehr nach dem Wechsel.
|
||||
func _seat_names() -> Dictionary:
|
||||
var out := {}
|
||||
for id in seats:
|
||||
out[seats[id]] = names.get(id, "")
|
||||
for pname in waiting:
|
||||
out[waiting[pname].seat] = pname
|
||||
return out
|
||||
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func set_successor(ip: String, me: bool) -> void:
|
||||
successor_ip = ip
|
||||
i_am_successor = me
|
||||
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func receive_backup(snap: Dictionary, seat_names: Dictionary) -> void:
|
||||
backup = snap
|
||||
backup_names = seat_names
|
||||
|
||||
|
||||
func _on_server_lost() -> void:
|
||||
if is_host:
|
||||
return
|
||||
if last_winner != "":
|
||||
_fail("Host hat das Spiel beendet")
|
||||
elif i_am_successor and not backup.is_empty():
|
||||
_become_host()
|
||||
elif successor_ip != "":
|
||||
status = "Host weg, verbinde mit dem Nachfolger …"
|
||||
status_changed.emit()
|
||||
_reconnect(successor_ip)
|
||||
else:
|
||||
_fail("Host hat das Spiel beendet")
|
||||
|
||||
|
||||
func _become_host() -> void:
|
||||
var snap := backup
|
||||
var seat_names := backup_names
|
||||
var seat := my_seat
|
||||
if multiplayer.multiplayer_peer != null:
|
||||
multiplayer.multiplayer_peer.close()
|
||||
var err := FAILED
|
||||
for attempt in 5:
|
||||
err = host()
|
||||
if err == OK:
|
||||
break
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
if err != OK:
|
||||
_fail("Host weg, Übernahme fehlgeschlagen")
|
||||
return
|
||||
game = Game.from_snapshot(snap)
|
||||
seats = {1: seat}
|
||||
my_seat = seat
|
||||
waiting = {}
|
||||
for s in seat_names:
|
||||
if int(s) != seat:
|
||||
waiting[seat_names[s]] = {"seat": int(s), "since": Time.get_unix_time_from_system()}
|
||||
game.say("%s übernimmt als Host." % my_name)
|
||||
status = "Du bist jetzt Host, die anderen verbinden neu …"
|
||||
successor_ip = ""
|
||||
i_am_successor = false
|
||||
backup = {}
|
||||
migrated.emit()
|
||||
|
||||
|
||||
func _reconnect(ip: String) -> void:
|
||||
reconnecting = true
|
||||
await get_tree().create_timer(2.0).timeout
|
||||
for attempt in 5:
|
||||
if join(ip) == OK:
|
||||
var t := 0.0
|
||||
while not connected and t < 4.0 and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTING:
|
||||
await get_tree().process_frame
|
||||
t += get_process_delta_time()
|
||||
if connected:
|
||||
reconnecting = false
|
||||
return
|
||||
await get_tree().create_timer(1.0).timeout
|
||||
reconnecting = false
|
||||
_fail("Nachfolge-Host nicht erreichbar")
|
||||
|
||||
|
||||
func _on_peer_connected(_id: int) -> void:
|
||||
pass
|
||||
|
||||
|
||||
func _on_peer_disconnected(id: int) -> void:
|
||||
if not is_host:
|
||||
return
|
||||
var pname: String = names.get(id, "")
|
||||
names.erase(id)
|
||||
picks.erase(id)
|
||||
spectators.erase(id)
|
||||
if game != null and seats.has(id):
|
||||
var p: Game.Player = game.players[seats[id]]
|
||||
if game.winner == "":
|
||||
waiting[pname] = {"seat": seats[id], "since": Time.get_unix_time_from_system()}
|
||||
game.say("%s hat die Verbindung verloren, der Platz bleibt %d s frei." % [p.name, int(rejoin_seconds)])
|
||||
seats.erase(id)
|
||||
_update_successor()
|
||||
action_applied.emit()
|
||||
lobby_update.rpc(names, spectators)
|
||||
lobby_changed.emit()
|
||||
|
||||
|
||||
## Host, je Bild: wartende Spieler überspringen; nach Ablauf der Frist scheiden sie aus.
|
||||
func tick() -> void:
|
||||
if not is_host or game == null or game.winner != "":
|
||||
return
|
||||
var now := Time.get_unix_time_from_system()
|
||||
for pname in waiting.keys():
|
||||
var w: Dictionary = waiting[pname]
|
||||
var p: Game.Player = game.players[w.seat]
|
||||
if now - float(w.since) > rejoin_seconds:
|
||||
waiting.erase(pname)
|
||||
game.say("%s kehrt nicht zurück und scheidet aus." % p.name)
|
||||
game.damage(p, 999)
|
||||
game.cleanup()
|
||||
game.check_end()
|
||||
if game.winner == "" and game.current() == p:
|
||||
game.end_turn() # sonst bliebe das Spiel beim Ausgeschiedenen hängen
|
||||
action_applied.emit()
|
||||
elif game.current() == p:
|
||||
game.say("%s ist nicht da, der Zug wird übersprungen." % p.name)
|
||||
game.end_turn()
|
||||
action_applied.emit()
|
||||
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func register(name: String, pick: Dictionary, as_spectator: bool = false) -> void:
|
||||
if not is_host:
|
||||
return
|
||||
var id := multiplayer.get_remote_sender_id()
|
||||
if as_spectator:
|
||||
spectators[id] = name
|
||||
lobby_update.rpc(names, spectators)
|
||||
lobby_changed.emit()
|
||||
if game != null:
|
||||
start_game.rpc_id(id, -1)
|
||||
receive_snapshot.rpc_id(id, game.snapshot(-2))
|
||||
return
|
||||
if game != null:
|
||||
# Laufendes Spiel: nur Rückkehr auf einen freigehaltenen Platz
|
||||
if waiting.has(name):
|
||||
seats[id] = waiting[name].seat
|
||||
names[id] = name
|
||||
waiting.erase(name)
|
||||
game.say("%s ist zurück." % name)
|
||||
start_game.rpc_id(id, seats[id])
|
||||
receive_snapshot.rpc_id(id, game.snapshot(seats[id]))
|
||||
_update_successor()
|
||||
action_applied.emit()
|
||||
else:
|
||||
status = "%s wollte beitreten, das Spiel läuft schon" % name
|
||||
return
|
||||
names[id] = name
|
||||
picks[id] = pick
|
||||
lobby_update.rpc(names, spectators)
|
||||
lobby_changed.emit()
|
||||
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func lobby_update(n: Dictionary, sp: Dictionary = {}) -> void:
|
||||
names = n
|
||||
spectators = sp
|
||||
lobby_changed.emit()
|
||||
|
||||
|
||||
## Host: Duell mit dem ersten Mitspieler starten; allein gegen die KI.
|
||||
func start_battle() -> void:
|
||||
var ids: Array = names.keys()
|
||||
ids.sort()
|
||||
ids = ids.slice(0, 2)
|
||||
var r := Game.make_rng()
|
||||
var players := []
|
||||
seats = {}
|
||||
for id in ids:
|
||||
var pick: Dictionary = picks.get(id, {})
|
||||
var p := Game.Player.new(names[id], true, Game.START_LIFE)
|
||||
for cid in pick.get("looks", {}):
|
||||
if Cards.has_id(cid):
|
||||
p.looks[cid] = int(pick.looks[cid])
|
||||
players.append(p)
|
||||
if players.size() < 2:
|
||||
players.append(DeckGen.random_opponent())
|
||||
game = Game.new(players[0], players[1], r)
|
||||
game.start()
|
||||
for id in ids:
|
||||
seats[id] = game.players.find(players[ids.find(id)])
|
||||
my_seat = seats[1]
|
||||
for id in ids:
|
||||
if id != 1:
|
||||
start_game.rpc_id(id, seats[id])
|
||||
for id in spectators:
|
||||
start_game.rpc_id(id, -1)
|
||||
_update_successor()
|
||||
started.emit(my_seat)
|
||||
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func start_game(seat: int) -> void:
|
||||
my_seat = seat
|
||||
started.emit(seat)
|
||||
|
||||
|
||||
## Host: Zustand an alle senden (Mitspieler aus ihrer Sicht, Zuschauer ohne Verborgenes,
|
||||
## der Nachfolger zusätzlich den vollen Spielstand).
|
||||
func broadcast() -> void:
|
||||
if not is_host or game == null:
|
||||
return
|
||||
for id in seats:
|
||||
if id != 1:
|
||||
receive_snapshot.rpc_id(id, game.snapshot(seats[id]))
|
||||
for id in spectators:
|
||||
receive_snapshot.rpc_id(id, game.snapshot(-2))
|
||||
var succ := _successor_id()
|
||||
if succ > 0:
|
||||
receive_backup.rpc_id(succ, game.snapshot(-1), _seat_names())
|
||||
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func receive_snapshot(snap: Dictionary) -> void:
|
||||
last_winner = str(snap.get("winner", ""))
|
||||
snapshot_received.emit(snap)
|
||||
|
||||
|
||||
## Mitspieler: Aktion an den Host schicken.
|
||||
func send_action(action: Dictionary) -> void:
|
||||
if is_host:
|
||||
_apply(1, action)
|
||||
else:
|
||||
request_action.rpc_id(1, action)
|
||||
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func request_action(action: Dictionary) -> void:
|
||||
if not is_host:
|
||||
return
|
||||
_apply(multiplayer.get_remote_sender_id(), action)
|
||||
|
||||
|
||||
func _apply(id: int, action: Dictionary) -> void:
|
||||
if game == null or not seats.has(id):
|
||||
return
|
||||
var p: Game.Player = game.players[seats[id]]
|
||||
if game.winner != "":
|
||||
return
|
||||
if game.current() != p:
|
||||
return
|
||||
match str(action.get("type", "")):
|
||||
"play":
|
||||
var idx := int(action.get("hand", -1))
|
||||
if idx < 0 or idx >= p.hand.size():
|
||||
return
|
||||
game.play_card(p, p.hand[idx], bool(action.get("face_down", false)))
|
||||
"reveal":
|
||||
var cr := game.find_creature(int(action.get("uid", -1)))
|
||||
if cr != null:
|
||||
game.reveal(p, cr)
|
||||
"discard":
|
||||
var hi := int(action.get("hand", -1))
|
||||
if hi >= 0 and hi < p.hand.size():
|
||||
game.discard(p, p.hand[hi])
|
||||
"attack":
|
||||
var cr := game.find_creature(int(action.get("uid", -1)))
|
||||
var t = resolve_ref(action.get("target"))
|
||||
if cr != null and t != null:
|
||||
game.attack(cr, t)
|
||||
"draw":
|
||||
game.draw(p, int(action.get("index", 0)))
|
||||
"fuse": # zwei Wesen (uid, other) oder zwei Handkarten (hand, other_hand)
|
||||
var a = game.find_creature(int(action.get("uid", -1)))
|
||||
var b = game.find_creature(int(action.get("other", -1)))
|
||||
var i := int(action.get("hand", -1))
|
||||
var j := int(action.get("other_hand", -1))
|
||||
if a == null and i >= 0 and j >= 0 and i < p.hand.size() and j < p.hand.size():
|
||||
a = p.hand[i]
|
||||
b = p.hand[j]
|
||||
if a != null and b != null:
|
||||
game.fuse(p, a, b)
|
||||
"end":
|
||||
game.end_turn()
|
||||
_:
|
||||
return
|
||||
action_applied.emit()
|
||||
|
||||
|
||||
func resolve_ref(ref):
|
||||
if ref == null or not (ref is Dictionary):
|
||||
return null
|
||||
if ref.get("kind") == "creature":
|
||||
return game.find_creature(int(ref.get("uid", -1)))
|
||||
var i := int(ref.get("index", -1))
|
||||
if i >= 0 and i < game.players.size():
|
||||
return game.players[i]
|
||||
return null
|
||||
|
||||
|
||||
static func ref_dict(target) -> Dictionary:
|
||||
if target is Game.Creature:
|
||||
return {"kind": "creature", "uid": target.uid}
|
||||
return {"kind": "player", "index": target.order}
|
||||
@@ -1 +0,0 @@
|
||||
uid://drhh23lblvq8o
|
||||
@@ -1,109 +0,0 @@
|
||||
class_name NetState
|
||||
## Serialisierter Spielzustand für das Netzwerk (ausgelagert aus den Spielregeln).
|
||||
|
||||
|
||||
## Karten reisen als ID; eigene Exemplare mit Schlüsselwörtern und verschmolzene als Ganzes.
|
||||
static func card_ref(c: Cards.Card):
|
||||
return c.id if Cards.has_id(c.id) and c.keywords.is_empty() and c.fusions == 0 else c.to_dict()
|
||||
|
||||
|
||||
static func card_of(ref) -> Cards.Card:
|
||||
return Cards.Card.from_dict(ref) if ref is Dictionary else Cards.by_id(ref).copy()
|
||||
|
||||
|
||||
## Zustand aus Sicht eines Spielers: fremde Hand nur als Anzahl, fremde
|
||||
## verdeckte Wesen ohne Karte und Werte. viewer -1: alles sichtbar (Host-Wechsel); -2: Zuschauer.
|
||||
static func snapshot(g: Game, viewer: int) -> Dictionary:
|
||||
var ps := []
|
||||
for i in g.players.size():
|
||||
var p: Game.Player = g.players[i]
|
||||
var mine := i == viewer or viewer == -1
|
||||
var board := []
|
||||
for c in p.board:
|
||||
var masked: bool = c.hidden and not mine
|
||||
board.append({"uid": c.uid, "card": card_ref(Cards.unknown() if masked else c.card),
|
||||
"attack": 0 if masked else c.attack, "health": 0 if masked else c.health,
|
||||
"max_health": 0 if masked else c.max_health, "attacks_done": c.attacks_done,
|
||||
"ready": c.ready, "hidden": c.hidden, "aura": c.aura})
|
||||
ps.append({"name": p.name, "title": p.title, "is_human": p.is_human, "life": p.life, "max_life": p.max_life,
|
||||
"energy": p.energy, "max_energy": p.max_energy,
|
||||
"order": p.order, "perks": p.perks, "looks": p.looks, "turns": p.turns, "played": p.played,
|
||||
"hand": p.hand.map(card_ref) if mine else p.hand.size(), "board": board,
|
||||
"offer": p.offer.map(card_ref) if mine else [], "drew": p.drew})
|
||||
var evs := []
|
||||
for ev in g.events:
|
||||
var e: Dictionary = ev.duplicate()
|
||||
if e.has("card"):
|
||||
e["card"] = card_ref(e.card)
|
||||
evs.append(e)
|
||||
return {"players": ps, "human": g.players.find(g.human), "turn": g.turn, "current_idx": g.current_idx,
|
||||
"winner": g.winner, "winner_player": g.players.find(g.winner_player) if g.winner_player != null else -1,
|
||||
"next_uid": g.next_uid, "deck_size": g.deck.size(),
|
||||
"log": g.log.slice(maxi(0, g.log.size() - 80)), "events": evs}
|
||||
|
||||
|
||||
## Baut einen Spielzustand aus einem Snapshot; verborgene Karten werden durch Platzhalter ersetzt.
|
||||
static func from_snapshot(d: Dictionary) -> Game:
|
||||
var placeholder: Cards.Card = Cards.all()[0]
|
||||
var list := []
|
||||
for pd in d.players:
|
||||
var p := Game.Player.new(pd.name, pd.is_human, int(pd.life), pd.title)
|
||||
p.max_life = int(pd.max_life)
|
||||
p.energy = int(pd.energy)
|
||||
p.max_energy = int(pd.max_energy)
|
||||
p.order = int(pd.order)
|
||||
p.perks = Array(pd.perks)
|
||||
for cid in pd.get("looks", {}):
|
||||
p.looks[cid] = int(pd.looks[cid])
|
||||
p.turns = int(pd.turns)
|
||||
for e in pd.get("played", {}):
|
||||
p.played[e] = int(pd.played[e])
|
||||
if pd.hand is Array:
|
||||
for ref in pd.hand:
|
||||
p.hand.append(card_of(ref))
|
||||
else:
|
||||
for i in int(pd.hand):
|
||||
p.hand.append(placeholder)
|
||||
for ref in pd.get("offer", []):
|
||||
p.offer.append(card_of(ref))
|
||||
p.drew = bool(pd.get("drew", false))
|
||||
for cd in pd.board:
|
||||
var card := card_of(cd.card)
|
||||
var c := Game.Creature.new(card, p, int(cd.uid))
|
||||
c.attack = int(cd.attack)
|
||||
c.health = int(cd.health)
|
||||
c.max_health = int(cd.max_health)
|
||||
c.attacks_done = int(cd.get("attacks_done", 0))
|
||||
c.ready = bool(cd.get("ready", false))
|
||||
c.hidden = bool(cd.get("hidden", false))
|
||||
c.aura = int(cd.get("aura", 0))
|
||||
p.board.append(c)
|
||||
list.append(p)
|
||||
var hi := int(d.human)
|
||||
var g := Game.new(list[hi], list[1 - hi], RandomNumberGenerator.new())
|
||||
g.players = list
|
||||
g.turn = int(d.turn)
|
||||
g.current_idx = int(d.current_idx)
|
||||
g.winner = d.winner
|
||||
g.winner_player = list[int(d.winner_player)] if int(d.winner_player) >= 0 else null
|
||||
g.next_uid = int(d.next_uid)
|
||||
for i in int(d.get("deck_size", 0)):
|
||||
g.deck.append(placeholder) # nur die Größe zählt; gezogen wird beim Host
|
||||
for line in d.log:
|
||||
g.log.append(str(line))
|
||||
for ev in d.events:
|
||||
var e: Dictionary = ev.duplicate()
|
||||
if e.has("card"):
|
||||
e["card"] = card_of(e.card)
|
||||
for key in ["player", "uid", "amount"]:
|
||||
if e.has(key):
|
||||
e[key] = int(e[key])
|
||||
for key in ["target", "attacker"]:
|
||||
if e.has(key):
|
||||
var r: Dictionary = e[key]
|
||||
if r.has("uid"):
|
||||
r["uid"] = int(r.uid)
|
||||
if r.has("index"):
|
||||
r["index"] = int(r.index)
|
||||
g.events.append(e)
|
||||
return g
|
||||
@@ -1 +0,0 @@
|
||||
uid://c3ao1mpgbrter
|
||||
@@ -1,14 +1,16 @@
|
||||
class_name Profile
|
||||
extends RefCounted
|
||||
## Spielstand: Münzen, Staub, Sammlung, Kampagne, Einstellungen.
|
||||
## Jede Karte gehört jedem von Anfang an. Gesammelt wird die Seltenheitsstufe, und die
|
||||
## ist reine Optik: sie ändert weder Werte noch Text einer Karte.
|
||||
## Spielstand: Münzen, Staub, Sammlung, Stapel, Kampagne, Einstellungen.
|
||||
## Man startet mit einem von vier Startdecks (je ein Element im Mittelpunkt); Booster und die
|
||||
## Werkstatt bringen den Rest. Jede eigene Karte hat eine Seltenheitsstufe, die +1/+1 je Stufe
|
||||
## gibt (Cards.Card.leveled); kommt eine seltenere Version, ersetzt sie die alte, die zu Staub wird.
|
||||
|
||||
const SAVE_VERSION := 6
|
||||
const SAVE_VERSION := 7
|
||||
const CRAFT_COST := 200 # Staub für eine neue Karte (Common)
|
||||
const SAVE_PATH := "user://spielstand.json"
|
||||
const BOOSTER_COST := 100
|
||||
const BOOSTER_SIZE := 5
|
||||
const RARITY_WEIGHTS := {"Common": 50.0, "Rare": 25.0, "Super Rare": 15.0, "Ultra Rare": 8.0, "Ultimate Rare": 2.0}
|
||||
const RARITY_WEIGHTS := {"Common": 45.0, "Rare": 25.0, "Super Rare": 15.0, "Ultra Rare": 10.0, "Ultimate Rare": 5.0}
|
||||
const START_COINS := 150
|
||||
const REWARD_WIN := 100
|
||||
const REWARD_LOSS := 50
|
||||
@@ -24,19 +26,34 @@ const DUST_UPGRADE := {"Rare": 100, "Super Rare": 200, "Ultra Rare": 500, "Ultim
|
||||
|
||||
var coins := 0
|
||||
var dust := 0
|
||||
var collection := {} # id -> Seltenheitsstufe (Index in Cards.RARITIES), fehlend = Common
|
||||
var collection := {} # eigene Karten: id -> Seltenheitsstufe (Index in Cards.RARITIES)
|
||||
var deck: Array = [] # Karten-IDs des eigenen Stapels (Game.DECK_MIN bis DECK_MAX)
|
||||
var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen
|
||||
var settings := {"volume": 0.0, "music": 0.0, "ai_speed": 1.0, "animations": true, "language": "de", "difficulty": "normal", "big_font": false}
|
||||
var notices: Array[String] = [] # Meldungen für die Oberfläche (Kampagnenfortschritt)
|
||||
var perk := "" # gewähltes eigenes Merkmal (nach Kapitel 1)
|
||||
|
||||
|
||||
## Neues Profil: noch ohne Karten, das Startdeck wählt der Spieler (choose_start).
|
||||
static func starter() -> Profile:
|
||||
var p := Profile.new()
|
||||
p.coins = START_COINS
|
||||
return p
|
||||
|
||||
|
||||
## Hat der Spieler schon ein Startdeck gewählt?
|
||||
func started() -> bool:
|
||||
return not collection.is_empty()
|
||||
|
||||
|
||||
## Das Startdeck eines Elements wird Sammlung und Stapel.
|
||||
func choose_start(element: String) -> void:
|
||||
collection = {}
|
||||
deck = DeckGen.start_deck(element)
|
||||
for id in deck:
|
||||
collection[id] = 0
|
||||
|
||||
|
||||
## Lädt den Spielstand; bei Schaden die Sicherung; sonst ein neues Profil.
|
||||
static func load_or_new(path: String = SAVE_PATH) -> Profile:
|
||||
for candidate in [path, path + ".bak"]:
|
||||
@@ -52,7 +69,7 @@ static func load_or_new(path: String = SAVE_PATH) -> Profile:
|
||||
|
||||
|
||||
## Liest jede Spielstand-Version; ältere Stände behalten Münzen, Staub, Kampagne und
|
||||
## Einstellungen, die alte Sammlung (Anzahl je Karte) hat keine Entsprechung mehr.
|
||||
## Einstellungen und bekommen die Startsammlung.
|
||||
static func from_dict(d: Dictionary) -> Profile:
|
||||
var p := Profile.new()
|
||||
p.coins = int(d.get("coins", 0))
|
||||
@@ -63,6 +80,11 @@ static func from_dict(d: Dictionary) -> Profile:
|
||||
for cid in coll:
|
||||
if Cards.has_id(cid):
|
||||
p.collection[cid] = clampi(int(coll[cid]), 0, Cards.RARITIES.size() - 1)
|
||||
var ids := []
|
||||
for cid in d.get("deck", []):
|
||||
if p.collection.has(cid) and not (cid in ids):
|
||||
ids.append(cid)
|
||||
p.deck = ids.slice(0, Game.DECK_MAX)
|
||||
var se = d.get("settings", {})
|
||||
p.settings = {"volume": float(se.get("volume", 0.0)), "music": float(se.get("music", 0.0)),
|
||||
"ai_speed": float(se.get("ai_speed", 1.0)), "animations": bool(se.get("animations", true)),
|
||||
@@ -77,7 +99,7 @@ static func from_dict(d: Dictionary) -> Profile:
|
||||
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "collection": collection,
|
||||
return {"version": SAVE_VERSION, "coins": coins, "dust": dust, "collection": collection, "deck": deck,
|
||||
"campaign_stage": campaign_stage, "settings": settings, "perk": perk}
|
||||
|
||||
|
||||
@@ -97,7 +119,11 @@ func take_notices() -> Array[String]:
|
||||
return out
|
||||
|
||||
|
||||
# --- Sammlung: Seltenheit als Aussehen --------------------------------------------
|
||||
# --- Sammlung und Stapel -----------------------------------------------------------
|
||||
|
||||
func owns(cid: String) -> bool:
|
||||
return collection.has(cid)
|
||||
|
||||
|
||||
func level(cid: String) -> int:
|
||||
return int(collection.get(cid, 0))
|
||||
@@ -116,17 +142,29 @@ func looks() -> Dictionary:
|
||||
return out
|
||||
|
||||
|
||||
## Anzahl Karten über Common.
|
||||
func upgraded_count() -> int:
|
||||
var n := 0
|
||||
for cid in collection:
|
||||
if int(collection[cid]) > 0:
|
||||
n += 1
|
||||
return n
|
||||
## Der Stapel für den Kampf: der gewählte, mit weiteren eigenen Karten bis DECK_MIN aufgefüllt,
|
||||
## falls er zu klein ist (Karten können durch Zerlegen wegfallen).
|
||||
func deck_cards() -> Array:
|
||||
return DeckGen.fill(deck.filter(func(id): return collection.has(id)), collection.keys(), Game.DECK_MIN)
|
||||
|
||||
|
||||
## Karte in den Stapel oder heraus. Gibt "" zurück oder eine Fehlermeldung.
|
||||
func toggle_deck(cid: String) -> String:
|
||||
if cid in deck:
|
||||
deck.erase(cid)
|
||||
return ""
|
||||
if not owns(cid):
|
||||
return "Karte nicht in der Sammlung"
|
||||
if deck.size() >= Game.DECK_MAX:
|
||||
return "Der Stapel ist voll (%d Karten)" % Game.DECK_MAX
|
||||
deck.append(cid)
|
||||
return ""
|
||||
|
||||
|
||||
## Wertet eine Karte um eine Stufe auf. Gibt "" zurück oder eine Fehlermeldung.
|
||||
func upgrade(cid: String) -> String:
|
||||
if not owns(cid):
|
||||
return "Karte nicht in der Sammlung"
|
||||
var lvl := level(cid)
|
||||
if lvl >= Cards.RARITIES.size() - 1:
|
||||
return "Schon auf der höchsten Stufe"
|
||||
@@ -138,13 +176,29 @@ func upgrade(cid: String) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
## Gibt eine Stufe ab und bekommt Staub dafür. Gibt "" zurück oder eine Fehlermeldung.
|
||||
## Entzaubern: eine Stufe abgeben bringt Staub; eine Common-Karte verlässt die Sammlung.
|
||||
## Gibt "" zurück oder eine Fehlermeldung.
|
||||
func dismantle(cid: String) -> String:
|
||||
if not owns(cid):
|
||||
return "Karte nicht in der Sammlung"
|
||||
var lvl := level(cid)
|
||||
if lvl == 0:
|
||||
return "Common-Karten lassen sich nicht zerlegen"
|
||||
dust += DUST_DISMANTLE[Cards.RARITIES[lvl]]
|
||||
collection[cid] = lvl - 1
|
||||
if lvl == 0:
|
||||
collection.erase(cid)
|
||||
deck.erase(cid)
|
||||
else:
|
||||
collection[cid] = lvl - 1
|
||||
return ""
|
||||
|
||||
|
||||
## Herstellen: eine fehlende Karte für Staub als Common. Gibt "" zurück oder eine Fehlermeldung.
|
||||
func craft(cid: String) -> String:
|
||||
if owns(cid):
|
||||
return "Karte schon in der Sammlung"
|
||||
if dust < CRAFT_COST:
|
||||
return "Nicht genug Staub (%d nötig)" % CRAFT_COST
|
||||
dust -= CRAFT_COST
|
||||
collection[cid] = 0
|
||||
return ""
|
||||
|
||||
|
||||
@@ -164,8 +218,9 @@ static func roll_booster(r: RandomNumberGenerator) -> Array:
|
||||
return out
|
||||
|
||||
|
||||
## Öffnet einen Booster. Höhere Stufen werten die Karte auf, alles andere wird zu Staub.
|
||||
## Gibt die Ergebnisse zurück, leer wenn die Münzen nicht reichen.
|
||||
## Öffnet einen Booster. Neue Karten kommen in Sammlung und Stapel (solange Platz ist). Von jeder
|
||||
## Karte zählt die seltenste Version: eine höhere ersetzt die alte, die zu Staub wird; eine gleiche
|
||||
## oder niedrigere wird selbst zu Staub. Gibt die Ergebnisse zurück, leer wenn die Münzen nicht reichen.
|
||||
func open_booster(r: RandomNumberGenerator = null) -> Array:
|
||||
if coins < BOOSTER_COST:
|
||||
return []
|
||||
@@ -175,14 +230,20 @@ func open_booster(r: RandomNumberGenerator = null) -> Array:
|
||||
var out := roll_booster(r)
|
||||
for res in out:
|
||||
var lvl: int = Cards.RARITIES.find(res.rarity)
|
||||
if lvl > level(res.card.id):
|
||||
res["new"] = not owns(res.card.id)
|
||||
res["upgraded"] = false
|
||||
res["dust"] = 0
|
||||
if res.new:
|
||||
collection[res.card.id] = lvl
|
||||
if deck.size() < Game.DECK_MAX:
|
||||
deck.append(res.card.id)
|
||||
elif lvl > level(res.card.id):
|
||||
res["upgraded"] = true
|
||||
res["dust"] = 0
|
||||
res["dust"] = DUST_DISMANTLE[rarity(res.card.id)]
|
||||
collection[res.card.id] = lvl
|
||||
else:
|
||||
res["upgraded"] = false
|
||||
res["dust"] = DUST_DISMANTLE[res.rarity]
|
||||
dust += res.dust
|
||||
dust += res.dust
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ func _next_zone(step: int) -> void:
|
||||
func items() -> Array:
|
||||
match zone:
|
||||
"deck":
|
||||
return screen.offer_views
|
||||
return [screen.deck] if screen.deck.visible else []
|
||||
"hand":
|
||||
return screen.hand_views
|
||||
"own":
|
||||
@@ -105,7 +105,7 @@ func confirm() -> void:
|
||||
var item = items[index]
|
||||
match zone:
|
||||
"deck":
|
||||
screen.on_click("deck", index)
|
||||
screen.on_click("deck", null)
|
||||
"hand":
|
||||
screen.on_click("hand", item.card)
|
||||
"own":
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
class_name BattleLayout
|
||||
## Feste Bildschirmaufteilung des Duells (1280x800). Mitte von oben nach unten: Hand des Gegners
|
||||
## (Rückseiten), sein Brett, seine Elementleiste, die eigene Elementleiste (die Naht liegt auf halber
|
||||
## Höhe), eigenes Brett, eigene Hand. Links: Kasten des Gegners, die drei offenen Karten des
|
||||
## Stapels (mittig), eigener Kasten. Rechts oben die Knöpfe, rechts mittig die Vorschau, darunter Meldungen.
|
||||
## (Rückseiten), sein Brett, eigenes Brett, eigene Hand. Links: Kasten des Gegners, daneben die
|
||||
## Elementzähler beider Seiten je neben ihrem Brett, Zusatzzeile und Meldungen. Rechts von oben:
|
||||
## Stapel und Kasten des Gegners, die Vorschau, eigener Kasten und Stapel. Optionen per Esc.
|
||||
|
||||
const B_GIVEUP := Rect2(1070, 10, 96, 32)
|
||||
const B_LOG := Rect2(1172, 10, 96, 32)
|
||||
const B_END := Rect2(1070, 48, 198, 32)
|
||||
const OPP_BOX := Rect2(20, 11, 210, 96)
|
||||
const B_UNDO := Rect2(20, 10, 210, 32) # links oben: Rückgängig, darunter Überspringen (Gegnerzug)
|
||||
const B_SKIP := Rect2(20, 48, 210, 32)
|
||||
const OPP_DECK := Rect2(1090, 10, 130, 182) # Stapel des Gegners rechts oben
|
||||
const OPP_BOX := Rect2(1050, 200, 210, 70) # Kasten des Gegners darunter
|
||||
const OPP_HAND_Y := 11
|
||||
const FIELD_OPP := Rect2(240, 364, 800, 32)
|
||||
const FIELD := Rect2(240, 404, 800, 32)
|
||||
const MSG := Rect2(1040, 630, 220, 60)
|
||||
const STATUS := Rect2(20, 694, 210, 96)
|
||||
const B_UNDO := Rect2(1070, 86, 198, 32)
|
||||
const OPP_ELEMENTS := Rect2(20, 220, 210, 128) # Elementzähler des Gegners, links von seinem Brett
|
||||
const OWN_ELEMENTS := Rect2(20, 440, 210, 128) # eigene Elementzähler, links vom eigenen Brett
|
||||
const INFO := Rect2(20, 580, 210, 60) # Zusatzzeile zur Vorschau
|
||||
const MSG := Rect2(20, 650, 210, 80) # Meldungen
|
||||
const STATUS := Rect2(1050, 526, 210, 70) # eigener Kasten rechts über dem Stapel
|
||||
const DECK := Rect2(1090, 606, 130, 182) # eigener Stapel rechts unten
|
||||
const OPTIONS := Rect2(440, 180, 400, 420) # Optionsmenü (Esc)
|
||||
const OWN_X := 345
|
||||
const HAND_W := 110 # Handkarten so groß wie die Wesen auf dem Feld
|
||||
const HAND_H := 140
|
||||
const HAND_GAP := 10
|
||||
const HAND_Y := 610
|
||||
const HAND_ARC := 3.0 # Grad Neigung und Pixel Absenkung je Platz von der Mitte (leichter Bogen)
|
||||
const OFFER_X := 70 # die drei offenen Karten des Stapels, links übereinander um die Mitte
|
||||
const OFFER_Y := 180
|
||||
const OFFER_GAP := 10
|
||||
const DECK_LABEL := Rect2(20, 632, 210, 22)
|
||||
const LOG_PANEL := Rect2(240, 60, 800, 560)
|
||||
const PREVIEW_SIZE := Vector2(180, 252)
|
||||
const PREVIEW_SIZE := Vector2(170, 238)
|
||||
const CENTER_X := 240
|
||||
const CENTER_W := 800
|
||||
|
||||
@@ -61,11 +59,11 @@ static func hand_arc(count: int, i: int, mirrored: bool = false) -> Array:
|
||||
return [t * HAND_ARC * sign, t * t * HAND_ARC * sign]
|
||||
|
||||
|
||||
## Vorschau der gezeigten Karte: fest rechts auf halber Höhe.
|
||||
## Vorschau der gezeigten Karte: fest rechts zwischen den beiden Kästen.
|
||||
static func preview_rect(_anchor: Rect2) -> Rect2:
|
||||
return Rect2(Vector2(1050, 400 - PREVIEW_SIZE.y / 2), PREVIEW_SIZE)
|
||||
return Rect2(Vector2(1070, 278), PREVIEW_SIZE)
|
||||
|
||||
|
||||
## Zusatzzeile (Elementbonus, Eigenschaften) unter der Vorschau.
|
||||
static func info_rect(preview: Rect2) -> Rect2:
|
||||
return Rect2(1030, preview.end.y + 8, 230, 80)
|
||||
## Zusatzzeile (Elementbonus, Eigenschaften) links unter den Elementzählern.
|
||||
static func info_rect(_preview: Rect2) -> Rect2:
|
||||
return INFO
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
class_name BattleScreen
|
||||
extends Control
|
||||
## Kampf: Zustand, Eingabe und Abgleich der Ansicht. Layout in BattleLayout, Animationen in BattleFx.
|
||||
## Esc öffnet die Optionen (Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden); den Zug beendet
|
||||
## die Leertaste oder der Rechtsklick ins Leere.
|
||||
## Mittlere Maustaste auf eine Handkarte legt sie verdeckt. Rechtsklick auf eine Handkarte oder
|
||||
## ein eigenes Wesen öffnet das Kartenmenü: verdeckt legen, aufdecken, Eigenschaften kaufen.
|
||||
## Verschmelzen: eine Karte auf eine andere ziehen und dort eine Sekunde stillhalten. Mit
|
||||
## Gamepad öffnet Y das Menü, dort zusätzlich mit „Verschmelzen mit …“.
|
||||
## Modi: Einzelspieler, Kampagne, Netzwerk (Host, Mitspieler oder Zuschauer).
|
||||
## Modi: Einzelspieler und Kampagne.
|
||||
|
||||
var app
|
||||
var stage := -1 # Kampagnenstufe oder -1
|
||||
var net_play := false # Netzwerkspiel (auch als Zuschauer)
|
||||
var online_seat := -1 # Netzwerk: eigener Platz, Zuschauer -1
|
||||
var net: Net
|
||||
var game: Game
|
||||
var perspective: Game.Player # der Mensch, dessen Hand unten liegt
|
||||
var fx: BattleFx
|
||||
@@ -60,12 +59,10 @@ var flash_until := {} # Spielerindex -> Zeitpunkt (ms)
|
||||
var hero_layer: Control
|
||||
var tile_layer: Control
|
||||
var hand_layer: Control
|
||||
var offer_views: Array = [] # die offenen Karten des Stapels (CardView)
|
||||
var offer_layer: Control
|
||||
var deck_label: Label
|
||||
var draw_enabled := false
|
||||
var deck: Deck
|
||||
var opp_deck: Deck # Stapel des Gegners, nur zum Ansehen
|
||||
var log_overlay: Control
|
||||
var b_log: Button
|
||||
var options: Control # Optionsmenü (Esc): Verlauf, Aufgeben, Einstellungen, Hauptmenü, Beenden
|
||||
var status_panel: Panel
|
||||
var fx_layer: Control
|
||||
var log_box: RichTextLabel
|
||||
@@ -74,8 +71,6 @@ var name_label: Label
|
||||
var life_label: Label
|
||||
var energy_label: Label
|
||||
var msg_label: Label
|
||||
var b_end: Button
|
||||
var b_giveup: Button
|
||||
var element_view: ElementView # eigene Elementleiste
|
||||
var opp_element_view: ElementView # die des Gegners darüber
|
||||
var info_label: Label # Vorschau: Elementbonus, Merkmale
|
||||
@@ -89,7 +84,6 @@ var reward_label: Label
|
||||
var notice_label: Label
|
||||
var confirm: Control
|
||||
var cursor_layer: Control
|
||||
var wait_label: Label
|
||||
var b_skip: Button
|
||||
var b_undo: Button
|
||||
|
||||
@@ -98,30 +92,6 @@ func _init(campaign_stage: int = -1) -> void:
|
||||
stage = campaign_stage
|
||||
|
||||
|
||||
static func online(seat: int) -> BattleScreen:
|
||||
var b := BattleScreen.new(-1)
|
||||
b.net_play = true
|
||||
b.online_seat = seat
|
||||
return b
|
||||
|
||||
|
||||
func is_online() -> bool:
|
||||
return net_play
|
||||
|
||||
|
||||
## Zuschauer im Netzwerk: kein eigener Platz, keine Hand.
|
||||
func spectating() -> bool:
|
||||
return is_online() and online_seat < 0
|
||||
|
||||
|
||||
func is_client() -> bool:
|
||||
return is_online() and not net.is_host
|
||||
|
||||
|
||||
func counts_for_profile() -> bool:
|
||||
return not is_online()
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var s: Dictionary = app.profile.settings
|
||||
fx = BattleFx.new(self)
|
||||
@@ -129,31 +99,19 @@ func _ready() -> void:
|
||||
pad = BattleGamepad.new(self)
|
||||
ai_delay = 0.75 / float(s.ai_speed)
|
||||
_build_static()
|
||||
if is_online():
|
||||
net = app.net
|
||||
net.ended.connect(_on_net_ended)
|
||||
net.migrated.connect(_on_migrated)
|
||||
net.status_changed.connect(_on_net_status)
|
||||
if net.is_host:
|
||||
game = net.game
|
||||
net.action_applied.connect(after_action)
|
||||
else:
|
||||
net.snapshot_received.connect(_on_snapshot)
|
||||
wait_label.visible = true
|
||||
return # der erste Zustand kommt vom Host
|
||||
elif stage >= 0:
|
||||
if stage >= 0:
|
||||
var r := Game.make_rng()
|
||||
game = Game.new_battle_with(Campaign.opponent(stage), r, false, own_rules())
|
||||
game = Game.new_battle_with(Campaign.opponent(stage, r), r, false, own_rules())
|
||||
else:
|
||||
game = Game.new_battle(Game.make_rng(), own_rules())
|
||||
smart_ai = bool(app.profile.difficulty().smart) or not counts_for_profile()
|
||||
smart_ai = bool(app.profile.difficulty().smart)
|
||||
_begin()
|
||||
|
||||
|
||||
## Kartenoptik, Gegnerleben (Schwierigkeit) und das eigene Merkmal (nach Kapitel 1).
|
||||
## Eigener Stapel und Kartenstufen, Gegnerleben (Schwierigkeit) und das eigene Merkmal (nach Kapitel 1).
|
||||
func own_rules() -> Dictionary:
|
||||
var p: Profile = app.profile
|
||||
var rules := {"looks": p.looks()}
|
||||
var rules := {"looks": p.looks(), "deck": p.deck_cards()}
|
||||
if stage < 0:
|
||||
rules["opponent_life"] = int(p.difficulty().life)
|
||||
if p.perk != "" and p.perk_unlocked():
|
||||
@@ -162,37 +120,22 @@ func own_rules() -> Dictionary:
|
||||
|
||||
|
||||
func _begin() -> void:
|
||||
if is_online():
|
||||
perspective = game.players[maxi(online_seat, 0)]
|
||||
else:
|
||||
game.events.clear()
|
||||
perspective = game.current() if game.current().is_human else game.humans[0]
|
||||
game.events.clear()
|
||||
perspective = game.human
|
||||
build_hero_boxes()
|
||||
if is_online() and net.is_host:
|
||||
net.broadcast()
|
||||
game.events.clear()
|
||||
refresh()
|
||||
announce_turn()
|
||||
|
||||
|
||||
func _build_static() -> void:
|
||||
var L := BattleLayout
|
||||
# oben links: Aufgeben und Verlauf, klein
|
||||
b_giveup = Style.button(self, "Aufgeben", L.B_GIVEUP, func(): confirm.visible = true)
|
||||
b_giveup.add_theme_font_size_override("font_size", 13)
|
||||
b_log = Style.button(self, "Verlauf", L.B_LOG, toggle_log)
|
||||
b_log.add_theme_font_size_override("font_size", 13)
|
||||
b_end = Style.button(self, "Zug beenden", L.B_END, end_turn, Style.PRIMARY)
|
||||
b_end.add_theme_font_size_override("font_size", 15)
|
||||
b_skip = Style.button(self, "Überspringen", L.B_END, func(): skip_ai = true)
|
||||
b_skip.add_theme_font_size_override("font_size", 15)
|
||||
b_skip = Style.button(self, "Überspringen", L.B_SKIP, func(): skip_ai = true)
|
||||
b_skip.add_theme_font_size_override("font_size", 13)
|
||||
b_skip.visible = false
|
||||
# Mitte: Elementzähler, Meldung, eigenes Brett mit fünf Plätzen, eigene Leiste links daneben
|
||||
Style.panel(self, L.FIELD_OPP, Style.darker(Style.PANEL, 0.9), 8)
|
||||
opp_element_view = ElementView.new(L.FIELD_OPP)
|
||||
# Elementzähler beider Seiten links neben ihrem Brett
|
||||
opp_element_view = ElementView.new(L.OPP_ELEMENTS)
|
||||
add_child(opp_element_view)
|
||||
Style.panel(self, L.FIELD, Style.darker(Style.PANEL, 0.9), 8)
|
||||
element_view = ElementView.new(L.FIELD)
|
||||
element_view = ElementView.new(L.OWN_ELEMENTS)
|
||||
add_child(element_view)
|
||||
msg_label = Style.label(self, "", 14, Style.ORANGE, L.MSG, "center", true)
|
||||
msg_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
@@ -207,18 +150,21 @@ func _build_static() -> void:
|
||||
var sx: float = L.STATUS.position.x
|
||||
var sy: float = L.STATUS.position.y
|
||||
var sw: float = L.STATUS.size.x
|
||||
name_label = Style.label(self, "", 18, Style.TEXT, Rect2(sx + 8, sy + 6, sw - 16, 26), "center", true)
|
||||
name_label = Style.label(self, "", 16, Style.TEXT, Rect2(sx + 8, sy + 4, sw - 16, 22), "center", true)
|
||||
name_label.clip_text = true
|
||||
life_label = Style.label(self, "", 30, Style.RED, Rect2(sx + 8, sy + 42, sw / 2 - 8, 40), "left", true)
|
||||
energy_label = Style.label(self, "", 30, Style.BLUE, Rect2(sx + sw / 2, sy + 42, sw / 2 - 8, 40), "right", true)
|
||||
life_label = Style.label(self, "", 26, Style.RED, Rect2(sx + 10, sy + 30, sw / 2 - 10, 34), "left", true)
|
||||
energy_label = Style.label(self, "", 26, Style.BLUE, Rect2(sx + sw / 2, sy + 30, sw / 2 - 10, 34), "right", true)
|
||||
b_undo = Style.button(self, "Rückgängig", L.B_UNDO, undo)
|
||||
b_undo.add_theme_font_size_override("font_size", 13)
|
||||
b_undo.visible = false
|
||||
deck_label = Style.label(self, "", 13, Style.MUTED, L.DECK_LABEL, "center")
|
||||
wait_label = Style.label(self, "Warte auf den Host …", 22, Style.MUTED, Rect2(0, 300, 1280, 40), "center")
|
||||
wait_label.visible = false
|
||||
deck = Deck.new(L.DECK)
|
||||
deck.clicked.connect(func(): on_click("deck", null))
|
||||
add_child(deck)
|
||||
opp_deck = Deck.new(L.OPP_DECK)
|
||||
opp_deck.interactive = false
|
||||
opp_deck.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(opp_deck)
|
||||
hero_layer = _layer()
|
||||
offer_layer = _layer()
|
||||
tile_layer = _layer()
|
||||
hand_layer = _layer()
|
||||
preview_holder = _layer()
|
||||
@@ -252,6 +198,28 @@ func _build_static() -> void:
|
||||
log_box.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
log_overlay.add_child(log_box)
|
||||
Style.button(log_overlay, "Schließen", Rect2(L.LOG_PANEL.end.x - 180, L.LOG_PANEL.end.y - 52, 160, 40), func(): log_overlay.visible = false)
|
||||
# Optionsmenü (Esc)
|
||||
options = Style.overlay(self)
|
||||
options.get_child(0).gui_input.connect(func(ev):
|
||||
if ev is InputEventMouseButton and ev.pressed:
|
||||
options.visible = false)
|
||||
Style.panel(options, L.OPTIONS, Style.PANEL, 10, Style.GOLD, 2)
|
||||
Style.label(options, "Optionen", 24, Style.GOLD, Rect2(L.OPTIONS.position.x, L.OPTIONS.position.y + 18, L.OPTIONS.size.x, 34), "center", true)
|
||||
var oy := L.OPTIONS.position.y + 76
|
||||
for entry in [["Verlauf", func():
|
||||
options.visible = false
|
||||
log_overlay.visible = true],
|
||||
["Aufgeben", func():
|
||||
options.visible = false
|
||||
confirm.visible = true],
|
||||
["Einstellungen", func():
|
||||
options.visible = false
|
||||
app.show_screen(SettingsScreen.new(), true)],
|
||||
["Hauptmenü", leave],
|
||||
["Beenden", func(): get_tree().quit()]]:
|
||||
var ob := Style.button(options, entry[0], Rect2(L.OPTIONS.position.x + 60, oy, L.OPTIONS.size.x - 120, 48), entry[1])
|
||||
ob.add_theme_font_size_override("font_size", 18)
|
||||
oy += 60
|
||||
# Spielende
|
||||
overlay = _overlay()
|
||||
result_label = Style.label(overlay, "", 56, Style.GREEN, Rect2(0, 250, 1280, 80), "center", true)
|
||||
@@ -261,8 +229,8 @@ func _build_static() -> void:
|
||||
confirm = Style.confirm(self, "Wirklich aufgeben?", "", "Ja, aufgeben", leave, "Weiterspielen")
|
||||
|
||||
|
||||
func toggle_log() -> void:
|
||||
log_overlay.visible = not log_overlay.visible
|
||||
func toggle_options() -> void:
|
||||
options.visible = not options.visible
|
||||
|
||||
|
||||
func _layer() -> Control:
|
||||
@@ -277,8 +245,6 @@ func _overlay() -> Control:
|
||||
|
||||
|
||||
func leave() -> void:
|
||||
if is_online():
|
||||
net.leave()
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
@@ -336,24 +302,22 @@ func build_hero_boxes() -> void:
|
||||
var title: String = ("%s · %s" % [o.name, o.title]) if o.title != "" else o.name
|
||||
if not o.perks.is_empty():
|
||||
title += " ★"
|
||||
labels["name"] = Style.label(hero_layer, title, 18, Style.TEXT, Rect2(box.position.x + 8, box.position.y + 6, box.size.x - 16, 26), "center", true)
|
||||
labels["name"] = Style.label(hero_layer, title, 16, Style.TEXT, Rect2(box.position.x + 8, box.position.y + 4, box.size.x - 16, 22), "center", true)
|
||||
labels.name.clip_text = true
|
||||
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
labels["life"] = Style.label(hero_layer, "", 30, Style.RED, Rect2(box.position.x + 8, box.position.y + 42, box.size.x / 2 - 8, 40), "left", true)
|
||||
labels["stats"] = Style.label(hero_layer, "", 30, Style.BLUE, Rect2(box.position.x + box.size.x / 2, box.position.y + 42, box.size.x / 2 - 8, 40), "right", true)
|
||||
labels["life"] = Style.label(hero_layer, "", 26, Style.RED, Rect2(box.position.x + 10, box.position.y + 30, box.size.x / 2 - 10, 34), "left", true)
|
||||
labels["stats"] = Style.label(hero_layer, "", 26, Style.BLUE, Rect2(box.position.x + box.size.x / 2, box.position.y + 30, box.size.x / 2 - 10, 34), "right", true)
|
||||
hero_labels[o] = labels
|
||||
|
||||
|
||||
# --- Ablauf und Eingabe ------------------------------------------------------------------
|
||||
|
||||
func my_turn() -> bool:
|
||||
if spectating():
|
||||
return false
|
||||
return game != null and game.winner == "" and game.current() == perspective
|
||||
|
||||
|
||||
func any_overlay() -> bool:
|
||||
return confirm.visible or overlay.visible or log_overlay.visible or menu_layer.visible
|
||||
return confirm.visible or overlay.visible or log_overlay.visible or menu_layer.visible or options.visible
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
@@ -400,10 +364,12 @@ func _input(event: InputEvent) -> void:
|
||||
close_menu()
|
||||
elif log_overlay.visible:
|
||||
log_overlay.visible = false
|
||||
elif options.visible:
|
||||
options.visible = false
|
||||
elif selected != null or fuse_first != null:
|
||||
cancel_all()
|
||||
elif not any_overlay():
|
||||
confirm.visible = true
|
||||
options.visible = true
|
||||
KEY_SPACE:
|
||||
if my_turn() and not any_overlay():
|
||||
end_turn()
|
||||
@@ -451,8 +417,6 @@ func _process(delta: float) -> void:
|
||||
_end_drag()
|
||||
do_fuse(a, b)
|
||||
after_action()
|
||||
if is_online() and net.is_host:
|
||||
net.tick()
|
||||
if game.winner != "":
|
||||
if not ended:
|
||||
ended = true
|
||||
@@ -461,8 +425,6 @@ func _process(delta: float) -> void:
|
||||
var cur := game.current()
|
||||
if cur.is_human:
|
||||
return
|
||||
if is_client():
|
||||
return
|
||||
if skip_ai:
|
||||
ai_timer = 0.0
|
||||
else:
|
||||
@@ -506,15 +468,10 @@ func _exit_tree() -> void:
|
||||
|
||||
func _finish() -> void:
|
||||
var won := game.winner == "human"
|
||||
if counts_for_profile():
|
||||
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
|
||||
reward = app.profile.record_battle(won, stage, factor)
|
||||
app.save()
|
||||
app.sfx.play("win" if won else "lose")
|
||||
else:
|
||||
var wp := game.winner_player
|
||||
var mine: bool = wp != null and wp == perspective
|
||||
app.sfx.play("win" if mine else "lose")
|
||||
var factor: float = float(app.profile.difficulty().reward) if stage < 0 else 1.0
|
||||
reward = app.profile.record_battle(won, stage, factor)
|
||||
app.save()
|
||||
app.sfx.play("win" if won else "lose")
|
||||
refresh()
|
||||
|
||||
|
||||
@@ -631,10 +588,7 @@ func fusion_partners(target) -> Array:
|
||||
|
||||
|
||||
func play(card: Cards.Card, face_down: bool = false) -> void:
|
||||
if is_client():
|
||||
net.send_action({"type": "play", "hand": perspective.hand.find(card), "face_down": face_down})
|
||||
return
|
||||
var before: Game = game.clone() if not is_online() else null
|
||||
var before: Game = game.clone()
|
||||
var log_len := game.log.size()
|
||||
var from := _hand_rect_of(card)
|
||||
var err := game.play_card(perspective, card, face_down)
|
||||
@@ -655,9 +609,6 @@ func play(card: Cards.Card, face_down: bool = false) -> void:
|
||||
## Deckt ein eigenes verdecktes Wesen auf (kostenlos).
|
||||
func do_reveal(cr: Game.Creature) -> void:
|
||||
clear_undo()
|
||||
if is_client():
|
||||
net.send_action({"type": "reveal", "uid": cr.uid})
|
||||
return
|
||||
var err := game.reveal(perspective, cr)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
@@ -667,7 +618,7 @@ func do_reveal(cr: Game.Creature) -> void:
|
||||
## Kartenmenü neben einer Handkarte oder einem eigenen Wesen. full: auch verschmelzen anbieten
|
||||
## (Gamepad, das nicht ziehen kann).
|
||||
func open_menu(target, anchor: Rect2, full: bool = false) -> void:
|
||||
if spectating() or not my_turn():
|
||||
if not my_turn():
|
||||
return
|
||||
cancel()
|
||||
Style.clear(menu_layer)
|
||||
@@ -685,7 +636,7 @@ func open_menu(target, anchor: Rect2, full: bool = false) -> void:
|
||||
entries.append([tr("Aufdecken"), func(): do_reveal(target), game.can_reveal(perspective, target)[0]])
|
||||
if full:
|
||||
var partners := fusion_partners(target)
|
||||
entries.append([tr("Verschmelzen mit … ◆ %d") % card.fusion_cost(), func():
|
||||
entries.append([tr("Verschmelzen mit …"), func():
|
||||
fuse_first = target
|
||||
say(tr("Ziel wählen"))
|
||||
refresh(), not partners.is_empty()])
|
||||
@@ -729,22 +680,16 @@ func close_menu() -> void:
|
||||
## Abwerfen: eine Handkarte gegen Mana loswerden.
|
||||
func do_discard(card: Cards.Card) -> void:
|
||||
clear_undo()
|
||||
if is_client():
|
||||
net.send_action({"type": "discard", "hand": perspective.hand.find(card)})
|
||||
return
|
||||
var err := game.discard(perspective, card)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
after_action()
|
||||
|
||||
|
||||
## Ziehen: kostet Mana, eine Karte kommt auf die Hand.
|
||||
func do_draw(index: int = 0) -> void:
|
||||
## Ziehen: eine Karte vom eigenen Stapel, die erste im Zug frei.
|
||||
func do_draw() -> void:
|
||||
clear_undo()
|
||||
if is_client():
|
||||
net.send_action({"type": "draw", "index": index})
|
||||
return
|
||||
var err := game.draw(perspective, index)
|
||||
var err := game.draw(perspective)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
else:
|
||||
@@ -755,12 +700,6 @@ func do_draw(index: int = 0) -> void:
|
||||
func do_fuse(a, b) -> void:
|
||||
cancel()
|
||||
clear_undo()
|
||||
if is_client():
|
||||
if a is Cards.Card:
|
||||
net.send_action({"type": "fuse", "hand": perspective.hand.find(a), "other_hand": perspective.hand.find(b)})
|
||||
else:
|
||||
net.send_action({"type": "fuse", "uid": a.uid, "other": b.uid})
|
||||
return
|
||||
var err := game.fuse(perspective, a, b)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
@@ -784,8 +723,6 @@ func undo() -> void:
|
||||
restored.say("Rückgängig.")
|
||||
game = restored
|
||||
perspective = game.players[idx]
|
||||
if is_online() and net.is_host:
|
||||
net.game = game
|
||||
clear_undo()
|
||||
cancel()
|
||||
build_hero_boxes()
|
||||
@@ -797,9 +734,6 @@ func end_turn() -> void:
|
||||
return
|
||||
cancel()
|
||||
clear_undo()
|
||||
if is_client():
|
||||
net.send_action({"type": "end"})
|
||||
return
|
||||
game.end_turn()
|
||||
ai_timer = ai_delay
|
||||
after_action()
|
||||
@@ -823,17 +757,14 @@ func on_click(kind: String, obj) -> void:
|
||||
selected = null
|
||||
if kind == "enemy" or kind == "hero":
|
||||
clear_undo()
|
||||
if is_client():
|
||||
net.send_action({"type": "attack", "uid": cr.uid, "target": Net.ref_dict(obj)})
|
||||
else:
|
||||
var err := g.attack(cr, obj)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
var err := g.attack(cr, obj)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
after_action()
|
||||
return
|
||||
match kind:
|
||||
"deck":
|
||||
do_draw(int(obj) if obj != null else 0)
|
||||
do_draw()
|
||||
"hand":
|
||||
var ok := g.can_play(perspective, obj)
|
||||
if not ok[0]:
|
||||
@@ -949,6 +880,41 @@ func update_attack_preview() -> void:
|
||||
v.queue_redraw()
|
||||
|
||||
|
||||
## Der eigene Stapel: Kartenstoß mit Kosten des nächsten Ziehens und Restzahl.
|
||||
class Deck extends Control:
|
||||
signal clicked
|
||||
var enabled := true
|
||||
var interactive := true # false: der Stapel des Gegners, nur Anzahl
|
||||
var count := 0
|
||||
var cost := 0
|
||||
|
||||
func _init(rect: Rect2) -> void:
|
||||
position = rect.position
|
||||
size = rect.size
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
accept_event()
|
||||
clicked.emit()
|
||||
|
||||
func _draw() -> void:
|
||||
for k in range(2, -1, -1):
|
||||
var r := Rect2(Vector2(k * 3, -k * 3), size - Vector2(6, 12))
|
||||
r.position.y += 6
|
||||
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD if enabled and interactive else Style.MUTED, 2), r)
|
||||
if k == 0:
|
||||
Style.border(self, r.grow(-12), Color("5a3c82"), 2, 6)
|
||||
if interactive:
|
||||
Style.text(self, tr("Ziehen"), 16, Style.TEXT if enabled else Style.MUTED, r.get_center() + Vector2(0, -12), "center", true)
|
||||
Style.text(self, "◆ %d" % cost, 20, Style.BLUE if enabled else Style.MUTED, r.get_center() + Vector2(0, 14), "center", true)
|
||||
Style.text(self, str(count), 12, Style.MUTED, Vector2(r.get_center().x, r.end.y - 18), "center")
|
||||
else:
|
||||
Style.text(self, str(count), 22, Style.TEXT, r.get_center(), "center", true)
|
||||
if interactive and not enabled:
|
||||
draw_rect(Rect2(Vector2.ZERO, size), Color(0, 0, 0, 0.35))
|
||||
|
||||
|
||||
## Eine verdeckte Handkarte des Gegners in voller Größe.
|
||||
class CardBack extends Control:
|
||||
func _init(rect: Rect2) -> void:
|
||||
@@ -967,51 +933,10 @@ class CardBack extends Control:
|
||||
|
||||
|
||||
|
||||
# --- Netzwerk -------------------------------------------------------------------------------
|
||||
|
||||
func _on_snapshot(snap: Dictionary) -> void:
|
||||
var first := game == null
|
||||
game = Game.from_snapshot(snap)
|
||||
wait_label.visible = false
|
||||
if first:
|
||||
_begin()
|
||||
return
|
||||
perspective = game.players[maxi(online_seat, 0)]
|
||||
build_hero_boxes()
|
||||
after_action()
|
||||
|
||||
|
||||
## Host-Wechsel: das Spiel läuft jetzt auf diesem Rechner weiter.
|
||||
func _on_migrated() -> void:
|
||||
game = net.game
|
||||
if not net.action_applied.is_connected(after_action):
|
||||
net.action_applied.connect(after_action)
|
||||
wait_label.visible = false
|
||||
perspective = game.players[online_seat]
|
||||
build_hero_boxes()
|
||||
say("Du bist jetzt Host.")
|
||||
after_action()
|
||||
|
||||
|
||||
func _on_net_status() -> void:
|
||||
wait_label.text = net.status
|
||||
wait_label.visible = true
|
||||
|
||||
|
||||
func _on_net_ended(reason: String) -> void:
|
||||
if game == null or game.winner == "":
|
||||
say(reason)
|
||||
await get_tree().create_timer(1.5).timeout
|
||||
if is_inside_tree():
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
# --- Ereignisse ----------------------------------------------------------------------------
|
||||
|
||||
## Nach jeder Aktion: Ereignisse sichern, Ansicht angleichen, Animationen abspielen.
|
||||
func after_action() -> void:
|
||||
if is_online() and net.is_host:
|
||||
net.broadcast()
|
||||
var evs := game.events.duplicate()
|
||||
game.events.clear()
|
||||
var centers := {}
|
||||
@@ -1138,23 +1063,12 @@ func refresh() -> void:
|
||||
tiles.erase(uid)
|
||||
fx.fade_out(t)
|
||||
update_attack_preview()
|
||||
Style.clear(offer_layer)
|
||||
offer_views = []
|
||||
draw_enabled = mine and g.can_draw(h)[0]
|
||||
if not spectating():
|
||||
for i in h.offer.size():
|
||||
var oc: Cards.Card = h.offer[i]
|
||||
var orect := Rect2(BattleLayout.OFFER_X, BattleLayout.OFFER_Y + i * (BattleLayout.HAND_H + BattleLayout.OFFER_GAP), BattleLayout.HAND_W, BattleLayout.HAND_H)
|
||||
var ov := CardView.new(oc, orect.size, false, oc.rarity_in(h.looks))
|
||||
ov.position = orect.position
|
||||
ov.dim = not draw_enabled
|
||||
var idx := i
|
||||
ov.clicked.connect(func(_v): on_click("deck", idx))
|
||||
ov.hover.connect(func(c, e): show_preview(c, e, h.looks, "", orect))
|
||||
offer_layer.add_child(ov)
|
||||
offer_views.append(ov)
|
||||
deck_label.text = "%s ◆ %d · %d" % [tr("Ziehen"), g.draw_cost(h), g.deck.size()]
|
||||
deck_label.visible = not spectating()
|
||||
deck.enabled = mine and g.can_draw(h)[0]
|
||||
deck.count = h.deck.size()
|
||||
deck.cost = g.draw_cost(h)
|
||||
deck.queue_redraw()
|
||||
opp_deck.count = opponent().deck.size()
|
||||
opp_deck.queue_redraw()
|
||||
|
||||
Style.clear(hand_layer)
|
||||
for o in others(): # die Hand des Gegners oben, nur Rückseiten, spiegelverkehrter Bogen
|
||||
@@ -1165,9 +1079,7 @@ func refresh() -> void:
|
||||
back.rotation_degrees = arc[0]
|
||||
back.position.y += arc[1]
|
||||
hand_layer.add_child(back)
|
||||
if spectating():
|
||||
Style.label(hand_layer, "Zuschauer: Hände sind verdeckt", 18, Style.MUTED, Rect2(240, BattleLayout.HAND_Y + 18, 800, 30), "center")
|
||||
else:
|
||||
if true:
|
||||
for i in h.hand.size():
|
||||
var card: Cards.Card = h.hand[i]
|
||||
var playable: bool = mine and g.can_play(h, card)[0]
|
||||
@@ -1199,10 +1111,8 @@ func refresh() -> void:
|
||||
name_label.text = h.name
|
||||
life_label.text = "♥ %d" % maxi(h.life, 0)
|
||||
energy_label.text = "◆ %d" % h.energy
|
||||
b_end.disabled = not mine
|
||||
var ai_running: bool = g.winner == "" and not g.current().is_human and not is_client()
|
||||
var ai_running: bool = g.winner == "" and not g.current().is_human
|
||||
b_skip.visible = ai_running
|
||||
b_end.visible = not ai_running and not spectating()
|
||||
b_undo.visible = undo_state != null and mine
|
||||
element_view.update_from(g, h)
|
||||
opp_element_view.update_from(g, opponent())
|
||||
@@ -1222,20 +1132,13 @@ func refresh() -> void:
|
||||
pad.draw_cursor()
|
||||
|
||||
if g.winner != "":
|
||||
var wp := g.winner_player
|
||||
if counts_for_profile():
|
||||
var won := g.winner == "human"
|
||||
result_label.text = "Sieg!" if won else "Niederlage"
|
||||
result_label.add_theme_color_override("font_color", Style.GREEN if won else Style.RED)
|
||||
reward_label.text = "+%d Münzen" % maxi(reward, 0)
|
||||
var notes: Array[String] = app.profile.take_notices()
|
||||
if not notes.is_empty():
|
||||
notice_label.text = "\n".join(notes)
|
||||
else:
|
||||
var mine_win: bool = wp != null and wp == perspective
|
||||
result_label.text = ("%s gewinnt!" % wp.name) if wp != null and wp.is_human else "Die KI gewinnt"
|
||||
result_label.add_theme_color_override("font_color", Style.GREEN if mine_win else Style.RED)
|
||||
reward_label.text = "Gemeinsame Partie, keine Münzen"
|
||||
var won := g.winner == "human"
|
||||
result_label.text = "Sieg!" if won else "Niederlage"
|
||||
result_label.add_theme_color_override("font_color", Style.GREEN if won else Style.RED)
|
||||
reward_label.text = "+%d Münzen" % maxi(reward, 0)
|
||||
var notes: Array[String] = app.profile.take_notices()
|
||||
if not notes.is_empty():
|
||||
notice_label.text = "\n".join(notes)
|
||||
overlay.visible = true
|
||||
confirm.visible = false
|
||||
|
||||
|
||||
@@ -137,13 +137,15 @@ func refresh() -> void:
|
||||
elif revealed.has(i):
|
||||
var res: Dictionary = cards[i]
|
||||
var c: Cards.Card = res.card
|
||||
var v := CardView.new(c, r.size, false, res.rarity)
|
||||
var v := CardView.new(c.leveled(Cards.RARITIES.find(res.rarity)), r.size, false, res.rarity)
|
||||
v.position = r.position
|
||||
slots.add_child(v)
|
||||
if fresh.has(i):
|
||||
flip(v, fresh[i], res.rarity)
|
||||
Style.label(slots, tr(res.rarity), 16, Cards.RARITY_COLORS[res.rarity], Rect2(r.position.x, r.end.y + 8, r.size.x, 24), "center", true)
|
||||
if res.upgraded:
|
||||
if res.get("new", false):
|
||||
Style.label(slots, "Neu!", 16, Style.GOLD, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
|
||||
elif res.upgraded:
|
||||
Style.label(slots, "Aufgewertet", 16, Style.GREEN, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center", true)
|
||||
else:
|
||||
Style.label(slots, "+%d Staub" % res.dust, 14, Style.MUTED, Rect2(r.position.x, r.end.y + 32, r.size.x, 24), "center")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class_name CollectionScreen
|
||||
extends Control
|
||||
## Sammlung: alle Karten gehören dir. Gesammelt wird die Seltenheitsstufe — reine Optik.
|
||||
## Aufwerten kostet Staub, eine Stufe abgeben bringt Staub.
|
||||
## Sammlung: alle Karten des Spiels, die fehlenden abgedunkelt. Eigene Karten lassen sich
|
||||
## aufwerten (Staub) oder zerlegen (bringt Staub), fehlende herstellen (Staub).
|
||||
|
||||
var app
|
||||
var filter := ""
|
||||
@@ -15,6 +15,7 @@ var preview_holder: Control
|
||||
var info_label: Label
|
||||
var b_dismantle: Button
|
||||
var b_upgrade: Button
|
||||
var b_craft: Button
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -43,6 +44,7 @@ func _ready() -> void:
|
||||
info_label = Style.label(self, "", 15, Style.MUTED, Rect2(840, 476, 380, 30), "center")
|
||||
b_upgrade = Style.button(self, "", Rect2(840, 530, 380, 46), upgrade, Style.PRIMARY)
|
||||
b_dismantle = Style.button(self, "", Rect2(840, 586, 380, 46), dismantle, Color("5a3c28"))
|
||||
b_craft = Style.button(self, "", Rect2(840, 530, 380, 46), craft, Style.PRIMARY)
|
||||
msg_label = Style.label(self, "", 15, Style.GREEN, Rect2(840, 650, 380, 48), "center")
|
||||
refresh()
|
||||
|
||||
@@ -57,7 +59,7 @@ func set_filter(e: String) -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
## Aufgewertete Karten zuerst, sonst in Kartenreihenfolge.
|
||||
## Eigene Karten zuerst (höhere Stufen vorn), dann die fehlenden, sonst in Kartenreihenfolge.
|
||||
func cards() -> Array:
|
||||
var out: Array = Cards.all().duplicate()
|
||||
if filter != "":
|
||||
@@ -69,7 +71,10 @@ func cards() -> Array:
|
||||
var order := {}
|
||||
for i in out.size():
|
||||
order[out[i].id] = i
|
||||
out.sort_custom(func(a, b): return p.level(a.id) > p.level(b.id) if p.level(a.id) != p.level(b.id) else order[a.id] < order[b.id])
|
||||
out.sort_custom(func(a, b):
|
||||
if p.owns(a.id) != p.owns(b.id):
|
||||
return p.owns(a.id)
|
||||
return p.level(a.id) > p.level(b.id) if p.level(a.id) != p.level(b.id) else order[a.id] < order[b.id])
|
||||
return out
|
||||
|
||||
|
||||
@@ -111,11 +116,31 @@ func dismantle() -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
func craft() -> void:
|
||||
if selected == null:
|
||||
return
|
||||
var err: String = app.profile.craft(selected.id)
|
||||
if err == "":
|
||||
app.sfx.play("combo")
|
||||
app.save()
|
||||
say(tr("%s hergestellt") % selected.title(), true)
|
||||
else:
|
||||
say(err, false)
|
||||
refresh()
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
dust_label.text = "Staub: %d" % p.dust
|
||||
grid.fill(cards(), func(c): return p.rarity(c.id))
|
||||
var shown := cards()
|
||||
grid.fill(shown.map(func(c): return c.leveled(p.level(c.id))), func(c): return Cards.RARITIES[c.look])
|
||||
var views := grid.views()
|
||||
for i in views.size():
|
||||
views[i].dim = not p.owns(shown[i].id)
|
||||
Style.clear(preview_holder)
|
||||
b_craft.visible = false
|
||||
b_upgrade.visible = true
|
||||
b_dismantle.visible = true
|
||||
if selected == null:
|
||||
info_label.text = "Keine Karte gewählt"
|
||||
b_upgrade.text = "Aufwerten"
|
||||
@@ -123,11 +148,19 @@ func refresh() -> void:
|
||||
b_upgrade.disabled = true
|
||||
b_dismantle.disabled = true
|
||||
return
|
||||
var v := CardView.new(selected, Vector2(220, 308), false, p.rarity(selected.id))
|
||||
var v := CardView.new(selected.leveled(p.level(selected.id)), Vector2(220, 308), false, p.rarity(selected.id))
|
||||
v.position = Vector2(920, 152)
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
preview_holder.add_child(v)
|
||||
var lvl := p.level(selected.id)
|
||||
if not p.owns(selected.id):
|
||||
info_label.text = tr("Nicht in der Sammlung")
|
||||
b_upgrade.visible = false
|
||||
b_dismantle.visible = false
|
||||
b_craft.visible = true
|
||||
b_craft.text = tr("Herstellen (%d Staub)") % Profile.CRAFT_COST
|
||||
b_craft.disabled = p.dust < Profile.CRAFT_COST
|
||||
return
|
||||
info_label.text = "%s · %s" % [tr(selected.combo()), tr(p.rarity(selected.id))]
|
||||
if lvl >= Cards.RARITIES.size() - 1:
|
||||
b_upgrade.text = "Höchste Stufe"
|
||||
@@ -136,5 +169,5 @@ func refresh() -> void:
|
||||
var next: String = Cards.RARITIES[lvl + 1]
|
||||
b_upgrade.text = "Aufwerten zu %s (%d Staub)" % [tr(next), Profile.DUST_UPGRADE[next]]
|
||||
b_upgrade.disabled = p.dust < Profile.DUST_UPGRADE[next]
|
||||
b_dismantle.text = "Zerlegen" if lvl == 0 else "Zerlegen (+%d Staub)" % Profile.DUST_DISMANTLE[Cards.RARITIES[lvl]]
|
||||
b_dismantle.disabled = lvl == 0
|
||||
b_dismantle.text = "Zerlegen (+%d Staub)" % Profile.DUST_DISMANTLE[Cards.RARITIES[lvl]]
|
||||
b_dismantle.disabled = false
|
||||
|
||||
93
scripts/ui/deck_screen.gd
Normal file
93
scripts/ui/deck_screen.gd
Normal file
@@ -0,0 +1,93 @@
|
||||
class_name DeckScreen
|
||||
extends Control
|
||||
## Deck zusammenstellen: alle eigenen Karten im Raster, ein Klick nimmt eine Karte in den
|
||||
## Stapel oder wieder heraus. Karten außerhalb des Stapels sind abgedunkelt. Der Stapel hält
|
||||
## 40 bis 60 Karten; „Auffüllen“ nimmt die teuersten eigenen Karten des gewählten Filters.
|
||||
|
||||
var app
|
||||
var filter := ""
|
||||
var grid: CardGrid
|
||||
var count_label: Label
|
||||
var msg_label: Label
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Style.header(self, "Deck", func(): app.show_screen(MenuScreen.new()))
|
||||
Style.button(self, "Alle", Rect2(40, 86, 60, 34), set_filter.bind(""))
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
var fb := Style.button(self, e, Rect2(108 + i * 82, 86, 78, 34), set_filter.bind(e), Style.darker(Cards.ELEMENT_COLORS[e], 0.8))
|
||||
fb.add_theme_font_size_override("font_size", 14)
|
||||
grid = CardGrid.new(Rect2(40, 132, CardGrid.COLS * (CardGrid.CW + CardGrid.GAP) + 10, 650))
|
||||
grid.card_clicked.connect(toggle)
|
||||
add_child(grid)
|
||||
Style.panel(self, Rect2(820, 86, 420, 696))
|
||||
count_label = Style.chip(self, "", Rect2(860, 100, 340, 36), Style.BLUE)
|
||||
var hint := Style.label(self, "Klick nimmt eine Karte in den Stapel oder heraus. Auffüllen nimmt die teuersten Karten des Filters.", 15, Style.MUTED, Rect2(840, 160, 380, 60), "center")
|
||||
hint.autowrap_mode = TextServer.AUTOWRAP_WORD
|
||||
Style.button(self, "Auffüllen", Rect2(840, 240, 380, 46), fill_up)
|
||||
Style.button(self, "Leeren", Rect2(840, 296, 380, 46), clear_deck, Color("5a3c28"))
|
||||
msg_label = Style.label(self, "", 15, Style.RED, Rect2(840, 360, 380, 48), "center")
|
||||
refresh()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func set_filter(e: String) -> void:
|
||||
filter = e
|
||||
refresh()
|
||||
|
||||
|
||||
func toggle(card: Cards.Card) -> void:
|
||||
var err: String = app.profile.toggle_deck(card.id)
|
||||
msg_label.text = err
|
||||
if err == "":
|
||||
app.sfx.play("click")
|
||||
app.save()
|
||||
refresh()
|
||||
|
||||
|
||||
## Füllt mit den teuersten eigenen Karten des gewählten Filters bis zum Höchstwert auf.
|
||||
func fill_up() -> void:
|
||||
var p: Profile = app.profile
|
||||
var pool: Array = p.collection.keys()
|
||||
if filter != "":
|
||||
pool = pool.filter(func(id): return filter in Cards.by_id(id).elements)
|
||||
p.deck = DeckGen.fill(p.deck, pool, Game.DECK_MAX)
|
||||
app.save()
|
||||
refresh()
|
||||
|
||||
|
||||
func clear_deck() -> void:
|
||||
app.profile.deck = []
|
||||
app.save()
|
||||
refresh()
|
||||
|
||||
|
||||
## Eigene Karten, die im Stapel zuerst, sonst nach Kosten.
|
||||
func cards() -> Array:
|
||||
var p: Profile = app.profile
|
||||
var out: Array = p.collection.keys().map(func(id): return Cards.by_id(id))
|
||||
if filter != "":
|
||||
out = out.filter(func(c): return filter in c.elements)
|
||||
out.sort_custom(func(a, b):
|
||||
var ia: bool = a.id in p.deck
|
||||
var ib: bool = b.id in p.deck
|
||||
if ia != ib:
|
||||
return ia
|
||||
return a.cost < b.cost if a.cost != b.cost else a.id < b.id)
|
||||
return out
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
count_label.text = tr("Stapel: %d Karten (%d bis %d)") % [p.deck.size(), Game.DECK_MIN, Game.DECK_MAX]
|
||||
count_label.add_theme_color_override("font_color", Style.RED if p.deck.size() < Game.DECK_MIN else Style.BLUE)
|
||||
var shown := cards()
|
||||
grid.fill(shown.map(func(c): return c.leveled(p.level(c.id))), func(c): return Cards.RARITIES[c.look])
|
||||
var views := grid.views()
|
||||
for i in views.size():
|
||||
views[i].dim = not (shown[i].id in p.deck)
|
||||
1
scripts/ui/deck_screen.gd.uid
Normal file
1
scripts/ui/deck_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ctt36od26uyea
|
||||
@@ -1,10 +1,11 @@
|
||||
class_name ElementView
|
||||
extends Control
|
||||
## Elementzähler der eigenen Seite, mittig: je Element zweimal das Symbol seines Sondereffekts in
|
||||
## der Elementfarbe, leuchtend je offenem eigenen Wesen (beide: Effekt aktiv), daneben in der
|
||||
## Elementfarbe das Mana der bisher ausgespielten Karten (je Game.ELEMENT_MANA gibt es +1 Angriff).
|
||||
## Elementzähler einer Seite als Liste neben ihrem Brett. Es stehen nur Elemente da, bei denen
|
||||
## etwas passiert ist: je Zeile Name, zweimal das Symbol seines Sondereffekts (leuchtend je
|
||||
## offenem Wesen, beide: Effekt aktiv), die Zahl der ausgespielten Karten und ab
|
||||
## Game.ELEMENT_COUNT der Bonus +1/+1.
|
||||
|
||||
const ENTRY_W := 180.0
|
||||
const ROW := 32.0
|
||||
|
||||
var own := {} # offene eigene Wesen je Element
|
||||
var played := {} # ausgespielte Karten je Element
|
||||
@@ -31,37 +32,41 @@ func paired(e: String) -> bool:
|
||||
|
||||
|
||||
func bonus(e: String) -> int:
|
||||
return int(played.get(e, 0)) / Game.ELEMENT_MANA
|
||||
return int(played.get(e, 0)) / Game.ELEMENT_COUNT
|
||||
|
||||
|
||||
## Elemente, bei denen sich etwas getan hat (offene Wesen oder ausgespielte Karten).
|
||||
func shown() -> Array:
|
||||
return Cards.ELEMENTS.filter(func(e): return int(own.get(e, 0)) > 0 or int(played.get(e, 0)) > 0)
|
||||
|
||||
|
||||
## Der Inhalt als Text (Tests, Protokoll): Element offen/gespielt, dazu Effekt und Bonus.
|
||||
func text() -> String:
|
||||
var parts: Array[String] = []
|
||||
for e in Cards.ELEMENTS:
|
||||
for e in shown():
|
||||
var s := "%s %d/%d" % [tr(e), int(own.get(e, 0)), int(played.get(e, 0))]
|
||||
if paired(e):
|
||||
s += " (%s)" % tr(Cards.ELEMENT_KEYWORD[e])
|
||||
if bonus(e) > 0:
|
||||
s += " (+%d)" % bonus(e)
|
||||
s += " (+%d/+%d)" % [bonus(e), bonus(e)]
|
||||
parts.append(s)
|
||||
return " · ".join(parts)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var x0 := (size.x - ENTRY_W * Cards.ELEMENTS.size()) / 2
|
||||
var cy := size.y / 2
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
var y := ROW / 2
|
||||
for e in shown():
|
||||
var col: Color = Cards.ELEMENT_COLORS[e]
|
||||
var active := paired(e) or bonus(e) > 0
|
||||
var px := x0 + i * ENTRY_W + 10
|
||||
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(px, cy), "midleft", active)
|
||||
px += Style.FONT_BOLD.get_string_size(tr(e), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(13)).x + 10
|
||||
draw_style_box(Style.box(Color(col, 0.18 if active else 0.1), 6, Color(col, 0.6 if active else 0.0), 1 if active else 0), Rect2(0, y - ROW / 2 + 2, size.x, ROW - 4))
|
||||
var px := 10.0
|
||||
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(px, y), "midleft", active)
|
||||
px = 76
|
||||
for k in Game.ELEMENT_PAIR:
|
||||
var filled := k < int(own.get(e, 0))
|
||||
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + k * 24 + 10, cy), 8, Style.lighter(col, 0.45) if filled else Color(col, 0.3))
|
||||
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + k * 24 + 10, y), 8, Style.lighter(col, 0.45) if filled else Color(col, 0.3))
|
||||
px += Game.ELEMENT_PAIR * 24 + 8
|
||||
Style.text(self, str(int(played.get(e, 0))), 15, Style.lighter(col, 0.45), Vector2(px, cy), "midleft", true)
|
||||
px += 20
|
||||
Style.text(self, str(int(played.get(e, 0))), 15, Style.lighter(col, 0.45), Vector2(px, y), "midleft", true)
|
||||
if bonus(e) > 0:
|
||||
Style.text(self, "+%d" % bonus(e), 12, Style.GREEN, Vector2(px, cy), "midleft", true)
|
||||
Style.text(self, "+%d/+%d" % [bonus(e), bonus(e)], 12, Style.GREEN, Vector2(size.x - 6, y), "midright", true)
|
||||
y += ROW
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
class_name LobbyScreen
|
||||
extends Control
|
||||
## Netzwerkspiel im lokalen Netz: Spiel eröffnen oder beitreten.
|
||||
|
||||
var app
|
||||
var net: Net
|
||||
var status_label: Label
|
||||
var list_label: Label
|
||||
var ip_box: LineEdit
|
||||
var b_host: Button
|
||||
var b_join: Button
|
||||
var b_watch: Button
|
||||
var b_start: Button
|
||||
var addr_label: Label
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
net = app.net
|
||||
net.my_name = "Spieler"
|
||||
net.my_pick = {"looks": app.profile.looks()}
|
||||
Style.header(self, "Netzwerkspiel", _back)
|
||||
var col := Style.column(self, 760, 110, 10)
|
||||
var hr := Style.row(col, 12)
|
||||
hr.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
b_host = Style.cbutton(hr, "Spiel eröffnen", Vector2(200, 46), _host, Color("285a3c"))
|
||||
ip_box = LineEdit.new()
|
||||
ip_box.custom_minimum_size = Vector2(220, 46)
|
||||
ip_box.placeholder_text = "Adresse des Hosts"
|
||||
ip_box.text = "127.0.0.1"
|
||||
hr.add_child(ip_box)
|
||||
b_join = Style.cbutton(hr, "Beitreten", Vector2(160, 46), _join)
|
||||
b_watch = Style.cbutton(hr, "Zuschauen", Vector2(150, 46), _watch)
|
||||
addr_label = Style.clabel(col, "", 14, Style.MUTED)
|
||||
status_label = Style.clabel(col, "", 16, Style.BLUE)
|
||||
Style.spacer(col, 6)
|
||||
Style.clabel(col, "Am Tisch", 20, Style.TEXT, "center", true)
|
||||
list_label = Style.clabel(col, "", 16, Style.TEXT)
|
||||
Style.spacer(col, 10)
|
||||
var br := Style.row(col, 20)
|
||||
br.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
b_start = Style.cbutton(br, "Start", Vector2(200, 48), _start, Color("285a3c"))
|
||||
net.lobby_changed.connect(refresh)
|
||||
net.started.connect(_started)
|
||||
net.ended.connect(func(_reason): refresh())
|
||||
refresh()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if net.lobby_changed.is_connected(refresh):
|
||||
net.lobby_changed.disconnect(refresh)
|
||||
if net.started.is_connected(_started):
|
||||
net.started.disconnect(_started)
|
||||
|
||||
|
||||
func _host() -> void:
|
||||
net.host()
|
||||
refresh()
|
||||
|
||||
|
||||
func _join() -> void:
|
||||
net.join(ip_box.text.strip_edges())
|
||||
refresh()
|
||||
|
||||
|
||||
## Als Zuschauer beitreten: sieht die Partie ohne Hände, spielt nicht mit.
|
||||
func _watch() -> void:
|
||||
net.spectator = true
|
||||
net.join(ip_box.text.strip_edges())
|
||||
refresh()
|
||||
|
||||
|
||||
func _start() -> void:
|
||||
net.start_battle()
|
||||
|
||||
|
||||
func _started(seat: int) -> void:
|
||||
app.show_screen(BattleScreen.online(seat))
|
||||
|
||||
|
||||
func _back() -> void:
|
||||
net.leave()
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func refresh() -> void:
|
||||
status_label.text = net.status
|
||||
var lines: Array[String] = []
|
||||
var ids: Array = net.names.keys()
|
||||
ids.sort()
|
||||
for id in ids:
|
||||
lines.append("%s%s" % [net.names[id], " (Host)" if id == 1 else ""])
|
||||
for id in net.spectators:
|
||||
lines.append("%s (%s)" % [net.spectators[id], tr("Zuschauer")])
|
||||
list_label.text = "\n".join(lines) if not lines.is_empty() else "–"
|
||||
b_host.disabled = net.connected
|
||||
b_join.disabled = net.connected
|
||||
b_watch.disabled = net.connected
|
||||
ip_box.editable = not net.connected
|
||||
var humans := ids.size()
|
||||
b_start.disabled = not net.is_host
|
||||
b_start.text = tr("Start") if humans >= 2 else tr("Start gegen die KI")
|
||||
addr_label.text = ("Deine Adressen: " + ", ".join(net.local_addresses())) if net.is_host else ""
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE and not ip_box.has_focus():
|
||||
_back()
|
||||
@@ -1 +0,0 @@
|
||||
uid://bhnnxjh6rsxnf
|
||||
@@ -5,7 +5,6 @@ var profile: Profile
|
||||
var screen: Control
|
||||
var save_path: String = Profile.SAVE_PATH
|
||||
var sfx: Sfx
|
||||
var net: Net
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -17,14 +16,9 @@ func _ready() -> void:
|
||||
add_child(bg)
|
||||
sfx = Sfx.new()
|
||||
add_child(sfx)
|
||||
# Fester Pfad /root/Net: Netzwerkaufrufe werden über den Knotenpfad zugestellt,
|
||||
# der bei Host und Mitspielern gleich sein muss.
|
||||
net = Net.new()
|
||||
net.name = "Net"
|
||||
get_tree().root.add_child.call_deferred(net)
|
||||
profile = Profile.load_or_new(save_path)
|
||||
apply_settings()
|
||||
show_screen(MenuScreen.new())
|
||||
show_screen(MenuScreen.new() if profile.started() else StartDeckScreen.new())
|
||||
|
||||
|
||||
func apply_settings() -> void:
|
||||
@@ -36,20 +30,32 @@ func apply_settings() -> void:
|
||||
theme = Style.make_theme()
|
||||
|
||||
|
||||
## Neues Profil: Sammlung und Fortschritt werden verworfen.
|
||||
## Neues Profil: Sammlung und Fortschritt werden verworfen, das Startdeck wird neu gewählt.
|
||||
func reset_profile() -> void:
|
||||
profile = Profile.starter()
|
||||
save()
|
||||
apply_settings()
|
||||
show_screen(StartDeckScreen.new())
|
||||
|
||||
|
||||
func set_language(code: String) -> void:
|
||||
TranslationServer.set_locale(code)
|
||||
|
||||
|
||||
func show_screen(s: Control) -> void:
|
||||
var kept: Array = [] # vorige Bildschirme, zu denen back() zurückführt (Einstellungen aus dem Kampf)
|
||||
|
||||
|
||||
## keep: der aktuelle Bildschirm bleibt erhalten und kommt mit back() zurück.
|
||||
func show_screen(s: Control, keep: bool = false) -> void:
|
||||
if screen != null:
|
||||
screen.queue_free()
|
||||
if keep:
|
||||
remove_child(screen)
|
||||
kept.append(screen)
|
||||
else:
|
||||
screen.queue_free()
|
||||
for k in kept:
|
||||
k.queue_free()
|
||||
kept.clear()
|
||||
screen = s
|
||||
s.app = self
|
||||
s.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
@@ -106,11 +112,17 @@ func _first_button(node: Node) -> Button:
|
||||
return null
|
||||
|
||||
|
||||
## Zurück: zum gemerkten Bildschirm, sonst ins Hauptmenü.
|
||||
func back() -> void:
|
||||
if kept.is_empty():
|
||||
show_screen(MenuScreen.new())
|
||||
return
|
||||
var prev: Control = kept.pop_back()
|
||||
screen.queue_free()
|
||||
screen = prev
|
||||
add_child(prev)
|
||||
|
||||
|
||||
func save() -> void:
|
||||
profile.save(save_path)
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
if net != null and net.is_inside_tree():
|
||||
net.leave()
|
||||
net.queue_free()
|
||||
|
||||
@@ -17,7 +17,7 @@ func _ready() -> void:
|
||||
var y := 230
|
||||
for entry in [["Einzelspieler", func(): app.show_screen(OpponentSelectScreen.new())],
|
||||
["Kampagne", func(): app.show_screen(CampaignScreen.new())],
|
||||
["Netzwerkspiel", func(): app.show_screen(LobbyScreen.new())]]:
|
||||
["Deck", func(): app.show_screen(DeckScreen.new())]]:
|
||||
var b := Style.button(self, entry[0], Rect2(80, y, 380, 62), entry[1], Style.PRIMARY)
|
||||
b.add_theme_font_size_override("font_size", 24)
|
||||
y += 74
|
||||
@@ -35,9 +35,9 @@ func _ready() -> void:
|
||||
refresh()
|
||||
|
||||
|
||||
## Drei Karten der Sammlung, die höchsten Stufen zuerst, leicht gefächert.
|
||||
## Drei eigene Karten, die höchsten Stufen zuerst, leicht gefächert.
|
||||
func _showcase(p: Profile) -> void:
|
||||
var cards: Array = Cards.all().duplicate()
|
||||
var cards: Array = p.collection.keys().map(func(id): return Cards.by_id(id))
|
||||
cards.sort_custom(func(a, b):
|
||||
if p.level(a.id) != p.level(b.id):
|
||||
return p.level(a.id) > p.level(b.id)
|
||||
@@ -45,7 +45,7 @@ func _showcase(p: Profile) -> void:
|
||||
var spots := [[Vector2(830, 300), -0.14], [Vector2(950, 270), 0.0], [Vector2(1070, 300), 0.14]]
|
||||
for i in 3:
|
||||
var c: Cards.Card = cards[i]
|
||||
var v := CardView.new(c, Vector2(180, 252), false, p.rarity(c.id))
|
||||
var v := CardView.new(c.leveled(p.level(c.id)), Vector2(180, 252), false, p.rarity(c.id))
|
||||
v.position = spots[i][0]
|
||||
v.rotation = spots[i][1]
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
@@ -55,8 +55,8 @@ func _showcase(p: Profile) -> void:
|
||||
func refresh() -> void:
|
||||
var p: Profile = app.profile
|
||||
coins_label.text = tr("Münzen: %d · Staub: %d") % [p.coins, p.dust]
|
||||
coll_label.text = tr("Aufgewertet: %d von %d Karten · Kampagne: %d/%d") % [
|
||||
p.upgraded_count(), Cards.all().size(), p.campaign_stage, Campaign.total()]
|
||||
coll_label.text = tr("Sammlung: %d von %d Karten · Kampagne: %d/%d") % [
|
||||
p.collection.size(), Cards.all().size(), p.campaign_stage, Campaign.total()]
|
||||
var notes := p.take_notices()
|
||||
notice_label.text = " · ".join(notes)
|
||||
if not notes.is_empty():
|
||||
|
||||
@@ -16,7 +16,7 @@ var speed_label: Label
|
||||
|
||||
func _ready() -> void:
|
||||
var s: Dictionary = app.profile.settings
|
||||
Style.header(self, "Einstellungen", func(): app.show_screen(MenuScreen.new()))
|
||||
Style.header(self, "Einstellungen", func(): app.back())
|
||||
var col := Style.column(self, 700, 110, 24)
|
||||
var grid := GridContainer.new()
|
||||
grid.columns = 3
|
||||
@@ -86,7 +86,6 @@ func _ready() -> void:
|
||||
|
||||
func _reset() -> void:
|
||||
app.reset_profile()
|
||||
app.show_screen(MenuScreen.new())
|
||||
|
||||
|
||||
func _on_big_font(on: bool) -> void:
|
||||
@@ -139,4 +138,4 @@ func _on_anim(on: bool) -> void:
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||
app.show_screen(MenuScreen.new())
|
||||
app.back()
|
||||
|
||||
37
scripts/ui/start_deck_screen.gd
Normal file
37
scripts/ui/start_deck_screen.gd
Normal file
@@ -0,0 +1,37 @@
|
||||
class_name StartDeckScreen
|
||||
extends Control
|
||||
## Erster Start: eines von vier Startdecks wählen, je eines mit einem Element im Mittelpunkt
|
||||
## (25 Karten davon, je fünf der anderen). Danach geht es ins Menü.
|
||||
|
||||
var app
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
Style.label(self, tr("Wähle dein Startdeck"), 40, Style.GOLD, Rect2(0, 60, 1280, 60), "center", true)
|
||||
Style.label(self, tr("25 Karten des Elements, dazu je fünf der anderen. Booster und Werkstatt bringen später den Rest."), 17, Style.MUTED, Rect2(140, 124, 1000, 30), "center")
|
||||
for i in Cards.ELEMENTS.size():
|
||||
var e: String = Cards.ELEMENTS[i]
|
||||
var x := 70 + i * 290
|
||||
var col: Color = Cards.ELEMENT_COLORS[e]
|
||||
Style.panel(self, Rect2(x, 190, 260, 470), Style.darker(col, 0.75), 12, col, 2)
|
||||
var ids := DeckGen.start_deck(e)
|
||||
var sample: Array = ids.filter(func(id): return Cards.by_id(id).elements[0] == e)
|
||||
sample.sort_custom(func(a, b): return Cards.by_id(a).cost > Cards.by_id(b).cost)
|
||||
for k in 3:
|
||||
var c: Cards.Card = Cards.by_id(sample[k * 7])
|
||||
var v := CardView.new(c, Vector2(110, 140), false, "Common")
|
||||
v.position = Vector2(x + 20 + k * 40, 210 + k * 14)
|
||||
v.rotation_degrees = -6 + k * 6
|
||||
v.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
add_child(v)
|
||||
Style.label(self, tr(e), 28, Style.TEXT, Rect2(x, 400, 260, 40), "center", true)
|
||||
Style.label(self, tr(Cards.KEYWORD_TEXT[Cards.ELEMENT_KEYWORD[e]]), 15, Style.MUTED, Rect2(x + 16, 446, 228, 60), "center")
|
||||
var b := Style.button(self, tr("%s wählen") % tr(e), Rect2(x + 30, 590, 200, 50), choose.bind(e), Style.PRIMARY)
|
||||
b.add_theme_font_size_override("font_size", 18)
|
||||
|
||||
|
||||
func choose(element: String) -> void:
|
||||
app.profile.choose_start(element)
|
||||
app.save()
|
||||
app.sfx.play("combo")
|
||||
app.show_screen(MenuScreen.new())
|
||||
1
scripts/ui/start_deck_screen.gd.uid
Normal file
1
scripts/ui/start_deck_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bxl6isdrgqb2b
|
||||
Reference in New Issue
Block a user