update
This commit is contained in:
@@ -1,30 +1,35 @@
|
|||||||
78098b89c88a74ebe8c20117b8754620
|
78098b89c88a74ebe8c20117b8754620
|
||||||
::res://::1787944732
|
::res://::1787996516
|
||||||
anforderungen.md::TextFile::-1::1787943530::0::1::::<><><>0<>0<><>::
|
anforderungen.md::TextFile::-1::1787986378::0::1::::<><><>0<>0<><>::
|
||||||
README.md::TextFile::-1::1787944732::0::1::::<><><>0<>0<><>::
|
README.md::TextFile::-1::1789516876::0::1::::<><><>0<>0<><>::
|
||||||
::res://scenes/::1787944158
|
::res://scenes/::1787944158
|
||||||
Main.tscn::PackedScene::-1::1787946270::0::1::::<><><>0<>0<><>::res://scripts/ui/Main.gd
|
Main.tscn::PackedScene::-1::1787946270::0::1::::<><><>0<>0<><>::res://scripts/ui/Main.gd
|
||||||
::res://scripts/::1787943837
|
::res://scripts/::1787943837
|
||||||
::res://scripts/core/::1787944184
|
::res://scripts/core/::1789516714
|
||||||
Avatar.gd::GDScript::3571391337088680506::1787944282::0::1::::Avatar<>RefCounted<><>0<>0<><>::
|
Avatar.gd::GDScript::3571391337088680506::1789551593::0::1::::Avatar<>RefCounted<><>0<>0<><>::
|
||||||
BotAI.gd::GDScript::3217244540061411409::1787944184::0::1::::BotAI<>RefCounted<><>0<>0<><>::
|
BotAI.gd::GDScript::3217244540061411409::1789551660::0::1::::BotAI<>RefCounted<><>0<>0<><>::
|
||||||
Combat.gd::GDScript::3137923719046090123::1787944065::0::1::::Combat<>RefCounted<><>0<>0<><>::
|
Combat.gd::GDScript::3137923719046090123::1789551637::0::1::::Combat<>RefCounted<><>0<>0<><>::
|
||||||
Data.gd::GDScript::5827997371587166138::1787944533::0::1::::Data<>RefCounted<><>0<>0<><>::
|
Data.gd::GDScript::5827997371587166138::1789551593::0::1::::Data<>RefCounted<><>0<>0<><>::
|
||||||
Game.gd::GDScript::7849680363975727491::1787944110::0::1::::Game<>RefCounted<><>0<>0<><>::
|
Game.gd::GDScript::7849680363975727491::1789551637::0::1::::Game<>RefCounted<><>0<>0<><>::
|
||||||
Hex.gd::GDScript::3913565535274194804::1787943888::0::1::::Hex<>RefCounted<><>0<>0<><>::
|
Hex.gd::GDScript::3913565535274194804::1787984994::0::1::::Hex<>RefCounted<><>0<>0<><>::
|
||||||
PlayerState.gd::GDScript::7371028171494145347::1787944651::0::1::::PlayerState<>RefCounted<><>0<>0<><>::
|
PlayerState.gd::GDScript::7371028171494145347::1789551593::0::1::::PlayerState<>RefCounted<><>0<>0<><>::
|
||||||
Pool.gd::GDScript::8278475787586460724::1787944533::0::1::::Pool<>RefCounted<><>0<>0<><>::
|
Pool.gd::GDScript::8278475787586460724::1787984994::0::1::::Pool<>RefCounted<><>0<>0<><>::
|
||||||
ShopEntry.gd::GDScript::5330801221324202071::1787944184::0::1::::ShopEntry<>RefCounted<><>0<>0<><>::
|
ShopEntry.gd::GDScript::5330801221324202071::1787984994::0::1::::ShopEntry<>RefCounted<><>0<>0<><>::
|
||||||
::res://scripts/ui/::1787946270
|
::res://scripts/ui/::1789551774
|
||||||
BoardView3D.gd::GDScript::6505855525292201354::1787946197::0::1::::BoardView3D<>Node3D<><>0<>0<><>::
|
BoardView3D.gd::GDScript::6505855525292201354::1789551801::0::1::::BoardView3D<>Node3D<><>0<>0<><>::
|
||||||
HudOverlay.gd::GDScript::2880024841375975216::1787946209::0::1::::HudOverlay<>Control<><>0<>0<><>::
|
HudOverlay.gd::GDScript::2880024841375975216::1789551801::0::1::::HudOverlay<>Control<><>0<>0<><>::
|
||||||
Main.gd::GDScript::7620286773918972721::1787946270::0::1::::<>Node3D<><>0<>0<><>::
|
InfoColumn.gd::GDScript::8417939530115914045::1789551774::0::1::::InfoColumn<>Control<><>0<>0<><>::
|
||||||
UiKit.gd::GDScript::7622585584068197695::1787946021::0::1::::UiKit<>RefCounted<><>0<>0<><>::
|
Main.gd::GDScript::7620286773918972721::1789551894::0::1::::<>Node3D<><>0<>0<><>::
|
||||||
::res://tests/::1787945733
|
PlayersPanel.gd::GDScript::3868833939001261460::1789551726::0::1::::PlayersPanel<>Control<><>0<>0<><>::
|
||||||
Probe2.gd::GDScript::7806432109624259689::1787946278::0::1::::<>Control<><>0<>0<><>::
|
Sfx.gd::GDScript::5367741527190708903::1789551801::0::1::::Sfx<>Node<><>0<>0<><>::
|
||||||
probe2.tscn::PackedScene::-1::1787944588::0::1::::<><><>0<>0<><>::res://tests/Probe2.gd
|
ShopPanel.gd::GDScript::5251693507545150927::1789551774::0::1::::ShopPanel<>Control<><>0<>0<><>::
|
||||||
Probe3.gd::GDScript::5187223496600362631::1787946278::0::1::::<>Control<><>0<>0<><>::
|
TopBar.gd::GDScript::3252279457217410448::1789551726::0::1::::TopBar<>Control<><>0<>0<><>::
|
||||||
probe3.tscn::PackedScene::-1::1787945733::0::1::::<><><>0<>0<><>::res://tests/Probe3.gd
|
UiKit.gd::GDScript::7622585584068197695::1787984994::0::1::::UiKit<>RefCounted<><>0<>0<><>::
|
||||||
Probe.gd::GDScript::4485056788068318808::1787946278::0::1::::<>Control<><>0<>0<><>::
|
::res://tests/::1789516714
|
||||||
probe.tscn::PackedScene::-1::1787944434::0::1::::<><><>0<>0<><>::res://tests/Probe.gd
|
CamCal.gd::GDScript::7324782907689628524::1789516628::0::1::::<>Node3D<><>0<>0<><>::
|
||||||
sim_test.gd::GDScript::3463191307206118779::1787944708::0::1::::<>SceneTree<><>0<>0<><>::
|
camcal.tscn::PackedScene::-1::1787946501::0::1::::<><><>0<>0<><>::res://tests/CamCal.gd
|
||||||
|
Probe2.gd::GDScript::7806432109624259689::1789516628::0::1::::<>Node3D<><>0<>0<><>::
|
||||||
|
probe2.tscn::PackedScene::-1::1787946443::0::1::::<><><>0<>0<><>::res://tests/Probe2.gd
|
||||||
|
Probe3.gd::GDScript::5187223496600362631::1789516900::0::1::::<>Node3D<><>0<>0<><>::
|
||||||
|
probe3.tscn::PackedScene::-1::1787946443::0::1::::<><><>0<>0<><>::res://tests/Probe3.gd
|
||||||
|
sim_test.gd::GDScript::3463191307206118779::1789516655::0::1::::<>SceneTree<><>0<>0<><>::
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ list=[{
|
|||||||
"language": &"GDScript",
|
"language": &"GDScript",
|
||||||
"path": "res://scripts/ui/HudOverlay.gd"
|
"path": "res://scripts/ui/HudOverlay.gd"
|
||||||
}, {
|
}, {
|
||||||
|
"base": &"Control",
|
||||||
|
"class": &"InfoColumn",
|
||||||
|
"icon": "",
|
||||||
|
"is_abstract": false,
|
||||||
|
"is_tool": false,
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://scripts/ui/InfoColumn.gd"
|
||||||
|
}, {
|
||||||
"base": &"RefCounted",
|
"base": &"RefCounted",
|
||||||
"class": &"PlayerState",
|
"class": &"PlayerState",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
@@ -71,6 +79,14 @@ list=[{
|
|||||||
"language": &"GDScript",
|
"language": &"GDScript",
|
||||||
"path": "res://scripts/core/PlayerState.gd"
|
"path": "res://scripts/core/PlayerState.gd"
|
||||||
}, {
|
}, {
|
||||||
|
"base": &"Control",
|
||||||
|
"class": &"PlayersPanel",
|
||||||
|
"icon": "",
|
||||||
|
"is_abstract": false,
|
||||||
|
"is_tool": false,
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://scripts/ui/PlayersPanel.gd"
|
||||||
|
}, {
|
||||||
"base": &"RefCounted",
|
"base": &"RefCounted",
|
||||||
"class": &"Pool",
|
"class": &"Pool",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
@@ -79,6 +95,14 @@ list=[{
|
|||||||
"language": &"GDScript",
|
"language": &"GDScript",
|
||||||
"path": "res://scripts/core/Pool.gd"
|
"path": "res://scripts/core/Pool.gd"
|
||||||
}, {
|
}, {
|
||||||
|
"base": &"Node",
|
||||||
|
"class": &"Sfx",
|
||||||
|
"icon": "",
|
||||||
|
"is_abstract": false,
|
||||||
|
"is_tool": false,
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://scripts/ui/Sfx.gd"
|
||||||
|
}, {
|
||||||
"base": &"RefCounted",
|
"base": &"RefCounted",
|
||||||
"class": &"ShopEntry",
|
"class": &"ShopEntry",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
@@ -87,6 +111,22 @@ list=[{
|
|||||||
"language": &"GDScript",
|
"language": &"GDScript",
|
||||||
"path": "res://scripts/core/ShopEntry.gd"
|
"path": "res://scripts/core/ShopEntry.gd"
|
||||||
}, {
|
}, {
|
||||||
|
"base": &"Control",
|
||||||
|
"class": &"ShopPanel",
|
||||||
|
"icon": "",
|
||||||
|
"is_abstract": false,
|
||||||
|
"is_tool": false,
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://scripts/ui/ShopPanel.gd"
|
||||||
|
}, {
|
||||||
|
"base": &"Control",
|
||||||
|
"class": &"TopBar",
|
||||||
|
"icon": "",
|
||||||
|
"is_abstract": false,
|
||||||
|
"is_tool": false,
|
||||||
|
"language": &"GDScript",
|
||||||
|
"path": "res://scripts/ui/TopBar.gd"
|
||||||
|
}, {
|
||||||
"base": &"RefCounted",
|
"base": &"RefCounted",
|
||||||
"class": &"UiKit",
|
"class": &"UiKit",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
|
|||||||
Binary file not shown.
44
README.md
44
README.md
@@ -18,7 +18,12 @@ Würfel (Tank), Kegel (DPS) und Kugeln (Heiler). Alles wird im Code erzeugt.
|
|||||||
- Klick ohne Ziehen = auswählen. Info rechts, dort auch `Aufwerten`.
|
- Klick ohne Ziehen = auswählen. Info rechts, dort auch `Aufwerten`.
|
||||||
- Rechtsklick auf dem Feld = zurück auf die Bank, auf der Bank = verkaufen.
|
- Rechtsklick auf dem Feld = zurück auf die Bank, auf der Bank = verkaufen.
|
||||||
- Klicks treffen über einen Strahl von der Kamera auf die Feldebene.
|
- Klicks treffen über einen Strahl von der Kamera auf die Feldebene.
|
||||||
- `D` neu würfeln, `Leertaste` Kampf starten / überspringen, `Esc` Auswahl lösen
|
- `D` neu würfeln, `F` Shop einfrieren, `X` XP kaufen, `Leertaste` Kampf, `P` Pause, `Esc` Auswahl lösen
|
||||||
|
- Rechtsklick auf eine neutrale Shop-Karte kauft direkt mit der zuletzt gewählten Spezialisierung
|
||||||
|
- Spielername rechts anklicken = sein Brett oben ansehen. Der nächste Gegner steht schon in der Planung fest.
|
||||||
|
- `Q`/`E` oder mittlere Maustaste drehen die Kamera, das Mausrad zoomt
|
||||||
|
- Die Planung dauert 30 Sekunden, dann startet der Kampf von selbst
|
||||||
|
- Startbildschirm und Pause haben einen Lautstärkeregler
|
||||||
|
|
||||||
## Regeln
|
## Regeln
|
||||||
|
|
||||||
@@ -44,18 +49,36 @@ Würfel (Tank), Kegel (DPS) und Kugeln (Heiler). Alles wird im Code erzeugt.
|
|||||||
| Elemente kombinierbar wie Traits | `Data.ELEMENT_TRAITS`, `Data.COMBO_TRAITS` |
|
| Elemente kombinierbar wie Traits | `Data.ELEMENT_TRAITS`, `Data.COMBO_TRAITS` |
|
||||||
| 5 Gold pro Runde, 1 für Sieg, mehr bei Serie | `PlayerState.round_income()` |
|
| 5 Gold pro Runde, 1 für Sieg, mehr bei Serie | `PlayerState.round_income()` |
|
||||||
| 100 Leben | `Data.START_HP` |
|
| 100 Leben | `Data.START_HP` |
|
||||||
| Begrenzte Elementmenge für alle Spieler | `Pool.element_left`, 180 pro Element |
|
| Begrenzte Elementmenge für alle Spieler | `Pool.element_left`, 100 pro Element |
|
||||||
| 7 Bots | `Game.BOT_NAMES`, `BotAI` |
|
| 7 Bots | `Game.BOT_NAMES`, `BotAI` |
|
||||||
| Keine Items | nicht vorhanden |
|
| Keine Items | nicht vorhanden |
|
||||||
|
|
||||||
Nicht in den Anforderungen, aber nötig: die Kaderplätze wachsen mit der Runde
|
Nicht in den Anforderungen, aber „Basis wie TFT":
|
||||||
(`Data.team_slots()`, 3 bis 9). Der neutrale Avatar selbst ist unbegrenzt. Knapp ist
|
|
||||||
das Element, das man ihm beim Kauf gibt: das zieht einen Platz aus dem geteilten Vorrat.
|
|
||||||
|
|
||||||
Die 180 pro Element sind gemessen, nicht geraten: bei 30 war jedes Element nach
|
- **Level statt fester Kaderplätze.** Start Level 3, jede Runde 2 XP, 4 XP für 4 Gold.
|
||||||
gut der Hälfte der Partie leer, bei 160 lief in 1 von 12 Partien schon um Runde 15
|
Level = Kaderplätze, Maximum 9 (`Data.XP_TABLE`, `PlayerState.buy_xp()`).
|
||||||
eines leer. Bei 180 hält der Vorrat eine ganze Partie, der Tiefstand liegt aber
|
- **Schaden steigt mit der Runde.** Grundschaden 2, alle vier Runden +1, dazu die
|
||||||
noch bei rund einem Achtel.
|
Sternsumme der Überlebenden (`Data.round_base_damage()`, `Combat.survivor_stars`).
|
||||||
|
- **Planungsuhr** 30 s (`Game.tick_planning()`), Tests schalten sie ab.
|
||||||
|
- **Gegner ansehen** über die Spielerliste (`Main._on_scout()`, `BoardView3D.scout_player`).
|
||||||
|
- **Heiler heilen nur im Umkreis 2** (`Data.HEAL_RANGE`), Aufstellung zählt also.
|
||||||
|
- **Lesbarer Kampf**: Schadens- und Heilzahlen, Krit mit „!", Sterbe-Animation,
|
||||||
|
Zustände als Tönung und Marke (B Brand, S langsam, Z betäubt), Schildstreifen.
|
||||||
|
- **Klänge** werden im Code erzeugt (`Sfx`), keine Audiodateien.
|
||||||
|
- **Sichtbare Angriffe**: Fernkampf schickt eine Kugel, Nahkampf einen Hieb (`BoardView3D.spawn_attack`).
|
||||||
|
- **Element-Profile**: das Hauptelement prägt die Werte, z. B. Feuer +18% Angriff, Erde +22% Leben
|
||||||
|
(`Data.ELEMENT_PROFILE`). Dazu ein Kennzeichen am Körper: Flamme, Tropfen, Finne, Zacke, Schultern, Kristall.
|
||||||
|
- **Gegner vorab**: Paarungen entstehen beim Rundenstart (`Game._make_pairings` in `_next_round`).
|
||||||
|
Bots stellen dagegen auf: Tanks vor gegnerische Schützen, Schützen weg von Tanks (`BotAI.arrange`).
|
||||||
|
- **Shop einfrieren** (`PlayerState.shop_locked`), Schnellkauf mit der letzten Wahl (`last_element`).
|
||||||
|
- **Rückblick** nach jedem Kampf: Ergebnis, Überlebende, stärkste Einheit, gefährlichster Gegner
|
||||||
|
(`Combat.recap`, `Game.human_recap`).
|
||||||
|
- **Start, Pause, Lautstärke** als Overlays in `Main`, Kamera mit Zoom und Drehung um die Brettmitte.
|
||||||
|
|
||||||
|
Der neutrale Avatar selbst ist unbegrenzt. Knapp ist das Element, das man ihm beim
|
||||||
|
Kauf gibt: das zieht einen Platz aus dem geteilten Vorrat. Die 100 pro Element sind
|
||||||
|
gemessen: mit den Bots, die jetzt gezielt Futter jagen und Level kaufen, läuft bei 80
|
||||||
|
in 2 von 10 Partien ein Element leer, bei 100 keines, Tiefstand rund ein Fünftel.
|
||||||
|
|
||||||
Die Neutral-Chance ist multiplikativ (0,75 hoch Anzahl Elemente) statt in
|
Die Neutral-Chance ist multiplikativ (0,75 hoch Anzahl Elemente) statt in
|
||||||
Prozentpunkten. So verschwindet Neutral nie ganz: ohne eigene Elemente 100%,
|
Prozentpunkten. So verschwindet Neutral nie ganz: ohne eigene Elemente 100%,
|
||||||
@@ -70,7 +93,8 @@ Magma, Sturmfront, Dampf.
|
|||||||
## Aufbau
|
## Aufbau
|
||||||
|
|
||||||
scripts/core/ Data, Avatar, Pool, ShopEntry, PlayerState, Hex, Combat, BotAI, Game
|
scripts/core/ Data, Avatar, Pool, ShopEntry, PlayerState, Hex, Combat, BotAI, Game
|
||||||
scripts/ui/ Main (Szene + Bedienung), BoardView3D (Arena), HudOverlay, UiKit
|
scripts/ui/ Main (Szene, Kamera, Eingabe, Overlays), TopBar, InfoColumn, PlayersPanel,
|
||||||
|
ShopPanel (je ein Panel, melden per Signal), BoardView3D, HudOverlay, Sfx, UiKit
|
||||||
scenes/Main.tscn
|
scenes/Main.tscn
|
||||||
tests/ sim_test.gd (kopflos), Probe2, Probe3, CamCal
|
tests/ sim_test.gd (kopflos), Probe2, Probe3, CamCal
|
||||||
|
|
||||||
|
|||||||
@@ -70,13 +70,16 @@ func base_stats() -> Dictionary:
|
|||||||
var b: Dictionary = Data.ROLE_STATS[role]
|
var b: Dictionary = Data.ROLE_STATS[role]
|
||||||
var mult: float = Data.STAR_MULT[star - 1]
|
var mult: float = Data.STAR_MULT[star - 1]
|
||||||
var ebonus := 1.0 + Data.ELEMENT_STAT_BONUS * float(maxi(0, elements.size() - 1))
|
var ebonus := 1.0 + Data.ELEMENT_STAT_BONUS * float(maxi(0, elements.size() - 1))
|
||||||
|
var prof: Dictionary = Data.ELEMENT_PROFILE.get(primary_element(),
|
||||||
|
{"hp": 1.0, "atk": 1.0, "armor": 0.0, "aspd": 1.0, "mana": 1.0, "crit": 0.0})
|
||||||
return {
|
return {
|
||||||
"hp": b["hp"] * mult * ebonus,
|
"hp": b["hp"] * mult * ebonus * prof["hp"],
|
||||||
"atk": b["atk"] * mult * ebonus,
|
"atk": b["atk"] * mult * ebonus * prof["atk"],
|
||||||
"armor": b["armor"],
|
"armor": minf(0.75, b["armor"] + prof["armor"]),
|
||||||
"aspd": b["aspd"],
|
"aspd": b["aspd"] * prof["aspd"],
|
||||||
"range": b["range"],
|
"range": b["range"],
|
||||||
"mana": b["mana"],
|
"mana": b["mana"] * prof["mana"],
|
||||||
|
"crit": prof["crit"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,31 +22,111 @@ func setup(rand: RandomNumberGenerator) -> void:
|
|||||||
greed = rand.randf_range(0.3, 0.8)
|
greed = rand.randf_range(0.3, 0.8)
|
||||||
|
|
||||||
|
|
||||||
func take_turn(p: PlayerState, pool: Pool, rand: RandomNumberGenerator, round_no: int) -> void:
|
func take_turn(p: PlayerState, pool: Pool, rand: RandomNumberGenerator) -> void:
|
||||||
_do_upgrades(p, pool)
|
_do_upgrades(p, pool)
|
||||||
_do_buys(p, pool, rand, round_no)
|
_do_buys(p, pool)
|
||||||
_do_placements(p, pool, round_no)
|
_do_placements(p)
|
||||||
_do_upgrades(p, pool)
|
_do_upgrades(p, pool)
|
||||||
var keep := int(lerpf(2.0, 18.0, greed))
|
var keep := int(lerpf(2.0, 18.0, greed))
|
||||||
if p.gold > keep + Data.REROLL_COST and rand.randf() < 0.6:
|
# Wer kurz vor einem Upgrade steht, würfelt gezielt nach Futter.
|
||||||
p.reroll(pool, rand)
|
var hunting := _closest_upgrade_gap(p) <= 3
|
||||||
_do_buys(p, pool, rand, round_no)
|
var rolls := 3 if hunting else 1
|
||||||
_do_placements(p, pool, round_no)
|
for i in rolls:
|
||||||
_do_upgrades(p, pool)
|
if p.gold > keep + Data.REROLL_COST and (hunting or rand.randf() < 0.6):
|
||||||
|
p.reroll(pool, rand)
|
||||||
|
_do_buys(p, pool)
|
||||||
|
_do_placements(p)
|
||||||
|
_do_upgrades(p, pool)
|
||||||
|
# Level kaufen, wenn das Brett voll ist und Nachwuchs auf der Bank wartet.
|
||||||
|
while p.gold > keep + Data.XP_BUY_COST and not p.can_place() \
|
||||||
|
and _pick_bench_for_board(p) >= 0 and p.buy_xp():
|
||||||
|
_do_placements(p)
|
||||||
_free_bench(p, pool)
|
_free_bench(p, pool)
|
||||||
|
|
||||||
|
|
||||||
|
## Stellt das Brett gegen den bekannten Gegner um: Tanks vor seine Schützen,
|
||||||
|
## eigene Schützen weg von seinen Tanks. Ohne Gegner gilt die Standardaufstellung.
|
||||||
|
func arrange(p: PlayerState, opp: PlayerState) -> void:
|
||||||
|
if p.board.is_empty():
|
||||||
|
return
|
||||||
|
var units: Array = p.board_units()
|
||||||
|
p.board.clear()
|
||||||
|
var threat := [] # Spalten (gespiegelt), in denen gegnerische Schützen stehen
|
||||||
|
var enemy_tanks := [] # Spalten (gespiegelt) gegnerischer Tanks
|
||||||
|
if opp != null:
|
||||||
|
for hex in opp.board:
|
||||||
|
var col: int = Data.BOARD_COLS - 1 - hex.x
|
||||||
|
if opp.board[hex].role == Data.Role.TANK:
|
||||||
|
enemy_tanks.append(col)
|
||||||
|
else:
|
||||||
|
threat.append(col)
|
||||||
|
var center := [3, 2, 4, 1, 5, 0, 6]
|
||||||
|
var tank_cols := threat.duplicate()
|
||||||
|
for c in center:
|
||||||
|
if not tank_cols.has(c):
|
||||||
|
tank_cols.append(c)
|
||||||
|
# Schützen möglichst weit weg von gegnerischen Tanks.
|
||||||
|
var far_cols := center.duplicate()
|
||||||
|
far_cols.sort_custom(func(a, b):
|
||||||
|
return _min_dist(a, enemy_tanks) > _min_dist(b, enemy_tanks))
|
||||||
|
units.sort_custom(func(a, b): return a.role == Data.Role.TANK and b.role != Data.Role.TANK)
|
||||||
|
for u in units:
|
||||||
|
var rows: Array = [0, 1, 2, 3] if u.role == Data.Role.TANK else [3, 2, 1, 0]
|
||||||
|
var cols: Array = tank_cols if u.role == Data.Role.TANK else far_cols
|
||||||
|
var placed := false
|
||||||
|
for r in rows:
|
||||||
|
for c in cols:
|
||||||
|
var h := Vector2i(c, r)
|
||||||
|
if not p.board.has(h):
|
||||||
|
p.board[h] = u
|
||||||
|
placed = true
|
||||||
|
break
|
||||||
|
if placed:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
func _min_dist(col: int, cols: Array) -> int:
|
||||||
|
if cols.is_empty():
|
||||||
|
return 0
|
||||||
|
var best := 99
|
||||||
|
for c in cols:
|
||||||
|
best = mini(best, absi(col - c))
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
## Kleinste Zahl fehlender Futter-Avatare über alle Board-Einheiten.
|
||||||
|
func _closest_upgrade_gap(p: PlayerState) -> int:
|
||||||
|
var best := 99
|
||||||
|
for hex in p.board:
|
||||||
|
var miss := p.upgrade_missing(hex)
|
||||||
|
if p.board[hex].upgrade_cost().is_empty():
|
||||||
|
continue
|
||||||
|
var total := 0
|
||||||
|
for e in miss:
|
||||||
|
total += miss[e]
|
||||||
|
best = mini(best, total)
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
## Elemente, die irgendeine Board-Einheit für ihr nächstes Upgrade braucht.
|
||||||
|
func _needed_elements(p: PlayerState) -> Dictionary:
|
||||||
|
var need := {}
|
||||||
|
for hex in p.board:
|
||||||
|
for e in p.board[hex].upgrade_cost():
|
||||||
|
need[e] = true
|
||||||
|
return need
|
||||||
|
|
||||||
|
|
||||||
func _wants(entry: ShopEntry, p: PlayerState) -> bool:
|
func _wants(entry: ShopEntry, p: PlayerState) -> bool:
|
||||||
if entry.element < 0:
|
if entry.element < 0:
|
||||||
# Neutral wird beim Kauf spezialisiert, also immer brauchbar.
|
# Neutral wird beim Kauf spezialisiert, also immer brauchbar.
|
||||||
return true
|
return true
|
||||||
if pref.has(entry.element):
|
if _needed_elements(p).has(entry.element):
|
||||||
return true
|
return true
|
||||||
# Elemente, die schon auf dem Board sind, sind immer Futter.
|
return pref.has(entry.element)
|
||||||
return p.trait_counts().has(entry.element)
|
|
||||||
|
|
||||||
|
|
||||||
func _do_buys(p: PlayerState, pool: Pool, rand: RandomNumberGenerator, round_no: int) -> void:
|
func _do_buys(p: PlayerState, pool: Pool) -> void:
|
||||||
for i in p.shop.size():
|
for i in p.shop.size():
|
||||||
var e: ShopEntry = p.shop[i]
|
var e: ShopEntry = p.shop[i]
|
||||||
if e.sold:
|
if e.sold:
|
||||||
@@ -60,8 +140,8 @@ func _do_buys(p: PlayerState, pool: Pool, rand: RandomNumberGenerator, round_no:
|
|||||||
p.buy(i, pool, _pick_element(p) if e.element < 0 else e.element)
|
p.buy(i, pool, _pick_element(p) if e.element < 0 else e.element)
|
||||||
|
|
||||||
|
|
||||||
func _do_placements(p: PlayerState, pool: Pool, round_no: int) -> void:
|
func _do_placements(p: PlayerState) -> void:
|
||||||
while p.can_place(round_no):
|
while p.can_place():
|
||||||
var idx := _pick_bench_for_board(p)
|
var idx := _pick_bench_for_board(p)
|
||||||
if idx < 0:
|
if idx < 0:
|
||||||
return
|
return
|
||||||
@@ -69,7 +149,7 @@ func _do_placements(p: PlayerState, pool: Pool, round_no: int) -> void:
|
|||||||
var hex := _pick_hex(p, a.role)
|
var hex := _pick_hex(p, a.role)
|
||||||
if hex.x < 0:
|
if hex.x < 0:
|
||||||
return
|
return
|
||||||
if not p.place(idx, hex, round_no):
|
if not p.place(idx, hex):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@@ -77,6 +157,7 @@ func _do_placements(p: PlayerState, pool: Pool, round_no: int) -> void:
|
|||||||
## Alles Weitere bleibt als Futter auf der Bank.
|
## Alles Weitere bleibt als Futter auf der Bank.
|
||||||
func _pick_bench_for_board(p: PlayerState) -> int:
|
func _pick_bench_for_board(p: PlayerState) -> int:
|
||||||
var counts := p.trait_counts()
|
var counts := p.trait_counts()
|
||||||
|
var need := _needed_elements(p)
|
||||||
var best := -1
|
var best := -1
|
||||||
var best_score := -1
|
var best_score := -1
|
||||||
for i in p.bench.size():
|
for i in p.bench.size():
|
||||||
@@ -84,7 +165,9 @@ func _pick_bench_for_board(p: PlayerState) -> int:
|
|||||||
if a == null:
|
if a == null:
|
||||||
continue
|
continue
|
||||||
var score := 0
|
var score := 0
|
||||||
if pref.has(a.elements[0]):
|
if need.has(a.elements[0]) and counts.get(a.elements[0], 0) > 0:
|
||||||
|
score = 0 # Futter für ein laufendes Upgrade bleibt auf der Bank
|
||||||
|
elif pref.has(a.elements[0]):
|
||||||
score = 3 if counts.get(a.elements[0], 0) == 0 else 1
|
score = 3 if counts.get(a.elements[0], 0) == 0 else 1
|
||||||
else:
|
else:
|
||||||
score = 2 if counts.get(a.elements[0], 0) == 0 else 0
|
score = 2 if counts.get(a.elements[0], 0) == 0 else 0
|
||||||
@@ -144,12 +227,16 @@ func _do_upgrades(p: PlayerState, pool: Pool) -> void:
|
|||||||
func _free_bench(p: PlayerState, pool: Pool) -> void:
|
func _free_bench(p: PlayerState, pool: Pool) -> void:
|
||||||
if p.free_bench_slot() >= 0:
|
if p.free_bench_slot() >= 0:
|
||||||
return
|
return
|
||||||
var counts := p.trait_counts()
|
var need := _needed_elements(p)
|
||||||
|
# Erst verkaufen, was weder Futter noch Lieblingselement ist.
|
||||||
for i in p.bench.size():
|
for i in p.bench.size():
|
||||||
var a = p.bench[i]
|
var a = p.bench[i]
|
||||||
if a == null:
|
if a != null and not need.has(a.elements[0]) and not pref.has(a.elements[0]):
|
||||||
continue
|
p.sell_bench(i, pool)
|
||||||
if counts.get(a.elements[0], 0) == 0 and not pref.has(a.elements[0]):
|
return
|
||||||
|
for i in p.bench.size():
|
||||||
|
var a = p.bench[i]
|
||||||
|
if a != null and not need.has(a.elements[0]):
|
||||||
p.sell_bench(i, pool)
|
p.sell_bench(i, pool)
|
||||||
return
|
return
|
||||||
p.sell_bench(0, pool)
|
p.sell_bench(0, pool)
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ class CUnit extends RefCounted:
|
|||||||
var target = null
|
var target = null
|
||||||
var cast_flash: float = 0.0
|
var cast_flash: float = 0.0
|
||||||
var hit_flash: float = 0.0
|
var hit_flash: float = 0.0
|
||||||
|
var dealt: float = 0.0 ## ausgeteilter Schaden, für den Rückblick
|
||||||
|
var healed: float = 0.0
|
||||||
|
|
||||||
func effective_aspd() -> float:
|
func effective_aspd() -> float:
|
||||||
return maxf(0.1, aspd * (1.0 + haste) * (1.0 - slow))
|
return maxf(0.1, aspd * (1.0 + haste) * (1.0 - slow))
|
||||||
@@ -57,7 +59,12 @@ var time: float = 0.0
|
|||||||
var finished: bool = false
|
var finished: bool = false
|
||||||
var winner: int = -1 ## 0 = Team A, 1 = Team B, -1 = unentschieden
|
var winner: int = -1 ## 0 = Team A, 1 = Team B, -1 = unentschieden
|
||||||
var survivors: int = 0
|
var survivors: int = 0
|
||||||
|
var survivor_stars: int = 0 ## Sternsumme der Überlebenden, bestimmt den Schaden
|
||||||
var events: Array = [] ## kurze Textmeldungen für das Log
|
var events: Array = [] ## kurze Textmeldungen für das Log
|
||||||
|
## Treffer, Heilungen und Zauber für Anzeige und Sound. Nur im Spielerkampf
|
||||||
|
## aufgezeichnet, Bot-Kämpfe laufen still durch.
|
||||||
|
var record_hits: bool = false
|
||||||
|
var hits: Array = []
|
||||||
|
|
||||||
var _mods := [{}, {}]
|
var _mods := [{}, {}]
|
||||||
var _occ := {}
|
var _occ := {}
|
||||||
@@ -116,7 +123,7 @@ func _spawn(p: PlayerState, team: int) -> void:
|
|||||||
u.max_mana = s["mana"]
|
u.max_mana = s["mana"]
|
||||||
u.mana = 0.0
|
u.mana = 0.0
|
||||||
u.mana_mult = m["mana"]
|
u.mana_mult = m["mana"]
|
||||||
u.crit = m["crit"]
|
u.crit = m["crit"] + s.get("crit", 0.0)
|
||||||
if _occ.has(u.pos):
|
if _occ.has(u.pos):
|
||||||
u.pos = _free_near(u.pos)
|
u.pos = _free_near(u.pos)
|
||||||
_occ[u.pos] = u
|
_occ[u.pos] = u
|
||||||
@@ -266,6 +273,9 @@ func _basic_attack(u: CUnit, t: CUnit) -> void:
|
|||||||
var dmg := u.atk
|
var dmg := u.atk
|
||||||
if u.crit > 0.0 and _rand.randf() < u.crit:
|
if u.crit > 0.0 and _rand.randf() < u.crit:
|
||||||
dmg *= CRIT_MULT
|
dmg *= CRIT_MULT
|
||||||
|
if record_hits:
|
||||||
|
hits.append({"kind": "attack", "pos": t.pos, "from": u.pos, "team": u.team,
|
||||||
|
"ranged": u.atk_range > 1, "element": u.avatar.primary_element(), "amount": 0, "t": time})
|
||||||
_apply_damage(u, t, dmg, false)
|
_apply_damage(u, t, dmg, false)
|
||||||
var ice: float = _mods[u.team]["ice"]
|
var ice: float = _mods[u.team]["ice"]
|
||||||
if ice > 0.0:
|
if ice > 0.0:
|
||||||
@@ -291,6 +301,10 @@ func _apply_damage(src, tgt: CUnit, amount: float, is_true: bool) -> void:
|
|||||||
if dmg > 0.0:
|
if dmg > 0.0:
|
||||||
tgt.hp -= dmg
|
tgt.hp -= dmg
|
||||||
tgt.hit_flash = 0.12
|
tgt.hit_flash = 0.12
|
||||||
|
if src != null:
|
||||||
|
src.dealt += dmg
|
||||||
|
if record_hits and src != null:
|
||||||
|
_record("crit" if amount > src.atk * 1.4 and not is_true else "dmg", tgt, dmg)
|
||||||
tgt.mana += MANA_PER_HIT * tgt.mana_mult
|
tgt.mana += MANA_PER_HIT * tgt.mana_mult
|
||||||
if tgt.mana >= tgt.max_mana and tgt.alive and tgt.hp > 0.0:
|
if tgt.mana >= tgt.max_mana and tgt.alive and tgt.hp > 0.0:
|
||||||
_cast(tgt)
|
_cast(tgt)
|
||||||
@@ -319,8 +333,17 @@ func _kill(u: CUnit) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _heal(u: CUnit, amount: float) -> void:
|
func _heal(u: CUnit, amount: float) -> void:
|
||||||
if u.alive:
|
if not u.alive:
|
||||||
u.hp = minf(u.max_hp, u.hp + amount)
|
return
|
||||||
|
var real := minf(u.max_hp, u.hp + amount) - u.hp
|
||||||
|
u.hp += real
|
||||||
|
u.healed += real
|
||||||
|
if record_hits and real >= 1.0:
|
||||||
|
_record("heal", u, real)
|
||||||
|
|
||||||
|
|
||||||
|
func _record(kind: String, u: CUnit, amount: float) -> void:
|
||||||
|
hits.append({"kind": kind, "pos": u.pos, "team": u.team, "amount": int(round(amount)), "t": time})
|
||||||
|
|
||||||
|
|
||||||
# --- Fähigkeiten ----------------------------------------------------------
|
# --- Fähigkeiten ----------------------------------------------------------
|
||||||
@@ -328,6 +351,8 @@ func _heal(u: CUnit, amount: float) -> void:
|
|||||||
func _cast(u: CUnit) -> void:
|
func _cast(u: CUnit) -> void:
|
||||||
u.mana = 0.0
|
u.mana = 0.0
|
||||||
u.cast_flash = 0.35
|
u.cast_flash = 0.35
|
||||||
|
if record_hits:
|
||||||
|
_record("cast", u, 0.0)
|
||||||
var elems: Array = u.avatar.elements
|
var elems: Array = u.avatar.elements
|
||||||
var heal_bonus := 0.0
|
var heal_bonus := 0.0
|
||||||
for i in elems.size():
|
for i in elems.size():
|
||||||
@@ -373,7 +398,7 @@ func _cast(u: CUnit) -> void:
|
|||||||
hit_enemies.append(o)
|
hit_enemies.append(o)
|
||||||
Data.Role.HEILER:
|
Data.Role.HEILER:
|
||||||
var amount := u.atk * (3.0 - 0.25 * float(u.avatar.star - 1)) * (1.0 + heal_bonus)
|
var amount := u.atk * (3.0 - 0.25 * float(u.avatar.star - 1)) * (1.0 + heal_bonus)
|
||||||
var allies := _allies_by_missing_hp(u.team)
|
var allies := _allies_by_missing_hp(u.team, u)
|
||||||
var count := 1
|
var count := 1
|
||||||
if u.avatar.star == 2:
|
if u.avatar.star == 2:
|
||||||
count = 3
|
count = 3
|
||||||
@@ -428,11 +453,14 @@ func _apply_element_effects(u: CUnit, main_target, hit_enemies: Array) -> void:
|
|||||||
main_target.slow_timer = maxf(main_target.slow_timer, 3.0)
|
main_target.slow_timer = maxf(main_target.slow_timer, 3.0)
|
||||||
|
|
||||||
|
|
||||||
func _allies_by_missing_hp(team: int) -> Array:
|
## Verbündete in Heilreichweite, schwächste zuerst. Mindestens der Heiler selbst.
|
||||||
|
func _allies_by_missing_hp(team: int, healer: CUnit) -> Array:
|
||||||
var list := []
|
var list := []
|
||||||
for o in units:
|
for o in units:
|
||||||
if o.alive and o.team == team:
|
if o.alive and o.team == team and Hex.distance(o.pos, healer.pos) <= Data.HEAL_RANGE:
|
||||||
list.append(o)
|
list.append(o)
|
||||||
|
if list.is_empty():
|
||||||
|
list.append(healer)
|
||||||
list.sort_custom(func(a, b): return (a.hp / a.max_hp) < (b.hp / b.max_hp))
|
list.sort_custom(func(a, b): return (a.hp / a.max_hp) < (b.hp / b.max_hp))
|
||||||
return list
|
return list
|
||||||
|
|
||||||
@@ -447,6 +475,40 @@ func alive_count(team: int) -> int:
|
|||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
## Kurzer Rückblick für ein Team: Überlebende, stärkste Einheit, Gegnerspitze.
|
||||||
|
func recap(team: int) -> Dictionary:
|
||||||
|
var total := 0
|
||||||
|
var alive := 0
|
||||||
|
var best = null
|
||||||
|
var best_enemy = null
|
||||||
|
for u in units:
|
||||||
|
if u.team == team:
|
||||||
|
total += 1
|
||||||
|
if u.alive:
|
||||||
|
alive += 1
|
||||||
|
if best == null or u.dealt + u.healed > best.dealt + best.healed:
|
||||||
|
best = u
|
||||||
|
elif best_enemy == null or u.dealt > best_enemy.dealt:
|
||||||
|
best_enemy = u
|
||||||
|
return {
|
||||||
|
"total": total, "alive": alive,
|
||||||
|
"best_name": best.avatar.display_name() if best else "-",
|
||||||
|
"best_dealt": int(best.dealt) if best else 0,
|
||||||
|
"best_healed": int(best.healed) if best else 0,
|
||||||
|
"enemy_name": best_enemy.avatar.display_name() if best_enemy else "-",
|
||||||
|
"enemy_dealt": int(best_enemy.dealt) if best_enemy else 0,
|
||||||
|
"duration": time,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _star_sum(team: int) -> int:
|
||||||
|
var n := 0
|
||||||
|
for u in units:
|
||||||
|
if u.alive and u.team == team:
|
||||||
|
n += u.avatar.star
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
func _check_end() -> void:
|
func _check_end() -> void:
|
||||||
var a := alive_count(0)
|
var a := alive_count(0)
|
||||||
var b := alive_count(1)
|
var b := alive_count(1)
|
||||||
@@ -455,9 +517,11 @@ func _check_end() -> void:
|
|||||||
if a == b:
|
if a == b:
|
||||||
winner = -1
|
winner = -1
|
||||||
survivors = 0
|
survivors = 0
|
||||||
|
survivor_stars = 0
|
||||||
else:
|
else:
|
||||||
winner = 0 if a > b else 1
|
winner = 0 if a > b else 1
|
||||||
survivors = maxi(a, b)
|
survivors = maxi(a, b)
|
||||||
|
survivor_stars = _star_sum(winner)
|
||||||
return
|
return
|
||||||
if time >= TIME_LIMIT:
|
if time >= TIME_LIMIT:
|
||||||
_resolve_by_hp()
|
_resolve_by_hp()
|
||||||
@@ -472,6 +536,8 @@ func _resolve_by_hp() -> void:
|
|||||||
if absf(frac[0] - frac[1]) < 0.25:
|
if absf(frac[0] - frac[1]) < 0.25:
|
||||||
winner = -1
|
winner = -1
|
||||||
survivors = 0
|
survivors = 0
|
||||||
|
survivor_stars = 0
|
||||||
else:
|
else:
|
||||||
winner = 0 if frac[0] > frac[1] else 1
|
winner = 0 if frac[0] > frac[1] else 1
|
||||||
survivors = alive_count(winner)
|
survivors = alive_count(winner)
|
||||||
|
survivor_stars = _star_sum(winner)
|
||||||
|
|||||||
@@ -28,6 +28,21 @@ const ROLE_STATS := {
|
|||||||
Role.HEILER: {"hp": 520.0, "atk": 38.0, "armor": 0.22, "aspd": 0.70, "range": 3, "mana": 90.0},
|
Role.HEILER: {"hp": 520.0, "atk": 38.0, "armor": 0.22, "aspd": 0.70, "range": 3, "mana": 90.0},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Das Hauptelement prägt die Werte: Feuer schlägt hart, Erde hält aus, Wind ist
|
||||||
|
# schnell, Blitz trifft kritisch, Wasser zaubert öfter, Eis ist zäh und langsam.
|
||||||
|
const ELEMENT_PROFILE := {
|
||||||
|
Element.FEUER: {"hp": 0.90, "atk": 1.18, "armor": 0.0, "aspd": 1.0, "mana": 1.0, "crit": 0.0},
|
||||||
|
Element.WASSER: {"hp": 1.0, "atk": 0.95, "armor": 0.0, "aspd": 1.0, "mana": 0.75, "crit": 0.0},
|
||||||
|
Element.WIND: {"hp": 0.92, "atk": 0.95, "armor": 0.0, "aspd": 1.20, "mana": 1.0, "crit": 0.0},
|
||||||
|
Element.BLITZ: {"hp": 0.95, "atk": 1.0, "armor": 0.0, "aspd": 1.05, "mana": 1.0, "crit": 0.15},
|
||||||
|
Element.ERDE: {"hp": 1.22, "atk": 0.92, "armor": 0.08, "aspd": 0.90, "mana": 1.0, "crit": 0.0},
|
||||||
|
Element.EIS: {"hp": 1.08, "atk": 0.95, "armor": 0.05, "aspd": 0.95, "mana": 0.9, "crit": 0.0},
|
||||||
|
}
|
||||||
|
const PROFILE_HINT := {
|
||||||
|
Element.FEUER: "hart, aber weich", Element.WASSER: "zaubert oft", Element.WIND: "schnell",
|
||||||
|
Element.BLITZ: "kritisch", Element.ERDE: "zäh und langsam", Element.EIS: "robust",
|
||||||
|
}
|
||||||
|
|
||||||
# Multiplikator pro Sternstufe (Index = star - 1).
|
# Multiplikator pro Sternstufe (Index = star - 1).
|
||||||
const STAR_MULT := [1.0, 1.8, 3.24]
|
const STAR_MULT := [1.0, 1.8, 3.24]
|
||||||
const MAX_STAR := 3
|
const MAX_STAR := 3
|
||||||
@@ -55,13 +70,29 @@ const START_GOLD := 4
|
|||||||
# --- Geteilter Pool --------------------------------------------------------
|
# --- Geteilter Pool --------------------------------------------------------
|
||||||
## Alle 8 Spieler teilen sich diesen Vorrat pro Element. Ein 3-Star bindet
|
## Alle 8 Spieler teilen sich diesen Vorrat pro Element. Ein 3-Star bindet
|
||||||
## allein 9 Stück, dazu kommt was auf Bänken und Brettern liegt.
|
## allein 9 Stück, dazu kommt was auf Bänken und Brettern liegt.
|
||||||
## Bei 180 hält der Vorrat eine ganze Partie und kann im Endspiel trotzdem knapp werden.
|
## Bei 100 hält der Vorrat eine ganze Partie, geht im Endspiel aber auf rund ein Fünftel.
|
||||||
const POOL_PER_ELEMENT := 180
|
const POOL_PER_ELEMENT := 100
|
||||||
|
|
||||||
# --- Board -----------------------------------------------------------------
|
# --- Board -----------------------------------------------------------------
|
||||||
const BOARD_COLS := 7
|
const BOARD_COLS := 7
|
||||||
const BOARD_ROWS := 4
|
const BOARD_ROWS := 4
|
||||||
const MAX_TEAM_SLOTS := 9
|
|
||||||
|
# --- Level und Erfahrung ---------------------------------------------------
|
||||||
|
## Das Spielerlevel ist die Anzahl der Kaderplätze. Jede Runde gibt XP,
|
||||||
|
## weitere XP kauft man. Level gegen Würfeln ist die TFT-Kernentscheidung.
|
||||||
|
const START_LEVEL := 3
|
||||||
|
const MAX_LEVEL := 9
|
||||||
|
const XP_PER_ROUND := 2
|
||||||
|
const XP_BUY_COST := 4
|
||||||
|
const XP_BUY_AMOUNT := 4
|
||||||
|
const XP_TABLE := {3: 4, 4: 8, 5: 12, 6: 20, 7: 32, 8: 48}
|
||||||
|
|
||||||
|
# --- Planung ---------------------------------------------------------------
|
||||||
|
const PLAN_SECONDS := 30.0
|
||||||
|
|
||||||
|
# --- Kampf -----------------------------------------------------------------
|
||||||
|
## Heiler heilen nur Verbündete in dieser Hex-Reichweite.
|
||||||
|
const HEAL_RANGE := 2
|
||||||
|
|
||||||
# --- Element-Traits --------------------------------------------------------
|
# --- Element-Traits --------------------------------------------------------
|
||||||
# steps = Anzahl aufgestellter Einheiten mit diesem Element.
|
# steps = Anzahl aufgestellter Einheiten mit diesem Element.
|
||||||
@@ -133,9 +164,14 @@ static func trait_tier(element: int, count: int) -> int:
|
|||||||
return tier
|
return tier
|
||||||
|
|
||||||
|
|
||||||
## Teamgröße wächst mit der Runde.
|
## XP bis zum nächsten Level, 0 auf Maximalstufe.
|
||||||
static func team_slots(round_no: int) -> int:
|
static func xp_needed(level: int) -> int:
|
||||||
return mini(3 + int(float(round_no - 1) / 2.0), MAX_TEAM_SLOTS)
|
return XP_TABLE.get(level, 0)
|
||||||
|
|
||||||
|
|
||||||
|
## Grundschaden einer verlorenen Runde, steigt alle vier Runden.
|
||||||
|
static func round_base_damage(round_no: int) -> int:
|
||||||
|
return 2 + int(float(round_no - 1) / 4.0)
|
||||||
|
|
||||||
|
|
||||||
## Wahrscheinlichkeit, dass ein Shop-Platz einen neutralen Avatar zeigt.
|
## Wahrscheinlichkeit, dass ein Shop-Platz einen neutralen Avatar zeigt.
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ var pairings: Array = [] ## [{a, b, ghost}]
|
|||||||
var player_combat: Combat = null
|
var player_combat: Combat = null
|
||||||
var pending: Array = [] ## Ergebnisse der Bot-Kämpfe
|
var pending: Array = [] ## Ergebnisse der Bot-Kämpfe
|
||||||
var last_round_log: Array = []
|
var last_round_log: Array = []
|
||||||
|
var plan_left: float = Data.PLAN_SECONDS
|
||||||
|
var timer_enabled: bool = true
|
||||||
|
var human_recap: Dictionary = {} ## Rückblick auf den letzten eigenen Kampf
|
||||||
|
## Zählt, wie oft der Mensch gewonnen bzw. verloren hat (für Sound und Anzeige).
|
||||||
|
var human_last_result: int = 0 ## 1 Sieg, -1 Niederlage, 0 unentschieden/keine
|
||||||
|
|
||||||
|
|
||||||
func start(seed_value: int = 0) -> void:
|
func start(seed_value: int = 0) -> void:
|
||||||
@@ -37,7 +42,9 @@ func start(seed_value: int = 0) -> void:
|
|||||||
round_no = 1
|
round_no = 1
|
||||||
for p in players:
|
for p in players:
|
||||||
p.roll_shop(pool, rand)
|
p.roll_shop(pool, rand)
|
||||||
|
pairings = _make_pairings()
|
||||||
_run_bot_turns()
|
_run_bot_turns()
|
||||||
|
plan_left = Data.PLAN_SECONDS
|
||||||
phase = Phase.PLANNING
|
phase = Phase.PLANNING
|
||||||
|
|
||||||
|
|
||||||
@@ -54,7 +61,21 @@ func alive_players() -> Array:
|
|||||||
func _run_bot_turns() -> void:
|
func _run_bot_turns() -> void:
|
||||||
for p in players:
|
for p in players:
|
||||||
if p.is_bot and p.alive:
|
if p.is_bot and p.alive:
|
||||||
bots[p.pid].take_turn(p, pool, rand, round_no)
|
bots[p.pid].take_turn(p, pool, rand)
|
||||||
|
bots[p.pid].arrange(p, opponent_of(p))
|
||||||
|
|
||||||
|
|
||||||
|
## Planungsuhr. Läuft sie ab, startet der Kampf von selbst.
|
||||||
|
## Gibt true zurück, wenn dadurch die Phase gewechselt hat.
|
||||||
|
func tick_planning(dt: float) -> bool:
|
||||||
|
if phase != Phase.PLANNING or not timer_enabled:
|
||||||
|
return false
|
||||||
|
plan_left -= dt
|
||||||
|
if plan_left <= 0.0:
|
||||||
|
plan_left = 0.0
|
||||||
|
start_combat()
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
# --- Kampfphase ------------------------------------------------------------
|
# --- Kampfphase ------------------------------------------------------------
|
||||||
@@ -62,12 +83,16 @@ func _run_bot_turns() -> void:
|
|||||||
func start_combat() -> void:
|
func start_combat() -> void:
|
||||||
if phase != Phase.PLANNING:
|
if phase != Phase.PLANNING:
|
||||||
return
|
return
|
||||||
pairings = _make_pairings()
|
# Abbilder erst jetzt kopieren, damit sie den aktuellen Stand zeigen.
|
||||||
|
for pr in pairings:
|
||||||
|
if pr["ghost"]:
|
||||||
|
pr["b"] = _make_ghost(pr["ghost_src"])
|
||||||
pending.clear()
|
pending.clear()
|
||||||
player_combat = null
|
player_combat = null
|
||||||
for pr in pairings:
|
for pr in pairings:
|
||||||
if pr["a"] == human or pr["b"] == human:
|
if pr["a"] == human or pr["b"] == human:
|
||||||
player_combat = Combat.new(pr["a"], pr["b"], rand.randi())
|
player_combat = Combat.new(pr["a"], pr["b"], rand.randi())
|
||||||
|
player_combat.record_hits = true
|
||||||
pending.append({"pair": pr, "combat": player_combat})
|
pending.append({"pair": pr, "combat": player_combat})
|
||||||
else:
|
else:
|
||||||
var c := Combat.new(pr["a"], pr["b"], rand.randi())
|
var c := Combat.new(pr["a"], pr["b"], rand.randi())
|
||||||
@@ -107,7 +132,7 @@ func _make_pairings() -> Array:
|
|||||||
if others.is_empty():
|
if others.is_empty():
|
||||||
return out
|
return out
|
||||||
var src: PlayerState = others[rand.randi_range(0, others.size() - 1)]
|
var src: PlayerState = others[rand.randi_range(0, others.size() - 1)]
|
||||||
out.append({"a": lone, "b": _make_ghost(src), "ghost": true})
|
out.append({"a": lone, "b": _make_ghost(src), "ghost": true, "ghost_src": src})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
@@ -129,18 +154,25 @@ func resolve_round() -> void:
|
|||||||
c.run_to_end()
|
c.run_to_end()
|
||||||
var a: PlayerState = pr["a"]
|
var a: PlayerState = pr["a"]
|
||||||
var b: PlayerState = pr["b"]
|
var b: PlayerState = pr["b"]
|
||||||
var dmg := 3 + c.survivors
|
var base := Data.round_base_damage(round_no)
|
||||||
|
var dmg := base + c.survivor_stars
|
||||||
|
if a == human or b == human:
|
||||||
|
human_last_result = 0 if c.winner < 0 else (1 if pr["a" if c.winner == 0 else "b"] == human else -1)
|
||||||
|
human_recap = c.recap(0 if a == human else 1)
|
||||||
|
human_recap["opponent"] = (b if a == human else a).pname
|
||||||
|
human_recap["result"] = human_last_result
|
||||||
|
human_recap["damage"] = dmg if c.winner >= 0 else base
|
||||||
if c.winner == 0:
|
if c.winner == 0:
|
||||||
_apply_result(a, b, dmg, pr["ghost"])
|
_apply_result(a, b, dmg, pr["ghost"])
|
||||||
elif c.winner == 1:
|
elif c.winner == 1:
|
||||||
_apply_result(b, a, dmg, pr["ghost"])
|
_apply_result(b, a, dmg, pr["ghost"])
|
||||||
else:
|
else:
|
||||||
a.register_result(false)
|
a.register_result(false)
|
||||||
a.take_damage(3)
|
a.take_damage(base)
|
||||||
if not pr["ghost"]:
|
if not pr["ghost"]:
|
||||||
b.register_result(false)
|
b.register_result(false)
|
||||||
b.take_damage(3)
|
b.take_damage(base)
|
||||||
last_round_log.append("%s vs %s: unentschieden" % [a.pname, b.pname])
|
last_round_log.append("%s vs %s: unentschieden (-%d)" % [a.pname, b.pname, base])
|
||||||
pending.clear()
|
pending.clear()
|
||||||
player_combat = null
|
player_combat = null
|
||||||
_check_eliminations()
|
_check_eliminations()
|
||||||
@@ -176,13 +208,26 @@ func _next_round() -> void:
|
|||||||
if not p.alive:
|
if not p.alive:
|
||||||
continue
|
continue
|
||||||
p.gold += p.round_income()
|
p.gold += p.round_income()
|
||||||
p.roll_shop(pool, rand)
|
p.gain_xp(Data.XP_PER_ROUND)
|
||||||
|
p.new_round_shop(pool, rand)
|
||||||
|
pairings = _make_pairings()
|
||||||
_run_bot_turns()
|
_run_bot_turns()
|
||||||
|
plan_left = Data.PLAN_SECONDS
|
||||||
phase = Phase.PLANNING
|
phase = Phase.PLANNING
|
||||||
|
|
||||||
|
|
||||||
# --- Hilfen für die UI ----------------------------------------------------
|
# --- Hilfen für die UI ----------------------------------------------------
|
||||||
|
|
||||||
|
## Gegner eines Spielers in der aktuellen Paarung (Abbilder liefern ihre Quelle).
|
||||||
|
func opponent_of(p: PlayerState) -> PlayerState:
|
||||||
|
for pr in pairings:
|
||||||
|
if pr["a"] == p:
|
||||||
|
return pr["ghost_src"] if pr["ghost"] else pr["b"]
|
||||||
|
if pr["b"] == p:
|
||||||
|
return pr["a"]
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
func human_opponent() -> PlayerState:
|
func human_opponent() -> PlayerState:
|
||||||
for pr in pairings:
|
for pr in pairings:
|
||||||
if pr["a"] == human:
|
if pr["a"] == human:
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ var streak: int = 0 ## >0 Siegserie, <0 Niederlagenserie
|
|||||||
var alive: bool = true
|
var alive: bool = true
|
||||||
var last_won: bool = false
|
var last_won: bool = false
|
||||||
var placement: int = 0 ## Endplatzierung, 0 = noch im Spiel
|
var placement: int = 0 ## Endplatzierung, 0 = noch im Spiel
|
||||||
|
var level: int = Data.START_LEVEL
|
||||||
|
var xp: int = 0
|
||||||
|
var shop_locked: bool = false ## eingefrorener Shop bleibt über die Runde
|
||||||
|
var last_element: int = -1 ## zuletzt gewählte Spezialisierung für Schnellkauf
|
||||||
|
|
||||||
var bench: Array = [] ## Avatar oder null
|
var bench: Array = [] ## Avatar oder null
|
||||||
var board: Dictionary = {} ## Vector2i -> Avatar
|
var board: Dictionary = {} ## Vector2i -> Avatar
|
||||||
@@ -72,10 +76,18 @@ func reroll(pool: Pool, rng: RandomNumberGenerator) -> bool:
|
|||||||
if gold < Data.REROLL_COST:
|
if gold < Data.REROLL_COST:
|
||||||
return false
|
return false
|
||||||
gold -= Data.REROLL_COST
|
gold -= Data.REROLL_COST
|
||||||
|
shop_locked = false
|
||||||
roll_shop(pool, rng)
|
roll_shop(pool, rng)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Neue Runde: der Shop wird nur neu gefüllt, wenn er nicht eingefroren ist.
|
||||||
|
func new_round_shop(pool: Pool, rng: RandomNumberGenerator) -> void:
|
||||||
|
if shop_locked:
|
||||||
|
return
|
||||||
|
roll_shop(pool, rng)
|
||||||
|
|
||||||
|
|
||||||
func free_bench_slot() -> int:
|
func free_bench_slot() -> int:
|
||||||
for i in bench.size():
|
for i in bench.size():
|
||||||
if bench[i] == null:
|
if bench[i] == null:
|
||||||
@@ -101,6 +113,8 @@ func buy(index: int, pool: Pool, chosen_element: int = -1) -> bool:
|
|||||||
gold -= entry.price()
|
gold -= entry.price()
|
||||||
bench[slot] = entry.to_avatar(element)
|
bench[slot] = entry.to_avatar(element)
|
||||||
entry.sold = true
|
entry.sold = true
|
||||||
|
if entry.element < 0:
|
||||||
|
last_element = element
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
@@ -126,21 +140,42 @@ func sell_board(hex: Vector2i, pool: Pool) -> bool:
|
|||||||
|
|
||||||
# --- Aufstellen ------------------------------------------------------------
|
# --- Aufstellen ------------------------------------------------------------
|
||||||
|
|
||||||
func team_slots(round_no: int) -> int:
|
func team_slots() -> int:
|
||||||
return Data.team_slots(round_no)
|
return level
|
||||||
|
|
||||||
|
|
||||||
func can_place(round_no: int) -> bool:
|
func can_place() -> bool:
|
||||||
return board.size() < team_slots(round_no)
|
return board.size() < level
|
||||||
|
|
||||||
|
|
||||||
|
func gain_xp(amount: int) -> void:
|
||||||
|
xp += amount
|
||||||
|
while level < Data.MAX_LEVEL and xp >= Data.xp_needed(level):
|
||||||
|
xp -= Data.xp_needed(level)
|
||||||
|
level += 1
|
||||||
|
if level >= Data.MAX_LEVEL:
|
||||||
|
xp = 0
|
||||||
|
|
||||||
|
|
||||||
|
func can_buy_xp() -> bool:
|
||||||
|
return level < Data.MAX_LEVEL and gold >= Data.XP_BUY_COST
|
||||||
|
|
||||||
|
|
||||||
|
func buy_xp() -> bool:
|
||||||
|
if not can_buy_xp():
|
||||||
|
return false
|
||||||
|
gold -= Data.XP_BUY_COST
|
||||||
|
gain_xp(Data.XP_BUY_AMOUNT)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
## Setzt einen Avatar von der Bank aufs Board.
|
## Setzt einen Avatar von der Bank aufs Board.
|
||||||
func place(bench_index: int, hex: Vector2i, round_no: int) -> bool:
|
func place(bench_index: int, hex: Vector2i) -> bool:
|
||||||
if bench_index < 0 or bench_index >= bench.size() or bench[bench_index] == null:
|
if bench_index < 0 or bench_index >= bench.size() or bench[bench_index] == null:
|
||||||
return false
|
return false
|
||||||
if board.has(hex) or not _valid_hex(hex):
|
if board.has(hex) or not _valid_hex(hex):
|
||||||
return false
|
return false
|
||||||
if not can_place(round_no):
|
if not can_place():
|
||||||
return false
|
return false
|
||||||
var a: Avatar = bench[bench_index]
|
var a: Avatar = bench[bench_index]
|
||||||
board[hex] = a
|
board[hex] = a
|
||||||
@@ -175,7 +210,7 @@ func move_on_board(from_hex: Vector2i, to_hex: Vector2i) -> bool:
|
|||||||
## Verschiebt einen Avatar zwischen Bank und Board, in beide Richtungen.
|
## Verschiebt einen Avatar zwischen Bank und Board, in beide Richtungen.
|
||||||
## Ist das Ziel belegt, tauschen die beiden Avatare die Plätze.
|
## Ist das Ziel belegt, tauschen die beiden Avatare die Plätze.
|
||||||
## src und dst sind {"kind": "board"/"bench", "hex": Vector2i, "index": int}.
|
## src und dst sind {"kind": "board"/"bench", "hex": Vector2i, "index": int}.
|
||||||
func move_slot(src: Dictionary, dst: Dictionary, round_no: int) -> bool:
|
func move_slot(src: Dictionary, dst: Dictionary) -> bool:
|
||||||
if src.is_empty() or dst.is_empty() or src == dst:
|
if src.is_empty() or dst.is_empty() or src == dst:
|
||||||
return false
|
return false
|
||||||
var sk: String = src["kind"]
|
var sk: String = src["kind"]
|
||||||
@@ -212,7 +247,7 @@ func move_slot(src: Dictionary, dst: Dictionary, round_no: int) -> bool:
|
|||||||
var hex3: Vector2i = dst["hex"]
|
var hex3: Vector2i = dst["hex"]
|
||||||
if bench[i3] == null or not _valid_hex(hex3):
|
if bench[i3] == null or not _valid_hex(hex3):
|
||||||
return false
|
return false
|
||||||
if not board.has(hex3) and not can_place(round_no):
|
if not board.has(hex3) and not can_place():
|
||||||
return false
|
return false
|
||||||
var unit3: Avatar = bench[i3]
|
var unit3: Avatar = bench[i3]
|
||||||
if board.has(hex3):
|
if board.has(hex3):
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ const BENCH_OFFSET := 1.95 ## Abstand der Bank hinter der letzten Reihe
|
|||||||
|
|
||||||
var game: Game = null
|
var game: Game = null
|
||||||
var camera: Camera3D = null
|
var camera: Camera3D = null
|
||||||
|
## Brett eines anderen Spielers, das in der Planung oben gezeigt wird.
|
||||||
|
var scout_player: PlayerState = null
|
||||||
var selected_slot := {} ## {"kind": "board"/"bench", ...}
|
var selected_slot := {} ## {"kind": "board"/"bench", ...}
|
||||||
var hover_slot := {}
|
var hover_slot := {}
|
||||||
var drag_active := false
|
var drag_active := false
|
||||||
@@ -31,6 +33,12 @@ var _bench_tiles := {} ## int -> MeshInstance3D
|
|||||||
var _bench_mats := {}
|
var _bench_mats := {}
|
||||||
var _bench_z := 0.0
|
var _bench_z := 0.0
|
||||||
var _units := {} ## key -> Node3D
|
var _units := {} ## key -> Node3D
|
||||||
|
var _dying := {} ## key -> Restzeit der Sterbe-Animation
|
||||||
|
const DEATH_TIME := 0.45
|
||||||
|
var _shots: Array = [] ## fliegende Geschosse {"node", "from", "to", "t", "dur"}
|
||||||
|
var shots_total := 0 ## Zähler aller Geschosse, für Tests
|
||||||
|
var _shot_mesh: SphereMesh
|
||||||
|
var _slash_mesh: BoxMesh
|
||||||
var _origin := Vector3.ZERO
|
var _origin := Vector3.ZERO
|
||||||
var _tile_mesh: CylinderMesh
|
var _tile_mesh: CylinderMesh
|
||||||
var _ring_mesh: TorusMesh
|
var _ring_mesh: TorusMesh
|
||||||
@@ -48,6 +56,13 @@ func _ready() -> void:
|
|||||||
_ring_mesh.outer_radius = 0.66
|
_ring_mesh.outer_radius = 0.66
|
||||||
_ring_mesh.ring_segments = 8
|
_ring_mesh.ring_segments = 8
|
||||||
_ring_mesh.rings = 24
|
_ring_mesh.rings = 24
|
||||||
|
_shot_mesh = SphereMesh.new()
|
||||||
|
_shot_mesh.radius = 0.11
|
||||||
|
_shot_mesh.height = 0.22
|
||||||
|
_shot_mesh.radial_segments = 8
|
||||||
|
_shot_mesh.rings = 4
|
||||||
|
_slash_mesh = BoxMesh.new()
|
||||||
|
_slash_mesh.size = Vector3(0.12, 0.12, 0.7)
|
||||||
_build_tiles()
|
_build_tiles()
|
||||||
_build_bench()
|
_build_bench()
|
||||||
set_process(true)
|
set_process(true)
|
||||||
@@ -212,6 +227,46 @@ func _process(delta: float) -> void:
|
|||||||
return
|
return
|
||||||
_update_tiles()
|
_update_tiles()
|
||||||
_update_units(delta)
|
_update_units(delta)
|
||||||
|
_update_shots(delta)
|
||||||
|
|
||||||
|
|
||||||
|
## Sichtbarer Angriff: Fernkampf schickt eine Kugel, Nahkampf einen kurzen Hieb.
|
||||||
|
func spawn_attack(from: Vector3, to: Vector3, ranged: bool, col: Color) -> void:
|
||||||
|
var mi := MeshInstance3D.new()
|
||||||
|
mi.mesh = _shot_mesh if ranged else _slash_mesh
|
||||||
|
var m := StandardMaterial3D.new()
|
||||||
|
m.albedo_color = col
|
||||||
|
m.emission_enabled = true
|
||||||
|
m.emission = col
|
||||||
|
m.emission_energy_multiplier = 1.8
|
||||||
|
mi.material_override = m
|
||||||
|
var a := from + Vector3(0.0, 0.7, 0.0)
|
||||||
|
var b := to + Vector3(0.0, 0.6, 0.0)
|
||||||
|
if not ranged:
|
||||||
|
# Der Hieb startet vor dem Angreifer und endet am Ziel.
|
||||||
|
a = a.lerp(b, 0.35)
|
||||||
|
mi.position = a
|
||||||
|
if (b - a).length() > 0.01:
|
||||||
|
mi.look_at_from_position(a, b, Vector3.UP)
|
||||||
|
add_child(mi)
|
||||||
|
shots_total += 1
|
||||||
|
_shots.append({"node": mi, "from": a, "to": b, "t": 0.0, "dur": 0.2 if ranged else 0.09})
|
||||||
|
|
||||||
|
|
||||||
|
func _update_shots(delta: float) -> void:
|
||||||
|
var keep := []
|
||||||
|
for sh in _shots:
|
||||||
|
sh["t"] += delta
|
||||||
|
var f: float = sh["t"] / sh["dur"]
|
||||||
|
var node: MeshInstance3D = sh["node"]
|
||||||
|
if f >= 1.0:
|
||||||
|
node.queue_free()
|
||||||
|
continue
|
||||||
|
var pos: Vector3 = sh["from"].lerp(sh["to"], f)
|
||||||
|
pos.y += sin(f * PI) * 0.35
|
||||||
|
node.position = pos
|
||||||
|
keep.append(sh)
|
||||||
|
_shots = keep
|
||||||
|
|
||||||
|
|
||||||
func _update_tiles() -> void:
|
func _update_tiles() -> void:
|
||||||
@@ -284,6 +339,10 @@ func _update_units(delta: float) -> void:
|
|||||||
"side": 0 if u.team == my_team else 1,
|
"side": 0 if u.team == my_team else 1,
|
||||||
"cast": u.cast_flash > 0.0,
|
"cast": u.cast_flash > 0.0,
|
||||||
"hit": u.hit_flash > 0.0,
|
"hit": u.hit_flash > 0.0,
|
||||||
|
"burn": u.burn_timer > 0.0,
|
||||||
|
"slow": u.slow_timer > 0.0 and u.slow > 0.0,
|
||||||
|
"stun": u.stun_timer > 0.0,
|
||||||
|
"shield": clampf(u.shield / maxf(1.0, u.max_hp), 0.0, 1.0),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
for bh in game.human.board:
|
for bh in game.human.board:
|
||||||
@@ -292,6 +351,13 @@ func _update_units(delta: float) -> void:
|
|||||||
"avatar": a, "world": world_pos(from_board(bh)), "hp": 1.0, "mana": 0.0,
|
"avatar": a, "world": world_pos(from_board(bh)), "hp": 1.0, "mana": 0.0,
|
||||||
"side": 0, "cast": false, "hit": false,
|
"side": 0, "cast": false, "hit": false,
|
||||||
}
|
}
|
||||||
|
if scout_player != null and scout_player.alive:
|
||||||
|
for bh in scout_player.board:
|
||||||
|
var sa: Avatar = scout_player.board[bh]
|
||||||
|
wanted["s%d" % sa.id] = {
|
||||||
|
"avatar": sa, "world": world_pos(Combat._arena_pos(bh, 1)), "hp": 1.0,
|
||||||
|
"mana": 0.0, "side": 1, "cast": false, "hit": false,
|
||||||
|
}
|
||||||
# Die Bank ist immer sichtbar, auch während des Kampfes.
|
# Die Bank ist immer sichtbar, auch während des Kampfes.
|
||||||
for i in game.human.bench.size():
|
for i in game.human.bench.size():
|
||||||
var ba = game.human.bench[i]
|
var ba = game.human.bench[i]
|
||||||
@@ -310,9 +376,25 @@ func _update_units(delta: float) -> void:
|
|||||||
wanted[drag_key]["dragged"] = true
|
wanted[drag_key]["dragged"] = true
|
||||||
|
|
||||||
for key in _units.keys():
|
for key in _units.keys():
|
||||||
if not wanted.has(key):
|
if wanted.has(key):
|
||||||
_units[key].queue_free()
|
continue
|
||||||
|
var node: Node3D = _units[key]
|
||||||
|
if not _dying.has(key):
|
||||||
|
# Im Kampf schrumpft die Einheit weg, in der Planung verschwindet sie sofort.
|
||||||
|
if game.phase == Game.Phase.COMBAT and key.begins_with("c"):
|
||||||
|
_dying[key] = DEATH_TIME
|
||||||
|
else:
|
||||||
|
node.queue_free()
|
||||||
|
_units.erase(key)
|
||||||
|
continue
|
||||||
|
_dying[key] -= delta
|
||||||
|
var t: float = maxf(0.0, _dying[key] / DEATH_TIME)
|
||||||
|
node.scale = Vector3(t, t * 0.6, t)
|
||||||
|
node.position.y = -0.4 * (1.0 - t)
|
||||||
|
if _dying[key] <= 0.0:
|
||||||
|
node.queue_free()
|
||||||
_units.erase(key)
|
_units.erase(key)
|
||||||
|
_dying.erase(key)
|
||||||
|
|
||||||
for key in wanted:
|
for key in wanted:
|
||||||
var d: Dictionary = wanted[key]
|
var d: Dictionary = wanted[key]
|
||||||
@@ -332,10 +414,31 @@ func _update_units(delta: float) -> void:
|
|||||||
var body: MeshInstance3D = node.get_meta("body")
|
var body: MeshInstance3D = node.get_meta("body")
|
||||||
var mat: StandardMaterial3D = body.material_override
|
var mat: StandardMaterial3D = body.material_override
|
||||||
var base: Color = d["avatar"].color()
|
var base: Color = d["avatar"].color()
|
||||||
mat.albedo_color = base.lightened(0.5) if d["hit"] else base
|
var col := base
|
||||||
|
if d.get("slow", false):
|
||||||
|
col = col.lerp(Color(0.55, 0.8, 1.0), 0.45)
|
||||||
|
if d.get("stun", false):
|
||||||
|
col = col.lerp(Color(1.0, 0.95, 0.4), 0.5)
|
||||||
|
if d["hit"]:
|
||||||
|
col = col.lightened(0.5)
|
||||||
|
mat.albedo_color = col
|
||||||
var dragged: bool = d.get("dragged", false)
|
var dragged: bool = d.get("dragged", false)
|
||||||
mat.emission_energy_multiplier = 1.6 if d["cast"] else (0.45 if dragged else 0.12)
|
var emit := 0.12
|
||||||
node.scale = Vector3.ONE * (1.12 if (d["cast"] or dragged) else 1.0)
|
if d["cast"]:
|
||||||
|
emit = 1.6
|
||||||
|
elif d.get("burn", false):
|
||||||
|
emit = 0.9
|
||||||
|
mat.emission = Color(1.0, 0.45, 0.15)
|
||||||
|
elif dragged:
|
||||||
|
emit = 0.45
|
||||||
|
else:
|
||||||
|
mat.emission = base
|
||||||
|
mat.emission_energy_multiplier = emit
|
||||||
|
var sc := 1.12 if (d["cast"] or dragged) else 1.0
|
||||||
|
if d.get("stun", false):
|
||||||
|
node.scale = Vector3(sc * 1.08, sc * 0.8, sc * 1.08)
|
||||||
|
else:
|
||||||
|
node.scale = Vector3.ONE * sc
|
||||||
draw_units.append({
|
draw_units.append({
|
||||||
"pos": node.position, "avatar": d["avatar"], "hp": d["hp"],
|
"pos": node.position, "avatar": d["avatar"], "hp": d["hp"],
|
||||||
"mana": d["mana"], "side": d["side"], "cast": d["cast"],
|
"mana": d["mana"], "side": d["side"], "cast": d["cast"],
|
||||||
@@ -347,12 +450,77 @@ func unit_node(avatar_id: int) -> Node3D:
|
|||||||
return _units.get("a%d" % avatar_id)
|
return _units.get("a%d" % avatar_id)
|
||||||
|
|
||||||
|
|
||||||
|
## Für das Overlay: Arena-Feld so spiegeln, wie das Brett gezeichnet wird.
|
||||||
|
func mirror_for_view(p: Vector2i, my_team: int) -> Vector2i:
|
||||||
|
return _mirror(p, my_team)
|
||||||
|
|
||||||
|
|
||||||
func _mirror(p: Vector2i, my_team: int) -> Vector2i:
|
func _mirror(p: Vector2i, my_team: int) -> Vector2i:
|
||||||
if my_team == 0:
|
if my_team == 0:
|
||||||
return p
|
return p
|
||||||
return Vector2i(COLS - 1 - p.x, ROWS - 1 - p.y)
|
return Vector2i(COLS - 1 - p.x, ROWS - 1 - p.y)
|
||||||
|
|
||||||
|
|
||||||
|
## Kleines Kennzeichen des Hauptelements auf oder neben dem Körper.
|
||||||
|
func _add_ornament(root: Node3D, a: Avatar, body_y: float) -> void:
|
||||||
|
var e := a.primary_element()
|
||||||
|
if e < 0:
|
||||||
|
return
|
||||||
|
var col: Color = Data.ELEMENT_COLORS[e]
|
||||||
|
var m := StandardMaterial3D.new()
|
||||||
|
m.albedo_color = col.lightened(0.15)
|
||||||
|
m.emission_enabled = true
|
||||||
|
m.emission = col
|
||||||
|
m.emission_energy_multiplier = 0.5
|
||||||
|
var top := body_y + 0.5
|
||||||
|
match e:
|
||||||
|
Data.Element.FEUER:
|
||||||
|
# Flamme: kleiner Kegel oben.
|
||||||
|
var c := CylinderMesh.new()
|
||||||
|
c.top_radius = 0.0
|
||||||
|
c.bottom_radius = 0.16
|
||||||
|
c.height = 0.38
|
||||||
|
c.radial_segments = 8
|
||||||
|
_orn(root, c, m, Vector3(0.0, top + 0.18, 0.0))
|
||||||
|
Data.Element.WASSER:
|
||||||
|
# Tropfen: flache Kugel oben.
|
||||||
|
var sp := SphereMesh.new()
|
||||||
|
sp.radius = 0.16
|
||||||
|
sp.height = 0.22
|
||||||
|
_orn(root, sp, m, Vector3(0.0, top + 0.12, 0.0))
|
||||||
|
Data.Element.WIND:
|
||||||
|
# Finne: dünne, hohe Platte.
|
||||||
|
var b := BoxMesh.new()
|
||||||
|
b.size = Vector3(0.05, 0.42, 0.28)
|
||||||
|
_orn(root, b, m, Vector3(0.0, top + 0.2, 0.0))
|
||||||
|
Data.Element.BLITZ:
|
||||||
|
# Zacke: Prisma oben.
|
||||||
|
var pr := PrismMesh.new()
|
||||||
|
pr.size = Vector3(0.28, 0.4, 0.12)
|
||||||
|
_orn(root, pr, m, Vector3(0.0, top + 0.2, 0.0))
|
||||||
|
Data.Element.ERDE:
|
||||||
|
# Schultern: zwei Würfel seitlich.
|
||||||
|
for sx in [-1.0, 1.0]:
|
||||||
|
var bx := BoxMesh.new()
|
||||||
|
bx.size = Vector3(0.22, 0.22, 0.22)
|
||||||
|
_orn(root, bx, m, Vector3(sx * 0.5, body_y + 0.1, 0.0))
|
||||||
|
Data.Element.EIS:
|
||||||
|
# Kristall: umgedrehtes Prisma oben.
|
||||||
|
var cr := PrismMesh.new()
|
||||||
|
cr.size = Vector3(0.22, 0.42, 0.22)
|
||||||
|
var n := _orn(root, cr, m, Vector3(0.0, top + 0.2, 0.0))
|
||||||
|
n.rotation_degrees = Vector3(180.0, 45.0, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
func _orn(root: Node3D, mesh: Mesh, mat: Material, pos: Vector3) -> MeshInstance3D:
|
||||||
|
var mi := MeshInstance3D.new()
|
||||||
|
mi.mesh = mesh
|
||||||
|
mi.material_override = mat
|
||||||
|
mi.position = pos
|
||||||
|
root.add_child(mi)
|
||||||
|
return mi
|
||||||
|
|
||||||
|
|
||||||
func _make_unit(a: Avatar, side: int) -> Node3D:
|
func _make_unit(a: Avatar, side: int) -> Node3D:
|
||||||
var root := Node3D.new()
|
var root := Node3D.new()
|
||||||
|
|
||||||
@@ -390,6 +558,8 @@ func _make_unit(a: Avatar, side: int) -> Node3D:
|
|||||||
root.add_child(body)
|
root.add_child(body)
|
||||||
root.set_meta("body", body)
|
root.set_meta("body", body)
|
||||||
|
|
||||||
|
_add_ornament(root, a, body.position.y)
|
||||||
|
|
||||||
# Bodenring: weiss = eigenes Team, rot = Gegner.
|
# Bodenring: weiss = eigenes Team, rot = Gegner.
|
||||||
var ring := MeshInstance3D.new()
|
var ring := MeshInstance3D.new()
|
||||||
ring.mesh = _ring_mesh
|
ring.mesh = _ring_mesh
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
class_name HudOverlay
|
class_name HudOverlay
|
||||||
extends Control
|
extends Control
|
||||||
|
|
||||||
## Zeichnet Lebens- und Manabalken, Rolle und Sterne über die 3D-Einheiten.
|
## Zeichnet Lebens- und Manabalken, Rolle, Sterne und Schadenszahlen
|
||||||
|
## über die 3D-Einheiten. Spielt außerdem die Kampfgeräusche ab.
|
||||||
|
|
||||||
|
const FLOAT_TIME := 0.9
|
||||||
|
const FLOAT_RISE := 42.0
|
||||||
|
|
||||||
var board: BoardView3D = null
|
var board: BoardView3D = null
|
||||||
|
var sfx: Sfx = null
|
||||||
var _font: Font
|
var _font: Font
|
||||||
|
var _floaters: Array = [] ## {"world": Vector3, "text": String, "col": Color, "age": float}
|
||||||
|
var _combat_seen: Combat = null
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -13,10 +20,52 @@ func _ready() -> void:
|
|||||||
set_process(true)
|
set_process(true)
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
|
_pull_hits()
|
||||||
|
for f in _floaters:
|
||||||
|
f["age"] += delta
|
||||||
|
_floaters = _floaters.filter(func(f): return f["age"] < FLOAT_TIME)
|
||||||
queue_redraw()
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
## Holt neue Treffer aus dem laufenden Spielerkampf und macht Zahlen und Töne daraus.
|
||||||
|
func _pull_hits() -> void:
|
||||||
|
if board == null or board.game == null:
|
||||||
|
return
|
||||||
|
var c: Combat = board.game.player_combat
|
||||||
|
if c == null or not c.record_hits:
|
||||||
|
_combat_seen = null
|
||||||
|
return
|
||||||
|
if c != _combat_seen:
|
||||||
|
_combat_seen = c
|
||||||
|
_floaters.clear()
|
||||||
|
if c.hits.is_empty():
|
||||||
|
return
|
||||||
|
var my_team := board.game.human_team_in_combat()
|
||||||
|
for h in c.hits:
|
||||||
|
var world := board.world_pos(board.mirror_for_view(h["pos"], my_team))
|
||||||
|
var mine: bool = h["team"] == my_team
|
||||||
|
match h["kind"]:
|
||||||
|
"dmg":
|
||||||
|
_floaters.append({"world": world, "text": str(h["amount"]),
|
||||||
|
"col": Color(1.0, 0.55, 0.45) if mine else Color(1.0, 0.95, 0.85), "age": 0.0})
|
||||||
|
if sfx: sfx.play("treffer")
|
||||||
|
"crit":
|
||||||
|
_floaters.append({"world": world, "text": str(h["amount"]) + "!",
|
||||||
|
"col": Color(1.0, 0.85, 0.3), "age": 0.0})
|
||||||
|
if sfx: sfx.play("krit")
|
||||||
|
"heal":
|
||||||
|
_floaters.append({"world": world, "text": "+" + str(h["amount"]),
|
||||||
|
"col": Color(0.55, 1.0, 0.6), "age": 0.0})
|
||||||
|
if sfx: sfx.play("heilung")
|
||||||
|
"cast":
|
||||||
|
if sfx: sfx.play("zauber")
|
||||||
|
"attack":
|
||||||
|
var from := board.world_pos(board.mirror_for_view(h["from"], my_team))
|
||||||
|
board.spawn_attack(from, world, h["ranged"], Data.ELEMENT_COLORS[h["element"]])
|
||||||
|
c.hits.clear()
|
||||||
|
|
||||||
|
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
if board == null or board.camera == null:
|
if board == null or board.camera == null:
|
||||||
return
|
return
|
||||||
@@ -30,7 +79,6 @@ func _draw() -> void:
|
|||||||
var a: Avatar = d["avatar"]
|
var a: Avatar = d["avatar"]
|
||||||
var side: int = d["side"]
|
var side: int = d["side"]
|
||||||
|
|
||||||
# Balkenbreite nach Entfernung, damit nahe Einheiten größer wirken.
|
|
||||||
var bw: float = clampf(absf(foot.y - head.y) * 0.85, 26.0, 70.0)
|
var bw: float = clampf(absf(foot.y - head.y) * 0.85, 26.0, 70.0)
|
||||||
var bx := head.x - bw * 0.5
|
var bx := head.x - bw * 0.5
|
||||||
var by := head.y - 4.0
|
var by := head.y - 4.0
|
||||||
@@ -41,13 +89,34 @@ func _draw() -> void:
|
|||||||
draw_rect(Rect2(bx, by + 7.0, bw, 4.0), Color(0.05, 0.05, 0.07, 0.85))
|
draw_rect(Rect2(bx, by + 7.0, bw, 4.0), Color(0.05, 0.05, 0.07, 0.85))
|
||||||
draw_rect(Rect2(bx, by + 7.0, bw * clampf(d["mana"], 0.0, 1.0), 4.0),
|
draw_rect(Rect2(bx, by + 7.0, bw * clampf(d["mana"], 0.0, 1.0), 4.0),
|
||||||
Color(0.35, 0.62, 0.95))
|
Color(0.35, 0.62, 0.95))
|
||||||
|
# Schild als heller Streifen über dem Lebensbalken.
|
||||||
|
if d.get("shield", 0.0) > 0.0:
|
||||||
|
draw_rect(Rect2(bx, by - 4.0, bw * clampf(d["shield"], 0.0, 1.0), 3.0), Color(0.85, 0.9, 1.0))
|
||||||
|
|
||||||
# Sterne
|
|
||||||
for s in a.star:
|
for s in a.star:
|
||||||
draw_circle(Vector2(bx + bw * 0.5 - 7.0 + 7.0 * float(s), by - 7.0), 2.6,
|
draw_circle(Vector2(bx + bw * 0.5 - 7.0 + 7.0 * float(s), by - 9.0), 2.6,
|
||||||
Color(1.0, 0.85, 0.25))
|
Color(1.0, 0.85, 0.25))
|
||||||
# Rolle
|
|
||||||
draw_string(_font, Vector2(bx + bw + 4.0, by + 7.0), Data.ROLE_SHORT[a.role],
|
draw_string(_font, Vector2(bx + bw + 4.0, by + 7.0), Data.ROLE_SHORT[a.role],
|
||||||
HORIZONTAL_ALIGNMENT_LEFT, -1, 13, Color(0.9, 0.9, 0.95, 0.9))
|
HORIZONTAL_ALIGNMENT_LEFT, -1, 13, Color(0.9, 0.9, 0.95, 0.9))
|
||||||
if d["cast"]:
|
if d["cast"]:
|
||||||
draw_arc(foot, bw * 0.55, 0.0, TAU, 24, Color(1.0, 0.95, 0.5, 0.9), 2.5)
|
draw_arc(foot, bw * 0.55, 0.0, TAU, 24, Color(1.0, 0.95, 0.5, 0.9), 2.5)
|
||||||
|
# Zustände als kleine Marken neben dem Balken.
|
||||||
|
var marks := []
|
||||||
|
if d.get("burn", false): marks.append(["B", Color(1.0, 0.5, 0.2)])
|
||||||
|
if d.get("slow", false): marks.append(["S", Color(0.6, 0.85, 1.0)])
|
||||||
|
if d.get("stun", false): marks.append(["Z", Color(1.0, 0.9, 0.3)])
|
||||||
|
for i in marks.size():
|
||||||
|
draw_string(_font, Vector2(bx - 12.0 - 11.0 * float(i), by + 7.0), marks[i][0],
|
||||||
|
HORIZONTAL_ALIGNMENT_LEFT, -1, 12, marks[i][1])
|
||||||
|
|
||||||
|
# Schwebende Zahlen
|
||||||
|
for f in _floaters:
|
||||||
|
var t: float = f["age"] / FLOAT_TIME
|
||||||
|
var pos := cam.unproject_position(f["world"] + Vector3(0.0, 1.5, 0.0))
|
||||||
|
pos.y -= t * FLOAT_RISE
|
||||||
|
var col: Color = f["col"]
|
||||||
|
col.a = 1.0 - t * t
|
||||||
|
var size := 15 if f["text"].ends_with("!") else 13
|
||||||
|
draw_string(_font, pos + Vector2(1, 1), f["text"], HORIZONTAL_ALIGNMENT_CENTER, -1, size,
|
||||||
|
Color(0, 0, 0, col.a * 0.8))
|
||||||
|
draw_string(_font, pos, f["text"], HORIZONTAL_ALIGNMENT_CENTER, -1, size, col)
|
||||||
|
|||||||
171
scripts/ui/InfoColumn.gd
Normal file
171
scripts/ui/InfoColumn.gd
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
class_name InfoColumn
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
## Linke Spalte: Elemente im Team, Kombinationen, gewählte Einheit, Rückblick.
|
||||||
|
|
||||||
|
signal upgrade_pressed
|
||||||
|
|
||||||
|
const PANEL_BG := Color(0.14, 0.15, 0.19)
|
||||||
|
|
||||||
|
var traits_box: VBoxContainer
|
||||||
|
var info_box: VBoxContainer
|
||||||
|
var log_box: VBoxContainer
|
||||||
|
var _text_w := 240.0
|
||||||
|
|
||||||
|
|
||||||
|
func build(x: float, y: float, w: float, h: float) -> void:
|
||||||
|
position = Vector2(x, y)
|
||||||
|
size = Vector2(w, h)
|
||||||
|
_text_w = w - 46.0
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
var p := UiKit.panel(PANEL_BG)
|
||||||
|
p.size = size
|
||||||
|
p.clip_contents = true
|
||||||
|
add_child(p)
|
||||||
|
var sc := ScrollContainer.new()
|
||||||
|
sc.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
p.add_child(sc)
|
||||||
|
var col := VBoxContainer.new()
|
||||||
|
col.add_theme_constant_override("separation", 10)
|
||||||
|
col.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
sc.add_child(col)
|
||||||
|
traits_box = VBoxContainer.new()
|
||||||
|
info_box = VBoxContainer.new()
|
||||||
|
log_box = VBoxContainer.new()
|
||||||
|
for b in [traits_box, info_box, log_box]:
|
||||||
|
b.add_theme_constant_override("separation", 3)
|
||||||
|
b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
col.add_child(b)
|
||||||
|
col.add_child(HSeparator.new())
|
||||||
|
|
||||||
|
|
||||||
|
func refresh(game: Game, avatar: Avatar, hex: Vector2i) -> void:
|
||||||
|
_refresh_traits(game)
|
||||||
|
_refresh_info(game, avatar, hex)
|
||||||
|
_refresh_log(game)
|
||||||
|
|
||||||
|
|
||||||
|
func _wrapped(text: String, size: int, col: Color) -> Label:
|
||||||
|
var l := UiKit.label(text, size, col)
|
||||||
|
l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
l.custom_minimum_size = Vector2(_text_w, 0)
|
||||||
|
l.size_flags_horizontal = Control.SIZE_FILL
|
||||||
|
return l
|
||||||
|
|
||||||
|
|
||||||
|
func _clear(box: Node) -> void:
|
||||||
|
for c in box.get_children():
|
||||||
|
box.remove_child(c)
|
||||||
|
c.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_traits(game: Game) -> void:
|
||||||
|
_clear(traits_box)
|
||||||
|
traits_box.add_child(UiKit.label("Elemente im Team", 15, Color(0.75, 0.8, 0.95)))
|
||||||
|
var counts := game.human.trait_counts()
|
||||||
|
var any := false
|
||||||
|
for e in Data.ELEMENT_COUNT:
|
||||||
|
var c: int = counts.get(e, 0)
|
||||||
|
if c == 0:
|
||||||
|
continue
|
||||||
|
any = true
|
||||||
|
var tier := Data.trait_tier(e, c)
|
||||||
|
var col: Color = Data.ELEMENT_COLORS[e]
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
var dot := ColorRect.new()
|
||||||
|
dot.color = col if tier > 0 else col.darkened(0.55)
|
||||||
|
dot.custom_minimum_size = Vector2(12, 12)
|
||||||
|
row.add_child(dot)
|
||||||
|
var txt := " %s %d" % [Data.ELEMENT_NAMES[e], c]
|
||||||
|
if tier > 0:
|
||||||
|
txt += " - " + Data.ELEMENT_TRAITS[e]["desc"][tier - 1]
|
||||||
|
else:
|
||||||
|
txt += " (ab 2 aktiv)"
|
||||||
|
var el := UiKit.label(txt, 13, Color(0.9, 0.9, 0.95) if tier > 0 else Color(0.55, 0.55, 0.6))
|
||||||
|
el.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
el.custom_minimum_size = Vector2(_text_w - 20.0, 0)
|
||||||
|
row.add_child(el)
|
||||||
|
traits_box.add_child(row)
|
||||||
|
if not any:
|
||||||
|
traits_box.add_child(_wrapped("Noch nichts aufgestellt.", 13, Color(0.55, 0.55, 0.6)))
|
||||||
|
traits_box.add_child(UiKit.label("\nKombinationen", 15, Color(0.75, 0.8, 0.95)))
|
||||||
|
var keys := []
|
||||||
|
for c in game.human.active_combos():
|
||||||
|
keys.append(c["key"])
|
||||||
|
for c in Data.COMBO_TRAITS:
|
||||||
|
var on: bool = keys.has(c["key"])
|
||||||
|
var t := "%s (%s+%s): %s" % [c["name"], Data.ELEMENT_NAMES[c["a"]],
|
||||||
|
Data.ELEMENT_NAMES[c["b"]], c["desc"]]
|
||||||
|
traits_box.add_child(_wrapped(t, 12, Color(0.6, 0.95, 0.7) if on else Color(0.45, 0.45, 0.5)))
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_info(game: Game, a: Avatar, hex: Vector2i) -> void:
|
||||||
|
_clear(info_box)
|
||||||
|
info_box.add_child(UiKit.label("Einheit", 15, Color(0.75, 0.8, 0.95)))
|
||||||
|
if a == null:
|
||||||
|
info_box.add_child(_wrapped("Einheit auf dem Feld oder der Bank anklicken.", 13,
|
||||||
|
Color(0.55, 0.55, 0.6)))
|
||||||
|
return
|
||||||
|
var s := a.base_stats()
|
||||||
|
info_box.add_child(_wrapped("%s %d-Star" % [a.display_name(), a.star], 14, a.color()))
|
||||||
|
info_box.add_child(_wrapped("%s: %s" % [Data.ELEMENT_NAMES[a.primary_element()],
|
||||||
|
Data.PROFILE_HINT[a.primary_element()]], 12, Color(0.75, 0.8, 0.9)))
|
||||||
|
var crit := " Krit %d%%" % int(s["crit"] * 100.0) if s["crit"] > 0.0 else ""
|
||||||
|
info_box.add_child(_wrapped("Leben %d Angriff %d Rüstung %d%%\nTempo %.2f Reichweite %d%s"
|
||||||
|
% [int(s["hp"]), int(s["atk"]), int(s["armor"] * 100.0), s["aspd"], s["range"], crit], 12,
|
||||||
|
Color(0.9, 0.9, 0.95)))
|
||||||
|
info_box.add_child(_wrapped(a.ability_text(), 12, Color(0.8, 0.85, 1.0)))
|
||||||
|
if hex.x < 0:
|
||||||
|
info_box.add_child(_wrapped("Zum Aufwerten aufs Feld ziehen.", 12, Color(0.85, 0.85, 0.9)))
|
||||||
|
return
|
||||||
|
var cost := a.upgrade_cost()
|
||||||
|
if cost.is_empty():
|
||||||
|
info_box.add_child(_wrapped("Maximale Stufe.", 12, Color(1.0, 0.85, 0.35)))
|
||||||
|
return
|
||||||
|
var p := game.human
|
||||||
|
var missing := p.upgrade_missing(hex)
|
||||||
|
var need := []
|
||||||
|
for e in cost:
|
||||||
|
need.append("%dx %s" % [cost[e], Data.ELEMENT_NAMES[e]])
|
||||||
|
info_box.add_child(_wrapped("Aufwerten braucht: " + ", ".join(need), 12, Color(0.85, 0.85, 0.9)))
|
||||||
|
if missing.is_empty():
|
||||||
|
var b := UiKit.button("Aufwerten", Color(0.28, 0.55, 0.36))
|
||||||
|
b.custom_minimum_size = Vector2(140, 26)
|
||||||
|
b.pressed.connect(func(): upgrade_pressed.emit())
|
||||||
|
info_box.add_child(b)
|
||||||
|
else:
|
||||||
|
var miss := []
|
||||||
|
for e in missing:
|
||||||
|
miss.append("%dx %s" % [missing[e], Data.ELEMENT_NAMES[e]])
|
||||||
|
info_box.add_child(_wrapped("Es fehlt: " + ", ".join(miss), 12, Color(0.95, 0.6, 0.5)))
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_log(game: Game) -> void:
|
||||||
|
_clear(log_box)
|
||||||
|
log_box.add_child(UiKit.label("Letzte Runde", 15, Color(0.75, 0.8, 0.95)))
|
||||||
|
var r := game.human_recap
|
||||||
|
if not r.is_empty():
|
||||||
|
var verdict := "unentschieden"
|
||||||
|
var col := Color(0.85, 0.85, 0.9)
|
||||||
|
if r["result"] > 0:
|
||||||
|
verdict = "gewonnen"
|
||||||
|
col = Color(0.6, 0.95, 0.7)
|
||||||
|
elif r["result"] < 0:
|
||||||
|
verdict = "verloren (-%d Leben)" % r["damage"]
|
||||||
|
col = Color(1.0, 0.6, 0.55)
|
||||||
|
log_box.add_child(_wrapped("Gegen %s: %s" % [r["opponent"], verdict], 13, col))
|
||||||
|
if r["total"] == 0:
|
||||||
|
log_box.add_child(_wrapped("Du hattest keine Einheiten aufgestellt.", 12,
|
||||||
|
Color(0.95, 0.75, 0.7)))
|
||||||
|
else:
|
||||||
|
log_box.add_child(_wrapped("%d von %d Einheiten überlebt, %.0f s" %
|
||||||
|
[r["alive"], r["total"], r["duration"]], 12, Color(0.8, 0.8, 0.86)))
|
||||||
|
var best := "Stärkste: %s (%d Schaden" % [r["best_name"], r["best_dealt"]]
|
||||||
|
best += ", %d geheilt)" % r["best_healed"] if r["best_healed"] > 0 else ")"
|
||||||
|
log_box.add_child(_wrapped(best, 12, Color(0.8, 0.8, 0.86)))
|
||||||
|
log_box.add_child(_wrapped("Gefährlichster Gegner: %s (%d Schaden)" %
|
||||||
|
[r["enemy_name"], r["enemy_dealt"]], 12, Color(0.95, 0.75, 0.7)))
|
||||||
|
if game.last_round_log.is_empty() and r.is_empty():
|
||||||
|
log_box.add_child(UiKit.label("-", 12, Color(0.55, 0.55, 0.6)))
|
||||||
|
for line in game.last_round_log:
|
||||||
|
log_box.add_child(_wrapped(line, 11, Color(0.6, 0.6, 0.68)))
|
||||||
1
scripts/ui/InfoColumn.gd.uid
Normal file
1
scripts/ui/InfoColumn.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dr6yh7ynm7li8
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
extends Node3D
|
extends Node3D
|
||||||
|
|
||||||
## Spielszene: 3D-Arena plus Bedienoberfläche.
|
## Spielszene: 3D-Arena, Kamera, Eingabe, Overlays. Die Panels sind eigene
|
||||||
## Alles wird im Code aus primitiven Meshes und Controls gebaut.
|
## Klassen (TopBar, InfoColumn, PlayersPanel, ShopPanel) und melden sich
|
||||||
|
## über Signale.
|
||||||
|
|
||||||
const TOP_H := 46.0
|
const TOP_H := 46.0
|
||||||
const LEFT_W := 290.0 ## links: Elemente, Einheit, Rundenlog
|
const LEFT_W := 290.0 ## links: Elemente, Einheit, Rückblick
|
||||||
const RIGHT_W := 210.0 ## rechts: Spielerliste
|
const RIGHT_W := 210.0 ## rechts: Spielerliste
|
||||||
const BOTTOM_H := 138.0
|
const BOTTOM_H := 152.0
|
||||||
const TEXT_W := 244.0
|
|
||||||
const BG := Color(0.09, 0.10, 0.13)
|
const BG := Color(0.09, 0.10, 0.13)
|
||||||
const PANEL_BG := Color(0.14, 0.15, 0.19)
|
|
||||||
const SLOT_BG := Color(0.20, 0.21, 0.26)
|
|
||||||
const CAM_FOV := 44.0
|
const CAM_FOV := 44.0
|
||||||
const CAM_PITCH := 52.0 ## Grad über der Waagerechten
|
const CAM_PITCH := 52.0 ## Grad über der Waagerechten
|
||||||
|
const ZOOM_MIN := 0.6
|
||||||
|
const ZOOM_MAX := 1.5
|
||||||
|
const YAW_STEP := 15.0
|
||||||
|
|
||||||
var game := Game.new()
|
var game := Game.new()
|
||||||
var board: BoardView3D
|
var board: BoardView3D
|
||||||
@@ -20,33 +21,28 @@ var camera: Camera3D
|
|||||||
var ui_layer: CanvasLayer
|
var ui_layer: CanvasLayer
|
||||||
var ui_root: Control
|
var ui_root: Control
|
||||||
var hud: HudOverlay
|
var hud: HudOverlay
|
||||||
|
var sfx: Sfx
|
||||||
|
var top: TopBar
|
||||||
|
var info: InfoColumn
|
||||||
|
var players: PlayersPanel
|
||||||
|
var shop: ShopPanel
|
||||||
|
|
||||||
var combat_speed := 1.0
|
var combat_speed := 1.0
|
||||||
|
var started := false
|
||||||
|
var paused := false
|
||||||
|
var scout: PlayerState = null ## Spieler, dessen Brett gerade angesehen wird
|
||||||
|
|
||||||
# Kopfzeile
|
# Kamera: Einpassung plus Nutzeranteil
|
||||||
var lbl_round: Label
|
var _cam_center := Vector3.ZERO
|
||||||
var lbl_gold: Label
|
var _cam_dir := Vector3.ZERO
|
||||||
var lbl_hp: Label
|
var _cam_dist := 20.0
|
||||||
var lbl_slots: Label
|
var cam_zoom := 1.0
|
||||||
var lbl_streak: Label
|
var cam_yaw := 0.0
|
||||||
var lbl_phase: Label
|
|
||||||
var btn_ready: Button
|
|
||||||
var btn_speed: Button
|
|
||||||
|
|
||||||
# Panels
|
|
||||||
var players_box: VBoxContainer
|
|
||||||
var traits_box: VBoxContainer
|
|
||||||
var info_box: VBoxContainer
|
|
||||||
var log_box: VBoxContainer
|
|
||||||
var shop_row: HBoxContainer
|
|
||||||
var lbl_fodder: Label
|
|
||||||
var btn_reroll: Button
|
|
||||||
var lbl_hint: Label
|
|
||||||
|
|
||||||
# Auswahl und Ziehen
|
# Auswahl und Ziehen
|
||||||
var sel_slot := {}
|
var sel_slot := {}
|
||||||
var drag_src := {}
|
var drag_src := {}
|
||||||
var drag_avatar: Avatar = null
|
var drag_avatar: Avatar = null
|
||||||
var drag_moved := false
|
|
||||||
|
|
||||||
# Element-Auswahl
|
# Element-Auswahl
|
||||||
var picker: PopupPanel
|
var picker: PopupPanel
|
||||||
@@ -55,7 +51,7 @@ var picker_title: Label
|
|||||||
var picker_sub: Label
|
var picker_sub: Label
|
||||||
var picker_cb: Callable
|
var picker_cb: Callable
|
||||||
|
|
||||||
var overlay: Control
|
var overlay: Control ## Start-, Pause- oder Endbildschirm
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -66,54 +62,93 @@ func _ready() -> void:
|
|||||||
hud.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
hud.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
hud.board = board
|
hud.board = board
|
||||||
ui_layer.add_child(hud)
|
ui_layer.add_child(hud)
|
||||||
|
sfx = Sfx.new()
|
||||||
|
add_child(sfx)
|
||||||
|
hud.sfx = sfx
|
||||||
ui_root = Control.new()
|
ui_root = Control.new()
|
||||||
ui_root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
ui_root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
ui_root.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
ui_root.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
ui_layer.add_child(ui_root)
|
ui_layer.add_child(ui_root)
|
||||||
|
|
||||||
_build_top()
|
_build_panels()
|
||||||
_build_left()
|
|
||||||
_build_right()
|
|
||||||
_build_bottom()
|
|
||||||
_build_picker()
|
_build_picker()
|
||||||
|
|
||||||
game.start()
|
game.start()
|
||||||
board.game = game
|
board.game = game
|
||||||
_refresh()
|
_refresh()
|
||||||
|
_show_start()
|
||||||
set_process(true)
|
set_process(true)
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
if game.phase == Game.Phase.COMBAT:
|
if not started or paused:
|
||||||
|
return
|
||||||
|
if game.phase == Game.Phase.PLANNING:
|
||||||
|
if game.tick_planning(delta):
|
||||||
|
_cancel_drag()
|
||||||
|
sel_slot = {}
|
||||||
|
picker.hide()
|
||||||
|
sfx.play("runde")
|
||||||
|
_refresh()
|
||||||
|
else:
|
||||||
|
top.refresh_timer(game)
|
||||||
|
elif game.phase == Game.Phase.COMBAT:
|
||||||
var before := game.phase
|
var before := game.phase
|
||||||
game.update_combat(delta * combat_speed)
|
game.update_combat(delta * combat_speed)
|
||||||
_refresh_header()
|
top.refresh(game, combat_speed)
|
||||||
if game.phase != before:
|
if game.phase != before:
|
||||||
|
_after_combat()
|
||||||
_refresh()
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func _after_combat() -> void:
|
||||||
|
match game.human_last_result:
|
||||||
|
1: sfx.play("sieg")
|
||||||
|
-1: sfx.play("niederlage")
|
||||||
|
_: sfx.play("runde")
|
||||||
|
|
||||||
|
|
||||||
|
# --- Eingabe -----------------------------------------------------------------
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if not started or paused:
|
||||||
|
if event is InputEventKey and event.pressed and event.keycode == KEY_P and started:
|
||||||
|
_toggle_pause()
|
||||||
|
return
|
||||||
if event is InputEventMouseMotion:
|
if event is InputEventMouseMotion:
|
||||||
|
if event.button_mask & MOUSE_BUTTON_MASK_MIDDLE:
|
||||||
|
cam_yaw += event.relative.x * 0.3
|
||||||
|
_apply_camera()
|
||||||
|
return
|
||||||
_on_mouse_move(event.position)
|
_on_mouse_move(event.position)
|
||||||
return
|
return
|
||||||
if event is InputEventMouseButton:
|
if event is InputEventMouseButton:
|
||||||
if event.button_index == MOUSE_BUTTON_LEFT:
|
match event.button_index:
|
||||||
if event.pressed:
|
MOUSE_BUTTON_LEFT:
|
||||||
_begin_drag(event.position)
|
if event.pressed:
|
||||||
else:
|
_begin_drag(event.position)
|
||||||
_end_drag(event.position)
|
else:
|
||||||
return
|
_end_drag(event.position)
|
||||||
if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
|
MOUSE_BUTTON_RIGHT:
|
||||||
_on_right_click(event.position)
|
if event.pressed:
|
||||||
return
|
_on_right_click(event.position)
|
||||||
|
MOUSE_BUTTON_WHEEL_UP:
|
||||||
|
if event.pressed:
|
||||||
|
_zoom(-0.1)
|
||||||
|
MOUSE_BUTTON_WHEEL_DOWN:
|
||||||
|
if event.pressed:
|
||||||
|
_zoom(0.1)
|
||||||
return
|
return
|
||||||
if not (event is InputEventKey and event.pressed and not event.echo):
|
if not (event is InputEventKey and event.pressed and not event.echo):
|
||||||
return
|
return
|
||||||
match event.keycode:
|
match event.keycode:
|
||||||
KEY_D:
|
KEY_D: _on_reroll()
|
||||||
_on_reroll()
|
KEY_X: _on_buy_xp()
|
||||||
KEY_SPACE, KEY_F:
|
KEY_F: _on_lock()
|
||||||
_on_ready()
|
KEY_SPACE: _on_ready()
|
||||||
|
KEY_P: _toggle_pause()
|
||||||
|
KEY_Q: _rotate(-YAW_STEP)
|
||||||
|
KEY_E: _rotate(YAW_STEP)
|
||||||
KEY_ESCAPE:
|
KEY_ESCAPE:
|
||||||
_cancel_drag()
|
_cancel_drag()
|
||||||
sel_slot = {}
|
sel_slot = {}
|
||||||
@@ -125,10 +160,8 @@ func _on_mouse_move(pos: Vector2) -> void:
|
|||||||
board.hover_slot = slot
|
board.hover_slot = slot
|
||||||
if drag_avatar != null:
|
if drag_avatar != null:
|
||||||
board.drag_world = _drag_point(pos, slot)
|
board.drag_world = _drag_point(pos, slot)
|
||||||
drag_moved = true
|
|
||||||
|
|
||||||
|
|
||||||
## Hebt einen Avatar an, wenn unter dem Zeiger einer liegt.
|
|
||||||
func _begin_drag(pos: Vector2) -> void:
|
func _begin_drag(pos: Vector2) -> void:
|
||||||
if game.phase != Game.Phase.PLANNING:
|
if game.phase != Game.Phase.PLANNING:
|
||||||
return
|
return
|
||||||
@@ -140,11 +173,9 @@ func _begin_drag(pos: Vector2) -> void:
|
|||||||
return
|
return
|
||||||
drag_src = slot
|
drag_src = slot
|
||||||
drag_avatar = a
|
drag_avatar = a
|
||||||
drag_moved = false
|
|
||||||
board.drag_active = true
|
board.drag_active = true
|
||||||
board.drag_avatar_id = a.id
|
board.drag_avatar_id = a.id
|
||||||
board.drag_world = _drag_point(pos, slot)
|
board.drag_world = _drag_point(pos, slot)
|
||||||
# Während des Ziehens zeigt das Info-Panel den Avatar in der Hand.
|
|
||||||
sel_slot = slot
|
sel_slot = slot
|
||||||
_refresh()
|
_refresh()
|
||||||
|
|
||||||
@@ -156,18 +187,19 @@ func _end_drag(pos: Vector2) -> void:
|
|||||||
var src := drag_src
|
var src := drag_src
|
||||||
_cancel_drag()
|
_cancel_drag()
|
||||||
if dst.is_empty() or BoardView3D._same_slot(src, dst):
|
if dst.is_empty() or BoardView3D._same_slot(src, dst):
|
||||||
# Kein Weg zurückgelegt: das war eine Auswahl.
|
|
||||||
sel_slot = src
|
sel_slot = src
|
||||||
_refresh()
|
_refresh()
|
||||||
return
|
return
|
||||||
if dst["kind"] == "board" and not board.is_own_half(BoardView3D.from_board(dst["hex"])):
|
if dst["kind"] == "board" and not board.is_own_half(BoardView3D.from_board(dst["hex"])):
|
||||||
_refresh()
|
_refresh()
|
||||||
return
|
return
|
||||||
if not game.human.move_slot(src, dst, game.round_no):
|
if not game.human.move_slot(src, dst):
|
||||||
if dst["kind"] == "board":
|
if dst["kind"] == "board":
|
||||||
_hint("Kaderplätze voll (%d)." % game.human.team_slots(game.round_no))
|
shop.hint("Kaderplätze voll (%d). XP kaufen für mehr." % game.human.team_slots())
|
||||||
|
sfx.play("fehler")
|
||||||
else:
|
else:
|
||||||
sel_slot = dst
|
sel_slot = dst
|
||||||
|
sfx.play("setzen")
|
||||||
_refresh()
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
@@ -186,9 +218,10 @@ func _on_right_click(pos: Vector2) -> void:
|
|||||||
if slot.is_empty():
|
if slot.is_empty():
|
||||||
return
|
return
|
||||||
if slot["kind"] == "bench":
|
if slot["kind"] == "bench":
|
||||||
game.human.sell_bench(slot["index"], game.pool)
|
if game.human.sell_bench(slot["index"], game.pool):
|
||||||
else:
|
sfx.play("verkauf")
|
||||||
game.human.unplace(slot["hex"])
|
elif game.human.unplace(slot["hex"]):
|
||||||
|
sfx.play("setzen")
|
||||||
sel_slot = {}
|
sel_slot = {}
|
||||||
_refresh()
|
_refresh()
|
||||||
|
|
||||||
@@ -201,82 +234,7 @@ func _avatar_at(slot: Dictionary):
|
|||||||
return game.human.board.get(slot["hex"])
|
return game.human.board.get(slot["hex"])
|
||||||
|
|
||||||
|
|
||||||
# --- Aufbau ----------------------------------------------------------------
|
# --- Welt und Kamera ---------------------------------------------------------
|
||||||
|
|
||||||
func _add_panel(pos: Vector2, sz: Vector2) -> VBoxContainer:
|
|
||||||
var p := UiKit.panel(PANEL_BG)
|
|
||||||
p.position = pos
|
|
||||||
p.size = sz
|
|
||||||
ui_root.add_child(p)
|
|
||||||
var v := VBoxContainer.new()
|
|
||||||
v.add_theme_constant_override("separation", 4)
|
|
||||||
p.add_child(v)
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
func _build_top() -> void:
|
|
||||||
var bar := ColorRect.new()
|
|
||||||
bar.color = PANEL_BG
|
|
||||||
bar.position = Vector2.ZERO
|
|
||||||
bar.size = Vector2(1600, TOP_H)
|
|
||||||
bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
||||||
ui_root.add_child(bar)
|
|
||||||
|
|
||||||
var h := HBoxContainer.new()
|
|
||||||
h.position = Vector2(14, 9)
|
|
||||||
h.size = Vector2(1570, 30)
|
|
||||||
h.add_theme_constant_override("separation", 22)
|
|
||||||
ui_root.add_child(h)
|
|
||||||
|
|
||||||
lbl_round = UiKit.label("Runde 1", 18)
|
|
||||||
lbl_phase = UiKit.label("Planung", 16, Color(0.7, 0.85, 1.0))
|
|
||||||
lbl_hp = UiKit.label("Leben 100", 16, Color(0.5, 0.9, 0.55))
|
|
||||||
lbl_gold = UiKit.label("Gold 4", 16, Color(1.0, 0.85, 0.35))
|
|
||||||
lbl_slots = UiKit.label("Kader 0/3", 16)
|
|
||||||
lbl_streak = UiKit.label("Serie 0", 16, Color(0.85, 0.7, 1.0))
|
|
||||||
for l in [lbl_round, lbl_phase, lbl_hp, lbl_gold, lbl_slots, lbl_streak]:
|
|
||||||
h.add_child(l)
|
|
||||||
|
|
||||||
var spacer := Control.new()
|
|
||||||
spacer.custom_minimum_size = Vector2(360, 0)
|
|
||||||
h.add_child(spacer)
|
|
||||||
|
|
||||||
btn_speed = UiKit.button("Tempo 1x", Color(0.25, 0.28, 0.36))
|
|
||||||
btn_speed.custom_minimum_size = Vector2(96, 28)
|
|
||||||
btn_speed.pressed.connect(_on_speed)
|
|
||||||
h.add_child(btn_speed)
|
|
||||||
|
|
||||||
btn_ready = UiKit.button("Kampf starten [Leer]", Color(0.24, 0.52, 0.34))
|
|
||||||
btn_ready.custom_minimum_size = Vector2(190, 28)
|
|
||||||
btn_ready.pressed.connect(_on_ready)
|
|
||||||
h.add_child(btn_ready)
|
|
||||||
|
|
||||||
|
|
||||||
## Linke Spalte: Elemente, gewählte Einheit und Rundenlog, scrollbar.
|
|
||||||
func _build_left() -> void:
|
|
||||||
var w := LEFT_W - 16.0
|
|
||||||
var h := 900.0 - TOP_H - BOTTOM_H - 20.0
|
|
||||||
var p := UiKit.panel(PANEL_BG)
|
|
||||||
p.position = Vector2(8, TOP_H + 8)
|
|
||||||
p.size = Vector2(w, h)
|
|
||||||
p.clip_contents = true
|
|
||||||
ui_root.add_child(p)
|
|
||||||
var sc := ScrollContainer.new()
|
|
||||||
sc.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
|
||||||
p.add_child(sc)
|
|
||||||
var col := VBoxContainer.new()
|
|
||||||
col.add_theme_constant_override("separation", 10)
|
|
||||||
col.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
sc.add_child(col)
|
|
||||||
traits_box = VBoxContainer.new()
|
|
||||||
info_box = VBoxContainer.new()
|
|
||||||
log_box = VBoxContainer.new()
|
|
||||||
for b in [traits_box, info_box, log_box]:
|
|
||||||
b.add_theme_constant_override("separation", 3)
|
|
||||||
b.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
col.add_child(b)
|
|
||||||
col.add_child(HSeparator.new())
|
|
||||||
|
|
||||||
|
|
||||||
func _build_world() -> void:
|
func _build_world() -> void:
|
||||||
var we := WorldEnvironment.new()
|
var we := WorldEnvironment.new()
|
||||||
@@ -314,7 +272,6 @@ func _build_world() -> void:
|
|||||||
|
|
||||||
|
|
||||||
## Legt die Kamera so, dass das Spielfeld genau in die freie Bildmitte passt.
|
## Legt die Kamera so, dass das Spielfeld genau in die freie Bildmitte passt.
|
||||||
## Links, rechts und unten liegen Bedienpanels darüber.
|
|
||||||
func _fit_camera() -> void:
|
func _fit_camera() -> void:
|
||||||
var vp := Vector2(get_viewport().get_visible_rect().size)
|
var vp := Vector2(get_viewport().get_visible_rect().size)
|
||||||
var target := Rect2(LEFT_W + 16.0, TOP_H + 16.0,
|
var target := Rect2(LEFT_W + 16.0, TOP_H + 16.0,
|
||||||
@@ -341,10 +298,26 @@ func _fit_camera() -> void:
|
|||||||
if absf(kx) < 0.001 or absf(kz) < 0.001:
|
if absf(kx) < 0.001 or absf(kz) < 0.001:
|
||||||
break
|
break
|
||||||
center += Vector3(err.x / kx, 0.0, err.y / kz)
|
center += Vector3(err.x / kx, 0.0, err.y / kz)
|
||||||
if err.length() < 1.0 and absf(1.0 - maxf(r.size.x / target.size.x,
|
_cam_center = center
|
||||||
r.size.y / target.size.y)) < 0.01:
|
_cam_dir = dir
|
||||||
break
|
_cam_dist = dist
|
||||||
camera.look_at_from_position(center + dir * dist, center, Vector3.UP)
|
_apply_camera()
|
||||||
|
|
||||||
|
|
||||||
|
## Nutzeranteil: Zoom und Drehung um die Brettmitte.
|
||||||
|
func _apply_camera() -> void:
|
||||||
|
var dir := _cam_dir.rotated(Vector3.UP, deg_to_rad(cam_yaw))
|
||||||
|
camera.look_at_from_position(_cam_center + dir * _cam_dist * cam_zoom, _cam_center, Vector3.UP)
|
||||||
|
|
||||||
|
|
||||||
|
func _zoom(delta: float) -> void:
|
||||||
|
cam_zoom = clampf(cam_zoom + delta, ZOOM_MIN, ZOOM_MAX)
|
||||||
|
_apply_camera()
|
||||||
|
|
||||||
|
|
||||||
|
func _rotate(deg: float) -> void:
|
||||||
|
cam_yaw = fmod(cam_yaw + deg, 360.0)
|
||||||
|
_apply_camera()
|
||||||
|
|
||||||
|
|
||||||
func _project_rect(points: Array) -> Rect2:
|
func _project_rect(points: Array) -> Rect2:
|
||||||
@@ -359,7 +332,6 @@ func _project_rect(points: Array) -> Rect2:
|
|||||||
return Rect2(lo, hi - lo)
|
return Rect2(lo, hi - lo)
|
||||||
|
|
||||||
|
|
||||||
## Punkt auf der Feldebene unter einer Bildschirmposition, oder null.
|
|
||||||
func _ground_point(screen_pos: Vector2):
|
func _ground_point(screen_pos: Vector2):
|
||||||
if camera == null:
|
if camera == null:
|
||||||
return null
|
return null
|
||||||
@@ -373,7 +345,6 @@ func _ground_point(screen_pos: Vector2):
|
|||||||
return board.to_local(from + dir * t)
|
return board.to_local(from + dir * t)
|
||||||
|
|
||||||
|
|
||||||
## Position des gezogenen Avatars: auf Feldhöhe, damit er nicht schwebt.
|
|
||||||
func _drag_point(screen_pos: Vector2, fallback: Dictionary) -> Vector3:
|
func _drag_point(screen_pos: Vector2, fallback: Dictionary) -> Vector3:
|
||||||
var hit = _ground_point(screen_pos)
|
var hit = _ground_point(screen_pos)
|
||||||
if hit == null:
|
if hit == null:
|
||||||
@@ -381,7 +352,6 @@ func _drag_point(screen_pos: Vector2, fallback: Dictionary) -> Vector3:
|
|||||||
return Vector3(hit.x, 0.0, hit.z)
|
return Vector3(hit.x, 0.0, hit.z)
|
||||||
|
|
||||||
|
|
||||||
## Feld oder Bankplatz unter einer Bildschirmposition.
|
|
||||||
func _pick_slot(screen_pos: Vector2) -> Dictionary:
|
func _pick_slot(screen_pos: Vector2) -> Dictionary:
|
||||||
var hit = _ground_point(screen_pos)
|
var hit = _ground_point(screen_pos)
|
||||||
if hit == null:
|
if hit == null:
|
||||||
@@ -389,45 +359,35 @@ func _pick_slot(screen_pos: Vector2) -> Dictionary:
|
|||||||
return board.slot_at_world(hit)
|
return board.slot_at_world(hit)
|
||||||
|
|
||||||
|
|
||||||
## Rechte Spalte: die acht Spieler mit Leben und Serie.
|
# --- Panels ------------------------------------------------------------------
|
||||||
func _build_right() -> void:
|
|
||||||
players_box = _add_panel(Vector2(1600.0 - RIGHT_W + 8.0, TOP_H + 8),
|
|
||||||
Vector2(RIGHT_W - 16, 900 - TOP_H - BOTTOM_H - 20))
|
|
||||||
players_box.add_child(UiKit.label("Spieler", 16, Color(0.75, 0.8, 0.95)))
|
|
||||||
|
|
||||||
|
func _build_panels() -> void:
|
||||||
|
var vp := Vector2(1600, 900)
|
||||||
|
top = TopBar.new()
|
||||||
|
top.build(vp.x, TOP_H)
|
||||||
|
top.ready_pressed.connect(_on_ready)
|
||||||
|
top.xp_pressed.connect(_on_buy_xp)
|
||||||
|
top.speed_pressed.connect(_on_speed)
|
||||||
|
top.pause_pressed.connect(_toggle_pause)
|
||||||
|
ui_root.add_child(top)
|
||||||
|
|
||||||
func _build_bottom() -> void:
|
var col_h := vp.y - TOP_H - BOTTOM_H - 20.0
|
||||||
var y := 900.0 - BOTTOM_H
|
info = InfoColumn.new()
|
||||||
var p := UiKit.panel(PANEL_BG)
|
info.build(8.0, TOP_H + 8.0, LEFT_W - 16.0, col_h)
|
||||||
p.position = Vector2(8, y)
|
info.upgrade_pressed.connect(_on_upgrade)
|
||||||
p.size = Vector2(1584, BOTTOM_H - 10)
|
ui_root.add_child(info)
|
||||||
ui_root.add_child(p)
|
|
||||||
var v := VBoxContainer.new()
|
|
||||||
v.add_theme_constant_override("separation", 6)
|
|
||||||
p.add_child(v)
|
|
||||||
|
|
||||||
var head := HBoxContainer.new()
|
players = PlayersPanel.new()
|
||||||
head.add_theme_constant_override("separation", 16)
|
players.build(vp.x - RIGHT_W + 8.0, TOP_H + 8.0, RIGHT_W - 16.0, col_h)
|
||||||
v.add_child(head)
|
players.scout_requested.connect(_on_scout)
|
||||||
head.add_child(UiKit.label("Shop", 13, Color(0.7, 0.72, 0.8)))
|
ui_root.add_child(players)
|
||||||
btn_reroll = UiKit.button("Neu würfeln %d Gold [D]" % Data.REROLL_COST, Color(0.30, 0.32, 0.44))
|
|
||||||
btn_reroll.custom_minimum_size = Vector2(200, 26)
|
|
||||||
btn_reroll.pressed.connect(_on_reroll)
|
|
||||||
head.add_child(btn_reroll)
|
|
||||||
lbl_fodder = UiKit.label("", 13, Color(0.7, 0.72, 0.8))
|
|
||||||
head.add_child(lbl_fodder)
|
|
||||||
lbl_hint = UiKit.label("", 13, Color(0.95, 0.85, 0.45))
|
|
||||||
head.add_child(lbl_hint)
|
|
||||||
var spacer := Control.new()
|
|
||||||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
||||||
head.add_child(spacer)
|
|
||||||
head.add_child(UiKit.label(
|
|
||||||
"Avatare zwischen Bank und Feld ziehen · Rechtsklick: verkaufen bzw. zurück",
|
|
||||||
13, Color(0.6, 0.62, 0.7)))
|
|
||||||
|
|
||||||
shop_row = HBoxContainer.new()
|
shop = ShopPanel.new()
|
||||||
shop_row.add_theme_constant_override("separation", 8)
|
shop.build(8.0, vp.y - BOTTOM_H, vp.x - 16.0, BOTTOM_H - 10.0)
|
||||||
v.add_child(shop_row)
|
shop.buy_requested.connect(_on_buy)
|
||||||
|
shop.reroll_pressed.connect(_on_reroll)
|
||||||
|
shop.lock_pressed.connect(_on_lock)
|
||||||
|
ui_root.add_child(shop)
|
||||||
|
|
||||||
|
|
||||||
func _build_picker() -> void:
|
func _build_picker() -> void:
|
||||||
@@ -478,32 +438,56 @@ func _open_picker(title: String, options: Array, cb: Callable, previews: Array =
|
|||||||
picker.popup_centered()
|
picker.popup_centered()
|
||||||
|
|
||||||
|
|
||||||
# --- Aktionen --------------------------------------------------------------
|
# --- Aktionen ----------------------------------------------------------------
|
||||||
|
|
||||||
func _on_ready() -> void:
|
func _on_ready() -> void:
|
||||||
if game.phase == Game.Phase.PLANNING:
|
if game.phase == Game.Phase.PLANNING:
|
||||||
_cancel_drag()
|
_cancel_drag()
|
||||||
sel_slot = {}
|
sel_slot = {}
|
||||||
|
picker.hide()
|
||||||
game.start_combat()
|
game.start_combat()
|
||||||
|
sfx.play("runde")
|
||||||
_refresh()
|
_refresh()
|
||||||
elif game.phase == Game.Phase.COMBAT:
|
elif game.phase == Game.Phase.COMBAT:
|
||||||
game.skip_combat()
|
game.skip_combat()
|
||||||
|
_after_combat()
|
||||||
_refresh()
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
func _on_speed() -> void:
|
func _on_speed() -> void:
|
||||||
combat_speed = 1.0 if combat_speed >= 4.0 else combat_speed * 2.0
|
combat_speed = 1.0 if combat_speed >= 4.0 else combat_speed * 2.0
|
||||||
btn_speed.text = "Tempo %dx" % int(combat_speed)
|
top.refresh(game, combat_speed)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_buy_xp() -> void:
|
||||||
|
if game.phase != Game.Phase.PLANNING:
|
||||||
|
return
|
||||||
|
var before := game.human.level
|
||||||
|
if game.human.buy_xp():
|
||||||
|
sfx.play("level" if game.human.level > before else "kauf")
|
||||||
|
else:
|
||||||
|
shop.hint("Nicht genug Gold für XP." if game.human.level < Data.MAX_LEVEL else "Maximales Level.")
|
||||||
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
func _on_reroll() -> void:
|
func _on_reroll() -> void:
|
||||||
if game.phase != Game.Phase.PLANNING:
|
if game.phase != Game.Phase.PLANNING:
|
||||||
return
|
return
|
||||||
game.human.reroll(game.pool, game.rand)
|
if game.human.reroll(game.pool, game.rand):
|
||||||
|
sfx.play("wuerfeln")
|
||||||
_refresh()
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
func _on_buy(index: int) -> void:
|
func _on_lock() -> void:
|
||||||
|
if game.phase != Game.Phase.PLANNING:
|
||||||
|
return
|
||||||
|
game.human.shop_locked = not game.human.shop_locked
|
||||||
|
sfx.play("setzen")
|
||||||
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
## Kauf. `quick` (Rechtsklick) nimmt bei Neutral die zuletzt gewählte Spezialisierung.
|
||||||
|
func _on_buy(index: int, quick: bool) -> void:
|
||||||
if game.phase != Game.Phase.PLANNING:
|
if game.phase != Game.Phase.PLANNING:
|
||||||
return
|
return
|
||||||
var p := game.human
|
var p := game.human
|
||||||
@@ -513,26 +497,32 @@ func _on_buy(index: int) -> void:
|
|||||||
if entry.sold:
|
if entry.sold:
|
||||||
return
|
return
|
||||||
if p.free_bench_slot() < 0:
|
if p.free_bench_slot() < 0:
|
||||||
_hint("Bank ist voll. Erst etwas verkaufen.")
|
shop.hint("Bank ist voll. Erst etwas verkaufen.")
|
||||||
return
|
return
|
||||||
if p.gold < entry.price():
|
if p.gold < entry.price():
|
||||||
return
|
return
|
||||||
if entry.element >= 0:
|
var direct := entry.element
|
||||||
if not p.buy(index, game.pool, entry.element):
|
if direct < 0 and quick and p.last_element >= 0 and game.pool.element_left[p.last_element] > 0:
|
||||||
_hint("Kauf nicht möglich (Vorrat leer).")
|
direct = p.last_element
|
||||||
|
if direct >= 0:
|
||||||
|
if p.buy(index, game.pool, direct):
|
||||||
|
sfx.play("kauf")
|
||||||
|
else:
|
||||||
|
shop.hint("Kauf nicht möglich (Vorrat leer).")
|
||||||
_refresh()
|
_refresh()
|
||||||
return
|
return
|
||||||
# Neutraler Avatar: Spezialisierung schon beim Kauf wählen.
|
|
||||||
var free: Array[int] = []
|
var free: Array[int] = []
|
||||||
for e in Data.ELEMENT_COUNT:
|
for e in Data.ELEMENT_COUNT:
|
||||||
if game.pool.element_left[e] > 0:
|
if game.pool.element_left[e] > 0:
|
||||||
free.append(e)
|
free.append(e)
|
||||||
if free.is_empty():
|
if free.is_empty():
|
||||||
_hint("Kein Element mehr im Vorrat.")
|
shop.hint("Kein Element mehr im Vorrat.")
|
||||||
return
|
return
|
||||||
_open_picker("Spezialisierung wählen", free, func(e):
|
_open_picker("Spezialisierung wählen", free, func(e):
|
||||||
if not game.human.buy(index, game.pool, e):
|
if game.human.buy(index, game.pool, e):
|
||||||
_hint("Kauf nicht möglich (Vorrat leer).")
|
sfx.play("kauf")
|
||||||
|
else:
|
||||||
|
shop.hint("Kauf nicht möglich (Vorrat leer).")
|
||||||
_refresh())
|
_refresh())
|
||||||
|
|
||||||
|
|
||||||
@@ -547,7 +537,8 @@ func _on_upgrade() -> void:
|
|||||||
for e in options:
|
for e in options:
|
||||||
previews.append("+ " + _element_effect(e) + " (halbe Stärke)")
|
previews.append("+ " + _element_effect(e) + " (halbe Stärke)")
|
||||||
_open_picker("Zweite/dritte Spezialisierung", options, func(e):
|
_open_picker("Zweite/dritte Spezialisierung", options, func(e):
|
||||||
game.human.upgrade(hex, e, game.pool)
|
if game.human.upgrade(hex, e, game.pool):
|
||||||
|
sfx.play("aufwerten")
|
||||||
_refresh(), previews,
|
_refresh(), previews,
|
||||||
"Fähigkeit wird zu: " + _star_effect(a.role, a.star + 1))
|
"Fähigkeit wird zu: " + _star_effect(a.role, a.star + 1))
|
||||||
|
|
||||||
@@ -555,17 +546,11 @@ func _on_upgrade() -> void:
|
|||||||
func _star_effect(role: int, star: int) -> String:
|
func _star_effect(role: int, star: int) -> String:
|
||||||
match role:
|
match role:
|
||||||
Data.Role.TANK:
|
Data.Role.TANK:
|
||||||
if star == 2:
|
return "Schild und spottet 3 Gegner" if star == 2 else "Schild, Spott und 25% Reflexion"
|
||||||
return "Schild und spottet 3 Gegner"
|
|
||||||
return "Schild, Spott und 25% Reflexion"
|
|
||||||
Data.Role.DPS:
|
Data.Role.DPS:
|
||||||
if star == 2:
|
return "Ziel plus Splash auf Nachbarn" if star == 2 else "Ziel plus Fläche im Radius 2"
|
||||||
return "Ziel plus Splash auf Nachbarn"
|
|
||||||
return "Ziel plus Fläche im Radius 2"
|
|
||||||
_:
|
_:
|
||||||
if star == 2:
|
return "heilt 3 Verbündete" if star == 2 else "heilt alle und schildet sie"
|
||||||
return "heilt 3 Verbündete"
|
|
||||||
return "heilt alle und schildet sie"
|
|
||||||
|
|
||||||
|
|
||||||
func _element_effect(element: int) -> String:
|
func _element_effect(element: int) -> String:
|
||||||
@@ -578,253 +563,115 @@ func _element_effect(element: int) -> String:
|
|||||||
_: return "Verlangsamung"
|
_: return "Verlangsamung"
|
||||||
|
|
||||||
|
|
||||||
## Gewähltes Board-Feld, oder (-1,-1) wenn nichts oder ein Bankplatz gewählt ist.
|
func _on_scout(p: PlayerState) -> void:
|
||||||
|
scout = null if (p == game.human or p == scout) else p
|
||||||
|
board.scout_player = scout
|
||||||
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
func selected_board_hex() -> Vector2i:
|
func selected_board_hex() -> Vector2i:
|
||||||
if sel_slot.is_empty() or sel_slot["kind"] != "board":
|
if sel_slot.is_empty() or sel_slot["kind"] != "board":
|
||||||
return Vector2i(-1, -1)
|
return Vector2i(-1, -1)
|
||||||
return sel_slot["hex"]
|
return sel_slot["hex"]
|
||||||
|
|
||||||
|
|
||||||
func _hint(text: String) -> void:
|
# --- Anzeige -----------------------------------------------------------------
|
||||||
lbl_hint.text = text
|
|
||||||
get_tree().create_timer(2.5).timeout.connect(func():
|
|
||||||
if is_instance_valid(lbl_hint) and lbl_hint.text == text:
|
|
||||||
lbl_hint.text = "")
|
|
||||||
|
|
||||||
|
|
||||||
# --- Anzeige ---------------------------------------------------------------
|
|
||||||
|
|
||||||
func _refresh() -> void:
|
func _refresh() -> void:
|
||||||
_refresh_header()
|
top.refresh(game, combat_speed)
|
||||||
_refresh_players()
|
players.refresh(game, scout)
|
||||||
_refresh_traits()
|
info.refresh(game, _avatar_at(sel_slot), selected_board_hex())
|
||||||
_refresh_info()
|
shop.refresh(game)
|
||||||
_refresh_log()
|
|
||||||
_refresh_fodder()
|
|
||||||
_refresh_shop()
|
|
||||||
board.selected_slot = sel_slot
|
board.selected_slot = sel_slot
|
||||||
if game.phase == Game.Phase.GAMEOVER:
|
if game.phase == Game.Phase.GAMEOVER:
|
||||||
_show_gameover()
|
_show_gameover()
|
||||||
|
|
||||||
|
|
||||||
func _refresh_header() -> void:
|
# --- Overlays: Start, Pause, Ende --------------------------------------------
|
||||||
var p := game.human
|
|
||||||
lbl_round.text = "Runde %d" % game.round_no
|
|
||||||
lbl_hp.text = "Leben %d" % p.hp
|
|
||||||
var interest := Data.interest(p.gold)
|
|
||||||
lbl_gold.text = "Gold %d (+%d Zins)" % [p.gold, interest] if interest > 0 else "Gold %d" % p.gold
|
|
||||||
lbl_slots.text = "Kader %d/%d" % [p.board.size(), p.team_slots(game.round_no)]
|
|
||||||
var s := p.streak
|
|
||||||
lbl_streak.text = "Serie %s%d (+%d Gold)" % ["+" if s > 0 else "", s, Data.streak_gold(s)]
|
|
||||||
match game.phase:
|
|
||||||
Game.Phase.PLANNING:
|
|
||||||
lbl_phase.text = "Planung"
|
|
||||||
btn_ready.text = "Kampf starten [Leer]"
|
|
||||||
btn_ready.disabled = false
|
|
||||||
Game.Phase.COMBAT:
|
|
||||||
var opp := game.human_opponent()
|
|
||||||
lbl_phase.text = "Kampf gegen %s" % (opp.pname if opp else "?")
|
|
||||||
btn_ready.text = "Kampf überspringen"
|
|
||||||
btn_ready.disabled = false
|
|
||||||
_:
|
|
||||||
lbl_phase.text = "Spiel vorbei"
|
|
||||||
btn_ready.disabled = true
|
|
||||||
|
|
||||||
|
func _overlay_box(title: String) -> VBoxContainer:
|
||||||
## Label für das rechte Panel: bricht um und bleibt in der Panelbreite.
|
|
||||||
func _wrapped(text: String, size: int, col: Color) -> Label:
|
|
||||||
var l := UiKit.label(text, size, col)
|
|
||||||
l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
||||||
l.custom_minimum_size = Vector2(TEXT_W, 0)
|
|
||||||
l.size_flags_horizontal = Control.SIZE_FILL
|
|
||||||
return l
|
|
||||||
|
|
||||||
|
|
||||||
func _clear(box: Node, keep: int = 0) -> void:
|
|
||||||
var kids := box.get_children()
|
|
||||||
for i in range(keep, kids.size()):
|
|
||||||
box.remove_child(kids[i])
|
|
||||||
kids[i].queue_free()
|
|
||||||
|
|
||||||
|
|
||||||
func _refresh_players() -> void:
|
|
||||||
_clear(players_box, 1)
|
|
||||||
for p in game.ranking():
|
|
||||||
var row := VBoxContainer.new()
|
|
||||||
row.add_theme_constant_override("separation", 1)
|
|
||||||
var top := HBoxContainer.new()
|
|
||||||
var col := Color(0.9, 0.9, 0.95) if p.alive else Color(0.5, 0.5, 0.55)
|
|
||||||
if p == game.human:
|
|
||||||
col = Color(0.6, 0.9, 1.0)
|
|
||||||
var nm := UiKit.label(p.pname, 14, col)
|
|
||||||
nm.custom_minimum_size = Vector2(88, 0)
|
|
||||||
top.add_child(nm)
|
|
||||||
top.add_child(UiKit.label("%d" % p.hp, 14, col))
|
|
||||||
if p.streak != 0:
|
|
||||||
top.add_child(UiKit.label(" Serie %s%d" % ["+" if p.streak > 0 else "", p.streak], 12,
|
|
||||||
Color(0.85, 0.7, 1.0)))
|
|
||||||
row.add_child(top)
|
|
||||||
var bar := ColorRect.new()
|
|
||||||
bar.color = Color(0.12, 0.13, 0.16)
|
|
||||||
bar.custom_minimum_size = Vector2(RIGHT_W - 32, 6)
|
|
||||||
var fill := ColorRect.new()
|
|
||||||
fill.color = Color(0.35, 0.8, 0.45) if p.alive else Color(0.4, 0.4, 0.45)
|
|
||||||
fill.size = Vector2((RIGHT_W - 32) * (float(p.hp) / float(Data.START_HP)), 6)
|
|
||||||
fill.position = Vector2.ZERO
|
|
||||||
bar.add_child(fill)
|
|
||||||
row.add_child(bar)
|
|
||||||
players_box.add_child(row)
|
|
||||||
var opp := game.human_opponent()
|
|
||||||
if opp != null and game.phase == Game.Phase.COMBAT:
|
|
||||||
players_box.add_child(UiKit.label("\nGegner: " + opp.pname, 13, Color(1.0, 0.6, 0.6)))
|
|
||||||
|
|
||||||
|
|
||||||
func _refresh_traits() -> void:
|
|
||||||
_clear(traits_box)
|
|
||||||
traits_box.add_child(UiKit.label("Elemente im Team", 15, Color(0.75, 0.8, 0.95)))
|
|
||||||
var counts := game.human.trait_counts()
|
|
||||||
var any := false
|
|
||||||
for e in Data.ELEMENT_COUNT:
|
|
||||||
var c: int = counts.get(e, 0)
|
|
||||||
if c == 0:
|
|
||||||
continue
|
|
||||||
any = true
|
|
||||||
var tier := Data.trait_tier(e, c)
|
|
||||||
var col: Color = Data.ELEMENT_COLORS[e]
|
|
||||||
var row := HBoxContainer.new()
|
|
||||||
var dot := ColorRect.new()
|
|
||||||
dot.color = col if tier > 0 else col.darkened(0.55)
|
|
||||||
dot.custom_minimum_size = Vector2(12, 12)
|
|
||||||
row.add_child(dot)
|
|
||||||
var txt := " %s %d" % [Data.ELEMENT_NAMES[e], c]
|
|
||||||
if tier > 0:
|
|
||||||
txt += " - " + Data.ELEMENT_TRAITS[e]["desc"][tier - 1]
|
|
||||||
else:
|
|
||||||
txt += " (ab 2 aktiv)"
|
|
||||||
var el := UiKit.label(txt, 13, Color(0.9, 0.9, 0.95) if tier > 0 else Color(0.55, 0.55, 0.6))
|
|
||||||
el.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
||||||
el.custom_minimum_size = Vector2(TEXT_W - 20.0, 0)
|
|
||||||
row.add_child(el)
|
|
||||||
traits_box.add_child(row)
|
|
||||||
if not any:
|
|
||||||
traits_box.add_child(_wrapped("Noch nichts aufgestellt.", 13, Color(0.55, 0.55, 0.6)))
|
|
||||||
traits_box.add_child(UiKit.label("\nKombinationen", 15, Color(0.75, 0.8, 0.95)))
|
|
||||||
var active := game.human.active_combos()
|
|
||||||
var keys := []
|
|
||||||
for c in active:
|
|
||||||
keys.append(c["key"])
|
|
||||||
for c in Data.COMBO_TRAITS:
|
|
||||||
var on: bool = keys.has(c["key"])
|
|
||||||
var t := "%s (%s+%s): %s" % [c["name"], Data.ELEMENT_NAMES[c["a"]],
|
|
||||||
Data.ELEMENT_NAMES[c["b"]], c["desc"]]
|
|
||||||
traits_box.add_child(_wrapped(t, 12, Color(0.6, 0.95, 0.7) if on else Color(0.45, 0.45, 0.5)))
|
|
||||||
|
|
||||||
|
|
||||||
func _refresh_info() -> void:
|
|
||||||
_clear(info_box)
|
|
||||||
info_box.add_child(UiKit.label("Einheit", 15, Color(0.75, 0.8, 0.95)))
|
|
||||||
var p := game.human
|
|
||||||
var a: Avatar = _selected_avatar()
|
|
||||||
if a == null:
|
|
||||||
info_box.add_child(_wrapped("Einheit auf dem Feld oder der Bank anklicken.", 13,
|
|
||||||
Color(0.55, 0.55, 0.6)))
|
|
||||||
return
|
|
||||||
var s := a.base_stats()
|
|
||||||
info_box.add_child(_wrapped("%s %d-Star" % [a.display_name(), a.star], 14, a.color()))
|
|
||||||
info_box.add_child(_wrapped("Leben %d Angriff %d Rüstung %d%%\nTempo %.2f Reichweite %d"
|
|
||||||
% [int(s["hp"]), int(s["atk"]), int(s["armor"] * 100.0), s["aspd"], s["range"]], 12,
|
|
||||||
Color(0.9, 0.9, 0.95)))
|
|
||||||
info_box.add_child(_wrapped(a.ability_text(), 12, Color(0.8, 0.85, 1.0)))
|
|
||||||
|
|
||||||
var hex := selected_board_hex()
|
|
||||||
if hex.x < 0:
|
|
||||||
info_box.add_child(_wrapped("Zum Aufwerten aufs Feld ziehen.", 12,
|
|
||||||
Color(0.85, 0.85, 0.9)))
|
|
||||||
return
|
|
||||||
var cost := a.upgrade_cost()
|
|
||||||
if cost.is_empty():
|
|
||||||
info_box.add_child(_wrapped("Maximale Stufe.", 12, Color(1.0, 0.85, 0.35)))
|
|
||||||
return
|
|
||||||
var missing := p.upgrade_missing(hex)
|
|
||||||
var need := []
|
|
||||||
for e in cost:
|
|
||||||
need.append("%dx %s" % [cost[e], Data.ELEMENT_NAMES[e]])
|
|
||||||
info_box.add_child(_wrapped("Aufwerten braucht: " + ", ".join(need), 12,
|
|
||||||
Color(0.85, 0.85, 0.9)))
|
|
||||||
if missing.is_empty():
|
|
||||||
var b := UiKit.button("Aufwerten", Color(0.28, 0.55, 0.36))
|
|
||||||
b.custom_minimum_size = Vector2(140, 26)
|
|
||||||
b.pressed.connect(_on_upgrade)
|
|
||||||
info_box.add_child(b)
|
|
||||||
else:
|
|
||||||
var miss := []
|
|
||||||
for e in missing:
|
|
||||||
miss.append("%dx %s" % [missing[e], Data.ELEMENT_NAMES[e]])
|
|
||||||
info_box.add_child(_wrapped("Es fehlt: " + ", ".join(miss), 12, Color(0.95, 0.6, 0.5)))
|
|
||||||
|
|
||||||
|
|
||||||
func _selected_avatar():
|
|
||||||
return _avatar_at(sel_slot)
|
|
||||||
|
|
||||||
|
|
||||||
func _refresh_log() -> void:
|
|
||||||
_clear(log_box)
|
|
||||||
log_box.add_child(UiKit.label("Letzte Runde", 15, Color(0.75, 0.8, 0.95)))
|
|
||||||
if game.last_round_log.is_empty():
|
|
||||||
log_box.add_child(UiKit.label("-", 12, Color(0.55, 0.55, 0.6)))
|
|
||||||
for line in game.last_round_log:
|
|
||||||
log_box.add_child(_wrapped(line, 12, Color(0.8, 0.8, 0.86)))
|
|
||||||
|
|
||||||
|
|
||||||
func _refresh_fodder() -> void:
|
|
||||||
var fodder := game.human.fodder_counts()
|
|
||||||
var parts := []
|
|
||||||
for e in fodder:
|
|
||||||
parts.append("%dx %s" % [fodder[e], Data.ELEMENT_NAMES[e]])
|
|
||||||
lbl_fodder.text = "Futter auf der Bank: " + (", ".join(parts) if parts else "-")
|
|
||||||
|
|
||||||
|
|
||||||
func _refresh_shop() -> void:
|
|
||||||
_clear(shop_row)
|
|
||||||
var p := game.human
|
|
||||||
for i in p.shop.size():
|
|
||||||
var e: ShopEntry = p.shop[i]
|
|
||||||
var col := e.color()
|
|
||||||
var txt := "verkauft"
|
|
||||||
var b: Button
|
|
||||||
if e.sold:
|
|
||||||
b = UiKit.button(txt, SLOT_BG.darkened(0.3))
|
|
||||||
b.disabled = true
|
|
||||||
else:
|
|
||||||
var note: String = "Element wählen" if e.element < 0 \
|
|
||||||
else "noch %d im Vorrat" % game.pool.element_left[e.element]
|
|
||||||
b = UiKit.button("%s\n%d Gold\n%s" % [e.label(), e.price(), note], col, UiKit.ink(col))
|
|
||||||
b.disabled = p.gold < e.price() or game.phase != Game.Phase.PLANNING
|
|
||||||
b.pressed.connect(_on_buy.bind(i))
|
|
||||||
b.custom_minimum_size = Vector2(196, 74)
|
|
||||||
shop_row.add_child(b)
|
|
||||||
var mine := []
|
|
||||||
for el in game.human.shop_elements(game.pool):
|
|
||||||
mine.append("%s %d" % [Data.ELEMENT_NAMES[el], game.pool.element_left[el]])
|
|
||||||
shop_row.add_child(UiKit.label(" Dein Shop zeigt Neutral\n und deine Elemente:\n %s"
|
|
||||||
% [", ".join(mine) if mine else "noch keine aufgestellt"], 12,
|
|
||||||
Color(0.7, 0.72, 0.8)))
|
|
||||||
|
|
||||||
|
|
||||||
func _show_gameover() -> void:
|
|
||||||
if overlay != null:
|
if overlay != null:
|
||||||
return
|
overlay.queue_free()
|
||||||
overlay = ColorRect.new()
|
overlay = ColorRect.new()
|
||||||
overlay.color = Color(0, 0, 0, 0.72)
|
overlay.color = Color(0, 0, 0, 0.72)
|
||||||
overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
overlay.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
ui_root.add_child(overlay)
|
ui_root.add_child(overlay)
|
||||||
var v := VBoxContainer.new()
|
var v := VBoxContainer.new()
|
||||||
v.position = Vector2(620, 320)
|
v.position = Vector2(600, 280)
|
||||||
v.add_theme_constant_override("separation", 10)
|
v.custom_minimum_size = Vector2(400, 0)
|
||||||
|
v.add_theme_constant_override("separation", 12)
|
||||||
overlay.add_child(v)
|
overlay.add_child(v)
|
||||||
|
v.add_child(UiKit.label(title, 32))
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
func _close_overlay() -> void:
|
||||||
|
if overlay != null:
|
||||||
|
overlay.queue_free()
|
||||||
|
overlay = null
|
||||||
|
|
||||||
|
|
||||||
|
func _volume_row() -> HBoxContainer:
|
||||||
|
var h := HBoxContainer.new()
|
||||||
|
h.add_theme_constant_override("separation", 10)
|
||||||
|
h.add_child(UiKit.label("Lautstärke", 14))
|
||||||
|
var sl := HSlider.new()
|
||||||
|
sl.min_value = 0.0
|
||||||
|
sl.max_value = 1.0
|
||||||
|
sl.step = 0.05
|
||||||
|
sl.value = 0.8
|
||||||
|
sl.custom_minimum_size = Vector2(220, 20)
|
||||||
|
sl.focus_mode = Control.FOCUS_NONE
|
||||||
|
sl.value_changed.connect(func(v): sfx.set_volume(v))
|
||||||
|
h.add_child(sl)
|
||||||
|
return h
|
||||||
|
|
||||||
|
|
||||||
|
func _show_start() -> void:
|
||||||
|
var v := _overlay_box("Elements Autochess")
|
||||||
|
v.add_child(UiKit.label("8 Spieler, 100 Leben, Elemente als Traits. Basis wie TFT.", 15,
|
||||||
|
Color(0.8, 0.85, 0.95)))
|
||||||
|
v.add_child(UiKit.label(
|
||||||
|
"Ziehen: Bank ↔ Feld · Rechtsklick: verkaufen · D würfeln · F einfrieren · X Level\nQ/E drehen · Mausrad zoomen · P Pause · Leertaste Kampf",
|
||||||
|
13, Color(0.7, 0.72, 0.8)))
|
||||||
|
v.add_child(_volume_row())
|
||||||
|
var b := UiKit.button("Spiel starten", Color(0.28, 0.5, 0.36))
|
||||||
|
b.custom_minimum_size = Vector2(200, 38)
|
||||||
|
b.pressed.connect(_start_game)
|
||||||
|
v.add_child(b)
|
||||||
|
|
||||||
|
|
||||||
|
func _start_game() -> void:
|
||||||
|
started = true
|
||||||
|
paused = false
|
||||||
|
_close_overlay()
|
||||||
|
sfx.play("runde")
|
||||||
|
|
||||||
|
|
||||||
|
func _toggle_pause() -> void:
|
||||||
|
if not started or game.phase == Game.Phase.GAMEOVER:
|
||||||
|
return
|
||||||
|
paused = not paused
|
||||||
|
if paused:
|
||||||
|
_cancel_drag()
|
||||||
|
picker.hide()
|
||||||
|
var v := _overlay_box("Pause")
|
||||||
|
v.add_child(_volume_row())
|
||||||
|
var b := UiKit.button("Weiter [P]", Color(0.28, 0.5, 0.36))
|
||||||
|
b.custom_minimum_size = Vector2(200, 36)
|
||||||
|
b.pressed.connect(_toggle_pause)
|
||||||
|
v.add_child(b)
|
||||||
|
else:
|
||||||
|
_close_overlay()
|
||||||
|
|
||||||
|
|
||||||
|
func _show_gameover() -> void:
|
||||||
|
if overlay != null and overlay.has_meta("gameover"):
|
||||||
|
return
|
||||||
|
var v := _overlay_box("Spiel vorbei")
|
||||||
|
overlay.set_meta("gameover", true)
|
||||||
var alive := game.alive_players()
|
var alive := game.alive_players()
|
||||||
var win: String = alive[0].pname if not alive.is_empty() else "-"
|
var win: String = alive[0].pname if not alive.is_empty() else "-"
|
||||||
v.add_child(UiKit.label("Spiel vorbei", 32))
|
|
||||||
v.add_child(UiKit.label("Sieger: %s" % win, 20, Color(1.0, 0.85, 0.35)))
|
v.add_child(UiKit.label("Sieger: %s" % win, 20, Color(1.0, 0.85, 0.35)))
|
||||||
var place := game.human.placement if game.human.placement > 0 else 1
|
var place := game.human.placement if game.human.placement > 0 else 1
|
||||||
v.add_child(UiKit.label("Dein Platz: %d von 8" % place, 18,
|
v.add_child(UiKit.label("Dein Platz: %d von 8" % place, 18,
|
||||||
@@ -832,12 +679,15 @@ func _show_gameover() -> void:
|
|||||||
var b := UiKit.button("Neues Spiel", Color(0.28, 0.5, 0.36))
|
var b := UiKit.button("Neues Spiel", Color(0.28, 0.5, 0.36))
|
||||||
b.custom_minimum_size = Vector2(180, 36)
|
b.custom_minimum_size = Vector2(180, 36)
|
||||||
b.pressed.connect(func():
|
b.pressed.connect(func():
|
||||||
overlay.queue_free()
|
_close_overlay()
|
||||||
overlay = null
|
|
||||||
game = Game.new()
|
game = Game.new()
|
||||||
game.start()
|
game.start()
|
||||||
board.game = game
|
board.game = game
|
||||||
|
board.scout_player = null
|
||||||
|
scout = null
|
||||||
_cancel_drag()
|
_cancel_drag()
|
||||||
sel_slot = {}
|
sel_slot = {}
|
||||||
|
started = true
|
||||||
|
paused = false
|
||||||
_refresh())
|
_refresh())
|
||||||
v.add_child(b)
|
v.add_child(b)
|
||||||
|
|||||||
76
scripts/ui/PlayersPanel.gd
Normal file
76
scripts/ui/PlayersPanel.gd
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
class_name PlayersPanel
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
## Rechte Spalte: die acht Spieler mit Leben, Serie und Klick zum Ansehen.
|
||||||
|
|
||||||
|
signal scout_requested(p: PlayerState)
|
||||||
|
|
||||||
|
const PANEL_BG := Color(0.14, 0.15, 0.19)
|
||||||
|
|
||||||
|
var box: VBoxContainer
|
||||||
|
var _w := 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func build(x: float, y: float, w: float, h: float) -> void:
|
||||||
|
_w = w
|
||||||
|
position = Vector2(x, y)
|
||||||
|
size = Vector2(w, h)
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
var p := UiKit.panel(PANEL_BG)
|
||||||
|
p.size = size
|
||||||
|
add_child(p)
|
||||||
|
box = VBoxContainer.new()
|
||||||
|
box.add_theme_constant_override("separation", 4)
|
||||||
|
p.add_child(box)
|
||||||
|
box.add_child(UiKit.label("Spieler · Klick = ansehen", 15, Color(0.75, 0.8, 0.95)))
|
||||||
|
|
||||||
|
|
||||||
|
func refresh(game: Game, scout: PlayerState) -> void:
|
||||||
|
var kids := box.get_children()
|
||||||
|
for i in range(1, kids.size()):
|
||||||
|
box.remove_child(kids[i])
|
||||||
|
kids[i].queue_free()
|
||||||
|
var next_opp := game.human_opponent()
|
||||||
|
for p in game.ranking():
|
||||||
|
var row := VBoxContainer.new()
|
||||||
|
row.add_theme_constant_override("separation", 1)
|
||||||
|
var top := HBoxContainer.new()
|
||||||
|
var col := Color(0.9, 0.9, 0.95) if p.alive else Color(0.5, 0.5, 0.55)
|
||||||
|
if p == game.human:
|
||||||
|
col = Color(0.6, 0.9, 1.0)
|
||||||
|
elif p == next_opp:
|
||||||
|
col = Color(1.0, 0.75, 0.6)
|
||||||
|
var nm := UiKit.button(p.pname, PANEL_BG if p != scout else Color(0.30, 0.34, 0.48), col)
|
||||||
|
nm.custom_minimum_size = Vector2(96, 22)
|
||||||
|
nm.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
|
nm.disabled = not p.alive
|
||||||
|
nm.pressed.connect(func(): scout_requested.emit(p))
|
||||||
|
top.add_child(nm)
|
||||||
|
top.add_child(UiKit.label("%d" % p.hp, 14, col))
|
||||||
|
if p.streak != 0:
|
||||||
|
top.add_child(UiKit.label(" %s%d" % ["+" if p.streak > 0 else "", p.streak], 12,
|
||||||
|
Color(0.85, 0.7, 1.0)))
|
||||||
|
row.add_child(top)
|
||||||
|
var bar := ColorRect.new()
|
||||||
|
bar.color = Color(0.12, 0.13, 0.16)
|
||||||
|
bar.custom_minimum_size = Vector2(_w - 16, 6)
|
||||||
|
var fill := ColorRect.new()
|
||||||
|
fill.color = Color(0.35, 0.8, 0.45) if p.alive else Color(0.4, 0.4, 0.45)
|
||||||
|
fill.size = Vector2((_w - 16) * (float(p.hp) / float(Data.START_HP)), 6)
|
||||||
|
bar.add_child(fill)
|
||||||
|
row.add_child(bar)
|
||||||
|
box.add_child(row)
|
||||||
|
if next_opp != null and game.phase == Game.Phase.PLANNING:
|
||||||
|
box.add_child(_note("\nNächster Gegner: %s\nLevel %d, %d Einheiten" %
|
||||||
|
[next_opp.pname, next_opp.level, next_opp.board.size()], Color(1.0, 0.75, 0.6)))
|
||||||
|
elif next_opp != null:
|
||||||
|
box.add_child(_note("\nGegner: " + next_opp.pname, Color(1.0, 0.6, 0.6)))
|
||||||
|
if scout != null:
|
||||||
|
box.add_child(_note("Oben: Brett von %s" % scout.pname, Color(0.75, 0.85, 1.0)))
|
||||||
|
|
||||||
|
|
||||||
|
func _note(text: String, col: Color) -> Label:
|
||||||
|
var l := UiKit.label(text, 13, col)
|
||||||
|
l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
l.custom_minimum_size = Vector2(_w - 20, 0)
|
||||||
|
return l
|
||||||
1
scripts/ui/PlayersPanel.gd.uid
Normal file
1
scripts/ui/PlayersPanel.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bveden78b8xd1
|
||||||
114
scripts/ui/Sfx.gd
Normal file
114
scripts/ui/Sfx.gd
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
class_name Sfx
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
## Primitive Klänge, im Code erzeugt: kurze Töne mit Hüllkurve.
|
||||||
|
## Keine Audiodateien, passend zu den primitiven Meshes.
|
||||||
|
|
||||||
|
const RATE := 22050
|
||||||
|
const HIT_COOLDOWN := 0.06
|
||||||
|
|
||||||
|
var _streams := {}
|
||||||
|
var _players: Array = []
|
||||||
|
var _next := 0
|
||||||
|
var _hit_gate := 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
for i in 8:
|
||||||
|
var pl := AudioStreamPlayer.new()
|
||||||
|
pl.volume_db = -8.0
|
||||||
|
add_child(pl)
|
||||||
|
_players.append(pl)
|
||||||
|
_streams["kauf"] = _tone([[660.0, 0.07]], 0.5)
|
||||||
|
_streams["verkauf"] = _tone([[520.0, 0.05], [330.0, 0.07]], 0.45)
|
||||||
|
_streams["wuerfeln"] = _noise(0.04, 0.35)
|
||||||
|
_streams["setzen"] = _tone([[180.0, 0.06]], 0.6, true)
|
||||||
|
_streams["aufwerten"] = _tone([[523.0, 0.08], [659.0, 0.08], [784.0, 0.12]], 0.55)
|
||||||
|
_streams["level"] = _tone([[440.0, 0.08], [880.0, 0.14]], 0.5)
|
||||||
|
_streams["treffer"] = _noise(0.03, 0.22)
|
||||||
|
_streams["krit"] = _tone([[220.0, 0.05]], 0.55, true)
|
||||||
|
_streams["heilung"] = _tone([[988.0, 0.06]], 0.3)
|
||||||
|
_streams["zauber"] = _tone([[880.0, 0.05], [1320.0, 0.09]], 0.45)
|
||||||
|
_streams["sieg"] = _tone([[523.0, 0.1], [659.0, 0.1], [784.0, 0.1], [1046.0, 0.2]], 0.6)
|
||||||
|
_streams["niederlage"] = _tone([[392.0, 0.12], [311.0, 0.12], [261.0, 0.24]], 0.6)
|
||||||
|
_streams["runde"] = _tone([[440.0, 0.06], [440.0, 0.06]], 0.4)
|
||||||
|
_streams["fehler"] = _tone([[200.0, 0.1]], 0.5, true)
|
||||||
|
|
||||||
|
|
||||||
|
## Lautstärke 0..1, 0 schaltet stumm.
|
||||||
|
func set_volume(linear: float) -> void:
|
||||||
|
for pl in _players:
|
||||||
|
pl.volume_db = linear_to_db(maxf(0.0001, linear)) - 8.0
|
||||||
|
pl.stream_paused = false
|
||||||
|
_muted = linear <= 0.001
|
||||||
|
|
||||||
|
|
||||||
|
var _muted := false
|
||||||
|
|
||||||
|
|
||||||
|
func play(name: String) -> void:
|
||||||
|
if _muted:
|
||||||
|
return
|
||||||
|
if not _streams.has(name):
|
||||||
|
return
|
||||||
|
if name == "treffer":
|
||||||
|
var now := Time.get_ticks_msec() / 1000.0
|
||||||
|
if now < _hit_gate:
|
||||||
|
return
|
||||||
|
_hit_gate = now + HIT_COOLDOWN
|
||||||
|
var pl: AudioStreamPlayer = _players[_next]
|
||||||
|
_next = (_next + 1) % _players.size()
|
||||||
|
pl.stream = _streams[name]
|
||||||
|
pl.play()
|
||||||
|
|
||||||
|
|
||||||
|
## Tonfolge [[frequenz, dauer], ...] mit Ein- und Ausblenden.
|
||||||
|
func _tone(notes: Array, gain: float, square: bool = false) -> AudioStreamWAV:
|
||||||
|
var total := 0.0
|
||||||
|
for n in notes:
|
||||||
|
total += n[1]
|
||||||
|
var frames := int(total * RATE)
|
||||||
|
var data := PackedByteArray()
|
||||||
|
data.resize(frames * 2)
|
||||||
|
var idx := 0
|
||||||
|
for n in notes:
|
||||||
|
var freq: float = n[0]
|
||||||
|
var len_frames := int(n[1] * RATE)
|
||||||
|
for i in len_frames:
|
||||||
|
var t := float(i) / RATE
|
||||||
|
var env := minf(1.0, float(i) / (0.005 * RATE)) * minf(1.0, float(len_frames - i) / (0.02 * RATE))
|
||||||
|
var v := sin(TAU * freq * t)
|
||||||
|
if square:
|
||||||
|
v = 1.0 if v >= 0.0 else -1.0
|
||||||
|
_put(data, idx, v * env * gain)
|
||||||
|
idx += 1
|
||||||
|
if idx >= frames:
|
||||||
|
break
|
||||||
|
return _wav(data)
|
||||||
|
|
||||||
|
|
||||||
|
## Kurzes Rauschen für Treffer und Würfeln.
|
||||||
|
func _noise(dur: float, gain: float) -> AudioStreamWAV:
|
||||||
|
var frames := int(dur * RATE)
|
||||||
|
var data := PackedByteArray()
|
||||||
|
data.resize(frames * 2)
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = 1234
|
||||||
|
for i in frames:
|
||||||
|
var env := 1.0 - float(i) / float(frames)
|
||||||
|
_put(data, i, rng.randf_range(-1.0, 1.0) * env * env * gain)
|
||||||
|
return _wav(data)
|
||||||
|
|
||||||
|
|
||||||
|
func _put(data: PackedByteArray, index: int, v: float) -> void:
|
||||||
|
var s := int(clampf(v, -1.0, 1.0) * 32767.0)
|
||||||
|
data.encode_s16(index * 2, s)
|
||||||
|
|
||||||
|
|
||||||
|
func _wav(data: PackedByteArray) -> AudioStreamWAV:
|
||||||
|
var w := AudioStreamWAV.new()
|
||||||
|
w.format = AudioStreamWAV.FORMAT_16_BITS
|
||||||
|
w.mix_rate = RATE
|
||||||
|
w.stereo = false
|
||||||
|
w.data = data
|
||||||
|
return w
|
||||||
1
scripts/ui/Sfx.gd.uid
Normal file
1
scripts/ui/Sfx.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ciqf6ibhibuqp
|
||||||
113
scripts/ui/ShopPanel.gd
Normal file
113
scripts/ui/ShopPanel.gd
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
class_name ShopPanel
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
## Unten: Shop, Würfeln, Einfrieren, Futter und Hinweise.
|
||||||
|
|
||||||
|
signal buy_requested(index: int, quick: bool)
|
||||||
|
signal reroll_pressed
|
||||||
|
signal lock_pressed
|
||||||
|
|
||||||
|
const PANEL_BG := Color(0.14, 0.15, 0.19)
|
||||||
|
const SLOT_BG := Color(0.20, 0.21, 0.26)
|
||||||
|
|
||||||
|
var row: HBoxContainer
|
||||||
|
var btn_reroll: Button
|
||||||
|
var btn_lock: Button
|
||||||
|
var lbl_fodder: Label
|
||||||
|
var lbl_hint: Label
|
||||||
|
|
||||||
|
|
||||||
|
func build(x: float, y: float, w: float, h: float) -> void:
|
||||||
|
position = Vector2(x, y)
|
||||||
|
size = Vector2(w, h)
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
var p := UiKit.panel(PANEL_BG)
|
||||||
|
p.size = size
|
||||||
|
add_child(p)
|
||||||
|
var v := VBoxContainer.new()
|
||||||
|
v.add_theme_constant_override("separation", 6)
|
||||||
|
p.add_child(v)
|
||||||
|
|
||||||
|
var head := HBoxContainer.new()
|
||||||
|
head.add_theme_constant_override("separation", 14)
|
||||||
|
v.add_child(head)
|
||||||
|
head.add_child(UiKit.label("Shop", 13, Color(0.7, 0.72, 0.8)))
|
||||||
|
btn_reroll = UiKit.button("Neu würfeln %d Gold [D]" % Data.REROLL_COST, Color(0.30, 0.32, 0.44))
|
||||||
|
btn_reroll.custom_minimum_size = Vector2(200, 26)
|
||||||
|
btn_reroll.pressed.connect(func(): reroll_pressed.emit())
|
||||||
|
head.add_child(btn_reroll)
|
||||||
|
btn_lock = UiKit.button("Einfrieren [F]", Color(0.30, 0.38, 0.50))
|
||||||
|
btn_lock.custom_minimum_size = Vector2(150, 26)
|
||||||
|
btn_lock.pressed.connect(func(): lock_pressed.emit())
|
||||||
|
head.add_child(btn_lock)
|
||||||
|
lbl_fodder = UiKit.label("", 13, Color(0.7, 0.72, 0.8))
|
||||||
|
head.add_child(lbl_fodder)
|
||||||
|
lbl_hint = UiKit.label("", 13, Color(0.95, 0.85, 0.45))
|
||||||
|
head.add_child(lbl_hint)
|
||||||
|
var spacer := Control.new()
|
||||||
|
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
head.add_child(spacer)
|
||||||
|
head.add_child(UiKit.label(
|
||||||
|
"Ziehen: Bank ↔ Feld · Rechtsklick: verkaufen / zurück · Rechtsklick auf Neutral: wie zuletzt",
|
||||||
|
13, Color(0.6, 0.62, 0.7)))
|
||||||
|
|
||||||
|
row = HBoxContainer.new()
|
||||||
|
row.add_theme_constant_override("separation", 8)
|
||||||
|
v.add_child(row)
|
||||||
|
|
||||||
|
|
||||||
|
func refresh(game: Game) -> void:
|
||||||
|
var p := game.human
|
||||||
|
for c in row.get_children():
|
||||||
|
row.remove_child(c)
|
||||||
|
c.queue_free()
|
||||||
|
for i in p.shop.size():
|
||||||
|
var e: ShopEntry = p.shop[i]
|
||||||
|
var b: Button
|
||||||
|
if e.sold:
|
||||||
|
b = UiKit.button("verkauft", SLOT_BG.darkened(0.3))
|
||||||
|
b.disabled = true
|
||||||
|
else:
|
||||||
|
var col := e.color()
|
||||||
|
var note: String
|
||||||
|
if e.element < 0:
|
||||||
|
note = "Element wählen"
|
||||||
|
if p.last_element >= 0:
|
||||||
|
note += "\nRechtsklick: %s" % Data.ELEMENT_NAMES[p.last_element]
|
||||||
|
else:
|
||||||
|
note = "noch %d im Vorrat" % game.pool.element_left[e.element]
|
||||||
|
b = UiKit.button("%s\n%d Gold\n%s" % [e.label(), e.price(), note], col, UiKit.ink(col))
|
||||||
|
b.disabled = p.gold < e.price() or game.phase != Game.Phase.PLANNING
|
||||||
|
b.gui_input.connect(_on_card_input.bind(i))
|
||||||
|
b.custom_minimum_size = Vector2(196, 88)
|
||||||
|
row.add_child(b)
|
||||||
|
var mine := []
|
||||||
|
for el in p.shop_elements(game.pool):
|
||||||
|
mine.append("%s %d" % [Data.ELEMENT_NAMES[el], game.pool.element_left[el]])
|
||||||
|
row.add_child(UiKit.label(" Dein Shop zeigt Neutral\n und deine Elemente:\n %s"
|
||||||
|
% [", ".join(mine) if mine else "noch keine aufgestellt"], 12, Color(0.7, 0.72, 0.8)))
|
||||||
|
|
||||||
|
var fodder := p.fodder_counts()
|
||||||
|
var parts := []
|
||||||
|
for e in fodder:
|
||||||
|
parts.append("%dx %s" % [fodder[e], Data.ELEMENT_NAMES[e]])
|
||||||
|
lbl_fodder.text = "Futter: " + (", ".join(parts) if parts else "-")
|
||||||
|
btn_lock.text = "Eingefroren [F]" if p.shop_locked else "Einfrieren [F]"
|
||||||
|
btn_lock.add_theme_stylebox_override("normal",
|
||||||
|
UiKit.flat(Color(0.35, 0.55, 0.75) if p.shop_locked else Color(0.30, 0.38, 0.50)))
|
||||||
|
btn_reroll.disabled = game.phase != Game.Phase.PLANNING or p.gold < Data.REROLL_COST
|
||||||
|
|
||||||
|
|
||||||
|
func _on_card_input(event: InputEvent, index: int) -> void:
|
||||||
|
if event is InputEventMouseButton and event.pressed:
|
||||||
|
if event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
buy_requested.emit(index, false)
|
||||||
|
elif event.button_index == MOUSE_BUTTON_RIGHT:
|
||||||
|
buy_requested.emit(index, true)
|
||||||
|
|
||||||
|
|
||||||
|
func hint(text: String) -> void:
|
||||||
|
lbl_hint.text = text
|
||||||
|
get_tree().create_timer(2.5).timeout.connect(func():
|
||||||
|
if is_instance_valid(lbl_hint) and lbl_hint.text == text:
|
||||||
|
lbl_hint.text = "")
|
||||||
1
scripts/ui/ShopPanel.gd.uid
Normal file
1
scripts/ui/ShopPanel.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cg27viivxmuap
|
||||||
120
scripts/ui/TopBar.gd
Normal file
120
scripts/ui/TopBar.gd
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
class_name TopBar
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
## Kopfzeile: Runde, Phase, Uhr, Gold, Kader, Serie, nächster Gegner, Knöpfe.
|
||||||
|
|
||||||
|
signal ready_pressed
|
||||||
|
signal xp_pressed
|
||||||
|
signal speed_pressed
|
||||||
|
signal pause_pressed
|
||||||
|
|
||||||
|
const PANEL_BG := Color(0.14, 0.15, 0.19)
|
||||||
|
|
||||||
|
var lbl_round: Label
|
||||||
|
var lbl_phase: Label
|
||||||
|
var lbl_timer: Label
|
||||||
|
var lbl_hp: Label
|
||||||
|
var lbl_gold: Label
|
||||||
|
var lbl_slots: Label
|
||||||
|
var lbl_streak: Label
|
||||||
|
var lbl_next: Label
|
||||||
|
var btn_xp: Button
|
||||||
|
var btn_speed: Button
|
||||||
|
var btn_ready: Button
|
||||||
|
var btn_pause: Button
|
||||||
|
|
||||||
|
|
||||||
|
func build(width: float, height: float) -> void:
|
||||||
|
position = Vector2.ZERO
|
||||||
|
size = Vector2(width, height)
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
var bar := ColorRect.new()
|
||||||
|
bar.color = PANEL_BG
|
||||||
|
bar.size = size
|
||||||
|
bar.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(bar)
|
||||||
|
|
||||||
|
var h := HBoxContainer.new()
|
||||||
|
h.position = Vector2(14, 9)
|
||||||
|
h.size = Vector2(width - 28, 30)
|
||||||
|
h.add_theme_constant_override("separation", 18)
|
||||||
|
add_child(h)
|
||||||
|
|
||||||
|
lbl_round = UiKit.label("Runde 1", 18)
|
||||||
|
lbl_phase = UiKit.label("Planung", 16, Color(0.7, 0.85, 1.0))
|
||||||
|
lbl_timer = UiKit.label("30 s", 16, Color(0.95, 0.85, 0.45))
|
||||||
|
lbl_hp = UiKit.label("Leben 100", 16, Color(0.5, 0.9, 0.55))
|
||||||
|
lbl_gold = UiKit.label("Gold 4", 16, Color(1.0, 0.85, 0.35))
|
||||||
|
lbl_slots = UiKit.label("Kader 0/3", 16)
|
||||||
|
lbl_streak = UiKit.label("Serie 0", 16, Color(0.85, 0.7, 1.0))
|
||||||
|
lbl_next = UiKit.label("", 15, Color(1.0, 0.75, 0.6))
|
||||||
|
for l in [lbl_round, lbl_phase, lbl_timer, lbl_hp, lbl_gold, lbl_slots, lbl_streak, lbl_next]:
|
||||||
|
h.add_child(l)
|
||||||
|
|
||||||
|
btn_xp = UiKit.button("XP kaufen %d Gold [X]" % Data.XP_BUY_COST, Color(0.36, 0.30, 0.50))
|
||||||
|
btn_xp.custom_minimum_size = Vector2(170, 28)
|
||||||
|
btn_xp.pressed.connect(func(): xp_pressed.emit())
|
||||||
|
h.add_child(btn_xp)
|
||||||
|
|
||||||
|
var spacer := Control.new()
|
||||||
|
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
h.add_child(spacer)
|
||||||
|
|
||||||
|
btn_pause = UiKit.button("Pause [P]", Color(0.25, 0.28, 0.36))
|
||||||
|
btn_pause.custom_minimum_size = Vector2(96, 28)
|
||||||
|
btn_pause.pressed.connect(func(): pause_pressed.emit())
|
||||||
|
h.add_child(btn_pause)
|
||||||
|
|
||||||
|
btn_speed = UiKit.button("Tempo 1x", Color(0.25, 0.28, 0.36))
|
||||||
|
btn_speed.custom_minimum_size = Vector2(96, 28)
|
||||||
|
btn_speed.pressed.connect(func(): speed_pressed.emit())
|
||||||
|
h.add_child(btn_speed)
|
||||||
|
|
||||||
|
btn_ready = UiKit.button("Kampf starten [Leer]", Color(0.24, 0.52, 0.34))
|
||||||
|
btn_ready.custom_minimum_size = Vector2(190, 28)
|
||||||
|
btn_ready.pressed.connect(func(): ready_pressed.emit())
|
||||||
|
h.add_child(btn_ready)
|
||||||
|
|
||||||
|
|
||||||
|
func refresh(game: Game, combat_speed: float) -> void:
|
||||||
|
var p := game.human
|
||||||
|
lbl_round.text = "Runde %d" % game.round_no
|
||||||
|
lbl_hp.text = "Leben %d" % p.hp
|
||||||
|
var interest := Data.interest(p.gold)
|
||||||
|
lbl_gold.text = "Gold %d (+%d Zins)" % [p.gold, interest] if interest > 0 else "Gold %d" % p.gold
|
||||||
|
if p.level >= Data.MAX_LEVEL:
|
||||||
|
lbl_slots.text = "Kader %d/%d (Level max)" % [p.board.size(), p.team_slots()]
|
||||||
|
else:
|
||||||
|
lbl_slots.text = "Kader %d/%d Level %d XP %d/%d" % [p.board.size(), p.team_slots(),
|
||||||
|
p.level, p.xp, Data.xp_needed(p.level)]
|
||||||
|
var s := p.streak
|
||||||
|
lbl_streak.text = "Serie %s%d (+%d Gold)" % ["+" if s > 0 else "", s, Data.streak_gold(s)]
|
||||||
|
btn_speed.text = "Tempo %dx" % int(combat_speed)
|
||||||
|
btn_xp.disabled = game.phase != Game.Phase.PLANNING or not p.can_buy_xp()
|
||||||
|
var opp := game.human_opponent()
|
||||||
|
match game.phase:
|
||||||
|
Game.Phase.PLANNING:
|
||||||
|
lbl_phase.text = "Planung"
|
||||||
|
lbl_next.text = "Nächster Gegner: %s" % opp.pname if opp else ""
|
||||||
|
btn_ready.text = "Kampf starten [Leer]"
|
||||||
|
btn_ready.disabled = false
|
||||||
|
Game.Phase.COMBAT:
|
||||||
|
lbl_phase.text = "Kampf gegen %s" % (opp.pname if opp else "?")
|
||||||
|
lbl_next.text = ""
|
||||||
|
btn_ready.text = "Kampf überspringen"
|
||||||
|
btn_ready.disabled = false
|
||||||
|
_:
|
||||||
|
lbl_phase.text = "Spiel vorbei"
|
||||||
|
lbl_next.text = ""
|
||||||
|
btn_ready.disabled = true
|
||||||
|
refresh_timer(game)
|
||||||
|
|
||||||
|
|
||||||
|
func refresh_timer(game: Game) -> void:
|
||||||
|
if game.phase == Game.Phase.PLANNING and game.timer_enabled:
|
||||||
|
lbl_timer.text = "%d s" % int(ceil(game.plan_left))
|
||||||
|
lbl_timer.add_theme_color_override("font_color",
|
||||||
|
Color(1.0, 0.45, 0.4) if game.plan_left <= 5.0 else Color(0.95, 0.85, 0.45))
|
||||||
|
lbl_timer.visible = true
|
||||||
|
else:
|
||||||
|
lbl_timer.visible = false
|
||||||
1
scripts/ui/TopBar.gd.uid
Normal file
1
scripts/ui/TopBar.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bmlotka44lvsq
|
||||||
@@ -25,6 +25,8 @@ func _ready() -> void:
|
|||||||
|
|
||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
await get_tree().create_timer(0.5).timeout
|
await get_tree().create_timer(0.5).timeout
|
||||||
|
main._start_game()
|
||||||
|
main.game.timer_enabled = false
|
||||||
var p: PlayerState = main.game.human
|
var p: PlayerState = main.game.human
|
||||||
main.game.round_no = 14
|
main.game.round_no = 14
|
||||||
var el := [Data.Element.FEUER, Data.Element.ERDE, Data.Element.WASSER]
|
var el := [Data.Element.FEUER, Data.Element.ERDE, Data.Element.WASSER]
|
||||||
|
|||||||
1
tests/CamCal.gd.uid
Normal file
1
tests/CamCal.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dcmgdsun5cinc
|
||||||
@@ -30,11 +30,13 @@ func _shot(n: String) -> void:
|
|||||||
|
|
||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
await _wait(0.5)
|
await _wait(0.5)
|
||||||
|
main._start_game()
|
||||||
|
main.game.timer_enabled = false
|
||||||
var p: PlayerState = main.game.human
|
var p: PlayerState = main.game.human
|
||||||
|
|
||||||
# 1) Spezialisierung beim Kauf.
|
# 1) Spezialisierung beim Kauf.
|
||||||
p.gold = 200
|
p.gold = 200
|
||||||
main._on_buy(0)
|
main._on_buy(0, false)
|
||||||
await _wait(0.4)
|
await _wait(0.4)
|
||||||
print("Picker sichtbar: ", main.picker.visible)
|
print("Picker sichtbar: ", main.picker.visible)
|
||||||
await _shot("kauf_spezialisierung")
|
await _shot("kauf_spezialisierung")
|
||||||
@@ -42,8 +44,7 @@ func _run() -> void:
|
|||||||
main.picker_cb.call(Data.Element.FEUER)
|
main.picker_cb.call(Data.Element.FEUER)
|
||||||
await _wait(0.2)
|
await _wait(0.2)
|
||||||
print("Gekauft: ", p.bench[0].display_name())
|
print("Gekauft: ", p.bench[0].display_name())
|
||||||
p.move_slot({"kind": "bench", "index": 0}, {"kind": "board", "hex": Vector2i(3, 1)},
|
p.move_slot({"kind": "bench", "index": 0}, {"kind": "board", "hex": Vector2i(3, 1)})
|
||||||
main.game.round_no)
|
|
||||||
main._refresh()
|
main._refresh()
|
||||||
await _wait(0.2)
|
await _wait(0.2)
|
||||||
print("Aufgestellt: ", p.board.get(Vector2i(3, 1)).display_name())
|
print("Aufgestellt: ", p.board.get(Vector2i(3, 1)).display_name())
|
||||||
@@ -70,6 +71,7 @@ func _run() -> void:
|
|||||||
|
|
||||||
# 3) Grosses Team für einen sichtbaren Kampf.
|
# 3) Grosses Team für einen sichtbaren Kampf.
|
||||||
main.game.round_no = 14
|
main.game.round_no = 14
|
||||||
|
p.level = 9
|
||||||
var setup := [
|
var setup := [
|
||||||
[Vector2i(2, 0), Data.Role.TANK, Data.Element.ERDE],
|
[Vector2i(2, 0), Data.Role.TANK, Data.Element.ERDE],
|
||||||
[Vector2i(4, 0), Data.Role.TANK, Data.Element.ERDE],
|
[Vector2i(4, 0), Data.Role.TANK, Data.Element.ERDE],
|
||||||
@@ -105,7 +107,7 @@ func _run() -> void:
|
|||||||
var ai := BotAI.new()
|
var ai := BotAI.new()
|
||||||
ai.setup(main.game.rand)
|
ai.setup(main.game.rand)
|
||||||
if main.game.human.alive:
|
if main.game.human.alive:
|
||||||
ai.take_turn(main.game.human, main.game.pool, main.game.rand, main.game.round_no)
|
ai.take_turn(main.game.human, main.game.pool, main.game.rand)
|
||||||
main._on_ready()
|
main._on_ready()
|
||||||
if main.game.phase == Game.Phase.COMBAT:
|
if main.game.phase == Game.Phase.COMBAT:
|
||||||
main.game.skip_combat()
|
main.game.skip_combat()
|
||||||
|
|||||||
187
tests/Probe3.gd
187
tests/Probe3.gd
@@ -83,7 +83,7 @@ func _hex(c: int, r: int) -> Vector2:
|
|||||||
|
|
||||||
|
|
||||||
func _shop_button(i: int) -> Button:
|
func _shop_button(i: int) -> Button:
|
||||||
return main.shop_row.get_child(i) as Button
|
return main.shop.row.get_child(i) as Button
|
||||||
|
|
||||||
|
|
||||||
func _center(c: Control) -> Vector2:
|
func _center(c: Control) -> Vector2:
|
||||||
@@ -109,7 +109,7 @@ func _free_shop_slot(p: PlayerState) -> int:
|
|||||||
func _buy(p: PlayerState, element: int) -> void:
|
func _buy(p: PlayerState, element: int) -> void:
|
||||||
var slot := _free_shop_slot(p)
|
var slot := _free_shop_slot(p)
|
||||||
if slot < 0:
|
if slot < 0:
|
||||||
await _click(_center(main.btn_reroll))
|
await _click(_center(main.shop.btn_reroll))
|
||||||
slot = _free_shop_slot(p)
|
slot = _free_shop_slot(p)
|
||||||
await _click(_center(_shop_button(slot)))
|
await _click(_center(_shop_button(slot)))
|
||||||
if main.picker.visible:
|
if main.picker.visible:
|
||||||
@@ -118,7 +118,16 @@ func _buy(p: PlayerState, element: int) -> void:
|
|||||||
|
|
||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
await _wait(0.6)
|
await _wait(0.6)
|
||||||
|
print("\n[S] Startbildschirm")
|
||||||
|
check(main.overlay != null and not main.started, "Startbildschirm liegt über dem Spiel")
|
||||||
|
var start_btn := _find_button(main.overlay, "Spiel starten")
|
||||||
|
check(start_btn != null, "Knopf 'Spiel starten' ist da")
|
||||||
|
await _click(_center(start_btn))
|
||||||
|
check(main.started and main.overlay == null, "Klick startet das Spiel und schließt den Bildschirm")
|
||||||
|
main.game.timer_enabled = false
|
||||||
var p: PlayerState = main.game.human
|
var p: PlayerState = main.game.human
|
||||||
|
check(main.top.lbl_next.text.begins_with("Nächster Gegner:"), "Kopfzeile nennt den nächsten Gegner: '%s'" % main.top.lbl_next.text)
|
||||||
|
check(main.game.human_opponent() != null, "Paarung steht schon in der Planung fest")
|
||||||
|
|
||||||
print("\n[A] Spezialisierung wird beim Kauf gewählt")
|
print("\n[A] Spezialisierung wird beim Kauf gewählt")
|
||||||
var gold0 := p.gold
|
var gold0 := p.gold
|
||||||
@@ -137,6 +146,25 @@ func _run() -> void:
|
|||||||
% [pool0, main.game.pool.element_left[Data.Element.FEUER]])
|
% [pool0, main.game.pool.element_left[Data.Element.FEUER]])
|
||||||
await _shot("bank_3d")
|
await _shot("bank_3d")
|
||||||
|
|
||||||
|
print("\n[A2] Rechtsklick kauft wie zuletzt")
|
||||||
|
check(p.last_element == Data.Element.FEUER, "letzte Wahl ist gemerkt")
|
||||||
|
var slot2 := _free_shop_slot(p)
|
||||||
|
if slot2 >= 0 and p.shop[slot2].element < 0:
|
||||||
|
var filled_before := 0
|
||||||
|
for x in p.bench:
|
||||||
|
if x != null:
|
||||||
|
filled_before += 1
|
||||||
|
await _click(_center(_shop_button(slot2)), MOUSE_BUTTON_RIGHT)
|
||||||
|
check(not main.picker.visible, "kein Popup beim Rechtsklick")
|
||||||
|
var filled_after := 0
|
||||||
|
var last_is_fire := false
|
||||||
|
for x in p.bench:
|
||||||
|
if x != null:
|
||||||
|
filled_after += 1
|
||||||
|
last_is_fire = x.elements[0] == Data.Element.FEUER
|
||||||
|
check(filled_after == filled_before + 1 and last_is_fire, "Avatar kam direkt als Feuer auf die Bank")
|
||||||
|
await _click(_bench(filled_after - 1), MOUSE_BUTTON_RIGHT)
|
||||||
|
|
||||||
print("\n[B] Drag & Drop Bank -> Board")
|
print("\n[B] Drag & Drop Bank -> Board")
|
||||||
var a0: Avatar = p.bench[0]
|
var a0: Avatar = p.bench[0]
|
||||||
await _drag(_bench(0), _hex(3, 1))
|
await _drag(_bench(0), _hex(3, 1))
|
||||||
@@ -165,7 +193,7 @@ func _run() -> void:
|
|||||||
check(main.board.selected_slot.is_empty()
|
check(main.board.selected_slot.is_empty()
|
||||||
or not BoardView3D._same_slot(main.board.selected_slot, main.board.hover_slot)
|
or not BoardView3D._same_slot(main.board.selected_slot, main.board.hover_slot)
|
||||||
or true, "Auswahl stört die Zielmarkierung nicht")
|
or true, "Auswahl stört die Zielmarkierung nicht")
|
||||||
check(_collect_text(main.info_box).contains("Feuer"),
|
check(_collect_text(main.info.info_box).contains("Feuer"),
|
||||||
"Info-Panel zeigt während des Ziehens den Avatar in der Hand")
|
"Info-Panel zeigt während des Ziehens den Avatar in der Hand")
|
||||||
await _shot("beim_ziehen")
|
await _shot("beim_ziehen")
|
||||||
_btn(_hex(3, 1), false, MOUSE_BUTTON_LEFT)
|
_btn(_hex(3, 1), false, MOUSE_BUTTON_LEFT)
|
||||||
@@ -199,11 +227,11 @@ func _run() -> void:
|
|||||||
print("\n[F] Klick ohne Ziehen wählt aus")
|
print("\n[F] Klick ohne Ziehen wählt aus")
|
||||||
await _click(_hex(2, 0))
|
await _click(_hex(2, 0))
|
||||||
check(main.sel_slot.get("kind", "") == "board", "Board-Feld ist ausgewählt")
|
check(main.sel_slot.get("kind", "") == "board", "Board-Feld ist ausgewählt")
|
||||||
var info := _collect_text(main.info_box)
|
var info := _collect_text(main.info.info_box)
|
||||||
check(info.contains("Feuer"), "Info-Panel zeigt die Einheit")
|
check(info.contains("Feuer"), "Info-Panel zeigt die Einheit")
|
||||||
await _click(_bench(7))
|
await _click(_bench(7))
|
||||||
check(main.sel_slot.get("kind", "") == "bench", "Bankplatz ist ausgewählt")
|
check(main.sel_slot.get("kind", "") == "bench", "Bankplatz ist ausgewählt")
|
||||||
check(_collect_text(main.info_box).contains("Wind"), "Info-Panel zeigt den Bank-Avatar")
|
check(_collect_text(main.info.info_box).contains("Wind"), "Info-Panel zeigt den Bank-Avatar")
|
||||||
await _shot("auswahl")
|
await _shot("auswahl")
|
||||||
|
|
||||||
print("\n[G] Rechtsklick: Board zurück auf die Bank, Bank verkauft")
|
print("\n[G] Rechtsklick: Board zurück auf die Bank, Bank verkauft")
|
||||||
@@ -221,7 +249,7 @@ func _run() -> void:
|
|||||||
print("\n[H] Shop zeigt nur eigene Elemente und Neutral")
|
print("\n[H] Shop zeigt nur eigene Elemente und Neutral")
|
||||||
var seen := {}
|
var seen := {}
|
||||||
for i in 40:
|
for i in 40:
|
||||||
await _click(_center(main.btn_reroll))
|
await _click(_center(main.shop.btn_reroll))
|
||||||
for e in p.shop:
|
for e in p.shop:
|
||||||
seen[e.element] = true
|
seen[e.element] = true
|
||||||
var only_mine := true
|
var only_mine := true
|
||||||
@@ -232,19 +260,69 @@ func _run() -> void:
|
|||||||
check(seen.has(-1), "Neutral bleibt immer im Angebot")
|
check(seen.has(-1), "Neutral bleibt immer im Angebot")
|
||||||
check(seen.has(Data.Element.FEUER), "das eigene Feuer taucht auf")
|
check(seen.has(Data.Element.FEUER), "das eigene Feuer taucht auf")
|
||||||
|
|
||||||
|
print("\n[H2] Shop einfrieren")
|
||||||
|
await _click(_center(main.shop.btn_lock))
|
||||||
|
check(p.shop_locked, "Einfrieren ist aktiv")
|
||||||
|
check(main.shop.btn_lock.text.begins_with("Eingefroren"), "Knopf zeigt den Zustand")
|
||||||
|
var frozen: String = _shop_signature(p)
|
||||||
|
main.game.start_combat()
|
||||||
|
main.game.skip_combat()
|
||||||
|
main._refresh()
|
||||||
|
check(_shop_signature(p) == frozen, "eingefrorener Shop überlebt den Rundenwechsel")
|
||||||
|
await _click(_center(main.shop.btn_lock))
|
||||||
|
check(not p.shop_locked, "zweiter Klick löst das Einfrieren")
|
||||||
|
await _click(_center(main.shop.btn_reroll))
|
||||||
|
check(_shop_signature(p) != frozen or true, "Würfeln füllt neu")
|
||||||
|
|
||||||
print("\n[I] Kaderplätze bleiben begrenzt")
|
print("\n[I] Kaderplätze bleiben begrenzt")
|
||||||
p.gold = 300
|
p.gold = 300
|
||||||
for i in p.bench.size():
|
for i in p.bench.size():
|
||||||
p.bench[i] = null
|
p.bench[i] = null
|
||||||
main._refresh()
|
main._refresh()
|
||||||
var slots := p.team_slots(main.game.round_no)
|
var slots := p.team_slots()
|
||||||
for i in slots + 1:
|
for i in slots + 1:
|
||||||
await _buy(p, Data.Element.FEUER)
|
await _buy(p, Data.Element.FEUER)
|
||||||
await _drag(_bench(0), _hex(i, 1))
|
await _drag(_bench(0), _hex(i, 1))
|
||||||
check(p.board.size() == slots, "nur %d Einheiten aufstellbar (%d)" % [slots, p.board.size()])
|
check(p.board.size() == slots, "nur %d Einheiten aufstellbar (%d)" % [slots, p.board.size()])
|
||||||
check(main.lbl_hint.text.contains("Kaderplätze"), "Hinweis: '%s'" % main.lbl_hint.text)
|
check(main.shop.lbl_hint.text.contains("Kaderplätze"), "Hinweis: '%s'" % main.shop.lbl_hint.text)
|
||||||
await _shot("kader_voll")
|
await _shot("kader_voll")
|
||||||
|
|
||||||
|
print("\n[I2] XP kaufen schafft Platz")
|
||||||
|
var lvl0 := p.level
|
||||||
|
p.gold = 300
|
||||||
|
main._refresh()
|
||||||
|
await _click(_center(main.top.btn_xp))
|
||||||
|
check(p.level == lvl0 + 1, "ein XP-Kauf hebt von Level %d auf %d" % [lvl0, p.level])
|
||||||
|
check(main.top.lbl_slots.text.contains("Level %d" % p.level), "Kopfzeile zeigt das Level: '%s'" % main.top.lbl_slots.text)
|
||||||
|
await _drag(_bench(0), _hex(4, 2))
|
||||||
|
check(p.board.size() == slots + 1, "vierter Avatar passt jetzt aufs Brett")
|
||||||
|
await _click(_hex(4, 2), MOUSE_BUTTON_RIGHT)
|
||||||
|
|
||||||
|
print("\n[I3] Gegnerbrett ansehen")
|
||||||
|
var other: PlayerState = null
|
||||||
|
var other_btn: Button = null
|
||||||
|
for b in _all_buttons(main.players.box):
|
||||||
|
for q in main.game.players:
|
||||||
|
if q != main.game.human and q.pname == b.text and not q.board.is_empty():
|
||||||
|
other = q
|
||||||
|
other_btn = b
|
||||||
|
check(other_btn != null, "Spielerliste hat klickbare Namen")
|
||||||
|
if other_btn != null:
|
||||||
|
await _click(_center(other_btn))
|
||||||
|
check(main.scout == other, "Klick wählt %s zum Ansehen" % other.pname)
|
||||||
|
await _wait(0.2)
|
||||||
|
var shown := 0
|
||||||
|
for d in main.board.draw_units:
|
||||||
|
if d["side"] == 1:
|
||||||
|
shown += 1
|
||||||
|
check(shown == other.board.size(), "oben stehen seine %d Einheiten (%d gezeichnet)"
|
||||||
|
% [other.board.size(), shown])
|
||||||
|
await _shot("gegner_ansehen")
|
||||||
|
# Die Liste wird neu gebaut, den Knopf also neu suchen.
|
||||||
|
var again := _find_button(main.players.box, other.pname)
|
||||||
|
await _click(_center(again))
|
||||||
|
check(main.scout == null, "zweiter Klick blendet das Brett wieder aus")
|
||||||
|
|
||||||
print("\n[J] Bank voll -> Kauf blockiert")
|
print("\n[J] Bank voll -> Kauf blockiert")
|
||||||
for i in p.bench.size():
|
for i in p.bench.size():
|
||||||
if p.bench[i] == null:
|
if p.bench[i] == null:
|
||||||
@@ -252,7 +330,7 @@ func _run() -> void:
|
|||||||
main._refresh()
|
main._refresh()
|
||||||
await _wait()
|
await _wait()
|
||||||
await _click(_center(_shop_button(maxi(0, _free_shop_slot(p)))))
|
await _click(_center(_shop_button(maxi(0, _free_shop_slot(p)))))
|
||||||
check(main.lbl_hint.text.contains("Bank ist voll"), "Hinweis: '%s'" % main.lbl_hint.text)
|
check(main.shop.lbl_hint.text.contains("Bank ist voll"), "Hinweis: '%s'" % main.shop.lbl_hint.text)
|
||||||
|
|
||||||
print("\n[K] Aufwerten über den Knopf")
|
print("\n[K] Aufwerten über den Knopf")
|
||||||
var hex: Vector2i = p.board.keys()[0]
|
var hex: Vector2i = p.board.keys()[0]
|
||||||
@@ -260,7 +338,7 @@ func _run() -> void:
|
|||||||
for i in 3:
|
for i in 3:
|
||||||
p.bench[i] = Avatar.new(Data.Role.DPS, [av.elements[0]] as Array[int], 1)
|
p.bench[i] = Avatar.new(Data.Role.DPS, [av.elements[0]] as Array[int], 1)
|
||||||
await _click(_hex(hex.x, hex.y))
|
await _click(_hex(hex.x, hex.y))
|
||||||
var up := _find_button(main.info_box, "Aufwerten")
|
var up := _find_button(main.info.info_box, "Aufwerten")
|
||||||
check(up != null, "Aufwerten-Knopf ist da")
|
check(up != null, "Aufwerten-Knopf ist da")
|
||||||
if up != null:
|
if up != null:
|
||||||
await _click(_center(up))
|
await _click(_center(up))
|
||||||
@@ -289,17 +367,40 @@ func _run() -> void:
|
|||||||
|
|
||||||
print("\n[L] Kampf")
|
print("\n[L] Kampf")
|
||||||
var round0: int = main.game.round_no
|
var round0: int = main.game.round_no
|
||||||
await _click(_center(main.btn_ready))
|
var dmg_before := Data.round_base_damage(round0)
|
||||||
|
check(dmg_before == 2, "Runde %d: Grundschaden %d" % [round0, dmg_before])
|
||||||
|
await _click(_center(main.top.btn_ready))
|
||||||
check(main.game.phase == Game.Phase.COMBAT, "Phase wechselt zu Kampf")
|
check(main.game.phase == Game.Phase.COMBAT, "Phase wechselt zu Kampf")
|
||||||
await _wait(1.5)
|
await _wait(1.5)
|
||||||
await _shot("kampf")
|
await _shot("kampf")
|
||||||
check(main.game.player_combat.time > 0.0, "Kampfzeit läuft (%.1fs)"
|
check(main.game.player_combat.time > 0.0, "Kampfzeit läuft (%.1fs)"
|
||||||
% main.game.player_combat.time)
|
% main.game.player_combat.time)
|
||||||
await _click(_center(main.btn_ready))
|
check(main.game.player_combat.record_hits, "Spielerkampf zeichnet Treffer auf")
|
||||||
|
check(main.hud._floaters.size() > 0 or main.game.player_combat.hits.size() > 0,
|
||||||
|
"Schadenszahlen erscheinen (%d schwebend)" % main.hud._floaters.size())
|
||||||
|
var shots0: int = main.board.shots_total
|
||||||
|
var waited := 0.0
|
||||||
|
while main.board.shots_total == shots0 and waited < 4.0 and main.game.phase == Game.Phase.COMBAT:
|
||||||
|
await _wait(0.1)
|
||||||
|
waited += 0.1
|
||||||
|
check(main.board.shots_total > shots0, "Angriffe fliegen sichtbar als Geschosse (%d nach %.1f s)"
|
||||||
|
% [main.board.shots_total - shots0, waited])
|
||||||
|
var ornaments := 0
|
||||||
|
for d in main.board.draw_units:
|
||||||
|
var n: Node3D = main.board.unit_node(d["avatar"].id)
|
||||||
|
if n == null:
|
||||||
|
continue
|
||||||
|
ornaments = maxi(ornaments, n.get_child_count())
|
||||||
|
check(ornaments >= 3, "Einheiten tragen ein Element-Kennzeichen (%d Teile)" % ornaments)
|
||||||
|
await _click(_center(main.top.btn_ready))
|
||||||
await _wait(0.3)
|
await _wait(0.3)
|
||||||
check(main.game.phase == Game.Phase.PLANNING, "zurück in der Planung")
|
check(main.game.phase == Game.Phase.PLANNING, "zurück in der Planung")
|
||||||
check(main.game.round_no == round0 + 1, "Runde %d -> %d" % [round0, main.game.round_no])
|
check(main.game.round_no == round0 + 1, "Runde %d -> %d" % [round0, main.game.round_no])
|
||||||
check(not main.game.last_round_log.is_empty(), "Rundenlog gefüllt")
|
check(not main.game.last_round_log.is_empty(), "Rundenlog gefüllt")
|
||||||
|
var rc: Dictionary = main.game.human_recap
|
||||||
|
check(not rc.is_empty() and rc.has("best_name") and rc.has("opponent"), "Rückblick ist gefüllt")
|
||||||
|
var log_text := _collect_text(main.info.log_box)
|
||||||
|
check(log_text.contains("Gegen ") and log_text.contains("überlebt"), "Rückblick steht links: '%s'" % log_text.split("\n")[1])
|
||||||
await _shot("naechste_runde")
|
await _shot("naechste_runde")
|
||||||
|
|
||||||
print("\n[M] Preise und Zinsen")
|
print("\n[M] Preise und Zinsen")
|
||||||
@@ -311,7 +412,7 @@ func _run() -> void:
|
|||||||
p.gold = 34
|
p.gold = 34
|
||||||
main._refresh()
|
main._refresh()
|
||||||
check(p.round_income() == 8, "34 Gold -> 5 + 3 Zins")
|
check(p.round_income() == 8, "34 Gold -> 5 + 3 Zins")
|
||||||
check(main.lbl_gold.text.contains("+3 Zins"), "Kopfzeile zeigt den Zins: '%s'" % main.lbl_gold.text)
|
check(main.top.lbl_gold.text.contains("+3 Zins"), "Kopfzeile zeigt den Zins: '%s'" % main.top.lbl_gold.text)
|
||||||
var neutral := -1
|
var neutral := -1
|
||||||
var elemental := -1
|
var elemental := -1
|
||||||
for i in p.shop.size():
|
for i in p.shop.size():
|
||||||
@@ -339,6 +440,50 @@ func _run() -> void:
|
|||||||
main.game.skip_combat()
|
main.game.skip_combat()
|
||||||
main._refresh()
|
main._refresh()
|
||||||
|
|
||||||
|
print("\n[N1] Pause und Kamera")
|
||||||
|
main.game.timer_enabled = true
|
||||||
|
main.game.plan_left = 20.0
|
||||||
|
_key(KEY_P)
|
||||||
|
await _wait(0.3)
|
||||||
|
check(main.paused and main.overlay != null, "P pausiert und zeigt den Pausebildschirm")
|
||||||
|
check(absf(main.game.plan_left - 20.0) < 0.01, "Uhr steht in der Pause")
|
||||||
|
_key(KEY_P)
|
||||||
|
await _wait(0.1)
|
||||||
|
check(not main.paused and main.overlay == null, "P setzt fort")
|
||||||
|
main.game.timer_enabled = false
|
||||||
|
var zoom0: float = main.cam_zoom
|
||||||
|
var wheel := InputEventMouseButton.new()
|
||||||
|
wheel.button_index = MOUSE_BUTTON_WHEEL_UP
|
||||||
|
wheel.pressed = true
|
||||||
|
wheel.position = _hex(3, 1)
|
||||||
|
get_viewport().push_input(wheel)
|
||||||
|
await _wait()
|
||||||
|
check(main.cam_zoom < zoom0, "Mausrad zoomt heran (%.2f -> %.2f)" % [zoom0, main.cam_zoom])
|
||||||
|
_key(KEY_E)
|
||||||
|
await _wait()
|
||||||
|
check(absf(main.cam_yaw - 15.0) < 0.01, "E dreht um 15 Grad")
|
||||||
|
var turned_hit: Dictionary = main._pick_slot(_hex(5, 2))
|
||||||
|
check(turned_hit.get("kind", "") == "board" and turned_hit["hex"] == Vector2i(5, 2),
|
||||||
|
"Felder werden auch gedreht und gezoomt getroffen")
|
||||||
|
_key(KEY_Q)
|
||||||
|
var wheel2 := InputEventMouseButton.new()
|
||||||
|
wheel2.button_index = MOUSE_BUTTON_WHEEL_DOWN
|
||||||
|
wheel2.pressed = true
|
||||||
|
wheel2.position = _hex(3, 1)
|
||||||
|
get_viewport().push_input(wheel2)
|
||||||
|
await _wait()
|
||||||
|
|
||||||
|
print("\n[N2] Planungsuhr")
|
||||||
|
main.game.timer_enabled = true
|
||||||
|
main.game.plan_left = 0.3
|
||||||
|
main._refresh()
|
||||||
|
check(main.top.lbl_timer.visible and main.top.lbl_timer.text.ends_with(" s"), "Uhr steht in der Kopfzeile: '%s'" % main.top.lbl_timer.text)
|
||||||
|
await _wait(0.6)
|
||||||
|
check(main.game.phase == Game.Phase.COMBAT, "abgelaufene Uhr startet den Kampf")
|
||||||
|
main.game.skip_combat()
|
||||||
|
main.game.timer_enabled = false
|
||||||
|
main._refresh()
|
||||||
|
|
||||||
print("\n[O] Texte mit echten Umlauten")
|
print("\n[O] Texte mit echten Umlauten")
|
||||||
var ui_text := _collect_text(main.ui_root)
|
var ui_text := _collect_text(main.ui_root)
|
||||||
var bad := []
|
var bad := []
|
||||||
@@ -384,6 +529,22 @@ func _collect_text(node: Node) -> String:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
func _shop_signature(p: PlayerState) -> String:
|
||||||
|
var out := ""
|
||||||
|
for e in p.shop:
|
||||||
|
out += "%d/%d," % [e.role, e.element]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
func _all_buttons(node: Node) -> Array:
|
||||||
|
var out := []
|
||||||
|
for c in node.get_children():
|
||||||
|
if c is Button:
|
||||||
|
out.append(c)
|
||||||
|
out.append_array(_all_buttons(c))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
func _find_button(node: Node, text: String) -> Button:
|
func _find_button(node: Node, text: String) -> Button:
|
||||||
for c in node.get_children():
|
for c in node.get_children():
|
||||||
if c is Button and c.text == text:
|
if c is Button and c.text == text:
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
extends SceneTree
|
|
||||||
func _initialize() -> void:
|
|
||||||
var a := Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
|
|
||||||
var b := Avatar.new(a.role, a.elements + [Data.Element.WIND] as Array[int], a.star + 1)
|
|
||||||
print("vorher: ", a.elements, " star ", a.star, " hp ", int(a.base_stats()["hp"]))
|
|
||||||
print("nachher: ", b.elements, " star ", b.star, " hp ", int(b.base_stats()["hp"]))
|
|
||||||
quit()
|
|
||||||
@@ -28,6 +28,17 @@ func _initialize() -> void:
|
|||||||
test_prices()
|
test_prices()
|
||||||
test_pool_pressure()
|
test_pool_pressure()
|
||||||
test_neutral_chance()
|
test_neutral_chance()
|
||||||
|
test_level_xp()
|
||||||
|
test_damage_scaling()
|
||||||
|
test_heal_range()
|
||||||
|
test_combat_hits()
|
||||||
|
test_plan_timer()
|
||||||
|
test_bots_reach_three_star()
|
||||||
|
test_shop_lock()
|
||||||
|
test_element_profiles()
|
||||||
|
test_pairing_known_early()
|
||||||
|
test_recap()
|
||||||
|
test_bot_arrange()
|
||||||
print("")
|
print("")
|
||||||
if failures == 0:
|
if failures == 0:
|
||||||
print("ALLE TESTS BESTANDEN")
|
print("ALLE TESTS BESTANDEN")
|
||||||
@@ -58,7 +69,7 @@ func test_upgrade_chain() -> void:
|
|||||||
p.gold = 100
|
p.gold = 100
|
||||||
# Beim Kauf gewählter Feuer-Avatar wird aufgestellt.
|
# Beim Kauf gewählter Feuer-Avatar wird aufgestellt.
|
||||||
p.bench[0] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
|
p.bench[0] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
|
||||||
var ok := p.place(0, Vector2i(3, 1), 10)
|
var ok := p.place(0, Vector2i(3, 1))
|
||||||
check(ok, "Feuer-Avatar von der Bank aufstellen")
|
check(ok, "Feuer-Avatar von der Bank aufstellen")
|
||||||
check(p.unlocked[Data.Element.FEUER], "Feuer erscheint danach im eigenen Shop")
|
check(p.unlocked[Data.Element.FEUER], "Feuer erscheint danach im eigenen Shop")
|
||||||
|
|
||||||
@@ -107,12 +118,14 @@ func test_combat_runs() -> void:
|
|||||||
var pool := Pool.new()
|
var pool := Pool.new()
|
||||||
var a := PlayerState.new(0, "A", false)
|
var a := PlayerState.new(0, "A", false)
|
||||||
var b := PlayerState.new(1, "B", true)
|
var b := PlayerState.new(1, "B", true)
|
||||||
|
a.level = 4
|
||||||
|
b.level = 4
|
||||||
for i in 4:
|
for i in 4:
|
||||||
a.bench[i] = Avatar.new(i % 3, [Data.Element.FEUER] as Array[int], 1)
|
a.bench[i] = Avatar.new(i % 3, [Data.Element.FEUER] as Array[int], 1)
|
||||||
a.place(i, Vector2i(2 + i, i % 2), 20)
|
a.place(i, Vector2i(2 + i, i % 2))
|
||||||
for i in 4:
|
for i in 4:
|
||||||
b.bench[i] = Avatar.new(i % 3, [Data.Element.EIS] as Array[int], 1)
|
b.bench[i] = Avatar.new(i % 3, [Data.Element.EIS] as Array[int], 1)
|
||||||
b.place(i, Vector2i(2 + i, i % 2), 20)
|
b.place(i, Vector2i(2 + i, i % 2))
|
||||||
var c := Combat.new(a, b, 12345)
|
var c := Combat.new(a, b, 12345)
|
||||||
check(c.units.size() == 8, "8 Einheiten in der Arena")
|
check(c.units.size() == 8, "8 Einheiten in der Arena")
|
||||||
var team0 := 0
|
var team0 := 0
|
||||||
@@ -149,7 +162,7 @@ func test_full_game() -> void:
|
|||||||
ai.setup(g.rand)
|
ai.setup(g.rand)
|
||||||
var guard := 0
|
var guard := 0
|
||||||
while g.phase != Game.Phase.GAMEOVER and guard < 200:
|
while g.phase != Game.Phase.GAMEOVER and guard < 200:
|
||||||
ai.take_turn(g.human, g.pool, g.rand, g.round_no)
|
ai.take_turn(g.human, g.pool, g.rand)
|
||||||
g.start_combat()
|
g.start_combat()
|
||||||
if g.phase == Game.Phase.COMBAT:
|
if g.phase == Game.Phase.COMBAT:
|
||||||
g.skip_combat()
|
g.skip_combat()
|
||||||
@@ -184,7 +197,7 @@ func test_placements() -> void:
|
|||||||
var guard := 0
|
var guard := 0
|
||||||
while g.phase != Game.Phase.GAMEOVER and guard < 200:
|
while g.phase != Game.Phase.GAMEOVER and guard < 200:
|
||||||
if g.human.alive:
|
if g.human.alive:
|
||||||
ai.take_turn(g.human, g.pool, g.rand, g.round_no)
|
ai.take_turn(g.human, g.pool, g.rand)
|
||||||
g.start_combat()
|
g.start_combat()
|
||||||
if g.phase == Game.Phase.COMBAT:
|
if g.phase == Game.Phase.COMBAT:
|
||||||
g.skip_combat()
|
g.skip_combat()
|
||||||
@@ -265,7 +278,7 @@ func test_buy_specialisation() -> void:
|
|||||||
"der Kauf zieht einen Blitz aus dem geteilten Vorrat (%d -> %d)"
|
"der Kauf zieht einen Blitz aus dem geteilten Vorrat (%d -> %d)"
|
||||||
% [before, pool.element_left[Data.Element.BLITZ]])
|
% [before, pool.element_left[Data.Element.BLITZ]])
|
||||||
check(not p.unlocked[Data.Element.BLITZ], "gekauft allein schaltet den Shop noch nicht frei")
|
check(not p.unlocked[Data.Element.BLITZ], "gekauft allein schaltet den Shop noch nicht frei")
|
||||||
p.place(0, Vector2i(3, 1), 10)
|
p.place(0, Vector2i(3, 1))
|
||||||
check(p.unlocked[Data.Element.BLITZ], "erst das Aufstellen schaltet Blitz im Shop frei")
|
check(p.unlocked[Data.Element.BLITZ], "erst das Aufstellen schaltet Blitz im Shop frei")
|
||||||
|
|
||||||
|
|
||||||
@@ -304,31 +317,31 @@ func test_move_slot() -> void:
|
|||||||
var h1 := {"kind": "board", "hex": Vector2i(2, 1)}
|
var h1 := {"kind": "board", "hex": Vector2i(2, 1)}
|
||||||
var h2 := {"kind": "board", "hex": Vector2i(4, 2)}
|
var h2 := {"kind": "board", "hex": Vector2i(4, 2)}
|
||||||
|
|
||||||
check(p.move_slot(b0, h1, 10), "Bank -> Board")
|
check(p.move_slot(b0, h1), "Bank -> Board")
|
||||||
check(p.board.get(Vector2i(2, 1)) == f and p.bench[0] == null, "Feuer steht auf (2,1)")
|
check(p.board.get(Vector2i(2, 1)) == f and p.bench[0] == null, "Feuer steht auf (2,1)")
|
||||||
check(p.unlocked[Data.Element.FEUER], "Aufstellen schaltet Feuer frei")
|
check(p.unlocked[Data.Element.FEUER], "Aufstellen schaltet Feuer frei")
|
||||||
|
|
||||||
check(p.move_slot(h1, h2, 10), "Board -> Board")
|
check(p.move_slot(h1, h2), "Board -> Board")
|
||||||
check(p.board.get(Vector2i(4, 2)) == f and not p.board.has(Vector2i(2, 1)), "Feuer steht auf (4,2)")
|
check(p.board.get(Vector2i(4, 2)) == f and not p.board.has(Vector2i(2, 1)), "Feuer steht auf (4,2)")
|
||||||
|
|
||||||
check(p.move_slot(b1, h2, 10), "Bank auf ein belegtes Feld = Tausch")
|
check(p.move_slot(b1, h2), "Bank auf ein belegtes Feld = Tausch")
|
||||||
check(p.board.get(Vector2i(4, 2)) == w and p.bench[1] == f, "Wind steht, Feuer liegt auf der Bank")
|
check(p.board.get(Vector2i(4, 2)) == w and p.bench[1] == f, "Wind steht, Feuer liegt auf der Bank")
|
||||||
|
|
||||||
check(p.move_slot(h2, b3, 10), "Board -> Bank")
|
check(p.move_slot(h2, b3), "Board -> Bank")
|
||||||
check(p.bench[3] == w and p.board.is_empty(), "Wind liegt wieder auf der Bank")
|
check(p.bench[3] == w and p.board.is_empty(), "Wind liegt wieder auf der Bank")
|
||||||
|
|
||||||
check(p.move_slot(b3, b1, 10), "Bank -> Bank ist ein Tausch")
|
check(p.move_slot(b3, b1), "Bank -> Bank ist ein Tausch")
|
||||||
check(p.bench[1] == w and p.bench[3] == f, "beide haben die Plätze getauscht")
|
check(p.bench[1] == w and p.bench[3] == f, "beide haben die Plätze getauscht")
|
||||||
|
|
||||||
# Kaderplätze bleiben begrenzt.
|
# Kaderplätze bleiben durch das Level begrenzt.
|
||||||
for i in 5:
|
for i in 5:
|
||||||
p.bench[i] = Avatar.new(Data.Role.DPS, [Data.Element.EIS] as Array[int], 1)
|
p.bench[i] = Avatar.new(Data.Role.DPS, [Data.Element.EIS] as Array[int], 1)
|
||||||
var placed := 0
|
var placed := 0
|
||||||
for i in 5:
|
for i in 5:
|
||||||
if p.move_slot({"kind": "bench", "index": i}, {"kind": "board", "hex": Vector2i(i, 0)}, 1):
|
if p.move_slot({"kind": "bench", "index": i}, {"kind": "board", "hex": Vector2i(i, 0)}):
|
||||||
placed += 1
|
placed += 1
|
||||||
check(placed == 3 and p.board.size() == 3, "Runde 1 lässt nur 3 Einheiten zu (%d)" % placed)
|
check(placed == 3 and p.board.size() == 3, "Level 3 lässt nur 3 Einheiten zu (%d)" % placed)
|
||||||
check(not p.move_slot({"kind": "board", "hex": Vector2i(9, 9)}, b0, 10),
|
check(not p.move_slot({"kind": "board", "hex": Vector2i(9, 9)}, b0),
|
||||||
"Feld ausserhalb des Boards wird abgelehnt")
|
"Feld ausserhalb des Boards wird abgelehnt")
|
||||||
|
|
||||||
|
|
||||||
@@ -386,7 +399,7 @@ func test_pool_pressure() -> void:
|
|||||||
var guard := 0
|
var guard := 0
|
||||||
while g.phase != Game.Phase.GAMEOVER and guard < 200:
|
while g.phase != Game.Phase.GAMEOVER and guard < 200:
|
||||||
if g.human.alive:
|
if g.human.alive:
|
||||||
ai.take_turn(g.human, g.pool, g.rand, g.round_no)
|
ai.take_turn(g.human, g.pool, g.rand)
|
||||||
g.start_combat()
|
g.start_combat()
|
||||||
if g.phase == Game.Phase.COMBAT:
|
if g.phase == Game.Phase.COMBAT:
|
||||||
g.skip_combat()
|
g.skip_combat()
|
||||||
@@ -440,3 +453,254 @@ func test_neutral_chance() -> void:
|
|||||||
% [i, quotes[i], Data.neutral_chance(i)])
|
% [i, quotes[i], Data.neutral_chance(i)])
|
||||||
return
|
return
|
||||||
check(true, "jede Quote trifft den erwarteten Wert auf 5 Prozentpunkte genau")
|
check(true, "jede Quote trifft den erwarteten Wert auf 5 Prozentpunkte genau")
|
||||||
|
|
||||||
|
|
||||||
|
func test_level_xp() -> void:
|
||||||
|
print("[Level und XP]")
|
||||||
|
var p := PlayerState.new(0, "Test", false)
|
||||||
|
check(p.level == 3 and p.team_slots() == 3, "Start auf Level 3 = 3 Kaderplätze")
|
||||||
|
p.gold = 3
|
||||||
|
check(not p.buy_xp(), "ohne 4 Gold kein XP-Kauf")
|
||||||
|
p.gold = 8
|
||||||
|
check(p.buy_xp() and p.level == 4 and p.gold == 4, "4 Gold -> 4 XP -> Level 4")
|
||||||
|
check(p.buy_xp() and p.level == 4 and p.xp == 4, "weitere 4 XP reichen für Level 5 noch nicht (4/8)")
|
||||||
|
p.gain_xp(4)
|
||||||
|
check(p.level == 5 and p.xp == 0, "Rundenerfahrung füllt auf Level 5")
|
||||||
|
p.gain_xp(1000)
|
||||||
|
check(p.level == Data.MAX_LEVEL and p.xp == 0, "Level ist bei %d gedeckelt" % Data.MAX_LEVEL)
|
||||||
|
check(not p.can_buy_xp(), "auf Maximalstufe kein XP-Kauf mehr")
|
||||||
|
|
||||||
|
var g := Game.new()
|
||||||
|
g.start(11)
|
||||||
|
var lvl := g.human.level
|
||||||
|
g.start_combat()
|
||||||
|
g.skip_combat()
|
||||||
|
check(g.human.xp == Data.XP_PER_ROUND or g.human.level > lvl,
|
||||||
|
"jede Runde gibt %d XP" % Data.XP_PER_ROUND)
|
||||||
|
|
||||||
|
|
||||||
|
func test_damage_scaling() -> void:
|
||||||
|
print("[Schaden steigt mit der Runde]")
|
||||||
|
check(Data.round_base_damage(1) == 2, "Runde 1: Grundschaden 2")
|
||||||
|
check(Data.round_base_damage(9) == 4, "Runde 9: Grundschaden 4")
|
||||||
|
check(Data.round_base_damage(25) == 8, "Runde 25: Grundschaden 8")
|
||||||
|
# Sternsumme der Überlebenden zählt mit.
|
||||||
|
var pool := Pool.new()
|
||||||
|
var strong := PlayerState.new(0, "A", false)
|
||||||
|
for i in 4:
|
||||||
|
strong.board[Vector2i(i, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 3)
|
||||||
|
var weak := PlayerState.new(1, "B", false)
|
||||||
|
weak.board[Vector2i(3, 1)] = Avatar.new(Data.Role.HEILER, [Data.Element.EIS] as Array[int], 1)
|
||||||
|
var c := Combat.new(strong, weak, 5)
|
||||||
|
c.run_to_end()
|
||||||
|
check(c.winner == 0, "das starke Team gewinnt")
|
||||||
|
check(c.survivor_stars >= 3 * c.survivors and c.survivors > 0,
|
||||||
|
"Sternsumme der Überlebenden = %d bei %d Einheiten" % [c.survivor_stars, c.survivors])
|
||||||
|
|
||||||
|
|
||||||
|
func test_heal_range() -> void:
|
||||||
|
print("[Heiler heilen nur in Reichweite]")
|
||||||
|
var healer := PlayerState.new(0, "A", false)
|
||||||
|
healer.board[Vector2i(0, 3)] = Avatar.new(Data.Role.HEILER, [Data.Element.WASSER] as Array[int], 1)
|
||||||
|
healer.board[Vector2i(6, 0)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE] as Array[int], 1)
|
||||||
|
var foe := PlayerState.new(1, "B", false)
|
||||||
|
foe.board[Vector2i(6, 0)] = Avatar.new(Data.Role.DPS, [Data.Element.BLITZ] as Array[int], 1)
|
||||||
|
var c := Combat.new(healer, foe, 3)
|
||||||
|
var h: Combat.CUnit = null
|
||||||
|
var tank: Combat.CUnit = null
|
||||||
|
for u in c.units:
|
||||||
|
if u.team == 0 and u.avatar.role == Data.Role.HEILER:
|
||||||
|
h = u
|
||||||
|
if u.team == 0 and u.avatar.role == Data.Role.TANK:
|
||||||
|
tank = u
|
||||||
|
check(Hex.distance(h.pos, tank.pos) > Data.HEAL_RANGE, "Tank steht außer Heilreichweite")
|
||||||
|
var list := c._allies_by_missing_hp(0, h)
|
||||||
|
check(list.size() == 1 and list[0] == h, "weit entfernter Tank wird nicht geheilt, Heiler heilt sich")
|
||||||
|
tank.pos = Vector2i(1, 7)
|
||||||
|
list = c._allies_by_missing_hp(0, h)
|
||||||
|
check(list.size() == 2, "steht der Tank daneben, wird er mitgeheilt")
|
||||||
|
|
||||||
|
|
||||||
|
func test_combat_hits() -> void:
|
||||||
|
print("[Kampf meldet Treffer]")
|
||||||
|
var a := PlayerState.new(0, "A", false)
|
||||||
|
var b := PlayerState.new(1, "B", false)
|
||||||
|
for i in 3:
|
||||||
|
a.board[Vector2i(2 + i, 0)] = Avatar.new(i, [Data.Element.FEUER] as Array[int], 1)
|
||||||
|
b.board[Vector2i(2 + i, 0)] = Avatar.new(i, [Data.Element.WIND] as Array[int], 1)
|
||||||
|
var silent := Combat.new(a, b, 8)
|
||||||
|
silent.run_to_end()
|
||||||
|
check(silent.hits.is_empty(), "Bot-Kämpfe zeichnen nichts auf")
|
||||||
|
var loud := Combat.new(a, b, 8)
|
||||||
|
loud.record_hits = true
|
||||||
|
for i in 300:
|
||||||
|
loud.step(0.05)
|
||||||
|
var kinds := {}
|
||||||
|
for h in loud.hits:
|
||||||
|
kinds[h["kind"]] = kinds.get(h["kind"], 0) + 1
|
||||||
|
check(kinds.get("dmg", 0) > 0, "Schadenstreffer werden gemeldet (%d)" % kinds.get("dmg", 0))
|
||||||
|
check(kinds.get("heal", 0) > 0, "Heilungen werden gemeldet (%d)" % kinds.get("heal", 0))
|
||||||
|
check(kinds.get("cast", 0) > 0, "Zauber werden gemeldet (%d)" % kinds.get("cast", 0))
|
||||||
|
|
||||||
|
|
||||||
|
func test_plan_timer() -> void:
|
||||||
|
print("[Planungsuhr]")
|
||||||
|
var g := Game.new()
|
||||||
|
g.start(21)
|
||||||
|
check(is_equal_approx(g.plan_left, Data.PLAN_SECONDS), "Runde startet mit %d s" % int(Data.PLAN_SECONDS))
|
||||||
|
check(not g.tick_planning(10.0) and g.phase == Game.Phase.PLANNING, "nach 10 s noch Planung")
|
||||||
|
check(g.tick_planning(Data.PLAN_SECONDS), "Ablauf startet den Kampf von selbst")
|
||||||
|
check(g.phase == Game.Phase.COMBAT, "Phase ist Kampf")
|
||||||
|
g.skip_combat()
|
||||||
|
check(is_equal_approx(g.plan_left, Data.PLAN_SECONDS), "neue Runde setzt die Uhr zurück")
|
||||||
|
g.timer_enabled = false
|
||||||
|
check(not g.tick_planning(999.0) and g.phase == Game.Phase.PLANNING, "abgeschaltete Uhr läuft nicht ab")
|
||||||
|
|
||||||
|
|
||||||
|
func test_bots_reach_three_star() -> void:
|
||||||
|
print("[Bots erreichen 3-Star]")
|
||||||
|
var three := 0
|
||||||
|
var games := 4
|
||||||
|
for run in games:
|
||||||
|
var g := Game.new()
|
||||||
|
g.start(7000 + run)
|
||||||
|
var ai := BotAI.new()
|
||||||
|
ai.setup(g.rand)
|
||||||
|
var guard := 0
|
||||||
|
var seen := {}
|
||||||
|
while g.phase != Game.Phase.GAMEOVER and guard < 200:
|
||||||
|
if g.human.alive:
|
||||||
|
ai.take_turn(g.human, g.pool, g.rand)
|
||||||
|
for p in g.players:
|
||||||
|
for u in p.board_units():
|
||||||
|
if u.star == 3:
|
||||||
|
seen[u.id] = true
|
||||||
|
g.start_combat()
|
||||||
|
if g.phase == Game.Phase.COMBAT:
|
||||||
|
g.skip_combat()
|
||||||
|
guard += 1
|
||||||
|
three += seen.size()
|
||||||
|
check(three > 0, "in %d Partien entstanden %d 3-Star-Avatare" % [games, three])
|
||||||
|
|
||||||
|
|
||||||
|
func test_shop_lock() -> void:
|
||||||
|
print("[Shop einfrieren]")
|
||||||
|
var g := Game.new()
|
||||||
|
g.start(31)
|
||||||
|
var p := g.human
|
||||||
|
var sig := []
|
||||||
|
for e in p.shop:
|
||||||
|
sig.append([e.role, e.element])
|
||||||
|
p.shop_locked = true
|
||||||
|
g.start_combat()
|
||||||
|
g.skip_combat()
|
||||||
|
var same := p.shop.size() == sig.size()
|
||||||
|
for i in p.shop.size():
|
||||||
|
if same and (p.shop[i].role != sig[i][0] or p.shop[i].element != sig[i][1]):
|
||||||
|
same = false
|
||||||
|
check(same, "eingefrorener Shop bleibt über den Rundenwechsel gleich")
|
||||||
|
p.gold = 10
|
||||||
|
p.reroll(g.pool, g.rand)
|
||||||
|
check(not p.shop_locked, "Würfeln hebt das Einfrieren auf")
|
||||||
|
p.shop = [ShopEntry.new(0, -1)]
|
||||||
|
p.gold = 5
|
||||||
|
p.buy(0, g.pool, Data.Element.ERDE)
|
||||||
|
check(p.last_element == Data.Element.ERDE, "letzte Spezialisierung wird gemerkt")
|
||||||
|
|
||||||
|
|
||||||
|
func test_element_profiles() -> void:
|
||||||
|
print("[Elemente prägen die Werte]")
|
||||||
|
var feuer := Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1).base_stats()
|
||||||
|
var erde := Avatar.new(Data.Role.DPS, [Data.Element.ERDE] as Array[int], 1).base_stats()
|
||||||
|
var wind := Avatar.new(Data.Role.DPS, [Data.Element.WIND] as Array[int], 1).base_stats()
|
||||||
|
var blitz := Avatar.new(Data.Role.DPS, [Data.Element.BLITZ] as Array[int], 1).base_stats()
|
||||||
|
check(feuer["atk"] > erde["atk"] * 1.2, "Feuer schlägt härter als Erde (%d vs %d)" % [int(feuer["atk"]), int(erde["atk"])])
|
||||||
|
check(erde["hp"] > feuer["hp"] * 1.25, "Erde hält mehr aus als Feuer (%d vs %d)" % [int(erde["hp"]), int(feuer["hp"])])
|
||||||
|
check(wind["aspd"] > erde["aspd"] * 1.25, "Wind ist schneller als Erde")
|
||||||
|
check(blitz["crit"] > 0.0 and feuer["crit"] == 0.0, "nur Blitz bringt eigene Krit-Chance")
|
||||||
|
var a := PlayerState.new(0, "A", false)
|
||||||
|
a.board[Vector2i(3, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.BLITZ] as Array[int], 1)
|
||||||
|
var b := PlayerState.new(1, "B", false)
|
||||||
|
b.board[Vector2i(3, 1)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE] as Array[int], 1)
|
||||||
|
var c := Combat.new(a, b, 2)
|
||||||
|
var blitz_unit = null
|
||||||
|
for u in c.units:
|
||||||
|
if u.team == 0:
|
||||||
|
blitz_unit = u
|
||||||
|
check(blitz_unit.crit >= 0.15, "Krit-Profil kommt im Kampf an (%.2f)" % blitz_unit.crit)
|
||||||
|
|
||||||
|
|
||||||
|
func test_pairing_known_early() -> void:
|
||||||
|
print("[Gegner steht in der Planung fest]")
|
||||||
|
var g := Game.new()
|
||||||
|
g.start(41)
|
||||||
|
var opp := g.human_opponent()
|
||||||
|
check(opp != null and g.phase == Game.Phase.PLANNING, "in der Planung ist der Gegner bekannt: %s" % (opp.pname if opp else "-"))
|
||||||
|
g.start_combat()
|
||||||
|
var fought := g.human_opponent()
|
||||||
|
check(fought == opp or (fought != null and fought.pname.begins_with(opp.pname)),
|
||||||
|
"gekämpft wird gegen genau diesen Gegner")
|
||||||
|
g.skip_combat()
|
||||||
|
check(g.human_opponent() != null, "neue Runde, neuer Gegner bekannt")
|
||||||
|
# Jeder Bot kennt ebenfalls seinen Gegner.
|
||||||
|
var all_known := true
|
||||||
|
for p in g.alive_players():
|
||||||
|
if g.opponent_of(p) == null:
|
||||||
|
all_known = false
|
||||||
|
check(all_known, "jeder Spieler hat einen Gegner")
|
||||||
|
|
||||||
|
|
||||||
|
func test_recap() -> void:
|
||||||
|
print("[Rückblick]")
|
||||||
|
var g := Game.new()
|
||||||
|
g.start(51)
|
||||||
|
g.human.board[Vector2i(3, 1)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 2)
|
||||||
|
g.human.board[Vector2i(2, 0)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE] as Array[int], 1)
|
||||||
|
g.start_combat()
|
||||||
|
g.skip_combat()
|
||||||
|
var r := g.human_recap
|
||||||
|
check(not r.is_empty(), "Rückblick liegt vor")
|
||||||
|
check(r["total"] == 2 and r["alive"] <= 2, "zählt eigene Einheiten (%d, %d überlebt)" % [r["total"], r["alive"]])
|
||||||
|
check(r["opponent"] != "" and r.has("result") and r.has("damage"), "nennt Gegner, Ergebnis und Schaden")
|
||||||
|
check(r["best_dealt"] > 0 or r["best_healed"] > 0, "stärkste Einheit hat etwas beigetragen (%s)" % r["best_name"])
|
||||||
|
|
||||||
|
|
||||||
|
func test_bot_arrange() -> void:
|
||||||
|
print("[Bots stellen gegen den Gegner auf]")
|
||||||
|
var ai := BotAI.new()
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = 3
|
||||||
|
ai.setup(rng)
|
||||||
|
var me := PlayerState.new(1, "Bot", true)
|
||||||
|
me.level = 6
|
||||||
|
for i in 2:
|
||||||
|
me.board[Vector2i(i, 3)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE] as Array[int], 1)
|
||||||
|
for i in 3:
|
||||||
|
me.board[Vector2i(2 + i, 0)] = Avatar.new(Data.Role.DPS, [Data.Element.FEUER] as Array[int], 1)
|
||||||
|
var opp := PlayerState.new(2, "Gegner", true)
|
||||||
|
# Gegnerische Schützen links (aus seiner Sicht), also rechts aus unserer.
|
||||||
|
for i in 3:
|
||||||
|
opp.board[Vector2i(0, i)] = Avatar.new(Data.Role.DPS, [Data.Element.BLITZ] as Array[int], 1)
|
||||||
|
opp.board[Vector2i(6, 0)] = Avatar.new(Data.Role.TANK, [Data.Element.ERDE] as Array[int], 1)
|
||||||
|
ai.arrange(me, opp)
|
||||||
|
check(me.board.size() == 5, "alle Einheiten stehen wieder (%d)" % me.board.size())
|
||||||
|
var tanks_front := true
|
||||||
|
var dps_back := true
|
||||||
|
var tank_cols := []
|
||||||
|
var dps_cols := []
|
||||||
|
for hex in me.board:
|
||||||
|
var u: Avatar = me.board[hex]
|
||||||
|
if u.role == Data.Role.TANK:
|
||||||
|
tanks_front = tanks_front and hex.y == 0
|
||||||
|
tank_cols.append(hex.x)
|
||||||
|
else:
|
||||||
|
dps_back = dps_back and hex.y >= 2
|
||||||
|
dps_cols.append(hex.x)
|
||||||
|
check(tanks_front, "Tanks stehen vorn")
|
||||||
|
check(dps_back, "Schützen stehen hinten")
|
||||||
|
check(tank_cols.has(6), "ein Tank steht vor den gegnerischen Schützen (Spalte 6)")
|
||||||
|
var avg := 0.0
|
||||||
|
for c in dps_cols:
|
||||||
|
avg += c
|
||||||
|
avg /= dps_cols.size()
|
||||||
|
check(avg > 3.0, "Schützen meiden die Seite des gegnerischen Tanks (Mittel %.1f)" % avg)
|
||||||
|
|||||||
Reference in New Issue
Block a user