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"} # Mana übrig und Platz auf der Hand: lieber ziehen als passen
|
||||
return {"type": "draw", "index": pick_index(game, p)} # Mana übrig und Platz auf der Hand: lieber ziehen als passen
|
||||
return best
|
||||
return map_action(game, view, best)
|
||||
|
||||
@@ -258,10 +258,24 @@ 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"}
|
||||
return {"type": "draw", "index": pick_index(game, p)}
|
||||
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):
|
||||
@@ -288,7 +302,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)
|
||||
game.draw(p, int(action.get("index", 0)))
|
||||
"reveal":
|
||||
game.reveal(p, action.creature)
|
||||
"attack":
|
||||
|
||||
@@ -98,11 +98,11 @@ static func cost_of(attack: int, health: int) -> int:
|
||||
return (2 * attack + health) / 3
|
||||
|
||||
|
||||
## Wert eines verschmolzenen Wesens: der bessere Wert plus ein Drittel des schwächeren,
|
||||
## Wert eines verschmolzenen Wesens: der bessere Wert plus ein Drittel des schwächeren (mindestens 1),
|
||||
## aufgerundet. Bei gleichen Werten sind das zwei Drittel der Summe, sonst mehr; nie schlechter
|
||||
## als das stärkere Wesen, und jeder Partner ab 1 bringt mindestens +1.
|
||||
## als das stärkere Wesen, und jeder Partner bringt mindestens +1, auch mit 0.
|
||||
static func fusion_value(a: int, b: int) -> int:
|
||||
return maxi(a, b) + ceili(mini(a, b) / 3.0)
|
||||
return maxi(a, b) + maxi(1, ceili(mini(a, b) / 3.0))
|
||||
|
||||
|
||||
## Name des verschmolzenen Wesens: die Kombo seiner Elemente (ein Element zählt doppelt).
|
||||
|
||||
126
scripts/game.gd
126
scripts/game.gd
@@ -10,7 +10,9 @@ const START_MANA := 1 # Mana in Zug 1; jede Runde eins mehr, ohne Grenze; Re
|
||||
const DRAW_COST := 2 # Ziehen: eine bezahlbare Karte vom Stapel
|
||||
const START_LIFE := 30
|
||||
const ELEMENT_PAIR := 2 # so viele eigene offene Wesen eines Elements: Sondereffekt des Elements
|
||||
const ELEMENT_STACK := 3 # so viele offene Wesen eines Elements auf dem Tisch: +1/+1
|
||||
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 DISCARD_GAIN := 1 # Abwerfen einer Handkarte bringt so viel Mana
|
||||
|
||||
# Merkmale der Kampagnen-Bosse (Player.perks).
|
||||
const PERKS := {
|
||||
@@ -105,8 +107,9 @@ class Creature extends RefCounted:
|
||||
return maxi(0, attacks_per_turn() - attacks_done)
|
||||
|
||||
## Angreifen darf, wer offen liegt, noch Angriffe hat und seit einer Runde liegt (oder flink ist).
|
||||
## Darf dieses Wesen angreifen? Ob es Schaden macht, hängt vom Ziel ab (Elementvorteil, Game.can_attack).
|
||||
func can_attack() -> bool:
|
||||
return not hidden and attacks_left() > 0 and power() > 0 and (ready or has("Flink"))
|
||||
return not hidden and attacks_left() > 0 and (ready or has("Flink"))
|
||||
|
||||
func clone(new_owner) -> Creature:
|
||||
var c := Creature.new(card, new_owner, uid)
|
||||
@@ -134,6 +137,9 @@ class Player extends RefCounted:
|
||||
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
|
||||
|
||||
func _init(n: String, h: bool, l: int, t: String = "") -> void:
|
||||
name = n
|
||||
@@ -141,6 +147,8 @@ class Player extends RefCounted:
|
||||
is_human = h
|
||||
life = l
|
||||
max_life = l
|
||||
for e in Cards.ELEMENTS:
|
||||
played[e] = 0
|
||||
|
||||
func alive() -> bool:
|
||||
return life > 0
|
||||
@@ -163,6 +171,9 @@ class Player extends RefCounted:
|
||||
p.perks = perks.duplicate()
|
||||
p.looks = looks
|
||||
p.turns = turns
|
||||
p.played = played.duplicate()
|
||||
p.offer = offer.duplicate()
|
||||
p.drew = drew
|
||||
for c in board:
|
||||
p.board.append(c.clone(p))
|
||||
return p
|
||||
@@ -276,6 +287,7 @@ func start(fixed_order: bool = false) -> void:
|
||||
p.order = i
|
||||
for c in draw_cards(START_HAND, START_MANA):
|
||||
p.hand.append(c.copy())
|
||||
p.offer = draw_cards(OFFER, START_MANA)
|
||||
turn = 1
|
||||
current_idx = 0
|
||||
say("Zugreihenfolge: " + ", ".join(players.map(func(p): return p.name)))
|
||||
@@ -285,9 +297,9 @@ func start(fixed_order: bool = false) -> void:
|
||||
begin_turn(current())
|
||||
|
||||
|
||||
## Nimmt n Karten vom Stapel, die höchstens max_cost kosten (was man sich in diesem Zug leisten
|
||||
## kann); zu teure wandern unter den Stapel. Ist der Stapel leer, kommt ein frischer.
|
||||
func draw_cards(n: int, max_cost: int) -> Array:
|
||||
## 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
|
||||
@@ -306,7 +318,8 @@ func draw_cards(n: int, max_cost: int) -> Array:
|
||||
return out
|
||||
|
||||
|
||||
## Ziehen: kostet DRAW_COST, eine Karte kommt auf die Hand.
|
||||
## 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.
|
||||
func can_draw(p: Player) -> Array:
|
||||
if winner != "" or p != current():
|
||||
return [false, "Nicht dein Zug"]
|
||||
@@ -318,16 +331,20 @@ func can_draw(p: Player) -> Array:
|
||||
|
||||
|
||||
func draw_cost(p: Player) -> int:
|
||||
return 0 if "ziehen_gratis" in p.perks else DRAW_COST
|
||||
return 0 if not p.drew or "ziehen_gratis" in p.perks else DRAW_COST
|
||||
|
||||
|
||||
func draw(p: Player) -> String:
|
||||
func draw(p: Player, index: int = 0) -> 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)
|
||||
var card: Cards.Card = draw_cards(1, p.max_energy)[0]
|
||||
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
|
||||
say("%s zieht." % p.name)
|
||||
emit({"type": "draw", "player": players.find(p)})
|
||||
return ""
|
||||
@@ -342,15 +359,9 @@ 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
|
||||
say("--- Zug %d: %s ---" % [turn, p.name])
|
||||
emit({"type": "turn", "player": players.find(p)})
|
||||
# Jeder Zug beginnt mit einer Karte vom Stapel, die man sich in diesem Zug leisten kann.
|
||||
if p.hand.size() < MAX_HAND:
|
||||
p.hand.append(draw_cards(1, p.max_energy)[0].copy())
|
||||
say("%s zieht eine Karte." % p.name)
|
||||
emit({"type": "draw", "player": players.find(p)})
|
||||
else:
|
||||
say("%s hat die Hand voll und zieht nicht." % p.name)
|
||||
|
||||
|
||||
## Zugende: Wesen mit Regeneration heilen ganz, dann ist der andere dran.
|
||||
@@ -405,6 +416,8 @@ func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
|
||||
cr.health += 1
|
||||
cr.max_health += 1
|
||||
p.board.append(cr)
|
||||
for e in card.elements:
|
||||
p.played[e] = int(p.played.get(e, 0)) + card.cost
|
||||
if face_down:
|
||||
say("%s legt ein Wesen verdeckt." % p.name)
|
||||
else:
|
||||
@@ -416,6 +429,26 @@ func play_card(p: Player, card: Cards.Card, face_down: bool = false) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
## Abwerfen: eine Handkarte loswerden und dafür DISCARD_GAIN Mana bekommen.
|
||||
func can_discard(p: Player, card: Cards.Card) -> Array:
|
||||
if winner != "" or p != current():
|
||||
return [false, "Nicht dein Zug"]
|
||||
if not (card in p.hand):
|
||||
return [false, "Nicht auf der Hand"]
|
||||
return [true, ""]
|
||||
|
||||
|
||||
func discard(p: Player, card: Cards.Card) -> String:
|
||||
var ok := can_discard(p, card)
|
||||
if not ok[0]:
|
||||
return ok[1]
|
||||
p.hand.erase(card)
|
||||
p.energy += DISCARD_GAIN
|
||||
say("%s wirft %s ab." % [p.name, card.name])
|
||||
emit({"type": "discard", "player": players.find(p)})
|
||||
return ""
|
||||
|
||||
|
||||
## Ein eigenes verdecktes Wesen aufdecken: kostenlos, im eigenen Zug, frühestens einen Zug
|
||||
## nach dem Legen. Aufgedeckt greift es sofort an.
|
||||
func can_reveal(p: Player, cr: Creature) -> Array:
|
||||
@@ -506,11 +539,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 - a.aura, b.max_health - b.aura))
|
||||
Cards.fusion_value(a.max_health, b.max_health))
|
||||
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(mini(a.health, a.max_health - a.aura), mini(b.health, b.max_health - b.aura)))
|
||||
cr.health = mini(card.health, Cards.fusion_value(a.health, b.health))
|
||||
cr.ready = a.ready and b.ready
|
||||
cr.attacks_done = maxi(a.attacks_done, b.attacks_done)
|
||||
var at := p.board.find(a)
|
||||
@@ -527,59 +560,38 @@ func _fuse_board(p: Player, a: Creature, b: Creature) -> String:
|
||||
|
||||
# --- Elementboni ---------------------------------------------------------------
|
||||
|
||||
## Offene Wesen je Element auf dem ganzen Tisch.
|
||||
func element_counts() -> Dictionary:
|
||||
var counts := {}
|
||||
for e in Cards.ELEMENTS:
|
||||
counts[e] = 0
|
||||
for p in players:
|
||||
for c in p.board:
|
||||
if not c.hidden:
|
||||
for e in c.card.elements:
|
||||
counts[e] += 1
|
||||
return counts
|
||||
|
||||
|
||||
## +1 je Element mit ELEMENT_STACK offenen Wesen auf dem Tisch; mehrere Elemente zählen alle.
|
||||
static func aura_of(elements: Array, counts: Dictionary) -> int:
|
||||
## +1 Angriff je volle ELEMENT_MANA an ausgespielten 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:
|
||||
if int(counts[e]) >= ELEMENT_STACK:
|
||||
a += 1
|
||||
a += int(played.get(e, 0)) / ELEMENT_MANA
|
||||
return a
|
||||
|
||||
|
||||
## Bonus eines offenen Wesens; verdeckte zählen nicht und bekommen nichts.
|
||||
func aura_for(c: Creature, counts: Dictionary) -> int:
|
||||
if c.hidden:
|
||||
return 0
|
||||
var a := aura_of(c.card.elements, counts)
|
||||
## Bonus eines offenen Wesens aus den ausgespielten Karten seines Besitzers; verdeckte bekommen nichts.
|
||||
func aura_for(c: Creature) -> int:
|
||||
var o = c.owner()
|
||||
if o != null and "element_doppelt" in o.perks:
|
||||
if c.hidden or o == null:
|
||||
return 0
|
||||
var a := aura_of(c.card.elements, o.played)
|
||||
if "element_doppelt" in o.perks:
|
||||
a *= 2
|
||||
return a
|
||||
|
||||
|
||||
## Rechnet die Elementboni neu. Kommt ein Bonus, steigen Leben und Höchstleben; geht er,
|
||||
## sinkt nur das Höchstleben, das Leben wird höchstens darauf gekappt.
|
||||
## Rechnet die Elementboni neu; sie wirken nur auf den Angriff (Creature.power).
|
||||
func apply_auras() -> void:
|
||||
var counts := element_counts()
|
||||
for p in players:
|
||||
for c in p.board:
|
||||
var a := aura_for(c, counts)
|
||||
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
|
||||
c.aura = aura_for(c)
|
||||
|
||||
|
||||
## Vorschau fürs Ausspielen: der Elementbonus, den die Karte offen gelegt bekäme.
|
||||
## 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 counts := element_counts()
|
||||
var played := p.played.duplicate()
|
||||
for e in card.elements:
|
||||
counts[e] += 1
|
||||
return aura_of(card.elements, counts) * (2 if "element_doppelt" in p.perks else 1)
|
||||
played[e] = int(played.get(e, 0)) + card.cost
|
||||
return aura_of(card.elements, played) * (2 if "element_doppelt" in p.perks else 1)
|
||||
|
||||
|
||||
# --- Kampf -------------------------------------------------------------------
|
||||
@@ -591,13 +603,15 @@ func can_attack(cr: Creature, target) -> Array:
|
||||
return [false, "Nicht dein Zug"]
|
||||
if cr.hidden:
|
||||
return [false, "%s liegt verdeckt" % cr.name]
|
||||
if not cr.ready and not cr.has("Flink") and cr.attacks_left() > 0 and cr.power() > 0:
|
||||
if not cr.ready and not cr.has("Flink") and cr.attacks_left() > 0:
|
||||
return [false, "%s greift erst im nächsten Zug an" % cr.name]
|
||||
if not cr.can_attack():
|
||||
return [false, "%s kann nicht angreifen" % cr.name]
|
||||
var tgt_owner = target.owner() if target is Creature else target
|
||||
if not (tgt_owner in enemies_of(cr.owner())):
|
||||
return [false, "Kein gültiges Ziel"]
|
||||
if cr.power() + matchup_bonus(cr, target) <= 0: # 0 Angriff reicht nur mit Elementvorteil
|
||||
return [false, "%s würde keinen Schaden machen" % cr.name]
|
||||
if not (target is Creature) and not tgt_owner.board.is_empty():
|
||||
return [false, "Erst die Wesen"]
|
||||
if target is Creature and not target.has("Spott") and tgt_owner.board.any(func(c): return c.has("Spott")):
|
||||
@@ -616,7 +630,7 @@ func attack(cr: Creature, target) -> String:
|
||||
say("%s greift %s an." % [cr.name, target.name])
|
||||
emit({"type": "attack", "attacker": ref_of(cr), "target": ref_of(target)})
|
||||
damage(target, cr.power() + matchup_bonus(cr, target))
|
||||
if target is Creature and target.power() > 0: # wer nichts austeilt, kontert auch nicht
|
||||
if target is Creature: # Konter mit Elementvorteil; wer so nichts austeilt, kontert nicht (damage ignoriert 0)
|
||||
damage(cr, target.power() + matchup_bonus(target, cr))
|
||||
cleanup()
|
||||
check_end()
|
||||
|
||||
@@ -398,13 +398,17 @@ func _apply(id: int, action: Dictionary) -> void:
|
||||
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)
|
||||
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)))
|
||||
|
||||
@@ -27,8 +27,9 @@ static func snapshot(g: Game, viewer: int) -> Dictionary:
|
||||
"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,
|
||||
"hand": p.hand.map(card_ref) if mine else p.hand.size(), "board": board})
|
||||
"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()
|
||||
@@ -55,12 +56,17 @@ static func from_snapshot(d: Dictionary) -> Game:
|
||||
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))
|
||||
|
||||
@@ -26,7 +26,7 @@ var coins := 0
|
||||
var dust := 0
|
||||
var collection := {} # id -> Seltenheitsstufe (Index in Cards.RARITIES), fehlend = Common
|
||||
var campaign_stage := 0 # Anzahl abgeschlossener Kampagnenstufen
|
||||
var settings := {"volume": 0.8, "music": 0.5, "ai_speed": 1.0, "animations": true, "language": "de", "difficulty": "normal", "big_font": false}
|
||||
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)
|
||||
|
||||
@@ -64,7 +64,7 @@ static func from_dict(d: Dictionary) -> Profile:
|
||||
if Cards.has_id(cid):
|
||||
p.collection[cid] = clampi(int(coll[cid]), 0, Cards.RARITIES.size() - 1)
|
||||
var se = d.get("settings", {})
|
||||
p.settings = {"volume": float(se.get("volume", 0.8)), "music": float(se.get("music", 0.5)),
|
||||
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)),
|
||||
"language": str(se.get("language", "de")), "difficulty": str(se.get("difficulty", "normal")),
|
||||
"big_font": bool(se.get("big_font", false))}
|
||||
|
||||
@@ -202,7 +202,7 @@ func _next_banner() -> void:
|
||||
var delay := 0.0
|
||||
var band := ColorRect.new()
|
||||
band.color = Color(0, 0, 0, 0.55)
|
||||
band.position = Vector2(0, 300)
|
||||
band.position = Vector2(0, 355)
|
||||
band.size = Vector2(1280, 90)
|
||||
band.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
band.scale.y = 0.0
|
||||
@@ -218,7 +218,7 @@ func _next_banner() -> void:
|
||||
l.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
screen.fx_layer.add_child(l)
|
||||
l.reset_size()
|
||||
l.position = Vector2(640, 345) - l.size / 2
|
||||
l.position = Vector2(640, 400) - l.size / 2
|
||||
l.pivot_offset = l.size / 2
|
||||
l.scale = Vector2(0.6, 0.6)
|
||||
l.modulate.a = 0.0
|
||||
|
||||
@@ -76,7 +76,7 @@ func _next_zone(step: int) -> void:
|
||||
func items() -> Array:
|
||||
match zone:
|
||||
"deck":
|
||||
return [screen.deck] if screen.deck.visible else []
|
||||
return screen.offer_views
|
||||
"hand":
|
||||
return screen.hand_views
|
||||
"own":
|
||||
@@ -105,7 +105,7 @@ func confirm() -> void:
|
||||
var item = items[index]
|
||||
match zone:
|
||||
"deck":
|
||||
screen.on_click("deck", null)
|
||||
screen.on_click("deck", index)
|
||||
"hand":
|
||||
screen.on_click("hand", item.card)
|
||||
"own":
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
class_name BattleLayout
|
||||
## Feste Bildschirmaufteilung des Duells (1280x800), von unten nach oben: Hand mit dem
|
||||
## Zieh-Stapel links und Zug beenden rechts, eigenes Brett (mit eigener Leiste links),
|
||||
## Elementband, oben der Gegner mit seinem Brett.
|
||||
## 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.
|
||||
|
||||
const B_GIVEUP := Rect2(10, 10, 96, 32)
|
||||
const B_LOG := Rect2(112, 10, 96, 32)
|
||||
const OPP_BOX := Rect2(330, 10, 620, 56)
|
||||
const FIELD := Rect2(240, 226, 800, 32)
|
||||
const MSG := Rect2(240, 262, 800, 22)
|
||||
const OWN_PANEL := Rect2(240, 288, 800, 160)
|
||||
const STATUS := Rect2(20, 288, 210, 160)
|
||||
const B_UNDO := Rect2(1050, 298, 210, 40)
|
||||
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 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 OWN_X := 345
|
||||
const HAND_W := 150
|
||||
const HAND_H := 205
|
||||
const HAND_W := 110 # Handkarten so groß wie die Wesen auf dem Feld
|
||||
const HAND_H := 140
|
||||
const HAND_GAP := 10
|
||||
const HAND_Y := 532
|
||||
const DECK := Rect2(50, 540, 130, 182)
|
||||
const B_END := Rect2(1060, 600, 200, 60)
|
||||
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 CENTER_X := 240
|
||||
@@ -25,38 +31,41 @@ const CENTER_W := 800
|
||||
|
||||
|
||||
static func own_slot(j: int) -> Rect2:
|
||||
return Rect2(OWN_X + j * 120, 298, 110, 140)
|
||||
return Rect2(OWN_X + j * 120, 440, 110, 140)
|
||||
|
||||
|
||||
## Brett des Gegners unter seinem Kasten.
|
||||
## Brett des Gegners über seiner Elementleiste.
|
||||
static func opp_slot(j: int) -> Rect2:
|
||||
return Rect2(OWN_X + j * 120, 76, 110, 140)
|
||||
return Rect2(OWN_X + j * 120, 220, 110, 140)
|
||||
|
||||
|
||||
## Hand: bis zu fünf große Karten unten, mittig.
|
||||
## Hand: bis zu fünf Karten unten, mittig; den Bogen legt hand_arc dazu.
|
||||
static func hand_rect(count: int, i: int) -> Rect2:
|
||||
var total := count * HAND_W + maxi(0, count - 1) * HAND_GAP
|
||||
var x := CENTER_X + (CENTER_W - total) / 2
|
||||
return Rect2(x + i * (HAND_W + HAND_GAP), HAND_Y, HAND_W, HAND_H)
|
||||
|
||||
|
||||
## Schwebende Vorschau neben der gezeigten Karte: über Handkarten, sonst rechts
|
||||
## davon (links, wenn rechts kein Platz ist); nie aus dem Bild.
|
||||
static func preview_rect(anchor: Rect2) -> Rect2:
|
||||
if anchor.position.y >= HAND_Y - 10:
|
||||
var x := clampf(anchor.get_center().x - PREVIEW_SIZE.x / 2, 10, 1270 - PREVIEW_SIZE.x)
|
||||
return Rect2(Vector2(x, anchor.position.y - 12 - PREVIEW_SIZE.y), PREVIEW_SIZE)
|
||||
var x := anchor.end.x + 12
|
||||
if x + PREVIEW_SIZE.x > 1270:
|
||||
x = anchor.position.x - 12 - PREVIEW_SIZE.x
|
||||
var y := clampf(anchor.get_center().y - PREVIEW_SIZE.y / 2, 10, 800 - PREVIEW_SIZE.y - 10)
|
||||
return Rect2(Vector2(x, y), PREVIEW_SIZE)
|
||||
## Hand des Gegners oben, gleich groß, nur die Rückseiten.
|
||||
static func opp_hand_rect(count: int, i: int) -> Rect2:
|
||||
var r := hand_rect(count, i)
|
||||
r.position.y = OPP_HAND_Y
|
||||
return r
|
||||
|
||||
|
||||
## Zusatzzeile (Elementbonus, Eigenschaften) neben der Vorschau, rechts davon oder links.
|
||||
## Leichter Bogen: Karte i von count dreht sich um ihre Mitte und sinkt zum Rand hin ab
|
||||
## (mirrored: für den Gegner oben spiegelverkehrt). Liefert [Grad, y-Versatz].
|
||||
static func hand_arc(count: int, i: int, mirrored: bool = false) -> Array:
|
||||
var t := i - (count - 1) / 2.0
|
||||
var sign := -1.0 if mirrored else 1.0
|
||||
return [t * HAND_ARC * sign, t * t * HAND_ARC * sign]
|
||||
|
||||
|
||||
## Vorschau der gezeigten Karte: fest rechts auf halber Höhe.
|
||||
static func preview_rect(_anchor: Rect2) -> Rect2:
|
||||
return Rect2(Vector2(1050, 400 - PREVIEW_SIZE.y / 2), PREVIEW_SIZE)
|
||||
|
||||
|
||||
## Zusatzzeile (Elementbonus, Eigenschaften) unter der Vorschau.
|
||||
static func info_rect(preview: Rect2) -> Rect2:
|
||||
var w := 230.0
|
||||
var x := preview.end.x + 8
|
||||
if x + w > 1275:
|
||||
x = preview.position.x - 8 - w
|
||||
return Rect2(x, preview.position.y + preview.size.y / 2 - 40, w, 80)
|
||||
return Rect2(1030, preview.end.y + 8, 230, 80)
|
||||
|
||||
@@ -60,7 +60,10 @@ var flash_until := {} # Spielerindex -> Zeitpunkt (ms)
|
||||
var hero_layer: Control
|
||||
var tile_layer: Control
|
||||
var hand_layer: Control
|
||||
var deck: Deck
|
||||
var offer_views: Array = [] # die offenen Karten des Stapels (CardView)
|
||||
var offer_layer: Control
|
||||
var deck_label: Label
|
||||
var draw_enabled := false
|
||||
var log_overlay: Control
|
||||
var b_log: Button
|
||||
var status_panel: Panel
|
||||
@@ -70,14 +73,12 @@ var preview_holder: Control
|
||||
var name_label: Label
|
||||
var life_label: Label
|
||||
var energy_label: Label
|
||||
var income_label: Label
|
||||
var status_label: Label
|
||||
var msg_label: Label
|
||||
var b_end: Button
|
||||
var b_giveup: Button
|
||||
var element_view: ElementView
|
||||
var element_view: ElementView # eigene Elementleiste
|
||||
var opp_element_view: ElementView # die des Gegners darüber
|
||||
var info_label: Label # Vorschau: Elementbonus, Merkmale
|
||||
var board_panel: Panel # eigenes Brett
|
||||
var menu_layer: Control # Kartenmenü (Rechtsklick)
|
||||
var menu_target = null # Karte oder Wesen, für das das Menü offen ist
|
||||
var arrivals := {} # uid -> Rect2: Wesen, das gerade aus der Hand aufs Brett fliegt
|
||||
@@ -181,43 +182,43 @@ func _build_static() -> void:
|
||||
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.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)
|
||||
add_child(opp_element_view)
|
||||
Style.panel(self, L.FIELD, Style.darker(Style.PANEL, 0.9), 8)
|
||||
element_view = ElementView.new(L.FIELD)
|
||||
add_child(element_view)
|
||||
msg_label = Style.label(self, "", 15, Style.ORANGE, L.MSG, "center", true)
|
||||
board_panel = Style.panel(self, L.OWN_PANEL, Style.darker(Style.PANEL, 0.8), 10)
|
||||
msg_label = Style.label(self, "", 14, Style.ORANGE, L.MSG, "center", true)
|
||||
msg_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
var slots := Control.new()
|
||||
slots.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
slots.draw.connect(func():
|
||||
for j in Game.MAX_BOARD:
|
||||
Style.border(slots, L.own_slot(j), Color(1, 1, 1, 0.08), 2, 8)
|
||||
Style.border(slots, L.own_slot(j), Color(1, 1, 1, 0.05), 2, 8)
|
||||
Style.border(slots, L.opp_slot(j), Color(1, 1, 1, 0.05), 2, 8))
|
||||
add_child(slots)
|
||||
status_panel = Style.panel(self, L.STATUS)
|
||||
var sx: float = L.STATUS.position.x
|
||||
var sy: float = L.STATUS.position.y
|
||||
name_label = Style.label(self, "", 18, Style.TEXT, Rect2(sx + 12, sy + 8, 190, 26), "left", true)
|
||||
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.clip_text = true
|
||||
life_label = Style.label(self, "", 26, Style.RED, Rect2(sx + 12, sy + 40, 190, 34), "left", true)
|
||||
energy_label = Style.label(self, "", 26, Style.BLUE, Rect2(sx + 12, sy + 78, 100, 34), "left", true)
|
||||
income_label = Style.label(self, "", 15, Style.MUTED, Rect2(sx + 100, sy + 88, 100, 22), "left")
|
||||
status_label = Style.label(self, "", 13, Style.MUTED, Rect2(sx + 12, sy + 124, 190, 22), "left")
|
||||
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)
|
||||
b_undo = Style.button(self, "Rückgängig", L.B_UNDO, undo)
|
||||
b_undo.add_theme_font_size_override("font_size", 15)
|
||||
b_undo.add_theme_font_size_override("font_size", 13)
|
||||
b_undo.visible = false
|
||||
# unten: Zieh-Stapel links, Hand in der Mitte, Zug beenden rechts
|
||||
deck = Deck.new(L.DECK)
|
||||
deck.clicked.connect(func(): on_click("deck", null))
|
||||
add_child(deck)
|
||||
b_end = Style.button(self, "Zug beenden", L.B_END, end_turn, Style.PRIMARY)
|
||||
b_end.add_theme_font_size_override("font_size", 20)
|
||||
b_skip = Style.button(self, "Gegnerzüge überspringen", L.B_END, func(): skip_ai = true)
|
||||
b_skip.add_theme_font_size_override("font_size", 14)
|
||||
b_skip.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
|
||||
hero_layer = _layer()
|
||||
offer_layer = _layer()
|
||||
tile_layer = _layer()
|
||||
hand_layer = _layer()
|
||||
preview_holder = _layer()
|
||||
@@ -317,7 +318,7 @@ func target_center(ref: Dictionary) -> Vector2:
|
||||
return Vector2(-1, -1)
|
||||
|
||||
|
||||
## Kasten des Gegners oben in der Mitte; ein Klick zielt auf den Helden.
|
||||
## Kasten des Gegners links oben, gebaut wie der eigene; ein Klick zielt auf den Helden.
|
||||
func build_hero_boxes() -> void:
|
||||
Style.clear(hero_layer)
|
||||
hero_panels = {}
|
||||
@@ -335,11 +336,11 @@ 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 + 4, box.size.x - 16, 22), "left", true)
|
||||
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.clip_text = true
|
||||
labels.name.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
labels["life"] = Style.label(hero_layer, "", 20, Style.RED, Rect2(box.position.x + 8, box.position.y + 30, 120, 24), "left", true)
|
||||
labels["stats"] = Style.label(hero_layer, "", 14, Style.MUTED, Rect2(box.position.x + 80, box.position.y + 34, box.size.x - 88, 20), "right")
|
||||
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)
|
||||
hero_labels[o] = labels
|
||||
|
||||
|
||||
@@ -366,8 +367,10 @@ func _input(event: InputEvent) -> void:
|
||||
open_menu(under.card, Rect2(under.position, under.size))
|
||||
elif under is CreatureView and under in own_views and my_turn():
|
||||
open_menu(under.creature, Rect2(under.slot, under.size))
|
||||
else:
|
||||
elif selected != null or fuse_first != null or any_overlay() or not my_turn():
|
||||
cancel_all()
|
||||
else:
|
||||
show_menu(tr("Zug %d") % game.turn, [[tr("Zug beenden"), end_turn, true]], Rect2(event.position, Vector2.ZERO))
|
||||
get_viewport().set_input_as_handled() # sonst schließt derselbe Klick das Menü gleich wieder
|
||||
return
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_MIDDLE:
|
||||
@@ -677,6 +680,7 @@ func open_menu(target, anchor: Rect2, full: bool = false) -> void:
|
||||
entries.append([tr("Verdeckt legen ◆ %d") % card.cost, func():
|
||||
play(card, true)
|
||||
after_action(), ok[0]])
|
||||
entries.append([tr("Abwerfen +%d ◆") % Game.DISCARD_GAIN, func(): do_discard(card), game.can_discard(perspective, card)[0]])
|
||||
elif not is_card and target.hidden:
|
||||
entries.append([tr("Aufdecken"), func(): do_reveal(target), game.can_reveal(perspective, target)[0]])
|
||||
if full:
|
||||
@@ -688,6 +692,12 @@ func open_menu(target, anchor: Rect2, full: bool = false) -> void:
|
||||
if entries.is_empty(): # offenes eigenes Wesen per Maus: nichts zu wählen
|
||||
close_menu()
|
||||
return
|
||||
show_menu(card.title(), entries, anchor)
|
||||
|
||||
|
||||
## Kleines Menü neben anchor: Überschrift und Knöpfe [Text, Callable, aktiv].
|
||||
func show_menu(title: String, entries: Array, anchor: Rect2) -> void:
|
||||
Style.clear(menu_layer)
|
||||
var w := 230.0
|
||||
var h := 18.0 + entries.size() * 44 + 34
|
||||
var x := clampf(anchor.end.x + 8, 10, 1270 - w)
|
||||
@@ -696,7 +706,7 @@ func open_menu(target, anchor: Rect2, full: bool = false) -> void:
|
||||
var y := clampf(anchor.position.y, 10, 790 - h)
|
||||
var box := Rect2(x, y, w, h)
|
||||
Style.panel(menu_layer, box, Style.PANEL, 10, Style.GOLD, 2)
|
||||
Style.label(menu_layer, card.title(), 15, Style.GOLD, Rect2(box.position.x + 12, box.position.y + 8, w - 24, 22), "left", true)
|
||||
Style.label(menu_layer, title, 15, Style.GOLD, Rect2(box.position.x + 12, box.position.y + 8, w - 24, 22), "left", true)
|
||||
var by := box.position.y + 36
|
||||
for e in entries:
|
||||
var act: Callable = e[1]
|
||||
@@ -716,13 +726,25 @@ func close_menu() -> void:
|
||||
menu_target = null
|
||||
|
||||
|
||||
## Ziehen: kostet Mana, eine Karte kommt auf die Hand.
|
||||
func do_draw() -> void:
|
||||
## Abwerfen: eine Handkarte gegen Mana loswerden.
|
||||
func do_discard(card: Cards.Card) -> void:
|
||||
clear_undo()
|
||||
if is_client():
|
||||
net.send_action({"type": "draw"})
|
||||
net.send_action({"type": "discard", "hand": perspective.hand.find(card)})
|
||||
return
|
||||
var err := game.draw(perspective)
|
||||
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:
|
||||
clear_undo()
|
||||
if is_client():
|
||||
net.send_action({"type": "draw", "index": index})
|
||||
return
|
||||
var err := game.draw(perspective, index)
|
||||
if err != "":
|
||||
say(tr(err))
|
||||
else:
|
||||
@@ -811,7 +833,7 @@ func on_click(kind: String, obj) -> void:
|
||||
return
|
||||
match kind:
|
||||
"deck":
|
||||
do_draw()
|
||||
do_draw(int(obj) if obj != null else 0)
|
||||
"hand":
|
||||
var ok := g.can_play(perspective, obj)
|
||||
if not ok[0]:
|
||||
@@ -881,7 +903,7 @@ func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2())
|
||||
var a := game.forecast(perspective, card)
|
||||
var info := ""
|
||||
if a > 0:
|
||||
info = tr("Elementbonus +%d/+%d") % [a, a]
|
||||
info = tr("Elementbonus +%d") % a
|
||||
show_preview(card, entered, perspective.looks, info, anchor)
|
||||
|
||||
|
||||
@@ -889,7 +911,7 @@ func show_hand_preview(card: Cards.Card, entered: bool, anchor: Rect2 = Rect2())
|
||||
func creature_info(cr: Game.Creature) -> String:
|
||||
var parts: Array[String] = []
|
||||
if cr.aura > 0:
|
||||
parts.append(tr("Elementbonus +%d/+%d") % [cr.aura, cr.aura])
|
||||
parts.append(tr("Elementbonus +%d") % cr.aura)
|
||||
var perk := cr.power() - cr.attack - cr.aura
|
||||
if perk > 0:
|
||||
parts.append(tr("Merkmal +%d") % perk)
|
||||
@@ -922,39 +944,25 @@ func update_attack_preview() -> void:
|
||||
for v in own_views:
|
||||
var mine: bool = v.creature == selected
|
||||
v.attack_shift = Game.matchup_bonus(selected, target) if mine and target != null else 0
|
||||
var counter: int = target.power() + Game.matchup_bonus(target, selected) if target != null and target.power() > 0 else 0
|
||||
var counter: int = maxi(0, target.power() + Game.matchup_bonus(target, selected)) if target != null else 0
|
||||
v.doomed = mine and target != null and counter >= selected.health
|
||||
v.queue_redraw()
|
||||
|
||||
|
||||
class Deck extends Control:
|
||||
signal clicked
|
||||
var enabled := true
|
||||
var count := 0
|
||||
var cost := Game.DRAW_COST
|
||||
|
||||
## Eine verdeckte Handkarte des Gegners in voller Größe.
|
||||
class CardBack extends Control:
|
||||
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()
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
|
||||
func _draw() -> void:
|
||||
for k in range(2, -1, -1):
|
||||
var r := Rect2(Vector2(k * 3, -k * 3), size - Vector2(6, 12)).grow(0)
|
||||
r.position.y += 6
|
||||
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD if enabled else Style.MUTED, 2), r)
|
||||
if k == 0:
|
||||
Style.border(self, r.grow(-12), Color("5a3c82"), 2, 6)
|
||||
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")
|
||||
if not enabled:
|
||||
draw_rect(Rect2(Vector2.ZERO, size), Color(0, 0, 0, 0.35))
|
||||
var r := Rect2(Vector2.ZERO, size)
|
||||
draw_style_box(Style.box(Color("3c285a"), 8, Style.GOLD, 2), r)
|
||||
Style.border(self, r.grow(-12), Color("5a3c82"), 2, 6)
|
||||
Style.text(self, "?", 40, Style.GOLD, size / 2, "center", true)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1048,7 +1056,7 @@ func refresh() -> void:
|
||||
labels.name.add_theme_color_override("font_color", Style.TEXT if o.alive() else Style.MUTED)
|
||||
if o.alive():
|
||||
labels.life.text = "♥ %d" % maxi(o.life, 0)
|
||||
labels.stats.text = "Hand %d ◆ %d" % [o.hand.size(), o.energy]
|
||||
labels.stats.text = "◆ %d" % o.energy
|
||||
else:
|
||||
labels.life.text = "besiegt"
|
||||
labels.life.add_theme_color_override("font_color", Style.MUTED)
|
||||
@@ -1088,6 +1096,11 @@ func refresh() -> void:
|
||||
var c := game.find_creature(uid)
|
||||
if c != null:
|
||||
_press(c, tile, pos))
|
||||
tile.double_clicked.connect(func(_v):
|
||||
var c := game.find_creature(uid)
|
||||
if c != null and c.owner() == perspective and my_turn() and not any_overlay():
|
||||
selected = c # Doppelklick: gleich auf das Leben des Gegners
|
||||
on_click("hero", opponent()))
|
||||
if arrivals.has(cr.uid):
|
||||
fx.fly_in(tile, arrivals[cr.uid])
|
||||
arrivals.erase(cr.uid)
|
||||
@@ -1125,13 +1138,33 @@ func refresh() -> void:
|
||||
tiles.erase(uid)
|
||||
fx.fade_out(t)
|
||||
update_attack_preview()
|
||||
deck.visible = not spectating()
|
||||
deck.enabled = mine and g.can_draw(h)[0]
|
||||
deck.count = g.deck.size()
|
||||
deck.cost = g.draw_cost(h)
|
||||
deck.queue_redraw()
|
||||
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()
|
||||
|
||||
Style.clear(hand_layer)
|
||||
for o in others(): # die Hand des Gegners oben, nur Rückseiten, spiegelverkehrter Bogen
|
||||
for i in o.hand.size():
|
||||
var back := CardBack.new(BattleLayout.opp_hand_rect(o.hand.size(), i))
|
||||
var arc: Array = BattleLayout.hand_arc(o.hand.size(), i, true)
|
||||
back.pivot_offset = back.size / 2
|
||||
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:
|
||||
@@ -1141,6 +1174,9 @@ func refresh() -> void:
|
||||
var r := BattleLayout.hand_rect(h.hand.size(), i)
|
||||
var v := CardView.new(card, r.size, false, card.rarity_in(h.looks))
|
||||
v.position = r.position
|
||||
var arc: Array = BattleLayout.hand_arc(h.hand.size(), i)
|
||||
v.rotation_degrees = arc[0]
|
||||
v.position.y += arc[1]
|
||||
v.lift = mine
|
||||
v.bonus = g.forecast(h, card)
|
||||
if fuse_first != null and mine and g.can_fuse(h, fuse_first, card)[0]:
|
||||
@@ -1163,16 +1199,13 @@ func refresh() -> void:
|
||||
name_label.text = h.name
|
||||
life_label.text = "♥ %d" % maxi(h.life, 0)
|
||||
energy_label.text = "◆ %d" % h.energy
|
||||
income_label.text = tr("nächster Zug ◆ %d") % (h.max_energy + 1)
|
||||
status_label.text = (tr("Zug %d · Du bist dran") % g.turn) if mine else (tr("Zug %d · %s ist dran …") % [g.turn, g.current().name])
|
||||
if spectating():
|
||||
status_label.text = tr("Zuschauer") + " · " + status_label.text
|
||||
b_end.disabled = not mine
|
||||
var ai_running: bool = g.winner == "" and not g.current().is_human and not is_client()
|
||||
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())
|
||||
var lines: Array[String] = []
|
||||
var start := maxi(0, g.log.size() - 60)
|
||||
for i in range(start, g.log.size()):
|
||||
|
||||
@@ -163,7 +163,7 @@ func _draw_full(rect: Rect2, frame: Color, seed: int) -> void:
|
||||
Style.stat_circle(self, Vector2(r_stat - 2, size.y - r_stat + 2), r_stat, str(card.attack), Style.AMBER, f_stat)
|
||||
Style.stat_circle(self, Vector2(size.x - r_stat + 2, size.y - r_stat + 2), r_stat, str(card.health), Style.LIFE, f_stat)
|
||||
if bonus > 0:
|
||||
Style.text_outlined(self, "+%d/+%d" % [bonus, bonus], f_text, Style.GREEN, Vector2(r_stat * 2 + 4, size.y - r_stat + 2), "midleft", 3)
|
||||
Style.text_outlined(self, "+%d" % bonus, f_text, Style.GREEN, Vector2(r_stat * 2 + 4, size.y - r_stat + 2), "midleft", 3)
|
||||
Style.rarity_gem(self, rarity, Vector2(size.x / 2, size.y - r_stat + 1), 8 if big else 6, seed)
|
||||
Style.border(self, rect, frame, 3 if big else 2, 8)
|
||||
Style.cost_crystal(self, Vector2(r_cost + 2, r_cost + 2), r_cost, card.cost, f_stat)
|
||||
|
||||
@@ -3,6 +3,7 @@ extends Control
|
||||
## Zeichnet ein Wesen auf dem Feld. Bleibt über Zustandsänderungen bestehen (für Animationen).
|
||||
|
||||
signal pressed(view: CreatureView, pos: Vector2) # linke Maustaste gedrückt; der Kampf macht daraus Klick oder Ziehen
|
||||
signal double_clicked(view: CreatureView) # Doppelklick: eigenes Wesen greift das Leben des Gegners an
|
||||
signal hover(card: Cards.Card, entered: bool)
|
||||
|
||||
var creature: Game.Creature
|
||||
@@ -38,7 +39,10 @@ func _process(_delta: float) -> void:
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
accept_event()
|
||||
pressed.emit(self, event.global_position)
|
||||
if event.double_click:
|
||||
double_clicked.emit(self)
|
||||
else:
|
||||
pressed.emit(self, event.global_position)
|
||||
|
||||
|
||||
func rarity() -> String:
|
||||
@@ -76,7 +80,7 @@ func _draw() -> void:
|
||||
var badge := ""
|
||||
var bw := 0.0
|
||||
if creature.aura != 0:
|
||||
badge = "+%d/+%d" % [creature.aura, creature.aura] if creature.aura > 0 else "−%d/−%d" % [-creature.aura, -creature.aura]
|
||||
badge = "+%d" % creature.aura if creature.aura > 0 else "−%d" % -creature.aura
|
||||
bw = Style.FONT_BOLD.get_string_size(badge, HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(10)).x + 8
|
||||
var by := Style.fs(fs) + 13
|
||||
draw_rect(Rect2(size.x - bw - 4, by, bw, 16), Color(0.05, 0.05, 0.08, 0.75))
|
||||
@@ -127,11 +131,11 @@ func _draw_back(rect: Rect2) -> void:
|
||||
|
||||
|
||||
## Angriff und Leben für den Angriff, der gerade vorbereitet wird. Fremdes Wesen bei gewähltem
|
||||
## Angreifer: sein Konter gegen ihn (im Nachteil 1 weniger, wer 0 Angriff hat, kontert gar nicht),
|
||||
## Angreifer: sein Konter gegen ihn (im Nachteil 1 weniger, im Vorteil 1 mehr, auch bei 0 Angriff),
|
||||
## das Leben bleibt echt. Eigener Angreifer über einem Ziel: sein Angriff mit Elementvorteil.
|
||||
func shown_stats() -> Array:
|
||||
if matchup != 0:
|
||||
var atk := 0 if creature.power() <= 0 else maxi(0, creature.power() - matchup)
|
||||
var atk := maxi(0, creature.power() - matchup)
|
||||
return [atk, creature.health]
|
||||
if attack_shift != 0:
|
||||
return [maxi(0, creature.power() + attack_shift), creature.health]
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
class_name ElementView
|
||||
extends Control
|
||||
## Elementboni auf einen Blick: offene Wesen je Element auf dem Tisch (ab drei +1/+1) und der
|
||||
## Sondereffekt der eigenen Seite (ab zwei eigenen: Erde Spott, Luft Flink, Wasser Regeneration,
|
||||
## Feuer Doppelschlag), als Symbol in der Elementfarbe.
|
||||
## 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).
|
||||
|
||||
var counts := {} # Tisch
|
||||
var own := {} # eigene Seite
|
||||
const ENTRY_W := 180.0
|
||||
|
||||
var own := {} # offene eigene Wesen je Element
|
||||
var played := {} # ausgespielte Karten je Element
|
||||
|
||||
|
||||
func _init(rect: Rect2) -> void:
|
||||
@@ -13,62 +15,53 @@ func _init(rect: Rect2) -> void:
|
||||
size = rect.size
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
for e in Cards.ELEMENTS:
|
||||
counts[e] = 0
|
||||
own[e] = 0
|
||||
played[e] = 0
|
||||
|
||||
|
||||
func update_from(g: Game, me: Game.Player) -> void:
|
||||
counts = g.element_counts()
|
||||
func update_from(_g: Game, me: Game.Player) -> void:
|
||||
for e in Cards.ELEMENTS:
|
||||
own[e] = me.element_count(e) if me != null else 0
|
||||
played[e] = int(me.played.get(e, 0)) if me != null else 0
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func strong(e: String) -> bool:
|
||||
return int(counts.get(e, 0)) >= Game.ELEMENT_STACK
|
||||
|
||||
|
||||
func paired(e: String) -> bool:
|
||||
return int(own.get(e, 0)) >= Game.ELEMENT_PAIR
|
||||
|
||||
|
||||
## Der Inhalt als Text (Tests, Protokoll).
|
||||
func bonus(e: String) -> int:
|
||||
return int(played.get(e, 0)) / Game.ELEMENT_MANA
|
||||
|
||||
|
||||
## 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:
|
||||
var s := "%s %d" % [tr(e), int(counts.get(e, 0))]
|
||||
if strong(e):
|
||||
s += " (+1/+1)"
|
||||
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)
|
||||
parts.append(s)
|
||||
return " · ".join(parts)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var w := size.x / Cards.ELEMENTS.size()
|
||||
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 col: Color = Cards.ELEMENT_COLORS[e]
|
||||
var x0 := i * w
|
||||
var cy := size.y / 2
|
||||
var n := int(counts.get(e, 0))
|
||||
if strong(e):
|
||||
draw_style_box(Style.box(Color(col, 0.35), 6, col, 2), Rect2(x0 + 2, 0, w - 4, size.y))
|
||||
elif paired(e):
|
||||
draw_style_box(Style.box(Color(col, 0.18), 6, Color(col, 0.6), 1), Rect2(x0 + 2, 0, w - 4, size.y))
|
||||
var active := strong(e) or paired(e)
|
||||
Style.text(self, tr(e), 13, Style.TEXT if active else Style.MUTED, Vector2(x0 + 12, cy), "midleft", active)
|
||||
var px := x0 + 12 + Style.FONT_BOLD.get_string_size(tr(e), HORIZONTAL_ALIGNMENT_LEFT, -1, Style.fs(13)).x + 12
|
||||
for k in Game.ELEMENT_STACK:
|
||||
var c := Vector2(px + k * 13, cy)
|
||||
if k < n:
|
||||
draw_circle(c, 4.5, col)
|
||||
else:
|
||||
draw_circle(c, 4.5, Color(0, 0, 0, 0.35))
|
||||
draw_arc(c, 4.5, 0, TAU, 16, Color(col, 0.6), 1.0)
|
||||
px += Game.ELEMENT_STACK * 13 + 2
|
||||
if paired(e):
|
||||
Style.keyword_icon(self, Cards.ELEMENT_KEYWORD[e], Vector2(px + 10, cy), 7, Style.lighter(col, 0.45))
|
||||
if strong(e):
|
||||
Style.text(self, "+1/+1", 13, Style.GREEN, Vector2(x0 + w - 12, cy), "midright", true)
|
||||
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
|
||||
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))
|
||||
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
|
||||
if bonus(e) > 0:
|
||||
Style.text(self, "+%d" % bonus(e), 12, Style.GREEN, Vector2(px, cy), "midleft", true)
|
||||
|
||||
@@ -4,9 +4,10 @@ extends Node
|
||||
|
||||
var streams := {}
|
||||
var players: Array[AudioStreamPlayer] = []
|
||||
var volume := 0.8
|
||||
var volume := 0.0 # Ton ist standardmäßig aus
|
||||
var muted := false # Tests: nie etwas abspielen, egal was die Regler sagen
|
||||
var music_player: AudioStreamPlayer
|
||||
var music_volume := 0.5
|
||||
var music_volume := 0.0
|
||||
var music_task := -1 # die Musik wird im Hintergrund berechnet (spart ~150 ms beim Start)
|
||||
var music_box := {}
|
||||
|
||||
@@ -78,7 +79,7 @@ func _music() -> AudioStreamWAV:
|
||||
|
||||
func set_music_volume(v: float) -> void:
|
||||
music_volume = clampf(v, 0.0, 1.0)
|
||||
if music_volume <= 0.0:
|
||||
if music_volume <= 0.0 or muted:
|
||||
music_player.stop()
|
||||
else:
|
||||
music_player.volume_db = linear_to_db(music_volume * 0.6)
|
||||
@@ -91,7 +92,7 @@ func set_volume(v: float) -> void:
|
||||
|
||||
|
||||
func play(kind: String) -> void:
|
||||
if volume <= 0.0 or not streams.has(kind):
|
||||
if volume <= 0.0 or muted or not streams.has(kind):
|
||||
return
|
||||
for p in players:
|
||||
if not p.playing:
|
||||
|
||||
Reference in New Issue
Block a user