3549 lines
171 KiB
GDScript
3549 lines
171 KiB
GDScript
extends Node2D
|
||
|
||
const SCREEN := Vector2(1600.0, 900.0)
|
||
const ENGLISH_UI := {
|
||
"PROFIL": "PROFILE", "EINSATZKARTE WÄHLEN • GESPERRTE SEKTOREN IM SHOP FREISCHALTEN": "SELECT A MAP • UNLOCK SECTORS IN THE SHOP",
|
||
"GESPERRT": "LOCKED", "Wähle einen Sektor, um Schwierigkeit und Missionsstart festzulegen.": "Select a sector to choose difficulty and start the mission.",
|
||
"< KARTE": "< MAP", "SCHWIERIGKEIT WÄHLEN": "SELECT DIFFICULTY", "LEICHT": "EASY", "MITTEL": "MEDIUM", "SCHWER": "HARD",
|
||
"MISSION STARTEN": "START MISSION", "BESTENLISTE": "LEADERBOARD", "ARSENAL / SHOP": "ARSENAL / SHOP", "EINSTELLUNGEN": "SETTINGS",
|
||
"CODE EINLÖSEN": "REDEEM CODE", "SPIEL BEENDEN": "QUIT GAME", "MULTIPLAYER": "MULTIPLAYER", "SCHLIESSEN": "CLOSE",
|
||
"MASTER-LAUTSTÄRKE": "MASTER VOLUME", "SPRACHE": "LANGUAGE", "DEUTSCH": "GERMAN", "ENGLISCH": "ENGLISH",
|
||
"Die Einstellung wird automatisch gespeichert.": "Settings are saved automatically.", "< ZURÜCK": "< BACK", "ZURÜCK": "BACK",
|
||
"KOOPERATIVE VERTEIDIGUNG": "COOPERATIVE DEFENSE", "ZWEI KOMMANDANTEN • EIN GEMEINSAMER REAKTOR": "TWO COMMANDERS • ONE SHARED REACTOR",
|
||
"GEMEINSAMER REAKTOR": "SHARED REACTOR", "SCHNELLES SPIEL": "QUICK MATCH", "RAUM BEITRETEN": "JOIN ROOM",
|
||
"RAUM ERSTELLEN": "CREATE ROOM", "MIT KI-PARTNER": "WITH AI PARTNER", "FREUNDESLISTE": "FRIENDS",
|
||
"FREUNDE EINLADEN": "INVITE FRIENDS", "AKTUALISIEREN": "REFRESH", "KENNUNG KOPIEREN": "COPY PLAYER ID",
|
||
"ANFRAGE SENDEN": "SEND REQUEST", "EINLADEN": "INVITE", "ENTFERNEN": "REMOVE", "ZURÜCKZIEHEN": "WITHDRAW",
|
||
"ANNEHMEN": "ACCEPT", "ONLINE": "ONLINE", "OFFLINE": "OFFLINE", "IM SPIEL": "IN GAME",
|
||
"ANFRAGE GESENDET": "REQUEST SENT", "ANFRAGE ERHALTEN": "REQUEST RECEIVED", "BLOCKIERT": "BLOCKED",
|
||
"Noch keine Freunde oder offenen Anfragen.": "No friends or pending requests yet.",
|
||
"Eindeutige Spielerkennung eingeben": "Enter unique player ID",
|
||
"Füge Freunde über ihre eindeutige Spielerkennung hinzu. Beide müssen die Anfrage bestätigen.": "Add friends using their unique player ID. Both players must confirm the request.",
|
||
"Kein Chat: Einladungen enthalten nur einen Raumcode, keine freie Nachricht.": "No chat: invites contain only a room code, never a custom message.",
|
||
"PRIVATEN RAUM BEITRETEN": "JOIN A PRIVATE ROOM", "GIB DEN SECHSSTELLIGEN CODE DEINES MITPILOTEN EIN": "ENTER YOUR PARTNER'S SIX-CHARACTER CODE",
|
||
"BUCHSTABEN UND ZAHLEN • GROSS-/KLEINSCHREIBUNG IST EGAL": "LETTERS AND NUMBERS • CASE DOES NOT MATTER",
|
||
"PRIVATER KOOP-RAUM": "PRIVATE CO-OP ROOM", "MITSPIELERSUCHE": "FINDING A PARTNER", "KOOP-SPIEL": "CO-OP GAME",
|
||
"NICHT BEREIT": "NOT READY", "BEREIT": "READY", "SUCHE ABBRECHEN": "CANCEL SEARCH",
|
||
"GLOBALE BESTENLISTE": "GLOBAL LEADERBOARD", "MISSION": "MISSION", "ENDLOSMODUS": "ENDLESS MODE",
|
||
"RANG": "RANK", "PILOT": "PILOT", "KARTE": "MAP", "STUFE": "LEVEL", "WELLE": "WAVE", "PUNKTE": "SCORE",
|
||
"PILOTENPROFIL": "PILOT PROFILE", "SPIELERNAME ÄNDERN": "CHANGE PLAYER NAME", "KONTO & CLOUD": "ACCOUNT & CLOUD",
|
||
"BASTION PRIME": "BASTION PRIME", "HELIOS-WERFT": "HELIOS SHIPYARD", "PYRAX-RAFFINERIE": "PYRAX REFINERY",
|
||
"NOVA-ARKOLOGIE": "NOVA ARCOLOGY", "CRYO-SCHLEUSE": "CRYO LOCK", "SOLARFORGE": "SOLAR FORGE", "VESTIGE-9": "VESTIGE-9",
|
||
"Dreifache Feuerlinie": "Triple firing line", "Langer Werft-Korridor": "Long shipyard corridor", "Gefährlicher Asteroidenring": "Dangerous asteroid ring",
|
||
"Biodom über dem Nebel": "Biodome above the nebula", "Eisige Raffinerie": "Frozen refinery", "Schmiede am Gasriesen": "Forge by the gas giant",
|
||
"Verlassene Megastruktur": "Abandoned megastructure", "VERKAUFEN": "SELL", "MAX. STUFE": "MAX LEVEL", "FREIGESCHALTET": "UNLOCKED",
|
||
"GESPERRT • %d KERNKRISTALLE": "LOCKED • %d CORE CRYSTALS", "KARTENBONUS %d • BESTE WELLE %02d • KERNKRISTALLE %04d": "MAP BONUS %d • BEST WAVE %02d • CORE CRYSTALS %04d",
|
||
"Bisheriges Balancing • 20 Reaktor • Kartenbonus 5": "Classic balance • 20 reactor health • map bonus 5",
|
||
"+32 % Gegnerhülle • +24 % Gegner • Kartenbonus 7": "+32% enemy armor • +24% enemies • map bonus 7",
|
||
"+72 % Grundhülle • stärkere Spätwellen • Start 340 • Kartenbonus 12": "+72% base armor • stronger late waves • start 340 • map bonus 12",
|
||
"GERGAMES-SERVER VERBUNDEN • ONLINE-KOOP IST SPIELBEREIT": "GERGAMES SERVER CONNECTED • ONLINE CO-OP READY",
|
||
"VERBINDUNG ZUM GERGAMES-SERVER WIRD AUTOMATISCH AUFGEBAUT": "CONNECTING TO THE GERGAMES SERVER AUTOMATICALLY",
|
||
"FREISCHALTEN • %d KERNKRISTALLE": "UNLOCK • %d CORE CRYSTALS", "SCH %.0f • %.2f SCHUSS/S": "DMG %.0f • %.2f SHOTS/S",
|
||
"RW %.0f • %s": "RANGE %.0f • %s", "LUFT": "AIR", "BODEN": "GROUND", "LUFT + BODEN": "AIR + GROUND",
|
||
"EINZELZIEL": "SINGLE TARGET", "FLÄCHE 76": "AREA 76", "PLASMA-FLÄCHE 92": "PLASMA AREA 92", "EMP-FLÄCHE 118 · ALLE": "EMP AREA 118 · ALL",
|
||
"PIERCING-STRAHL": "PIERCING BEAM", "LUFT-EINZELZIEL": "AIR SINGLE TARGET",
|
||
"DEINE SPIELERKENNUNG: %s": "YOUR PLAYER ID: %s", "EINLADUNG VON %s ANNEHMEN": "ACCEPT INVITE FROM %s",
|
||
"Anfrage an %s gesendet.": "Request sent to %s.", "Spielerkennung nicht gefunden oder Anfrage fehlgeschlagen.": "Player ID not found or request failed.",
|
||
"Anfrage bestätigt.": "Request accepted.", "Bestätigung fehlgeschlagen.": "Could not accept request.",
|
||
"Einladung gesendet.": "Invite sent.", "Einladung fehlgeschlagen oder gerade erst gesendet.": "Invite failed or was sent too recently.",
|
||
"Anfrage zurückgezogen.": "Request withdrawn.", "Freund entfernt.": "Friend removed.", "Aktion fehlgeschlagen.": "Action failed.",
|
||
"Spielerkennung kopiert.": "Player ID copied.", "Keine Serververbindung. Bitte später erneut versuchen.": "No server connection. Please try again later.",
|
||
"Freundesliste konnte nicht geladen werden.": "Could not load friends.",
|
||
"Privater Raum %s: Freunde mit EINLADEN direkt benachrichtigen.": "Private room %s: use INVITE to notify friends directly.",
|
||
"MITSPIELER": "PARTNER", "REAKTORLEBEN %02d • GETRENNTE RESSOURCEN": "REACTOR HEALTH %02d • SEPARATE RESOURCES",
|
||
"ENERGIE": "ENERGY",
|
||
"VERSION %s • GERGAMES-ONLINEKONTO • GLOBALE BESTENLISTEN": "VERSION %s • GERGAMES ONLINE ACCOUNT • GLOBAL LEADERBOARDS",
|
||
"ONLINE • SUCHE NACH EINEM MITPILOTEN": "ONLINE • SEARCHING FOR A PARTNER",
|
||
"VERBINDUNG ZUM GERGAMES-SERVER WIRD AUFGEBAUT …": "CONNECTING TO THE GERGAMES SERVER …",
|
||
"SERVER NOCH NICHT ERREICHBAR • BITTE SPÄTER ERNEUT VERSUCHEN": "SERVER UNAVAILABLE • PLEASE TRY AGAIN LATER",
|
||
"MITPILOT GEFUNDEN • WÄHLE BEREIT": "PARTNER FOUND • SELECT READY",
|
||
"BEIDE SPIELER MÜSSEN BEREIT SEIN": "BOTH PLAYERS MUST BE READY",
|
||
"VERBINDUNG WIRD WIEDERHERGESTELLT .": "RECONNECTING …", "VERBINDUNG VERLOREN • BITTE ZURÜCKKEHREN": "CONNECTION LOST • PLEASE GO BACK",
|
||
"PRIVATER RAUM WIRD ERSTELLT .": "CREATING PRIVATE ROOM …", "TEILE DEN RAUMCODE MIT DEINEM MITPILOTEN • WARTE AUF BEITRITT": "SHARE THE ROOM CODE WITH YOUR PARTNER • WAITING FOR JOIN",
|
||
"PRIVATER RAUM WIRD GESUCHT .": "FINDING PRIVATE ROOM …", "RAUM NICHT GEFUNDEN ODER BEREITS VOLL": "ROOM NOT FOUND OR FULL",
|
||
"LINKE VERTEIDIGUNG": "LEFT DEFENSE", "RECHTE VERTEIDIGUNG": "RIGHT DEFENSE", "NICHT VERBUNDEN": "DISCONNECTED",
|
||
"WARTE AUF MITPILOTEN": "WAITING FOR PARTNER", "RAUMCODE %s": "ROOM CODE %s",
|
||
"REAKTOR %02d": "REACTOR %02d", "ENERGIE %04d": "ENERGY %04d", "PARTNER": "PARTNER", "KI": "AI",
|
||
"WELLE %d STARTEN": "START WAVE %d", "%d IM ANFLUG": "%d INCOMING", "JETZT • %02d/%02d": "NOW • %02d/%02d",
|
||
"DANN • %02d": "NEXT • %02d", "DANN • --": "NEXT • --", "%d SCHIFFE": "%d UNITS",
|
||
"NÄCHSTE WELLE AUTO STARTEN": "AUTO-START NEXT WAVE", "VIDEO: +150 NOTFALLENERGIE": "VIDEO: +150 EMERGENCY ENERGY",
|
||
"ANTIPPEN • BAUPLATZ WÄHLEN": "TAP • SELECT BUILD SLOT", "GESPERRT • IM SHOP FREISCHALTEN": "LOCKED • UNLOCK IN SHOP",
|
||
"UPGRADE %d": "UPGRADE %d", "SCHILD": "SHIELD", "KOMMANDOSCHIFF": "COMMAND SHIP",
|
||
"TÜRME": "TOWERS", "KARTEN": "MAPS", "Türme und Einsatzsektoren mit Kernkristallen freischalten": "Unlock towers and sectors using core crystals",
|
||
"KERNKRISTALLE %04d": "CORE CRYSTALS %04d", "Weitere Verteidigungen lassen sich dauerhaft mit Kernkristallen freischalten.": "Unlock additional defenses permanently with core crystals.",
|
||
"GERGAMES-ARSENAL": "GERGAMES ARSENAL", "80-PX-RASTER • DIREKTZIEL REAKTOR": "80-PX GRID • DIRECT REACTOR TARGET",
|
||
"Jeder Sektor hat einen eigenen, rastergenauen Laufweg bis in den Reaktor.": "Each sector has its own grid-aligned route into the reactor.",
|
||
"SCH %.0f • %.2f/S • RW %.0f": "DMG %.0f • %.2f/S • RANGE %.0f",
|
||
"%s • STUFE %d": "%s • LEVEL %d", "%s • BAUVORSCHAU": "%s • BUILD PREVIEW",
|
||
"+%.0f SCH +%.2f/S +%.0f RW BEIM UPGRADE": "+%.0f DMG +%.2f/S +%.0f RANGE ON UPGRADE",
|
||
"MAXIMALE AUSBAUSTUFE": "MAXIMUM LEVEL", "SPIELER-ID %s": "PLAYER ID %s",
|
||
"LOKALE BESTWERTE": "LOCAL BESTS", "EINSATZSEKTOR": "SECTOR", "ENDLOS": "ENDLESS", "BESTE PUNKTE": "BEST SCORE",
|
||
"GERGAMES ONLINE VERBUNDEN": "GERGAMES ONLINE CONNECTED", "GAME CENTER / APPLE NÄCHSTE STUFE": "GAME CENTER / APPLE COMING LATER",
|
||
"GOOGLE PLAY GAMES NÄCHSTE STUFE": "GOOGLE PLAY GAMES COMING LATER",
|
||
"DATENSCHUTZ": "PRIVACY", "LÖSCHEN BESTÄTIGEN": "CONFIRM DELETION", "KONTO LÖSCHEN": "DELETE ACCOUNT",
|
||
"SPIELERNAME": "PLAYER NAME", "Dieser Name ist für Mitspieler und in Ranglisten sichtbar.": "This name is visible to partners and on leaderboards.",
|
||
"3–20 Zeichen · Buchstaben, Zahlen, Leerzeichen, _ und -": "3–20 characters · letters, numbers, spaces, _ and -",
|
||
"SPIELERNAME SPEICHERN": "SAVE PLAYER NAME", "Die Online-Partie läuft währenddessen weiter.": "The online match continues in the background.",
|
||
"Die Verteidigung ist angehalten.": "Defense is paused.", "FORTSETZEN": "RESUME", "PARTIE VERLASSEN": "LEAVE MATCH", "MISSION VERLASSEN": "LEAVE MISSION",
|
||
"REAKTOR VERLOREN": "REACTOR LOST", "Erreichte Welle: %d": "Wave reached: %d", "Highscore: %d • Effizienzbonus: ×%.2f": "Score: %d • Efficiency bonus: ×%.2f",
|
||
"NEUER VERSUCH": "TRY AGAIN", "SEKTOR GESICHERT": "SECTOR SECURED", "Alle %d Angriffswellen wurden abgewehrt.": "All %d attack waves were repelled.",
|
||
"Der Orbitalkern bleibt unter unserer Kontrolle.": "The orbital core remains under our control.",
|
||
"+%d KERNKRISTALLE • GUTHABEN %d": "+%d CORE CRYSTALS • BALANCE %d", "ZUM HAUPTMENÜ": "MAIN MENU",
|
||
"Alle %d regulären Wellen wurden abgewehrt.": "All %d regular waves were repelled.",
|
||
"Möchtest du den Einsatz im immer schwereren Endlosmodus fortsetzen?": "Continue in an increasingly difficult endless mode?",
|
||
"WERBUNG WIRD GELADEN • KERNKRISTALLE DANACH: %d": "LOADING AD • CORE CRYSTALS AFTERWARDS: %d",
|
||
"+%d Kernkristalle gutgeschrieben. Ein späteres Ende gilt nicht als gescheitert.": "+%d core crystals awarded. A later defeat does not undo this victory.",
|
||
"ENDLOSMODUS STARTEN": "START ENDLESS MODE", "MISSION BEENDEN": "FINISH MISSION",
|
||
"ENDLOSAUFTRAG BEENDET": "ENDLESS MISSION ENDED", "Der reguläre Sektorauftrag bleibt erfolgreich abgeschlossen.": "The regular sector mission remains completed.",
|
||
"KERNKRISTALLE %d": "CORE CRYSTALS %d", "NEUER EINSATZ": "NEW MISSION",
|
||
"SONDER-CODE": "SPECIAL CODE", "Gib einen persönlichen Freischaltcode ein.": "Enter a personal unlock code.",
|
||
"FREISCHALTCODE": "UNLOCK CODE", "BITTE WARTEN …": "PLEASE WAIT …",
|
||
"Spielernamen werden aus dem aktuellen öffentlichen Profil geladen.": "Player names are loaded from their current public profiles.",
|
||
"WERTUNG: WELLEN + REAKTORREST + RESTENERGIE • WENIGER GEBAUTE TÜRME ERHÖHEN DEN EFFIZIENZ-MULTIPLIKATOR": "SCORE: WAVES + REACTOR HEALTH + REMAINING ENERGY • FEWER TOWERS BUILT INCREASE EFFICIENCY",
|
||
"Zuverlässig gegen Bodenziele.": "Reliable against ground targets.",
|
||
"Mehrzwecklaser; durchdringt Schilde von Luft- und Bodenzielen.": "Versatile laser; penetrates shields on air and ground targets.",
|
||
"Bodenraketen treffen Gegnergruppen.": "Ground rockets hit groups of enemies.",
|
||
"Mehrzweckwaffe mit reduziertem Schaden.": "Versatile weapon with reduced damage.",
|
||
"Impuls trifft alle Luft- und Bodenziele im Radius mit 20 Schaden.": "Pulse deals 20 damage to all air and ground targets in range.",
|
||
"Lila Durchschlagsstrahl trifft alle Gegner auf seiner Schusslinie.": "Purple piercing beam hits all enemies along its path.",
|
||
"Schnelle Abfangkanone gegen gegnerische Luftschiffe.": "Rapid interceptor cannon against enemy aircraft.",
|
||
"1/3 VERTEIDIGUNG WÄHLEN": "1/3 SELECT DEFENSE", "Wähle unten das KINETIK-Geschütz.": "Select the KINETIC cannon below.",
|
||
"2/3 TURM PLATZIEREN": "2/3 PLACE TOWER", "Setze den Turm auf eine freie Metallfläche neben der Flugroute.": "Place the tower on a free metal tile beside the enemy route.",
|
||
"3/3 ANGRIFF STARTEN": "3/3 START ATTACK", "Starte oben rechts die erste Welle.": "Start the first wave in the top-right corner.",
|
||
"ÜBERSPRINGEN": "SKIP", "KINETIK": "KINETIC", "RAKETE": "ROCKET", "EMP-PULS": "EMP PULSE"
|
||
}
|
||
const WORLD_SCALE := 1.0
|
||
const WORLD_SIZE := SCREEN
|
||
const SAVE_PATH := "user://void_bastion_save.json"
|
||
const MAX_WAVES := 15
|
||
const BOSS_WAVE_INTERVAL := 5
|
||
const PLAYFIELD_TOP := 84.0
|
||
const PLAYFIELD_BOTTOM := 758.0
|
||
const GRID_SIZE := 80.0
|
||
const TOWER_TEXTURE := preload("res://assets/defense_towers.png")
|
||
const PLASMA_TEXTURE := preload("res://assets/tower_plasma.png")
|
||
const EMP_TEXTURE := preload("res://assets/tower_emp.png")
|
||
const RAILGUN_TEXTURE := preload("res://assets/tower_railgun.png")
|
||
const ENEMY_TEXTURE := preload("res://assets/enemy_ships.png")
|
||
const ENEMY_INTERCEPTOR_TEXTURE := preload("res://assets/enemy_interceptor.png")
|
||
const ENEMY_SHIELD_CARRIER_TEXTURE := preload("res://assets/enemy_shield_carrier.png")
|
||
const ENEMY_SIEGE_DESTROYER_TEXTURE := preload("res://assets/enemy_siege_destroyer.png")
|
||
const ENEMY_GROUND_ROVER_TEXTURE := preload("res://assets/enemy_ground_rover.png")
|
||
const ENEMY_GROUND_TANK_TEXTURE := preload("res://assets/enemy_ground_tank.png")
|
||
const ENEMY_GROUND_WALKER_TEXTURE := preload("res://assets/enemy_ground_walker.png")
|
||
const ROAD_STRAIGHT_TEXTURE := preload("res://assets/road_straight.svg")
|
||
const ROAD_CORNER_TEXTURE := preload("res://assets/road_corner.svg")
|
||
const ROAD_SURFACE_TEXTURE := preload("res://assets/road_surface_material_v1.png")
|
||
const ROAD_CORNER_SURFACE_TEXTURE := preload("res://assets/road_corner_surface_v1.png")
|
||
const MAP_TEXTURES := [
|
||
preload("res://assets/orbital_platform_grid_base.png"),
|
||
preload("res://assets/orbital_shipyard_grid_base.png"),
|
||
preload("res://assets/asteroid_refinery_grid_base.png"),
|
||
preload("res://assets/orbital_biodome_bg.png"),
|
||
preload("res://assets/cryo_refinery_bg.png"),
|
||
preload("res://assets/solar_forge_bg.png"),
|
||
preload("res://assets/vestige_megastructure_bg.png")
|
||
]
|
||
const MAP_NAMES := ["BASTION PRIME", "HELIOS-WERFT", "PYRAX-RAFFINERIE", "NOVA-ARKOLOGIE", "CRYO-SCHLEUSE", "SOLARFORGE", "VESTIGE-9"]
|
||
const MAP_DESCRIPTIONS := ["Dreifache Feuerlinie", "Langer Werft-Korridor", "Gefährlicher Asteroidenring", "Biodom über dem Nebel", "Eisige Raffinerie", "Schmiede am Gasriesen", "Verlassene Megastruktur"]
|
||
const MAP_UNLOCK_COSTS := [0, 0, 0, 24, 32, 40, 48]
|
||
const MAP_PATHS := [
|
||
[Vector2(-40, 120), Vector2(1160, 120), Vector2(1160, 360), Vector2(360, 360), Vector2(360, 680), Vector2(1560, 680), Vector2(1560, 520), Vector2(1480, 520), Vector2(1480, 440)],
|
||
[Vector2(-40, 280), Vector2(1320, 280), Vector2(1320, 440), Vector2(280, 440), Vector2(280, 680), Vector2(1560, 680), Vector2(1560, 440), Vector2(1480, 440)],
|
||
[Vector2(-40, 120), Vector2(360, 120), Vector2(360, 600), Vector2(1320, 600), Vector2(1320, 280), Vector2(1480, 280), Vector2(1480, 360)],
|
||
[Vector2(-40, 120), Vector2(1240, 120), Vector2(1240, 280), Vector2(280, 280), Vector2(280, 520), Vector2(1480, 520), Vector2(1480, 440)],
|
||
[Vector2(-40, 680), Vector2(1000, 680), Vector2(1000, 520), Vector2(200, 520), Vector2(200, 360), Vector2(1400, 360), Vector2(1480, 360), Vector2(1480, 440)],
|
||
[Vector2(-40, 120), Vector2(1400, 120), Vector2(1400, 280), Vector2(200, 280), Vector2(200, 600), Vector2(1480, 600), Vector2(1480, 440)],
|
||
[Vector2(-40, 360), Vector2(200, 360), Vector2(200, 120), Vector2(1400, 120), Vector2(1400, 280), Vector2(440, 280), Vector2(440, 600), Vector2(1240, 600), Vector2(1240, 520), Vector2(1480, 520), Vector2(1480, 440)]
|
||
]
|
||
const MAP_BUILD_ZONES := [
|
||
[Rect2(155, 95, 1380, 660)],
|
||
[Rect2(120, 95, 1380, 660)],
|
||
[Rect2(180, 110, 1270, 645)],
|
||
[Rect2(40, 96, 1520, 664)],
|
||
[Rect2(40, 96, 1520, 664)],
|
||
[Rect2(40, 96, 1520, 664)],
|
||
[Rect2(40, 96, 1520, 664)]
|
||
]
|
||
const MAP_REACTOR_ZONES := [
|
||
[{"center": Vector2(1480, 440), "radius": 190.0}],
|
||
[{"center": Vector2(1480, 440), "radius": 185.0}],
|
||
[
|
||
{"center": Vector2(1480, 360), "radius": 165.0},
|
||
{"center": Vector2(690, 385), "radius": 155.0}
|
||
],
|
||
[{"center": Vector2(1480, 440), "radius": 185.0}],
|
||
[{"center": Vector2(1480, 440), "radius": 185.0}],
|
||
[{"center": Vector2(1480, 440), "radius": 185.0}],
|
||
[{"center": Vector2(1480, 440), "radius": 185.0}]
|
||
]
|
||
const COOP_MIRROR_X := 1480.0
|
||
const COOP_WORLD_WIDTH := COOP_MIRROR_X * 2.0
|
||
const COOP_LEFT_PATH := [Vector2(-40, 120), Vector2(1160, 120), Vector2(1160, 360), Vector2(360, 360), Vector2(360, 680), Vector2(1400, 680), Vector2(1400, 520), Vector2(1480, 520), Vector2(1480, 440)]
|
||
const COOP_REACTOR_CENTER := Vector2(COOP_MIRROR_X, 440)
|
||
const COOP_REACTOR_RADIUS := 112.0
|
||
|
||
const DIFFICULTY_DATA := [
|
||
{"name": "LEICHT", "hp": 1.0, "speed": 1.0, "count": 1.0, "income": 1.0, "spawn_delay": 1.0, "start_credits": 450, "lives": 20, "crystals": 5},
|
||
{"name": "MITTEL", "hp": 1.32, "speed": 1.07, "count": 1.24, "income": 0.94, "spawn_delay": 0.91, "start_credits": 430, "lives": 17, "crystals": 7},
|
||
{"name": "SCHWER", "hp": 1.72, "speed": 1.15, "count": 1.48, "income": 0.78, "spawn_delay": 0.82, "start_credits": 340, "lives": 12, "crystals": 12}
|
||
]
|
||
const DIFFICULTY_DESCRIPTIONS := [
|
||
"Bisheriges Balancing • 20 Reaktor • Kartenbonus 5",
|
||
"+32 % Gegnerhülle • +24 % Gegner • Kartenbonus 7",
|
||
"+72 % Grundhülle • stärkere Spätwellen • Start 340 • Kartenbonus 12"
|
||
]
|
||
|
||
const TOWER_DATA := [
|
||
{"name": "KINETIK", "cost": 120, "damage": 22.0, "range": 190.0, "rate": 0.55, "shot_speed": 620.0, "damage_type": "EINZELZIEL", "target_class": "ground", "color": Color("7de7ff"), "unlock_cost": 0, "description": "Zuverlässig gegen Bodenziele."},
|
||
{"name": "LASER", "cost": 180, "damage": 12.0, "range": 165.0, "rate": 0.18, "shot_speed": 1100.0, "damage_type": "EINZELZIEL", "target_class": "both", "both_damage_scale": 0.82, "color": Color("62fff1"), "unlock_cost": 0, "description": "Mehrzwecklaser; durchdringt Schilde von Luft- und Bodenzielen."},
|
||
{"name": "RAKETE", "cost": 250, "damage": 65.0, "range": 260.0, "rate": 1.40, "shot_speed": 360.0, "damage_type": "FLÄCHE 76", "target_class": "ground", "color": Color("ffb35a"), "unlock_cost": 0, "description": "Bodenraketen treffen Gegnergruppen."},
|
||
{"name": "PLASMA", "cost": 230, "damage": 34.0, "range": 240.0, "rate": 1.05, "shot_speed": 480.0, "damage_type": "PLASMA-FLÄCHE 92", "target_class": "both", "both_damage_scale": 0.72, "color": Color("e27bff"), "unlock_cost": 18, "description": "Mehrzweckwaffe mit reduziertem Schaden."},
|
||
{"name": "EMP-PULS", "cost": 210, "damage": 20.0, "range": 215.0, "rate": 0.92, "shot_speed": 540.0, "damage_type": "EMP-FLÄCHE 118 · ALLE", "target_class": "both", "both_damage_scale": 1.0, "color": Color("77e8ff"), "unlock_cost": 24, "description": "Impuls trifft alle Luft- und Bodenziele im Radius mit 20 Schaden."},
|
||
{"name": "RAILGUN", "cost": 300, "damage": 77.0, "range": 330.0, "rate": 1.85, "shot_speed": 1450.0, "damage_type": "PIERCING-STRAHL", "target_class": "both", "both_damage_scale": 0.70, "color": Color("c86bff"), "unlock_cost": 30, "description": "Lila Durchschlagsstrahl trifft alle Gegner auf seiner Schusslinie."},
|
||
{"name": "FLAK", "cost": 210, "damage": 20.0, "range": 230.0, "rate": 0.42, "shot_speed": 920.0, "damage_type": "LUFT-EINZELZIEL", "target_class": "air", "color": Color("ff737c"), "unlock_cost": 0, "description": "Schnelle Abfangkanone gegen gegnerische Luftschiffe."}
|
||
]
|
||
|
||
const ENEMY_DATA := [
|
||
{"name": "JAEGER", "category": "air", "hp": 70.0, "speed": 105.0, "reward": 18, "size": 58.0, "breach_damage": 1, "shield_resistance": 0.28},
|
||
{"name": "FREGATTE", "category": "air", "hp": 185.0, "speed": 72.0, "reward": 34, "size": 78.0, "breach_damage": 2, "shield_resistance": 0.28},
|
||
{"name": "KREUZER", "category": "air", "hp": 520.0, "speed": 43.0, "reward": 78, "size": 112.0, "breach_damage": 3, "shield_resistance": 0.28},
|
||
{"name": "ABFANGJAEGER", "category": "air", "hp": 52.0, "speed": 178.0, "reward": 22, "size": 48.0, "breach_damage": 2, "shield_resistance": 0.28},
|
||
{"name": "SCHILDKREUZER", "category": "air", "hp": 260.0, "speed": 58.0, "reward": 52, "size": 92.0, "breach_damage": 2, "shield_resistance": 0.18, "default_shield": true},
|
||
{"name": "BELAGERER", "category": "air", "hp": 740.0, "speed": 48.0, "reward": 108, "size": 128.0, "breach_damage": 4, "shield_resistance": 0.28},
|
||
{"name": "SPAEHER-ROVER", "category": "ground", "hp": 88.0, "speed": 142.0, "reward": 22, "size": 54.0, "breach_damage": 1, "shield_resistance": 0.28},
|
||
{"name": "STURMPANZER", "category": "ground", "hp": 390.0, "speed": 61.0, "reward": 61, "size": 92.0, "breach_damage": 3, "shield_resistance": 0.28},
|
||
{"name": "BELAGERUNGSLAEUFER", "category": "ground", "hp": 980.0, "speed": 36.0, "reward": 132, "size": 124.0, "breach_damage": 5, "shield_resistance": 0.28}
|
||
]
|
||
|
||
var path_points := PackedVector2Array()
|
||
var path_cells: Dictionary = {}
|
||
var build_zones: Array[Rect2] = []
|
||
var road_tile_cache: Array[Dictionary] = []
|
||
var build_cell_cache: Array[Dictionary] = []
|
||
var draw_geometry_dirty := true
|
||
var selected_map := 0
|
||
var selected_difficulty := 1
|
||
var camera_offset := Vector2.ZERO
|
||
|
||
var game_mode := "menu"
|
||
var menu_step := "map"
|
||
var store_return_mode := "menu"
|
||
var credits := 450
|
||
var reactor_lives := 20
|
||
var wave := 0
|
||
var best_wave := 0
|
||
var best_endless_wave := 0
|
||
var core_crystals := 0
|
||
var unlocked_towers := [0, 1, 2, 6]
|
||
var unlocked_maps := [0, 1, 2]
|
||
var store_tab := "towers"
|
||
var tutorial_completed := false
|
||
var tutorial_step := 0
|
||
var game_speed := 1.0
|
||
const SPEED_OPTIONS := [1.0, 2.0, 3.0, 4.0, 5.0]
|
||
var speed_dropdown_open := false
|
||
var auto_start_waves := false
|
||
var auto_wave_countdown := 0.0
|
||
var towers_built_this_run := 0
|
||
var cursor_position := Vector2(-1000, -1000)
|
||
var cursor_screen_position := Vector2(-1000, -1000)
|
||
var selected_tower_type := -1
|
||
var selected_tower_index := -1
|
||
var pending_touch_build_position := Vector2(-1, -1)
|
||
var towers: Array[Dictionary] = []
|
||
var enemies: Array[Dictionary] = []
|
||
var projectiles: Array[Dictionary] = []
|
||
var beams: Array[Dictionary] = []
|
||
var particles: Array[Dictionary] = []
|
||
var next_enemy_id := 1
|
||
var wave_active := false
|
||
var enemies_to_spawn := 0
|
||
var spawn_timer := 0.0
|
||
var wave_type_schedule: Array[Dictionary] = []
|
||
var wave_spawn_index := 0
|
||
var online_wave_types_remaining: Array[int] = []
|
||
var enemy_strength_multiplier := 1.0
|
||
var developer_panel_open := false
|
||
var status_text := ""
|
||
var status_timer := 0.0
|
||
var elapsed := 0.0
|
||
var endless_mode := false
|
||
var mission_reward_granted := false
|
||
var last_mission_reward := 0
|
||
var victory_reward_pending := false
|
||
var coop_mode := false
|
||
var coop_ai_mode := false
|
||
var coop_online_mode := false
|
||
var local_player_slot := 0
|
||
var coop_paths: Array[PackedVector2Array] = []
|
||
var next_spawn_route := 0
|
||
|
||
var start_button := Rect2(625, 674, 350, 58)
|
||
var map_buttons := [
|
||
Rect2(30, 390, 340, 130), Rect2(430, 390, 340, 130), Rect2(830, 390, 340, 130), Rect2(1230, 390, 340, 130),
|
||
Rect2(255, 540, 340, 130), Rect2(630, 540, 340, 130), Rect2(1005, 540, 340, 130)
|
||
]
|
||
var difficulty_buttons := [Rect2(535, 520, 165, 50), Rect2(718, 520, 165, 50), Rect2(901, 520, 165, 50)]
|
||
var wave_button := Rect2(1216, 10, 359, 46)
|
||
var pause_button := Rect2(1142, 14, 62, 54)
|
||
var speed_button := Rect2(1070, 14, 62, 54)
|
||
var auto_wave_button := Rect2(1216, 58, 359, 19)
|
||
var wave_current_preview_rect := Rect2(220, 8, 258, 66)
|
||
var wave_next_preview_rect := Rect2(486, 8, 258, 66)
|
||
var developer_panel_rect := Rect2(1060, 104, 515, 78)
|
||
var developer_strength_down_button := Rect2(1074, 140, 42, 30)
|
||
var developer_strength_up_button := Rect2(1260, 140, 42, 30)
|
||
var developer_crystals_down_button := Rect2(1320, 140, 50, 30)
|
||
var developer_crystals_up_button := Rect2(1512, 140, 50, 30)
|
||
var tower_buttons := [
|
||
Rect2(13, 778, 218, 112), Rect2(238, 778, 218, 112), Rect2(463, 778, 218, 112),
|
||
Rect2(688, 778, 218, 112), Rect2(913, 778, 218, 112), Rect2(1138, 778, 218, 112),
|
||
Rect2(1363, 778, 218, 112)
|
||
]
|
||
var upgrade_button := Rect2(350, 698, 160, 44)
|
||
var sell_button := Rect2(518, 698, 140, 44)
|
||
var pause_resume_button := Rect2(570, 500, 220, 58)
|
||
var pause_exit_button := Rect2(810, 500, 220, 58)
|
||
var tutorial_skip_button := Rect2(365, 205, 145, 30)
|
||
var reward_ad_button := Rect2(1070, 690, 300, 54)
|
||
var tower_info_rect := Rect2(16, 690, 320, 60)
|
||
var target_priority_button := Rect2(666, 698, 300, 44)
|
||
var endless_button := Rect2(545, 515, 510, 70)
|
||
var finish_mission_button := Rect2(625, 610, 350, 62)
|
||
var profile_button := Rect2(1280, 25, 270, 52)
|
||
var leaderboard_button := Rect2(40, 790, 200, 54)
|
||
var shop_button := Rect2(568, 790, 200, 54)
|
||
var map_step_back_button := Rect2(55, 320, 170, 50)
|
||
var shop_back_button := Rect2(55, 35, 185, 54)
|
||
var shop_purchase_buttons := [Rect2(190, 545, 360, 62), Rect2(620, 545, 360, 62), Rect2(1050, 545, 360, 62)]
|
||
var map_shop_purchase_buttons := [Rect2(70, 565, 350, 62), Rect2(460, 565, 350, 62), Rect2(850, 565, 350, 62), Rect2(1240, 565, 350, 62)]
|
||
var store_tower_tab_button := Rect2(590, 174, 200, 42)
|
||
var store_map_tab_button := Rect2(810, 174, 200, 42)
|
||
var leaderboard_back_button := Rect2(55, 35, 185, 54)
|
||
var leaderboard_mission_button := Rect2(510, 212, 270, 54)
|
||
var leaderboard_endless_button := Rect2(820, 212, 270, 54)
|
||
var leaderboard_refresh_button := Rect2(1265, 35, 280, 54)
|
||
var leaderboard_map_previous_button := Rect2(235, 150, 58, 50)
|
||
var leaderboard_map_button := Rect2(303, 150, 410, 50)
|
||
var leaderboard_map_next_button := Rect2(723, 150, 58, 50)
|
||
var leaderboard_difficulty_previous_button := Rect2(819, 150, 58, 50)
|
||
var leaderboard_difficulty_button := Rect2(887, 150, 300, 50)
|
||
var leaderboard_difficulty_next_button := Rect2(1197, 150, 58, 50)
|
||
var leaderboard_rows: Array = []
|
||
var leaderboard_endless := false
|
||
var leaderboard_map := 0
|
||
var leaderboard_difficulty := 1
|
||
var leaderboard_loading := false
|
||
var leaderboard_message := ""
|
||
var profile_back_button := Rect2(55, 35, 185, 54)
|
||
var multiplayer_button := Rect2(304, 790, 200, 54)
|
||
var multiplayer_back_button := Rect2(55, 35, 185, 54)
|
||
var quick_match_button := Rect2(125, 665, 300, 64)
|
||
var friend_match_button := Rect2(475, 665, 300, 64)
|
||
var private_match_button := Rect2(825, 665, 300, 64)
|
||
var ai_match_button := Rect2(1175, 665, 300, 64)
|
||
var friends_button := Rect2(600, 753, 400, 54)
|
||
var friends_back_button := Rect2(55, 35, 185, 54)
|
||
var friends_refresh_button := Rect2(1190, 35, 345, 54)
|
||
var friend_name_rect := Rect2(260, 200, 740, 58)
|
||
var friend_add_button := Rect2(1030, 200, 300, 58)
|
||
var friend_copy_button := Rect2(1030, 135, 300, 46)
|
||
var friend_action_buttons: Array[Rect2] = []
|
||
var friend_return_mode := "multiplayer_menu"
|
||
var friends_message := ""
|
||
var friend_name_line_edit: LineEdit
|
||
var pending_match_invite: Dictionary = {}
|
||
var cancel_search_button := Rect2(625, 650, 350, 64)
|
||
var lobby_ready_button := Rect2(625, 580, 350, 58)
|
||
var room_code_input_rect := Rect2(510, 340, 580, 90)
|
||
var join_room_button := Rect2(625, 525, 350, 64)
|
||
var coop_side_button := Rect2(916, 14, 140, 54)
|
||
var online_projectiles: Array[Dictionary] = []
|
||
var online_snapshot_elapsed := 0.0
|
||
var online_snapshot_interval := 0.1
|
||
var online_frame_stall_count := 0
|
||
var online_frame_stall_report_ms := 0
|
||
var online_apply_max_us := 0
|
||
var online_apply_report_ms := 0
|
||
var room_code_input := ""
|
||
var profile_name_input := ""
|
||
var profile_message := ""
|
||
var profile_delete_armed := false
|
||
var profile_rename_button := Rect2(220, 305, 250, 48)
|
||
var profile_support_button := Rect2(570, 685, 250, 48)
|
||
var profile_privacy_button := Rect2(840, 685, 250, 48)
|
||
var profile_delete_button := Rect2(1110, 685, 250, 48)
|
||
var profile_name_input_rect := Rect2(460, 330, 680, 82)
|
||
var profile_name_save_button := Rect2(625, 470, 350, 62)
|
||
var update_button := Rect2(30, 25, 390, 54)
|
||
var update_dismiss_button := Rect2(430, 25, 54, 54)
|
||
var available_update: Dictionary = {}
|
||
var update_notice_visible := false
|
||
var audio_settings_open := false
|
||
var master_volume := 0.65
|
||
var audio_settings_button := Rect2(832, 790, 200, 54)
|
||
var promo_code_button := Rect2(1096, 790, 200, 54)
|
||
var exit_button := Rect2(1360, 790, 200, 54)
|
||
var promo_code_input_rect := Rect2(500, 335, 600, 64)
|
||
var promo_code_redeem_button := Rect2(650, 460, 300, 58)
|
||
var promo_code_cancel_button := Rect2(55, 35, 185, 54)
|
||
var promo_code_line_edit: LineEdit
|
||
var promo_code_message := ""
|
||
var promo_code_pending := false
|
||
var audio_panel_rect := Rect2(450, 280, 700, 355)
|
||
var audio_close_button := Rect2(1010, 300, 110, 44)
|
||
var audio_slider_rect := Rect2(565, 460, 520, 32)
|
||
var language_button_de := Rect2(565, 540, 240, 42)
|
||
var language_button_en := Rect2(825, 540, 240, 42)
|
||
var ui_language := "de"
|
||
|
||
@onready var ads_manager = $AdsManager
|
||
@onready var account_manager = $AccountManager
|
||
@onready var multiplayer_manager = $MultiplayerManager
|
||
@onready var online_manager = $OnlineManager
|
||
@onready var update_manager = $UpdateManager
|
||
@onready var music_player: AudioStreamPlayer = $MusicPlayer
|
||
@onready var ui_sound_player: AudioStreamPlayer = $UiSoundPlayer
|
||
|
||
|
||
func _ready() -> void:
|
||
_select_map(selected_map)
|
||
_load_progress()
|
||
_load_audio_settings()
|
||
# The Android AudioTrack backend currently crashes on this generated PCM stream
|
||
# on the supported emulator/device test target. Keep the game playable there
|
||
# without starting native audio until we replace it with a packaged audio asset.
|
||
if not OS.has_feature("android"):
|
||
music_player.stream = _build_ambient_stream()
|
||
music_player.play()
|
||
ui_sound_player.stream = _build_ui_click_stream()
|
||
ads_manager.energy_reward_granted.connect(_on_energy_reward_granted)
|
||
ads_manager.ad_unavailable.connect(_show_status)
|
||
ads_manager.victory_interstitial_finished.connect(_on_victory_interstitial_finished)
|
||
multiplayer_manager.online_game_ready.connect(_start_coop_online_game)
|
||
multiplayer_manager.online_snapshot_updated.connect(_apply_online_snapshot)
|
||
multiplayer_manager.online_event_received.connect(_handle_online_event)
|
||
update_manager.update_available.connect(_on_update_available)
|
||
if not online_manager.authenticated.is_connected(_on_online_authenticated):
|
||
online_manager.authenticated.connect(_on_online_authenticated)
|
||
online_manager.friends_changed.connect(queue_redraw)
|
||
online_manager.match_invite_received.connect(_on_match_invite_received)
|
||
_create_promo_code_input()
|
||
_create_friend_name_input()
|
||
queue_redraw()
|
||
|
||
|
||
func _create_friend_name_input() -> void:
|
||
friend_name_line_edit = LineEdit.new()
|
||
friend_name_line_edit.position = friend_name_rect.position + Vector2(10, 3)
|
||
friend_name_line_edit.size = friend_name_rect.size - Vector2(20, 6)
|
||
friend_name_line_edit.max_length = 40
|
||
friend_name_line_edit.placeholder_text = _ui("Eindeutige Spielerkennung eingeben")
|
||
friend_name_line_edit.text_submitted.connect(func(_value: String) -> void: _add_friend_from_input())
|
||
add_child(friend_name_line_edit)
|
||
friend_name_line_edit.hide()
|
||
|
||
|
||
func _on_match_invite_received(invite: Dictionary) -> void:
|
||
pending_match_invite = invite
|
||
_show_status("EINLADUNG VON %s" % String(invite.get("from", "MITSPIELER")))
|
||
queue_redraw()
|
||
|
||
|
||
func _open_friends(return_mode: String) -> void:
|
||
friend_return_mode = return_mode
|
||
game_mode = "friends"
|
||
friend_name_line_edit.show()
|
||
friends_message = ""
|
||
_refresh_friends()
|
||
queue_redraw()
|
||
|
||
|
||
func _close_friends() -> void:
|
||
friend_name_line_edit.hide()
|
||
game_mode = friend_return_mode
|
||
queue_redraw()
|
||
|
||
|
||
func _refresh_friends() -> void:
|
||
if not online_manager.is_authenticated():
|
||
friends_message = _ui("Keine Serververbindung. Bitte später erneut versuchen.")
|
||
return
|
||
var ok: bool = await online_manager.refresh_friends()
|
||
if not ok:
|
||
friends_message = _ui("Freundesliste konnte nicht geladen werden.")
|
||
queue_redraw()
|
||
|
||
|
||
func _add_friend_from_input() -> void:
|
||
var username := friend_name_line_edit.text.strip_edges()
|
||
if username.is_empty():
|
||
return
|
||
var ok: bool = await online_manager.add_friend_by_username(username)
|
||
friends_message = _ui("Anfrage an %s gesendet.") % username if ok else _ui("Spielerkennung nicht gefunden oder Anfrage fehlgeschlagen.")
|
||
if ok:
|
||
friend_name_line_edit.clear()
|
||
queue_redraw()
|
||
|
||
|
||
func _friend_row_action(friend: Dictionary) -> void:
|
||
var state := int(friend.get("state", -1))
|
||
var user_id := String(friend.get("id", ""))
|
||
if state == 2:
|
||
friends_message = _ui("Anfrage bestätigt.") if await online_manager.accept_friend(user_id) else _ui("Bestätigung fehlgeschlagen.")
|
||
elif state == 0 and _can_invite_friends():
|
||
friends_message = _ui("Einladung gesendet.") if await online_manager.invite_friend(user_id, multiplayer_manager.private_room_code) else _ui("Einladung fehlgeschlagen oder gerade erst gesendet.")
|
||
elif state == 1:
|
||
friends_message = _ui("Anfrage zurückgezogen.") if await online_manager.remove_friend(user_id) else _ui("Aktion fehlgeschlagen.")
|
||
elif state == 0:
|
||
friends_message = _ui("Freund entfernt.") if await online_manager.remove_friend(user_id) else _ui("Aktion fehlgeschlagen.")
|
||
queue_redraw()
|
||
|
||
|
||
func _can_invite_friends() -> bool:
|
||
return friend_return_mode == "matchmaking" and multiplayer_manager.matchmaking_mode == "private_host" and multiplayer_manager.matchmaking_state == "hosting" and not multiplayer_manager.private_room_code.is_empty()
|
||
|
||
|
||
func _create_promo_code_input() -> void:
|
||
promo_code_line_edit = LineEdit.new()
|
||
promo_code_line_edit.position = promo_code_input_rect.position + Vector2(14, 5)
|
||
promo_code_line_edit.size = promo_code_input_rect.size - Vector2(28, 10)
|
||
promo_code_line_edit.max_length = 24
|
||
promo_code_line_edit.placeholder_text = "CODE EINGEBEN"
|
||
promo_code_line_edit.alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||
promo_code_line_edit.add_theme_color_override("font_color", Color("e8fbff"))
|
||
promo_code_line_edit.add_theme_color_override("font_placeholder_color", Color("7699a8"))
|
||
var input_style := StyleBoxFlat.new()
|
||
input_style.bg_color = Color("081a27")
|
||
input_style.border_color = Color("4ab8d1")
|
||
input_style.set_border_width_all(1)
|
||
input_style.set_content_margin_all(10)
|
||
promo_code_line_edit.add_theme_stylebox_override("normal", input_style)
|
||
promo_code_line_edit.add_theme_stylebox_override("focus", input_style)
|
||
promo_code_line_edit.text_changed.connect(func(value: String) -> void:
|
||
var normalized := value.to_upper().replace(" ", "")
|
||
if value != normalized:
|
||
promo_code_line_edit.text = normalized
|
||
promo_code_line_edit.caret_column = normalized.length()
|
||
)
|
||
promo_code_line_edit.text_submitted.connect(func(_value: String) -> void:
|
||
_submit_promo_code()
|
||
)
|
||
add_child(promo_code_line_edit)
|
||
promo_code_line_edit.hide()
|
||
if online_manager.is_authenticated():
|
||
_on_online_authenticated("", "")
|
||
|
||
|
||
func _on_online_authenticated(_user_id: String, _username: String) -> void:
|
||
var entitlements: Dictionary = await online_manager.get_promo_unlocks()
|
||
_apply_promo_unlocks(entitlements)
|
||
|
||
|
||
func _apply_promo_unlocks(entitlements: Dictionary) -> void:
|
||
if bool(entitlements.get("all_towers", false)):
|
||
unlocked_towers = []
|
||
for tower_type in TOWER_DATA.size():
|
||
unlocked_towers.append(tower_type)
|
||
if bool(entitlements.get("all_maps", false)):
|
||
unlocked_maps = []
|
||
for map_index in MAP_NAMES.size():
|
||
unlocked_maps.append(map_index)
|
||
_save_progress()
|
||
queue_redraw()
|
||
|
||
|
||
func _submit_promo_code() -> void:
|
||
if promo_code_pending:
|
||
return
|
||
if not online_manager.is_authenticated():
|
||
promo_code_message = "Für das Einlösen ist eine Internetverbindung erforderlich."
|
||
queue_redraw()
|
||
return
|
||
var code := promo_code_line_edit.text.strip_edges().to_upper().replace("-", "")
|
||
if code.is_empty():
|
||
promo_code_message = "Bitte gib einen Code ein."
|
||
queue_redraw()
|
||
return
|
||
promo_code_pending = true
|
||
promo_code_message = "CODE WIRD GEPRÜFT …"
|
||
queue_redraw()
|
||
var result: Dictionary = await online_manager.redeem_unlock_code(code)
|
||
promo_code_pending = false
|
||
if bool(result.get("accepted", false)):
|
||
_apply_promo_unlocks(result)
|
||
var reward := str(result.get("reward", ""))
|
||
promo_code_message = "Freigeschaltet: %s" % _promo_reward_label(reward)
|
||
promo_code_line_edit.clear()
|
||
else:
|
||
match str(result.get("error", "invalid")):
|
||
"offline": promo_code_message = "Server nicht erreichbar. Bitte später erneut versuchen."
|
||
"already_redeemed": promo_code_message = "Dieser Code wurde bereits eingelöst."
|
||
"exhausted": promo_code_message = "Dieser Code wurde bereits vollständig verwendet."
|
||
"rate_limited": promo_code_message = "Zu viele Versuche. Bitte warte kurz."
|
||
_: promo_code_message = "Code ungültig oder nicht mehr verfügbar."
|
||
queue_redraw()
|
||
|
||
|
||
func _promo_reward_label(reward: String) -> String:
|
||
match reward:
|
||
"all_towers": return "alle Türme"
|
||
"all_maps": return "alle Karten"
|
||
"all": return "alle Türme und Karten"
|
||
_: return "Bonus"
|
||
|
||
|
||
func _process(delta: float) -> void:
|
||
elapsed += delta
|
||
if coop_online_mode and delta > 0.2:
|
||
online_frame_stall_count += 1
|
||
var now_ms := Time.get_ticks_msec()
|
||
if now_ms - online_frame_stall_report_ms > 10000:
|
||
print("ONLINE DIAG frame_stall_ms=%d count=%d" % [roundi(delta * 1000.0), online_frame_stall_count])
|
||
online_frame_stall_report_ms = now_ms
|
||
online_frame_stall_count = 0
|
||
if status_timer > 0.0:
|
||
status_timer -= delta
|
||
if game_mode == "playing" or (game_mode == "paused" and coop_online_mode):
|
||
if auto_wave_countdown > 0.0:
|
||
auto_wave_countdown -= delta
|
||
if auto_wave_countdown <= 0.0 and auto_start_waves and not wave_active and (endless_mode or wave < MAX_WAVES):
|
||
_start_wave()
|
||
if coop_online_mode:
|
||
_update_online_interpolation(delta)
|
||
_update_online_projectiles(delta)
|
||
_update_effects(delta)
|
||
else:
|
||
var simulation_delta := delta * game_speed
|
||
_update_wave(simulation_delta)
|
||
_update_enemies(simulation_delta)
|
||
_update_towers(simulation_delta)
|
||
_update_projectiles(simulation_delta)
|
||
_update_effects(simulation_delta)
|
||
queue_redraw()
|
||
|
||
|
||
func _update_wave(delta: float) -> void:
|
||
if not wave_active:
|
||
return
|
||
if enemies_to_spawn > 0:
|
||
spawn_timer -= delta
|
||
if spawn_timer <= 0.0:
|
||
_spawn_enemy()
|
||
enemies_to_spawn -= 1
|
||
spawn_timer = maxf(0.24, (0.82 - wave * 0.025) * float(_difficulty().spawn_delay))
|
||
elif enemies.is_empty():
|
||
wave_active = false
|
||
if wave >= MAX_WAVES and not endless_mode:
|
||
_offer_endless_mode()
|
||
return
|
||
var bonus := int((55 + wave * 8) * float(_difficulty().income))
|
||
_award_credits(0, bonus)
|
||
if coop_mode:
|
||
_award_credits(1, bonus)
|
||
_show_status("WELLE %d ABGEWEHRT +%d" % [wave, bonus])
|
||
if wave > best_wave:
|
||
best_wave = wave
|
||
_save_progress()
|
||
if auto_start_waves and (wave < MAX_WAVES or endless_mode) and not coop_online_mode:
|
||
auto_wave_countdown = 1.4
|
||
_show_status("WELLE ABGEWEHRT • NÄCHSTE WELLE IN 1,4 S")
|
||
|
||
|
||
func _spawn_enemy() -> void:
|
||
var schedule_entry: Dictionary = wave_type_schedule[wave_spawn_index] if wave_spawn_index < wave_type_schedule.size() else {}
|
||
var is_boss := bool(schedule_entry.get("boss", wave > 0 and wave % BOSS_WAVE_INTERVAL == 0 and enemies_to_spawn <= (2 if coop_mode else 1)))
|
||
var type := int(schedule_entry.get("type", _enemy_type_for_wave(wave, randf(), is_boss)))
|
||
wave_spawn_index += 1
|
||
var data: Dictionary = ENEMY_DATA[type]
|
||
var health_scale := _enemy_health_scale(wave, is_boss) * enemy_strength_multiplier
|
||
var has_shield := bool(data.get("default_shield", false)) or (wave >= 4 and randf() < minf(0.42, 0.10 + wave * 0.018 + selected_difficulty * 0.06))
|
||
var max_shield_hp: float = data.hp * health_scale * 0.55 if has_shield else 0.0
|
||
var route_id := 0
|
||
var route := path_points
|
||
if coop_mode:
|
||
route_id = next_spawn_route
|
||
next_spawn_route = (next_spawn_route + 1) % coop_paths.size()
|
||
route = coop_paths[route_id]
|
||
enemies.append({
|
||
"id": next_enemy_id,
|
||
"type": type,
|
||
"pos": route[0],
|
||
"path_index": 1,
|
||
"route_id": route_id,
|
||
"hp": data.hp * health_scale,
|
||
"max_hp": data.hp * health_scale,
|
||
"base_max_hp": data.hp * _enemy_health_scale(wave, is_boss),
|
||
"speed": data.speed * (1.0 + wave * 0.012) * (0.82 if is_boss else 1.0) * float(_difficulty().speed),
|
||
"reward": int((data.reward + wave) * (4 if is_boss else 1) * float(_difficulty().income)),
|
||
"angle": 0.0,
|
||
"flash": 0.0,
|
||
"boss": is_boss,
|
||
"size_scale": 1.34 if is_boss else 1.0,
|
||
"shield": has_shield,
|
||
"shield_hp": max_shield_hp,
|
||
"max_shield_hp": max_shield_hp,
|
||
"shield_resistance": float(data.shield_resistance)
|
||
})
|
||
if is_boss:
|
||
_show_status("WARNUNG: KOMMANDOSCHIFF IM ANFLUG")
|
||
next_enemy_id += 1
|
||
|
||
|
||
func _enemy_type_for_wave(for_wave: int, roll: float, is_boss: bool) -> int:
|
||
if is_boss:
|
||
return 8 if for_wave >= 10 else 5
|
||
var advanced_offset := 2 if selected_difficulty == 2 else (1 if selected_difficulty == 1 else 0)
|
||
if for_wave >= 10 and roll >= 0.96:
|
||
return 8
|
||
if for_wave >= 8 and roll >= 0.84 and roll < 0.94:
|
||
return 7
|
||
if for_wave >= 3 and roll >= 0.52 and roll < 0.64:
|
||
return 6
|
||
if for_wave >= 10 - advanced_offset and roll >= 0.94:
|
||
return 5
|
||
if for_wave >= 7 - advanced_offset and roll >= 0.80:
|
||
return 4
|
||
if for_wave >= 4 - advanced_offset and roll >= 0.65:
|
||
return 3
|
||
if for_wave >= 7 and roll >= 0.45:
|
||
return 2
|
||
if for_wave >= 3 and roll >= 0.28:
|
||
return 1
|
||
return 0
|
||
|
||
|
||
func _wave_population(for_wave: int) -> int:
|
||
if for_wave <= 0:
|
||
return 0
|
||
var population_scale := 1.0 + maxf(0.0, for_wave - 3.0) * 0.08
|
||
if selected_difficulty == 2:
|
||
population_scale += maxf(0.0, for_wave - 4.0) * 0.025
|
||
if for_wave > MAX_WAVES:
|
||
population_scale += (for_wave - MAX_WAVES) * 0.035
|
||
return ceili((7 + for_wave * 2) * float(_difficulty().count) * population_scale) * (2 if coop_mode else 1)
|
||
|
||
|
||
func _build_wave_schedule(for_wave: int, count: int) -> Array[Dictionary]:
|
||
var schedule: Array[Dictionary] = []
|
||
var boss_count := mini(2 if coop_mode else 1, count) if for_wave > 0 and for_wave % BOSS_WAVE_INTERVAL == 0 else 0
|
||
for index in count:
|
||
var is_boss := index >= count - boss_count
|
||
var roll := fposmod(float(index + 1) * 0.61803398875 + float(for_wave) * 0.38196601125, 1.0)
|
||
var type := _enemy_type_for_wave(for_wave, roll, is_boss)
|
||
schedule.append({"type": type, "boss": is_boss})
|
||
return schedule
|
||
|
||
|
||
func _enemy_category(enemy_type: int) -> String:
|
||
return str(ENEMY_DATA[clampi(enemy_type, 0, ENEMY_DATA.size() - 1)].category)
|
||
|
||
|
||
func _tower_target_class(tower_type: int) -> String:
|
||
return str(TOWER_DATA[clampi(tower_type, 0, TOWER_DATA.size() - 1)].get("target_class", "both"))
|
||
|
||
|
||
func _tower_display_order() -> Array[int]:
|
||
var ordered_types: Array[int] = []
|
||
for target_class in ["air", "ground", "both"]:
|
||
for tower_type in TOWER_DATA.size():
|
||
if _tower_target_class(tower_type) == target_class:
|
||
ordered_types.append(tower_type)
|
||
return ordered_types
|
||
|
||
|
||
func _tower_can_target(tower_type: int, enemy_type: int) -> bool:
|
||
var target_class := _tower_target_class(tower_type)
|
||
return target_class == "both" or target_class == _enemy_category(enemy_type)
|
||
|
||
|
||
func _tower_target_label(tower_type: int) -> String:
|
||
match _tower_target_class(tower_type):
|
||
"air": return "LUFT"
|
||
"ground": return "BODEN"
|
||
_: return "LUFT + BODEN"
|
||
|
||
|
||
func _set_enemy_strength_multiplier(value: float) -> void:
|
||
if coop_online_mode:
|
||
_show_status("TESTMULTIPLIKATOR IST IM ONLINE-KOOP DEAKTIVIERT")
|
||
return
|
||
enemy_strength_multiplier = clampf(roundf(value * 10.0) / 10.0, 0.5, 3.0)
|
||
for enemy in enemies:
|
||
var health_ratio := clampf(float(enemy.hp) / maxf(1.0, float(enemy.max_hp)), 0.0, 1.0)
|
||
var adjusted_max := float(enemy.get("base_max_hp", enemy.max_hp)) * enemy_strength_multiplier
|
||
enemy.max_hp = adjusted_max
|
||
enemy.hp = adjusted_max * health_ratio
|
||
_show_status("TEST: GEGNERSTÄRKE ×%.1f" % enemy_strength_multiplier)
|
||
queue_redraw()
|
||
|
||
|
||
func _adjust_core_crystals(amount: int) -> void:
|
||
core_crystals = clampi(core_crystals + amount, 0, 999999)
|
||
_save_progress()
|
||
_show_status("TEST: KERNKRISTALLE %s25 • STAND %d" % ["+" if amount >= 0 else "−", core_crystals])
|
||
queue_redraw()
|
||
|
||
|
||
func _enemy_health_scale(for_wave: int, is_boss: bool) -> float:
|
||
var early_ramp := maxf(0.0, for_wave - 1.0) * 0.20
|
||
var post_four_ramp := maxf(0.0, for_wave - 3.0) * 0.09
|
||
var hard_late_ramp := maxf(0.0, for_wave - 5.0) * 0.055 if selected_difficulty == 2 else 0.0
|
||
var endless_ramp := maxf(0.0, for_wave - MAX_WAVES) * 0.035 if endless_mode else 0.0
|
||
return (1.0 + early_ramp + post_four_ramp + hard_late_ramp + endless_ramp) * (3.25 if is_boss else 1.0) * float(_difficulty().hp)
|
||
|
||
|
||
func _update_enemies(delta: float) -> void:
|
||
for enemy in enemies.duplicate():
|
||
if enemy.flash > 0.0:
|
||
enemy.flash -= delta
|
||
var route: PackedVector2Array = _enemy_route(enemy)
|
||
var target: Vector2 = route[enemy.path_index]
|
||
var direction: Vector2 = enemy.pos.direction_to(target)
|
||
enemy.angle = direction.angle()
|
||
enemy.pos += direction * enemy.speed * delta
|
||
if enemy.pos.distance_to(target) < 9.0:
|
||
enemy.path_index += 1
|
||
if enemy.path_index >= route.size():
|
||
enemies.erase(enemy)
|
||
var enemy_data: Dictionary = ENEMY_DATA[int(enemy.type)]
|
||
reactor_lives -= int(enemy_data.breach_damage) + (4 if enemy.boss else 0)
|
||
if coop_mode:
|
||
multiplayer_manager.shared_reactor_lives = maxi(0, reactor_lives)
|
||
_spawn_burst(route[-1], Color("54dfff"), 16)
|
||
if reactor_lives <= 0:
|
||
_game_over()
|
||
|
||
|
||
func _update_towers(delta: float) -> void:
|
||
for tower in towers:
|
||
tower.cooldown -= delta
|
||
var target := _find_target(tower)
|
||
if target.is_empty():
|
||
continue
|
||
tower.angle = _tower_aim_angle(tower.type, tower.pos, target.pos)
|
||
if tower.cooldown <= 0.0:
|
||
_fire_tower(tower, target)
|
||
tower.cooldown = _tower_cooldown(tower.type, tower.level)
|
||
|
||
|
||
func _find_target(tower: Dictionary) -> Dictionary:
|
||
var best: Dictionary = {}
|
||
var priority := "shield" if int(tower.type) == 1 else str(tower.get("target_priority", "first"))
|
||
var best_progress := -INF if priority != "last" else INF
|
||
var best_hp := -1.0
|
||
var best_is_shielded := false
|
||
var effective_range := _tower_range(tower.type, tower.level)
|
||
for enemy in enemies:
|
||
if _tower_can_target(tower.type, int(enemy.type)) and tower.pos.distance_to(enemy.pos) <= effective_range:
|
||
var route: PackedVector2Array = _enemy_route(enemy)
|
||
var progress: float = enemy.path_index * 1000.0 - enemy.pos.distance_to(route[enemy.path_index])
|
||
var enemy_hp := float(enemy.hp)
|
||
var is_shielded := bool(enemy.get("shield", false))
|
||
var is_better := best.is_empty()
|
||
match priority:
|
||
"last": is_better = is_better or progress < best_progress
|
||
"strongest": is_better = is_better or enemy_hp > best_hp
|
||
"shield":
|
||
is_better = is_better or (is_shielded and not best_is_shielded)
|
||
if is_shielded == best_is_shielded and not best.is_empty():
|
||
is_better = progress > best_progress
|
||
_:
|
||
is_better = is_better or progress > best_progress
|
||
if is_better:
|
||
best_progress = progress
|
||
best_hp = enemy_hp
|
||
best_is_shielded = is_shielded
|
||
best = enemy
|
||
return best
|
||
|
||
|
||
func _tower_priority_label(priority: String) -> String:
|
||
match priority:
|
||
"shield": return "SCHILD ZUERST"
|
||
"last": return "LETZTE"
|
||
"strongest": return "MEISTE HP"
|
||
_: return "ERSTE"
|
||
|
||
|
||
func _cycle_selected_tower_priority() -> void:
|
||
if selected_tower_index < 0 or selected_tower_index >= towers.size():
|
||
return
|
||
var tower: Dictionary = towers[selected_tower_index]
|
||
if int(tower.type) == 1:
|
||
_show_status("LASER VISIERT SCHILDGEGNER IMMER ZUERST AN")
|
||
return
|
||
var priorities := ["first", "last", "strongest", "shield"]
|
||
var current := priorities.find(str(tower.get("target_priority", "first")))
|
||
tower.target_priority = priorities[(current + 1) % priorities.size()]
|
||
_show_status("TURMZIEL: %s" % _tower_priority_label(str(tower.target_priority)))
|
||
queue_redraw()
|
||
|
||
|
||
func _fire_tower(tower: Dictionary, target: Dictionary) -> void:
|
||
var data: Dictionary = TOWER_DATA[tower.type]
|
||
var damage := _tower_damage(tower.type, tower.level)
|
||
if _tower_target_class(tower.type) == "both":
|
||
damage *= float(data.get("both_damage_scale", 0.7))
|
||
if tower.type == 1:
|
||
_apply_damage(target, damage, int(tower.get("owner", 0)), tower.type)
|
||
beams.append({"from": tower.pos, "to": target.pos, "ttl": 0.09, "color": data.color})
|
||
elif tower.type == 5:
|
||
var direction: Vector2 = tower.pos.direction_to(target.pos)
|
||
var beam_end: Vector2 = tower.pos + direction * _tower_range(tower.type, tower.level)
|
||
for enemy in enemies.duplicate():
|
||
if not _tower_can_target(tower.type, int(enemy.type)):
|
||
continue
|
||
var offset: Vector2 = enemy.pos - tower.pos
|
||
var along_beam: float = offset.dot(direction)
|
||
var across_beam: float = absf(offset.cross(direction))
|
||
if along_beam >= 0.0 and along_beam <= _tower_range(tower.type, tower.level) and across_beam <= 18.0:
|
||
_apply_damage(enemy, damage, int(tower.get("owner", 0)), tower.type)
|
||
beams.append({"from": tower.pos, "to": beam_end, "ttl": 0.14, "color": data.color, "piercing": true})
|
||
else:
|
||
projectiles.append({
|
||
"pos": tower.pos,
|
||
"target_id": target.id,
|
||
"damage": damage,
|
||
"speed": data.shot_speed,
|
||
"type": tower.type,
|
||
"owner": int(tower.get("owner", 0)),
|
||
"color": data.color
|
||
})
|
||
|
||
|
||
func _update_projectiles(delta: float) -> void:
|
||
for shot in projectiles.duplicate():
|
||
var target := _enemy_by_id(shot.target_id)
|
||
if target.is_empty():
|
||
projectiles.erase(shot)
|
||
continue
|
||
var direction: Vector2 = shot.pos.direction_to(target.pos)
|
||
shot.pos += direction * shot.speed * delta
|
||
if shot.pos.distance_to(target.pos) < 14.0:
|
||
if shot.type in [2, 3, 4]:
|
||
var blast_radius := 76.0 if shot.type == 2 else (92.0 if shot.type == 3 else 118.0)
|
||
var splash_factor := 0.45 if shot.type == 2 else (0.62 if shot.type == 3 else 1.0)
|
||
for enemy in enemies.duplicate():
|
||
if _tower_can_target(int(shot.type), int(enemy.type)) and enemy.pos.distance_to(target.pos) < blast_radius:
|
||
_apply_damage(enemy, shot.damage if enemy.id == target.id else shot.damage * splash_factor, int(shot.get("owner", 0)), int(shot.type))
|
||
var blast_color: Color = Color("ff9b4a") if shot.type == 2 else (Color("db65ff") if shot.type == 3 else Color("71eaff"))
|
||
_spawn_burst(target.pos, blast_color, 14 if shot.type == 2 else 18)
|
||
else:
|
||
_apply_damage(target, shot.damage, int(shot.get("owner", 0)), int(shot.type))
|
||
_spawn_burst(target.pos, shot.color, 4)
|
||
projectiles.erase(shot)
|
||
|
||
|
||
func _apply_damage(enemy: Dictionary, amount: float, owner := 0, weapon_type := -1) -> void:
|
||
if not enemies.has(enemy):
|
||
return
|
||
var applied_damage := amount
|
||
if bool(enemy.get("shield", false)):
|
||
if weapon_type == 1:
|
||
var shield_hp := float(enemy.get("shield_hp", enemy.max_hp * 0.55))
|
||
var shield_damage := minf(shield_hp, amount)
|
||
shield_hp -= shield_damage
|
||
enemy.shield_hp = shield_hp
|
||
if shield_hp <= 0.0:
|
||
enemy.shield = false
|
||
applied_damage = amount - shield_damage
|
||
else:
|
||
applied_damage = 0.0
|
||
elif weapon_type != 5:
|
||
applied_damage *= float(enemy.get("shield_resistance", 0.28))
|
||
enemy.hp -= applied_damage
|
||
enemy.flash = 0.06
|
||
if enemy.hp <= 0.0:
|
||
_award_credits(owner, int(enemy.reward))
|
||
_spawn_burst(enemy.pos, Color("ff6b3d"), 11 + enemy.type * 4)
|
||
enemies.erase(enemy)
|
||
|
||
|
||
func _enemy_route(enemy: Dictionary) -> PackedVector2Array:
|
||
if coop_mode and not coop_paths.is_empty():
|
||
return coop_paths[clampi(int(enemy.get("route_id", 0)), 0, coop_paths.size() - 1)]
|
||
return path_points
|
||
|
||
|
||
func _enemy_by_id(id: int) -> Dictionary:
|
||
for enemy in enemies:
|
||
if enemy.id == id:
|
||
return enemy
|
||
return {}
|
||
|
||
|
||
func _spawn_burst(position: Vector2, color: Color, count: int) -> void:
|
||
for index in count:
|
||
var angle := randf() * TAU
|
||
var speed := randf_range(45.0, 180.0)
|
||
particles.append({"pos": position, "vel": Vector2.from_angle(angle) * speed, "ttl": randf_range(0.22, 0.55), "color": color})
|
||
|
||
|
||
func _update_effects(delta: float) -> void:
|
||
for beam in beams.duplicate():
|
||
beam.ttl -= delta
|
||
if beam.ttl <= 0.0:
|
||
beams.erase(beam)
|
||
for particle in particles.duplicate():
|
||
particle.pos += particle.vel * delta
|
||
particle.vel *= 0.94
|
||
particle.ttl -= delta
|
||
if particle.ttl <= 0.0:
|
||
particles.erase(particle)
|
||
|
||
|
||
func _unhandled_input(event: InputEvent) -> void:
|
||
if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_F9 and game_mode in ["playing", "paused"]:
|
||
developer_panel_open = not developer_panel_open
|
||
queue_redraw()
|
||
return
|
||
if event is InputEventKey and event.pressed and not event.echo and event.keycode in [KEY_ESCAPE, KEY_BACK, KEY_Q]:
|
||
if game_mode == "promo_code":
|
||
game_mode = "menu"
|
||
promo_code_line_edit.hide()
|
||
queue_redraw()
|
||
return
|
||
if audio_settings_open:
|
||
audio_settings_open = false
|
||
_save_audio_settings()
|
||
queue_redraw()
|
||
return
|
||
if game_mode == "menu":
|
||
get_tree().quit()
|
||
return
|
||
if audio_settings_open and event is InputEventMouseMotion and (event.button_mask & MOUSE_BUTTON_MASK_LEFT) != 0:
|
||
_set_master_volume((event.position.x - audio_slider_rect.position.x) / audio_slider_rect.size.x)
|
||
return
|
||
if audio_settings_open and event is InputEventScreenDrag:
|
||
_set_master_volume((event.position.x - audio_slider_rect.position.x) / audio_slider_rect.size.x)
|
||
return
|
||
if audio_settings_open and ((event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and not event.pressed) or (event is InputEventScreenTouch and not event.pressed)):
|
||
_save_audio_settings()
|
||
return
|
||
if game_mode == "profile_name_entry" and event is InputEventKey and event.pressed and not event.echo:
|
||
if event.keycode == KEY_ESCAPE:
|
||
_hide_virtual_keyboard()
|
||
game_mode = "profile"
|
||
elif event.keycode == KEY_BACKSPACE:
|
||
profile_name_input = profile_name_input.left(maxi(0, profile_name_input.length() - 1))
|
||
elif event.keycode == KEY_ENTER:
|
||
_save_profile_name()
|
||
elif event.unicode > 0 and profile_name_input.length() < AccountManager.MAX_DISPLAY_NAME_LENGTH:
|
||
var character := char(event.unicode)
|
||
var allowed := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 _-ÄÖÜäöüß"
|
||
if allowed.contains(character):
|
||
profile_name_input += character
|
||
queue_redraw()
|
||
return
|
||
if game_mode == "room_code_entry" and event is InputEventKey and event.pressed and not event.echo:
|
||
if event.keycode == KEY_ESCAPE:
|
||
_hide_virtual_keyboard()
|
||
game_mode = "multiplayer_menu"
|
||
elif event.keycode == KEY_BACKSPACE:
|
||
room_code_input = room_code_input.left(maxi(0, room_code_input.length() - 1))
|
||
elif event.keycode == KEY_ENTER:
|
||
_join_entered_room()
|
||
elif event.unicode > 0 and room_code_input.length() < 6:
|
||
var character := char(event.unicode).to_upper()
|
||
if "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".contains(character):
|
||
room_code_input += character
|
||
queue_redraw()
|
||
return
|
||
if game_mode == "friends" and event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||
_close_friends()
|
||
return
|
||
if event is InputEventKey and event.pressed and not event.echo and event.keycode in [KEY_P, KEY_ESCAPE] and game_mode in ["playing", "paused"]:
|
||
_toggle_pause()
|
||
return
|
||
if event is InputEventScreenTouch and event.pressed:
|
||
cursor_screen_position = event.position
|
||
cursor_position = event.position + camera_offset
|
||
_handle_pointer(event.position, true)
|
||
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||
cursor_screen_position = event.position
|
||
cursor_position = event.position + camera_offset
|
||
_handle_pointer(event.position, false)
|
||
elif event is InputEventMouseMotion:
|
||
cursor_screen_position = event.position
|
||
cursor_position = event.position + camera_offset
|
||
|
||
|
||
func _handle_pointer(position: Vector2, is_touch := false) -> void:
|
||
_play_ui_sound()
|
||
if developer_panel_open and game_mode in ["playing", "paused"]:
|
||
if developer_strength_down_button.has_point(position):
|
||
_set_enemy_strength_multiplier(enemy_strength_multiplier - 0.1)
|
||
return
|
||
if developer_strength_up_button.has_point(position):
|
||
_set_enemy_strength_multiplier(enemy_strength_multiplier + 0.1)
|
||
return
|
||
if developer_crystals_down_button.has_point(position):
|
||
_adjust_core_crystals(-25)
|
||
return
|
||
if developer_crystals_up_button.has_point(position):
|
||
_adjust_core_crystals(25)
|
||
return
|
||
if developer_panel_rect.has_point(position):
|
||
return
|
||
if audio_settings_open:
|
||
if audio_close_button.has_point(position) or not audio_panel_rect.has_point(position):
|
||
audio_settings_open = false
|
||
_save_audio_settings()
|
||
elif language_button_de.has_point(position):
|
||
_set_ui_language("de")
|
||
elif language_button_en.has_point(position):
|
||
_set_ui_language("en")
|
||
elif audio_slider_rect.grow(12.0).has_point(position):
|
||
_set_master_volume((position.x - audio_slider_rect.position.x) / audio_slider_rect.size.x)
|
||
_save_audio_settings()
|
||
queue_redraw()
|
||
return
|
||
if game_mode == "menu":
|
||
if not pending_match_invite.is_empty() and Rect2(470, 226, 660, 54).has_point(position):
|
||
room_code_input = String(pending_match_invite.get("room_code", ""))
|
||
pending_match_invite.clear()
|
||
_join_entered_room()
|
||
return
|
||
if update_notice_visible and update_button.has_point(position):
|
||
OS.shell_open(str(available_update.get("url", "https://void-bastion.gergames.de/")))
|
||
return
|
||
if update_notice_visible and update_dismiss_button.has_point(position):
|
||
update_notice_visible = false
|
||
queue_redraw()
|
||
return
|
||
if profile_button.has_point(position):
|
||
game_mode = "profile"
|
||
queue_redraw()
|
||
return
|
||
if audio_settings_button.has_point(position):
|
||
audio_settings_open = true
|
||
queue_redraw()
|
||
return
|
||
if exit_button.has_point(position):
|
||
get_tree().quit()
|
||
return
|
||
if promo_code_button.has_point(position):
|
||
game_mode = "promo_code"
|
||
promo_code_message = ""
|
||
promo_code_line_edit.show()
|
||
promo_code_line_edit.grab_focus()
|
||
queue_redraw()
|
||
return
|
||
if shop_button.has_point(position):
|
||
store_return_mode = "menu"
|
||
store_tab = "towers"
|
||
game_mode = "store"
|
||
queue_redraw()
|
||
return
|
||
if menu_step == "difficulty" and map_step_back_button.has_point(position):
|
||
menu_step = "map"
|
||
queue_redraw()
|
||
return
|
||
if menu_step == "map":
|
||
for index in map_buttons.size():
|
||
if map_buttons[index].has_point(position):
|
||
var map_index := index
|
||
if not unlocked_maps.has(map_index):
|
||
store_return_mode = "menu"
|
||
store_tab = "maps"
|
||
game_mode = "store"
|
||
queue_redraw()
|
||
return
|
||
_select_map(map_index)
|
||
menu_step = "difficulty"
|
||
queue_redraw()
|
||
return
|
||
else:
|
||
for index in difficulty_buttons.size():
|
||
if difficulty_buttons[index].has_point(position):
|
||
selected_difficulty = index
|
||
_save_progress()
|
||
queue_redraw()
|
||
return
|
||
if menu_step == "difficulty" and start_button.has_point(position):
|
||
_start_game()
|
||
elif leaderboard_button.has_point(position):
|
||
_open_leaderboard()
|
||
elif multiplayer_button.has_point(position):
|
||
game_mode = "multiplayer_menu"
|
||
queue_redraw()
|
||
return
|
||
if game_mode == "promo_code":
|
||
if promo_code_cancel_button.has_point(position):
|
||
game_mode = "menu"
|
||
promo_code_line_edit.hide()
|
||
elif promo_code_redeem_button.has_point(position):
|
||
_submit_promo_code()
|
||
elif promo_code_input_rect.has_point(position):
|
||
promo_code_line_edit.grab_focus()
|
||
queue_redraw()
|
||
return
|
||
if game_mode == "store":
|
||
if shop_back_button.has_point(position):
|
||
game_mode = store_return_mode
|
||
queue_redraw()
|
||
return
|
||
if store_tower_tab_button.has_point(position):
|
||
store_tab = "towers"
|
||
queue_redraw()
|
||
return
|
||
if store_map_tab_button.has_point(position):
|
||
store_tab = "maps"
|
||
queue_redraw()
|
||
return
|
||
if store_tab == "maps":
|
||
for index in map_shop_purchase_buttons.size():
|
||
if map_shop_purchase_buttons[index].has_point(position):
|
||
_buy_map_with_crystals(index + 3)
|
||
queue_redraw()
|
||
return
|
||
else:
|
||
for index in shop_purchase_buttons.size():
|
||
if shop_purchase_buttons[index].has_point(position):
|
||
_buy_tower_with_crystals(index + 3)
|
||
queue_redraw()
|
||
return
|
||
return
|
||
if game_mode == "leaderboard":
|
||
if leaderboard_back_button.has_point(position):
|
||
game_mode = "menu"
|
||
elif leaderboard_mission_button.has_point(position) and leaderboard_endless:
|
||
leaderboard_endless = false
|
||
_refresh_leaderboard()
|
||
elif leaderboard_endless_button.has_point(position) and not leaderboard_endless:
|
||
leaderboard_endless = true
|
||
_refresh_leaderboard()
|
||
elif leaderboard_refresh_button.has_point(position):
|
||
_refresh_leaderboard()
|
||
elif leaderboard_map_previous_button.has_point(position):
|
||
leaderboard_map = wrapi(leaderboard_map - 1, 0, MAP_NAMES.size())
|
||
_refresh_leaderboard()
|
||
elif leaderboard_map_button.has_point(position) or leaderboard_map_next_button.has_point(position):
|
||
leaderboard_map = wrapi(leaderboard_map + 1, 0, MAP_NAMES.size())
|
||
_refresh_leaderboard()
|
||
elif leaderboard_difficulty_previous_button.has_point(position):
|
||
leaderboard_difficulty = wrapi(leaderboard_difficulty - 1, 0, DIFFICULTY_DATA.size())
|
||
_refresh_leaderboard()
|
||
elif leaderboard_difficulty_button.has_point(position) or leaderboard_difficulty_next_button.has_point(position):
|
||
leaderboard_difficulty = wrapi(leaderboard_difficulty + 1, 0, DIFFICULTY_DATA.size())
|
||
_refresh_leaderboard()
|
||
queue_redraw()
|
||
return
|
||
if game_mode == "multiplayer_menu":
|
||
if not pending_match_invite.is_empty() and Rect2(470, 585, 660, 55).has_point(position):
|
||
room_code_input = String(pending_match_invite.get("room_code", ""))
|
||
pending_match_invite.clear()
|
||
_join_entered_room()
|
||
return
|
||
if multiplayer_back_button.has_point(position):
|
||
game_mode = "menu"
|
||
elif quick_match_button.has_point(position):
|
||
_open_matchmaking("quick")
|
||
elif friend_match_button.has_point(position):
|
||
_open_room_code_entry()
|
||
elif private_match_button.has_point(position):
|
||
_open_private_room()
|
||
elif ai_match_button.has_point(position):
|
||
_start_coop_ai_game()
|
||
elif friends_button.has_point(position):
|
||
_open_friends("multiplayer_menu")
|
||
queue_redraw()
|
||
return
|
||
if game_mode == "friends":
|
||
if friends_back_button.has_point(position):
|
||
_close_friends()
|
||
elif friends_refresh_button.has_point(position):
|
||
_refresh_friends()
|
||
elif friend_add_button.has_point(position):
|
||
_add_friend_from_input()
|
||
elif friend_copy_button.has_point(position) and online_manager.session != null:
|
||
DisplayServer.clipboard_set(String(online_manager.session.username))
|
||
friends_message = _ui("Spielerkennung kopiert.")
|
||
else:
|
||
for index in mini(friend_action_buttons.size(), online_manager.friends.size()):
|
||
if friend_action_buttons[index].has_point(position):
|
||
_friend_row_action(online_manager.friends[index])
|
||
break
|
||
queue_redraw()
|
||
return
|
||
if game_mode == "room_code_entry":
|
||
if multiplayer_back_button.has_point(position):
|
||
_hide_virtual_keyboard()
|
||
game_mode = "multiplayer_menu"
|
||
elif room_code_input_rect.has_point(position):
|
||
_show_room_code_keyboard()
|
||
elif join_room_button.has_point(position):
|
||
_join_entered_room()
|
||
queue_redraw()
|
||
return
|
||
if game_mode == "matchmaking":
|
||
if lobby_ready_button.has_point(position) and multiplayer_manager.matchmaking_state in ["hosting", "matched", "lobby", "reconnected"]:
|
||
multiplayer_manager.toggle_lobby_ready()
|
||
elif friends_button.has_point(position) and multiplayer_manager.matchmaking_mode == "private_host":
|
||
_open_friends("matchmaking")
|
||
elif cancel_search_button.has_point(position) or multiplayer_back_button.has_point(position):
|
||
multiplayer_manager.cancel_search()
|
||
game_mode = "multiplayer_menu"
|
||
queue_redraw()
|
||
return
|
||
if game_mode == "profile":
|
||
if profile_back_button.has_point(position):
|
||
profile_delete_armed = false
|
||
game_mode = "menu"
|
||
elif profile_rename_button.has_point(position):
|
||
_open_profile_name_entry()
|
||
elif profile_support_button.has_point(position):
|
||
OS.shell_open(_support_email_url())
|
||
elif profile_privacy_button.has_point(position):
|
||
OS.shell_open("https://void-bastion.gergames.de/privacy.html")
|
||
elif profile_delete_button.has_point(position):
|
||
if not online_manager.is_authenticated():
|
||
profile_message = "Kontolöschung benötigt eine Verbindung zum GerGames-Server."
|
||
elif not profile_delete_armed:
|
||
profile_delete_armed = true
|
||
profile_message = "Noch einmal bestätigen: Online-Konto, Ranglisten und lokale Werte werden gelöscht."
|
||
else:
|
||
_delete_profile_account()
|
||
queue_redraw()
|
||
return
|
||
if game_mode == "profile_name_entry":
|
||
if profile_back_button.has_point(position):
|
||
_hide_virtual_keyboard()
|
||
game_mode = "profile"
|
||
elif profile_name_input_rect.has_point(position):
|
||
_show_profile_name_keyboard()
|
||
elif profile_name_save_button.has_point(position):
|
||
_save_profile_name()
|
||
queue_redraw()
|
||
return
|
||
if game_mode == "paused":
|
||
if pause_button.has_point(position) or pause_resume_button.has_point(position):
|
||
_toggle_pause()
|
||
elif pause_exit_button.has_point(position):
|
||
_return_to_menu()
|
||
queue_redraw()
|
||
return
|
||
if coop_mode and coop_side_button.has_point(position):
|
||
_toggle_coop_side()
|
||
return
|
||
if game_mode in ["game_over", "victory", "endless_result"]:
|
||
if start_button.has_point(position):
|
||
if game_mode == "victory":
|
||
_return_to_menu()
|
||
elif coop_ai_mode:
|
||
_start_coop_ai_game()
|
||
else:
|
||
_start_game()
|
||
return
|
||
if game_mode == "endless_offer":
|
||
if victory_reward_pending:
|
||
return
|
||
if endless_button.has_point(position):
|
||
_start_endless_mode()
|
||
elif finish_mission_button.has_point(position):
|
||
_mission_complete()
|
||
return
|
||
if not tutorial_completed and tutorial_skip_button.has_point(position):
|
||
_complete_tutorial()
|
||
return
|
||
if selected_tower_type < 0 and selected_tower_index < 0 and ads_manager.can_offer_rewarded() and reward_ad_button.has_point(position):
|
||
ads_manager.request_rewarded_energy()
|
||
return
|
||
if pause_button.has_point(position):
|
||
_toggle_pause()
|
||
return
|
||
if speed_dropdown_open:
|
||
for speed_index in SPEED_OPTIONS.size():
|
||
if _speed_option_rect(speed_index).has_point(position):
|
||
game_speed = SPEED_OPTIONS[speed_index]
|
||
speed_dropdown_open = false
|
||
_show_status("SIMULATION %.0f×" % game_speed)
|
||
queue_redraw()
|
||
return
|
||
speed_dropdown_open = false
|
||
if speed_button.has_point(position):
|
||
if coop_online_mode:
|
||
_show_status("ONLINE-SIMULATION LÄUFT AUF SERVERTEMPO")
|
||
return
|
||
speed_dropdown_open = true
|
||
queue_redraw()
|
||
return
|
||
if auto_wave_button.has_point(position):
|
||
auto_start_waves = not auto_start_waves
|
||
if not auto_start_waves:
|
||
auto_wave_countdown = 0.0
|
||
_show_status("AUTO-WELLENSTART %s" % ("EIN" if auto_start_waves else "AUS"))
|
||
queue_redraw()
|
||
return
|
||
if game_mode == "paused":
|
||
return
|
||
if wave_button.has_point(position):
|
||
_start_wave()
|
||
return
|
||
var tower_display_order := _tower_display_order()
|
||
for index in tower_buttons.size():
|
||
if tower_buttons[index].has_point(position):
|
||
var tower_type: int = tower_display_order[index]
|
||
if not unlocked_towers.has(tower_type):
|
||
store_return_mode = game_mode
|
||
game_mode = "store"
|
||
queue_redraw()
|
||
return
|
||
selected_tower_type = tower_type
|
||
selected_tower_index = -1
|
||
pending_touch_build_position = Vector2(-1, -1)
|
||
if not tutorial_completed and tutorial_step == 0:
|
||
tutorial_step = 1
|
||
return
|
||
if selected_tower_index >= 0:
|
||
if target_priority_button.has_point(position):
|
||
if coop_online_mode:
|
||
_show_status("ZIELPRIORITÄT IST IM ONLINE-KOOP NOCH NICHT EINSTELLBAR")
|
||
else:
|
||
_cycle_selected_tower_priority()
|
||
return
|
||
if upgrade_button.has_point(position):
|
||
_upgrade_selected()
|
||
return
|
||
if sell_button.has_point(position):
|
||
_sell_selected()
|
||
return
|
||
if tower_info_rect.has_point(position):
|
||
return
|
||
var world_position := position + camera_offset
|
||
var existing := _tower_at(world_position)
|
||
if existing >= 0:
|
||
if coop_mode and int(towers[existing].get("owner", 0)) != _local_credit_slot():
|
||
_show_status("PARTNER-TURM • NICHT VERFÜGBAR")
|
||
return
|
||
selected_tower_index = existing
|
||
selected_tower_type = -1
|
||
pending_touch_build_position = Vector2(-1, -1)
|
||
return
|
||
if selected_tower_type >= 0 and position.y > PLAYFIELD_TOP and position.y < PLAYFIELD_BOTTOM:
|
||
var snapped_position := _snap_to_grid(world_position)
|
||
if is_touch and pending_touch_build_position != snapped_position:
|
||
pending_touch_build_position = snapped_position
|
||
_show_status("REICHWEITE PRÜFEN • ERNEUT TIPPEN ZUM BAUEN")
|
||
return
|
||
_place_tower(world_position)
|
||
|
||
|
||
func _select_map(index: int) -> void:
|
||
coop_mode = false
|
||
coop_ai_mode = false
|
||
coop_online_mode = false
|
||
selected_map = clampi(index, 0, MAP_TEXTURES.size() - 1)
|
||
path_points = PackedVector2Array()
|
||
for point in MAP_PATHS[selected_map]:
|
||
path_points.append(point * WORLD_SCALE)
|
||
_validate_route_ends_at_reactor(path_points, selected_map)
|
||
_rebuild_path_cells()
|
||
build_zones.clear()
|
||
for zone in MAP_BUILD_ZONES[selected_map]:
|
||
build_zones.append(Rect2(zone.position * WORLD_SCALE, zone.size * WORLD_SCALE))
|
||
draw_geometry_dirty = true
|
||
camera_offset = Vector2.ZERO
|
||
queue_redraw()
|
||
|
||
|
||
func _configure_coop_arena() -> void:
|
||
coop_paths.clear()
|
||
var left := PackedVector2Array(COOP_LEFT_PATH)
|
||
var right: PackedVector2Array = multiplayer_manager.mirrored_path(left, COOP_WORLD_WIDTH)
|
||
coop_paths.append(left)
|
||
coop_paths.append(right)
|
||
path_points = left
|
||
path_cells.clear()
|
||
for route in coop_paths:
|
||
_add_path_cells(route)
|
||
build_zones = [Rect2(120, PLAYFIELD_TOP, 1280, PLAYFIELD_BOTTOM - PLAYFIELD_TOP), Rect2(1560, PLAYFIELD_TOP, 1280, PLAYFIELD_BOTTOM - PLAYFIELD_TOP)]
|
||
draw_geometry_dirty = true
|
||
camera_offset = Vector2.ZERO
|
||
if left[-1].distance_to(COOP_REACTOR_CENTER) > 1.0 or right[-1].distance_to(COOP_REACTOR_CENTER) > 1.0:
|
||
push_error("Koop-Routen müssen sichtbar im gemeinsamen Reaktor enden.")
|
||
|
||
|
||
func _validate_route_ends_at_reactor(route: PackedVector2Array, map_index: int) -> bool:
|
||
if route.is_empty() or map_index < 0 or map_index >= MAP_REACTOR_ZONES.size():
|
||
push_error("Kartenroute oder Reaktordaten fehlen.")
|
||
return false
|
||
var reactor_center: Vector2 = MAP_REACTOR_ZONES[map_index][0].center
|
||
var is_connected := route[-1].distance_to(reactor_center) <= 1.0
|
||
if not is_connected:
|
||
push_error("Karte %s: Der Laufweg muss exakt im Reaktormittelpunkt enden." % MAP_NAMES[map_index])
|
||
return is_connected
|
||
|
||
|
||
func _toggle_coop_side() -> void:
|
||
selected_tower_type = -1
|
||
selected_tower_index = -1
|
||
pending_touch_build_position = Vector2(-1, -1)
|
||
camera_offset.x = COOP_WORLD_WIDTH - SCREEN.x if camera_offset.x < 1.0 else 0.0
|
||
cursor_position = cursor_screen_position + camera_offset
|
||
var side_name := "PARTNER-SEITE" if coop_online_mode else "KI-SEITE"
|
||
_show_status(side_name if camera_offset.x > 0.0 else "DEINE SEITE")
|
||
|
||
|
||
func _start_coop_ai_game() -> void:
|
||
online_manager.set_presence_status("playing")
|
||
coop_mode = true
|
||
coop_ai_mode = true
|
||
coop_online_mode = false
|
||
local_player_slot = 0
|
||
_configure_coop_arena()
|
||
multiplayer_manager.reset_coop_session(int(_difficulty().start_credits), 30)
|
||
credits = multiplayer_manager.player_credits[0]
|
||
reactor_lives = multiplayer_manager.shared_reactor_lives
|
||
wave = 0
|
||
towers_built_this_run = 0
|
||
auto_wave_countdown = 0.0
|
||
towers.clear()
|
||
enemies.clear()
|
||
projectiles.clear()
|
||
online_projectiles.clear()
|
||
beams.clear()
|
||
particles.clear()
|
||
selected_tower_type = -1
|
||
selected_tower_index = -1
|
||
pending_touch_build_position = Vector2(-1, -1)
|
||
wave_active = false
|
||
endless_mode = false
|
||
mission_reward_granted = false
|
||
last_mission_reward = 0
|
||
victory_reward_pending = false
|
||
next_spawn_route = 0
|
||
game_mode = "playing"
|
||
game_speed = 1.0
|
||
tutorial_step = 3
|
||
ads_manager.begin_mission()
|
||
_run_ai_builder(true)
|
||
_show_status("KOOP-ARENA ONLINE • KI-PARTNER BEREIT")
|
||
|
||
|
||
func _start_coop_online_game(player_slot: int) -> void:
|
||
online_manager.set_presence_status("playing")
|
||
local_player_slot = clampi(player_slot, 0, 1)
|
||
coop_mode = true
|
||
coop_ai_mode = false
|
||
coop_online_mode = true
|
||
_configure_coop_arena()
|
||
wave = 0
|
||
towers.clear()
|
||
enemies.clear()
|
||
projectiles.clear()
|
||
online_projectiles.clear()
|
||
beams.clear()
|
||
particles.clear()
|
||
selected_tower_type = -1
|
||
selected_tower_index = -1
|
||
pending_touch_build_position = Vector2(-1, -1)
|
||
wave_active = false
|
||
endless_mode = false
|
||
mission_reward_granted = false
|
||
last_mission_reward = 0
|
||
victory_reward_pending = false
|
||
game_mode = "playing"
|
||
game_speed = 1.0
|
||
tutorial_step = 3
|
||
camera_offset.x = COOP_WORLD_WIDTH - SCREEN.x if local_player_slot == 1 else 0.0
|
||
ads_manager.begin_mission()
|
||
_apply_online_snapshot(multiplayer_manager.latest_snapshot)
|
||
_show_status("ONLINE-KOOP BEREIT • DU VERTEIDIGST %s" % ("RECHTS" if local_player_slot == 1 else "LINKS"))
|
||
|
||
|
||
func _open_matchmaking(mode: String) -> void:
|
||
multiplayer_manager.reset_coop_session(int(_difficulty().start_credits), 30)
|
||
multiplayer_manager.begin_search(mode, selected_difficulty)
|
||
game_mode = "matchmaking"
|
||
queue_redraw()
|
||
|
||
|
||
func _open_private_room() -> void:
|
||
multiplayer_manager.reset_coop_session(int(_difficulty().start_credits), 30)
|
||
multiplayer_manager.create_private_room(selected_difficulty)
|
||
game_mode = "matchmaking"
|
||
queue_redraw()
|
||
|
||
|
||
func _open_room_code_entry() -> void:
|
||
room_code_input = ""
|
||
game_mode = "room_code_entry"
|
||
_show_room_code_keyboard()
|
||
queue_redraw()
|
||
|
||
|
||
func _join_entered_room() -> void:
|
||
if room_code_input.length() != 6:
|
||
_show_status("DER RAUMCODE MUSS SECHS ZEICHEN HABEN")
|
||
return
|
||
_hide_virtual_keyboard()
|
||
multiplayer_manager.reset_coop_session(int(_difficulty().start_credits), 30)
|
||
multiplayer_manager.join_private_room(room_code_input)
|
||
game_mode = "matchmaking"
|
||
queue_redraw()
|
||
|
||
|
||
func _show_room_code_keyboard() -> void:
|
||
if DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD):
|
||
DisplayServer.virtual_keyboard_show(room_code_input, room_code_input_rect, DisplayServer.KEYBOARD_TYPE_DEFAULT, 6, room_code_input.length(), room_code_input.length())
|
||
|
||
|
||
func _hide_virtual_keyboard() -> void:
|
||
if DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD):
|
||
DisplayServer.virtual_keyboard_hide()
|
||
|
||
|
||
func _app_version() -> String:
|
||
return str(ProjectSettings.get_setting("application/config/version", "Entwicklung"))
|
||
|
||
|
||
func _support_email_url() -> String:
|
||
var subject := "Void Bastion Feedback %s" % _app_version()
|
||
var body := "Version: %s\nPlattform: %s\nSpieler-ID: %s\n\nBeschreibung:\n" % [
|
||
_app_version(),
|
||
OS.get_name(),
|
||
account_manager.short_id(),
|
||
]
|
||
return "mailto:support@gergames.de?subject=%s&body=%s" % [subject.uri_encode(), body.uri_encode()]
|
||
|
||
|
||
func _open_profile_name_entry() -> void:
|
||
profile_name_input = account_manager.display_name
|
||
profile_message = ""
|
||
profile_delete_armed = false
|
||
game_mode = "profile_name_entry"
|
||
_show_profile_name_keyboard()
|
||
queue_redraw()
|
||
|
||
|
||
func _show_profile_name_keyboard() -> void:
|
||
if DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD):
|
||
DisplayServer.virtual_keyboard_show(profile_name_input, profile_name_input_rect, DisplayServer.KEYBOARD_TYPE_DEFAULT, AccountManager.MAX_DISPLAY_NAME_LENGTH, profile_name_input.length(), profile_name_input.length())
|
||
|
||
|
||
func _save_profile_name() -> void:
|
||
var validation_error: String = account_manager.validate_display_name(profile_name_input)
|
||
if not validation_error.is_empty():
|
||
profile_message = validation_error
|
||
queue_redraw()
|
||
return
|
||
profile_message = "Spielername wird gespeichert …"
|
||
queue_redraw()
|
||
var result: Dictionary = await online_manager.update_display_name(profile_name_input)
|
||
if not bool(result.get("accepted", false)):
|
||
profile_message = str(result.get("error", "Der Spielername konnte nicht gespeichert werden."))
|
||
queue_redraw()
|
||
return
|
||
_hide_virtual_keyboard()
|
||
profile_message = "Spielername gespeichert%s." % (" und online synchronisiert" if bool(result.get("online", false)) else "")
|
||
game_mode = "profile"
|
||
queue_redraw()
|
||
|
||
|
||
func _delete_profile_account() -> void:
|
||
profile_delete_armed = false
|
||
profile_message = "Konto wird unwiderruflich gelöscht …"
|
||
queue_redraw()
|
||
var result: Dictionary = await online_manager.delete_online_account()
|
||
if not bool(result.get("accepted", false)):
|
||
profile_message = str(result.get("error", "Das Konto konnte nicht gelöscht werden."))
|
||
queue_redraw()
|
||
return
|
||
best_wave = 0
|
||
best_endless_wave = 0
|
||
core_crystals = 0
|
||
unlocked_towers = [0, 1, 2, 6]
|
||
unlocked_maps = [0, 1, 2]
|
||
_save_progress()
|
||
profile_message = "Online-Konto und lokale Profildaten gelöscht. Ein neues Gastprofil wurde erstellt."
|
||
queue_redraw()
|
||
|
||
|
||
func _open_leaderboard() -> void:
|
||
game_mode = "leaderboard"
|
||
leaderboard_endless = false
|
||
leaderboard_map = selected_map
|
||
leaderboard_difficulty = selected_difficulty
|
||
_refresh_leaderboard()
|
||
|
||
|
||
func _refresh_leaderboard() -> void:
|
||
if leaderboard_loading:
|
||
return
|
||
leaderboard_loading = true
|
||
leaderboard_message = "BESTENLISTE WIRD GELADEN …"
|
||
leaderboard_rows.clear()
|
||
queue_redraw()
|
||
if not online_manager.is_authenticated():
|
||
leaderboard_loading = false
|
||
leaderboard_message = "GERGAMES-SERVER WIRD VERBUNDEN – BITTE GLEICH ERNEUT LADEN"
|
||
queue_redraw()
|
||
return
|
||
leaderboard_rows = await online_manager.load_leaderboard(leaderboard_map, leaderboard_difficulty, leaderboard_endless, 12)
|
||
leaderboard_loading = false
|
||
leaderboard_message = "NOCH KEINE EINTRÄGE" if leaderboard_rows.is_empty() else ""
|
||
queue_redraw()
|
||
|
||
|
||
func _start_game() -> void:
|
||
online_manager.set_presence_status("playing")
|
||
coop_online_mode = false
|
||
credits = int(_difficulty().start_credits)
|
||
reactor_lives = int(_difficulty().lives)
|
||
wave = 0
|
||
towers_built_this_run = 0
|
||
auto_wave_countdown = 0.0
|
||
towers.clear()
|
||
enemies.clear()
|
||
projectiles.clear()
|
||
online_projectiles.clear()
|
||
beams.clear()
|
||
particles.clear()
|
||
selected_tower_type = -1
|
||
selected_tower_index = -1
|
||
pending_touch_build_position = Vector2(-1, -1)
|
||
wave_active = false
|
||
endless_mode = false
|
||
mission_reward_granted = false
|
||
last_mission_reward = 0
|
||
victory_reward_pending = false
|
||
game_mode = "playing"
|
||
game_speed = 1.0
|
||
camera_offset = Vector2.ZERO
|
||
tutorial_step = 0 if not tutorial_completed else 3
|
||
ads_manager.begin_mission()
|
||
_show_status("VERTEIDIGUNG ONLINE")
|
||
|
||
|
||
func _return_to_menu() -> void:
|
||
online_manager.set_presence_status("menu")
|
||
if coop_online_mode:
|
||
online_manager.leave_current_match()
|
||
game_mode = "menu"
|
||
menu_step = "map"
|
||
wave_active = false
|
||
game_speed = 1.0
|
||
enemies_to_spawn = 0
|
||
towers.clear()
|
||
enemies.clear()
|
||
projectiles.clear()
|
||
online_projectiles.clear()
|
||
beams.clear()
|
||
particles.clear()
|
||
selected_tower_type = -1
|
||
selected_tower_index = -1
|
||
pending_touch_build_position = Vector2(-1, -1)
|
||
camera_offset = Vector2.ZERO
|
||
coop_mode = false
|
||
coop_ai_mode = false
|
||
coop_online_mode = false
|
||
_select_map(selected_map)
|
||
queue_redraw()
|
||
|
||
|
||
func _start_wave() -> void:
|
||
if wave_active:
|
||
_show_status("WELLE LÄUFT BEREITS")
|
||
return
|
||
if coop_online_mode:
|
||
online_manager.request_wave_start()
|
||
_show_status("WELLENSTART AN SERVER GESENDET")
|
||
return
|
||
wave += 1
|
||
enemies_to_spawn = _wave_population(wave)
|
||
wave_spawn_index = 0
|
||
wave_type_schedule = _build_wave_schedule(wave, enemies_to_spawn)
|
||
spawn_timer = 0.15
|
||
wave_active = true
|
||
if coop_ai_mode:
|
||
_run_ai_builder(false)
|
||
if not tutorial_completed and tutorial_step >= 2:
|
||
_complete_tutorial()
|
||
_show_status("BOSSWELLE %d" % wave if wave % BOSS_WAVE_INTERVAL == 0 else "WELLE %d NÄHERT SICH" % wave)
|
||
|
||
|
||
func _toggle_pause() -> void:
|
||
if coop_online_mode:
|
||
if game_mode == "playing":
|
||
game_mode = "paused"
|
||
_show_status("ONLINE-PARTIE LÄUFT WÄHREND DER PAUSE WEITER")
|
||
elif game_mode == "paused":
|
||
game_mode = "playing"
|
||
return
|
||
if game_mode == "playing":
|
||
game_mode = "paused"
|
||
elif game_mode == "paused":
|
||
game_mode = "playing"
|
||
|
||
|
||
func _game_over() -> void:
|
||
if game_mode != "playing":
|
||
return
|
||
if endless_mode:
|
||
_endless_run_complete()
|
||
return
|
||
game_mode = "game_over"
|
||
wave_active = false
|
||
if wave > best_wave:
|
||
best_wave = wave
|
||
_record_account_score(false)
|
||
_save_progress()
|
||
|
||
|
||
func _mission_complete() -> void:
|
||
game_mode = "victory"
|
||
wave_active = false
|
||
|
||
|
||
func _offer_endless_mode() -> void:
|
||
game_mode = "endless_offer"
|
||
wave_active = false
|
||
best_wave = maxi(best_wave, wave)
|
||
_record_account_score(false)
|
||
_save_progress()
|
||
victory_reward_pending = true
|
||
ads_manager.show_victory_interstitial()
|
||
|
||
|
||
func _start_endless_mode() -> void:
|
||
endless_mode = true
|
||
game_mode = "playing"
|
||
_show_status("ENDLOSMODUS AKTIV • NÄCHSTE WELLE: %d" % (wave + 1))
|
||
|
||
|
||
func _endless_run_complete() -> void:
|
||
game_mode = "endless_result"
|
||
wave_active = false
|
||
best_wave = maxi(best_wave, wave)
|
||
best_endless_wave = maxi(best_endless_wave, wave)
|
||
_record_account_score(true)
|
||
_save_progress()
|
||
|
||
|
||
func _on_victory_interstitial_finished(_was_shown: bool) -> void:
|
||
if not victory_reward_pending:
|
||
return
|
||
victory_reward_pending = false
|
||
_grant_map_reward()
|
||
_save_progress()
|
||
queue_redraw()
|
||
|
||
|
||
func _record_account_score(was_endless: bool) -> void:
|
||
var score := _calculate_run_score()
|
||
account_manager.record_run(selected_map, selected_difficulty, was_endless, wave, score)
|
||
|
||
|
||
func _calculate_run_score() -> int:
|
||
var base_score := wave * 1000 + maxi(reactor_lives, 0) * 150 + _player_credits(_local_credit_slot()) * 2
|
||
var tower_efficiency := clampf(1.0 + (10.0 - towers_built_this_run) * 0.05, 0.50, 1.50)
|
||
return roundi(base_score * tower_efficiency)
|
||
|
||
|
||
func _grant_map_reward() -> void:
|
||
if mission_reward_granted:
|
||
return
|
||
mission_reward_granted = true
|
||
last_mission_reward = int(_difficulty().crystals)
|
||
core_crystals += last_mission_reward
|
||
_save_progress()
|
||
|
||
|
||
func _buy_tower_with_crystals(tower_type: int) -> void:
|
||
if tower_type < 3 or tower_type >= TOWER_DATA.size():
|
||
return
|
||
if unlocked_towers.has(tower_type):
|
||
_show_status("%s IST BEREITS FREIGESCHALTET" % TOWER_DATA[tower_type].name)
|
||
return
|
||
var price := int(TOWER_DATA[tower_type].unlock_cost)
|
||
if core_crystals < price:
|
||
_show_status("ZU WENIG KERNKRISTALLE • BENÖTIGT: %d" % price)
|
||
return
|
||
core_crystals -= price
|
||
unlocked_towers.append(tower_type)
|
||
unlocked_towers.sort()
|
||
_save_progress()
|
||
_show_status("%s FREIGESCHALTET • IM ARSENAL VERFÜGBAR" % TOWER_DATA[tower_type].name)
|
||
|
||
|
||
func _buy_map_with_crystals(map_index: int) -> void:
|
||
if map_index < 3 or map_index >= MAP_NAMES.size():
|
||
return
|
||
if unlocked_maps.has(map_index):
|
||
_show_status("%s IST BEREITS FREIGESCHALTET" % MAP_NAMES[map_index])
|
||
return
|
||
var price := int(MAP_UNLOCK_COSTS[map_index])
|
||
if core_crystals < price:
|
||
_show_status("ZU WENIG KERNKRISTALLE • BENÖTIGT: %d" % price)
|
||
return
|
||
core_crystals -= price
|
||
unlocked_maps.append(map_index)
|
||
unlocked_maps.sort()
|
||
_save_progress()
|
||
_show_status("%s FREIGESCHALTET • NEUER EINSATZSEKTOR" % MAP_NAMES[map_index])
|
||
|
||
|
||
func _difficulty() -> Dictionary:
|
||
return DIFFICULTY_DATA[clampi(selected_difficulty, 0, DIFFICULTY_DATA.size() - 1)]
|
||
|
||
|
||
func _on_update_available(info: Dictionary) -> void:
|
||
available_update = info.duplicate(true)
|
||
update_notice_visible = true
|
||
queue_redraw()
|
||
|
||
|
||
func _on_energy_reward_granted(amount: int) -> void:
|
||
_award_credits(0, amount)
|
||
_show_status("NOTFALLENERGIE +%d" % amount)
|
||
|
||
|
||
func _complete_tutorial() -> void:
|
||
tutorial_completed = true
|
||
tutorial_step = 3
|
||
_save_progress()
|
||
_show_status("EINFÜHRUNG ABGESCHLOSSEN")
|
||
|
||
|
||
func _place_tower(position: Vector2) -> void:
|
||
position = _snap_to_grid(position)
|
||
if selected_tower_type < 0 or not unlocked_towers.has(selected_tower_type):
|
||
_show_status("TURM ZUERST IM ARSENAL FREISCHALTEN")
|
||
return
|
||
var data: Dictionary = TOWER_DATA[selected_tower_type]
|
||
var credit_slot := _local_credit_slot()
|
||
if _player_credits(credit_slot) < data.cost:
|
||
_show_status("NICHT GENUG ENERGIE")
|
||
return
|
||
if not _is_valid_build_position(position, credit_slot):
|
||
_show_status("BAUPLATZ BLOCKIERT")
|
||
return
|
||
if coop_online_mode:
|
||
online_manager.request_tower_build(selected_tower_type, position)
|
||
selected_tower_type = -1
|
||
pending_touch_build_position = Vector2(-1, -1)
|
||
_show_status("BAUAUFTRAG AN SERVER GESENDET")
|
||
return
|
||
_spend_credits(0, int(data.cost))
|
||
towers_built_this_run += 1
|
||
towers.append({"pos": position, "type": selected_tower_type, "level": 1, "cooldown": 0.15, "angle": 0.0, "owner": 0, "target_priority": "shield" if selected_tower_type == 1 else "first"})
|
||
selected_tower_index = towers.size() - 1
|
||
selected_tower_type = -1
|
||
pending_touch_build_position = Vector2(-1, -1)
|
||
if not tutorial_completed and tutorial_step == 1:
|
||
tutorial_step = 2
|
||
_spawn_burst(position, Color("65eaff"), 12)
|
||
|
||
|
||
func _upgrade_selected() -> void:
|
||
if selected_tower_index < 0 or selected_tower_index >= towers.size():
|
||
return
|
||
var tower: Dictionary = towers[selected_tower_index]
|
||
if coop_online_mode and int(tower.get("owner", -1)) != local_player_slot:
|
||
_show_status("NUR EIGENE TÜRME KÖNNEN VERBESSERT WERDEN")
|
||
return
|
||
if tower.level >= 3:
|
||
_show_status("MAXIMALE STUFE")
|
||
return
|
||
var cost: int = _upgrade_cost(tower)
|
||
if _player_credits(_local_credit_slot()) < cost:
|
||
_show_status("NICHT GENUG ENERGIE")
|
||
return
|
||
if coop_online_mode:
|
||
online_manager.request_tower_upgrade(int(tower.get("id", -1)))
|
||
_show_status("UPGRADE AN SERVER GESENDET")
|
||
return
|
||
_spend_credits(0, cost)
|
||
tower.level += 1
|
||
_spawn_burst(tower.pos, Color("65eaff"), 16)
|
||
|
||
|
||
func _sell_selected() -> void:
|
||
if selected_tower_index < 0 or selected_tower_index >= towers.size():
|
||
return
|
||
var tower: Dictionary = towers[selected_tower_index]
|
||
if coop_online_mode:
|
||
if int(tower.get("owner", -1)) != local_player_slot:
|
||
_show_status("NUR EIGENE TÜRME KÖNNEN VERKAUFT WERDEN")
|
||
return
|
||
online_manager.request_tower_sell(int(tower.get("id", -1)))
|
||
selected_tower_index = -1
|
||
_show_status("VERKAUF AN SERVER GESENDET")
|
||
return
|
||
var base_cost: int = TOWER_DATA[tower.type].cost
|
||
_award_credits(0, int(base_cost * (0.55 + tower.level * 0.12)))
|
||
towers.remove_at(selected_tower_index)
|
||
selected_tower_index = -1
|
||
|
||
|
||
func _upgrade_cost(tower: Dictionary) -> int:
|
||
return int(TOWER_DATA[tower.type].cost * (0.72 + tower.level * 0.28))
|
||
|
||
|
||
func _tower_damage(type: int, level: int) -> float:
|
||
return TOWER_DATA[type].damage * (1.0 + (level - 1) * 0.52)
|
||
|
||
|
||
func _tower_range(type: int, level: int) -> float:
|
||
return TOWER_DATA[type].range * (1.0 + (level - 1) * 0.08)
|
||
|
||
|
||
func _tower_cooldown(type: int, level: int) -> float:
|
||
return TOWER_DATA[type].rate / (1.0 + (level - 1) * 0.12)
|
||
|
||
|
||
func _tower_shots_per_second(type: int, level: int) -> float:
|
||
return 1.0 / _tower_cooldown(type, level)
|
||
|
||
|
||
func _tower_aim_angle(type: int, origin: Vector2, target: Vector2) -> float:
|
||
return origin.angle_to_point(target) + (PI if type in [0, 3, 5, 6] else 0.0)
|
||
|
||
|
||
func _tower_at(position: Vector2, radius := 52.0) -> int:
|
||
for index in towers.size():
|
||
if towers[index].pos.distance_to(position) <= radius:
|
||
return index
|
||
return -1
|
||
|
||
|
||
func _grid_cell(position: Vector2) -> Vector2i:
|
||
return Vector2i(
|
||
roundi((position.x - GRID_SIZE * 0.5) / GRID_SIZE),
|
||
roundi((position.y - GRID_SIZE * 0.5) / GRID_SIZE)
|
||
)
|
||
|
||
|
||
func _cell_center(cell: Vector2i) -> Vector2:
|
||
return Vector2(cell) * GRID_SIZE + Vector2.ONE * GRID_SIZE * 0.5
|
||
|
||
|
||
func _rebuild_path_cells() -> void:
|
||
path_cells.clear()
|
||
_add_path_cells(path_points)
|
||
|
||
|
||
func _add_path_cells(route: PackedVector2Array) -> void:
|
||
for index in route.size() - 1:
|
||
var start := route[index]
|
||
var finish := route[index + 1]
|
||
var steps := maxi(1, ceili(start.distance_to(finish) / (GRID_SIZE * 0.20)))
|
||
for step in steps + 1:
|
||
var sample := start.lerp(finish, float(step) / float(steps))
|
||
path_cells[_grid_cell(sample)] = true
|
||
|
||
|
||
func _is_path_cell(position: Vector2) -> bool:
|
||
return path_cells.has(_grid_cell(position))
|
||
|
||
|
||
func _snap_to_grid(position: Vector2) -> Vector2:
|
||
return Vector2(
|
||
floor(position.x / GRID_SIZE) * GRID_SIZE + GRID_SIZE * 0.5,
|
||
floor(position.y / GRID_SIZE) * GRID_SIZE + GRID_SIZE * 0.5
|
||
)
|
||
|
||
|
||
func _is_in_build_zone(position: Vector2) -> bool:
|
||
for zone in build_zones:
|
||
if zone.has_point(position):
|
||
return true
|
||
return false
|
||
|
||
|
||
func _is_in_reactor_zone(position: Vector2) -> bool:
|
||
if coop_mode:
|
||
return position.distance_to(COOP_REACTOR_CENTER) <= COOP_REACTOR_RADIUS
|
||
for reactor in MAP_REACTOR_ZONES[selected_map]:
|
||
if position.distance_to(reactor.center) <= float(reactor.radius):
|
||
return true
|
||
return false
|
||
|
||
|
||
func _player_credits(player_index: int) -> int:
|
||
if coop_mode:
|
||
return int(multiplayer_manager.player_credits[clampi(player_index, 0, 1)])
|
||
return credits
|
||
|
||
|
||
func _spend_credits(player_index: int, amount: int) -> bool:
|
||
if coop_mode:
|
||
var success: bool = multiplayer_manager.spend_player_credits(player_index, amount)
|
||
credits = multiplayer_manager.player_credits[0]
|
||
return success
|
||
if credits < amount:
|
||
return false
|
||
credits -= amount
|
||
return true
|
||
|
||
|
||
func _award_credits(player_index: int, amount: int) -> void:
|
||
if coop_mode:
|
||
multiplayer_manager.add_player_credits(player_index, amount)
|
||
credits = multiplayer_manager.player_credits[0]
|
||
else:
|
||
credits += amount
|
||
|
||
|
||
func _run_ai_builder(initial_setup: bool) -> void:
|
||
if not coop_ai_mode:
|
||
return
|
||
var desired_towers := mini(9, 2 + int(wave / 2))
|
||
if initial_setup:
|
||
desired_towers = 2
|
||
var ai_tower_count := 0
|
||
for tower in towers:
|
||
if int(tower.get("owner", 0)) == 1:
|
||
ai_tower_count += 1
|
||
while ai_tower_count < desired_towers:
|
||
var preferred_category := _ai_priority_category()
|
||
var tower_type := _ai_choose_tower_type(preferred_category)
|
||
var cost: int = int(TOWER_DATA[tower_type].cost)
|
||
if _player_credits(1) < cost:
|
||
break
|
||
var build_position := _find_ai_build_position(ai_tower_count, tower_type)
|
||
if build_position == Vector2.ZERO:
|
||
break
|
||
_spend_credits(1, cost)
|
||
towers.append({"pos": build_position, "type": tower_type, "level": 1, "cooldown": 0.15, "angle": PI, "owner": 1})
|
||
_spawn_burst(build_position, Color("ffbf69"), 12)
|
||
ai_tower_count += 1
|
||
if not initial_setup and ai_tower_count >= desired_towers:
|
||
_upgrade_ai_tower()
|
||
|
||
|
||
func _ai_priority_category() -> String:
|
||
var incoming := {"air": 0.0, "ground": 0.0}
|
||
var first_wave := maxi(1, wave)
|
||
var weights := [1.0, 0.65, 0.35]
|
||
for offset in weights.size():
|
||
var forecast_wave := first_wave + offset
|
||
if forecast_wave > MAX_WAVES and not endless_mode:
|
||
continue
|
||
for enemy_type in _wave_preview_types(forecast_wave):
|
||
var category := _enemy_category(int(enemy_type))
|
||
incoming[category] = float(incoming[category]) + float(weights[offset])
|
||
var air_defense := 0.0
|
||
var ground_defense := 0.0
|
||
for tower in towers:
|
||
if int(tower.get("owner", 0)) != 1:
|
||
continue
|
||
var type := int(tower.type)
|
||
var dps := _tower_damage(type, int(tower.get("level", 1))) * _tower_shots_per_second(type, int(tower.get("level", 1)))
|
||
match _tower_target_class(type):
|
||
"air": air_defense += dps
|
||
"ground": ground_defense += dps
|
||
_:
|
||
var both_scale := float(TOWER_DATA[type].get("both_damage_scale", 1.0))
|
||
air_defense += dps * both_scale * 0.62
|
||
ground_defense += dps * both_scale * 0.62
|
||
var air_need := float(incoming.air) / maxf(1.0, air_defense)
|
||
var ground_need := float(incoming.ground) / maxf(1.0, ground_defense)
|
||
if incoming.air <= 0.0:
|
||
return "ground"
|
||
if incoming.ground <= 0.0:
|
||
return "air"
|
||
return "air" if air_need >= ground_need else "ground"
|
||
|
||
|
||
func _ai_choose_tower_type(category: String) -> int:
|
||
var best_type := 1
|
||
var best_score := -INF
|
||
for tower_type in TOWER_DATA.size():
|
||
var target_class := _tower_target_class(tower_type)
|
||
if target_class != category and target_class != "both":
|
||
continue
|
||
var data: Dictionary = TOWER_DATA[tower_type]
|
||
var level := 1
|
||
var dps := _tower_damage(tower_type, level) * _tower_shots_per_second(tower_type, level)
|
||
if target_class == "both":
|
||
dps *= float(data.get("both_damage_scale", 1.0)) * 0.72
|
||
if str(data.damage_type).contains("FLÄCHE") or str(data.damage_type).contains("EMP-FLÄCHE"):
|
||
dps *= 1.18
|
||
var specialization_bonus := 1.18 if target_class == category else 1.0
|
||
var score := dps * specialization_bonus / maxf(1.0, float(data.cost))
|
||
if score > best_score:
|
||
best_score = score
|
||
best_type = tower_type
|
||
return best_type
|
||
|
||
|
||
func _find_ai_build_position(offset: int, tower_type: int) -> Vector2:
|
||
if coop_paths.size() < 2:
|
||
return Vector2.ZERO
|
||
var route: PackedVector2Array = coop_paths[1]
|
||
var desired_towers := mini(9, 2 + int(wave / 2))
|
||
var target_progress := _route_length(route) * float(offset + 1) / float(desired_towers + 1)
|
||
var tower_range: float = TOWER_DATA[tower_type].range
|
||
var best_position := Vector2.ZERO
|
||
var best_score := INF
|
||
for x in range(1560, 2841, 80):
|
||
for y in range(120, 721, 80):
|
||
var candidate := Vector2(x, y)
|
||
if _is_valid_build_position(candidate, 1):
|
||
var distance := _distance_to_route(candidate, route)
|
||
if distance < 82.0 or distance > tower_range - 12.0:
|
||
continue
|
||
var progress := _route_progress_at(candidate, route)
|
||
var score := absf(progress - target_progress) + absf(distance - tower_range * 0.70) * 1.25
|
||
for tower in towers:
|
||
if int(tower.get("owner", 0)) == 1:
|
||
var separation: float = candidate.distance_to(tower.pos)
|
||
if separation < 150.0:
|
||
score += (150.0 - separation) * 4.0
|
||
if score < best_score:
|
||
best_score = score
|
||
best_position = candidate
|
||
return best_position
|
||
|
||
|
||
func _route_length(route: PackedVector2Array) -> float:
|
||
var result := 0.0
|
||
for index in route.size() - 1:
|
||
result += route[index].distance_to(route[index + 1])
|
||
return result
|
||
|
||
|
||
func _route_progress_at(position: Vector2, route: PackedVector2Array) -> float:
|
||
var travelled := 0.0
|
||
var best_distance := INF
|
||
var best_progress := 0.0
|
||
for index in route.size() - 1:
|
||
var start: Vector2 = route[index]
|
||
var segment: Vector2 = route[index + 1] - start
|
||
var segment_length := segment.length()
|
||
var ratio := clampf((position - start).dot(segment) / maxf(0.001, segment.length_squared()), 0.0, 1.0)
|
||
var point := start + segment * ratio
|
||
var distance := position.distance_to(point)
|
||
if distance < best_distance:
|
||
best_distance = distance
|
||
best_progress = travelled + segment_length * ratio
|
||
travelled += segment_length
|
||
return best_progress
|
||
|
||
|
||
func _distance_to_route(position: Vector2, route: PackedVector2Array) -> float:
|
||
var result := INF
|
||
for index in route.size() - 1:
|
||
var closest := Geometry2D.get_closest_point_to_segment(position, route[index], route[index + 1])
|
||
result = minf(result, position.distance_to(closest))
|
||
return result
|
||
|
||
|
||
func _upgrade_ai_tower() -> void:
|
||
for tower in towers:
|
||
if int(tower.get("owner", 0)) != 1 or int(tower.level) >= 3:
|
||
continue
|
||
var cost := _upgrade_cost(tower)
|
||
if _spend_credits(1, cost):
|
||
tower.level += 1
|
||
_spawn_burst(tower.pos, Color("ffbf69"), 10)
|
||
return
|
||
|
||
|
||
func _local_credit_slot() -> int:
|
||
return local_player_slot if coop_online_mode else 0
|
||
|
||
|
||
func _apply_online_snapshot(snapshot: Dictionary) -> void:
|
||
if not coop_online_mode or snapshot.is_empty():
|
||
return
|
||
var apply_start_us := Time.get_ticks_usec()
|
||
online_snapshot_interval = clampf(online_snapshot_elapsed, 0.08, 0.2) if online_snapshot_elapsed > 0.0 else 0.1
|
||
online_snapshot_elapsed = 0.0
|
||
selected_difficulty = clampi(int(snapshot.get("difficulty", selected_difficulty)), 0, DIFFICULTY_DATA.size() - 1)
|
||
if bool(snapshot.get("connection_interrupted", false)):
|
||
_show_status("MITPILOT GETRENNT • WIEDERBEITRITT %d S" % int(snapshot.get("reconnect_seconds", 0)))
|
||
reactor_lives = int(snapshot.get("reactor_lives", reactor_lives))
|
||
wave = int(snapshot.get("wave", wave))
|
||
wave_active = bool(snapshot.get("wave_active", false))
|
||
enemies_to_spawn = int(snapshot.get("enemies_to_spawn", 0))
|
||
online_wave_types_remaining.clear()
|
||
for enemy_type in snapshot.get("wave_types_remaining", []):
|
||
online_wave_types_remaining.append(int(enemy_type))
|
||
credits = _player_credits(local_player_slot)
|
||
|
||
var previous_enemies: Dictionary = {}
|
||
for enemy in enemies:
|
||
previous_enemies[int(enemy.get("id", -1))] = enemy
|
||
var synced_enemies: Array[Dictionary] = []
|
||
for entry in snapshot.get("enemies", []):
|
||
if not entry is Dictionary:
|
||
continue
|
||
var enemy_id := int(entry.get("id", -1))
|
||
var target_position := Vector2(float(entry.get("x", 0.0)), float(entry.get("y", 0.0)))
|
||
var enemy: Dictionary = previous_enemies.get(enemy_id, {})
|
||
if enemy.is_empty():
|
||
enemy = {"id": enemy_id, "pos": target_position, "flash": 0.0}
|
||
enemy["from_pos"] = enemy.get("pos", target_position)
|
||
enemy["target_pos"] = target_position
|
||
enemy["type"] = int(entry.get("type", 0))
|
||
enemy["path_index"] = int(entry.get("path_index", 1))
|
||
enemy["route_id"] = int(entry.get("route_id", 0))
|
||
enemy["hp"] = float(entry.get("hp", 1.0))
|
||
enemy["max_hp"] = float(entry.get("max_hp", 1.0))
|
||
enemy["shield"] = bool(entry.get("shield", false))
|
||
enemy["shield_hp"] = float(entry.get("shield_hp", 0.0))
|
||
enemy["max_shield_hp"] = float(entry.get("max_shield_hp", 0.0))
|
||
enemy["angle"] = float(entry.get("angle", 0.0))
|
||
enemy["boss"] = bool(entry.get("boss", false))
|
||
enemy["size_scale"] = 1.34 if enemy["boss"] else 1.0
|
||
synced_enemies.append(enemy)
|
||
enemies = synced_enemies
|
||
|
||
var previous_towers: Dictionary = {}
|
||
for tower in towers:
|
||
previous_towers[int(tower.get("id", -1))] = tower
|
||
var synced_towers: Array[Dictionary] = []
|
||
for entry in snapshot.get("towers", []):
|
||
if entry is Dictionary:
|
||
var tower_id := int(entry.get("id", -1))
|
||
var tower: Dictionary = previous_towers.get(tower_id, {})
|
||
tower["id"] = tower_id
|
||
tower["pos"] = Vector2(float(entry.get("x", 0.0)), float(entry.get("y", 0.0)))
|
||
tower["type"] = int(entry.get("type", 0))
|
||
tower["level"] = int(entry.get("level", 1))
|
||
tower["owner"] = int(entry.get("owner", 0))
|
||
tower["angle"] = float(entry.get("angle", 0.0))
|
||
synced_towers.append(tower)
|
||
towers = synced_towers
|
||
for fired_shot in snapshot.get("shots", []):
|
||
if not fired_shot is Dictionary:
|
||
continue
|
||
var tower_type := clampi(int(fired_shot.get("type", 0)), 0, TOWER_DATA.size() - 1)
|
||
var shot_from := Vector2(float(fired_shot.get("from_x", 0.0)), float(fired_shot.get("from_y", 0.0)))
|
||
var shot_to := Vector2(float(fired_shot.get("to_x", 0.0)), float(fired_shot.get("to_y", 0.0)))
|
||
var shot_color: Color = TOWER_DATA[tower_type].color
|
||
if tower_type in [1, 5]:
|
||
beams.append({"from": shot_from, "to": shot_to, "ttl": 0.12 if tower_type == 1 else 0.16, "color": shot_color, "piercing": tower_type == 5})
|
||
else:
|
||
online_projectiles.append({"pos": shot_from, "target": shot_to, "speed": float(TOWER_DATA[tower_type].shot_speed), "type": tower_type, "color": shot_color})
|
||
if selected_tower_index >= towers.size():
|
||
selected_tower_index = -1
|
||
if bool(snapshot.get("game_over", false)) and game_mode == "playing":
|
||
_game_over()
|
||
online_apply_max_us = maxi(online_apply_max_us, Time.get_ticks_usec() - apply_start_us)
|
||
var now_ms := Time.get_ticks_msec()
|
||
if online_apply_report_ms == 0:
|
||
online_apply_report_ms = now_ms
|
||
elif now_ms - online_apply_report_ms >= 10000:
|
||
print("ONLINE DIAG max_apply_us=%d enemies=%d towers=%d" % [online_apply_max_us, enemies.size(), towers.size()])
|
||
online_apply_max_us = 0
|
||
online_apply_report_ms = now_ms
|
||
|
||
|
||
func _update_online_interpolation(delta: float) -> void:
|
||
online_snapshot_elapsed += delta
|
||
var weight := clampf(online_snapshot_elapsed / online_snapshot_interval, 0.0, 1.0)
|
||
for enemy in enemies:
|
||
enemy.pos = (enemy.get("from_pos", enemy.pos) as Vector2).lerp(enemy.get("target_pos", enemy.pos), weight)
|
||
|
||
|
||
func _update_online_projectiles(delta: float) -> void:
|
||
for index in range(online_projectiles.size() - 1, -1, -1):
|
||
var shot: Dictionary = online_projectiles[index]
|
||
var target: Vector2 = shot.target
|
||
var step := maxf(0.0, float(shot.speed) * delta)
|
||
if shot.pos.distance_to(target) <= maxf(14.0, step):
|
||
_spawn_burst(target, shot.color, 6)
|
||
online_projectiles.remove_at(index)
|
||
else:
|
||
shot.pos = shot.pos.move_toward(target, step)
|
||
|
||
|
||
func _handle_online_event(event: Dictionary) -> void:
|
||
if not coop_online_mode:
|
||
return
|
||
match String(event.get("type", "")):
|
||
"wave_complete":
|
||
_show_status("WELLE %d ABGEWEHRT +%d" % [int(event.get("wave", wave)), int(event.get("bonus", 0))])
|
||
"partner_left":
|
||
_show_status("VERBINDUNG ZUM PARTNER VERLOREN")
|
||
"command_rejected":
|
||
_show_status("AKTION VOM SERVER ABGELEHNT")
|
||
"server_shutdown":
|
||
_show_status("SPIELSERVER WIRD NEU GESTARTET")
|
||
|
||
|
||
func _show_status(text: String) -> void:
|
||
status_text = _ui(text)
|
||
status_timer = 2.1
|
||
|
||
|
||
func _load_audio_settings() -> void:
|
||
var config := ConfigFile.new()
|
||
if config.load("user://void_bastion_settings.cfg") == OK:
|
||
master_volume = clampf(float(config.get_value("audio", "master_volume", master_volume)), 0.0, 1.0)
|
||
ui_language = String(config.get_value("interface", "language", "de"))
|
||
if ui_language not in ["de", "en"]:
|
||
ui_language = "de"
|
||
_apply_master_volume()
|
||
if friend_name_line_edit:
|
||
friend_name_line_edit.placeholder_text = _ui("Eindeutige Spielerkennung eingeben")
|
||
|
||
|
||
func _ui(text: String) -> String:
|
||
return String(ENGLISH_UI.get(text, text)) if ui_language == "en" else text
|
||
|
||
|
||
func _set_ui_language(value: String) -> void:
|
||
if value not in ["de", "en"]:
|
||
return
|
||
ui_language = value
|
||
if friend_name_line_edit:
|
||
friend_name_line_edit.placeholder_text = _ui("Eindeutige Spielerkennung eingeben")
|
||
_save_audio_settings()
|
||
queue_redraw()
|
||
|
||
|
||
func _set_master_volume(value: float) -> void:
|
||
master_volume = clampf(value, 0.0, 1.0)
|
||
_apply_master_volume()
|
||
queue_redraw()
|
||
|
||
|
||
func _apply_master_volume() -> void:
|
||
var master_bus := AudioServer.get_bus_index("Master")
|
||
if master_bus >= 0:
|
||
AudioServer.set_bus_volume_db(master_bus, linear_to_db(maxf(master_volume, 0.0001)))
|
||
|
||
|
||
func _save_audio_settings() -> void:
|
||
var config := ConfigFile.new()
|
||
config.set_value("audio", "master_volume", master_volume)
|
||
config.set_value("interface", "language", ui_language)
|
||
config.save("user://void_bastion_settings.cfg")
|
||
|
||
|
||
func _build_ambient_stream() -> AudioStreamWAV:
|
||
const sample_rate := 22050
|
||
const duration := 12
|
||
var frame_count := sample_rate * duration
|
||
var samples := PackedByteArray()
|
||
samples.resize(frame_count * 4)
|
||
for frame in frame_count:
|
||
var time := float(frame) / sample_rate
|
||
var slow_breathe := 0.72 + 0.28 * sin(TAU * time / duration)
|
||
var chord := sin(TAU * 55.0 * time) * 0.34 + sin(TAU * 82.5 * time) * 0.22 + sin(TAU * 110.0 * time) * 0.16 + sin(TAU * 165.0 * time) * 0.08
|
||
var shimmer := sin(TAU * 220.0 * time + sin(TAU * time / duration) * 0.45) * 0.035
|
||
var mono := clampf((chord + shimmer) * slow_breathe * 0.12, -1.0, 1.0)
|
||
var left := int(mono * 32767.0)
|
||
var right := int(clampf(mono * (0.97 + 0.03 * sin(TAU * time / duration)), -1.0, 1.0) * 32767.0)
|
||
samples.encode_s16(frame * 4, left)
|
||
samples.encode_s16(frame * 4 + 2, right)
|
||
var stream := AudioStreamWAV.new()
|
||
stream.format = AudioStreamWAV.FORMAT_16_BITS
|
||
stream.mix_rate = sample_rate
|
||
stream.stereo = true
|
||
stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
|
||
stream.loop_end = frame_count
|
||
stream.data = samples
|
||
return stream
|
||
|
||
|
||
func _build_ui_click_stream() -> AudioStreamWAV:
|
||
const sample_rate := 22050
|
||
const duration := 0.075
|
||
var frame_count := int(sample_rate * duration)
|
||
var samples := PackedByteArray()
|
||
samples.resize(frame_count * 2)
|
||
var phase := 0.0
|
||
for frame in frame_count:
|
||
var time := float(frame) / sample_rate
|
||
var frequency := lerpf(1350.0, 720.0, time / duration)
|
||
phase += TAU * frequency / sample_rate
|
||
var envelope := 1.0 - time / duration
|
||
samples.encode_s16(frame * 2, int(sin(phase) * envelope * 0.14 * 32767.0))
|
||
var stream := AudioStreamWAV.new()
|
||
stream.format = AudioStreamWAV.FORMAT_16_BITS
|
||
stream.mix_rate = sample_rate
|
||
stream.stereo = false
|
||
stream.data = samples
|
||
return stream
|
||
|
||
|
||
func _play_ui_sound() -> void:
|
||
if ui_sound_player.stream != null:
|
||
ui_sound_player.play()
|
||
|
||
|
||
func _save_progress() -> void:
|
||
var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
|
||
if file:
|
||
file.store_string(JSON.stringify({
|
||
"best_wave": best_wave,
|
||
"best_endless_wave": best_endless_wave,
|
||
"core_crystals": core_crystals,
|
||
"unlocked_towers": unlocked_towers,
|
||
"unlocked_maps": unlocked_maps,
|
||
"selected_difficulty": selected_difficulty,
|
||
"tutorial_completed": tutorial_completed
|
||
}))
|
||
|
||
|
||
func _load_progress() -> void:
|
||
if not FileAccess.file_exists(SAVE_PATH):
|
||
return
|
||
var file := FileAccess.open(SAVE_PATH, FileAccess.READ)
|
||
if not file:
|
||
return
|
||
var data = JSON.parse_string(file.get_as_text())
|
||
if data is Dictionary:
|
||
best_wave = int(data.get("best_wave", 0))
|
||
best_endless_wave = int(data.get("best_endless_wave", 0))
|
||
core_crystals = maxi(0, int(data.get("core_crystals", 0)))
|
||
unlocked_towers = [0, 1, 2, 6]
|
||
var saved_unlocks = data.get("unlocked_towers", [0, 1, 2, 6])
|
||
if saved_unlocks is Array:
|
||
for tower_type in saved_unlocks:
|
||
if int(tower_type) >= 0 and int(tower_type) < 3 and not unlocked_towers.has(int(tower_type)):
|
||
unlocked_towers.append(int(tower_type))
|
||
elif int(tower_type) >= 3 and int(tower_type) < TOWER_DATA.size() and not unlocked_towers.has(int(tower_type)):
|
||
unlocked_towers.append(int(tower_type))
|
||
unlocked_maps = [0, 1, 2]
|
||
var saved_maps = data.get("unlocked_maps", [0, 1, 2])
|
||
if saved_maps is Array:
|
||
for map_index in saved_maps:
|
||
if int(map_index) >= 3 and int(map_index) < MAP_NAMES.size() and not unlocked_maps.has(int(map_index)):
|
||
unlocked_maps.append(int(map_index))
|
||
selected_difficulty = clampi(int(data.get("selected_difficulty", 1)), 0, DIFFICULTY_DATA.size() - 1)
|
||
tutorial_completed = bool(data.get("tutorial_completed", false))
|
||
|
||
|
||
func _draw() -> void:
|
||
if game_mode in ["menu", "promo_code", "store", "leaderboard", "profile", "profile_name_entry", "multiplayer_menu", "friends", "room_code_entry", "matchmaking"]:
|
||
draw_texture_rect(MAP_TEXTURES[selected_map], Rect2(Vector2.ZERO, SCREEN), false, Color("f3f6fb"))
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.01, 0.025, 0.055, 0.06))
|
||
if game_mode == "menu":
|
||
_draw_menu()
|
||
if audio_settings_open:
|
||
_draw_audio_settings()
|
||
elif game_mode == "promo_code":
|
||
_draw_menu()
|
||
_draw_promo_code_dialog()
|
||
elif game_mode == "store":
|
||
_draw_store()
|
||
elif game_mode == "leaderboard":
|
||
_draw_leaderboard()
|
||
elif game_mode == "profile":
|
||
_draw_profile()
|
||
elif game_mode == "profile_name_entry":
|
||
_draw_profile_name_entry()
|
||
elif game_mode == "multiplayer_menu":
|
||
_draw_multiplayer_menu()
|
||
elif game_mode == "friends":
|
||
_draw_friends()
|
||
elif game_mode == "room_code_entry":
|
||
_draw_room_code_entry()
|
||
else:
|
||
_draw_matchmaking()
|
||
return
|
||
draw_set_transform(-camera_offset, 0.0, Vector2.ONE)
|
||
if coop_mode:
|
||
_draw_coop_background()
|
||
else:
|
||
draw_texture_rect(MAP_TEXTURES[selected_map], Rect2(Vector2.ZERO, WORLD_SIZE), false, Color.WHITE)
|
||
draw_rect(Rect2(Vector2.ZERO, Vector2(_active_world_width(), WORLD_SIZE.y)), Color(0.01, 0.025, 0.055, 0.035))
|
||
_draw_world()
|
||
draw_set_transform(Vector2.ZERO, 0.0, Vector2.ONE)
|
||
_draw_hud()
|
||
if game_mode == "playing" and not tutorial_completed and not coop_mode:
|
||
_draw_tutorial()
|
||
if game_mode == "paused":
|
||
_draw_pause_menu()
|
||
elif game_mode == "game_over":
|
||
_draw_game_over()
|
||
elif game_mode == "victory":
|
||
_draw_victory()
|
||
elif game_mode == "endless_offer":
|
||
_draw_endless_offer()
|
||
elif game_mode == "endless_result":
|
||
_draw_endless_result()
|
||
|
||
|
||
func _active_world_width() -> float:
|
||
return COOP_WORLD_WIDTH if coop_mode else WORLD_SIZE.x
|
||
|
||
|
||
func _draw_coop_background() -> void:
|
||
var texture: Texture2D = MAP_TEXTURES[selected_map]
|
||
var source_width := texture.get_width() * COOP_MIRROR_X / WORLD_SIZE.x
|
||
var source := Rect2(0, 0, source_width, texture.get_height())
|
||
draw_texture_rect_region(texture, Rect2(0, 0, COOP_MIRROR_X, WORLD_SIZE.y), source, Color.WHITE)
|
||
draw_set_transform(Vector2(COOP_WORLD_WIDTH, 0) - camera_offset, 0.0, Vector2(-1, 1))
|
||
draw_texture_rect_region(texture, Rect2(0, 0, COOP_MIRROR_X, WORLD_SIZE.y), source, Color.WHITE)
|
||
draw_set_transform(-camera_offset, 0.0, Vector2.ONE)
|
||
|
||
|
||
func _draw_world() -> void:
|
||
_prepare_draw_geometry()
|
||
_draw_route()
|
||
if coop_mode:
|
||
draw_circle(COOP_REACTOR_CENTER, COOP_REACTOR_RADIUS, Color(0.02, 0.20, 0.30, 0.72))
|
||
for radius in [94.0, 70.0, 44.0]:
|
||
draw_arc(COOP_REACTOR_CENTER, radius, 0.0, TAU, 64, Color(0.28, 0.92, 1.0, 0.72), 3.0)
|
||
draw_circle(COOP_REACTOR_CENTER, 25.0, Color("8ffaff"))
|
||
_draw_reactor_approach()
|
||
_draw_build_grid()
|
||
if selected_tower_type >= 0 and cursor_screen_position.y > PLAYFIELD_TOP and cursor_screen_position.y < PLAYFIELD_BOTTOM:
|
||
var snapped_position := _snap_to_grid(cursor_position)
|
||
var valid_position := _is_valid_build_position(snapped_position, _local_credit_slot())
|
||
var placement_color := Color(0.25, 1.0, 0.7, 0.22) if valid_position else Color(1.0, 0.2, 0.2, 0.22)
|
||
var preview_range := _tower_range(selected_tower_type, 1)
|
||
draw_circle(snapped_position, preview_range, Color(placement_color, 0.025))
|
||
draw_arc(snapped_position, preview_range, 0, TAU, 96, Color(placement_color, 0.46), 1.5)
|
||
draw_rect(Rect2(snapped_position - Vector2(35, 35), Vector2(70, 70)), Color(placement_color, 0.10), true)
|
||
draw_rect(Rect2(snapped_position - Vector2(35, 35), Vector2(70, 70)), Color(placement_color, 0.62), false, 1.5)
|
||
_draw_tower({"pos": snapped_position, "type": selected_tower_type, "level": 1, "angle": 0.0})
|
||
for tower_index in towers.size():
|
||
var tower: Dictionary = towers[tower_index]
|
||
if tower_index == selected_tower_index:
|
||
var range_value := _tower_range(tower.type, tower.level)
|
||
draw_circle(tower.pos, range_value, Color(0.2, 0.85, 1.0, 0.07))
|
||
draw_arc(tower.pos, range_value, 0, TAU, 80, Color(0.35, 0.9, 1.0, 0.45), 2.0)
|
||
_draw_tower(tower)
|
||
for enemy in enemies:
|
||
_draw_enemy(enemy)
|
||
for shot in projectiles:
|
||
draw_circle(shot.pos, 6.0 if shot.type == 2 else 3.5, shot.color)
|
||
draw_circle(shot.pos, 12.0 if shot.type == 2 else 7.0, Color(shot.color, 0.20))
|
||
for shot in online_projectiles:
|
||
draw_circle(shot.pos, 6.0 if shot.type == 2 else 3.5, shot.color)
|
||
draw_circle(shot.pos, 12.0 if shot.type == 2 else 7.0, Color(shot.color, 0.20))
|
||
for beam in beams:
|
||
if bool(beam.get("piercing", false)):
|
||
draw_line(beam.from, beam.to, Color("c86bff", 0.18), 18.0)
|
||
draw_line(beam.from, beam.to, Color("c86bff", 0.9), 8.0)
|
||
draw_line(beam.from, beam.to, Color("f3ddff", 1.0), 2.0)
|
||
else:
|
||
draw_line(beam.from, beam.to, Color(beam.color, 0.92), 3.0)
|
||
draw_line(beam.from, beam.to, Color.WHITE, 1.0)
|
||
for particle in particles:
|
||
draw_circle(particle.pos, maxf(1.0, particle.ttl * 8.0), Color(particle.color, minf(1.0, particle.ttl * 3.0)))
|
||
|
||
|
||
func _prepare_draw_geometry() -> void:
|
||
if not draw_geometry_dirty:
|
||
return
|
||
road_tile_cache.clear()
|
||
build_cell_cache.clear()
|
||
for cell in path_cells:
|
||
var center := _cell_center(cell)
|
||
if center.x < -GRID_SIZE or center.x > _active_world_width() + GRID_SIZE or center.y < PLAYFIELD_TOP - GRID_SIZE or center.y > PLAYFIELD_BOTTOM + GRID_SIZE:
|
||
continue
|
||
var left := path_cells.has(cell + Vector2i(-1, 0))
|
||
var right := path_cells.has(cell + Vector2i(1, 0))
|
||
var up := path_cells.has(cell + Vector2i(0, -1))
|
||
var down := path_cells.has(cell + Vector2i(0, 1))
|
||
var texture := ROAD_STRAIGHT_TEXTURE
|
||
var angle := 0.0
|
||
var is_corner := (left or right) and (up or down)
|
||
if is_corner:
|
||
texture = ROAD_CORNER_TEXTURE
|
||
if left and down:
|
||
angle = 0.0
|
||
elif left and up:
|
||
angle = PI * 0.5
|
||
elif right and up:
|
||
angle = PI
|
||
else:
|
||
angle = PI * 1.5
|
||
elif up or down:
|
||
angle = PI * 0.5
|
||
var variant := absi(cell.x * 3 + cell.y * 5) % 4
|
||
road_tile_cache.append({"center": center, "angle": angle, "texture": texture,
|
||
"corner": is_corner, "variant": variant, "opacity": _path_cell_opacity(cell)})
|
||
for x_index in int(_active_world_width() / GRID_SIZE):
|
||
for y_index in int(WORLD_SIZE.y / GRID_SIZE):
|
||
var center := Vector2(x_index * GRID_SIZE + GRID_SIZE * 0.5, y_index * GRID_SIZE + GRID_SIZE * 0.5)
|
||
if not _is_in_build_zone(center) or _is_in_reactor_zone(center) or _is_path_cell(center):
|
||
continue
|
||
build_cell_cache.append({"rect": Rect2(center - Vector2(34, 34), Vector2(68, 68)),
|
||
"partner": coop_mode and center.x > COOP_MIRROR_X})
|
||
draw_geometry_dirty = false
|
||
|
||
|
||
func _draw_route() -> void:
|
||
for tile in road_tile_cache:
|
||
draw_set_transform(tile.center - camera_offset, tile.angle, Vector2.ONE)
|
||
var variant: int = tile.variant
|
||
var ray_x := -18.0 if variant % 2 == 0 else 18.0
|
||
var tile_opacity: float = tile.opacity
|
||
var texture: Texture2D = tile.texture
|
||
var is_corner: bool = tile.corner
|
||
if not is_corner:
|
||
draw_colored_polygon(PackedVector2Array([Vector2(ray_x - 5, -36), Vector2(ray_x + 5, -36), Vector2(ray_x + 14, -62), Vector2(ray_x - 14, -62)]), Color(1.0, 0.48, 0.12, 0.055 * tile_opacity))
|
||
draw_colored_polygon(PackedVector2Array([Vector2(ray_x - 5, 36), Vector2(ray_x + 5, 36), Vector2(ray_x + 14, 62), Vector2(ray_x - 14, 62)]), Color(1.0, 0.48, 0.12, 0.055 * tile_opacity))
|
||
var source_positions := [Vector2(80, 80), Vector2(650, 80), Vector2(80, 700), Vector2(650, 700)]
|
||
var source_position: Vector2 = source_positions[variant]
|
||
draw_texture_rect_region(ROAD_SURFACE_TEXTURE, Rect2(-40, -31, 80, 62), Rect2(source_position, Vector2(520, 403)), Color(1.0, 1.0, 1.0, tile_opacity))
|
||
draw_texture_rect(texture, Rect2(-GRID_SIZE * 0.5, -GRID_SIZE * 0.5, GRID_SIZE, GRID_SIZE), false, Color(1.0, 1.0, 1.0, tile_opacity))
|
||
else:
|
||
draw_texture_rect(texture, Rect2(-GRID_SIZE * 0.5, -GRID_SIZE * 0.5, GRID_SIZE, GRID_SIZE), false, Color(1.0, 1.0, 1.0, tile_opacity))
|
||
draw_texture_rect(ROAD_CORNER_SURFACE_TEXTURE, Rect2(-GRID_SIZE * 0.5, -GRID_SIZE * 0.5, GRID_SIZE, GRID_SIZE), false, Color(1.0, 1.0, 1.0, tile_opacity))
|
||
draw_set_transform(-camera_offset, 0.0, Vector2.ONE)
|
||
|
||
|
||
func _path_cell_opacity(cell: Vector2i) -> float:
|
||
var routes: Array = [path_points]
|
||
if coop_mode:
|
||
routes = coop_paths
|
||
var center := _cell_center(cell)
|
||
var nearest_remaining := INF
|
||
for route in routes:
|
||
if route.size() < 2:
|
||
continue
|
||
var length_after_segment := 0.0
|
||
for segment_index in range(route.size() - 2, -1, -1):
|
||
var segment_start: Vector2 = route[segment_index]
|
||
var segment_end: Vector2 = route[segment_index + 1]
|
||
var segment := segment_end - segment_start
|
||
var segment_length := segment.length()
|
||
if segment_length > 0.0:
|
||
var direction := segment / segment_length
|
||
var distance_along := clampf((center - segment_start).dot(direction), 0.0, segment_length)
|
||
var projection := segment_start + direction * distance_along
|
||
if center.distance_to(projection) <= GRID_SIZE * 0.55:
|
||
var remaining := center.distance_to(projection) + segment_length - distance_along + length_after_segment
|
||
nearest_remaining = minf(nearest_remaining, remaining)
|
||
length_after_segment += segment_length
|
||
if nearest_remaining >= 320.0:
|
||
return 1.0
|
||
var fade_progress := clampf(nearest_remaining / 320.0, 0.0, 1.0)
|
||
return fade_progress * fade_progress * (3.0 - 2.0 * fade_progress)
|
||
|
||
|
||
func _draw_reactor_approach() -> void:
|
||
var routes: Array = [path_points]
|
||
if coop_mode:
|
||
routes = coop_paths
|
||
for route in routes:
|
||
if route.size() < 2:
|
||
continue
|
||
var approach_start: Vector2 = route[route.size() - 2]
|
||
var reactor_end: Vector2 = route[route.size() - 1]
|
||
var direction := approach_start.direction_to(reactor_end)
|
||
var approach_color := Color("65eaff")
|
||
# Fade the road's cyan guide in short overlapping steps as it reaches the core.
|
||
for segment_index in 12:
|
||
var progress_start := float(segment_index) / 12.0
|
||
var progress_end := float(segment_index + 1) / 12.0
|
||
var fade := 1.0 - (progress_start + progress_end) * 0.5
|
||
var fade_start := approach_start.lerp(reactor_end, progress_start)
|
||
var fade_end := approach_start.lerp(reactor_end, progress_end)
|
||
draw_line(fade_start, fade_end, Color(approach_color, 0.10 * fade), 13.0, true)
|
||
draw_line(fade_start, fade_end, Color(approach_color, 0.18 * fade), 2.0, true)
|
||
# Multiple dim pulses travel inward, like energy being carried to the core.
|
||
for pulse_index in 3:
|
||
var pulse_progress := fposmod(elapsed * 0.34 + float(pulse_index) / 3.0, 1.0)
|
||
var pulse_position := approach_start.lerp(reactor_end, pulse_progress)
|
||
var pulse_alpha := (0.22 + 0.12 * sin(pulse_progress * PI)) * (1.0 - pulse_progress * 0.72)
|
||
draw_line(pulse_position - direction * 12.0, pulse_position + direction * 12.0, Color(approach_color, pulse_alpha), 3.0, true)
|
||
draw_circle(pulse_position, 7.0, Color(approach_color, pulse_alpha * 0.20))
|
||
draw_circle(pulse_position, 2.2, Color(0.78, 0.98, 1.0, pulse_alpha + 0.18))
|
||
var reactor_breathe := 0.5 + 0.5 * sin(elapsed * TAU * 0.42)
|
||
draw_circle(reactor_end, 13.0 + reactor_breathe * 3.0, Color(approach_color, 0.035 + reactor_breathe * 0.025))
|
||
draw_arc(reactor_end, 22.0 + reactor_breathe * 2.0, 0.0, TAU, 40, Color(approach_color, 0.10 + reactor_breathe * 0.08), 1.5)
|
||
|
||
|
||
func _draw_build_grid() -> void:
|
||
var line_color := Color(0.24, 0.82, 1.0, 0.14 if selected_tower_type >= 0 else 0.10)
|
||
var fill_color := Color(0.08, 0.55, 0.72, 0.035 if selected_tower_type >= 0 else 0.025)
|
||
for cell in build_cell_cache:
|
||
var cell_line_color := Color(1.0, 0.67, 0.32, 0.16) if cell.partner else line_color
|
||
var cell_fill_color := Color(0.75, 0.35, 0.08, 0.025) if cell.partner else fill_color
|
||
draw_rect(cell.rect, cell_fill_color, true)
|
||
draw_rect(cell.rect, cell_line_color, false, 1.0)
|
||
|
||
|
||
func _draw_tower(tower: Dictionary) -> void:
|
||
var size: float = 74.0 + (tower.level - 1) * 4.0
|
||
if coop_mode and int(tower.get("owner", 0)) == 1:
|
||
draw_circle(tower.pos, size * 0.54, Color(1.0, 0.56, 0.20, 0.18))
|
||
draw_arc(tower.pos, size * 0.54, 0.0, TAU, 28, Color(1.0, 0.70, 0.34, 0.72), 2.0)
|
||
draw_set_transform(tower.pos - camera_offset, tower.angle, Vector2.ONE)
|
||
var destination := Rect2(-size * 0.5, -size * 0.5, size, size)
|
||
if tower.type < 3:
|
||
var source_width: float = TOWER_TEXTURE.get_width() / 3.0
|
||
draw_texture_rect_region(TOWER_TEXTURE, destination, Rect2(source_width * tower.type, 0, source_width, TOWER_TEXTURE.get_height()))
|
||
elif tower.type == 6:
|
||
var source_width: float = TOWER_TEXTURE.get_width() / 3.0
|
||
draw_texture_rect_region(TOWER_TEXTURE, destination, Rect2(0, 0, source_width, TOWER_TEXTURE.get_height()), Color("ff8b92"))
|
||
else:
|
||
var tower_texture: Texture2D = PLASMA_TEXTURE if tower.type == 3 else (EMP_TEXTURE if tower.type == 4 else RAILGUN_TEXTURE)
|
||
draw_texture_rect(tower_texture, destination, false)
|
||
draw_set_transform(-camera_offset, 0.0, Vector2.ONE)
|
||
for level_mark in tower.level:
|
||
draw_circle(tower.pos + Vector2(-10 + level_mark * 10, 43), 3.0, Color("6cecff"))
|
||
|
||
|
||
func _draw_enemy(enemy: Dictionary) -> void:
|
||
var enemy_type := int(enemy.type)
|
||
var destination: Rect2
|
||
var size: float = ENEMY_DATA[enemy.type].size * enemy.size_scale
|
||
var enemy_texture := _enemy_texture(enemy_type)
|
||
var source := _enemy_source_rect(enemy_type)
|
||
destination = Rect2(-size * 0.75, -size * 0.5, size * 1.5, size)
|
||
var tint := Color.WHITE if enemy.flash <= 0.0 else Color("ffb6a3")
|
||
if bool(enemy.get("shield", false)):
|
||
var shield_ratio := clampf(float(enemy.get("shield_hp", 1.0)) / maxf(1.0, float(enemy.get("max_shield_hp", 1.0))), 0.0, 1.0)
|
||
draw_circle(enemy.pos, size * 0.63, Color(0.25, 0.80, 1.0, 0.08))
|
||
draw_arc(enemy.pos, size * 0.63, 0.0, TAU, 40, Color(0.28, 0.66, 0.82, 0.28), 2.0)
|
||
draw_arc(enemy.pos, size * 0.63, -PI * 0.5, -PI * 0.5 - TAU * shield_ratio, 48, Color(0.38, 0.86, 1.0, 0.92), 3.2)
|
||
draw_set_transform(enemy.pos - camera_offset, enemy.angle, Vector2.ONE)
|
||
draw_texture_rect_region(enemy_texture, destination, source, tint)
|
||
draw_set_transform(-camera_offset, 0.0, Vector2.ONE)
|
||
var bar_width := size * 0.72
|
||
var ratio: float = clampf(enemy.hp / enemy.max_hp, 0.0, 1.0)
|
||
draw_rect(Rect2(enemy.pos + Vector2(-bar_width * 0.5, -size * 0.48), Vector2(bar_width, 5)), Color("261923"))
|
||
draw_rect(Rect2(enemy.pos + Vector2(-bar_width * 0.5, -size * 0.48), Vector2(bar_width * ratio, 5)), Color("ff6049"))
|
||
if bool(enemy.get("shield", false)):
|
||
draw_string(ThemeDB.fallback_font, enemy.pos + Vector2(-30, -size * 0.62), _ui("SCHILD"), HORIZONTAL_ALIGNMENT_CENTER, 60, 9, Color("75eaff"))
|
||
if enemy.boss:
|
||
draw_string(ThemeDB.fallback_font, enemy.pos + Vector2(-45, -size * 0.58), _ui("KOMMANDOSCHIFF"), HORIZONTAL_ALIGNMENT_CENTER, 90, 11, Color("ffab78"))
|
||
|
||
|
||
func _enemy_texture(enemy_type: int) -> Texture2D:
|
||
if enemy_type < 3:
|
||
return ENEMY_TEXTURE
|
||
if enemy_type < 6:
|
||
return [ENEMY_INTERCEPTOR_TEXTURE, ENEMY_SHIELD_CARRIER_TEXTURE, ENEMY_SIEGE_DESTROYER_TEXTURE][enemy_type - 3]
|
||
return [ENEMY_GROUND_ROVER_TEXTURE, ENEMY_GROUND_TANK_TEXTURE, ENEMY_GROUND_WALKER_TEXTURE][enemy_type - 6]
|
||
|
||
|
||
func _enemy_source_rect(enemy_type: int) -> Rect2:
|
||
var texture := _enemy_texture(enemy_type)
|
||
if enemy_type < 3:
|
||
var source_width := texture.get_width() / 3.0
|
||
return Rect2(source_width * enemy_type, 0.0, source_width, texture.get_height())
|
||
return Rect2(Vector2.ZERO, texture.get_size())
|
||
|
||
|
||
func _draw_hud() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(0, 0, 1600, 82), Color(0.015, 0.035, 0.07, 0.92))
|
||
draw_line(Vector2(0, 82), Vector2(1600, 82), Color("2e7593"), 2.0)
|
||
draw_string(font, Vector2(25, 31), _ui("ENERGIE %04d") % _player_credits(_local_credit_slot()), HORIZONTAL_ALIGNMENT_LEFT, 190, 16, Color("65eaff"))
|
||
var reactor_label := _ui("REAKTOR %02d") % max(reactor_lives, 0)
|
||
draw_string(font, Vector2(25, 59), reactor_label, HORIZONTAL_ALIGNMENT_LEFT, 165, 16, Color("8feeff"))
|
||
if coop_mode:
|
||
var partner_slot := 1 - _local_credit_slot()
|
||
var partner_name := _ui("PARTNER" if coop_online_mode else "KI")
|
||
draw_string(font, Vector2(752, 58), "%s %04d EN" % [partner_name, _player_credits(partner_slot)], HORIZONTAL_ALIGNMENT_CENTER, 154, 12, Color("ffcf7a"))
|
||
if coop_mode:
|
||
var viewing_right := camera_offset.x > 0.0
|
||
var viewing_own_side := (viewing_right and _local_credit_slot() == 1) or (not viewing_right and _local_credit_slot() == 0)
|
||
var switch_label := ("< PARTNER" if viewing_right else "PARTNER >") if viewing_own_side else ("< ICH" if viewing_right else "ICH >")
|
||
_draw_button(coop_side_button, switch_label, false, Color("604827"))
|
||
_draw_button(speed_button, "%.0f×" % game_speed, game_speed > 1.0)
|
||
_draw_button(pause_button, "II" if game_mode == "playing" else ">", false)
|
||
_draw_button(wave_button, _ui("WELLE %d STARTEN") % (wave + 1) if not wave_active else _ui("%d IM ANFLUG") % (enemies.size() + enemies_to_spawn), wave_active)
|
||
if tutorial_completed and selected_tower_type < 0 and selected_tower_index < 0 and ads_manager.can_offer_rewarded():
|
||
_draw_button(reward_ad_button, "VIDEO: +150 NOTFALLENERGIE", false, Color("285044"))
|
||
draw_rect(Rect2(0, 760, 1600, 140), Color(0.012, 0.028, 0.055, 0.97))
|
||
draw_line(Vector2(0, 760), Vector2(1600, 760), Color("2e7593"), 2.0)
|
||
var tower_display_order := _tower_display_order()
|
||
for index in tower_buttons.size():
|
||
var tower_type: int = tower_display_order[index]
|
||
_draw_tower_card(index, tower_type, selected_tower_type == tower_type)
|
||
if selected_tower_index >= 0 and selected_tower_index < towers.size():
|
||
var tower: Dictionary = towers[selected_tower_index]
|
||
var upgrade_label := "MAX. STUFE" if tower.level >= 3 else _ui("UPGRADE %d") % _upgrade_cost(tower)
|
||
_draw_button(upgrade_button, upgrade_label, false)
|
||
_draw_button(sell_button, "VERKAUFEN", false, Color("6b3542"))
|
||
if not coop_online_mode:
|
||
var priority_label := "ZIEL: %s" % _tower_priority_label(str(tower.get("target_priority", "first")))
|
||
if int(tower.type) == 1:
|
||
priority_label = "ZIEL: SCHILD ZUERST (LASER FIX)"
|
||
_draw_button(target_priority_button, priority_label, int(tower.type) == 1, Color("26394a"))
|
||
if selected_tower_type >= 0 or selected_tower_index >= 0:
|
||
_draw_tower_info_panel()
|
||
draw_rect(Rect2(wave_current_preview_rect.position, Vector2(wave_next_preview_rect.end.x - wave_current_preview_rect.position.x, wave_current_preview_rect.size.y)), Color(0.008, 0.028, 0.052, 0.90), true)
|
||
draw_rect(Rect2(wave_current_preview_rect.position, Vector2(wave_next_preview_rect.end.x - wave_current_preview_rect.position.x, wave_current_preview_rect.size.y)), Color(0.22, 0.54, 0.66, 0.68), false, 1.0)
|
||
draw_line(Vector2(wave_next_preview_rect.position.x - 4.0, wave_current_preview_rect.position.y + 6.0), Vector2(wave_next_preview_rect.position.x - 4.0, wave_current_preview_rect.end.y - 6.0), Color("2b596a", 0.8), 1.0)
|
||
_draw_wave_preview_panel(wave_current_preview_rect, _ui("JETZT • %02d/%02d") % [wave, MAX_WAVES], wave)
|
||
var next_wave := wave + 1
|
||
if wave >= MAX_WAVES and not endless_mode:
|
||
next_wave = 0
|
||
var next_heading := _ui("DANN • %02d") % next_wave if next_wave > 0 else _ui("DANN • --")
|
||
_draw_wave_preview_panel(wave_next_preview_rect, next_heading, next_wave)
|
||
draw_rect(auto_wave_button, Color("102d40"), true)
|
||
draw_rect(auto_wave_button, Color("37677b"), false, 1.0)
|
||
draw_rect(Rect2(auto_wave_button.position + Vector2(7, 4), Vector2(14, 14)), Color("07131f"), true)
|
||
if auto_start_waves:
|
||
draw_rect(Rect2(auto_wave_button.position + Vector2(10, 7), Vector2(8, 8)), Color("65eaff"), true)
|
||
draw_string(font, auto_wave_button.position + Vector2(29, 14), _ui("NÄCHSTE WELLE AUTO STARTEN"), HORIZONTAL_ALIGNMENT_LEFT, 220, 10, Color("dceaf0"))
|
||
if speed_dropdown_open:
|
||
for speed_index in SPEED_OPTIONS.size():
|
||
var option_rect := _speed_option_rect(speed_index)
|
||
draw_rect(option_rect, Color("102d40"), true)
|
||
draw_rect(option_rect, Color("37677b"), false, 1.0)
|
||
draw_string(font, option_rect.position + Vector2(0, 18), "%.0f×" % SPEED_OPTIONS[speed_index], HORIZONTAL_ALIGNMENT_CENTER, option_rect.size.x, 13, Color("8ff4ff") if is_equal_approx(game_speed, SPEED_OPTIONS[speed_index]) else Color.WHITE)
|
||
if developer_panel_open:
|
||
draw_rect(developer_panel_rect, Color(0.008, 0.024, 0.045, 0.96), true)
|
||
draw_rect(developer_panel_rect, Color("ffad62"), false, 1.5)
|
||
draw_string(font, developer_panel_rect.position + Vector2(12, 22), "TESTMODUS • F9 ZUM SCHLIESSEN", HORIZONTAL_ALIGNMENT_LEFT, 490, 11, Color("ffc277"))
|
||
_draw_button(developer_strength_down_button, "−", false, Color("44333a"))
|
||
draw_string(font, developer_panel_rect.position + Vector2(58, 57), "GEGNER ×%.1f" % enemy_strength_multiplier, HORIZONTAL_ALIGNMENT_CENTER, 128, 13, Color.WHITE)
|
||
_draw_button(developer_strength_up_button, "+", false, Color("44333a"))
|
||
_draw_button(developer_crystals_down_button, "−25", false, Color("44333a"))
|
||
draw_string(font, developer_panel_rect.position + Vector2(310, 57), "KRISTALLE %04d" % core_crystals, HORIZONTAL_ALIGNMENT_CENTER, 136, 13, Color("8ff4ff"))
|
||
_draw_button(developer_crystals_up_button, "+25", false, Color("44333a"))
|
||
if status_timer > 0.0:
|
||
draw_rect(Rect2(560, 108, 480, 44), Color(0.01, 0.035, 0.07, 0.88), true)
|
||
draw_string(font, Vector2(570, 138), status_text, HORIZONTAL_ALIGNMENT_CENTER, 460, 18, Color("a9efff"))
|
||
|
||
|
||
func _draw_wave_preview_panel(rect: Rect2, heading: String, for_wave: int) -> void:
|
||
var font := ThemeDB.fallback_font
|
||
var summary := _wave_preview_summary(for_wave)
|
||
draw_string(font, rect.position + Vector2(8, 14), heading, HORIZONTAL_ALIGNMENT_LEFT, 160, 9, Color("96d8e8"))
|
||
draw_string(font, rect.position + Vector2(164, 14), _ui("%d SCHIFFE") % int(summary.total), HORIZONTAL_ALIGNMENT_RIGHT, 84, 8, Color("dceaf0"))
|
||
var type_counts: Dictionary = summary.types
|
||
var type_order: Array = summary.get("type_order", [])
|
||
for category_info in [{"name": "air", "x": 7.0, "color": Color("ff777d")}, {"name": "ground", "x": 133.0, "color": Color("78f29a")}]:
|
||
var category: String = category_info.name
|
||
var category_x: float = float(category_info.x)
|
||
var category_color: Color = category_info.color
|
||
_draw_wave_category_icon(Vector2(rect.position.x + category_x + 7.0, rect.position.y + 44.0), category, category_color)
|
||
var drawn := 0
|
||
for type in type_order:
|
||
if _enemy_category(type) != category:
|
||
continue
|
||
if drawn >= 3:
|
||
break
|
||
var icon_rect := Rect2(rect.position.x + category_x + 17.0 + drawn * 35.0, rect.position.y + 32.0, 15.0, 13.0)
|
||
_draw_enemy_miniature(type, icon_rect, category_color)
|
||
draw_string(font, icon_rect.position + Vector2(16, 12), "×%d" % int(type_counts[type]), HORIZONTAL_ALIGNMENT_LEFT, 19, 8, Color("dceaf0"))
|
||
drawn += 1
|
||
|
||
|
||
func _wave_preview_summary(for_wave: int) -> Dictionary:
|
||
var summary := {"total": 0, "air": 0, "ground": 0, "types": {}, "type_order": []}
|
||
for enemy_type in _wave_preview_types(for_wave):
|
||
var type := int(enemy_type)
|
||
var category := _enemy_category(type)
|
||
if not summary.types.has(type):
|
||
summary.types[type] = 0
|
||
summary.type_order.append(type)
|
||
summary.types[type] = int(summary.types[type]) + 1
|
||
summary[category] = int(summary[category]) + 1
|
||
summary.total = int(summary.total) + 1
|
||
return summary
|
||
|
||
|
||
func _wave_preview_types(for_wave: int) -> Array[int]:
|
||
var result: Array[int] = []
|
||
if for_wave <= 0:
|
||
return result
|
||
if coop_online_mode and for_wave == wave:
|
||
for enemy in enemies:
|
||
result.append(int(enemy.type))
|
||
result.append_array(online_wave_types_remaining)
|
||
return result
|
||
if for_wave == wave and not wave_type_schedule.is_empty():
|
||
for entry in wave_type_schedule:
|
||
result.append(int(entry.type))
|
||
return result
|
||
for entry in _build_wave_schedule(for_wave, _wave_population(for_wave)):
|
||
result.append(int(entry.type))
|
||
return result
|
||
|
||
|
||
func _draw_wave_category_icon(center: Vector2, category: String, color: Color) -> void:
|
||
var direction := 1.0 if category == "air" else -1.0
|
||
var arrow := PackedVector2Array([
|
||
center + Vector2(0, -7.0 * direction),
|
||
center + Vector2(-5, -1.0 * direction),
|
||
center + Vector2(-2, -1.0 * direction),
|
||
center + Vector2(-2, 5.0 * direction),
|
||
center + Vector2(2, 5.0 * direction),
|
||
center + Vector2(2, -1.0 * direction),
|
||
center + Vector2(5, -1.0 * direction)
|
||
])
|
||
draw_colored_polygon(arrow, color)
|
||
|
||
|
||
func _draw_enemy_miniature(enemy_type: int, rect: Rect2, category_color := Color("4e8194")) -> void:
|
||
draw_rect(rect.grow(1.0), Color(category_color, 0.18), true)
|
||
draw_texture_rect_region(_enemy_texture(enemy_type), rect, _enemy_source_rect(enemy_type), Color.WHITE)
|
||
draw_rect(rect.grow(1.0), Color(category_color, 0.82), false, 1.0)
|
||
|
||
|
||
func _speed_option_rect(index: int) -> Rect2:
|
||
return Rect2(speed_button.position.x, speed_button.end.y + 2.0 + index * 28.0, speed_button.size.x, 27.0)
|
||
|
||
|
||
func _draw_tower_card(slot: int, type: int, active: bool) -> void:
|
||
var rect: Rect2 = tower_buttons[slot]
|
||
var data: Dictionary = TOWER_DATA[type]
|
||
var font := ThemeDB.fallback_font
|
||
var unlocked := unlocked_towers.has(type)
|
||
var class_color := _tower_class_color(type)
|
||
var panel_color := class_color.lightened(0.28) if active else (class_color.darkened(0.42) if unlocked else class_color.darkened(0.62))
|
||
draw_rect(rect, panel_color, true)
|
||
draw_rect(rect, class_color.lightened(0.30) if active else class_color.darkened(0.18), false, 1.5)
|
||
draw_rect(Rect2(rect.position, Vector2(5, rect.size.y)), class_color.lightened(0.25) if unlocked else class_color.darkened(0.15), true)
|
||
draw_string(font, rect.position + Vector2(9, 20), _ui(data.name), HORIZONTAL_ALIGNMENT_LEFT, 122, 13, Color.WHITE if unlocked else Color("9ab0ba"))
|
||
draw_string(font, rect.position + Vector2(132, 20), "%d EN" % data.cost, HORIZONTAL_ALIGNMENT_RIGHT, 77, 10, Color("7ce7ff"))
|
||
draw_string(font, rect.position + Vector2(9, 42), _ui("SCH %.0f • %.2f SCHUSS/S") % [data.damage, _tower_shots_per_second(type, 1)], HORIZONTAL_ALIGNMENT_LEFT, 200, 9, Color("dceaf0"))
|
||
draw_string(font, rect.position + Vector2(9, 63), _ui("RW %.0f • %s") % [data.range, _ui(_tower_target_label(type))], HORIZONTAL_ALIGNMENT_LEFT, 200, 9, class_color.lightened(0.45))
|
||
var hint := "ANTIPPEN • BAUPLATZ WÄHLEN" if unlocked else "GESPERRT • IM SHOP FREISCHALTEN"
|
||
draw_string(font, rect.position + Vector2(9, 85), _ui(hint), HORIZONTAL_ALIGNMENT_LEFT, 200, 8, Color("719aaa") if unlocked else Color("ffcf7a"))
|
||
|
||
|
||
func _tower_class_color(tower_type: int) -> Color:
|
||
match _tower_target_class(tower_type):
|
||
"air":
|
||
return Color("bb5260") # Rot: Luft
|
||
"ground":
|
||
return Color("668c65") # Grün: Boden
|
||
_:
|
||
return Color("347b98") # Blau-Cyan: Luft + Boden
|
||
|
||
|
||
func _draw_store() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.70), true)
|
||
_draw_button(shop_back_button, "< ZURÜCK", false, Color("26394a"))
|
||
draw_string(font, Vector2(0, 115), _ui("GERGAMES-ARSENAL"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 42, Color("c7f5ff"))
|
||
draw_string(font, Vector2(0, 160), _ui("Türme und Einsatzsektoren mit Kernkristallen freischalten"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 18, Color("8feeff"))
|
||
draw_string(font, Vector2(1270, 55), _ui("KERNKRISTALLE %04d") % core_crystals, HORIZONTAL_ALIGNMENT_RIGHT, 270, 20, Color("8ff4ff"))
|
||
_draw_button(store_tower_tab_button, "TÜRME", store_tab == "towers", Color("24475b"))
|
||
_draw_button(store_map_tab_button, "KARTEN", store_tab == "maps", Color("24475b"))
|
||
if store_tab == "maps":
|
||
for slot in map_shop_purchase_buttons.size():
|
||
var map_index := slot + 3
|
||
var button: Rect2 = map_shop_purchase_buttons[slot]
|
||
var card := Rect2(button.position.x, 225, 350, 405)
|
||
var is_unlocked := unlocked_maps.has(map_index)
|
||
draw_rect(card, Color(0.012, 0.04, 0.07, 0.94), true)
|
||
draw_rect(card, Color("65dfff", 0.72), false, 2.0)
|
||
draw_texture_rect(MAP_TEXTURES[map_index], Rect2(card.position + Vector2(12, 12), Vector2(326, 176)), false)
|
||
draw_string(font, card.position + Vector2(10, 222), _ui(MAP_NAMES[map_index]), HORIZONTAL_ALIGNMENT_CENTER, 330, 17, Color("9feeff"))
|
||
draw_string(font, card.position + Vector2(10, 250), _ui(MAP_DESCRIPTIONS[map_index]), HORIZONTAL_ALIGNMENT_CENTER, 330, 13, Color("dceaf0"))
|
||
draw_string(font, card.position + Vector2(10, 282), _ui("80-PX-RASTER • DIREKTZIEL REAKTOR"), HORIZONTAL_ALIGNMENT_CENTER, 330, 10, Color("8fddef"))
|
||
var button_text := "FREIGESCHALTET" if is_unlocked else _ui("FREISCHALTEN • %d KERNKRISTALLE") % int(MAP_UNLOCK_COSTS[map_index])
|
||
_draw_button(button, button_text, is_unlocked, Color("24516a") if not is_unlocked else Color("245843"))
|
||
draw_string(font, Vector2(0, 685), _ui("Jeder Sektor hat einen eigenen, rastergenauen Laufweg bis in den Reaktor."), HORIZONTAL_ALIGNMENT_CENTER, 1600, 17, Color("ffcf7a"))
|
||
return
|
||
var textures: Array[Texture2D] = [PLASMA_TEXTURE, EMP_TEXTURE, RAILGUN_TEXTURE]
|
||
for slot in shop_purchase_buttons.size():
|
||
var tower_type := slot + 3
|
||
var card := Rect2(shop_purchase_buttons[slot].position.x, 215, 360, 400)
|
||
var data: Dictionary = TOWER_DATA[tower_type]
|
||
draw_rect(card, Color(0.012, 0.04, 0.07, 0.94), true)
|
||
draw_rect(card, Color(data.color, 0.72), false, 2.0)
|
||
draw_texture_rect(textures[slot], Rect2(card.position + Vector2(112, 18), Vector2(136, 136)), false)
|
||
draw_string(font, card.position + Vector2(20, 190), _ui(data.name), HORIZONTAL_ALIGNMENT_CENTER, 320, 23, Color(data.color))
|
||
draw_string(font, card.position + Vector2(22, 224), _ui("SCH %.0f • %.2f SCHUSS/S") % [data.damage, _tower_shots_per_second(tower_type, 1)], HORIZONTAL_ALIGNMENT_CENTER, 316, 15, Color.WHITE)
|
||
draw_string(font, card.position + Vector2(22, 251), _ui("RW %.0f • %s") % [data.range, _ui(_tower_target_label(tower_type))], HORIZONTAL_ALIGNMENT_CENTER, 316, 14, Color("9fdce9"))
|
||
draw_string(font, card.position + Vector2(22, 295), _ui(data.description), HORIZONTAL_ALIGNMENT_CENTER, 316, 14, Color("c4d7df"))
|
||
var is_unlocked := unlocked_towers.has(tower_type)
|
||
var button_text := "FREIGESCHALTET" if is_unlocked else _ui("FREISCHALTEN • %d KERNKRISTALLE") % int(data.unlock_cost)
|
||
_draw_button(shop_purchase_buttons[slot], button_text, is_unlocked, Color("24516a") if not is_unlocked else Color("245843"))
|
||
draw_string(font, Vector2(0, 685), _ui("Weitere Verteidigungen lassen sich dauerhaft mit Kernkristallen freischalten."), HORIZONTAL_ALIGNMENT_CENTER, 1600, 18, Color("ffcf7a"))
|
||
|
||
|
||
func _draw_tower_info_panel() -> void:
|
||
var type := selected_tower_type
|
||
var level := 1
|
||
var is_built := selected_tower_index >= 0 and selected_tower_index < towers.size()
|
||
if is_built:
|
||
var tower: Dictionary = towers[selected_tower_index]
|
||
type = tower.type
|
||
level = tower.level
|
||
if type < 0:
|
||
return
|
||
var data: Dictionary = TOWER_DATA[type]
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(tower_info_rect, Color(0.008, 0.028, 0.055, 0.58), true)
|
||
draw_rect(tower_info_rect, Color(0.29, 0.60, 0.70, 0.72), false, 1.5)
|
||
var heading := _ui("%s • STUFE %d") % [_ui(data.name), level] if is_built else _ui("%s • BAUVORSCHAU") % _ui(data.name)
|
||
draw_string(font, tower_info_rect.position + Vector2(10, 19), heading, HORIZONTAL_ALIGNMENT_LEFT, 300, 11, Color("a9efff"))
|
||
draw_string(font, tower_info_rect.position + Vector2(10, 37), _ui("SCH %.0f • %.2f/S • RW %.0f") % [_tower_damage(type, level), _tower_shots_per_second(type, level), _tower_range(type, level)], HORIZONTAL_ALIGNMENT_LEFT, 300, 9, Color.WHITE)
|
||
if is_built and level < 3:
|
||
var next_level := level + 1
|
||
draw_string(font, tower_info_rect.position + Vector2(10, 55), _ui("+%.0f SCH +%.2f/S +%.0f RW BEIM UPGRADE") % [_tower_damage(type, next_level) - _tower_damage(type, level), _tower_shots_per_second(type, next_level) - _tower_shots_per_second(type, level), _tower_range(type, next_level) - _tower_range(type, level)], HORIZONTAL_ALIGNMENT_LEFT, 300, 8, Color("84ffca"))
|
||
elif is_built:
|
||
draw_string(font, tower_info_rect.position + Vector2(10, 55), _ui("MAXIMALE AUSBAUSTUFE"), HORIZONTAL_ALIGNMENT_LEFT, 300, 8, Color("84ffca"))
|
||
else:
|
||
draw_string(font, tower_info_rect.position + Vector2(10, 55), "%s • %s" % [_ui(_tower_target_label(type)), _ui(str(data.damage_type))], HORIZONTAL_ALIGNMENT_LEFT, 300, 8, Color("84ffca"))
|
||
|
||
|
||
func _draw_button(rect: Rect2, text: String, active: bool, base := Color("173c55")) -> void:
|
||
var font := ThemeDB.fallback_font
|
||
var color: Color = Color("226c88") if active else base
|
||
draw_rect(rect, color, true)
|
||
draw_rect(rect, Color("65dfff") if active else Color("37677b"), false, 2.0)
|
||
var lines := _ui(text).split("\n")
|
||
if lines.size() == 1:
|
||
draw_string(font, rect.position + Vector2(0, rect.size.y * 0.62), lines[0], HORIZONTAL_ALIGNMENT_CENTER, rect.size.x, 17, Color.WHITE)
|
||
else:
|
||
draw_string(font, rect.position + Vector2(0, 28), lines[0], HORIZONTAL_ALIGNMENT_CENTER, rect.size.x, 18, Color.WHITE)
|
||
draw_string(font, rect.position + Vector2(0, 54), lines[1], HORIZONTAL_ALIGNMENT_CENTER, rect.size.x, 14, Color("7ce7ff"))
|
||
|
||
|
||
func _draw_menu() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.52))
|
||
draw_string(font, Vector2(0, 145), "VOID BASTION", HORIZONTAL_ALIGNMENT_CENTER, 1600, 64, Color("c7f5ff"))
|
||
draw_string(font, Vector2(0, 202), "ORBITAL DEFENSE PROTOCOL", HORIZONTAL_ALIGNMENT_CENTER, 1600, 23, Color("56cce9"))
|
||
if not pending_match_invite.is_empty():
|
||
_draw_button(Rect2(470, 226, 660, 54), _ui("EINLADUNG VON %s ANNEHMEN") % String(pending_match_invite.get("from", "MITSPIELER")), true, Color("1d665b"))
|
||
_draw_button(profile_button, "%s • %s" % [account_manager.display_name, _ui("PROFIL")], false, Color("233f57"))
|
||
if menu_step == "map":
|
||
draw_string(font, Vector2(0, 300), _ui("EINSATZKARTE WÄHLEN • GESPERRTE SEKTOREN IM SHOP FREISCHALTEN"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 20, Color("8feeff"))
|
||
for index in map_buttons.size():
|
||
var card: Rect2 = map_buttons[index]
|
||
var map_index := index
|
||
var is_unlocked := unlocked_maps.has(map_index)
|
||
draw_texture_rect(MAP_TEXTURES[map_index], card, false, Color(0.82, 0.88, 0.94, 1.0) if is_unlocked else Color(0.38, 0.46, 0.54, 1.0))
|
||
draw_rect(card, Color(0.005, 0.015, 0.035, 0.22 if is_unlocked else 0.48), true)
|
||
draw_rect(card, Color("70eaff") if selected_map == map_index and is_unlocked else Color("436574"), false, 4.0 if selected_map == map_index and is_unlocked else 2.0)
|
||
var footer_y := card.size.y - 52.0
|
||
draw_rect(Rect2(card.position + Vector2(0, footer_y), Vector2(card.size.x, 52)), Color(0.01, 0.025, 0.05, 0.88), true)
|
||
draw_string(font, card.position + Vector2(0, footer_y + 21), _ui(MAP_NAMES[map_index]), HORIZONTAL_ALIGNMENT_CENTER, card.size.x, 14, Color.WHITE)
|
||
var description: String = _ui(MAP_DESCRIPTIONS[map_index]) if is_unlocked else _ui("GESPERRT • %d KERNKRISTALLE") % int(MAP_UNLOCK_COSTS[map_index])
|
||
draw_string(font, card.position + Vector2(0, footer_y + 42), description, HORIZONTAL_ALIGNMENT_CENTER, card.size.x, 11, Color("8fddef") if is_unlocked else Color("ffcf7a"))
|
||
draw_string(font, Vector2(0, 720), _ui("Wähle einen Sektor, um Schwierigkeit und Missionsstart festzulegen."), HORIZONTAL_ALIGNMENT_CENTER, 1600, 16, Color("d7e2ea"))
|
||
else:
|
||
_draw_button(map_step_back_button, "< KARTE", false, Color("26394a"))
|
||
var selected_card := Rect2(500, 315, 600, 145)
|
||
draw_texture_rect(MAP_TEXTURES[selected_map], selected_card, false, Color(0.86, 0.90, 0.95, 1.0))
|
||
draw_rect(selected_card, Color(0.005, 0.015, 0.035, 0.34), true)
|
||
draw_rect(selected_card, Color("70eaff"), false, 3.0)
|
||
draw_rect(Rect2(selected_card.position + Vector2(0, 92), Vector2(selected_card.size.x, 53)), Color(0.01, 0.025, 0.05, 0.88), true)
|
||
draw_string(font, selected_card.position + Vector2(0, 116), _ui(MAP_NAMES[selected_map]), HORIZONTAL_ALIGNMENT_CENTER, selected_card.size.x, 18, Color.WHITE)
|
||
draw_string(font, selected_card.position + Vector2(0, 138), _ui(MAP_DESCRIPTIONS[selected_map]), HORIZONTAL_ALIGNMENT_CENTER, selected_card.size.x, 13, Color("8fddef"))
|
||
draw_string(font, Vector2(0, 490), _ui("SCHWIERIGKEIT WÄHLEN"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 18, Color("9fdce9"))
|
||
for index in difficulty_buttons.size():
|
||
_draw_button(difficulty_buttons[index], DIFFICULTY_DATA[index].name, selected_difficulty == index, Color("27394d"))
|
||
draw_string(font, Vector2(0, 603), _ui(DIFFICULTY_DESCRIPTIONS[selected_difficulty]), HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("ffcf7a"))
|
||
_draw_button(start_button, "MISSION STARTEN", false, Color("18556f"))
|
||
draw_string(font, Vector2(0, 755), _ui("KARTENBONUS %d • BESTE WELLE %02d • KERNKRISTALLE %04d") % [int(_difficulty().crystals), best_wave, core_crystals], HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, Color("86c8d8"))
|
||
_draw_button(leaderboard_button, "BESTENLISTE", false, Color("24475b"))
|
||
_draw_button(multiplayer_button, "MULTIPLAYER", false, Color("254f75"))
|
||
_draw_button(shop_button, "ARSENAL / SHOP", false, Color("24516a"))
|
||
_draw_button(audio_settings_button, "EINSTELLUNGEN", false, Color("24516a"))
|
||
_draw_button(promo_code_button, "CODE EINLÖSEN", false, Color("34576b"))
|
||
_draw_button(exit_button, "SPIEL BEENDEN", false, Color("5c3342"))
|
||
draw_string(font, Vector2(0, 860), _ui("VERSION %s • GERGAMES-ONLINEKONTO • GLOBALE BESTENLISTEN") % _app_version(), HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("688293"))
|
||
if update_notice_visible:
|
||
_draw_button(update_button, "UPDATE %s VERFÜGBAR" % str(available_update.get("version", "")), true, Color("1f5d4d"))
|
||
_draw_button(update_dismiss_button, "×", false, Color("3d4650"))
|
||
|
||
|
||
func _draw_promo_code_dialog() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.0, 0.01, 0.03, 0.72), true)
|
||
var panel := Rect2(420, 265, 760, 390)
|
||
draw_rect(panel, Color(0.008, 0.028, 0.055, 0.98), true)
|
||
draw_rect(panel, Color("4ab8d1"), false, 2.0)
|
||
_draw_button(promo_code_cancel_button, "< ZURÜCK", false, Color("26394a"))
|
||
draw_string(font, Vector2(panel.position.x, 330), _ui("SONDER-CODE"), HORIZONTAL_ALIGNMENT_CENTER, panel.size.x, 30, Color("c7f5ff"))
|
||
draw_string(font, Vector2(panel.position.x + 60, 370), _ui("Gib einen persönlichen Freischaltcode ein."), HORIZONTAL_ALIGNMENT_CENTER, panel.size.x - 120, 16, Color("9dbac8"))
|
||
draw_string(font, Vector2(promo_code_input_rect.position.x, promo_code_input_rect.position.y - 12), _ui("FREISCHALTCODE"), HORIZONTAL_ALIGNMENT_LEFT, promo_code_input_rect.size.x, 12, Color("8feeff"))
|
||
_draw_button(promo_code_redeem_button, "CODE EINLÖSEN" if not promo_code_pending else "BITTE WARTEN …", promo_code_pending, Color("185f68"))
|
||
if not promo_code_message.is_empty():
|
||
draw_string(font, Vector2(panel.position.x + 35, 555), promo_code_message, HORIZONTAL_ALIGNMENT_CENTER, panel.size.x - 70, 15, Color("8fffea") if promo_code_message.begins_with("Freigeschaltet") else Color("ffcf7a"))
|
||
|
||
|
||
func _draw_audio_settings() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.002, 0.008, 0.02, 0.62), true)
|
||
draw_rect(audio_panel_rect, Color(0.01, 0.035, 0.065, 0.98), true)
|
||
draw_rect(audio_panel_rect, Color("4ab8d1"), false, 2.0)
|
||
draw_string(font, audio_panel_rect.position + Vector2(30, 54), _ui("EINSTELLUNGEN"), HORIZONTAL_ALIGNMENT_LEFT, 450, 25, Color("bcefff"))
|
||
_draw_button(audio_close_button, "SCHLIESSEN", false, Color("26394a"))
|
||
draw_string(font, Vector2(audio_slider_rect.position.x, audio_slider_rect.position.y - 18), _ui("MASTER-LAUTSTÄRKE"), HORIZONTAL_ALIGNMENT_LEFT, 260, 16, Color("dceaf0"))
|
||
draw_string(font, Vector2(audio_slider_rect.end.x - 80, audio_slider_rect.position.y - 18), "%d%%" % roundi(master_volume * 100.0), HORIZONTAL_ALIGNMENT_RIGHT, 80, 16, Color("8ff4ff"))
|
||
var track := Rect2(audio_slider_rect.position.x, audio_slider_rect.position.y + 13, audio_slider_rect.size.x, 6)
|
||
draw_rect(track, Color("24475b"), true)
|
||
draw_rect(Rect2(track.position, Vector2(track.size.x * master_volume, track.size.y)), Color("45d4ee"), true)
|
||
var knob_position := Vector2(track.position.x + track.size.x * master_volume, track.position.y + track.size.y * 0.5)
|
||
draw_circle(knob_position, 13, Color("0b1d2c"))
|
||
draw_circle(knob_position, 9, Color("8ff4ff"))
|
||
draw_string(font, Vector2(audio_panel_rect.position.x + 115, 530), _ui("SPRACHE"), HORIZONTAL_ALIGNMENT_LEFT, 460, 17, Color("dceaf0"))
|
||
_draw_button(language_button_de, "DEUTSCH", ui_language == "de", Color("24516a"))
|
||
_draw_button(language_button_en, "ENGLISCH", ui_language == "en", Color("24516a"))
|
||
draw_string(font, Vector2(audio_panel_rect.position.x + 30, audio_panel_rect.end.y - 28), _ui("Die Einstellung wird automatisch gespeichert."), HORIZONTAL_ALIGNMENT_LEFT, audio_panel_rect.size.x - 60, 13, Color("91a8bc"))
|
||
|
||
|
||
func _draw_leaderboard() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.84))
|
||
_draw_button(leaderboard_back_button, "< ZURÜCK", false, Color("26394a"))
|
||
_draw_button(leaderboard_refresh_button, "AKTUALISIEREN", leaderboard_loading, Color("24516a"))
|
||
draw_string(font, Vector2(0, 108), _ui("GLOBALE BESTENLISTE"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 42, Color("c7f5ff"))
|
||
_draw_button(leaderboard_map_previous_button, "<", false, Color("26394a"))
|
||
_draw_button(leaderboard_map_button, "%s • %s" % [_ui("KARTE"), _ui(MAP_NAMES[leaderboard_map])], true, Color("24475b"))
|
||
_draw_button(leaderboard_map_next_button, ">", false, Color("26394a"))
|
||
_draw_button(leaderboard_difficulty_previous_button, "<", false, Color("26394a"))
|
||
_draw_button(leaderboard_difficulty_button, "%s • %s" % [_ui("STUFE"), _ui(DIFFICULTY_DATA[leaderboard_difficulty].name)], true, Color("5b452b"))
|
||
_draw_button(leaderboard_difficulty_next_button, ">", false, Color("26394a"))
|
||
_draw_button(leaderboard_mission_button, "MISSION", not leaderboard_endless, Color("24475b"))
|
||
_draw_button(leaderboard_endless_button, "ENDLOSMODUS", leaderboard_endless, Color("5b452b"))
|
||
var panel := Rect2(205, 280, 1190, 505)
|
||
draw_rect(panel, Color(0.012, 0.035, 0.065, 0.96), true)
|
||
draw_rect(panel, Color("39788e"), false, 2.0)
|
||
draw_string(font, Vector2(235, 319), _ui("RANG"), HORIZONTAL_ALIGNMENT_LEFT, 75, 13, Color("91a8bc"))
|
||
draw_string(font, Vector2(325, 319), _ui("PILOT"), HORIZONTAL_ALIGNMENT_LEFT, 270, 13, Color("91a8bc"))
|
||
draw_string(font, Vector2(610, 319), _ui("KARTE"), HORIZONTAL_ALIGNMENT_LEFT, 240, 13, Color("91a8bc"))
|
||
draw_string(font, Vector2(850, 319), _ui("STUFE"), HORIZONTAL_ALIGNMENT_LEFT, 130, 13, Color("91a8bc"))
|
||
draw_string(font, Vector2(995, 319), _ui("WELLE"), HORIZONTAL_ALIGNMENT_CENTER, 110, 13, Color("91a8bc"))
|
||
draw_string(font, Vector2(1160, 319), _ui("PUNKTE"), HORIZONTAL_ALIGNMENT_RIGHT, 190, 13, Color("91a8bc"))
|
||
draw_line(Vector2(230, 334), Vector2(1370, 334), Color("315c70"), 1.0)
|
||
var own_user_id: String = str(online_manager.session.user_id) if online_manager.session != null else ""
|
||
for index in leaderboard_rows.size():
|
||
var row: Dictionary = leaderboard_rows[index]
|
||
var y := 370.0 + index * 32.0
|
||
if index % 2 == 0:
|
||
draw_rect(Rect2(225, y - 23, 1150, 29), Color(0.03, 0.09, 0.14, 0.64), true)
|
||
var owner_color := Color("83ffc8") if not own_user_id.is_empty() and str(row.get("owner_id", "")) == own_user_id else Color.WHITE
|
||
draw_string(font, Vector2(240, y), "%02d" % int(row.get("rank", index + 1)), HORIZONTAL_ALIGNMENT_LEFT, 65, 14, Color("8ff4ff"))
|
||
draw_string(font, Vector2(325, y), str(row.get("username", "PILOT")).to_upper(), HORIZONTAL_ALIGNMENT_LEFT, 270, 14, owner_color)
|
||
draw_string(font, Vector2(610, y), _ui(MAP_NAMES[int(row.get("map", leaderboard_map))]), HORIZONTAL_ALIGNMENT_LEFT, 225, 13, Color("9fdce9"))
|
||
draw_string(font, Vector2(850, y), _ui(DIFFICULTY_DATA[int(row.get("difficulty", leaderboard_difficulty))].name), HORIZONTAL_ALIGNMENT_LEFT, 130, 13, Color("ffcf7a"))
|
||
draw_string(font, Vector2(995, y), "%02d" % int(row.get("wave", 0)), HORIZONTAL_ALIGNMENT_CENTER, 110, 14, Color("ffcf7a"))
|
||
draw_string(font, Vector2(1160, y), "%09d" % int(row.get("score", 0)), HORIZONTAL_ALIGNMENT_RIGHT, 190, 14, Color("8fffea"))
|
||
if not leaderboard_message.is_empty():
|
||
draw_string(font, Vector2(300, 520), leaderboard_message, HORIZONTAL_ALIGNMENT_CENTER, 1000, 17, Color("ffcf7a"))
|
||
draw_string(font, Vector2(0, 810), _ui("Spielernamen werden aus dem aktuellen öffentlichen Profil geladen."), HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("91a8bc"))
|
||
draw_string(font, Vector2(0, 838), _ui("WERTUNG: WELLEN + REAKTORREST + RESTENERGIE • WENIGER GEBAUTE TÜRME ERHÖHEN DEN EFFIZIENZ-MULTIPLIKATOR"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 12, Color("75aaba"))
|
||
|
||
|
||
func _draw_multiplayer_menu() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.78))
|
||
_draw_button(multiplayer_back_button, "< ZURÜCK", false, Color("26394a"))
|
||
draw_string(font, Vector2(0, 105), _ui("KOOPERATIVE VERTEIDIGUNG"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 42, Color("c7f5ff"))
|
||
draw_string(font, Vector2(0, 142), _ui("ZWEI KOMMANDANTEN • EIN GEMEINSAMER REAKTOR"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 17, Color("66dff3"))
|
||
var arena := Rect2(170, 180, 1260, 410)
|
||
draw_rect(arena, Color(0.01, 0.04, 0.075, 0.96), true)
|
||
draw_rect(arena, Color("39788e"), false, 2.0)
|
||
draw_line(Vector2(800, 200), Vector2(800, 570), Color(0.35, 0.82, 1.0, 0.24), 2.0)
|
||
_draw_coop_lane_preview(false)
|
||
_draw_coop_lane_preview(true)
|
||
for radius in [66.0, 48.0, 30.0]:
|
||
draw_arc(Vector2(800, 385), radius, 0.0, TAU, 48, Color(0.25, 0.92, 1.0, 0.78), 3.0)
|
||
draw_circle(Vector2(800, 385), 18.0, Color("8ffaff"))
|
||
draw_string(font, Vector2(665, 490), _ui("GEMEINSAMER REAKTOR"), HORIZONTAL_ALIGNMENT_CENTER, 270, 15, Color("8ff4ff"))
|
||
draw_string(font, Vector2(215, 225), "%s • %s %d" % [account_manager.display_name, _ui("ENERGIE"), multiplayer_manager.player_credits[0]], HORIZONTAL_ALIGNMENT_LEFT, 440, 17, Color("8ff4ff"))
|
||
draw_string(font, Vector2(945, 225), "%s • %s %d" % [_ui("MITSPIELER"), _ui("ENERGIE"), multiplayer_manager.player_credits[1]], HORIZONTAL_ALIGNMENT_RIGHT, 440, 17, Color("ffcf7a"))
|
||
draw_string(font, Vector2(570, 555), _ui("REAKTORLEBEN %02d • GETRENNTE RESSOURCEN") % multiplayer_manager.shared_reactor_lives, HORIZONTAL_ALIGNMENT_CENTER, 460, 16, Color.WHITE)
|
||
_draw_button(quick_match_button, "SCHNELLES SPIEL", false, Color("185f68"))
|
||
_draw_button(friend_match_button, "RAUM BEITRETEN", false, Color("254f75"))
|
||
_draw_button(private_match_button, "RAUM ERSTELLEN", false, Color("493f67"))
|
||
_draw_button(ai_match_button, "MIT KI-PARTNER", false, Color("6b4b28"))
|
||
_draw_button(friends_button, "FREUNDESLISTE", false, Color("285568"))
|
||
var service_text := "GERGAMES-SERVER VERBUNDEN • ONLINE-KOOP IST SPIELBEREIT" if online_manager.is_online() else "VERBINDUNG ZUM GERGAMES-SERVER WIRD AUTOMATISCH AUFGEBAUT"
|
||
var service_color := Color("83ffc8") if online_manager.is_online() else Color("ffcf7a")
|
||
draw_string(font, Vector2(0, 842), _ui(service_text), HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, service_color)
|
||
if not pending_match_invite.is_empty():
|
||
_draw_button(Rect2(470, 585, 660, 55), _ui("EINLADUNG VON %s ANNEHMEN") % String(pending_match_invite.get("from", "MITSPIELER")), true, Color("1d665b"))
|
||
|
||
|
||
func _draw_friends() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.84))
|
||
_draw_button(friends_back_button, "< ZURÜCK", false, Color("26394a"))
|
||
_draw_button(friends_refresh_button, "AKTUALISIEREN", false, Color("24516a"))
|
||
draw_string(font, Vector2(0, 105), _ui("FREUNDESLISTE"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 40, Color("c7f5ff"))
|
||
var own_username := String(online_manager.session.username) if online_manager.session != null else "OFFLINE"
|
||
draw_string(font, Vector2(260, 169), _ui("DEINE SPIELERKENNUNG: %s") % own_username, HORIZONTAL_ALIGNMENT_LEFT, 740, 18, Color("8ff4ff"))
|
||
_draw_button(friend_copy_button, "KENNUNG KOPIEREN", false, Color("285568"))
|
||
draw_rect(friend_name_rect, Color(0.01, 0.035, 0.065, 0.98), true)
|
||
draw_rect(friend_name_rect, Color("4ab8d1"), false, 2.0)
|
||
_draw_button(friend_add_button, "ANFRAGE SENDEN", false, Color("185f68"))
|
||
draw_string(font, Vector2(260, 292), _ui("Füge Freunde über ihre eindeutige Spielerkennung hinzu. Beide müssen die Anfrage bestätigen."), HORIZONTAL_ALIGNMENT_LEFT, 1090, 15, Color("9fdce9"))
|
||
draw_rect(Rect2(220, 315, 1160, 455), Color(0.01, 0.035, 0.065, 0.96), true)
|
||
draw_rect(Rect2(220, 315, 1160, 455), Color("39788e"), false, 2.0)
|
||
friend_action_buttons.clear()
|
||
for index in mini(online_manager.friends.size(), 8):
|
||
var friend: Dictionary = online_manager.friends[index]
|
||
var row := Rect2(240, 336 + index * 52, 1120, 46)
|
||
var action := Rect2(1100, row.position.y + 3, 235, 40)
|
||
friend_action_buttons.append(action)
|
||
draw_rect(row, Color(0.035, 0.09, 0.14, 0.82), true)
|
||
var state := int(friend.get("state", -1))
|
||
var status := ""
|
||
var action_text := ""
|
||
if state == 0:
|
||
var presence := String(online_manager.friend_statuses.get(String(friend.get("id", "")), "menu" if bool(friend.get("online", false)) else ""))
|
||
status = "IM SPIEL" if presence == "playing" else ("ONLINE" if not presence.is_empty() else "OFFLINE")
|
||
action_text = "EINLADEN" if _can_invite_friends() else "ENTFERNEN"
|
||
elif state == 1:
|
||
status = "ANFRAGE GESENDET"
|
||
action_text = "ZURÜCKZIEHEN"
|
||
elif state == 2:
|
||
status = "ANFRAGE ERHALTEN"
|
||
action_text = "ANNEHMEN"
|
||
else:
|
||
status = "BLOCKIERT"
|
||
draw_string(font, row.position + Vector2(20, 30), String(friend.get("name", "PILOT")), HORIZONTAL_ALIGNMENT_LEFT, 450, 19, Color.WHITE)
|
||
draw_string(font, row.position + Vector2(520, 30), _ui(status), HORIZONTAL_ALIGNMENT_LEFT, 270, 15, Color("83ffc8") if status == "ONLINE" or status == "IM SPIEL" else Color("ffcf7a"))
|
||
if not action_text.is_empty():
|
||
_draw_button(action, action_text, false, Color("24516a"))
|
||
if online_manager.friends.is_empty():
|
||
draw_string(font, Vector2(280, 410), _ui("Noch keine Freunde oder offenen Anfragen."), HORIZONTAL_ALIGNMENT_LEFT, 850, 19, Color("9dbac8"))
|
||
if not friends_message.is_empty():
|
||
draw_string(font, Vector2(240, 805), friends_message, HORIZONTAL_ALIGNMENT_LEFT, 1120, 16, Color("ffcf7a"))
|
||
if _can_invite_friends():
|
||
draw_string(font, Vector2(240, 835), _ui("Privater Raum %s: Freunde mit EINLADEN direkt benachrichtigen.") % multiplayer_manager.private_room_code, HORIZONTAL_ALIGNMENT_LEFT, 1120, 14, Color("8feeff"))
|
||
draw_string(font, Vector2(240, 866), _ui("Kein Chat: Einladungen enthalten nur einen Raumcode, keine freie Nachricht."), HORIZONTAL_ALIGNMENT_LEFT, 1120, 13, Color("91a8bc"))
|
||
|
||
|
||
func _draw_room_code_entry() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.88))
|
||
_draw_button(multiplayer_back_button, "< ZURÜCK", false, Color("26394a"))
|
||
draw_string(font, Vector2(0, 205), _ui("PRIVATEN RAUM BEITRETEN"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 42, Color("c7f5ff"))
|
||
draw_string(font, Vector2(0, 260), _ui("GIB DEN SECHSSTELLIGEN CODE DEINES MITPILOTEN EIN"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 17, Color("66dff3"))
|
||
draw_rect(room_code_input_rect, Color(0.01, 0.035, 0.065, 0.98), true)
|
||
draw_rect(room_code_input_rect, Color("65eaff") if room_code_input.length() == 6 else Color("39788e"), false, 3.0)
|
||
var shown_code := room_code_input
|
||
if shown_code.is_empty():
|
||
shown_code = "------"
|
||
draw_string(font, Vector2(room_code_input_rect.position.x, room_code_input_rect.position.y + 61), " ".join(shown_code.split("")), HORIZONTAL_ALIGNMENT_CENTER, room_code_input_rect.size.x, 38, Color("8ffaff") if not room_code_input.is_empty() else Color("688293"))
|
||
draw_string(font, Vector2(0, 470), _ui("BUCHSTABEN UND ZAHLEN • GROSS-/KLEINSCHREIBUNG IST EGAL"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("9fdce9"))
|
||
_draw_button(join_room_button, "RAUM BEITRETEN", room_code_input.length() == 6, Color("185f68") if room_code_input.length() == 6 else Color("26394a"))
|
||
|
||
|
||
func _draw_coop_lane_preview(mirrored: bool) -> void:
|
||
var points := PackedVector2Array([Vector2(205, 300), Vector2(505, 300), Vector2(505, 385), Vector2(720, 385)])
|
||
if mirrored:
|
||
points = multiplayer_manager.mirrored_path(points, 1600.0)
|
||
for index in points.size() - 1:
|
||
draw_line(points[index], points[index + 1], Color(0.18, 0.34, 0.42, 0.95), 54.0, true)
|
||
draw_line(points[index], points[index + 1], Color(0.52, 0.82, 0.88, 0.42), 2.0, true)
|
||
for index in 5:
|
||
var x := 300.0 + index * 78.0
|
||
if mirrored:
|
||
x = 1600.0 - x
|
||
draw_rect(Rect2(Vector2(x - 27, 465), Vector2(54, 54)), Color(0.08, 0.42, 0.55, 0.12), true)
|
||
draw_rect(Rect2(Vector2(x - 27, 465), Vector2(54, 54)), Color(0.28, 0.72, 0.82, 0.36), false, 1.0)
|
||
|
||
|
||
func _draw_matchmaking() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.88))
|
||
_draw_button(multiplayer_back_button, "< ZURÜCK", false, Color("26394a"))
|
||
var private_mode: bool = multiplayer_manager.matchmaking_mode in ["private_host", "private_join"]
|
||
draw_string(font, Vector2(0, 210), _ui("PRIVATER KOOP-RAUM" if private_mode else "MITSPIELERSUCHE"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 46, Color("c7f5ff"))
|
||
var in_lobby: bool = multiplayer_manager.player_slot >= 0 and multiplayer_manager.matchmaking_state in ["hosting", "matched", "lobby", "reconnected"]
|
||
if in_lobby:
|
||
_draw_online_lobby_players(font)
|
||
else:
|
||
var pulse := 56.0 + sin(elapsed * 3.0) * 8.0
|
||
draw_arc(Vector2(800, 390), pulse, -PI * 0.2, PI * 1.45, 52, Color("65eaff"), 5.0)
|
||
draw_circle(Vector2(800, 390), 22.0, Color("8ffaff"))
|
||
var mode_labels := {"quick": "SCHNELLES SPIEL", "private_host": "RAUM ERSTELLEN", "private_join": "RAUM BEITRETEN"}
|
||
var mode_text: String = mode_labels.get(multiplayer_manager.matchmaking_mode, "KOOP-SPIEL")
|
||
if not multiplayer_manager.private_room_code.is_empty():
|
||
mode_text = "RAUMCODE %s" % " ".join(multiplayer_manager.private_room_code.split(""))
|
||
draw_string(font, Vector2(0, 500), _ui("RAUMCODE %s") % " ".join(multiplayer_manager.private_room_code.split("")) if not multiplayer_manager.private_room_code.is_empty() else _ui(mode_text), HORIZONTAL_ALIGNMENT_CENTER, 1600, 23, Color.WHITE)
|
||
if in_lobby:
|
||
_draw_button(lobby_ready_button, "NICHT BEREIT" if multiplayer_manager.local_ready else "BEREIT", multiplayer_manager.local_ready, Color("8b6330") if multiplayer_manager.local_ready else Color("185f68"))
|
||
_draw_button(cancel_search_button, "ZURÜCK" if multiplayer_manager.matchmaking_state == "room_not_found" else "SUCHE ABBRECHEN", false, Color("5c3342"))
|
||
if multiplayer_manager.matchmaking_mode == "private_host":
|
||
_draw_button(friends_button, "FREUNDE EINLADEN", false, Color("285568"))
|
||
var online_text := "ONLINE • SUCHE NACH EINEM MITPILOTEN"
|
||
var online_color := Color("83ffc8")
|
||
if multiplayer_manager.matchmaking_state == "waiting_for_service":
|
||
online_text = "VERBINDUNG ZUM GERGAMES-SERVER WIRD AUFGEBAUT …"
|
||
online_color = Color("ffcf7a")
|
||
elif multiplayer_manager.matchmaking_state == "offline":
|
||
online_text = "SERVER NOCH NICHT ERREICHBAR • BITTE SPÄTER ERNEUT VERSUCHEN"
|
||
online_color = Color("ff8e8e")
|
||
elif multiplayer_manager.matchmaking_state == "matched":
|
||
online_text = "MITPILOT GEFUNDEN • WÄHLE BEREIT"
|
||
online_color = Color("8ffaff")
|
||
elif multiplayer_manager.matchmaking_state in ["lobby", "reconnected"]:
|
||
online_text = "BEIDE SPIELER MÜSSEN BEREIT SEIN"
|
||
online_color = Color("8ffaff")
|
||
elif multiplayer_manager.matchmaking_state == "reconnecting":
|
||
online_text = "VERBINDUNG WIRD WIEDERHERGESTELLT ."
|
||
online_color = Color("ffcf7a")
|
||
elif multiplayer_manager.matchmaking_state == "connection_lost":
|
||
online_text = "VERBINDUNG VERLOREN • BITTE ZURÜCKKEHREN"
|
||
online_color = Color("ff8e8e")
|
||
elif multiplayer_manager.matchmaking_state == "creating_room":
|
||
online_text = "PRIVATER RAUM WIRD ERSTELLT ."
|
||
online_color = Color("ffcf7a")
|
||
elif multiplayer_manager.matchmaking_state == "hosting":
|
||
online_text = "TEILE DEN RAUMCODE MIT DEINEM MITPILOTEN • WARTE AUF BEITRITT"
|
||
online_color = Color("83ffc8")
|
||
elif multiplayer_manager.matchmaking_state == "joining_room":
|
||
online_text = "PRIVATER RAUM WIRD GESUCHT ."
|
||
online_color = Color("ffcf7a")
|
||
elif multiplayer_manager.matchmaking_state == "room_not_found":
|
||
online_text = "RAUM NICHT GEFUNDEN ODER BEREITS VOLL"
|
||
online_color = Color("ff8e8e")
|
||
draw_rect(Rect2(180, 520, 1240, 44), Color(0.005, 0.015, 0.035, 0.98), true)
|
||
draw_string(font, Vector2(0, 548), _ui(online_text), HORIZONTAL_ALIGNMENT_CENTER, 1600, 16, online_color)
|
||
|
||
|
||
func _draw_online_lobby_players(font: Font) -> void:
|
||
for slot in 2:
|
||
var rect := Rect2(270 + slot * 560, 285, 500, 130)
|
||
var player = multiplayer_manager.lobby_players[slot] if slot < multiplayer_manager.lobby_players.size() else null
|
||
var occupied: bool = player is Dictionary
|
||
var connected: bool = occupied and bool(player.get("connected", false))
|
||
var ready: bool = occupied and bool(player.get("ready", false))
|
||
var border := Color("5de8a8") if ready else (Color("65eaff") if connected else Color("425567"))
|
||
draw_rect(rect, Color(0.012, 0.035, 0.065, 0.96), true)
|
||
draw_rect(rect, border, false, 3.0)
|
||
var name := String(player.get("name", "PILOT")) if occupied else "WARTE AUF MITPILOTEN"
|
||
var side := _ui("LINKE VERTEIDIGUNG" if slot == 0 else "RECHTE VERTEIDIGUNG")
|
||
var state := _ui("BEREIT" if ready else ("NICHT BEREIT" if connected else "NICHT VERBUNDEN"))
|
||
draw_string(font, Vector2(rect.position.x + 24, rect.position.y + 43), name.to_upper(), HORIZONTAL_ALIGNMENT_LEFT, rect.size.x - 48, 24, Color("e8fbff"))
|
||
draw_string(font, Vector2(rect.position.x + 24, rect.position.y + 78), side, HORIZONTAL_ALIGNMENT_LEFT, rect.size.x - 48, 14, Color("8fcddd"))
|
||
draw_string(font, Vector2(rect.position.x + 24, rect.position.y + 108), state, HORIZONTAL_ALIGNMENT_LEFT, rect.size.x - 48, 16, border)
|
||
|
||
|
||
func _draw_profile() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.76))
|
||
_draw_button(profile_back_button, "< ZURÜCK", false, Color("26394a"))
|
||
draw_string(font, Vector2(0, 112), _ui("PILOTENPROFIL"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 44, Color("c7f5ff"))
|
||
draw_string(font, Vector2(1110, 112), "VERSION %s" % _app_version(), HORIZONTAL_ALIGNMENT_RIGHT, 290, 14, Color("91a8bc"))
|
||
draw_rect(Rect2(170, 155, 1260, 600), Color(0.012, 0.035, 0.065, 0.94), true)
|
||
draw_rect(Rect2(170, 155, 1260, 600), Color("4ab8d1"), false, 2.0)
|
||
draw_string(font, Vector2(220, 220), account_manager.display_name, HORIZONTAL_ALIGNMENT_LEFT, 520, 30, Color("8ff4ff"))
|
||
draw_string(font, Vector2(220, 258), account_manager.provider_label(), HORIZONTAL_ALIGNMENT_LEFT, 650, 16, Color("ffcf7a"))
|
||
draw_string(font, Vector2(220, 292), _ui("SPIELER-ID %s") % account_manager.short_id(), HORIZONTAL_ALIGNMENT_LEFT, 650, 14, Color("91a8bc"))
|
||
_draw_button(profile_rename_button, "SPIELERNAME ÄNDERN", false, Color("24516a"))
|
||
draw_string(font, Vector2(840, 215), _ui("KONTO & CLOUD"), HORIZONTAL_ALIGNMENT_LEFT, 470, 18, Color("8feeff"))
|
||
var cloud_label := "GERGAMES ONLINE VERBUNDEN" if online_manager.is_authenticated() else "GERGAMES ONLINE OFFLINE"
|
||
var cloud_color := Color("83ffc8") if online_manager.is_authenticated() else Color("ffcf7a")
|
||
draw_string(font, Vector2(840, 252), _ui(cloud_label), HORIZONTAL_ALIGNMENT_LEFT, 470, 15, cloud_color)
|
||
draw_string(font, Vector2(840, 282), _ui("GAME CENTER / APPLE NÄCHSTE STUFE"), HORIZONTAL_ALIGNMENT_LEFT, 470, 15, Color("d7e2ea"))
|
||
draw_string(font, Vector2(840, 312), _ui("GOOGLE PLAY GAMES NÄCHSTE STUFE"), HORIZONTAL_ALIGNMENT_LEFT, 470, 15, Color("d7e2ea"))
|
||
draw_line(Vector2(220, 345), Vector2(1380, 345), Color("315c70"), 1.0)
|
||
draw_string(font, Vector2(220, 392), _ui("LOKALE BESTWERTE"), HORIZONTAL_ALIGNMENT_LEFT, 1160, 20, Color("8feeff"))
|
||
draw_string(font, Vector2(220, 430), _ui("EINSATZSEKTOR"), HORIZONTAL_ALIGNMENT_LEFT, 420, 14, Color("91a8bc"))
|
||
draw_string(font, Vector2(700, 430), _ui("MISSION"), HORIZONTAL_ALIGNMENT_CENTER, 180, 14, Color("91a8bc"))
|
||
draw_string(font, Vector2(930, 430), _ui("ENDLOS"), HORIZONTAL_ALIGNMENT_CENTER, 180, 14, Color("91a8bc"))
|
||
draw_string(font, Vector2(1160, 430), _ui("BESTE PUNKTE"), HORIZONTAL_ALIGNMENT_CENTER, 180, 14, Color("91a8bc"))
|
||
for index in MAP_NAMES.size():
|
||
var y := 466.0 + index * 28.0
|
||
draw_rect(Rect2(210, y - 19, 1180, 25), Color(0.03, 0.09, 0.14, 0.70), true)
|
||
draw_string(font, Vector2(235, y), _ui(MAP_NAMES[index]), HORIZONTAL_ALIGNMENT_LEFT, 420, 12, Color.WHITE)
|
||
draw_string(font, Vector2(700, y), _ui("WELLE") + " %02d" % account_manager.get_map_best_wave(index, false), HORIZONTAL_ALIGNMENT_CENTER, 180, 12, Color("8ff4ff"))
|
||
draw_string(font, Vector2(930, y), _ui("WELLE") + " %02d" % account_manager.get_map_best_wave(index, true), HORIZONTAL_ALIGNMENT_CENTER, 180, 12, Color("ffcf7a"))
|
||
var score := maxi(account_manager.get_map_best_score(index, false), account_manager.get_map_best_score(index, true))
|
||
draw_string(font, Vector2(1160, y), "%07d" % score, HORIZONTAL_ALIGNMENT_CENTER, 180, 12, Color("8fffea"))
|
||
if not profile_message.is_empty():
|
||
draw_string(font, Vector2(220, 658), profile_message, HORIZONTAL_ALIGNMENT_CENTER, 1160, 14, Color("ffcf7a"))
|
||
_draw_button(profile_support_button, "SUPPORT / FEEDBACK", false, Color("24516a"))
|
||
_draw_button(profile_privacy_button, "DATENSCHUTZ", false, Color("24475b"))
|
||
_draw_button(profile_delete_button, "LÖSCHEN BESTÄTIGEN" if profile_delete_armed else "KONTO LÖSCHEN", profile_delete_armed, Color("6b2d3b"))
|
||
|
||
|
||
func _draw_profile_name_entry() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.82))
|
||
_draw_button(profile_back_button, "< ZURÜCK", false, Color("26394a"))
|
||
draw_string(font, Vector2(0, 190), _ui("SPIELERNAME"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 44, Color("c7f5ff"))
|
||
draw_string(font, Vector2(0, 250), _ui("Dieser Name ist für Mitspieler und in Ranglisten sichtbar."), HORIZONTAL_ALIGNMENT_CENTER, 1600, 18, Color("a9d8e5"))
|
||
draw_rect(profile_name_input_rect, Color(0.012, 0.035, 0.065, 0.96), true)
|
||
draw_rect(profile_name_input_rect, Color("65dfff"), false, 3.0)
|
||
draw_string(font, profile_name_input_rect.position + Vector2(24, 53), profile_name_input, HORIZONTAL_ALIGNMENT_CENTER, profile_name_input_rect.size.x - 48, 26, Color.WHITE)
|
||
draw_string(font, Vector2(0, 448), _ui("3–20 Zeichen · Buchstaben, Zahlen, Leerzeichen, _ und -"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, Color("91a8bc"))
|
||
_draw_button(profile_name_save_button, "SPIELERNAME SPEICHERN", false, Color("185f68"))
|
||
if not profile_message.is_empty():
|
||
draw_string(font, Vector2(300, 585), profile_message, HORIZONTAL_ALIGNMENT_CENTER, 1000, 16, Color("ffcf7a"))
|
||
|
||
|
||
func _draw_pause_menu() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.0, 0.01, 0.03, 0.72))
|
||
var panel := Rect2(500, 330, 600, 290)
|
||
draw_rect(panel, Color(0.008, 0.028, 0.055, 0.96), true)
|
||
draw_rect(panel, Color("3d8ba5"), false, 2.0)
|
||
draw_string(font, Vector2(panel.position.x, 405), "PAUSE", HORIZONTAL_ALIGNMENT_CENTER, panel.size.x, 38, Color("c7f5ff"))
|
||
var pause_description := "Die Online-Partie läuft währenddessen weiter." if coop_online_mode else "Die Verteidigung ist angehalten."
|
||
draw_string(font, Vector2(panel.position.x, 447), _ui(pause_description), HORIZONTAL_ALIGNMENT_CENTER, panel.size.x, 16, Color("9dbac8"))
|
||
_draw_button(pause_resume_button, "FORTSETZEN", false, Color("185f68"))
|
||
_draw_button(pause_exit_button, "PARTIE VERLASSEN" if coop_online_mode else "MISSION VERLASSEN", false, Color("60404a"))
|
||
|
||
|
||
func _draw_overlay(title: String, subtitle: String) -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.0, 0.01, 0.03, 0.72))
|
||
draw_string(font, Vector2(0, 390), title, HORIZONTAL_ALIGNMENT_CENTER, 1600, 54, Color.WHITE)
|
||
draw_string(font, Vector2(0, 445), subtitle, HORIZONTAL_ALIGNMENT_CENTER, 1600, 19, Color("8adff2"))
|
||
|
||
|
||
func _draw_game_over() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.05, 0.005, 0.015, 0.76))
|
||
draw_string(font, Vector2(0, 330), _ui("REAKTOR VERLOREN"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 48, Color("ff8b78"))
|
||
draw_string(font, Vector2(0, 395), _ui("Erreichte Welle: %d") % wave, HORIZONTAL_ALIGNMENT_CENTER, 1600, 22, Color.WHITE)
|
||
draw_string(font, Vector2(0, 430), _ui("Highscore: %d • Effizienzbonus: ×%.2f") % [_calculate_run_score(), clampf(1.0 + (10.0 - towers_built_this_run) * 0.05, 0.50, 1.50)], HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, Color("d6eaf0"))
|
||
_draw_button(start_button, "NEUER VERSUCH", false, Color("6a2835"))
|
||
|
||
|
||
func _draw_victory() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.0, 0.035, 0.05, 0.82))
|
||
draw_string(font, Vector2(0, 310), _ui("SEKTOR GESICHERT"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 50, Color("8fffea"))
|
||
draw_string(font, Vector2(0, 375), _ui("Alle %d Angriffswellen wurden abgewehrt.") % MAX_WAVES, HORIZONTAL_ALIGNMENT_CENTER, 1600, 22, Color.WHITE)
|
||
draw_string(font, Vector2(0, 415), _ui("Der Orbitalkern bleibt unter unserer Kontrolle."), HORIZONTAL_ALIGNMENT_CENTER, 1600, 18, Color("8adff2"))
|
||
draw_string(font, Vector2(0, 458), _ui("+%d KERNKRISTALLE • GUTHABEN %d") % [last_mission_reward, core_crystals], HORIZONTAL_ALIGNMENT_CENTER, 1600, 18, Color("8fffea"))
|
||
draw_string(font, Vector2(0, 492), _ui("Highscore: %d • Effizienzbonus: ×%.2f") % [_calculate_run_score(), clampf(1.0 + (10.0 - towers_built_this_run) * 0.05, 0.50, 1.50)], HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, Color("d6eaf0"))
|
||
_draw_button(start_button, "ZUM HAUPTMENÜ", false, Color("185f68"))
|
||
|
||
|
||
func _draw_endless_offer() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.0, 0.025, 0.055, 0.88))
|
||
draw_string(font, Vector2(0, 280), _ui("SEKTOR GESICHERT"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 50, Color("8fffea"))
|
||
draw_string(font, Vector2(0, 345), _ui("Alle %d regulären Wellen wurden abgewehrt.") % MAX_WAVES, HORIZONTAL_ALIGNMENT_CENTER, 1600, 22, Color.WHITE)
|
||
draw_string(font, Vector2(0, 405), _ui("Möchtest du den Einsatz im immer schwereren Endlosmodus fortsetzen?"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 19, Color("8adff2"))
|
||
if victory_reward_pending:
|
||
draw_string(font, Vector2(0, 450), _ui("WERBUNG WIRD GELADEN • KERNKRISTALLE DANACH: %d") % int(_difficulty().crystals), HORIZONTAL_ALIGNMENT_CENTER, 1600, 16, Color("ffcf7a"))
|
||
else:
|
||
draw_string(font, Vector2(0, 450), _ui("+%d Kernkristalle gutgeschrieben. Ein späteres Ende gilt nicht als gescheitert.") % last_mission_reward, HORIZONTAL_ALIGNMENT_CENTER, 1600, 16, Color("8fffea"))
|
||
_draw_button(endless_button, "ENDLOSMODUS STARTEN", false, Color("215b68"))
|
||
_draw_button(finish_mission_button, "MISSION BEENDEN", false, Color("24394c"))
|
||
|
||
|
||
func _draw_endless_result() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.0, 0.02, 0.045, 0.84))
|
||
draw_string(font, Vector2(0, 315), _ui("ENDLOSAUFTRAG BEENDET"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 48, Color("8feeff"))
|
||
draw_string(font, Vector2(0, 382), _ui("Erreichte Welle: %d") % wave, HORIZONTAL_ALIGNMENT_CENTER, 1600, 23, Color.WHITE)
|
||
draw_string(font, Vector2(0, 425), _ui("Der reguläre Sektorauftrag bleibt erfolgreich abgeschlossen."), HORIZONTAL_ALIGNMENT_CENTER, 1600, 17, Color("8adff2"))
|
||
draw_string(font, Vector2(0, 462), _ui("KERNKRISTALLE %d") % core_crystals, HORIZONTAL_ALIGNMENT_CENTER, 1600, 17, Color("8fffea"))
|
||
draw_string(font, Vector2(0, 492), _ui("Highscore: %d • Effizienzbonus: ×%.2f") % [_calculate_run_score(), clampf(1.0 + (10.0 - towers_built_this_run) * 0.05, 0.50, 1.50)], HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, Color("d6eaf0"))
|
||
_draw_button(start_button, "NEUER EINSATZ", false, Color("18556f"))
|
||
|
||
|
||
func _draw_tutorial() -> void:
|
||
var font := ThemeDB.fallback_font
|
||
var title := "1/3 VERTEIDIGUNG WÄHLEN"
|
||
var body := "Wähle unten das KINETIK-Geschütz."
|
||
if tutorial_step == 1:
|
||
title = "2/3 TURM PLATZIEREN"
|
||
body = "Setze den Turm auf eine freie Metallfläche neben der Flugroute."
|
||
elif tutorial_step == 2:
|
||
title = "3/3 ANGRIFF STARTEN"
|
||
body = "Starte oben rechts die erste Welle."
|
||
draw_rect(Rect2(30, 110, 500, 132), Color(0.01, 0.035, 0.07, 0.95))
|
||
draw_rect(Rect2(30, 110, 500, 132), Color("55d8ee"), false, 2.0)
|
||
draw_string(font, Vector2(50, 147), _ui(title), HORIZONTAL_ALIGNMENT_LEFT, 450, 19, Color("8ff4ff"))
|
||
draw_string(font, Vector2(50, 184), _ui(body), HORIZONTAL_ALIGNMENT_LEFT, 445, 15, Color.WHITE)
|
||
_draw_button(tutorial_skip_button, "ÜBERSPRINGEN", false, Color("24394c"))
|
||
|
||
|
||
func _is_valid_build_position(position: Vector2, owner := 0) -> bool:
|
||
var correct_coop_side := true
|
||
if coop_mode:
|
||
correct_coop_side = position.x < COOP_MIRROR_X - GRID_SIZE * 0.5 if owner == 0 else position.x > COOP_MIRROR_X + GRID_SIZE * 0.5
|
||
return correct_coop_side and _is_in_build_zone(position) and not _is_in_reactor_zone(position) and not _is_path_cell(position) and position.x >= GRID_SIZE * 0.5 and position.x <= _active_world_width() - GRID_SIZE * 0.5 and position.y >= GRID_SIZE * 0.5 and position.y <= WORLD_SIZE.y - GRID_SIZE * 0.5 and _tower_at(position, GRID_SIZE * 0.72) < 0
|