Initial commit — Godot space roguelite source
- Touch controls: direct InputEventScreenTouch in shop_ui (bypass relay) - ItemDB: static preload list instead of DirAccess scan (export fix) - All 18 items with EN localisation (name_en, desc_en, category_en) - Ship playstyles: NOVA-1 shield, INFERNO ram, AURORA agile/tank - Quasar: SMBH visual, jet boost, merge, push, BH-eating - Atlas & UI text updated EN+DE Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"mcp__godot-mcp-pro__validate_script",
|
||||||
|
"mcp__godot-mcp-pro__get_editor_errors",
|
||||||
|
"mcp__godot-mcp-pro__reload_project",
|
||||||
|
"mcp__godot-mcp-pro__play_scene",
|
||||||
|
"mcp__godot-mcp-pro__create_script",
|
||||||
|
"mcp__godot-mcp-pro__get_project_info",
|
||||||
|
"mcp__godot-mcp-pro__get_scene_tree",
|
||||||
|
"mcp__godot-mcp-pro__read_script",
|
||||||
|
"mcp__godot-mcp-pro__get_game_screenshot",
|
||||||
|
"mcp__godot-mcp-pro__simulate_action",
|
||||||
|
"mcp__godot-mcp-pro__execute_game_script",
|
||||||
|
"mcp__godot-mcp-pro__stop_scene",
|
||||||
|
"mcp__godot-mcp-pro__get_filesystem_tree",
|
||||||
|
"mcp__godot-mcp-pro__get_scene_file_content",
|
||||||
|
"mcp__godot-mcp-pro__get_input_actions",
|
||||||
|
"mcp__godot-mcp-pro__execute_editor_script"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Normalize EOL for all files that Git considers text files.
|
||||||
|
* text=auto eol=lf
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Godot 4+ specific ignores
|
||||||
|
.godot/
|
||||||
|
/android/
|
||||||
|
addons/
|
||||||
|
build/
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"godot-mcp-pro": {
|
||||||
|
"command": "node",
|
||||||
|
"args": [
|
||||||
|
"C:/_gamedev/godot_mcp_server/build/index.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"godotTools.editorPath.godot4": "c:\\Program Files\\Godot\\Godot_v4.6-stable_win64.exe"
|
||||||
|
}
|
||||||
+431
@@ -0,0 +1,431 @@
|
|||||||
|
# spacel — Architektur-Dokumentation
|
||||||
|
|
||||||
|
## Überblick
|
||||||
|
|
||||||
|
**spacel** ist ein 2D-Space-Shooter mit Roguelite-Elementen, gebaut in **Godot 4.6** (Forward Plus, D3D12). Das Spiel unterstützt 1–2 Spieler (Split-Keyboard). Es gibt keine externen Assets — alles wird prozedural gezeichnet und synthetisiert (keine Sprites, keine Audio-Dateien).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
spacel/
|
||||||
|
├── project.godot # Projekt-Konfiguration, Autoloads, Input-Map
|
||||||
|
├── scenes/
|
||||||
|
│ ├── main.tscn # Root-Scene (enthält alles)
|
||||||
|
│ ├── game_world.tscn # Spielwelt / Hintergrund-Canvas
|
||||||
|
│ ├── hud.tscn # HUD (CanvasLayer)
|
||||||
|
│ └── ship_select.tscn # Schiff-Auswahlbildschirm
|
||||||
|
├── scripts/
|
||||||
|
│ ├── main.gd # State Machine — Spielfluss-Controller
|
||||||
|
│ ├── game_world.gd # Spielwelt: Simulation, Rendering, Kollisionen
|
||||||
|
│ ├── spaceship.gd # Spieler-Schiff (Datenklasse)
|
||||||
|
│ ├── enemy_ship.gd # Gegner-Schiff mit KI
|
||||||
|
│ ├── boss_ship.gd # Bosse: WRAITH (Welle 5) & LEVIATHAN (Welle 8)
|
||||||
|
│ ├── bullet.gd # Projektil
|
||||||
|
│ ├── black_hole.gd # Schwarzes Loch mit Gravitation + Supernova
|
||||||
|
│ ├── big_wipe.gd # Big-Wipe-Event (Bildschirm-Reset)
|
||||||
|
│ ├── cosmic_objects.gd # Alle Weltraum-Objekte (Star, Planet, ...)
|
||||||
|
│ ├── ship_stats.gd # Roguelite-Stat-System
|
||||||
|
│ ├── item_db.gd # Item-Datenbank (20 Items, 4 Seltenheiten)
|
||||||
|
│ ├── hud.gd # HUD-Logik
|
||||||
|
│ ├── hud_draw.gd # HUD-Rendering
|
||||||
|
│ ├── settings.gd # Einstellungen (Autoload)
|
||||||
|
│ ├── sound_manager.gd # Procedurales Audio (Autoload)
|
||||||
|
│ ├── music_player.gd # Musik
|
||||||
|
│ ├── shop_ui.gd # Shop zwischen den Wellen
|
||||||
|
│ ├── ship_select.gd # Schiff-Auswahl UI
|
||||||
|
│ ├── main_menu.gd # Hauptmenü
|
||||||
|
│ ├── pause_menu.gd # Pause-Menü
|
||||||
|
│ └── tr.gd # Lokalisierung (Autoload, de/en)
|
||||||
|
└── addons/godot_mcp/ # MCP-Plugin für KI-gestützte Entwicklung
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Autoloads (Singletons)
|
||||||
|
|
||||||
|
| Name | Script | Funktion |
|
||||||
|
|------|--------|----------|
|
||||||
|
| `Settings` | `settings.gd` | Lautstärke, Grafik, Sprache — persisted in `user://settings.cfg` |
|
||||||
|
| `SoundManager` | `sound_manager.gd` | Alle SFX prozedural generiert (Sinus/Säge/Square-Wellen, kein Audio-File) |
|
||||||
|
| `Tr` | `tr.gd` | Übersetzungen DE/EN |
|
||||||
|
| MCP-Dienste | `addons/godot_mcp/` | Nur für Entwicklung (Editor-Plugin) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## State Machine (`main.gd`)
|
||||||
|
|
||||||
|
`main.gd` ist der zentrale Controller. Er verwaltet alle UI-Panels und schaltet zwischen folgenden Zuständen um:
|
||||||
|
|
||||||
|
```
|
||||||
|
MAIN_MENU → SELECT → (SELECT_P2) → LAUNCHING → PLAYING ⟷ PAUSED
|
||||||
|
↓
|
||||||
|
RETURNING → SHOP → LAUNCHING (nächste Welle)
|
||||||
|
↓
|
||||||
|
GAMEOVER → MAIN_MENU
|
||||||
|
WAVE_CLEAR → SHOP
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wichtige Variablen in `main.gd`:**
|
||||||
|
- `lives_p1 / credits_p1 / stats_p1 / owned_items_p1` — Run-Zustand Spieler 1
|
||||||
|
- `wave_number` — aktuelle Wellennummer (steigt nach jedem Shop)
|
||||||
|
- `SHIPS[]` — 4 Schiffe (NOVA-1, INFERNO, AURORA, TITAN) mit Farbpaletten und Basis-Stats
|
||||||
|
|
||||||
|
**Schiff-Spielstile** (`_apply_ship_base_stats()` in `main.gd`):
|
||||||
|
| ID | Name | Besonderheit |
|
||||||
|
|----|------|--------------|
|
||||||
|
| `classic` | NOVA-1 | 1 Schutzschild — ausgewogener Einstieg |
|
||||||
|
| `inferno` | INFERNO | Speed +28 %, Feuerrate +55 %, Kurve −28 % · Passiv: Ramm-Schaden ab ≥ 4.5 px/s |
|
||||||
|
| `aurora` | AURORA | Kurve +55 %, Speed −20 %, 2 Schilde, BH-Resist 55 %, 2× Unverwundbarkeitszeit |
|
||||||
|
| `titan` | TITAN | Speed −28 %, Kurve −15 % · Aktiv: Boost-Impuls (SHIFT, 5 s Cooldown) |
|
||||||
|
|
||||||
|
`ship_id: String` in `ShipStats` wird beim Spielstart gesetzt und ermöglicht runtime-Verzweigungen (z. B. INFERNO-Rammen in `game_world.gd`).
|
||||||
|
|
||||||
|
**Hauptlogik:**
|
||||||
|
- `_set_state(new_state)` — zeigt/versteckt UI, initialisiert `game_world`
|
||||||
|
- `_process()` — Countdown vor Spielstart, Return-/Wave-Timer
|
||||||
|
- `add_credits(player, amount)` — Credits dem Spieler gutschreiben
|
||||||
|
- `on_game_over() / on_wave_complete()` — von `game_world` aufgerufen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Spielwelt (`game_world.gd`)
|
||||||
|
|
||||||
|
Das Herzstück des Spiels. `game_world.gd` ist ein `Node2D` und macht **alles selbst** per `_draw()` — keine Child-Nodes für Spielobjekte.
|
||||||
|
|
||||||
|
### Physik-Loop
|
||||||
|
|
||||||
|
Fixed-Timestep bei **60 Hz** (`PHYS_DT = 1/60`):
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
_process(delta):
|
||||||
|
_phys_accum += delta # akkumuliert reale Zeit
|
||||||
|
while _phys_accum >= PHYS_DT:
|
||||||
|
_phys_accum -= PHYS_DT
|
||||||
|
frame += 1
|
||||||
|
_tick(PHYS_DT)
|
||||||
|
queue_redraw()
|
||||||
|
```
|
||||||
|
|
||||||
|
### `_tick(dt)` — Reihenfolge pro Frame
|
||||||
|
|
||||||
|
1. `_handle_input(dt)` — Spieler-Input lesen, Schüsse erzeugen
|
||||||
|
2. `_update_objects(dt)` — Alle kosmischen Objekte updaten, Gegner KI
|
||||||
|
3. `_update_bullets()` — Projektile bewegen, tote entfernen
|
||||||
|
4. Boss-Update (falls vorhanden) → erzeugt Bullets
|
||||||
|
5. `_update_particles(dt)` — Partikeleffekte
|
||||||
|
6. `_check_collisions()` — Bullet/Enemy, Bullet/Player, Antimatter/Player, Bullets/Boss
|
||||||
|
7. `_check_big_wipe()` — Threshold prüfen (> 500 Objekte)
|
||||||
|
8. BigWipe-Update
|
||||||
|
9. Schwierigkeit + Wave-Timer + Credit-Trickle
|
||||||
|
10. All-Dead-Check → `main_node.on_game_over()`
|
||||||
|
11. Kometen- und Antimatter-Spawn-Timer
|
||||||
|
|
||||||
|
### Arrays der Spielobjekte
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
var stars, planets, nebulae, comets, galaxies: Array
|
||||||
|
var black_holes, quasars, white_holes, neutron_stars: Array
|
||||||
|
var antimatter, antimatter_stars: Array
|
||||||
|
var bullets, particles: Array
|
||||||
|
var players, enemies: Array
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rendering (`_draw()`)
|
||||||
|
|
||||||
|
Alles wird mit Godot's Canvas-API gezeichnet — **keine Sprites**:
|
||||||
|
- `draw_rect()` für Pixel/Rumpfe
|
||||||
|
- `draw_circle()` für Schwarze Löcher, White Holes
|
||||||
|
- `draw_arc()` für Akkretionsscheiben, Ringe
|
||||||
|
- `draw_line()` für Trails
|
||||||
|
|
||||||
|
**Zeichenreihenfolge:** Nebulae → Sterne → Galaxien → Planeten → Kometen → Quasare → White Holes → Neutronensterne → Schwarze Löcher → Antimatter → Bullets → Enemies → Boss → Spieler → Partikel → BigWipe-Overlay
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Datenklassen (alle `extends RefCounted`)
|
||||||
|
|
||||||
|
Alle Spielobjekte sind **keine Nodes** — sie sind reine Datenklassen, die von `game_world` in Arrays verwaltet werden. Das verhindert den Node-Overhead und ermöglicht den eigenen Physik-Loop.
|
||||||
|
|
||||||
|
### `Spaceship` (`spaceship.gd`)
|
||||||
|
|
||||||
|
Spieler-Schiff.
|
||||||
|
|
||||||
|
| Eigenschaft | Wert | Beschreibung |
|
||||||
|
|-------------|------|--------------|
|
||||||
|
| `THRUST` | 0.28 | Beschleunigung pro Frame |
|
||||||
|
| `TURN_SPEED` | 0.08 | Drehgeschwindigkeit (rad/frame) |
|
||||||
|
| `MAX_SPEED` | 7.5 | Maximale Geschwindigkeit |
|
||||||
|
| `DRAG` | 0.985 | Trägheit (Reibung) |
|
||||||
|
| `INVULN_FRAMES` | 90 | ~1.5s Unverwundbarkeit nach Treffer |
|
||||||
|
| `stats.ship_id` | String | Schiff-ID für runtime-spezifische Mechaniken |
|
||||||
|
|
||||||
|
**Methoden:**
|
||||||
|
- `update(thrust, turn, W, H, delta)` — Bewegung, Screen-Wrap, Trail
|
||||||
|
- `shoot_burst() → Array[Bullet]` — erzeugt 1–N Bullets je nach `stats.bullet_count`
|
||||||
|
- `draw(canvas, frame)` — malt Rumpf aus `HULL_PIXELS`-Tabelle mit Heading-Rotation
|
||||||
|
|
||||||
|
**Screen-Wrap:** Wenn Schiff den Bildschirmrand verlässt, erscheint es auf der anderen Seite (Toroidal).
|
||||||
|
|
||||||
|
### `EnemyShip` (`enemy_ship.gd`)
|
||||||
|
|
||||||
|
KI-Gegner. 2 Farbvarianten (idx 0 = Rot, idx 1 = Cyan). Jeder Gegner hat eine **Role** (AGGRO / CIRCLE / FLANK), die per `role_id % 3` beim Spawn bestimmt wird und über Respawns hinweg erhalten bleibt. `role_id` wird in `game_world.gd` als laufender Index vergeben.
|
||||||
|
|
||||||
|
**Rollen:**
|
||||||
|
- **AGGRO**: stürmt direkt auf den nächsten Spieler zu
|
||||||
|
- **CIRCLE**: hält Abstand (~180 px) und kreist kontinuierlich um den Spieler (orbit_offset dreht sich)
|
||||||
|
- **FLANK**: nähert sich aus ±90°-Flankenwinkel statt frontal
|
||||||
|
|
||||||
|
**Gemeinsamkeiten:**
|
||||||
|
- Schuss immer direkt auf Spieler gezielt (unabhängig von der Bewegungsrichtung) → CIRCLE/FLANK-Gegner bleiben gefährlich
|
||||||
|
- **Separation Force**: Gegner stoßen sich gegenseitig ab (Radius 110 px) → kein Clumping
|
||||||
|
- **Patrol**: außerhalb Reichweite navigieren Gegner zu zufälligen Zielpunkten auf dem Bildschirm (nicht mehr reines Heading-Drehen) → natürliche Verteilung
|
||||||
|
- **Schwarzloch-Ausweichen**: spürt BH-Radius und weicht ab
|
||||||
|
- Feuer-Timer ist pro `role_id` gestaffelt (FIRE_INTERVAL + rid×17) → Salven verteilt
|
||||||
|
- Nach Tod: respawnt nach 4–8s vom zufälligen Bildschirmrand, behält Role bei
|
||||||
|
|
||||||
|
**Wellenskalierung:**
|
||||||
|
- Alte Formel: `2 + (wave-1)/2` (cap 6→8 nach letztem Balancing-Pass)
|
||||||
|
- **Neue Formel:** `min(2 + (wave - 1), MAX_ENEMEYS)` → Welle 1: 2, Welle 5: 6, Welle 10: 11, Welle 20: 21
|
||||||
|
|
||||||
|
### `BossShip` (`boss_ship.gd`)
|
||||||
|
|
||||||
|
Zwei Bosse — gleiche Klasse, unterschiedliche Parameter:
|
||||||
|
|
||||||
|
| | WRAITH (Welle 5) | LEVIATHAN (Welle 8) |
|
||||||
|
|--|--|--|
|
||||||
|
| HP | 20 | 50 |
|
||||||
|
| Farbe | Magenta | Orange/Feuerrot |
|
||||||
|
| Orbit-Geschwindigkeit | 0.55 rad/s | 0.44 / 0.72 (Phase 2) |
|
||||||
|
| Feuer-Intervall | 72 Frames | 60 / 42 (Phase 2) |
|
||||||
|
| Schüsse | 3-Way | 5-Way / 8-Way |
|
||||||
|
| Pixel-Größe | 5 | 7 |
|
||||||
|
|
||||||
|
**Mechaniken:**
|
||||||
|
- Orbitiert elliptisch um Bildschirmmitte
|
||||||
|
- Heading zeigt immer zur Mitte (schießt nach innen)
|
||||||
|
- LEVIATHAN: Phase-2-Übergang bei ≤ 50% HP → Black Hole spawnt, Musik verdichtet
|
||||||
|
|
||||||
|
### `Bullet` (`bullet.gd`)
|
||||||
|
|
||||||
|
- Geschwindigkeit: 9.6 px/frame
|
||||||
|
- Lebensdauer: 240 Frames, Fade ab Frame 210
|
||||||
|
- Besitzer-Typen: `"p1"`, `"p2"`, `"enemy"`, `"boss"` (bestimmt Kollisions-Check und Farbe)
|
||||||
|
- `pierce`: true wenn `damage_mult >= 2.0` → trifft bis zu 2 Ziele
|
||||||
|
|
||||||
|
### `BlackHole` (`black_hole.gd`)
|
||||||
|
|
||||||
|
Komplexestes Objekt im Spiel.
|
||||||
|
|
||||||
|
**Parameter:**
|
||||||
|
- `PULL_RADIUS`: 160px Gravitationsfeld
|
||||||
|
- `SWALLOW_RADIUS`: 14px Verschluck-Radius
|
||||||
|
- `FORCE_MULT`: 45.0 (Gravitationsstärke)
|
||||||
|
- `SUPERNOVA_AT`: 30 verschluckte Objekte → Supernova
|
||||||
|
|
||||||
|
**Mechaniken:**
|
||||||
|
- Wandert langsam umher (Micro-Drift)
|
||||||
|
- 18% Chance: jagt den nächsten Spieler (Hunting-Mode)
|
||||||
|
- Wächst mit jedem verschluckten Objekt (radius, pull_radius, gravity)
|
||||||
|
- Bei 30 Verschluckungen: **Supernova** → stirbt, spawnt Quasar + neue BHs + White/Neutron Star + Sterne/Planeten
|
||||||
|
- **SMBH**: nach 12 verschluckten Galaxien → Super-Massive BH, nach 45s kollabiert er
|
||||||
|
- BH-BH-Verschmelzung: größerer frisst kleineren
|
||||||
|
|
||||||
|
### `BigWipe` (`big_wipe.gd`)
|
||||||
|
|
||||||
|
Notfall-Reset wenn > 500 kosmische Objekte vorhanden sind.
|
||||||
|
|
||||||
|
**Phasen:**
|
||||||
|
1. `COLLAPSE` (2.33s): Bildschirm verdunkelt sich, Spieler müssen Wipe-Taste halten
|
||||||
|
2. `FLASH` (0.4s): Weißer Flash
|
||||||
|
3. Alle Objekte außer Planeten/Sternen werden gelöscht
|
||||||
|
|
||||||
|
**Spieler-Reaktion:** Signal `p1_wipe` / `p2_wipe` drücken während COLLAPSE → überleben. Wer nicht drückt, stirbt. Belohnung: 25 Credits (+ Credit-Bonus).
|
||||||
|
|
||||||
|
### `CosmicObjects` (`cosmic_objects.gd`)
|
||||||
|
|
||||||
|
Statische äußere Klasse mit inneren Klassen:
|
||||||
|
|
||||||
|
| Klasse | Beschreibung |
|
||||||
|
|--------|-------------|
|
||||||
|
| `Star` | Wandert nach oben, kann gravitationell angezogen werden, spiralt ins BH |
|
||||||
|
| `Planet` | Kreist um Orbit-Punkt, kann von BH gefangen und zerrissen werden |
|
||||||
|
| `Nebula` | Rein dekorativ, bewegt sich langsam |
|
||||||
|
| `Comet` | Fliegt von Bildschirmrand zu Bildschirmrand |
|
||||||
|
| `Galaxy` | Spiral-Galaxie, kann von BH konsumiert werden → SMBH |
|
||||||
|
| `Quasar` | Entsteht aus Supernova, lebt 30s, rein dekorativ |
|
||||||
|
| `WhiteHole` | Gegenteil des BH: stößt Spieler ab, ejiziert Sterne/Planeten, lebt 60s |
|
||||||
|
| `NeutronStar` | Rotierender Pulsar-Strahl, stößt Objekte im Beam-Bereich ab |
|
||||||
|
| `Antimatter` | Partikel, tötet Spieler/Gegner bei Berührung, ziehen sich an |
|
||||||
|
| `AntimatterStar` | Entsteht wenn 5+ Antimatter-Partikel clustern, repulsiert Spieler, lebt 50s |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Roguelite-System
|
||||||
|
|
||||||
|
### `ShipStats` (`ship_stats.gd`)
|
||||||
|
|
||||||
|
Alle Stats sind **multiplikativ** (ausgenommen additive: `bullet_count`, `shield_charges`, `bh_resist`):
|
||||||
|
|
||||||
|
| Stat | Standard | Effekt |
|
||||||
|
|------|----------|--------|
|
||||||
|
| `speed_mult` | 1.0 | Schub und Max-Speed |
|
||||||
|
| `turn_mult` | 1.0 | Drehgeschwindigkeit |
|
||||||
|
| `fire_rate_mult` | 1.0 | Teilt Cooldown (höher = schneller) |
|
||||||
|
| `damage_mult` | 1.0 | Trefferzone; ≥ 2.0 → Pierce |
|
||||||
|
| `bullet_speed_mult` | 1.0 | Projektil-Geschwindigkeit |
|
||||||
|
| `bullet_count` | 1 | Projektile pro Schuss |
|
||||||
|
| `shield_charges` | 0 | Absorbiert N Treffer |
|
||||||
|
| `invuln_mult` | 1.0 | Unverwundbarkeitszeit nach Treffer |
|
||||||
|
| `bh_resist` | 0.0 | 0–0.9: Anteil des BH-Zugs der negiert wird |
|
||||||
|
| `wipe_mult` | 1.0 | BigWipe-Haltezeit-Faktor |
|
||||||
|
| `credit_bonus` | 1.0 | Multiplikator auf alle Credits |
|
||||||
|
|
||||||
|
### `ItemDB` (`item_db.gd`)
|
||||||
|
|
||||||
|
Zwei Pools:
|
||||||
|
- **Legacy `ITEMS`** (6 Einträge) — nur noch für Enemy-Boosts ab Welle 10 (`game_world.gd` rollt 1× daraus und wendet es auf jeden Gegner an).
|
||||||
|
- **Werkstatt-Plugin-Pool** (auto-discover unter `res://items/`, derzeit 17 ItemDefs) — der eigentliche Shop-Pool.
|
||||||
|
|
||||||
|
Seltenheiten:
|
||||||
|
|
||||||
|
| Seltenheit | Gewicht | Farbe |
|
||||||
|
|------------|---------|-------|
|
||||||
|
| STANDARD (Common) | 60 | Grau |
|
||||||
|
| SELTEN (Uncommon) | 25 | Grün |
|
||||||
|
| EPISCH (Rare) | 12 | Blau |
|
||||||
|
| LEGENDÄR (Epic) | 3 | Lila |
|
||||||
|
|
||||||
|
Shop zeigt nach jeder Welle 4 zufällige Items (gekaufte werden aus Folge-Rolls ausgeschlossen). Credits kommen aus:
|
||||||
|
- Kill: 15 Credits × `credit_bonus`
|
||||||
|
- Passiv: 5 Credits alle 10 Sekunden
|
||||||
|
- BigWipe überlebt: 25 Credits × `credit_bonus`
|
||||||
|
- Boss getötet: 150 (Miniboss) / 300 (Boss) Credits
|
||||||
|
|
||||||
|
### Werkstatt-Flow (`shop_ui.gd`)
|
||||||
|
|
||||||
|
Zwei Phasen pro Shop-Öffnung:
|
||||||
|
|
||||||
|
1. **Attribut-Phase** — nur in **ungeraden Wellen** (1, 3, 5, …). 3 zufällige Gratis-Buffs, einer wird gewählt. Werte `+8 % bis +18 %`. Gerade Wellen (2, 4, …) starten direkt in der Shop-Phase → dämpft frühes Snowballing.
|
||||||
|
2. **Shop-Phase** — 4 Karten aus dem Plugin-Pool. Kauf per ENTER, Reroll per R, SPACE/ESC zum Verlassen.
|
||||||
|
|
||||||
|
**Reroll-Kosten:** `60 + 42 × reroll_count` CR → `60 / 102 / 144 / 186 / 228 …`. Der `reroll_count` wird in `main.gd` (`reroll_count_p1` / `_p2`) gehalten und **wellenübergreifend** persistiert — verteuerter Reroll schützt den Rest des Runs vor billigem Ketten-Reroll. Reset nur bei neuem Run / Game Over.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Input-Mapping
|
||||||
|
|
||||||
|
| Aktion | Spieler 1 | Spieler 2 |
|
||||||
|
|--------|-----------|-----------|
|
||||||
|
| Schub | Pfeil Oben | W |
|
||||||
|
| Links | Pfeil Links | A |
|
||||||
|
| Rechts | Pfeil Rechts | D |
|
||||||
|
| Schießen | Leertaste | F |
|
||||||
|
| BigWipe | N | E |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wellen-Progression
|
||||||
|
|
||||||
|
| Welle | Dauer | Gegner | Boss |
|
||||||
|
|-------|-------|--------|------|
|
||||||
|
| 1 | 20s | 2 | — |
|
||||||
|
| 3 | 36s | 3 | — |
|
||||||
|
| 5 | 52s | 4 | WRAITH (Miniboss) |
|
||||||
|
| 7 | 68s | 5 | — |
|
||||||
|
| 8 | 76s | 5 | LEVIATHAN (Full Boss) |
|
||||||
|
| 10+ | ≤120s | 6 (mit Stats!) | — |
|
||||||
|
| 13+ | 120s | 8 (Cap) | — |
|
||||||
|
|
||||||
|
Formel: `enemy_count = min(2 + (wave-1)/2, 8)`. Enemy-Stats bleiben Welle 1–9 auf Basis; ab Welle 10 bekommt jeder Gegner zusätzlich 1 zufälliges Legacy-Item.
|
||||||
|
|
||||||
|
**Schwierigkeits-Ramp:** `difficulty = clamp(game_time / 300.0, 0.0, 1.0)` — steuert Kometen-Spawn-Rate, BH-Cap und mehr.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Balancing-Notizen
|
||||||
|
|
||||||
|
Die Power-Kurve ist bewusst **langsam**. Quellen wurden 2026-04-20 global gesenkt, um zu verhindern, dass der Spieler Welle 3 bereits dominiert.
|
||||||
|
|
||||||
|
### Plugin-Items (Kern-Multiplikatoren)
|
||||||
|
|
||||||
|
| Kategorie | Item | Kern-Effekt | Kosten |
|
||||||
|
|-----------|------|-------------|--------|
|
||||||
|
| Waffe | `wk_burst` (EPIC) | fire_rate ×1.80, damage ×0.45 | 160 |
|
||||||
|
| Waffe | `wk_laser` | fire_rate ×1.35, damage ×0.70 | 115 |
|
||||||
|
| Waffe | `wk_plasma` | damage ×1.55, proj_speed ×0.60 | 130 |
|
||||||
|
| Waffe | `wk_ion` | damage ×1.25, +1 Projektil, speed ×0.70 | 140 |
|
||||||
|
| Waffe | `wk_rail` | proj_speed ×1.50, fire_rate ×0.50 | 125 |
|
||||||
|
| Waffe | `wk_sniper` | proj_speed ×1.30, damage ×1.18, fire_rate ×0.55 | 115 |
|
||||||
|
| Waffe | `wk_shotgun` | +2 Projektile, proj_speed ×0.40, damage ×0.80 | 105 |
|
||||||
|
| Waffe | `wk_scatter` | +1 Projektil, damage ×0.75 | 115 |
|
||||||
|
| Antrieb | `drive_overdrive` | speed ×1.35, −1 Schild | 95 |
|
||||||
|
| Antrieb | `drive_quantum` | speed ×1.22, turn ×1.18, proj_speed ×0.80 | 135 |
|
||||||
|
| Antrieb | `drive_steer` | turn ×1.35, speed ×0.90 | 85 |
|
||||||
|
| Hülle | `hull_giant` (EPIC) | +2 Schild, invuln ×1.20, speed ×0.70, turn ×0.80 | 220 |
|
||||||
|
| Hülle | `hull_plating` | +1 Schild, speed ×0.85 | 120 |
|
||||||
|
| Hülle | `hull_reaktor` | invuln ×1.45, fire_rate ×0.85 | 115 |
|
||||||
|
| Hülle | `hull_nullfeld` | bh_resist +0.45, speed ×0.80 | 125 |
|
||||||
|
| Spezial | `special_credit_mag` | credit_bonus ×1.18 | 170 |
|
||||||
|
| Spezial | `special_wipe_core` | wipe_mult ×0.65 | 150 |
|
||||||
|
|
||||||
|
### Legacy-ITEMS (nur Enemy-Boosts Welle 10+)
|
||||||
|
|
||||||
|
| id | Effekt | Kosten |
|
||||||
|
|----|--------|--------|
|
||||||
|
| thrust_1 | speed ×1.12 | 50 |
|
||||||
|
| firerate_1 | fire_rate ×1.15 | 50 |
|
||||||
|
| damage_1 | damage ×1.15 | 50 |
|
||||||
|
| shield_1 | +1 Schild | 55 |
|
||||||
|
| firerate_2 | fire_rate ×1.22 | 90 |
|
||||||
|
| damage_2 | damage ×1.22, proj_speed ×1.10 | 100 |
|
||||||
|
|
||||||
|
### Attribute (Gratis, ungerade Wellen)
|
||||||
|
|
||||||
|
`ATTR_POOL` in `shop_ui.gd` — Werte: speed/turn/fire/damage/proj je **1.08–1.12**, invuln 1.18, credit_bonus 1.08, Schild +1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audio-System (`sound_manager.gd`)
|
||||||
|
|
||||||
|
Kein einziger Audio-File im Projekt. Alle Sounds werden mit `AudioStreamGenerator` pro Frame synthetisiert:
|
||||||
|
|
||||||
|
- `_play_tone(freq, duration, wave, vol_db, end_freq)` — Sinus/Säge/Square mit Frequenz-Sweep und Hüllkurve
|
||||||
|
- `_play_noise(duration, vol_db)` — Weißes Rauschen mit Decay
|
||||||
|
- `_play_chord_fanfare()` — Drei Töne mit kurzem Delay (BigWipe überlebt)
|
||||||
|
|
||||||
|
SFX-Typen: `player_shoot`, `enemy_shoot`, `enemy_die`, `player_die`, `antimatter_hit`, `bh_swallow`, `wipe_start`, `wipe_flash`, `wipe_survived`, `smbh_spawn`, `antimatter_swarm`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Grafik-System
|
||||||
|
|
||||||
|
**Viewport:** 960×600px, Stretch-Modus `canvas_items / expand`
|
||||||
|
**Hintergrundfarbe:** `#0a0a14` (Tiefschwarz-Blau)
|
||||||
|
**FPS-Cap:** 60
|
||||||
|
**Pixel-Filter:** Nearest-Neighbor (`default_texture_filter=0`)
|
||||||
|
|
||||||
|
Alle Schiffe benutzen `HULL_PIXELS`-Arrays — lokale Koordinaten-Offsets, die per `cos(heading)/sin(heading)` in Weltkoordinaten gedreht werden. Jeder "Pixel" ist ein 3×3-`draw_rect`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Signalfluss
|
||||||
|
|
||||||
|
```
|
||||||
|
game_world → main_node.on_game_over() # alle Spieler tot
|
||||||
|
game_world → main_node.on_wave_complete() # Wave-Timer abgelaufen
|
||||||
|
game_world → main_node.add_credits(player, n) # Credits vergeben
|
||||||
|
game_world → main_node.start_boss_music(final) # Boss spawnt
|
||||||
|
game_world → main_node.boss_phase_changed(2) # Leviathan Phase 2
|
||||||
|
big_wipe.wipe_complete → game_world._on_wipe_complete()
|
||||||
|
shop_ui.shop_closed → main._on_shop_closed()
|
||||||
|
pause_menu.resume_requested → main._on_pause_resume()
|
||||||
|
main_menu.mode_selected → main._on_mode_selected(multi)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MCP-Plugin (`addons/godot_mcp/`)
|
||||||
|
|
||||||
|
Das Godot-MCP-Pro-Plugin verbindet den Godot-Editor per WebSocket mit externen KI-Tools (Claude Code). Ermöglicht das Lesen/Schreiben von Szenen, Scripts und Eigenschaften direkt aus der KI-Konversation heraus. Nur für die Entwicklung relevant — kein Einfluss auf das Spiel. Referenz dazu in der CLaude.md
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# Godot MCP Pro - AI Assistant Instructions
|
||||||
|
|
||||||
|
You have access to the Godot MCP Pro toolset for building and testing Godot games through the editor. Follow these rules carefully.
|
||||||
|
|
||||||
|
## Critical: Editor vs Runtime Tools
|
||||||
|
|
||||||
|
Tools are split into two categories. **Using a runtime tool without starting the game will always fail.**
|
||||||
|
|
||||||
|
### Editor Tools (always available)
|
||||||
|
These work on the currently open scene in the Godot editor:
|
||||||
|
- **Scene**: `get_scene_tree`, `create_scene`, `open_scene`, `save_scene`, `delete_scene`, `add_scene_instance`, `get_scene_file_content`, `get_scene_exports`
|
||||||
|
- **Nodes**: `add_node`, `delete_node`, `duplicate_node`, `move_node`, `rename_node`, `update_property`, `get_node_properties`, `add_resource`, `set_anchor_preset`, `connect_signal`, `disconnect_signal`, `get_node_groups`, `set_node_groups`, `find_nodes_in_group`
|
||||||
|
- **Scripts**: `create_script`, `read_script`, `edit_script`, `validate_script`, `attach_script`, `get_open_scripts`, `list_scripts`
|
||||||
|
- **Project**: `get_project_info`, `get_project_settings`, `set_project_setting`, `get_project_statistics`, `get_filesystem_tree`, `get_input_actions`, `set_input_action`
|
||||||
|
- **Editor**: `execute_editor_script`, `get_editor_errors`, `get_output_log`, `get_editor_screenshot`, `clear_output`, `reload_plugin`, `reload_project`
|
||||||
|
- **Resources**: `create_resource`, `read_resource`, `edit_resource`, `get_resource_preview`
|
||||||
|
- **Batch**: `batch_add_nodes`, `batch_set_property`, `find_nodes_by_type`, `find_signal_connections`, `find_node_references`, `get_scene_dependencies`, `cross_scene_set_property`
|
||||||
|
- **3D**: `add_mesh_instance`, `setup_environment`, `setup_lighting`, `setup_camera_3d`, `setup_collision`, `setup_physics_body`, `set_material_3d`, `add_raycast`, `add_gridmap`
|
||||||
|
- **Animation**: `create_animation`, `add_animation_track`, `set_animation_keyframe`, `list_animations`, `get_animation_info`, `remove_animation`
|
||||||
|
- **Animation Tree**: `create_animation_tree`, `get_animation_tree_structure`, `add_state_machine_state`, `add_state_machine_transition`, `remove_state_machine_state`, `remove_state_machine_transition`, `set_blend_tree_node`, `set_tree_parameter`
|
||||||
|
- **Audio**: `add_audio_player`, `add_audio_bus`, `add_audio_bus_effect`, `set_audio_bus`, `get_audio_bus_layout`, `get_audio_info`
|
||||||
|
- **Navigation**: `setup_navigation_region`, `setup_navigation_agent`, `bake_navigation_mesh`, `set_navigation_layers`, `get_navigation_info`
|
||||||
|
- **Particles**: `create_particles`, `set_particle_material`, `set_particle_color_gradient`, `apply_particle_preset`, `get_particle_info`
|
||||||
|
- **Physics**: `get_physics_layers`, `set_physics_layers`, `get_collision_info`
|
||||||
|
- **Shader**: `create_shader`, `read_shader`, `edit_shader`, `assign_shader_material`, `get_shader_params`, `set_shader_param`
|
||||||
|
- **Theme**: `create_theme`, `get_theme_info`, `set_theme_color`, `set_theme_font_size`, `set_theme_constant`, `set_theme_stylebox`
|
||||||
|
- **Tilemap**: `tilemap_get_info`, `tilemap_set_cell`, `tilemap_get_cell`, `tilemap_fill_rect`, `tilemap_clear`, `tilemap_get_used_cells`
|
||||||
|
- **Export**: `list_export_presets`, `get_export_info`, `export_project`
|
||||||
|
- **Analysis**: `analyze_scene_complexity`, `analyze_signal_flow`, `detect_circular_dependencies`, `find_unused_resources`, `get_performance_monitors`, `search_files`, `search_in_files`, `find_script_references`
|
||||||
|
- **Profiling**: `get_editor_performance`
|
||||||
|
|
||||||
|
### Runtime Tools (require `play_scene` first)
|
||||||
|
You MUST call `play_scene` before using any of these. They interact with the running game:
|
||||||
|
- **Game State**: `get_game_scene_tree`, `get_game_node_properties`, `set_game_node_property`, `execute_game_script`, `get_game_screenshot`, `get_autoload`, `find_nodes_by_script`
|
||||||
|
- **Input Simulation**: `simulate_key`, `simulate_mouse_click`, `simulate_mouse_move`, `simulate_action`, `simulate_sequence`
|
||||||
|
- **Capture/Recording**: `capture_frames`, `record_frames`, `monitor_properties`, `start_recording`, `stop_recording`, `replay_recording`, `batch_get_properties`
|
||||||
|
- **UI Interaction**: `find_ui_elements`, `click_button_by_text`, `wait_for_node`, `find_nearby_nodes`, `navigate_to`, `move_to`
|
||||||
|
- **Testing**: `run_test_scenario`, `assert_node_state`, `assert_screen_text`, `run_stress_test`, `get_test_report`
|
||||||
|
- **Screenshots**: `get_game_screenshot`, `compare_screenshots`
|
||||||
|
- **Control**: `play_scene`, `stop_scene`
|
||||||
|
|
||||||
|
## Workflow Patterns
|
||||||
|
|
||||||
|
### Building a scene from scratch
|
||||||
|
1. `create_scene` or `open_scene`
|
||||||
|
2. Use `add_node` or `batch_add_nodes` to add nodes
|
||||||
|
3. `create_script` + `attach_script` for behavior
|
||||||
|
4. `save_scene`
|
||||||
|
|
||||||
|
### Testing gameplay
|
||||||
|
1. Build scene with editor tools (above)
|
||||||
|
2. `play_scene` to start the game
|
||||||
|
3. Use `simulate_key`/`simulate_mouse_click` for input
|
||||||
|
4. `get_game_screenshot` or `capture_frames` to observe results
|
||||||
|
5. `stop_scene` when done
|
||||||
|
|
||||||
|
### Inspecting a project
|
||||||
|
1. `get_project_info` for overview
|
||||||
|
2. `get_scene_tree` for current scene structure
|
||||||
|
3. `read_script` to read code
|
||||||
|
4. `get_node_properties` for specific node details
|
||||||
|
|
||||||
|
### Migrating code properties to inspector
|
||||||
|
When a script hardcodes visual properties (colors, sizes, positions, theme overrides) that should be in the inspector:
|
||||||
|
1. `read_script` to find hardcoded property assignments (e.g. `modulate = Color(...)`, `add_theme_color_override(...)`)
|
||||||
|
2. `get_node_properties` to see current inspector values
|
||||||
|
3. `update_property` to set the same values as node properties in the inspector
|
||||||
|
4. `edit_script` to remove the hardcoded lines from the script
|
||||||
|
5. `save_scene` to persist the inspector changes
|
||||||
|
6. `validate_script` to verify the script still works
|
||||||
|
|
||||||
|
## Formatting Rules
|
||||||
|
|
||||||
|
### execute_editor_script
|
||||||
|
The `code` parameter must be valid GDScript. Use `_mcp_print(value)` to return output.
|
||||||
|
|
||||||
|
```
|
||||||
|
# Correct
|
||||||
|
_mcp_print("hello")
|
||||||
|
|
||||||
|
# Correct - multi-line
|
||||||
|
var nodes = []
|
||||||
|
for child in EditorInterface.get_edited_scene_root().get_children():
|
||||||
|
nodes.append(child.name)
|
||||||
|
_mcp_print(str(nodes))
|
||||||
|
```
|
||||||
|
|
||||||
|
### execute_game_script
|
||||||
|
Same as above but runs inside the running game. Additional rules:
|
||||||
|
- No nested functions (`func` inside `func` is invalid GDScript)
|
||||||
|
- Use `.get("property")` instead of `.property` for safe access
|
||||||
|
- Runs in a temporary node — use `get_tree()` to access the scene tree
|
||||||
|
|
||||||
|
### batch_add_nodes
|
||||||
|
Pass an array of node definitions. Nodes are processed in order, so earlier nodes can be parents for later ones:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{"type": "Node2D", "name": "Container", "parent_path": "."},
|
||||||
|
{"type": "Sprite2D", "name": "Icon", "parent_path": "Container"},
|
||||||
|
{"type": "Label", "name": "Title", "parent_path": "Container", "properties": {"text": "Hello"}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Prefer inspector properties over code** — When changing visual properties (colors, sizes, theme overrides, transforms, etc.), use `update_property` to set them directly on the node. This keeps values visible in the Godot inspector and easy to tweak. Only use GDScript when the property isn't available in the inspector or needs to be dynamic at runtime.
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
1. **Never edit project.godot directly** — Use `set_project_setting` instead. The Godot editor overwrites the file.
|
||||||
|
2. **GDScript type inference** — Use explicit type annotations in for-loops: `for item: String in array` instead of `for item in array`.
|
||||||
|
3. **Reload after script changes** — After `create_script`, call `reload_project` if the script doesn't take effect.
|
||||||
|
4. **Property values as strings** — Properties like position accept string format: `"Vector2(100, 200)"`, `"Color(1, 0, 0, 1)"`.
|
||||||
|
5. **simulate_key duration** — Use short durations (0.3-0.5s) for precise movement. Integer seconds (1, 2, 3) cause overshooting.
|
||||||
|
6. **compare_screenshots** — Pass file paths (`user://screenshot.png`), not base64 data.
|
||||||
|
|
||||||
|
## CLI Mode (Alternative to MCP Tools)
|
||||||
|
|
||||||
|
If MCP tools are unavailable or you have a terminal/bash tool, you can control Godot via the CLI.
|
||||||
|
The CLI requires the server to be built first (`node build/setup.js install` in the server directory).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Discover available command groups
|
||||||
|
node /path/to/server/build/cli.js --help
|
||||||
|
|
||||||
|
# Discover commands in a group
|
||||||
|
node /path/to/server/build/cli.js scene --help
|
||||||
|
|
||||||
|
# Discover options for a specific command
|
||||||
|
node /path/to/server/build/cli.js node add --help
|
||||||
|
|
||||||
|
# Execute commands
|
||||||
|
node /path/to/server/build/cli.js project info
|
||||||
|
node /path/to/server/build/cli.js scene tree
|
||||||
|
node /path/to/server/build/cli.js node add --type CharacterBody3D --name Player --parent /root/Main
|
||||||
|
node /path/to/server/build/cli.js script read --path res://player.gd
|
||||||
|
node /path/to/server/build/cli.js scene play
|
||||||
|
node /path/to/server/build/cli.js input key --key W --duration 0.5
|
||||||
|
node /path/to/server/build/cli.js runtime tree
|
||||||
|
```
|
||||||
|
|
||||||
|
**Command groups**: project, scene, node, script, editor, input, runtime
|
||||||
|
|
||||||
|
Always start by running `--help` to discover available commands. Use the CLI when MCP tools are not loaded or when you need to reduce context usage.
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
[preset.0]
|
||||||
|
|
||||||
|
name="Windows Desktop"
|
||||||
|
platform="Windows Desktop"
|
||||||
|
runnable=true
|
||||||
|
dedicated_server=false
|
||||||
|
custom_features=""
|
||||||
|
export_filter="all_resources"
|
||||||
|
include_filter=""
|
||||||
|
exclude_filter=""
|
||||||
|
export_path="build/build9/spacel.exe"
|
||||||
|
patches=PackedStringArray()
|
||||||
|
patch_delta_encoding=false
|
||||||
|
patch_delta_compression_level_zstd=19
|
||||||
|
patch_delta_min_reduction=0.1
|
||||||
|
patch_delta_include_filters="*"
|
||||||
|
patch_delta_exclude_filters=""
|
||||||
|
encryption_include_filters=""
|
||||||
|
encryption_exclude_filters=""
|
||||||
|
seed=0
|
||||||
|
encrypt_pck=false
|
||||||
|
encrypt_directory=false
|
||||||
|
script_export_mode=2
|
||||||
|
|
||||||
|
[preset.0.options]
|
||||||
|
|
||||||
|
custom_template/debug=""
|
||||||
|
custom_template/release=""
|
||||||
|
debug/export_console_wrapper=1
|
||||||
|
binary_format/embed_pck=true
|
||||||
|
texture_format/s3tc_bptc=true
|
||||||
|
texture_format/etc2_astc=false
|
||||||
|
shader_baker/enabled=false
|
||||||
|
binary_format/architecture="x86_64"
|
||||||
|
codesign/enable=false
|
||||||
|
codesign/timestamp=true
|
||||||
|
codesign/timestamp_server_url=""
|
||||||
|
codesign/digest_algorithm=1
|
||||||
|
codesign/description=""
|
||||||
|
codesign/custom_options=PackedStringArray()
|
||||||
|
application/modify_resources=true
|
||||||
|
application/icon=""
|
||||||
|
application/console_wrapper_icon=""
|
||||||
|
application/icon_interpolation=4
|
||||||
|
application/file_version=""
|
||||||
|
application/product_version=""
|
||||||
|
application/company_name=""
|
||||||
|
application/product_name=""
|
||||||
|
application/file_description=""
|
||||||
|
application/copyright="Eric Reischel (alpcaman)"
|
||||||
|
application/trademarks=""
|
||||||
|
application/export_angle=0
|
||||||
|
application/export_d3d12=0
|
||||||
|
application/d3d12_agility_sdk_multiarch=true
|
||||||
|
ssh_remote_deploy/enabled=false
|
||||||
|
ssh_remote_deploy/host="user@host_ip"
|
||||||
|
ssh_remote_deploy/port="22"
|
||||||
|
ssh_remote_deploy/extra_args_ssh=""
|
||||||
|
ssh_remote_deploy/extra_args_scp=""
|
||||||
|
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
|
||||||
|
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
|
||||||
|
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
|
||||||
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||||
|
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
|
||||||
|
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
|
||||||
|
Start-ScheduledTask -TaskName godot_remote_debug
|
||||||
|
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
|
||||||
|
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
|
||||||
|
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
|
||||||
|
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
|
||||||
|
Remove-Item -Recurse -Force '{temp_dir}'"
|
||||||
|
|
||||||
|
[preset.1]
|
||||||
|
|
||||||
|
name="Linux"
|
||||||
|
platform="Linux"
|
||||||
|
runnable=true
|
||||||
|
dedicated_server=false
|
||||||
|
custom_features=""
|
||||||
|
export_filter="all_resources"
|
||||||
|
include_filter=""
|
||||||
|
exclude_filter=""
|
||||||
|
export_path="build/build9/spacelbuild8.x86_64"
|
||||||
|
patches=PackedStringArray()
|
||||||
|
patch_delta_encoding=false
|
||||||
|
patch_delta_compression_level_zstd=19
|
||||||
|
patch_delta_min_reduction=0.1
|
||||||
|
patch_delta_include_filters="*"
|
||||||
|
patch_delta_exclude_filters=""
|
||||||
|
encryption_include_filters=""
|
||||||
|
encryption_exclude_filters=""
|
||||||
|
seed=0
|
||||||
|
encrypt_pck=false
|
||||||
|
encrypt_directory=false
|
||||||
|
script_export_mode=2
|
||||||
|
|
||||||
|
[preset.1.options]
|
||||||
|
|
||||||
|
custom_template/debug=""
|
||||||
|
custom_template/release=""
|
||||||
|
debug/export_console_wrapper=1
|
||||||
|
binary_format/embed_pck=true
|
||||||
|
texture_format/s3tc_bptc=true
|
||||||
|
texture_format/etc2_astc=false
|
||||||
|
shader_baker/enabled=false
|
||||||
|
binary_format/architecture="x86_64"
|
||||||
|
ssh_remote_deploy/enabled=false
|
||||||
|
ssh_remote_deploy/host="user@host_ip"
|
||||||
|
ssh_remote_deploy/port="22"
|
||||||
|
ssh_remote_deploy/extra_args_ssh=""
|
||||||
|
ssh_remote_deploy/extra_args_scp=""
|
||||||
|
ssh_remote_deploy/run_script="#!/usr/bin/env bash
|
||||||
|
export DISPLAY=:0
|
||||||
|
unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"
|
||||||
|
\"{temp_dir}/{exe_name}\" {cmd_args}"
|
||||||
|
ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash
|
||||||
|
pkill -x -f \"{temp_dir}/{exe_name} {cmd_args}\"
|
||||||
|
rm -rf \"{temp_dir}\""
|
||||||
|
|
||||||
|
[preset.2]
|
||||||
|
|
||||||
|
name="Android"
|
||||||
|
platform="Android"
|
||||||
|
runnable=true
|
||||||
|
dedicated_server=false
|
||||||
|
custom_features=""
|
||||||
|
export_filter="all_resources"
|
||||||
|
include_filter=""
|
||||||
|
exclude_filter=""
|
||||||
|
export_path="build/build9/spacel.apk"
|
||||||
|
patches=PackedStringArray()
|
||||||
|
patch_delta_encoding=false
|
||||||
|
patch_delta_compression_level_zstd=19
|
||||||
|
patch_delta_min_reduction=0.1
|
||||||
|
patch_delta_include_filters="*"
|
||||||
|
patch_delta_exclude_filters=""
|
||||||
|
encryption_include_filters=""
|
||||||
|
encryption_exclude_filters=""
|
||||||
|
seed=0
|
||||||
|
encrypt_pck=false
|
||||||
|
encrypt_directory=false
|
||||||
|
script_export_mode=2
|
||||||
|
|
||||||
|
[preset.2.options]
|
||||||
|
|
||||||
|
custom_template/debug=""
|
||||||
|
custom_template/release=""
|
||||||
|
gradle_build/use_gradle_build=false
|
||||||
|
gradle_build/gradle_build_directory=""
|
||||||
|
gradle_build/android_source_template=""
|
||||||
|
gradle_build/compress_native_libraries=false
|
||||||
|
gradle_build/export_format=0
|
||||||
|
gradle_build/min_sdk=""
|
||||||
|
gradle_build/target_sdk=""
|
||||||
|
gradle_build/custom_theme_attributes={}
|
||||||
|
architectures/armeabi-v7a=false
|
||||||
|
architectures/arm64-v8a=true
|
||||||
|
architectures/x86=false
|
||||||
|
architectures/x86_64=false
|
||||||
|
version/code=1
|
||||||
|
version/name=""
|
||||||
|
package/unique_name="com.alpacaman.$genname"
|
||||||
|
package/name="spacel"
|
||||||
|
package/signed=true
|
||||||
|
package/app_category=2
|
||||||
|
package/retain_data_on_uninstall=false
|
||||||
|
package/exclude_from_recents=false
|
||||||
|
package/show_in_android_tv=false
|
||||||
|
package/show_in_app_library=true
|
||||||
|
package/show_as_launcher_app=false
|
||||||
|
launcher_icons/main_192x192=""
|
||||||
|
launcher_icons/adaptive_foreground_432x432=""
|
||||||
|
launcher_icons/adaptive_background_432x432=""
|
||||||
|
launcher_icons/adaptive_monochrome_432x432=""
|
||||||
|
graphics/opengl_debug=false
|
||||||
|
shader_baker/enabled=false
|
||||||
|
xr_features/xr_mode=0
|
||||||
|
gesture/swipe_to_dismiss=false
|
||||||
|
screen/immersive_mode=true
|
||||||
|
screen/edge_to_edge=false
|
||||||
|
screen/support_small=true
|
||||||
|
screen/support_normal=true
|
||||||
|
screen/support_large=true
|
||||||
|
screen/support_xlarge=true
|
||||||
|
screen/background_color=Color(0, 0, 0, 1)
|
||||||
|
user_data_backup/allow=false
|
||||||
|
command_line/extra_args=""
|
||||||
|
apk_expansion/enable=false
|
||||||
|
apk_expansion/SALT=""
|
||||||
|
apk_expansion/public_key=""
|
||||||
|
permissions/custom_permissions=PackedStringArray()
|
||||||
|
permissions/access_checkin_properties=false
|
||||||
|
permissions/access_coarse_location=false
|
||||||
|
permissions/access_fine_location=false
|
||||||
|
permissions/access_location_extra_commands=false
|
||||||
|
permissions/access_media_location=false
|
||||||
|
permissions/access_mock_location=false
|
||||||
|
permissions/access_network_state=false
|
||||||
|
permissions/access_surface_flinger=false
|
||||||
|
permissions/access_wifi_state=false
|
||||||
|
permissions/account_manager=false
|
||||||
|
permissions/add_voicemail=false
|
||||||
|
permissions/authenticate_accounts=false
|
||||||
|
permissions/battery_stats=false
|
||||||
|
permissions/bind_accessibility_service=false
|
||||||
|
permissions/bind_appwidget=false
|
||||||
|
permissions/bind_device_admin=false
|
||||||
|
permissions/bind_input_method=false
|
||||||
|
permissions/bind_nfc_service=false
|
||||||
|
permissions/bind_notification_listener_service=false
|
||||||
|
permissions/bind_print_service=false
|
||||||
|
permissions/bind_remoteviews=false
|
||||||
|
permissions/bind_text_service=false
|
||||||
|
permissions/bind_vpn_service=false
|
||||||
|
permissions/bind_wallpaper=false
|
||||||
|
permissions/bluetooth=false
|
||||||
|
permissions/bluetooth_admin=false
|
||||||
|
permissions/bluetooth_privileged=false
|
||||||
|
permissions/brick=false
|
||||||
|
permissions/broadcast_package_removed=false
|
||||||
|
permissions/broadcast_sms=false
|
||||||
|
permissions/broadcast_sticky=false
|
||||||
|
permissions/broadcast_wap_push=false
|
||||||
|
permissions/call_phone=false
|
||||||
|
permissions/call_privileged=false
|
||||||
|
permissions/camera=false
|
||||||
|
permissions/capture_audio_output=false
|
||||||
|
permissions/capture_secure_video_output=false
|
||||||
|
permissions/capture_video_output=false
|
||||||
|
permissions/change_component_enabled_state=false
|
||||||
|
permissions/change_configuration=false
|
||||||
|
permissions/change_network_state=false
|
||||||
|
permissions/change_wifi_multicast_state=false
|
||||||
|
permissions/change_wifi_state=false
|
||||||
|
permissions/clear_app_cache=false
|
||||||
|
permissions/clear_app_user_data=false
|
||||||
|
permissions/control_location_updates=true
|
||||||
|
permissions/delete_cache_files=false
|
||||||
|
permissions/delete_packages=false
|
||||||
|
permissions/device_power=false
|
||||||
|
permissions/diagnostic=false
|
||||||
|
permissions/disable_keyguard=false
|
||||||
|
permissions/dump=false
|
||||||
|
permissions/expand_status_bar=false
|
||||||
|
permissions/factory_test=false
|
||||||
|
permissions/flashlight=false
|
||||||
|
permissions/force_back=false
|
||||||
|
permissions/get_accounts=false
|
||||||
|
permissions/get_package_size=false
|
||||||
|
permissions/get_tasks=false
|
||||||
|
permissions/get_top_activity_info=false
|
||||||
|
permissions/global_search=false
|
||||||
|
permissions/hardware_test=false
|
||||||
|
permissions/inject_events=false
|
||||||
|
permissions/install_location_provider=false
|
||||||
|
permissions/install_packages=false
|
||||||
|
permissions/install_shortcut=false
|
||||||
|
permissions/internal_system_window=false
|
||||||
|
permissions/internet=false
|
||||||
|
permissions/kill_background_processes=false
|
||||||
|
permissions/location_hardware=false
|
||||||
|
permissions/manage_accounts=false
|
||||||
|
permissions/manage_app_tokens=false
|
||||||
|
permissions/manage_documents=false
|
||||||
|
permissions/manage_external_storage=false
|
||||||
|
permissions/manage_media=false
|
||||||
|
permissions/master_clear=false
|
||||||
|
permissions/media_content_control=false
|
||||||
|
permissions/modify_audio_settings=false
|
||||||
|
permissions/modify_phone_state=false
|
||||||
|
permissions/mount_format_filesystems=false
|
||||||
|
permissions/mount_unmount_filesystems=false
|
||||||
|
permissions/nfc=false
|
||||||
|
permissions/persistent_activity=false
|
||||||
|
permissions/post_notifications=false
|
||||||
|
permissions/process_outgoing_calls=false
|
||||||
|
permissions/read_calendar=false
|
||||||
|
permissions/read_call_log=false
|
||||||
|
permissions/read_contacts=false
|
||||||
|
permissions/read_external_storage=false
|
||||||
|
permissions/read_frame_buffer=false
|
||||||
|
permissions/read_history_bookmarks=false
|
||||||
|
permissions/read_input_state=false
|
||||||
|
permissions/read_logs=false
|
||||||
|
permissions/read_media_audio=false
|
||||||
|
permissions/read_media_images=false
|
||||||
|
permissions/read_media_video=false
|
||||||
|
permissions/read_media_visual_user_selected=false
|
||||||
|
permissions/read_phone_state=false
|
||||||
|
permissions/read_profile=false
|
||||||
|
permissions/read_sms=false
|
||||||
|
permissions/read_social_stream=false
|
||||||
|
permissions/read_sync_settings=false
|
||||||
|
permissions/read_sync_stats=false
|
||||||
|
permissions/read_user_dictionary=false
|
||||||
|
permissions/reboot=false
|
||||||
|
permissions/receive_boot_completed=false
|
||||||
|
permissions/receive_mms=false
|
||||||
|
permissions/receive_sms=false
|
||||||
|
permissions/receive_wap_push=false
|
||||||
|
permissions/record_audio=false
|
||||||
|
permissions/reorder_tasks=false
|
||||||
|
permissions/restart_packages=false
|
||||||
|
permissions/send_respond_via_message=false
|
||||||
|
permissions/send_sms=false
|
||||||
|
permissions/set_activity_watcher=false
|
||||||
|
permissions/set_alarm=false
|
||||||
|
permissions/set_always_finish=false
|
||||||
|
permissions/set_animation_scale=false
|
||||||
|
permissions/set_debug_app=false
|
||||||
|
permissions/set_orientation=false
|
||||||
|
permissions/set_pointer_speed=false
|
||||||
|
permissions/set_preferred_applications=false
|
||||||
|
permissions/set_process_limit=false
|
||||||
|
permissions/set_time=false
|
||||||
|
permissions/set_time_zone=false
|
||||||
|
permissions/set_wallpaper=false
|
||||||
|
permissions/set_wallpaper_hints=false
|
||||||
|
permissions/signal_persistent_processes=false
|
||||||
|
permissions/status_bar=false
|
||||||
|
permissions/subscribed_feeds_read=false
|
||||||
|
permissions/subscribed_feeds_write=false
|
||||||
|
permissions/system_alert_window=false
|
||||||
|
permissions/transmit_ir=false
|
||||||
|
permissions/uninstall_shortcut=false
|
||||||
|
permissions/update_device_stats=false
|
||||||
|
permissions/use_credentials=false
|
||||||
|
permissions/use_sip=false
|
||||||
|
permissions/vibrate=false
|
||||||
|
permissions/wake_lock=false
|
||||||
|
permissions/write_apn_settings=false
|
||||||
|
permissions/write_calendar=false
|
||||||
|
permissions/write_call_log=false
|
||||||
|
permissions/write_contacts=false
|
||||||
|
permissions/write_external_storage=false
|
||||||
|
permissions/write_gservices=false
|
||||||
|
permissions/write_history_bookmarks=false
|
||||||
|
permissions/write_profile=false
|
||||||
|
permissions/write_secure_settings=false
|
||||||
|
permissions/write_settings=false
|
||||||
|
permissions/write_sms=false
|
||||||
|
permissions/write_social_stream=false
|
||||||
|
permissions/write_sync_settings=false
|
||||||
|
permissions/write_user_dictionary=false
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128"><rect width="124" height="124" x="2" y="2" fill="#363d52" stroke="#212532" stroke-width="4" rx="14"/><g fill="#fff" transform="translate(12.322 12.322)scale(.101)"><path d="M105 673v33q407 354 814 0v-33z"/><path fill="#478cbf" d="m105 673 152 14q12 1 15 14l4 67 132 10 8-61q2-11 15-15h162q13 4 15 15l8 61 132-10 4-67q3-13 15-14l152-14V427q30-39 56-81-35-59-83-108-43 20-82 47-40-37-88-64 7-51 8-102-59-28-123-42-26 43-46 89-49-7-98 0-20-46-46-89-64 14-123 42 1 51 8 102-48 27-88 64-39-27-82-47-48 49-83 108 26 42 56 81zm0 33v39c0 276 813 276 814 0v-39l-134 12-5 69q-2 10-14 13l-162 11q-12 0-16-11l-10-65H446l-10 65q-4 11-16 11l-162-11q-12-3-14-13l-5-69z"/><path d="M483 600c0 34 58 34 58 0v-86c0-34-58-34-58 0z"/><circle cx="725" cy="526" r="90"/><circle cx="299" cy="526" r="90"/></g><g fill="#414042" transform="translate(12.322 12.322)scale(.101)"><circle cx="307" cy="532" r="60"/><circle cx="717" cy="532" r="60"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 995 B |
@@ -0,0 +1,43 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cttlglpbaw6jc"
|
||||||
|
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://icon.svg"
|
||||||
|
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
|
svg/scale=1.0
|
||||||
|
editor/scale_with_editor_scale=false
|
||||||
|
editor/convert_colors_with_editor_theme=false
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "drive_overdrive"
|
||||||
|
name = "Überantrieb"
|
||||||
|
name_en = "Overdrive"
|
||||||
|
desc = "Massiver Speed-Schub — fragilere Hülle"
|
||||||
|
desc_en = "Massive speed boost — fragile hull"
|
||||||
|
category = "ANTRIEBSMOD"
|
||||||
|
category_en = "DRIVE MOD"
|
||||||
|
icon = "▲"
|
||||||
|
cost = 95
|
||||||
|
rarity = 1
|
||||||
|
effects = { "speed_mult": 1.35, "shield_charges": -1 }
|
||||||
|
visual_pixels = [
|
||||||
|
[-5, 0, "accent"],
|
||||||
|
[-6, 0, "bright"],
|
||||||
|
[-5, -1, "mid"],
|
||||||
|
[-5, 1, "mid"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dft6ejbi8x82t
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "drive_quantum"
|
||||||
|
name = "Quantenantrieb"
|
||||||
|
name_en = "Quantum Drive"
|
||||||
|
desc = "Speed + Wendekraft — fragile Projektile"
|
||||||
|
desc_en = "Speed + agility — fragile projectiles"
|
||||||
|
category = "ANTRIEBSMOD"
|
||||||
|
category_en = "DRIVE MOD"
|
||||||
|
icon = "◈"
|
||||||
|
cost = 135
|
||||||
|
rarity = 2
|
||||||
|
effects = { "speed_mult": 1.22, "turn_mult": 1.18, "bullet_speed_mult": 0.80 }
|
||||||
|
visual_pixels = [
|
||||||
|
[-5, -2, "accent"],
|
||||||
|
[-5, 2, "accent"],
|
||||||
|
[-6, -3, "mid"],
|
||||||
|
[-6, 3, "mid"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cey4o4kxhhdmn
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "drive_steer"
|
||||||
|
name = "Steuerdüsen"
|
||||||
|
name_en = "Steering Jets"
|
||||||
|
desc = "Massiv wendiger — niedrigerer Topspeed"
|
||||||
|
desc_en = "Massively more agile — lower top speed"
|
||||||
|
category = "ANTRIEBSMOD"
|
||||||
|
category_en = "DRIVE MOD"
|
||||||
|
icon = "↺"
|
||||||
|
cost = 85
|
||||||
|
rarity = 0
|
||||||
|
effects = { "turn_mult": 1.35, "speed_mult": 0.90 }
|
||||||
|
visual_pixels = [
|
||||||
|
[-3, -3, "bright"],
|
||||||
|
[-3, 3, "bright"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b1nqolbilm25g
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "hull_giant"
|
||||||
|
name = "Kolossal-Hülle"
|
||||||
|
name_en = "Colossal Hull"
|
||||||
|
desc = "Massiv verstärktes Schiff — stark vergrößert, sehr träge"
|
||||||
|
desc_en = "Massively reinforced ship — much larger, very sluggish"
|
||||||
|
category = "HÜLLENMOD"
|
||||||
|
category_en = "HULL MOD"
|
||||||
|
icon = "◼"
|
||||||
|
cost = 220
|
||||||
|
rarity = 3
|
||||||
|
effects = { "shield_charges": 2, "invuln_mult": 1.20, "speed_mult": 0.70, "turn_mult": 0.80 }
|
||||||
|
visual_pixels = [
|
||||||
|
[-5, -6, "shadow"],
|
||||||
|
[-5, 6, "shadow"],
|
||||||
|
[ 2, -6, "dim"],
|
||||||
|
[ 2, 6, "dim"],
|
||||||
|
[-5, -5, "mid"],
|
||||||
|
[-5, 5, "mid"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.8
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cne4dtcoi12x4
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "hull_nullfeld"
|
||||||
|
name = "Nullfeld-Hülle"
|
||||||
|
name_en = "Null-Field Hull"
|
||||||
|
desc = "Schwerkraft-Immunität — langsamer"
|
||||||
|
desc_en = "Gravity immunity — slower"
|
||||||
|
category = "HÜLLENMOD"
|
||||||
|
category_en = "HULL MOD"
|
||||||
|
icon = "⬡"
|
||||||
|
cost = 125
|
||||||
|
rarity = 2
|
||||||
|
effects = { "bh_resist": 0.45, "speed_mult": 0.80 }
|
||||||
|
visual_pixels = [
|
||||||
|
[-1, -4, "accent"],
|
||||||
|
[-1, 4, "accent"],
|
||||||
|
[-3, -3, "dim"],
|
||||||
|
[-3, 3, "dim"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://xnlr0664qydt
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "hull_plating"
|
||||||
|
name = "Panzerplatten"
|
||||||
|
name_en = "Armor Plating"
|
||||||
|
desc = "Schweres Titan-Gehäuse — breiter aber träger"
|
||||||
|
desc_en = "Heavy titanium shell — wider but slower"
|
||||||
|
category = "HÜLLENMOD"
|
||||||
|
category_en = "HULL MOD"
|
||||||
|
icon = "◎"
|
||||||
|
cost = 120
|
||||||
|
rarity = 1
|
||||||
|
effects = { "shield_charges": 1, "speed_mult": 0.85 }
|
||||||
|
visual_pixels = [
|
||||||
|
[-3, -5, "dim"],
|
||||||
|
[-3, 5, "dim"],
|
||||||
|
[-4, -5, "shadow"],
|
||||||
|
[-4, 5, "shadow"],
|
||||||
|
[-2, -5, "mid"],
|
||||||
|
[-2, 5, "mid"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.4
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://gwidt7dkenor
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "hull_reaktor"
|
||||||
|
name = "Reaktorschild"
|
||||||
|
name_en = "Reactor Shield"
|
||||||
|
desc = "Verlängerte Unverwundbarkeit — geringere Feuerrate"
|
||||||
|
desc_en = "Extended invulnerability — reduced fire rate"
|
||||||
|
category = "HÜLLENMOD"
|
||||||
|
category_en = "HULL MOD"
|
||||||
|
icon = "⬢"
|
||||||
|
cost = 115
|
||||||
|
rarity = 1
|
||||||
|
effects = { "invuln_mult": 1.45, "fire_rate_mult": 0.85 }
|
||||||
|
visual_pixels = [
|
||||||
|
[-2, -3, "accent"],
|
||||||
|
[-2, 3, "accent"],
|
||||||
|
[-1, 0, "bright"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bvaoatus42p8j
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "special_credit_mag"
|
||||||
|
name = "Kreditmagnet"
|
||||||
|
name_en = "Credit Magnet"
|
||||||
|
desc = "+18% Kreditgewinn — keine Nachteile"
|
||||||
|
desc_en = "+18% credit gain — no downsides"
|
||||||
|
category = "SPEZIAL"
|
||||||
|
category_en = "SPECIAL"
|
||||||
|
icon = "¢"
|
||||||
|
cost = 170
|
||||||
|
rarity = 2
|
||||||
|
effects = { "credit_bonus": 1.18 }
|
||||||
|
visual_pixels = [
|
||||||
|
[0, -6, "accent"],
|
||||||
|
[0, 6, "accent"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c0g7a21c4snoj
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "special_wipe_core"
|
||||||
|
name = "Warp-Kern"
|
||||||
|
name_en = "Warp Core"
|
||||||
|
desc = "Big Wipe lädt 35% schneller"
|
||||||
|
desc_en = "Big Wipe charges 35% faster"
|
||||||
|
category = "SPEZIAL"
|
||||||
|
category_en = "SPECIAL"
|
||||||
|
icon = "✺"
|
||||||
|
cost = 150
|
||||||
|
rarity = 2
|
||||||
|
effects = { "wipe_mult": 0.65 }
|
||||||
|
visual_pixels = [
|
||||||
|
[-2, 0, "accent"],
|
||||||
|
[-3, -1, "bright"],
|
||||||
|
[-3, 1, "bright"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dgfs6a1mixcxi
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "wk_burst"
|
||||||
|
name = "Burst-Kern"
|
||||||
|
name_en = "Burst Core"
|
||||||
|
desc = "Feuerrate explodiert — schwächere Treffer"
|
||||||
|
desc_en = "Fire rate explodes — weaker hits"
|
||||||
|
category = "WAFFENMODUL"
|
||||||
|
category_en = "WEAPON MOD"
|
||||||
|
icon = "✦"
|
||||||
|
cost = 160
|
||||||
|
rarity = 3
|
||||||
|
effects = { "fire_rate_mult": 1.80, "damage_mult": 0.45 }
|
||||||
|
visual_pixels = [
|
||||||
|
[3, 0, "accent"],
|
||||||
|
[2, -1, "bright"],
|
||||||
|
[2, 1, "bright"],
|
||||||
|
[1, 0, "nose"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://j6tdxxbnnxfs
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "wk_charge"
|
||||||
|
name = "Laserkanone"
|
||||||
|
name_en = "Charge Cannon"
|
||||||
|
desc = "Gedrückt halten = Aufladen — loslassen = mächtiger Schuss"
|
||||||
|
desc_en = "Hold to charge — release for a powerful shot"
|
||||||
|
category = "WAFFENMODUL"
|
||||||
|
category_en = "WEAPON MOD"
|
||||||
|
icon = "◎"
|
||||||
|
cost = 150
|
||||||
|
rarity = 3
|
||||||
|
effects = { "damage_mult": 1.60, "fire_rate_mult": 0.40 }
|
||||||
|
visual_pixels = [
|
||||||
|
[8, 0, "nose"],
|
||||||
|
[9, 0, "bright"],
|
||||||
|
[10, 0, "accent"],
|
||||||
|
[7, -1, "edge"],
|
||||||
|
[7, 1, "edge"],
|
||||||
|
[6, -2, "dim"],
|
||||||
|
[6, 2, "dim"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bw14fge7v00xq
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "wk_ion"
|
||||||
|
name = "Ionenstrahl"
|
||||||
|
name_en = "Ion Beam"
|
||||||
|
desc = "Schaden + zusätzliches Projektil — langsamer"
|
||||||
|
desc_en = "Damage + extra projectile — slower"
|
||||||
|
category = "WAFFENMODUL"
|
||||||
|
category_en = "WEAPON MOD"
|
||||||
|
icon = "⚡"
|
||||||
|
cost = 140
|
||||||
|
rarity = 2
|
||||||
|
effects = { "damage_mult": 1.25, "bullet_count": 1, "speed_mult": 0.70 }
|
||||||
|
visual_pixels = [
|
||||||
|
[5, 0, "accent"],
|
||||||
|
[7, 0, "bright"],
|
||||||
|
[4, -2, "accent"],
|
||||||
|
[4, 2, "accent"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c24q5i65gcqhl
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "wk_laser"
|
||||||
|
name = "Laser-Kanone"
|
||||||
|
name_en = "Laser Gun"
|
||||||
|
desc = "Schnelles Dauerfeuer — schwächere Treffer"
|
||||||
|
desc_en = "Fast sustained fire — weaker hits"
|
||||||
|
category = "WAFFENMODUL"
|
||||||
|
category_en = "WEAPON MOD"
|
||||||
|
icon = "◈"
|
||||||
|
cost = 115
|
||||||
|
rarity = 1
|
||||||
|
effects = { "fire_rate_mult": 1.35, "damage_mult": 0.70 }
|
||||||
|
visual_pixels = [
|
||||||
|
[9, -2, "accent"],
|
||||||
|
[9, 2, "accent"],
|
||||||
|
[8, -2, "bright"],
|
||||||
|
[8, 2, "bright"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://7psg6avxedy2
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "wk_plasma"
|
||||||
|
name = "Plasmawerfer"
|
||||||
|
name_en = "Plasma Launcher"
|
||||||
|
desc = "Explosiver Schaden — träge Projektile"
|
||||||
|
desc_en = "Explosive damage — sluggish projectiles"
|
||||||
|
category = "WAFFENMODUL"
|
||||||
|
category_en = "WEAPON MOD"
|
||||||
|
icon = "◉"
|
||||||
|
cost = 130
|
||||||
|
rarity = 2
|
||||||
|
effects = { "damage_mult": 1.55, "bullet_speed_mult": 0.60 }
|
||||||
|
visual_pixels = [
|
||||||
|
[7, 0, "accent"],
|
||||||
|
[8, 0, "bright"],
|
||||||
|
[6, -3, "dim"],
|
||||||
|
[6, 3, "dim"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://by4hroe3nyrb8
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "wk_rail"
|
||||||
|
name = "Railgun"
|
||||||
|
name_en = "Railgun"
|
||||||
|
desc = "Hypersonisches Projektil — lange Ladezeit"
|
||||||
|
desc_en = "Hypersonic projectile — long reload"
|
||||||
|
category = "WAFFENMODUL"
|
||||||
|
category_en = "WEAPON MOD"
|
||||||
|
icon = "→"
|
||||||
|
cost = 125
|
||||||
|
rarity = 2
|
||||||
|
effects = { "bullet_speed_mult": 1.50, "fire_rate_mult": 0.50 }
|
||||||
|
visual_pixels = [
|
||||||
|
[7, 0, "bright"],
|
||||||
|
[8, 0, "nose"],
|
||||||
|
[9, 0, "accent"],
|
||||||
|
[6, -1, "edge"],
|
||||||
|
[6, 1, "edge"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ca0ny064gti12
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "wk_scatter"
|
||||||
|
name = "Streuschuß"
|
||||||
|
name_en = "Scatter Shot"
|
||||||
|
desc = "Mehr Projektile — schwächer pro Treffer"
|
||||||
|
desc_en = "More projectiles — weaker per hit"
|
||||||
|
category = "WAFFENMODUL"
|
||||||
|
category_en = "WEAPON MOD"
|
||||||
|
icon = "⁂"
|
||||||
|
cost = 115
|
||||||
|
rarity = 1
|
||||||
|
effects = { "bullet_count": 1, "damage_mult": 0.75 }
|
||||||
|
visual_pixels = [
|
||||||
|
[5, -4, "mid"],
|
||||||
|
[5, 4, "mid"],
|
||||||
|
[6, -3, "accent"],
|
||||||
|
[6, 3, "accent"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://chbitntcvo2sp
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "wk_shotgun"
|
||||||
|
name = "Schrotflinte"
|
||||||
|
name_en = "Shotgun"
|
||||||
|
desc = "Breite Salve — kurze Reichweite"
|
||||||
|
desc_en = "Wide burst — short range"
|
||||||
|
category = "WAFFENMODUL"
|
||||||
|
category_en = "WEAPON MOD"
|
||||||
|
icon = "▦"
|
||||||
|
cost = 105
|
||||||
|
rarity = 1
|
||||||
|
effects = { "bullet_count": 2, "bullet_speed_mult": 0.40, "damage_mult": 0.80 }
|
||||||
|
visual_pixels = [
|
||||||
|
[5, -3, "mid"],
|
||||||
|
[5, 0, "mid"],
|
||||||
|
[5, 3, "mid"],
|
||||||
|
[6, -3, "accent"],
|
||||||
|
[6, 0, "accent"],
|
||||||
|
[6, 3, "accent"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://m4ec31l51x41
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
extends ItemDef
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
id = "wk_sniper"
|
||||||
|
name = "Präzisionswerfer"
|
||||||
|
name_en = "Precision Rifle"
|
||||||
|
desc = "Präzision + Reichweite — halbierte Kadenz"
|
||||||
|
desc_en = "Precision + range — halved fire rate"
|
||||||
|
category = "WAFFENMODUL"
|
||||||
|
category_en = "WEAPON MOD"
|
||||||
|
icon = "◎"
|
||||||
|
cost = 115
|
||||||
|
rarity = 1
|
||||||
|
effects = { "bullet_speed_mult": 1.30, "damage_mult": 1.18, "fire_rate_mult": 0.55 }
|
||||||
|
visual_pixels = [
|
||||||
|
[7, 0, "edge"],
|
||||||
|
[8, 0, "dim"],
|
||||||
|
[9, 0, "bright"],
|
||||||
|
[10, 0, "nose"],
|
||||||
|
]
|
||||||
|
hull_size_bonus = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dloo2irxpr0r4
|
||||||
+175
@@ -0,0 +1,175 @@
|
|||||||
|
; Engine configuration file.
|
||||||
|
; It's best edited using the editor UI and not directly,
|
||||||
|
; since the parameters that go here are not all obvious.
|
||||||
|
;
|
||||||
|
; Format:
|
||||||
|
; [section] ; section goes between []
|
||||||
|
; param=value ; assign values to parameters
|
||||||
|
|
||||||
|
config_version=5
|
||||||
|
|
||||||
|
[application]
|
||||||
|
|
||||||
|
config/name="spacel"
|
||||||
|
run/main_scene="res://scenes/main.tscn"
|
||||||
|
config/features=PackedStringArray("4.6", "Forward Plus")
|
||||||
|
run/max_fps=60.0
|
||||||
|
config/icon="res://icon.svg"
|
||||||
|
|
||||||
|
[autoload]
|
||||||
|
|
||||||
|
Settings="*res://scripts/settings.gd"
|
||||||
|
SoundManager="*res://scripts/sound_manager.gd"
|
||||||
|
Leaderboard="*res://scripts/leaderboard.gd"
|
||||||
|
MCPScreenshot="*res://addons/godot_mcp/mcp_screenshot_service.gd"
|
||||||
|
MCPInputService="*res://addons/godot_mcp/mcp_input_service.gd"
|
||||||
|
MCPGameInspector="*res://addons/godot_mcp/mcp_game_inspector_service.gd"
|
||||||
|
Tr="*res://scripts/tr.gd"
|
||||||
|
|
||||||
|
[display]
|
||||||
|
|
||||||
|
window/size/viewport_width=960.0
|
||||||
|
window/size/viewport_height=600.0
|
||||||
|
window/size/mode=3.0
|
||||||
|
window/size/window_width_override=960.0
|
||||||
|
window/size/window_height_override=600.0
|
||||||
|
window/stretch/mode="canvas_items"
|
||||||
|
window/stretch/aspect="expand"
|
||||||
|
window/handheld/orientation="landscape"
|
||||||
|
|
||||||
|
[editor_plugins]
|
||||||
|
|
||||||
|
enabled=PackedStringArray("res://addons/godot_mcp/plugin.cfg")
|
||||||
|
|
||||||
|
[input]
|
||||||
|
|
||||||
|
ui_accept={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194309,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194310,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
ui_cancel={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":1,"pressure":0.0,"pressed":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":6,"pressure":0.0,"pressed":false,"script":null)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
ui_left={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null)
|
||||||
|
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":-1.0,"script":null)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
ui_right={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":14,"pressure":0.0,"pressed":false,"script":null)
|
||||||
|
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":1.0,"script":null)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
ui_up={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":11,"pressure":0.0,"pressed":false,"script":null)
|
||||||
|
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":-1.0,"script":null)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
ui_down={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":12,"pressure":0.0,"pressed":false,"script":null)
|
||||||
|
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":1.0,"script":null)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
p1_thrust={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":11,"pressure":0.0,"pressed":false,"script":null)
|
||||||
|
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":-1.0,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
p1_left={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":13,"pressure":0.0,"pressed":false,"script":null)
|
||||||
|
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":-1.0,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
p1_right={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":14,"pressure":0.0,"pressed":false,"script":null)
|
||||||
|
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":1.0,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
p1_shoot={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":32,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":2,"pressure":0.0,"pressed":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":10,"pressure":0.0,"pressed":false,"script":null)
|
||||||
|
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":5,"axis_value":1.0,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
p1_wipe={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":78,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":3,"pressure":0.0,"pressed":false,"script":null)
|
||||||
|
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":9,"pressure":0.0,"pressed":false,"script":null)
|
||||||
|
, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":4,"axis_value":1.0,"script":null)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
p2_thrust={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":87,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
p2_left={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":65,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
p2_right={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":68,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
p2_shoot={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":70,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
p2_wipe={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":69,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
p1_boost={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194325,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
p2_boost={
|
||||||
|
"deadzone": 0.5,
|
||||||
|
"events": Array[InputEvent]([Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":81,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
[input_devices]
|
||||||
|
|
||||||
|
pointing/emulate_touch_from_mouse=true
|
||||||
|
|
||||||
|
[physics]
|
||||||
|
|
||||||
|
3d/physics_engine="Jolt Physics"
|
||||||
|
|
||||||
|
[rendering]
|
||||||
|
|
||||||
|
textures/canvas_textures/default_texture_filter=0.0
|
||||||
|
rendering_device/driver.windows="d3d12"
|
||||||
|
textures/vram_compression/import_etc2_astc=true
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene format=3 uid="uid://cduyy76lfld7f"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://dgjwy8o1rgfd7" path="res://scripts/game_world.gd" id="1_0biqy"]
|
||||||
|
|
||||||
|
[node name="GameWorld" type="Node2D" unique_id=536762585]
|
||||||
|
script = ExtResource("1_0biqy")
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene format=3 uid="uid://c5o85b0e7tydc"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://kn3e6fiywxr7" path="res://scripts/hud.gd" id="1_ahhtf"]
|
||||||
|
|
||||||
|
[node name="HUD" type="CanvasLayer" unique_id=941486947]
|
||||||
|
script = ExtResource("1_ahhtf")
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
[gd_scene format=3 uid="uid://bq46s1hievrq4"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://ctbcvqj0aoo3v" path="res://scripts/main.gd" id="1_o6xl0"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://cduyy76lfld7f" path="res://scenes/game_world.tscn" id="2_tipki"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dpjrso6jkwunn" path="res://scenes/ship_select.tscn" id="4_choun"]
|
||||||
|
[ext_resource type="Script" uid="uid://18s6j8gme2ne" path="res://scripts/shop_ui.gd" id="5_tbgi4"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://c5o85b0e7tydc" path="res://scenes/hud.tscn" id="6_eb6dy"]
|
||||||
|
[ext_resource type="Script" uid="uid://lmuic5md3b5p" path="res://scripts/pause_menu.gd" id="7_pause"]
|
||||||
|
[ext_resource type="Script" uid="uid://cp36y6rjo8aqs" path="res://scripts/main_menu.gd" id="8_mainmenu"]
|
||||||
|
[ext_resource type="Script" uid="uid://dxun4apx8cxf1" path="res://scripts/music_player.gd" id="9_music"]
|
||||||
|
[ext_resource type="Script" uid="uid://dbj3vbayteqo0" path="res://scripts/atlas_ui.gd" id="10_atlas"]
|
||||||
|
[ext_resource type="Script" uid="uid://cn7sutxtu7qym" path="res://scripts/touch_controls.gd" id="11_touch"]
|
||||||
|
|
||||||
|
[node name="Main" type="Node2D" unique_id=262978007]
|
||||||
|
script = ExtResource("1_o6xl0")
|
||||||
|
|
||||||
|
[node name="GameWorld" parent="." unique_id=536762585 instance=ExtResource("2_tipki")]
|
||||||
|
|
||||||
|
[node name="ShipSelectUI" parent="." unique_id=541251044 instance=ExtResource("4_choun")]
|
||||||
|
|
||||||
|
[node name="HUD" parent="." unique_id=941486947 instance=ExtResource("6_eb6dy")]
|
||||||
|
|
||||||
|
[node name="TouchControls" type="Node2D" parent="HUD" unique_id=1039181909]
|
||||||
|
script = ExtResource("11_touch")
|
||||||
|
|
||||||
|
[node name="ShopUI" type="Node2D" parent="." unique_id=1039181903]
|
||||||
|
visible = false
|
||||||
|
script = ExtResource("5_tbgi4")
|
||||||
|
|
||||||
|
[node name="PauseMenu" type="Node2D" parent="." unique_id=1039181904]
|
||||||
|
visible = false
|
||||||
|
script = ExtResource("7_pause")
|
||||||
|
|
||||||
|
[node name="MainMenu" type="Node2D" parent="." unique_id=1039181905]
|
||||||
|
script = ExtResource("8_mainmenu")
|
||||||
|
|
||||||
|
[node name="AtlasUI" type="Node2D" parent="." unique_id=1039181907]
|
||||||
|
visible = false
|
||||||
|
script = ExtResource("10_atlas")
|
||||||
|
|
||||||
|
[node name="MusicPlayer" type="Node" parent="." unique_id=1039181906]
|
||||||
|
script = ExtResource("9_music")
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene format=3 uid="uid://dpjrso6jkwunn"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://bc1kys1vqyll6" path="res://scripts/ship_select.gd" id="1_27sl3"]
|
||||||
|
|
||||||
|
[node name="ShipSelectUI" type="Node2D" unique_id=541251044]
|
||||||
|
script = ExtResource("1_27sl3")
|
||||||
@@ -0,0 +1,673 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# ─── Atlas UI ────────────────────────────────────────────────────────────────
|
||||||
|
# Beschreibt alle kosmischen Objekte, Schiffe und Events mit Live-Preview.
|
||||||
|
# Previews verwenden die echten Spielobjekte (draw-Methoden identisch zum Spiel).
|
||||||
|
# Sichtbar über das Hauptmenü (ATLAS). Sendet `closed` wenn beendet.
|
||||||
|
|
||||||
|
signal closed
|
||||||
|
|
||||||
|
const CosmicObjects = preload("res://scripts/cosmic_objects.gd")
|
||||||
|
const BlackHoleClass = preload("res://scripts/black_hole.gd")
|
||||||
|
const PlanetClass = preload("res://scripts/planet.gd")
|
||||||
|
|
||||||
|
# NOVA-1 Palette — exakt aus main.gd SHIPS[0]
|
||||||
|
const PAL_NOVA1 := {
|
||||||
|
"nose": Color("#ffffff"), "bright": Color("#dddddd"), "mid": Color("#cccccc"),
|
||||||
|
"dim": Color("#aaaaaa"), "accent": Color("#88aaff"), "edge": Color("#888888"),
|
||||||
|
"shadow": Color("#666688"), "trail": Color(0.533, 0.667, 1.0, 0.251),
|
||||||
|
"thrustHot": Color("#ffcc44"), "thrustCool": Color(1.0, 0.533, 0.267, 0.533)
|
||||||
|
}
|
||||||
|
|
||||||
|
const COL_BG := Color(0.0, 0.0, 0.04, 0.90)
|
||||||
|
const COL_PRIMARY := Color(0.0, 1.0, 0.533, 1.0)
|
||||||
|
const COL_ACCENT := Color(0.27, 1.0, 0.8, 1.0)
|
||||||
|
const COL_DIM := Color(0.0, 0.27, 0.13, 0.55)
|
||||||
|
const COL_WHITE := Color(1.0, 1.0, 1.0, 0.90)
|
||||||
|
const COL_WARN := Color(1.0, 0.67, 0.0, 0.90)
|
||||||
|
|
||||||
|
var W: float = 960.0
|
||||||
|
var H: float = 600.0
|
||||||
|
var _blink: float = 0.0
|
||||||
|
var _cursor: int = 0
|
||||||
|
var _scroll: int = 0
|
||||||
|
var _time: float = 0.0
|
||||||
|
|
||||||
|
# Preview box center (fixed for 960×600 viewport)
|
||||||
|
# right_x=300, pbox_x=318, pbox_y=116, pbox_sz=180
|
||||||
|
const PREV_CX := 408.0
|
||||||
|
const PREV_CY := 206.0
|
||||||
|
|
||||||
|
# Preview objects — real game instances
|
||||||
|
var _preview_obj = null # single object (most kinds)
|
||||||
|
var _preview_list: Array = [] # multiple objects (antimatter)
|
||||||
|
var _preview_kind: String = ""
|
||||||
|
|
||||||
|
# Entry schema:
|
||||||
|
# kind: id used by preview dispatcher
|
||||||
|
# cat: category key (atlas_cat_*)
|
||||||
|
# name_key: translation key for name
|
||||||
|
# desc_key: translation key for description
|
||||||
|
# props: Array of [label_key, value_string] (value is NOT translated)
|
||||||
|
const ENTRIES: Array = [
|
||||||
|
{"kind": "star", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_star",
|
||||||
|
"desc_key": "atlas_d_star", "props": [
|
||||||
|
["atlas_prop_size", "1–7 px"],
|
||||||
|
["atlas_prop_speed", "0.05–0.3 px/f"],
|
||||||
|
["atlas_prop_spawn", "~150–300"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_none"],
|
||||||
|
]},
|
||||||
|
{"kind": "planet_terr", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_planet_terr",
|
||||||
|
"desc_key": "atlas_d_planet_terr", "props": [
|
||||||
|
["atlas_prop_size", "5–10 px"],
|
||||||
|
["atlas_prop_orbit", "19–72 px"],
|
||||||
|
["atlas_prop_effect", "Wolken/Polkappen"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_none"],
|
||||||
|
]},
|
||||||
|
{"kind": "planet_desert", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_planet_desert",
|
||||||
|
"desc_key": "atlas_d_planet_desert", "props": [
|
||||||
|
["atlas_prop_size", "5–10 px"],
|
||||||
|
["atlas_prop_orbit", "19–72 px"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_none"],
|
||||||
|
]},
|
||||||
|
{"kind": "planet_gas", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_planet_gas",
|
||||||
|
"desc_key": "atlas_d_planet_gas", "props": [
|
||||||
|
["atlas_prop_size", "12–19 px"],
|
||||||
|
["atlas_prop_orbit", "19–72 px"],
|
||||||
|
["atlas_prop_effect", "Ringe, Monde, Sturm"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_none"],
|
||||||
|
]},
|
||||||
|
{"kind": "planet_ice", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_planet_ice",
|
||||||
|
"desc_key": "atlas_d_planet_ice", "props": [
|
||||||
|
["atlas_prop_size", "5–10 px"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_none"],
|
||||||
|
]},
|
||||||
|
{"kind": "planet_lava", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_planet_lava",
|
||||||
|
"desc_key": "atlas_d_planet_lava", "props": [
|
||||||
|
["atlas_prop_size", "5–10 px"],
|
||||||
|
["atlas_prop_effect", "Halo + Glut"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_none"],
|
||||||
|
]},
|
||||||
|
{"kind": "planet_toxic", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_planet_toxic",
|
||||||
|
"desc_key": "atlas_d_planet_toxic", "props": [
|
||||||
|
["atlas_prop_size", "5–10 px"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_none"],
|
||||||
|
]},
|
||||||
|
{"kind": "nebula", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_nebula",
|
||||||
|
"desc_key": "atlas_d_nebula", "props": [
|
||||||
|
["atlas_prop_size", "120–220 px"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_none"],
|
||||||
|
]},
|
||||||
|
{"kind": "comet", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_comet",
|
||||||
|
"desc_key": "atlas_d_comet", "props": [
|
||||||
|
["atlas_prop_speed", "1–4 px/f"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_none"],
|
||||||
|
]},
|
||||||
|
{"kind": "galaxy", "cat": "atlas_cat_cosmic", "name_key": "atlas_n_galaxy",
|
||||||
|
"desc_key": "atlas_d_galaxy", "props": [
|
||||||
|
["atlas_prop_size", "~80 px"],
|
||||||
|
["atlas_prop_effect", "SMBH-Trigger"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_none"],
|
||||||
|
]},
|
||||||
|
{"kind": "blackhole", "cat": "atlas_cat_exotic", "name_key": "atlas_n_blackhole",
|
||||||
|
"desc_key": "atlas_d_blackhole", "props": [
|
||||||
|
["atlas_prop_size", "14–40 px"],
|
||||||
|
["atlas_prop_effect", "Pull 160 px"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_deadly"],
|
||||||
|
]},
|
||||||
|
{"kind": "whitehole", "cat": "atlas_cat_exotic", "name_key": "atlas_n_whitehole",
|
||||||
|
"desc_key": "atlas_d_whitehole", "props": [
|
||||||
|
["atlas_prop_size", "~16 px"],
|
||||||
|
["atlas_prop_effect", "Push 340 px"],
|
||||||
|
["atlas_prop_life", "~60 s"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_low"],
|
||||||
|
]},
|
||||||
|
{"kind": "neutron", "cat": "atlas_cat_exotic", "name_key": "atlas_n_neutron",
|
||||||
|
"desc_key": "atlas_d_neutron", "props": [
|
||||||
|
["atlas_prop_size", "~6 px"],
|
||||||
|
["atlas_prop_effect", "Pulsar-Beam"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_low"],
|
||||||
|
]},
|
||||||
|
{"kind": "quasar", "cat": "atlas_cat_exotic", "name_key": "atlas_n_quasar",
|
||||||
|
"desc_key": "atlas_d_quasar", "props": [
|
||||||
|
["atlas_prop_life", "~30 s"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_none"],
|
||||||
|
]},
|
||||||
|
{"kind": "antimatter", "cat": "atlas_cat_anti", "name_key": "atlas_n_antimatter",
|
||||||
|
"desc_key": "atlas_d_antimatter", "props": [
|
||||||
|
["atlas_prop_size", "2 px"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_deadly"],
|
||||||
|
]},
|
||||||
|
{"kind": "antistar", "cat": "atlas_cat_anti", "name_key": "atlas_n_antistar",
|
||||||
|
"desc_key": "atlas_d_antistar", "props": [
|
||||||
|
["atlas_prop_life", "~50 s"],
|
||||||
|
["atlas_prop_effect", "Push"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_deadly"],
|
||||||
|
]},
|
||||||
|
{"kind": "player", "cat": "atlas_cat_ships", "name_key": "atlas_n_player",
|
||||||
|
"desc_key": "atlas_d_player", "props": [
|
||||||
|
["atlas_prop_hp", "1 (+Schilde)"],
|
||||||
|
["atlas_prop_speed", "7.5 Max"],
|
||||||
|
["atlas_prop_damage", "1.0 (Pierce ≥2)"],
|
||||||
|
]},
|
||||||
|
{"kind": "enemy", "cat": "atlas_cat_ships", "name_key": "atlas_n_enemy",
|
||||||
|
"desc_key": "atlas_d_enemy", "props": [
|
||||||
|
["atlas_prop_hp", "1"],
|
||||||
|
["atlas_prop_reward", "15 Credits"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_mid"],
|
||||||
|
]},
|
||||||
|
{"kind": "wraith", "cat": "atlas_cat_ships", "name_key": "atlas_n_wraith",
|
||||||
|
"desc_key": "atlas_d_wraith", "props": [
|
||||||
|
["atlas_prop_hp", "20"],
|
||||||
|
["atlas_prop_reward", "150 Credits"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_high"],
|
||||||
|
]},
|
||||||
|
{"kind": "leviathan", "cat": "atlas_cat_ships", "name_key": "atlas_n_leviathan",
|
||||||
|
"desc_key": "atlas_d_leviathan", "props": [
|
||||||
|
["atlas_prop_hp", "50"],
|
||||||
|
["atlas_prop_reward", "300 Credits"],
|
||||||
|
["atlas_prop_hazard", "atlas_hazard_deadly"],
|
||||||
|
]},
|
||||||
|
{"kind": "bullet", "cat": "atlas_cat_events", "name_key": "atlas_n_bullet",
|
||||||
|
"desc_key": "atlas_d_bullet", "props": [
|
||||||
|
["atlas_prop_speed", "9.6 px/f"],
|
||||||
|
["atlas_prop_life", "240 f"],
|
||||||
|
]},
|
||||||
|
{"kind": "bigwipe", "cat": "atlas_cat_events", "name_key": "atlas_n_bigwipe",
|
||||||
|
"desc_key": "atlas_d_bigwipe", "props": [
|
||||||
|
["atlas_prop_effect", "> 500 Objekte"],
|
||||||
|
["atlas_prop_reward", "25 Credits"],
|
||||||
|
]},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
func open() -> void:
|
||||||
|
visible = true
|
||||||
|
_cursor = 0
|
||||||
|
_scroll = 0
|
||||||
|
_time = 0.0
|
||||||
|
_rebuild_preview()
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func close() -> void:
|
||||||
|
visible = false
|
||||||
|
_preview_obj = null
|
||||||
|
_preview_list = []
|
||||||
|
_preview_kind = ""
|
||||||
|
closed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Loop ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if not visible: return
|
||||||
|
_blink += delta
|
||||||
|
_time += delta
|
||||||
|
_update_preview(delta)
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_preview(delta: float) -> void:
|
||||||
|
match _preview_kind:
|
||||||
|
"star":
|
||||||
|
if _preview_obj:
|
||||||
|
_preview_obj.update(delta, 99999.0, 99999.0)
|
||||||
|
_preview_obj.x = PREV_CX
|
||||||
|
_preview_obj.y = PREV_CY
|
||||||
|
"galaxy":
|
||||||
|
if _preview_obj:
|
||||||
|
_preview_obj.update(delta, 99999.0, 99999.0)
|
||||||
|
_preview_obj.x = PREV_CX
|
||||||
|
_preview_obj.y = PREV_CY
|
||||||
|
"blackhole":
|
||||||
|
if _preview_obj:
|
||||||
|
_preview_obj.update(delta, [], 99999.0, 99999.0)
|
||||||
|
_preview_obj.x = PREV_CX
|
||||||
|
_preview_obj.y = PREV_CY
|
||||||
|
"whitehole":
|
||||||
|
if _preview_obj:
|
||||||
|
_preview_obj.update(delta)
|
||||||
|
_preview_obj.x = PREV_CX
|
||||||
|
_preview_obj.y = PREV_CY
|
||||||
|
"neutron", "quasar":
|
||||||
|
if _preview_obj:
|
||||||
|
_preview_obj.update(delta)
|
||||||
|
"antistar":
|
||||||
|
if _preview_obj:
|
||||||
|
_preview_obj.update(delta)
|
||||||
|
_preview_obj.x = PREV_CX
|
||||||
|
_preview_obj.y = PREV_CY
|
||||||
|
"antimatter":
|
||||||
|
for k: int in _preview_list.size():
|
||||||
|
var am = _preview_list[k]
|
||||||
|
var ang := float(k) * TAU / float(_preview_list.size()) + _time * 0.6
|
||||||
|
var r := 20.0 + sin(_time + float(k)) * 12.0
|
||||||
|
am.x = PREV_CX + cos(ang) * r
|
||||||
|
am.y = PREV_CY + sin(ang) * r
|
||||||
|
am.update(99999.0, 99999.0, delta, [])
|
||||||
|
# reset position after update (velocity might shift it)
|
||||||
|
am.x = PREV_CX + cos(ang) * r
|
||||||
|
am.y = PREV_CY + sin(ang) * r
|
||||||
|
"wraith", "leviathan":
|
||||||
|
if _preview_obj:
|
||||||
|
_preview_obj.update(delta, PREV_CX, PREV_CY)
|
||||||
|
"planet_terr", "planet_desert", "planet_gas", "planet_ice", "planet_lava", "planet_toxic":
|
||||||
|
if _preview_obj and _preview_obj.has_method("update"):
|
||||||
|
_preview_obj.update(delta)
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if not visible: return
|
||||||
|
if event.is_action_pressed("ui_up"):
|
||||||
|
_cursor = (_cursor - 1 + ENTRIES.size()) % ENTRIES.size()
|
||||||
|
_ensure_cursor_visible()
|
||||||
|
_rebuild_preview()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif event.is_action_pressed("ui_down"):
|
||||||
|
_cursor = (_cursor + 1) % ENTRIES.size()
|
||||||
|
_ensure_cursor_visible()
|
||||||
|
_rebuild_preview()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif event.is_action_pressed("ui_cancel") or event.is_action_pressed("ui_accept"):
|
||||||
|
close()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func _ensure_cursor_visible() -> void:
|
||||||
|
var visible_rows := 14
|
||||||
|
if _cursor < _scroll:
|
||||||
|
_scroll = _cursor
|
||||||
|
elif _cursor >= _scroll + visible_rows:
|
||||||
|
_scroll = _cursor - visible_rows + 1
|
||||||
|
|
||||||
|
|
||||||
|
func _rebuild_preview() -> void:
|
||||||
|
var kind: String = ENTRIES[_cursor]["kind"]
|
||||||
|
_preview_kind = kind
|
||||||
|
_preview_obj = null
|
||||||
|
_preview_list = []
|
||||||
|
_time = 0.0
|
||||||
|
|
||||||
|
match kind:
|
||||||
|
"star":
|
||||||
|
var s := CosmicObjects.Star.new()
|
||||||
|
s.init(PREV_CX, PREV_CY, 99999.0, 99999.0)
|
||||||
|
_preview_obj = s
|
||||||
|
|
||||||
|
"nebula":
|
||||||
|
var n := CosmicObjects.Nebula.new()
|
||||||
|
n.init(99999.0, 99999.0)
|
||||||
|
n.x = PREV_CX
|
||||||
|
n.y = PREV_CY
|
||||||
|
_preview_obj = n
|
||||||
|
|
||||||
|
"comet":
|
||||||
|
var c := CosmicObjects.Comet.new()
|
||||||
|
c.init(99999.0, 99999.0)
|
||||||
|
# Position it so it travels across the preview box area
|
||||||
|
c.x = PREV_CX - 70.0
|
||||||
|
c.y = PREV_CY - 40.0
|
||||||
|
_preview_obj = c
|
||||||
|
|
||||||
|
"galaxy":
|
||||||
|
var g := CosmicObjects.Galaxy.new()
|
||||||
|
g.init(PREV_CX, PREV_CY)
|
||||||
|
_preview_obj = g
|
||||||
|
|
||||||
|
"blackhole":
|
||||||
|
var bh := BlackHoleClass.new()
|
||||||
|
bh.init(PREV_CX, PREV_CY, false)
|
||||||
|
_preview_obj = bh
|
||||||
|
|
||||||
|
"whitehole":
|
||||||
|
var wh := CosmicObjects.WhiteHole.new()
|
||||||
|
wh.init(PREV_CX, PREV_CY)
|
||||||
|
_preview_obj = wh
|
||||||
|
|
||||||
|
"neutron":
|
||||||
|
var ns := CosmicObjects.NeutronStar.new()
|
||||||
|
ns.init(PREV_CX, PREV_CY)
|
||||||
|
_preview_obj = ns
|
||||||
|
|
||||||
|
"quasar":
|
||||||
|
var q := CosmicObjects.Quasar.new()
|
||||||
|
q.init(PREV_CX, PREV_CY)
|
||||||
|
_preview_obj = q
|
||||||
|
|
||||||
|
"antimatter":
|
||||||
|
for k: int in 10:
|
||||||
|
var am := CosmicObjects.Antimatter.new()
|
||||||
|
var ang := float(k) * TAU / 10.0
|
||||||
|
var r := 22.0
|
||||||
|
am.init(PREV_CX + cos(ang) * r, PREV_CY + sin(ang) * r, ang, 0.0)
|
||||||
|
_preview_list.append(am)
|
||||||
|
|
||||||
|
"antistar":
|
||||||
|
var a := CosmicObjects.AntimatterStar.new()
|
||||||
|
a.init(PREV_CX, PREV_CY)
|
||||||
|
_preview_obj = a
|
||||||
|
|
||||||
|
"player":
|
||||||
|
var sp := Spaceship.new()
|
||||||
|
sp.init(PREV_CX, PREV_CY, PAL_NOVA1, 0)
|
||||||
|
sp.heading = -PI * 0.5 # zeigt nach oben
|
||||||
|
_preview_obj = sp
|
||||||
|
|
||||||
|
"enemy":
|
||||||
|
var en := EnemyShip.new()
|
||||||
|
en.init(PREV_CX, PREV_CY, 0) # idx=0 → roter Gegner
|
||||||
|
en.heading = PI * 0.5
|
||||||
|
_preview_obj = en
|
||||||
|
|
||||||
|
"wraith":
|
||||||
|
var boss := BossShip.new()
|
||||||
|
boss.init_miniboss(PREV_CX, PREV_CY)
|
||||||
|
_preview_obj = boss
|
||||||
|
|
||||||
|
"leviathan":
|
||||||
|
var boss := BossShip.new()
|
||||||
|
boss.init_boss(PREV_CX, PREV_CY)
|
||||||
|
_preview_obj = boss
|
||||||
|
|
||||||
|
"planet_terr", "planet_desert", "planet_gas", "planet_ice", "planet_lava", "planet_toxic":
|
||||||
|
var p := PlanetClass.new()
|
||||||
|
p.init(0.0, 0.0, 960.0, 600.0)
|
||||||
|
p.ptype = _ptype_for(kind)
|
||||||
|
p._setup_palette()
|
||||||
|
p._setup_noise()
|
||||||
|
p._setup_animation()
|
||||||
|
if kind == "planet_gas":
|
||||||
|
p.radius = 22.0
|
||||||
|
else:
|
||||||
|
p.radius = 18.0
|
||||||
|
p.initial_radius = p.radius
|
||||||
|
p.color = p.palette[0]
|
||||||
|
if kind == "planet_gas":
|
||||||
|
p.ring = true
|
||||||
|
p.ring_count = 2
|
||||||
|
p.ring_inner = p.radius + 6.0
|
||||||
|
p.ring_outer = p.ring_inner + 8.0
|
||||||
|
p.ring_tilt = 0.28
|
||||||
|
p.ring_colors = [p.palette[0], p.palette[2]]
|
||||||
|
p.ring_gaps = []
|
||||||
|
else:
|
||||||
|
p.ring = false
|
||||||
|
p.moons.clear()
|
||||||
|
_preview_obj = p
|
||||||
|
|
||||||
|
# bullet / bigwipe: no live object, drawn procedurally below
|
||||||
|
|
||||||
|
|
||||||
|
func _ptype_for(kind: String) -> int:
|
||||||
|
match kind:
|
||||||
|
"planet_terr": return PlanetClass.PType.TERRESTRIAL
|
||||||
|
"planet_desert": return PlanetClass.PType.DESERT
|
||||||
|
"planet_gas": return PlanetClass.PType.GAS_GIANT
|
||||||
|
"planet_ice": return PlanetClass.PType.ICE
|
||||||
|
"planet_lava": return PlanetClass.PType.LAVA
|
||||||
|
"planet_toxic": return PlanetClass.PType.TOXIC
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Drawing ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var vs: Vector2 = get_viewport_rect().size
|
||||||
|
W = vs.x; H = vs.y
|
||||||
|
|
||||||
|
# Full-screen darkening
|
||||||
|
draw_rect(Rect2(0, 0, W, H), COL_BG)
|
||||||
|
|
||||||
|
# Outer brackets
|
||||||
|
_draw_brackets(12.0, 12.0, W - 12.0, H - 12.0, COL_DIM, 18.0)
|
||||||
|
|
||||||
|
# Title header
|
||||||
|
_draw_text_c(Tr.t("atlas_title"), W * 0.5, 26.0, 14, COL_PRIMARY)
|
||||||
|
var line_y := 54.0
|
||||||
|
draw_line(Vector2(W * 0.25, line_y), Vector2(W * 0.75, line_y),
|
||||||
|
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.35), 1.0)
|
||||||
|
|
||||||
|
# Layout
|
||||||
|
var top := 70.0
|
||||||
|
var bottom := H - 40.0
|
||||||
|
var list_x := 24.0
|
||||||
|
var list_w := 260.0
|
||||||
|
var right_x := list_x + list_w + 16.0
|
||||||
|
var right_w := W - right_x - 24.0
|
||||||
|
|
||||||
|
_draw_list(list_x, top, list_w, bottom - top)
|
||||||
|
_draw_details(right_x, top, right_w, bottom - top)
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
_draw_text_c(Tr.t("atlas_footer"), W * 0.5, H - 22.0, 8, COL_DIM)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_list(x: float, y: float, w: float, h: float) -> void:
|
||||||
|
_draw_terminal_box(x, y, w, h)
|
||||||
|
var visible_rows := 14
|
||||||
|
var row_h := 22.0
|
||||||
|
var last_cat := ""
|
||||||
|
var draw_y := y + 14.0
|
||||||
|
var i := _scroll
|
||||||
|
var rows_left := visible_rows
|
||||||
|
while i < ENTRIES.size() and rows_left > 0:
|
||||||
|
var entry: Dictionary = ENTRIES[i]
|
||||||
|
var cat: String = entry["cat"]
|
||||||
|
if cat != last_cat:
|
||||||
|
var ch := Tr.t(cat)
|
||||||
|
_draw_text(ch, x + 14.0, draw_y, 9,
|
||||||
|
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.55))
|
||||||
|
draw_y += 14.0
|
||||||
|
last_cat = cat
|
||||||
|
var is_sel := (i == _cursor)
|
||||||
|
var pulse := 0.6 + 0.4 * sin(_blink * 3.0)
|
||||||
|
var col: Color
|
||||||
|
if is_sel:
|
||||||
|
col = Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse)
|
||||||
|
else:
|
||||||
|
col = Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.60)
|
||||||
|
var prefix := "▶ " if is_sel else " "
|
||||||
|
_draw_text(prefix + Tr.t(entry["name_key"]), x + 20.0, draw_y, 10, col)
|
||||||
|
draw_y += row_h - 2.0
|
||||||
|
i += 1
|
||||||
|
rows_left -= 1
|
||||||
|
|
||||||
|
# Scroll indicators
|
||||||
|
if _scroll > 0:
|
||||||
|
_draw_text(" ▲", x + w - 24.0, y + 6.0, 10, COL_DIM)
|
||||||
|
if _scroll + visible_rows < ENTRIES.size():
|
||||||
|
_draw_text(" ▼", x + w - 24.0, y + h - 22.0, 10, COL_DIM)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_details(x: float, y: float, w: float, h: float) -> void:
|
||||||
|
_draw_terminal_box(x, y, w, h)
|
||||||
|
var entry: Dictionary = ENTRIES[_cursor]
|
||||||
|
|
||||||
|
# Name header
|
||||||
|
var name_y := y + 18.0
|
||||||
|
_draw_text(Tr.t(entry["name_key"]), x + 18.0, name_y, 14, COL_ACCENT)
|
||||||
|
|
||||||
|
# Preview box (left sub-area)
|
||||||
|
var pbox_sz := 180.0
|
||||||
|
var pbox_x := x + 18.0
|
||||||
|
var pbox_y := name_y + 28.0
|
||||||
|
_draw_preview_box(pbox_x, pbox_y, pbox_sz, pbox_sz, entry["kind"])
|
||||||
|
|
||||||
|
# Props (right of preview)
|
||||||
|
var prop_x := pbox_x + pbox_sz + 24.0
|
||||||
|
var prop_y := pbox_y
|
||||||
|
_draw_text(Tr.t("atlas_props"), prop_x, prop_y, 9,
|
||||||
|
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.65))
|
||||||
|
prop_y += 16.0
|
||||||
|
draw_line(Vector2(prop_x, prop_y), Vector2(x + w - 24.0, prop_y),
|
||||||
|
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.25), 1.0)
|
||||||
|
prop_y += 8.0
|
||||||
|
var props: Array = entry["props"]
|
||||||
|
for pair in props:
|
||||||
|
var lbl: String = Tr.t(pair[0])
|
||||||
|
var val_raw: String = pair[1]
|
||||||
|
var val := val_raw
|
||||||
|
if val_raw.begins_with("atlas_hazard_"):
|
||||||
|
val = Tr.t(val_raw)
|
||||||
|
_draw_text(lbl, prop_x, prop_y, 10, Color(COL_WHITE.r, COL_WHITE.g, COL_WHITE.b, 0.55))
|
||||||
|
_draw_text(val, prop_x + 100.0, prop_y, 10, COL_WHITE)
|
||||||
|
prop_y += 20.0
|
||||||
|
|
||||||
|
# Description (full width below)
|
||||||
|
var desc_y := pbox_y + pbox_sz + 22.0
|
||||||
|
_draw_text(Tr.t("atlas_desc"), x + 18.0, desc_y, 9,
|
||||||
|
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.65))
|
||||||
|
desc_y += 16.0
|
||||||
|
draw_line(Vector2(x + 18.0, desc_y), Vector2(x + w - 18.0, desc_y),
|
||||||
|
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.25), 1.0)
|
||||||
|
desc_y += 8.0
|
||||||
|
var text: String = Tr.t(entry["desc_key"])
|
||||||
|
_draw_wrapped(text, x + 18.0, desc_y, w - 36.0, 11,
|
||||||
|
Color(COL_WHITE.r, COL_WHITE.g, COL_WHITE.b, 0.85))
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Preview dispatcher ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _draw_preview_box(px: float, py: float, pw: float, ph: float, kind: String) -> void:
|
||||||
|
# Dark frame
|
||||||
|
draw_rect(Rect2(px, py, pw, ph), Color(0.02, 0.02, 0.06, 0.85))
|
||||||
|
draw_rect(Rect2(px, py, pw, ph), Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.25), false, 1.0)
|
||||||
|
var cx := px + pw * 0.5
|
||||||
|
var cy := py + ph * 0.5
|
||||||
|
|
||||||
|
match kind:
|
||||||
|
"antimatter":
|
||||||
|
# Multiple real Antimatter objects
|
||||||
|
for am in _preview_list:
|
||||||
|
am.draw(self)
|
||||||
|
|
||||||
|
"player":
|
||||||
|
if _preview_obj:
|
||||||
|
_preview_obj.x = cx
|
||||||
|
_preview_obj.y = cy
|
||||||
|
_preview_obj.draw(self, 0) # frame=0 → nie blinken
|
||||||
|
|
||||||
|
"comet":
|
||||||
|
# Comet trail needs manual positioning within the box
|
||||||
|
_prev_comet(px, py, pw, ph)
|
||||||
|
|
||||||
|
"bullet":
|
||||||
|
_prev_bullet(cx, cy)
|
||||||
|
|
||||||
|
"bigwipe":
|
||||||
|
_prev_bigwipe(px, py, pw, ph)
|
||||||
|
|
||||||
|
_:
|
||||||
|
# Alle anderen: echtes Spielobjekt an Preview-Mitte positionieren und zeichnen
|
||||||
|
if _preview_obj:
|
||||||
|
_preview_obj.x = cx
|
||||||
|
_preview_obj.y = cy
|
||||||
|
_preview_obj.draw(self)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Custom previews (keine Entsprechung als einzelnes Spielobjekt) ───────────
|
||||||
|
|
||||||
|
func _prev_comet(px: float, py: float, pw: float, ph: float) -> void:
|
||||||
|
var tip_x := px + 30.0 + fmod(_time * 80.0, pw - 60.0)
|
||||||
|
var tip_y := py + 30.0 + fmod(_time * 40.0, ph - 60.0)
|
||||||
|
var tail := 26
|
||||||
|
for i: int in tail:
|
||||||
|
var t := float(i) / float(tail)
|
||||||
|
var tx := tip_x - cos(0.5) * float(i) * 3.0
|
||||||
|
var ty := tip_y - sin(0.5) * float(i) * 3.0
|
||||||
|
var a := (1.0 - t)
|
||||||
|
draw_rect(Rect2(tx, ty, 2, 2), Color(0.9, 0.95, 1.0, a * 0.85))
|
||||||
|
draw_rect(Rect2(tip_x - 2, tip_y - 2, 4, 4), Color(1, 1, 1, 1.0))
|
||||||
|
|
||||||
|
|
||||||
|
func _prev_bullet(cx: float, cy: float) -> void:
|
||||||
|
# Player bullet
|
||||||
|
draw_rect(Rect2(cx - 24, cy - 12, 4, 4), Color(0.7, 0.9, 1.0, 1.0))
|
||||||
|
draw_rect(Rect2(cx - 26, cy - 13, 6, 2), Color(0.7, 0.9, 1.0, 0.4))
|
||||||
|
# Pierce (white)
|
||||||
|
draw_rect(Rect2(cx + 2, cy - 12, 4, 4), Color(1, 1, 1, 1))
|
||||||
|
draw_rect(Rect2(cx - 8, cy - 8, 6, 2), Color(1, 1, 1, 0.3))
|
||||||
|
# Enemy bullet
|
||||||
|
draw_rect(Rect2(cx - 24, cy + 10, 4, 4), Color(1.0, 0.4, 0.4, 1.0))
|
||||||
|
# Boss bullet
|
||||||
|
draw_rect(Rect2(cx + 2, cy + 10, 4, 4), Color(1.0, 0.6, 0.2, 1.0))
|
||||||
|
_draw_text("player", cx - 52, cy - 18, 8, Color(0.7, 0.9, 1.0, 0.7))
|
||||||
|
_draw_text("pierce", cx + 12, cy - 18, 8, Color(1, 1, 1, 0.7))
|
||||||
|
_draw_text("enemy", cx - 52, cy + 4, 8, Color(1, 0.4, 0.4, 0.7))
|
||||||
|
_draw_text("boss", cx + 12, cy + 4, 8, Color(1, 0.6, 0.2, 0.7))
|
||||||
|
|
||||||
|
|
||||||
|
func _prev_bigwipe(px: float, py: float, pw: float, ph: float) -> void:
|
||||||
|
var flash := 0.5 + 0.5 * sin(_time * 6.0)
|
||||||
|
draw_rect(Rect2(px + 4, py + 4, pw - 8, ph - 8), Color(0.0, 0.0, 0.05, 0.75))
|
||||||
|
for i: int in 18:
|
||||||
|
var ly := py + 6.0 + float(i) * 10.0
|
||||||
|
if ly > py + ph - 6: continue
|
||||||
|
draw_line(Vector2(px + 4, ly), Vector2(px + pw - 4, ly),
|
||||||
|
Color(1, 1, 1, 0.08 + 0.12 * flash), 1.0)
|
||||||
|
var cx := px + pw * 0.5; var cy := py + ph * 0.5
|
||||||
|
draw_circle(Vector2(cx, cy), 22.0 + flash * 8.0, Color(1, 1, 1, 0.25 * flash))
|
||||||
|
_draw_text_c("[ N ]", cx, cy + 48.0, 14, Color(1, 1, 1, 0.7 + 0.3 * flash))
|
||||||
|
_draw_text_c("HOLD", cx, cy + 66.0, 8, Color(1, 1, 1, 0.6))
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Drawing helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _draw_terminal_box(bx: float, by: float, bw: float, bh: float) -> void:
|
||||||
|
draw_rect(Rect2(bx, by, bw, bh), Color(0.0, 0.04, 0.02, 0.95))
|
||||||
|
var bc := Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.35)
|
||||||
|
draw_line(Vector2(bx, by), Vector2(bx+bw, by), bc, 1.0)
|
||||||
|
draw_line(Vector2(bx, by+bh), Vector2(bx+bw, by+bh), bc, 1.0)
|
||||||
|
draw_line(Vector2(bx, by), Vector2(bx, by+bh), bc, 1.0)
|
||||||
|
draw_line(Vector2(bx+bw, by), Vector2(bx+bw, by+bh), bc, 1.0)
|
||||||
|
var cl := 12.0
|
||||||
|
for cx: float in [bx, bx+bw]:
|
||||||
|
for cy: float in [by, by+bh]:
|
||||||
|
var sx := 1.0 if cx == bx else -1.0
|
||||||
|
var sy := 1.0 if cy == by else -1.0
|
||||||
|
draw_line(Vector2(cx, cy), Vector2(cx + sx*cl, cy), COL_PRIMARY, 1.5)
|
||||||
|
draw_line(Vector2(cx, cy), Vector2(cx, cy + sy*cl), COL_PRIMARY, 1.5)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_brackets(x1: float, y1: float, x2: float, y2: float, col: Color, arm: float) -> void:
|
||||||
|
var bw := 1.5
|
||||||
|
draw_line(Vector2(x1, y1), Vector2(x1+arm, y1), col, bw)
|
||||||
|
draw_line(Vector2(x1, y1), Vector2(x1, y1+arm), col, bw)
|
||||||
|
draw_line(Vector2(x2, y1), Vector2(x2-arm, y1), col, bw)
|
||||||
|
draw_line(Vector2(x2, y1), Vector2(x2, y1+arm), col, bw)
|
||||||
|
draw_line(Vector2(x1, y2), Vector2(x1+arm, y2), col, bw)
|
||||||
|
draw_line(Vector2(x1, y2), Vector2(x1, y2-arm), col, bw)
|
||||||
|
draw_line(Vector2(x2, y2), Vector2(x2-arm, y2), col, bw)
|
||||||
|
draw_line(Vector2(x2, y2), Vector2(x2, y2-arm), col, bw)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_text(text: String, x: float, y: float, sz: int, col: Color) -> void:
|
||||||
|
draw_string(ThemeDB.fallback_font, Vector2(x, y + sz),
|
||||||
|
text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz, col)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_text_c(text: String, x: float, y: float, sz: int, col: Color) -> void:
|
||||||
|
var tw := _text_w(text, sz)
|
||||||
|
draw_string(ThemeDB.fallback_font, Vector2(x - tw * 0.5, y + sz),
|
||||||
|
text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz, col)
|
||||||
|
|
||||||
|
|
||||||
|
func _text_w(text: String, sz: int) -> float:
|
||||||
|
return ThemeDB.fallback_font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_wrapped(text: String, x: float, y: float, w: float, sz: int, col: Color) -> void:
|
||||||
|
var words := text.split(" ", false)
|
||||||
|
var line := ""
|
||||||
|
var row_y := y
|
||||||
|
for word: String in words:
|
||||||
|
var trial := (line + " " + word) if line != "" else word
|
||||||
|
if _text_w(trial, sz) > w and line != "":
|
||||||
|
_draw_text(line, x, row_y, sz, col)
|
||||||
|
row_y += float(sz) + 4.0
|
||||||
|
line = word
|
||||||
|
else:
|
||||||
|
line = trial
|
||||||
|
if line != "":
|
||||||
|
_draw_text(line, x, row_y, sz, col)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dbj3vbayteqo0
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
extends RefCounted
|
||||||
|
class_name BigWipe
|
||||||
|
|
||||||
|
enum Phase { INACTIVE, COLLAPSE, FLASH, DONE }
|
||||||
|
var phase: Phase = Phase.INACTIVE
|
||||||
|
var timer: float = 0.0
|
||||||
|
var flash_timer: float = 0.0
|
||||||
|
var p1_survived: bool = false
|
||||||
|
var p2_survived: bool = false
|
||||||
|
var p1_pressed: bool = false
|
||||||
|
var p2_pressed: bool = false
|
||||||
|
|
||||||
|
const COLLAPSE_DURATION := 140.0 / 60.0 # frames to seconds
|
||||||
|
const FLASH_DURATION := 24.0 / 60.0
|
||||||
|
const WIPE_BONUS := 500
|
||||||
|
|
||||||
|
signal wipe_complete(p1_survived, p2_survived)
|
||||||
|
|
||||||
|
func start() -> void:
|
||||||
|
phase = Phase.COLLAPSE
|
||||||
|
timer = 0.0
|
||||||
|
p1_pressed = false
|
||||||
|
p2_pressed = false
|
||||||
|
p1_survived = false
|
||||||
|
p2_survived = false
|
||||||
|
|
||||||
|
func is_active() -> bool:
|
||||||
|
return phase != Phase.INACTIVE and phase != Phase.DONE
|
||||||
|
|
||||||
|
func collapse_progress() -> float:
|
||||||
|
return clamp(timer / COLLAPSE_DURATION, 0.0, 1.0)
|
||||||
|
|
||||||
|
func update(delta: float, is_multiplayer: bool) -> void:
|
||||||
|
match phase:
|
||||||
|
Phase.COLLAPSE:
|
||||||
|
timer += delta
|
||||||
|
if timer >= COLLAPSE_DURATION:
|
||||||
|
phase = Phase.FLASH
|
||||||
|
flash_timer = 0.0
|
||||||
|
p1_survived = p1_pressed
|
||||||
|
p2_survived = p2_pressed if is_multiplayer else false
|
||||||
|
Phase.FLASH:
|
||||||
|
flash_timer += delta
|
||||||
|
if flash_timer >= FLASH_DURATION:
|
||||||
|
phase = Phase.DONE
|
||||||
|
wipe_complete.emit(p1_survived, p2_survived)
|
||||||
|
|
||||||
|
func player_press_survive(player_idx: int) -> void:
|
||||||
|
if phase != Phase.COLLAPSE: return
|
||||||
|
if player_idx == 0: p1_pressed = true
|
||||||
|
else: p2_pressed = true
|
||||||
|
|
||||||
|
func draw_overlay(canvas: CanvasItem, world_w: float, world_h: float, _blink_t: float) -> void:
|
||||||
|
match phase:
|
||||||
|
Phase.FLASH:
|
||||||
|
var f := 1.0 - flash_timer / FLASH_DURATION
|
||||||
|
canvas.draw_rect(Rect2(0, 0, world_w, world_h), Color(1, 1, 1, f))
|
||||||
|
Phase.COLLAPSE:
|
||||||
|
var dim: float = collapse_progress() * 0.4
|
||||||
|
canvas.draw_rect(Rect2(0, 0, world_w, world_h), Color(0, 0, 0, dim))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://qe52wshn58wj
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
extends RefCounted
|
||||||
|
class_name BlackHole
|
||||||
|
|
||||||
|
const GRAVITY_STRENGTH := 0.8
|
||||||
|
# Original was 150px for ~1440px-wide window (10% of screen).
|
||||||
|
# Our canvas is 960px; 10% = 96px. Use ~160 for noticeable-but-fair gravity.
|
||||||
|
const PULL_RADIUS := 160.0
|
||||||
|
const SWALLOW_RADIUS := 14.0 # slightly smaller for fairer play
|
||||||
|
const MAX_BH_SPEED := 0.72 # 0.3 * 2.4
|
||||||
|
const DRIFT_PER_TICK := 0.012 # 0.005 * 2.4
|
||||||
|
const SUPERNOVA_AT := 30 # original value
|
||||||
|
const MAX_RADIUS := 60.0 # reasonable size at 960px
|
||||||
|
const FORCE_MULT := 45.0 # tuned: meaningful pull without instant death
|
||||||
|
const OBJ_MAX_VEL := 5.0 # velocity cap for attracted objects
|
||||||
|
|
||||||
|
# Hunting constants — slow drift, creates tension without instant death
|
||||||
|
const HUNT_DETECTION := 500.0 # roughly half-screen detection radius
|
||||||
|
const HUNT_ACCEL := 0.008 # very gentle acceleration toward player
|
||||||
|
const HUNT_MAX_SPEED := 0.38 # original 0.6 on 1440px → 0.6*(960/1440) = 0.4
|
||||||
|
const HUNT_LOSE_FRAMES := 180 # stops hunting if player out of range for 3s
|
||||||
|
const HUNT_CHANCE := 0.18
|
||||||
|
|
||||||
|
# SMBH lifespan — after SMBH_MAX_LIFE seconds it collapses
|
||||||
|
const SMBH_MAX_LIFE := 45.0
|
||||||
|
const SMBH_COLLAPSE_DURATION := 4.0
|
||||||
|
|
||||||
|
var x: float; var y: float
|
||||||
|
var vx: float = 0.0; var vy: float = 0.0
|
||||||
|
var radius: float = 12.0
|
||||||
|
var base_radius: float = 12.0
|
||||||
|
var pull_radius: float = PULL_RADIUS
|
||||||
|
var gravity: float = GRAVITY_STRENGTH
|
||||||
|
var consumed: int = 0
|
||||||
|
var pulse: float = 0.0
|
||||||
|
var dead: bool = false
|
||||||
|
var is_smbh: bool = false
|
||||||
|
var eject_timer: float = 0.0
|
||||||
|
|
||||||
|
var hunting: bool = false
|
||||||
|
var hunt_lost_timer: int = 0
|
||||||
|
|
||||||
|
var smbh_life: float = 0.0
|
||||||
|
var smbh_dying: bool = false
|
||||||
|
var smbh_collapse_timer: float = 0.0
|
||||||
|
|
||||||
|
var flash_intensity: float = 0.0
|
||||||
|
var flash_color: Color = Color(1.0, 1.0, 1.0, 0.0)
|
||||||
|
var accretion_flare: float = 0.0
|
||||||
|
|
||||||
|
func init(px: float, py: float, _mobile: bool = false) -> void:
|
||||||
|
x = px; y = py
|
||||||
|
var r := randf_range(6.0, 12.0) * 2.4 # 6..12 * 2.4
|
||||||
|
radius = r
|
||||||
|
base_radius = r
|
||||||
|
# Initial velocity: ±0.36 (original ±0.15 * 2.4)
|
||||||
|
vx = randf_range(-0.36, 0.36)
|
||||||
|
vy = randf_range(-0.36, 0.36)
|
||||||
|
hunting = randf() < HUNT_CHANCE
|
||||||
|
|
||||||
|
func become_smbh() -> void:
|
||||||
|
is_smbh = true
|
||||||
|
radius = randf_range(60.0, 90.0) # original 55-75 * ~1.4
|
||||||
|
base_radius = radius
|
||||||
|
pull_radius = min(PULL_RADIUS * 4.0, pull_radius * 2.5)
|
||||||
|
gravity = min(GRAVITY_STRENGTH * 4.0, gravity * 2.0)
|
||||||
|
|
||||||
|
func update(delta: float, players: Array, world_w: float, world_h: float) -> void:
|
||||||
|
pulse += delta * 2.0
|
||||||
|
eject_timer += delta
|
||||||
|
|
||||||
|
# Per-tick micro-drift — matches original rand(-0.005, 0.005) per frame * 2.4
|
||||||
|
vx += randf_range(-DRIFT_PER_TICK, DRIFT_PER_TICK)
|
||||||
|
vy += randf_range(-DRIFT_PER_TICK, DRIFT_PER_TICK)
|
||||||
|
|
||||||
|
# Hunting: 18% of BHs chase player within detection radius
|
||||||
|
if hunting and players.size() > 0:
|
||||||
|
var nearest_dist := INF
|
||||||
|
var nearest_p = null
|
||||||
|
for p in players:
|
||||||
|
if p.dead: continue
|
||||||
|
var dx: float = float(p.x) - x
|
||||||
|
var dy: float = float(p.y) - y
|
||||||
|
var d: float = sqrt(dx*dx + dy*dy)
|
||||||
|
if d < nearest_dist:
|
||||||
|
nearest_dist = d
|
||||||
|
nearest_p = p
|
||||||
|
if nearest_p != null and nearest_dist < HUNT_DETECTION and nearest_dist > 0.0:
|
||||||
|
var dx: float = float(nearest_p.x) - x
|
||||||
|
var dy: float = float(nearest_p.y) - y
|
||||||
|
var dist: float = sqrt(dx*dx + dy*dy)
|
||||||
|
vx += (dx/dist) * HUNT_ACCEL
|
||||||
|
vy += (dy/dist) * HUNT_ACCEL
|
||||||
|
var sp := sqrt(vx*vx + vy*vy)
|
||||||
|
if sp > HUNT_MAX_SPEED:
|
||||||
|
vx = vx/sp * HUNT_MAX_SPEED
|
||||||
|
vy = vy/sp * HUNT_MAX_SPEED
|
||||||
|
hunt_lost_timer = 0
|
||||||
|
else:
|
||||||
|
hunt_lost_timer += 1
|
||||||
|
if hunt_lost_timer >= HUNT_LOSE_FRAMES:
|
||||||
|
hunting = false
|
||||||
|
|
||||||
|
# SMBH lifespan — collapses after SMBH_MAX_LIFE seconds
|
||||||
|
if is_smbh and not dead:
|
||||||
|
smbh_life += delta
|
||||||
|
if smbh_life >= SMBH_MAX_LIFE and not smbh_dying:
|
||||||
|
smbh_dying = true
|
||||||
|
smbh_collapse_timer = SMBH_COLLAPSE_DURATION
|
||||||
|
if smbh_dying:
|
||||||
|
smbh_collapse_timer -= delta
|
||||||
|
radius = max(8.0, base_radius * (smbh_collapse_timer / SMBH_COLLAPSE_DURATION))
|
||||||
|
if smbh_collapse_timer <= 0.0:
|
||||||
|
dead = true
|
||||||
|
|
||||||
|
# Clamp wander speed
|
||||||
|
vx = clamp(vx, -MAX_BH_SPEED, MAX_BH_SPEED)
|
||||||
|
vy = clamp(vy, -MAX_BH_SPEED, MAX_BH_SPEED)
|
||||||
|
|
||||||
|
x += vx; y += vy
|
||||||
|
|
||||||
|
# Soft bounce off edges (5% / 95% margins like original)
|
||||||
|
if x < world_w * 0.05: vx = abs(vx)
|
||||||
|
elif x > world_w * 0.95: vx = -abs(vx)
|
||||||
|
if y < world_h * 0.05: vy = abs(vy)
|
||||||
|
elif y > world_h * 0.95: vy = -abs(vy)
|
||||||
|
|
||||||
|
# Consume flash/flare decay
|
||||||
|
if flash_intensity > 0.0:
|
||||||
|
flash_intensity = maxf(0.0, flash_intensity - delta * 3.0)
|
||||||
|
if accretion_flare > 0.0:
|
||||||
|
accretion_flare = maxf(0.0, accretion_flare - delta * 1.5)
|
||||||
|
|
||||||
|
func check_swallow(ox: float, oy: float) -> bool:
|
||||||
|
var dx := ox - x; var dy := oy - y
|
||||||
|
return sqrt(dx*dx + dy*dy) < SWALLOW_RADIUS + radius * 0.5
|
||||||
|
|
||||||
|
func apply_gravity(ox: float, oy: float, ovx: float, ovy: float) -> Vector2:
|
||||||
|
var dx := ox - x; var dy := oy - y
|
||||||
|
var dist := sqrt(dx*dx + dy*dy)
|
||||||
|
var nvx := ovx; var nvy := ovy
|
||||||
|
if dist < pull_radius and dist > 0.01:
|
||||||
|
var force := gravity / (dist * dist) * FORCE_MULT
|
||||||
|
nvx += (dx/dist) * -force
|
||||||
|
nvy += (dy/dist) * -force
|
||||||
|
return Vector2(nvx, nvy)
|
||||||
|
|
||||||
|
func on_swallow() -> bool:
|
||||||
|
consumed += 1
|
||||||
|
# Growth formula from original: radius = min(MAX, base + consumed * 0.4) * 2.4
|
||||||
|
radius = min(MAX_RADIUS, base_radius + consumed * 0.96) # 0.4 * 2.4
|
||||||
|
pull_radius = min(PULL_RADIUS * 2.0, PULL_RADIUS + consumed * 7.2) # 3 * 2.4
|
||||||
|
gravity = min(GRAVITY_STRENGTH * 2.0, GRAVITY_STRENGTH + consumed * 0.05)
|
||||||
|
if consumed >= SUPERNOVA_AT and not is_smbh:
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
func trigger_flash(col: Color, intensity: float = 1.0) -> void:
|
||||||
|
flash_intensity = intensity
|
||||||
|
flash_color = col
|
||||||
|
accretion_flare = intensity
|
||||||
|
|
||||||
|
func draw(canvas: CanvasItem) -> void:
|
||||||
|
var p := 0.6 + 0.4 * sin(pulse)
|
||||||
|
var cv := Vector2(x, y)
|
||||||
|
var ring_r := radius + 4.0 + 4.0 * sin(pulse)
|
||||||
|
var ring_col := Color(0.6, 0.2, 1.0, p * 0.9) if not is_smbh else Color(1.0, 0.4, 0.0, p)
|
||||||
|
|
||||||
|
# Outer glow halo — drawn first so core covers it
|
||||||
|
canvas.draw_circle(cv, radius + ring_r * 0.35, Color(ring_col.r, ring_col.g, ring_col.b, p * 0.08))
|
||||||
|
|
||||||
|
# Accretion disk: rotating segmented arcs — flare brightens + thickens on consume
|
||||||
|
var flare_mult: float = 1.0 + accretion_flare * 2.0
|
||||||
|
var disk_w: float = 2.5 + accretion_flare * 3.0
|
||||||
|
var disk_r := ring_r + 8.0
|
||||||
|
for seg in 6:
|
||||||
|
var sa := pulse * 0.55 + float(seg) / 6.0 * TAU
|
||||||
|
var arc_col := Color(ring_col.r, ring_col.g, ring_col.b, minf(ring_col.a * flare_mult, 1.0))
|
||||||
|
canvas.draw_arc(cv, ring_r, sa, sa + TAU / 6.0 * 0.75, 6, arc_col, disk_w)
|
||||||
|
for seg in 4:
|
||||||
|
var sa := -pulse * 0.3 + float(seg) / 4.0 * TAU
|
||||||
|
canvas.draw_arc(cv, disk_r, sa, sa + TAU / 4.0 * 0.6, 5,
|
||||||
|
Color(0.9, 0.5, 0.2, minf(p * 0.35 * flare_mult, 1.0)), disk_w * 0.6)
|
||||||
|
|
||||||
|
# Black core — single draw_circle replaces O(radius²) pixel loop
|
||||||
|
canvas.draw_circle(cv, radius, Color(0.0, 0.0, 0.0, 1.0))
|
||||||
|
|
||||||
|
# SMBH extra rings
|
||||||
|
if is_smbh:
|
||||||
|
for ri in 3:
|
||||||
|
var er := ring_r + float(ri + 1) * 10.0
|
||||||
|
var rot_dir := 1.0 if ri % 2 == 0 else -1.0
|
||||||
|
var sa := pulse * (0.35 + float(ri) * 0.1) * rot_dir
|
||||||
|
canvas.draw_arc(cv, er, sa, sa + TAU, 24,
|
||||||
|
Color(1.0, 0.6, 0.0, p * 0.3), 1.5)
|
||||||
|
|
||||||
|
# Consume flash — expanding ring + inner glow
|
||||||
|
if flash_intensity > 0.0:
|
||||||
|
var flash_r: float = radius + 10.0 + (1.0 - flash_intensity) * 25.0
|
||||||
|
canvas.draw_arc(cv, flash_r, 0.0, TAU, 24,
|
||||||
|
Color(flash_color.r, flash_color.g, flash_color.b, flash_intensity * 0.7), 2.5)
|
||||||
|
canvas.draw_circle(cv, radius + 4.0,
|
||||||
|
Color(flash_color.r, flash_color.g, flash_color.b, flash_intensity * 0.15))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dmqsqq8etgp7e
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
extends RefCounted
|
||||||
|
class_name BossShip
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# BossShip — Mini-Boss "WRAITH" (Welle 5) & Boss "LEVIATHAN" (Welle 8)
|
||||||
|
# Orbitet elliptisch um die Bildschirmmitte, feuert Spread-Geschosse.
|
||||||
|
# Wird von game_world.gd verwaltet (update/draw/take_hit).
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
var x: float = 0.0
|
||||||
|
var y: float = 0.0
|
||||||
|
var heading: float = 0.0
|
||||||
|
var orbit_angle: float = 0.0
|
||||||
|
var hp: int = 20
|
||||||
|
var max_hp: int = 20
|
||||||
|
var dead: bool = false
|
||||||
|
var fire_timer: int = 0
|
||||||
|
var is_miniboss: bool = true # false = Boss
|
||||||
|
var phase: int = 1 # 1 oder 2 (nur Boss)
|
||||||
|
var _phase2_triggered: bool = false
|
||||||
|
var _just_entered_phase2: bool = false
|
||||||
|
var pixel_size: int = 5 # Miniboss=5, Boss=7
|
||||||
|
|
||||||
|
const HULL_PIXELS: Array = [
|
||||||
|
[6, 0, "nose"],
|
||||||
|
[4, -2, "mid"], [4, 2, "mid"],
|
||||||
|
[2, 0, "dim"],
|
||||||
|
[0, -4, "accent"], [0, 4, "accent"],
|
||||||
|
[0, 0, "bright"],
|
||||||
|
[-2, -2, "dim"], [-2, 2, "dim"],
|
||||||
|
[-4, -4, "shadow"], [-4, 4, "shadow"],
|
||||||
|
[-4, 0, "edge"],
|
||||||
|
]
|
||||||
|
|
||||||
|
# Mini-Boss WRAITH: Magenta/Lila
|
||||||
|
const PAL_MINI: Dictionary = {
|
||||||
|
"nose": Color(1.0, 0.15, 0.9, 1.0),
|
||||||
|
"bright": Color(1.0, 0.5, 1.0, 1.0),
|
||||||
|
"mid": Color(0.75, 0.05, 0.7, 1.0),
|
||||||
|
"dim": Color(0.45, 0.02, 0.38, 1.0),
|
||||||
|
"accent": Color(1.0, 0.0, 0.85, 1.0),
|
||||||
|
"edge": Color(0.28, 0.0, 0.28, 1.0),
|
||||||
|
"shadow": Color(0.12, 0.0, 0.12, 1.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Boss LEVIATHAN: Feuer-Orange
|
||||||
|
const PAL_BOSS: Dictionary = {
|
||||||
|
"nose": Color(1.0, 0.65, 0.0, 1.0),
|
||||||
|
"bright": Color(1.0, 0.9, 0.25, 1.0),
|
||||||
|
"mid": Color(0.8, 0.38, 0.0, 1.0),
|
||||||
|
"dim": Color(0.5, 0.18, 0.0, 1.0),
|
||||||
|
"accent": Color(1.0, 0.72, 0.0, 1.0),
|
||||||
|
"edge": Color(0.28, 0.08, 0.0, 1.0),
|
||||||
|
"shadow": Color(0.12, 0.03, 0.0, 1.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Initialisierung ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func init_miniboss(cx: float, cy: float) -> void:
|
||||||
|
x = cx
|
||||||
|
y = cy - 120.0
|
||||||
|
orbit_angle = -PI * 0.5 # startet oben
|
||||||
|
hp = 20; max_hp = 20
|
||||||
|
is_miniboss = true
|
||||||
|
pixel_size = 5
|
||||||
|
fire_timer = 25 # kurze Verzögerung vor dem ersten Schuss
|
||||||
|
|
||||||
|
func init_boss(cx: float, cy: float) -> void:
|
||||||
|
x = cx
|
||||||
|
y = cy - 140.0
|
||||||
|
orbit_angle = -PI * 0.5
|
||||||
|
hp = 50; max_hp = 50
|
||||||
|
is_miniboss = false
|
||||||
|
phase = 1
|
||||||
|
pixel_size = 7
|
||||||
|
fire_timer = 25
|
||||||
|
|
||||||
|
# ── Update ─────────────────────────────────────────────────────────────────
|
||||||
|
# Gibt Array[Bullet] zurück. Wird von game_world._tick() pro Frame aufgerufen.
|
||||||
|
|
||||||
|
func update(delta: float, cx: float, cy: float) -> Array:
|
||||||
|
if dead:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Orbitalbewegung (elliptisch, um Spannung zu erzeugen)
|
||||||
|
var orbit_speed: float
|
||||||
|
if is_miniboss:
|
||||||
|
orbit_speed = 0.55
|
||||||
|
elif phase == 1:
|
||||||
|
orbit_speed = 0.44
|
||||||
|
else:
|
||||||
|
orbit_speed = 0.72 # Phase 2 schneller → gefährlicher
|
||||||
|
|
||||||
|
orbit_angle += orbit_speed * delta
|
||||||
|
var orbit_r: float = 185.0 if is_miniboss else 215.0
|
||||||
|
x = cx + cos(orbit_angle) * orbit_r
|
||||||
|
y = cy + sin(orbit_angle) * orbit_r * 0.58 # leicht elliptisch
|
||||||
|
|
||||||
|
# Heading zeigt immer zur Bildschirmmitte (schießt nach innen)
|
||||||
|
heading = atan2(cy - y, cx - x)
|
||||||
|
|
||||||
|
# Feuer-Intervall
|
||||||
|
fire_timer += 1
|
||||||
|
var fire_interval: int
|
||||||
|
if is_miniboss:
|
||||||
|
fire_interval = 72
|
||||||
|
elif phase == 1:
|
||||||
|
fire_interval = 60
|
||||||
|
else:
|
||||||
|
fire_interval = 42
|
||||||
|
|
||||||
|
var result: Array = []
|
||||||
|
if fire_timer >= fire_interval:
|
||||||
|
fire_timer = 0
|
||||||
|
result = _fire()
|
||||||
|
|
||||||
|
# Phase-2-Übergang (Boss, < 50% HP)
|
||||||
|
if not is_miniboss and not _phase2_triggered and hp <= max_hp / 2:
|
||||||
|
phase = 2
|
||||||
|
_phase2_triggered = true
|
||||||
|
_just_entered_phase2 = true
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
func _fire() -> Array:
|
||||||
|
var ways: int
|
||||||
|
if is_miniboss:
|
||||||
|
ways = 3
|
||||||
|
elif phase == 1:
|
||||||
|
ways = 5
|
||||||
|
else:
|
||||||
|
ways = 8
|
||||||
|
|
||||||
|
var bullet_speed: float = 4.2 if is_miniboss else 5.0
|
||||||
|
var spread: float = PI / float(ways + 2)
|
||||||
|
var bullets: Array = []
|
||||||
|
|
||||||
|
for i in ways:
|
||||||
|
var angle: float = heading + (float(i) - float(ways - 1) * 0.5) * spread
|
||||||
|
var b := Bullet.new()
|
||||||
|
b.init(x, y, angle, "boss")
|
||||||
|
# Geschwindigkeit überschreiben (langsamer als Spielerkugeln für Ausweichen)
|
||||||
|
b.vx = cos(angle) * bullet_speed
|
||||||
|
b.vy = sin(angle) * bullet_speed
|
||||||
|
if is_miniboss:
|
||||||
|
b.color = Color(1.0, 0.15, 0.9, 1.0) # Magenta
|
||||||
|
elif phase == 1:
|
||||||
|
b.color = Color(1.0, 0.5, 0.0, 1.0) # Orange
|
||||||
|
else:
|
||||||
|
b.color = Color(1.0, 0.1, 0.0, 1.0) # Dunkelrot
|
||||||
|
bullets.append(b)
|
||||||
|
|
||||||
|
return bullets
|
||||||
|
|
||||||
|
func take_hit() -> void:
|
||||||
|
hp -= 1
|
||||||
|
if hp <= 0:
|
||||||
|
hp = 0
|
||||||
|
dead = true
|
||||||
|
|
||||||
|
# ── Zeichnen ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func draw(canvas: CanvasItem) -> void:
|
||||||
|
if dead:
|
||||||
|
return
|
||||||
|
|
||||||
|
var pal: Dictionary = PAL_MINI if is_miniboss else PAL_BOSS
|
||||||
|
var ps: float = float(pixel_size)
|
||||||
|
var hs: float = ps * 0.5
|
||||||
|
var ch: float = cos(heading)
|
||||||
|
var sh: float = sin(heading)
|
||||||
|
|
||||||
|
# Rumpf — gleiche Offset-Tabelle wie EnemyShip, skaliert mit pixel_size
|
||||||
|
for pix: Array in HULL_PIXELS:
|
||||||
|
var lx: float = float(pix[0]) * ps
|
||||||
|
var ly: float = float(pix[1]) * ps
|
||||||
|
var wx: float = x + lx * ch - ly * sh
|
||||||
|
var wy: float = y + lx * sh + ly * ch
|
||||||
|
canvas.draw_rect(Rect2(wx - hs, wy - hs, ps, ps), pal[pix[2]])
|
||||||
|
|
||||||
|
# Thruster-Glühen (hinter dem Schiff)
|
||||||
|
var glow_x: float = x + cos(heading + PI) * (ps * 4.2)
|
||||||
|
var glow_y: float = y + sin(heading + PI) * (ps * 4.2)
|
||||||
|
var glow_r: float = ps + 2.0
|
||||||
|
var pulse: float = 0.48 + 0.38 * sin(float(Time.get_ticks_msec()) * 0.012)
|
||||||
|
var glow_col: Color
|
||||||
|
if is_miniboss:
|
||||||
|
glow_col = Color(1.0, 0.0, 0.85, pulse)
|
||||||
|
elif phase == 1:
|
||||||
|
glow_col = Color(1.0, 0.5, 0.0, pulse)
|
||||||
|
else:
|
||||||
|
glow_col = Color(1.0, 0.15, 0.0, pulse + 0.15) # Phase 2 heller
|
||||||
|
canvas.draw_circle(Vector2(glow_x, glow_y), glow_r, glow_col)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://r3q2arux2t1n
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
extends RefCounted
|
||||||
|
class_name Bullet
|
||||||
|
|
||||||
|
const BULLET_SPEED := 9.6
|
||||||
|
const MAX_LIFE := 180
|
||||||
|
const FADE_START := 150
|
||||||
|
const HIT_RADIUS := 9.6
|
||||||
|
|
||||||
|
var x: float; var y: float
|
||||||
|
var vx: float; var vy: float
|
||||||
|
var life: int = 0
|
||||||
|
var owner_type: String
|
||||||
|
var color: Color
|
||||||
|
var dead: bool = false
|
||||||
|
var effective_hit_radius: float = HIT_RADIUS
|
||||||
|
var pierce: bool = false
|
||||||
|
var pierce_hits: int = 0
|
||||||
|
var style: String = "default" # set by spaceship based on equipped weapon
|
||||||
|
|
||||||
|
func init(px: float, py: float, heading: float, otype: String) -> void:
|
||||||
|
x = px; y = py
|
||||||
|
vx = cos(heading) * BULLET_SPEED
|
||||||
|
vy = sin(heading) * BULLET_SPEED
|
||||||
|
owner_type = otype
|
||||||
|
match otype:
|
||||||
|
"p1": color = Color("#88aaff")
|
||||||
|
"p2": color = Color("#44ffaa")
|
||||||
|
"enemy": color = Color("#ff4444")
|
||||||
|
_: color = Color.WHITE
|
||||||
|
|
||||||
|
func update(world_w: float, world_h: float) -> void:
|
||||||
|
x += vx; y += vy
|
||||||
|
life += 1
|
||||||
|
if x < 0: x += world_w
|
||||||
|
elif x > world_w: x -= world_w
|
||||||
|
if y < 0: y += world_h
|
||||||
|
elif y > world_h: y -= world_h
|
||||||
|
if life >= MAX_LIFE: dead = true
|
||||||
|
|
||||||
|
func get_alpha() -> float:
|
||||||
|
if life >= FADE_START:
|
||||||
|
return 1.0 - float(life - FADE_START) / float(MAX_LIFE - FADE_START)
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
func draw(canvas: CanvasItem) -> void:
|
||||||
|
var a := get_alpha()
|
||||||
|
match style:
|
||||||
|
|
||||||
|
"plasma":
|
||||||
|
# Strahl — dicke Linie entlang der Flugrichtung, orange-gelb
|
||||||
|
var angle := atan2(vy, vx)
|
||||||
|
var len := 26.0
|
||||||
|
var ca := cos(angle); var sa := sin(angle)
|
||||||
|
var bx := x - ca * len * 0.6; var by := y - sa * len * 0.6
|
||||||
|
var ex := x + ca * len * 0.4; var ey := y + sa * len * 0.4
|
||||||
|
canvas.draw_line(Vector2(bx, by), Vector2(ex, ey), Color(1.0, 0.45, 0.05, a * 0.45), 5.0)
|
||||||
|
canvas.draw_line(Vector2(bx, by), Vector2(ex, ey), Color(1.0, 0.75, 0.15, a), 2.0)
|
||||||
|
canvas.draw_rect(Rect2(x - 1, y - 1, 3, 3), Color(1.0, 1.0, 0.7, a))
|
||||||
|
|
||||||
|
"laser":
|
||||||
|
# Dünner schneller Strahl, cyan-weiß
|
||||||
|
var angle := atan2(vy, vx)
|
||||||
|
var len := 22.0
|
||||||
|
var ca := cos(angle); var sa := sin(angle)
|
||||||
|
var bx := x - ca * len; var by := y - sa * len
|
||||||
|
canvas.draw_line(Vector2(bx, by), Vector2(x, y), Color(0.3, 0.9, 1.0, a * 0.4), 3.0)
|
||||||
|
canvas.draw_line(Vector2(bx, by), Vector2(x, y), Color(0.8, 1.0, 1.0, a), 1.0)
|
||||||
|
canvas.draw_rect(Rect2(x - 1, y - 1, 2, 2), Color(1.0, 1.0, 1.0, a))
|
||||||
|
|
||||||
|
"rail":
|
||||||
|
# Lange helle Nadel mit blauem Schweif
|
||||||
|
var angle := atan2(vy, vx)
|
||||||
|
var len := 32.0
|
||||||
|
var ca := cos(angle); var sa := sin(angle)
|
||||||
|
var bx := x - ca * len; var by := y - sa * len
|
||||||
|
canvas.draw_line(Vector2(bx, by), Vector2(x, y), Color(0.4, 0.7, 1.0, a * 0.35), 4.0)
|
||||||
|
canvas.draw_line(Vector2(bx, by), Vector2(x, y), Color(0.9, 0.98, 1.0, a), 1.0)
|
||||||
|
canvas.draw_rect(Rect2(x - 1, y - 1, 2, 2), Color(1.0, 1.0, 1.0, a))
|
||||||
|
|
||||||
|
"sniper":
|
||||||
|
# Mittellange scharfe Nadel, strahlend weiß
|
||||||
|
var angle := atan2(vy, vx)
|
||||||
|
var len := 18.0
|
||||||
|
var ca := cos(angle); var sa := sin(angle)
|
||||||
|
var bx := x - ca * len; var by := y - sa * len
|
||||||
|
canvas.draw_line(Vector2(bx, by), Vector2(x, y), Color(1.0, 0.95, 0.8, a * 0.5), 2.0)
|
||||||
|
canvas.draw_line(Vector2(bx, by), Vector2(x, y), Color(1.0, 1.0, 1.0, a), 1.0)
|
||||||
|
|
||||||
|
"ion":
|
||||||
|
# Leuchtender türkiser Orb
|
||||||
|
canvas.draw_circle(Vector2(x, y), 6.0, Color(0.2, 0.9, 0.85, a * 0.3))
|
||||||
|
canvas.draw_circle(Vector2(x, y), 3.5, Color(0.4, 1.0, 0.95, a * 0.75))
|
||||||
|
canvas.draw_rect(Rect2(x - 1, y - 1, 3, 3), Color(0.9, 1.0, 1.0, a))
|
||||||
|
|
||||||
|
"scatter":
|
||||||
|
# Kleine schnelle Schrotkügelchen, orange
|
||||||
|
canvas.draw_rect(Rect2(x - 1, y - 1, 3, 3), Color(1.0, 0.65, 0.2, a * 0.5))
|
||||||
|
canvas.draw_rect(Rect2(x, y, 2, 2), Color(1.0, 0.85, 0.5, a))
|
||||||
|
|
||||||
|
"burst":
|
||||||
|
# Winzige schnelle Punkte, hellblau
|
||||||
|
canvas.draw_rect(Rect2(x - 1, y - 1, 3, 3), Color(0.5, 0.85, 1.0, a * 0.5))
|
||||||
|
canvas.draw_rect(Rect2(x, y, 1, 1), Color(1.0, 1.0, 1.0, a))
|
||||||
|
|
||||||
|
"charge":
|
||||||
|
# Großer pulsierender Orb — Radius skaliert mit effective_hit_radius
|
||||||
|
var r: float = clamp(effective_hit_radius * 0.55, 4.0, 20.0)
|
||||||
|
canvas.draw_circle(Vector2(x, y), r + 4.0, Color(1.0, 0.85, 0.2, a * 0.2))
|
||||||
|
canvas.draw_circle(Vector2(x, y), r + 1.5, Color(1.0, 0.95, 0.4, a * 0.45))
|
||||||
|
canvas.draw_circle(Vector2(x, y), r, Color(1.0, 1.0, 0.75, a * 0.85))
|
||||||
|
canvas.draw_circle(Vector2(x, y), r * 0.45, Color(1.0, 1.0, 1.0, a))
|
||||||
|
|
||||||
|
_: # "default" — klassischer Dot
|
||||||
|
canvas.draw_rect(Rect2(x - 2, y - 2, 5, 5), Color(color.r, color.g, color.b, a * 0.5))
|
||||||
|
canvas.draw_rect(Rect2(x - 1, y - 1, 3, 3), Color(color.r, color.g, color.b, a))
|
||||||
|
canvas.draw_rect(Rect2(x, y, 1, 1), Color(1, 1, 1, a * 0.9))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://6a8ufydt7pd8
|
||||||
@@ -0,0 +1,590 @@
|
|||||||
|
extends RefCounted
|
||||||
|
class_name CosmicObjects
|
||||||
|
|
||||||
|
# Planet lives in its own file now; keep CosmicObjects.Planet working.
|
||||||
|
const Planet = preload("res://scripts/planet.gd")
|
||||||
|
|
||||||
|
# ─── Star ─────────────────────────────────────────────────────────────────────
|
||||||
|
class Star extends RefCounted:
|
||||||
|
var x: float; var y: float; var speed: float; var size: float
|
||||||
|
var color: Color; var alpha: float = 1.0; var glow: bool = false
|
||||||
|
var twinkle_phase: float; var twinkle_speed: float; var dead: bool = false
|
||||||
|
var rotation_angle: float = 0.0
|
||||||
|
var is_antimatter: bool = false
|
||||||
|
var life: float = 0.0; var max_life: float = 0.0
|
||||||
|
# Gravity-induced drift (vx/vy from BH attraction, like original)
|
||||||
|
var grav_vx: float = 0.0; var grav_vy: float = 0.0
|
||||||
|
# Spiral-into-BH state
|
||||||
|
var is_spiraling: bool = false
|
||||||
|
var spiral_timer: float = 0.0
|
||||||
|
var spiral_bh_x: float = 0.0; var spiral_bh_y: float = 0.0
|
||||||
|
var spiral_angle: float = 0.0
|
||||||
|
var spiral_dist: float = 0.0; var spiral_initial_dist: float = 0.0
|
||||||
|
var trail_points: Array = []
|
||||||
|
var original_color: Color = Color.WHITE
|
||||||
|
const SPIRAL_DURATION := 0.8
|
||||||
|
const TRAIL_MAX := 16
|
||||||
|
|
||||||
|
func init(px: float, py: float, _w: float, _h: float) -> void:
|
||||||
|
x = px; y = py
|
||||||
|
# Three tiers (matching original): supergiant / bright / faint
|
||||||
|
var tier := randf()
|
||||||
|
if tier < 0.05: # supergiant
|
||||||
|
size = randf_range(5.0, 7.0) * 2.4 / 4.0 # scale size
|
||||||
|
alpha = 0.85 + randf() * 0.15
|
||||||
|
glow = true
|
||||||
|
elif tier < 0.20: # bright main-sequence
|
||||||
|
size = randf_range(3.0, 4.0) * 2.4 / 4.0
|
||||||
|
alpha = 0.65 + randf() * 0.3
|
||||||
|
glow = true
|
||||||
|
else: # faint background
|
||||||
|
size = 1.0 if randf() > 0.4 else 2.0
|
||||||
|
alpha = 0.2 + randf() * 0.55
|
||||||
|
glow = false
|
||||||
|
speed = randf_range(0.05, 0.3) * 2.4 # original 0.05..0.3 * scale
|
||||||
|
twinkle_phase = randf() * TAU
|
||||||
|
twinkle_speed = randf_range(0.01, 0.03) * 60.0 # per-frame → per-second
|
||||||
|
var star_colors := ["#ffffff","#ffe8a0","#a0c4ff","#ffc0c0","#c0ffc0"]
|
||||||
|
color = Color(star_colors[randi() % star_colors.size()])
|
||||||
|
is_antimatter = randf() < 0.10
|
||||||
|
if is_antimatter:
|
||||||
|
color = Color("#ff88ee"); max_life = randf_range(8.0, 18.0)
|
||||||
|
|
||||||
|
func start_spiral(bh_x: float, bh_y: float) -> void:
|
||||||
|
is_spiraling = true
|
||||||
|
spiral_timer = 0.0
|
||||||
|
spiral_bh_x = bh_x; spiral_bh_y = bh_y
|
||||||
|
var dx := x - bh_x; var dy := y - bh_y
|
||||||
|
spiral_dist = sqrt(dx * dx + dy * dy)
|
||||||
|
spiral_initial_dist = maxf(spiral_dist, 1.0)
|
||||||
|
spiral_angle = atan2(dy, dx)
|
||||||
|
original_color = color
|
||||||
|
trail_points.clear()
|
||||||
|
|
||||||
|
func update(delta: float, world_w: float, world_h: float) -> bool:
|
||||||
|
if is_spiraling:
|
||||||
|
spiral_timer += delta
|
||||||
|
var t: float = clampf(spiral_timer / SPIRAL_DURATION, 0.0, 1.0)
|
||||||
|
# Radius shrinks quadratically — tightening orbit
|
||||||
|
spiral_dist = spiral_initial_dist * (1.0 - t * t)
|
||||||
|
# Angular speed increases (Kepler-like feel)
|
||||||
|
var angular_speed: float = 6.0 + 14.0 * t * t
|
||||||
|
spiral_angle += angular_speed * delta
|
||||||
|
# Position on orbit
|
||||||
|
x = spiral_bh_x + cos(spiral_angle) * spiral_dist
|
||||||
|
y = spiral_bh_y + sin(spiral_angle) * spiral_dist
|
||||||
|
# Light trail
|
||||||
|
trail_points.push_front(Vector2(x, y))
|
||||||
|
if trail_points.size() > TRAIL_MAX: trail_points.pop_back()
|
||||||
|
# Blueshift — color lerps toward blue-white
|
||||||
|
color = original_color.lerp(Color(0.6, 0.7, 1.0), clampf(t * 1.5, 0.0, 1.0))
|
||||||
|
# Alpha fades only in final 30%
|
||||||
|
if t > 0.7:
|
||||||
|
alpha = maxf(0.0, 1.0 - (t - 0.7) / 0.3)
|
||||||
|
# Spaghettification — star shrinks
|
||||||
|
size = maxf(0.5, size * (1.0 - delta * 2.0))
|
||||||
|
if spiral_timer >= SPIRAL_DURATION:
|
||||||
|
dead = true
|
||||||
|
return false
|
||||||
|
rotation_angle += twinkle_speed * delta * 0.07
|
||||||
|
twinkle_phase += twinkle_speed * delta
|
||||||
|
var flicker := sin(twinkle_phase) * 0.25
|
||||||
|
alpha = max(0.05, min(1.0, alpha + flicker * delta * 3.0))
|
||||||
|
# Upward drift + wavy horizontal (matching original: x += sin(y*0.02)*0.15)
|
||||||
|
y -= speed
|
||||||
|
x += grav_vx + sin(y * 0.0085) * 0.36 # 0.02/2.4 freq, 0.15*2.4 amplitude
|
||||||
|
y += grav_vy
|
||||||
|
if y < -10.0: y += world_h + 10.0
|
||||||
|
if x < -10.0: x += world_w + 10.0
|
||||||
|
elif x > world_w + 10.0: x -= world_w + 10.0
|
||||||
|
if is_antimatter:
|
||||||
|
life += delta
|
||||||
|
if life >= max_life: return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
func draw(canvas: CanvasItem) -> void:
|
||||||
|
# Light trail when spiraling into BH
|
||||||
|
if is_spiraling and trail_points.size() > 1:
|
||||||
|
for i in range(trail_points.size() - 1):
|
||||||
|
var fade: float = 1.0 - float(i) / float(TRAIL_MAX)
|
||||||
|
var tc := Color(color.r, color.g, color.b, alpha * fade * 0.6)
|
||||||
|
canvas.draw_line(trail_points[i], trail_points[i + 1], tc,
|
||||||
|
maxf(1.0, size * 0.5 * fade))
|
||||||
|
var c := color
|
||||||
|
if glow:
|
||||||
|
# Soft halo around bright stars
|
||||||
|
var halo_r := size + 2.0
|
||||||
|
canvas.draw_rect(Rect2(x - halo_r, y - halo_r, halo_r*2.0+1.0, halo_r*2.0+1.0),
|
||||||
|
Color(c.r, c.g, c.b, alpha * 0.18))
|
||||||
|
# Rotating cross flare for large stars
|
||||||
|
if size >= 4.0:
|
||||||
|
var fl := size + 4.0
|
||||||
|
for arm_i in 4:
|
||||||
|
var arm_a := rotation_angle + float(arm_i) * PI * 0.5
|
||||||
|
for dist in range(1, int(fl) + 1):
|
||||||
|
var ax := x + cos(arm_a) * dist
|
||||||
|
var ay := y + sin(arm_a) * dist
|
||||||
|
var fade := 1.0 - float(dist) / fl
|
||||||
|
canvas.draw_rect(Rect2(ax, ay, 1, 1), Color(c.r, c.g, c.b, alpha * 0.5 * fade))
|
||||||
|
var sz: float = maxf(1.0, size)
|
||||||
|
canvas.draw_rect(Rect2(x, y, sz, sz), Color(c.r, c.g, c.b, alpha))
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Comet ────────────────────────────────────────────────────────────────────
|
||||||
|
class Comet extends RefCounted:
|
||||||
|
var x: float; var y: float; var vx: float; var vy: float
|
||||||
|
var alpha: float = 1.0; var dead: bool = false
|
||||||
|
var trail_pts: Array = []
|
||||||
|
const TRAIL_MAX := 24
|
||||||
|
|
||||||
|
func init(world_w: float, world_h: float) -> void:
|
||||||
|
var side := randi() % 4
|
||||||
|
match side:
|
||||||
|
0: x = randf()*world_w; y = 0; vx = randf_range(-3.0,3.0); vy = randf_range(1.0,4.0)
|
||||||
|
1: x = randf()*world_w; y = world_h; vx = randf_range(-3.0,3.0); vy = randf_range(-4.0,-1.0)
|
||||||
|
2: x = 0; y = randf()*world_h; vx = randf_range(1.0,4.0); vy = randf_range(-3.0,3.0)
|
||||||
|
3: x = world_w; y = randf()*world_h; vx = randf_range(-4.0,-1.0); vy = randf_range(-3.0,3.0)
|
||||||
|
alpha = 1.0
|
||||||
|
|
||||||
|
func update(world_w: float, world_h: float) -> void:
|
||||||
|
trail_pts.push_front(Vector2(x, y))
|
||||||
|
if trail_pts.size() > TRAIL_MAX: trail_pts.pop_back()
|
||||||
|
x += vx; y += vy
|
||||||
|
if x < -40 or x > world_w+40 or y < -40 or y > world_h+40:
|
||||||
|
dead = true
|
||||||
|
|
||||||
|
func draw(canvas: CanvasItem) -> void:
|
||||||
|
for i in trail_pts.size():
|
||||||
|
var t := 1.0 - float(i) / float(TRAIL_MAX)
|
||||||
|
var c := Color(1.0, 1.0, 1.0, t * 0.85)
|
||||||
|
canvas.draw_rect(Rect2(trail_pts[i].x, trail_pts[i].y, 2, 2), c)
|
||||||
|
canvas.draw_rect(Rect2(x-2, y-2, 5, 5), Color(1,1,1,alpha))
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Nebula ───────────────────────────────────────────────────────────────────
|
||||||
|
class Nebula extends RefCounted:
|
||||||
|
var x: float; var y: float; var radius: float; var color: Color
|
||||||
|
var vx: float; var vy: float
|
||||||
|
|
||||||
|
func init(world_w: float, world_h: float) -> void:
|
||||||
|
x = randf() * world_w; y = randf() * world_h
|
||||||
|
radius = randf_range(80.0, 160.0)
|
||||||
|
var neb_colors := ["#6633cc80","#cc336680","#33669980","#66993380"]
|
||||||
|
color = Color(neb_colors[randi() % neb_colors.size()])
|
||||||
|
vx = randf_range(-0.2, 0.2); vy = randf_range(-0.2, 0.2)
|
||||||
|
|
||||||
|
func update(world_w: float, world_h: float) -> void:
|
||||||
|
x += vx; y += vy
|
||||||
|
if x < -radius: x += world_w + radius*2
|
||||||
|
elif x > world_w+radius: x -= world_w + radius*2
|
||||||
|
if y < -radius: y += world_h + radius*2
|
||||||
|
elif y > world_h+radius: y -= world_h + radius*2
|
||||||
|
|
||||||
|
func draw(canvas: CanvasItem) -> void:
|
||||||
|
var ir := int(radius)
|
||||||
|
for py in range(-ir, ir+1, 8):
|
||||||
|
for px in range(-ir, ir+1, 8):
|
||||||
|
var dist := sqrt(float(px*px + py*py))
|
||||||
|
if dist <= radius:
|
||||||
|
var fade := 1.0 - dist / radius
|
||||||
|
fade = floor(fade * 4.0) / 4.0
|
||||||
|
if fade <= 0.0: continue
|
||||||
|
var c := Color(color.r, color.g, color.b, color.a * fade * 0.45)
|
||||||
|
canvas.draw_rect(Rect2(x+px, y+py, 8, 8), c)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Quasar ───────────────────────────────────────────────────────────────────
|
||||||
|
class Quasar extends RefCounted:
|
||||||
|
var x: float; var y: float; var pulse: float = 0.0
|
||||||
|
var jet_angle: float; var dead: bool = false
|
||||||
|
var life: float = 0.0; const MAX_LIFE := 30.0
|
||||||
|
var boost_sound_cd: float = 0.0
|
||||||
|
var radius: float = 22.0
|
||||||
|
|
||||||
|
const JET_LENGTH := 220.0
|
||||||
|
const JET_HALF_ANG := 0.25 # ~14° half-width — wide enough to feel generous
|
||||||
|
const JET_FORCE := 0.55 # added velocity per frame while inside beam
|
||||||
|
|
||||||
|
func init(px: float, py: float) -> void:
|
||||||
|
x = px; y = py
|
||||||
|
jet_angle = randf() * TAU
|
||||||
|
|
||||||
|
func update(delta: float) -> void:
|
||||||
|
pulse += delta * 3.0
|
||||||
|
jet_angle += delta * 0.4
|
||||||
|
life += delta
|
||||||
|
if boost_sound_cd > 0.0: boost_sound_cd = max(0.0, boost_sound_cd - delta)
|
||||||
|
if life >= MAX_LIFE: dead = true
|
||||||
|
|
||||||
|
func push_object(ox: float, oy: float, ovx: float, ovy: float) -> Vector2:
|
||||||
|
const PUSH_RADIUS := 150.0
|
||||||
|
const PUSH_STRENGTH := 0.45
|
||||||
|
var dx := ox - x; var dy := oy - y
|
||||||
|
var dist := sqrt(dx*dx + dy*dy)
|
||||||
|
if dist < PUSH_RADIUS and dist > 0.5:
|
||||||
|
var force := PUSH_STRENGTH / (dist * 0.05 + 1.0)
|
||||||
|
return Vector2(ovx + (dx/dist)*force, ovy + (dy/dist)*force)
|
||||||
|
return Vector2(ovx, ovy)
|
||||||
|
|
||||||
|
# Returns modified velocity if the position is inside one of the two jets.
|
||||||
|
func boost_if_in_jet(ox: float, oy: float, ovx: float, ovy: float) -> Vector2:
|
||||||
|
var dx := ox - x; var dy := oy - y
|
||||||
|
var dist := sqrt(dx*dx + dy*dy)
|
||||||
|
if dist > JET_LENGTH or dist < 4.0:
|
||||||
|
return Vector2(ovx, ovy)
|
||||||
|
var angle := atan2(dy, dx)
|
||||||
|
for jd: float in [0.0, PI]:
|
||||||
|
var ja := jet_angle + jd
|
||||||
|
var diff := fmod(abs(angle - ja) + PI, TAU) - PI
|
||||||
|
if abs(diff) < JET_HALF_ANG:
|
||||||
|
var fade := 1.0 - dist / JET_LENGTH
|
||||||
|
var force := JET_FORCE * (0.3 + 0.7 * fade)
|
||||||
|
return Vector2(ovx + cos(ja) * force, ovy + sin(ja) * force)
|
||||||
|
return Vector2(ovx, ovy)
|
||||||
|
|
||||||
|
func draw(canvas: CanvasItem) -> void:
|
||||||
|
var p := 0.7 + 0.3 * sin(pulse)
|
||||||
|
var cv := Vector2(x, y)
|
||||||
|
var R := radius
|
||||||
|
|
||||||
|
# Luminosity halos — quasars outshine entire galaxies
|
||||||
|
canvas.draw_circle(cv, R * 5.5, Color(1.0, 0.95, 0.4, 0.025))
|
||||||
|
canvas.draw_circle(cv, R * 3.2, Color(1.0, 0.9, 0.3, 0.06 * p))
|
||||||
|
canvas.draw_circle(cv, R * 2.0, Color(1.0, 0.95, 0.6, 0.11 * p))
|
||||||
|
|
||||||
|
# Relativistic jets — the defining quasar feature; cyan beams in opposite directions
|
||||||
|
for jd: float in [0.0, PI]:
|
||||||
|
var ja := jet_angle + jd
|
||||||
|
for jl in range(6, 220, 4):
|
||||||
|
var fade := 1.0 - float(jl) / 220.0
|
||||||
|
var jx := x + cos(ja) * float(jl)
|
||||||
|
var jy := y + sin(ja) * float(jl)
|
||||||
|
canvas.draw_circle(Vector2(jx, jy), maxf(0.5, fade * 3.2),
|
||||||
|
Color(0.3, 0.85, 1.0, fade * p * 0.85))
|
||||||
|
|
||||||
|
# Accretion disk — much brighter and denser than SMBH (yellow/white palette)
|
||||||
|
var ring_r := R + 3.0 + 3.0 * sin(pulse)
|
||||||
|
for seg in 8:
|
||||||
|
var sa := pulse * 0.55 + float(seg) / 8.0 * TAU
|
||||||
|
canvas.draw_arc(cv, ring_r, sa, sa + TAU / 8.0 * 0.8, 6,
|
||||||
|
Color(1.0, 0.92, 0.3, minf(p * 0.95, 1.0)), 3.5)
|
||||||
|
for seg in 5:
|
||||||
|
var sa := -pulse * 0.4 + float(seg) / 5.0 * TAU
|
||||||
|
canvas.draw_arc(cv, ring_r + 10.0, sa, sa + TAU / 5.0 * 0.7, 5,
|
||||||
|
Color(1.0, 1.0, 0.75, p * 0.6), 2.0)
|
||||||
|
# Innermost corona ring
|
||||||
|
canvas.draw_arc(cv, ring_r - 5.0, 0.0, TAU, 24, Color(1.0, 1.0, 0.9, p * 0.45), 1.5)
|
||||||
|
|
||||||
|
# Black core (event horizon — same as BH/SMBH, links the visual family)
|
||||||
|
canvas.draw_circle(cv, R * 0.55, Color(0.0, 0.0, 0.0, 1.0))
|
||||||
|
# Inner corona glow
|
||||||
|
canvas.draw_circle(cv, R * 0.85, Color(1.0, 1.0, 0.8, 0.18 * p))
|
||||||
|
|
||||||
|
|
||||||
|
# ─── WhiteHole ────────────────────────────────────────────────────────────────
|
||||||
|
class WhiteHole extends RefCounted:
|
||||||
|
var x: float; var y: float; var radius: float = 16.0
|
||||||
|
var push_strength: float = 0.9; var push_radius: float = 340.0
|
||||||
|
var alpha: float = 1.0; var dead: bool = false
|
||||||
|
var pulse: float = 0.0; var life: float = 0.0
|
||||||
|
const MAX_LIFE := 60.0; const EJECT_INTERVAL := 3.0
|
||||||
|
var eject_timer: float = 0.0
|
||||||
|
|
||||||
|
func init(px: float, py: float) -> void:
|
||||||
|
x = px; y = py
|
||||||
|
|
||||||
|
func update(delta: float) -> Dictionary:
|
||||||
|
pulse += delta * 2.5
|
||||||
|
life += delta
|
||||||
|
eject_timer += delta
|
||||||
|
var new_stars: Array = []
|
||||||
|
var new_planets: Array = []
|
||||||
|
if eject_timer >= EJECT_INTERVAL:
|
||||||
|
eject_timer = 0.0
|
||||||
|
# Eject stars outward in all directions
|
||||||
|
for _i in randi_range(4, 8):
|
||||||
|
var s := Star.new()
|
||||||
|
var eject_angle := randf() * TAU
|
||||||
|
var eject_dist := randf_range(8.0, 20.0)
|
||||||
|
s.x = x + cos(eject_angle) * eject_dist
|
||||||
|
s.y = y + sin(eject_angle) * eject_dist
|
||||||
|
s.speed = randf_range(0.4, 1.5)
|
||||||
|
s.size = randf_range(1.0, 3.5)
|
||||||
|
s.alpha = 0.7 + randf() * 0.3
|
||||||
|
s.glow = s.size >= 2.5
|
||||||
|
s.color = Color("#aaddff")
|
||||||
|
s.twinkle_phase = randf() * TAU
|
||||||
|
s.twinkle_speed = randf_range(0.5, 2.0)
|
||||||
|
s.grav_vx = cos(eject_angle) * randf_range(0.3, 1.2)
|
||||||
|
s.grav_vy = sin(eject_angle) * randf_range(0.3, 1.2)
|
||||||
|
new_stars.append(s)
|
||||||
|
# Occasionally eject a planet
|
||||||
|
if randf() < 0.4:
|
||||||
|
var p := Planet.new()
|
||||||
|
p.init(x + randf_range(-25.0, 25.0), y + randf_range(-25.0, 25.0), 960.0, 600.0)
|
||||||
|
new_planets.append(p)
|
||||||
|
# Fade out and die after MAX_LIFE (unless a BH consumes it first)
|
||||||
|
if life > MAX_LIFE:
|
||||||
|
alpha = max(0.0, alpha - delta * 0.4)
|
||||||
|
if alpha <= 0.05: dead = true
|
||||||
|
return {"stars": new_stars, "planets": new_planets}
|
||||||
|
|
||||||
|
func push_object(ox: float, oy: float, ovx: float, ovy: float) -> Vector2:
|
||||||
|
var dx := ox - x; var dy := oy - y
|
||||||
|
var dist := sqrt(dx*dx + dy*dy)
|
||||||
|
if dist < push_radius and dist > 0.01:
|
||||||
|
var force := push_strength / (dist * 0.05 + 1.0)
|
||||||
|
return Vector2(ovx + (dx/dist)*force, ovy + (dy/dist)*force)
|
||||||
|
return Vector2(ovx, ovy)
|
||||||
|
|
||||||
|
func draw(canvas: CanvasItem) -> void:
|
||||||
|
var p := 0.7 + 0.3 * sin(pulse)
|
||||||
|
var cv := Vector2(x, y)
|
||||||
|
# Outer glow
|
||||||
|
canvas.draw_circle(cv, radius + 8.0, Color(0.5, 0.85, 1.0, alpha * 0.12))
|
||||||
|
# White core — single draw_circle replaces O(r²) pixel loop
|
||||||
|
canvas.draw_circle(cv, radius, Color(1.0, 1.0, 1.0, alpha * p))
|
||||||
|
# Pulsing ring — draw_arc replaces 48 individual draw_rects
|
||||||
|
var ring_r := radius + 6.0 + 4.0 * sin(pulse)
|
||||||
|
canvas.draw_arc(cv, ring_r, 0.0, TAU, 32, Color(0.6, 0.9, 1.0, alpha * 0.8), 2.0)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── NeutronStar ──────────────────────────────────────────────────────────────
|
||||||
|
class NeutronStar extends RefCounted:
|
||||||
|
var x: float; var y: float; var beam_angle: float = 0.0
|
||||||
|
var dead: bool = false; var life: float = 0.0; const MAX_LIFE := 40.0
|
||||||
|
const BEAM_HALF_WIDTH := 0.18; const BEAM_LENGTH := 440.0
|
||||||
|
const PULL_RADIUS := 120.0; const GRAVITY := 0.6
|
||||||
|
|
||||||
|
func init(px: float, py: float) -> void:
|
||||||
|
x = px; y = py; beam_angle = randf() * TAU
|
||||||
|
|
||||||
|
func update(delta: float) -> void:
|
||||||
|
beam_angle += delta * 1.8
|
||||||
|
life += delta
|
||||||
|
if life > MAX_LIFE: dead = true
|
||||||
|
|
||||||
|
func in_beam(ox: float, oy: float) -> bool:
|
||||||
|
var dx := ox - x; var dy := oy - y
|
||||||
|
var angle := atan2(dy, dx)
|
||||||
|
var diff := fmod(abs(angle - beam_angle) + PI, TAU) - PI
|
||||||
|
var diff2 := fmod(abs(angle - beam_angle - PI) + PI, TAU) - PI
|
||||||
|
return abs(diff) < BEAM_HALF_WIDTH or abs(diff2) < BEAM_HALF_WIDTH
|
||||||
|
|
||||||
|
func push_if_in_beam(ox: float, oy: float, ovx: float, ovy: float) -> Vector2:
|
||||||
|
var dx := ox - x; var dy := oy - y
|
||||||
|
var dist := sqrt(dx*dx + dy*dy)
|
||||||
|
if dist < BEAM_LENGTH and in_beam(ox, oy):
|
||||||
|
var force := 1.5 / (dist * 0.05 + 1.0)
|
||||||
|
return Vector2(ovx + (dx/dist)*force, ovy + (dy/dist)*force)
|
||||||
|
return Vector2(ovx, ovy)
|
||||||
|
|
||||||
|
func draw(canvas: CanvasItem) -> void:
|
||||||
|
for bd in [0.0, PI]:
|
||||||
|
var ba: float = beam_angle + float(bd)
|
||||||
|
for bl in range(0, int(BEAM_LENGTH), 4):
|
||||||
|
var bx := x + cos(ba) * bl
|
||||||
|
var by := y + sin(ba) * bl
|
||||||
|
var ba2 := 1.0 - float(bl) / BEAM_LENGTH
|
||||||
|
canvas.draw_rect(Rect2(bx, by, 2, 2), Color(0.8, 1.0, 0.8, ba2 * 0.7))
|
||||||
|
canvas.draw_rect(Rect2(x-3, y-3, 7, 7), Color(0.9, 1.0, 0.9, 1.0))
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Antimatter ───────────────────────────────────────────────────────────────
|
||||||
|
class Antimatter extends RefCounted:
|
||||||
|
var x: float; var y: float; var vx: float; var vy: float
|
||||||
|
var dead: bool = false; var pulse: float = 0.0
|
||||||
|
|
||||||
|
func init(px: float, py: float, angle: float, speed: float) -> void:
|
||||||
|
x = px; y = py
|
||||||
|
vx = cos(angle) * speed; vy = sin(angle) * speed
|
||||||
|
pulse = randf() * TAU
|
||||||
|
|
||||||
|
func update(world_w: float, world_h: float, delta: float, nearby: Array = []) -> void:
|
||||||
|
pulse += delta * 5.0
|
||||||
|
# Magnetic attraction to nearby antimatter particles
|
||||||
|
const ATTRACT_RADIUS := 60.0
|
||||||
|
const ATTRACT_FORCE := 0.008
|
||||||
|
for other in nearby:
|
||||||
|
if other == self: continue
|
||||||
|
var dx: float = float(other.x) - x
|
||||||
|
var dy: float = float(other.y) - y
|
||||||
|
var d: float = sqrt(dx*dx + dy*dy)
|
||||||
|
if d < ATTRACT_RADIUS and d > 1.0:
|
||||||
|
vx += (dx / d) * ATTRACT_FORCE
|
||||||
|
vy += (dy / d) * ATTRACT_FORCE
|
||||||
|
# Clamp speed so clustering doesn't cause runaway velocity
|
||||||
|
var spd := sqrt(vx*vx + vy*vy)
|
||||||
|
if spd > 1.8:
|
||||||
|
vx = vx / spd * 1.8
|
||||||
|
vy = vy / spd * 1.8
|
||||||
|
x += vx; y += vy
|
||||||
|
if x < 0: x += world_w
|
||||||
|
elif x > world_w: x -= world_w
|
||||||
|
if y < 0: y += world_h
|
||||||
|
elif y > world_h: y -= world_h
|
||||||
|
|
||||||
|
func draw(canvas: CanvasItem) -> void:
|
||||||
|
var p := 0.7 + 0.3 * sin(pulse)
|
||||||
|
canvas.draw_rect(Rect2(x-3, y-3, 7, 7), Color(1.0, 0.2, 0.8, p * 0.3))
|
||||||
|
canvas.draw_rect(Rect2(x-1, y-1, 3, 3), Color(1.0, 0.2, 0.8, p))
|
||||||
|
canvas.draw_rect(Rect2(x, y, 1, 1), Color(1.0, 0.8, 1.0, p * 0.9))
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Galaxy ───────────────────────────────────────────────────────────────────
|
||||||
|
class Galaxy extends RefCounted:
|
||||||
|
var x: float; var y: float; var angle: float = 0.0
|
||||||
|
var rotation_speed: float = 0.3 # rad/s, increases during consumption
|
||||||
|
var radius: float; var vx: float = 0.0; var vy: float = 0.0
|
||||||
|
var color: Color; var num_arms: int = 3
|
||||||
|
var star_points: Array = []
|
||||||
|
var dead: bool = false; var alpha: float = 1.0
|
||||||
|
var respawn_timer: float = 0.0; var respawning: bool = false
|
||||||
|
# Gradual consumption (drifting into a BH)
|
||||||
|
var being_consumed: bool = false
|
||||||
|
var consume_timer: float = 0.0
|
||||||
|
const CONSUME_DURATION := 2.5
|
||||||
|
var consume_bh_x: float = 0.0
|
||||||
|
var consume_bh_y: float = 0.0
|
||||||
|
var consuming_bh = null # BlackHole ref — set by game_world
|
||||||
|
var consume_initial_radius: float = 0.0
|
||||||
|
var consume_initial_alpha: float = 0.0
|
||||||
|
var original_color: Color = Color.WHITE
|
||||||
|
|
||||||
|
func init(px: float, py: float) -> void:
|
||||||
|
x = px; y = py
|
||||||
|
vx = randf_range(-0.08, 0.08) * 2.4 # original ±0.08 * scale
|
||||||
|
vy = randf_range(-0.05, 0.05) * 2.4
|
||||||
|
radius = randf_range(35.0, 65.0) * 2.4 / 2.0 # scale to 2.4x but not full
|
||||||
|
angle = randf() * TAU
|
||||||
|
rotation_speed = randf_range(0.001, 0.003) * 60.0 # rad/s
|
||||||
|
num_arms = 2 if randf() < 0.4 else 3
|
||||||
|
var gal_colors := ["#ffeeaa","#aaddff","#ffaacc","#aaffcc","#ddaaff"]
|
||||||
|
color = Color(gal_colors[randi() % gal_colors.size()])
|
||||||
|
alpha = randf_range(0.25, 0.55)
|
||||||
|
_generate_stars()
|
||||||
|
|
||||||
|
func _generate_stars() -> void:
|
||||||
|
star_points.clear()
|
||||||
|
for arm in num_arms:
|
||||||
|
var arm_offset := (TAU / num_arms) * arm
|
||||||
|
for i in 26:
|
||||||
|
var t := float(i) / 25.0
|
||||||
|
var a := angle + arm_offset + t * 3.5
|
||||||
|
var r := 4.0 + t * (radius - 4.0)
|
||||||
|
var scatter := randf_range(-6.0, 6.0)
|
||||||
|
star_points.append(Vector2(cos(a)*r + scatter, sin(a)*r + scatter))
|
||||||
|
|
||||||
|
func update(delta: float, world_w: float, world_h: float) -> void:
|
||||||
|
if being_consumed:
|
||||||
|
consume_timer += delta
|
||||||
|
var t: float = clampf(consume_timer / CONSUME_DURATION, 0.0, 1.0)
|
||||||
|
# Drift toward BH with accelerating speed
|
||||||
|
var target_dx: float = consume_bh_x - x
|
||||||
|
var target_dy: float = consume_bh_y - y
|
||||||
|
var target_dist: float = sqrt(target_dx * target_dx + target_dy * target_dy)
|
||||||
|
if target_dist > 2.0:
|
||||||
|
var drift_speed: float = 30.0 * t * t
|
||||||
|
x += (target_dx / target_dist) * drift_speed * delta
|
||||||
|
y += (target_dy / target_dist) * drift_speed * delta
|
||||||
|
# Spin accelerates as it's consumed
|
||||||
|
angle += rotation_speed * delta * (1.0 + t * 8.0)
|
||||||
|
# Compress radius — tidal compression
|
||||||
|
radius = maxf(3.0, consume_initial_radius * (1.0 - t * 0.85))
|
||||||
|
# Regenerate star points with compressed radius periodically
|
||||||
|
if int(consume_timer * 5.0) > int((consume_timer - delta) * 5.0):
|
||||||
|
_generate_stars()
|
||||||
|
# Color warms toward orange/white (accretion heating)
|
||||||
|
color = original_color.lerp(Color(1.0, 0.7, 0.3), t * 0.6)
|
||||||
|
# Alpha fades in final 40%
|
||||||
|
if t > 0.6:
|
||||||
|
alpha = maxf(0.0, consume_initial_alpha * (1.0 - (t - 0.6) / 0.4))
|
||||||
|
if consume_timer >= CONSUME_DURATION or alpha <= 0.0:
|
||||||
|
dead = true
|
||||||
|
return
|
||||||
|
angle += rotation_speed * delta
|
||||||
|
x += vx; y += vy
|
||||||
|
# Screen wrap
|
||||||
|
if x < -radius: x += world_w + radius*2.0
|
||||||
|
elif x > world_w + radius: x -= world_w + radius*2.0
|
||||||
|
if y < -radius: y += world_h + radius*2.0
|
||||||
|
elif y > world_h + radius: y -= world_h + radius*2.0
|
||||||
|
if respawning:
|
||||||
|
respawn_timer -= delta
|
||||||
|
if respawn_timer <= 0.0:
|
||||||
|
respawning = false; dead = false; alpha = randf_range(0.25, 0.55)
|
||||||
|
being_consumed = false; consume_timer = 0.0; consuming_bh = null
|
||||||
|
radius = randf_range(35.0, 65.0) * 2.4 / 2.0
|
||||||
|
rotation_speed = randf_range(0.001, 0.003) * 60.0
|
||||||
|
x = randf() * world_w; y = randf() * world_h
|
||||||
|
angle = randf() * TAU
|
||||||
|
num_arms = 2 if randf() < 0.4 else 3
|
||||||
|
_generate_stars()
|
||||||
|
|
||||||
|
func draw(canvas: CanvasItem) -> void:
|
||||||
|
if dead or respawning: return
|
||||||
|
var cos_a := cos(angle); var sin_a := sin(angle)
|
||||||
|
# Core halo
|
||||||
|
canvas.draw_rect(Rect2(x - 5.0, y - 5.0, 11.0, 11.0),
|
||||||
|
Color(color.r, color.g, color.b, alpha * 0.3))
|
||||||
|
# Spiral arms
|
||||||
|
for pt in star_points:
|
||||||
|
var rx: float = x + float(pt.x) * cos_a - float(pt.y) * sin_a
|
||||||
|
var ry: float = y + float(pt.x) * sin_a + float(pt.y) * cos_a
|
||||||
|
var dot_size := 2.0 if abs(float(pt.x)) < radius * 0.3 else 1.0
|
||||||
|
canvas.draw_rect(Rect2(rx, ry, dot_size, 1.0),
|
||||||
|
Color(color.r, color.g, color.b, alpha * 0.75))
|
||||||
|
# Bright core
|
||||||
|
canvas.draw_rect(Rect2(x - 2.0, y - 2.0, 5.0, 5.0),
|
||||||
|
Color(color.r, color.g, color.b, alpha * 0.6))
|
||||||
|
canvas.draw_rect(Rect2(x - 1.0, y - 1.0, 3.0, 3.0),
|
||||||
|
Color(1.0, 1.0, 1.0, alpha * 0.9))
|
||||||
|
|
||||||
|
|
||||||
|
# ─── AntimatterStar ───────────────────────────────────────────────────────────
|
||||||
|
class AntimatterStar extends RefCounted:
|
||||||
|
const MAX_LIFE := 50.0
|
||||||
|
const REPEL_RADIUS := 90.0
|
||||||
|
const REPEL_FORCE := 0.055
|
||||||
|
|
||||||
|
var x: float; var y: float
|
||||||
|
var radius: float = 18.0
|
||||||
|
var life: float = MAX_LIFE
|
||||||
|
var pulse: float = 0.0
|
||||||
|
var dead: bool = false
|
||||||
|
|
||||||
|
func init(px: float, py: float) -> void:
|
||||||
|
x = px; y = py
|
||||||
|
pulse = randf() * TAU
|
||||||
|
|
||||||
|
func update(delta: float) -> void:
|
||||||
|
life -= delta
|
||||||
|
pulse += delta * 3.0
|
||||||
|
if life <= 0.0:
|
||||||
|
dead = true
|
||||||
|
|
||||||
|
func apply_repulsion(ox: float, oy: float, ovx: float, ovy: float) -> Vector2:
|
||||||
|
var dx := ox - x; var dy := oy - y
|
||||||
|
var d := sqrt(dx*dx + dy*dy)
|
||||||
|
if d < REPEL_RADIUS and d > 0.01:
|
||||||
|
var force := REPEL_FORCE * (1.0 - d / REPEL_RADIUS)
|
||||||
|
return Vector2(ovx + (dx / d) * force, ovy + (dy / d) * force)
|
||||||
|
return Vector2(ovx, ovy)
|
||||||
|
|
||||||
|
func draw(canvas: CanvasItem) -> void:
|
||||||
|
var p: float = 0.6 + 0.4 * sin(pulse)
|
||||||
|
var alpha: float = clamp(life / MAX_LIFE, 0.0, 1.0)
|
||||||
|
var cv := Vector2(x, y)
|
||||||
|
# Outer magenta glow
|
||||||
|
canvas.draw_circle(cv, radius + 8.0, Color(0.75, 0.0, 0.9, p * 0.10 * alpha))
|
||||||
|
canvas.draw_circle(cv, radius + 3.0, Color(0.6, 0.0, 0.85, p * 0.18 * alpha))
|
||||||
|
# Dark antimatter core
|
||||||
|
canvas.draw_circle(cv, radius, Color(0.15, 0.0, 0.22, alpha))
|
||||||
|
# Rotating ring
|
||||||
|
var ring_r := radius + 5.0 + 2.0 * sin(pulse)
|
||||||
|
canvas.draw_arc(cv, ring_r, pulse * 0.7, pulse * 0.7 + TAU * 0.8, 14,
|
||||||
|
Color(1.0, 0.2, 1.0, p * 0.85 * alpha), 2.0)
|
||||||
|
canvas.draw_arc(cv, ring_r - 3.0, -pulse * 0.5, -pulse * 0.5 + TAU * 0.6, 10,
|
||||||
|
Color(0.8, 0.0, 1.0, p * 0.5 * alpha), 1.5)
|
||||||
|
# Bright centre dot
|
||||||
|
canvas.draw_circle(cv, 3.0, Color(1.0, 0.5, 1.0, alpha * p))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://j7pdrterps08
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
extends RefCounted
|
||||||
|
class_name EnemyShip
|
||||||
|
|
||||||
|
const THRUST := 0.19
|
||||||
|
const DRAG := 0.988
|
||||||
|
const MAX_SPEED := 4.8
|
||||||
|
const TRAIL_LEN := 16
|
||||||
|
const ATTACK_RANGE := 600.0
|
||||||
|
const FIRE_INTERVAL := 90
|
||||||
|
const RESPAWN_MIN := 4.0
|
||||||
|
const RESPAWN_MAX := 8.0
|
||||||
|
const SEP_RADIUS := 110.0 # anti-clump separation distance
|
||||||
|
const ORBIT_RADIUS := 180.0 # circle-strafe orbit distance
|
||||||
|
|
||||||
|
# Three attack roles that prevent enemies from all rushing from the same angle.
|
||||||
|
enum Role { AGGRO, CIRCLE, FLANK }
|
||||||
|
|
||||||
|
var x: float; var y: float
|
||||||
|
var vx: float = 0.0; var vy: float = 0.0
|
||||||
|
var heading: float = 0.0
|
||||||
|
var trail: Array = []
|
||||||
|
var dead: bool = false
|
||||||
|
var respawn_timer: float = 0.0
|
||||||
|
var fire_timer: int = 0
|
||||||
|
var enemy_index: int = 0 # 0=red, 1=cyan (colour variant)
|
||||||
|
var role_id: int = 0 # sequential id, persists across respawns
|
||||||
|
var role: int = Role.AGGRO
|
||||||
|
var orbit_offset: float = 0.0
|
||||||
|
|
||||||
|
var patrol_timer: int = 0
|
||||||
|
var patrol_target: Vector2 = Vector2.ZERO
|
||||||
|
|
||||||
|
var stats: ShipStats = null
|
||||||
|
var current_shields: int = 0
|
||||||
|
var invuln_timer: int = 0
|
||||||
|
|
||||||
|
func apply_stats(s: ShipStats) -> void:
|
||||||
|
stats = s
|
||||||
|
current_shields = s.shield_charges
|
||||||
|
|
||||||
|
# Hull offsets ×2, rendered as 3×3 rects
|
||||||
|
const HULL_PIXELS := [
|
||||||
|
[6, 0, "nose"],
|
||||||
|
[4, -2, "mid"], [4, 2, "mid"],
|
||||||
|
[2, 0, "dim"],
|
||||||
|
[0, -4, "accent"],[0, 4, "accent"],
|
||||||
|
[0, 0, "bright"],
|
||||||
|
[-2,-2, "dim"], [-2, 2, "dim"],
|
||||||
|
[-4,-4, "shadow"],[-4, 4, "shadow"],
|
||||||
|
[-4, 0, "edge"],
|
||||||
|
]
|
||||||
|
|
||||||
|
var palette: Dictionary
|
||||||
|
|
||||||
|
func init(px: float, py: float, idx: int, rid: int = 0) -> void:
|
||||||
|
x = px; y = py
|
||||||
|
enemy_index = idx
|
||||||
|
role_id = rid
|
||||||
|
role = rid % 3
|
||||||
|
# Each role_id starts at a different point on the orbit so enemies spread out
|
||||||
|
orbit_offset = float(rid) * (TAU / 3.0)
|
||||||
|
dead = false
|
||||||
|
respawn_timer = 0.0
|
||||||
|
# Stagger fire timers so enemies don't all shoot at the same frame
|
||||||
|
fire_timer = FIRE_INTERVAL + rid * 17
|
||||||
|
patrol_target = Vector2(px + randf_range(-250.0, 250.0), py + randf_range(-250.0, 250.0))
|
||||||
|
patrol_timer = randi_range(60, 180)
|
||||||
|
invuln_timer = 60
|
||||||
|
if stats: current_shields = stats.shield_charges
|
||||||
|
if idx == 0:
|
||||||
|
palette = {
|
||||||
|
"nose": Color("#ff8888"), "bright": Color("#ff4444"), "mid": Color("#cc2222"),
|
||||||
|
"dim": Color("#882222"), "accent": Color("#ffaaaa"), "edge": Color("#441111"),
|
||||||
|
"shadow": Color("#220000"), "trail": Color(1.0, 0.2, 0.2, 0.25),
|
||||||
|
"thrustHot": Color("#ffcc44"), "thrustCool": Color(1.0, 0.4, 0.0, 0.5)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
palette = {
|
||||||
|
"nose": Color("#88ffff"), "bright": Color("#44ccff"), "mid": Color("#2288cc"),
|
||||||
|
"dim": Color("#224488"), "accent": Color("#aaccff"), "edge": Color("#112244"),
|
||||||
|
"shadow": Color("#001122"), "trail": Color(0.2, 0.8, 1.0, 0.25),
|
||||||
|
"thrustHot": Color("#aaffcc"), "thrustCool": Color(0.2, 0.8, 1.0, 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
# enemies array is passed in so each enemy can compute separation from allies.
|
||||||
|
func update(players: Array, black_holes: Array, enemies: Array, world_w: float, world_h: float, delta: float) -> Bullet:
|
||||||
|
if dead:
|
||||||
|
respawn_timer -= delta
|
||||||
|
return null
|
||||||
|
if invuln_timer > 0:
|
||||||
|
invuln_timer -= 1
|
||||||
|
|
||||||
|
var eff_thrust: float = THRUST * (stats.speed_mult if stats else 1.0)
|
||||||
|
var eff_max: float = MAX_SPEED * (stats.speed_mult if stats else 1.0)
|
||||||
|
var eff_turn: float = 0.1 * (stats.turn_mult if stats else 1.0)
|
||||||
|
var eff_interval: int = max(10, int(float(FIRE_INTERVAL) / (stats.fire_rate_mult if stats else 1.0)))
|
||||||
|
|
||||||
|
# Find nearest living player
|
||||||
|
var nearest_player = null
|
||||||
|
var nearest_dist := INF
|
||||||
|
for p in players:
|
||||||
|
if p.dead: continue
|
||||||
|
var dx: float = float(p.x) - x
|
||||||
|
var dy: float = float(p.y) - y
|
||||||
|
var d: float = sqrt(dx*dx + dy*dy)
|
||||||
|
if d < nearest_dist:
|
||||||
|
nearest_dist = d
|
||||||
|
nearest_player = p
|
||||||
|
|
||||||
|
# Black-hole avoidance — steer away when too close to pull radius
|
||||||
|
var avoid_vx := 0.0; var avoid_vy := 0.0
|
||||||
|
for bh in black_holes:
|
||||||
|
var dx: float = x - float(bh.x)
|
||||||
|
var dy: float = y - float(bh.y)
|
||||||
|
var d: float = sqrt(dx*dx + dy*dy)
|
||||||
|
if d < bh.pull_radius * 1.3 and d > 0.01:
|
||||||
|
avoid_vx += (dx / d) * 3.0
|
||||||
|
avoid_vy += (dy / d) * 3.0
|
||||||
|
|
||||||
|
# Separation force — push away from nearby allies to prevent clumping
|
||||||
|
var sep_vx := 0.0; var sep_vy := 0.0
|
||||||
|
for en in enemies:
|
||||||
|
var oe: EnemyShip = en
|
||||||
|
if oe == self or oe.dead: continue
|
||||||
|
var dx: float = x - oe.x
|
||||||
|
var dy: float = y - oe.y
|
||||||
|
var d: float = sqrt(dx*dx + dy*dy)
|
||||||
|
if d < SEP_RADIUS and d > 0.01:
|
||||||
|
var strength: float = (SEP_RADIUS - d) / SEP_RADIUS
|
||||||
|
sep_vx += (dx / d) * strength * 2.0
|
||||||
|
sep_vy += (dy / d) * strength * 2.0
|
||||||
|
|
||||||
|
# Role-based movement
|
||||||
|
if nearest_player != null and nearest_dist < ATTACK_RANGE:
|
||||||
|
var pdx: float = float(nearest_player.x) - x
|
||||||
|
var pdy: float = float(nearest_player.y) - y
|
||||||
|
_move_attack(pdx, pdy, eff_thrust, eff_turn)
|
||||||
|
else:
|
||||||
|
_move_patrol(eff_thrust, eff_turn, world_w, world_h)
|
||||||
|
|
||||||
|
vx += (avoid_vx + sep_vx) * 0.05
|
||||||
|
vy += (avoid_vy + sep_vy) * 0.05
|
||||||
|
|
||||||
|
vx *= DRAG; vy *= DRAG
|
||||||
|
var spd := sqrt(vx*vx + vy*vy)
|
||||||
|
if spd > eff_max:
|
||||||
|
vx = vx / spd * eff_max
|
||||||
|
vy = vy / spd * eff_max
|
||||||
|
|
||||||
|
trail.push_front(Vector2(x, y))
|
||||||
|
if trail.size() > TRAIL_LEN: trail.pop_back()
|
||||||
|
|
||||||
|
x += vx; y += vy
|
||||||
|
if x < 0: x += world_w
|
||||||
|
elif x > world_w: x -= world_w
|
||||||
|
if y < 0: y += world_h
|
||||||
|
elif y > world_h: y -= world_h
|
||||||
|
|
||||||
|
fire_timer -= 1
|
||||||
|
if fire_timer <= 0 and nearest_player != null and nearest_dist < ATTACK_RANGE:
|
||||||
|
fire_timer = eff_interval
|
||||||
|
# Aim toward player with distance-based spread:
|
||||||
|
# accurate at close range (~0.08 rad), less so at max range (~0.32 rad).
|
||||||
|
var fdx: float = float(nearest_player.x) - x
|
||||||
|
var fdy: float = float(nearest_player.y) - y
|
||||||
|
var spread: float = lerp(0.08, 0.32, clamp(nearest_dist / ATTACK_RANGE, 0.0, 1.0))
|
||||||
|
var fire_angle: float = atan2(fdy, fdx) + randf_range(-spread, spread)
|
||||||
|
var b := Bullet.new()
|
||||||
|
b.init(x + cos(fire_angle)*8.0, y + sin(fire_angle)*8.0, fire_angle, "enemy")
|
||||||
|
if stats:
|
||||||
|
b.vx *= stats.bullet_speed_mult
|
||||||
|
b.vy *= stats.bullet_speed_mult
|
||||||
|
b.effective_hit_radius = Bullet.HIT_RADIUS * stats.damage_mult
|
||||||
|
return b
|
||||||
|
return null
|
||||||
|
|
||||||
|
# Role.AGGRO: rush straight at player.
|
||||||
|
# Role.CIRCLE: maintain orbit around player at ORBIT_RADIUS.
|
||||||
|
# Role.FLANK: approach from a perpendicular angle left or right.
|
||||||
|
func _move_attack(pdx: float, pdy: float, eff_thrust: float, eff_turn: float) -> void:
|
||||||
|
match role:
|
||||||
|
Role.AGGRO:
|
||||||
|
var ta := atan2(pdy, pdx)
|
||||||
|
var ad := fmod(ta - heading + PI * 3.0, TAU) - PI
|
||||||
|
heading += clamp(ad, -eff_turn, eff_turn)
|
||||||
|
vx += cos(heading) * eff_thrust
|
||||||
|
vy += sin(heading) * eff_thrust
|
||||||
|
|
||||||
|
Role.CIRCLE:
|
||||||
|
orbit_offset += 0.012
|
||||||
|
# Target = player position + point on orbit circle
|
||||||
|
var player_x: float = x + pdx
|
||||||
|
var player_y: float = y + pdy
|
||||||
|
var orbit_x: float = player_x + cos(orbit_offset) * ORBIT_RADIUS
|
||||||
|
var orbit_y: float = player_y + sin(orbit_offset) * ORBIT_RADIUS
|
||||||
|
var tdx: float = orbit_x - x
|
||||||
|
var tdy: float = orbit_y - y
|
||||||
|
var ta: float = atan2(tdy, tdx)
|
||||||
|
var ad: float = fmod(ta - heading + PI * 3.0, TAU) - PI
|
||||||
|
heading += clamp(ad, -eff_turn * 1.2, eff_turn * 1.2)
|
||||||
|
vx += cos(heading) * eff_thrust
|
||||||
|
vy += sin(heading) * eff_thrust
|
||||||
|
|
||||||
|
Role.FLANK:
|
||||||
|
# Approach from +90° or −90° relative to the direct line to player
|
||||||
|
var direct: float = atan2(pdy, pdx)
|
||||||
|
var flank_side: float = PI * 0.5 if (role_id % 2 == 0) else -PI * 0.5
|
||||||
|
var player_x: float = x + pdx
|
||||||
|
var player_y: float = y + pdy
|
||||||
|
var flank_x: float = player_x + cos(direct + flank_side) * 80.0
|
||||||
|
var flank_y: float = player_y + sin(direct + flank_side) * 80.0
|
||||||
|
var tdx: float = flank_x - x
|
||||||
|
var tdy: float = flank_y - y
|
||||||
|
var ta: float = atan2(tdy, tdx)
|
||||||
|
var ad: float = fmod(ta - heading + PI * 3.0, TAU) - PI
|
||||||
|
heading += clamp(ad, -eff_turn, eff_turn)
|
||||||
|
vx += cos(heading) * eff_thrust
|
||||||
|
vy += sin(heading) * eff_thrust
|
||||||
|
|
||||||
|
func _move_patrol(eff_thrust: float, eff_turn: float, world_w: float, world_h: float) -> void:
|
||||||
|
patrol_timer -= 1
|
||||||
|
var to_target := Vector2(patrol_target.x - x, patrol_target.y - y)
|
||||||
|
# Pick a new target when close enough or timer expires → spreads enemies across map
|
||||||
|
if patrol_timer <= 0 or to_target.length() < 40.0:
|
||||||
|
patrol_target = Vector2(
|
||||||
|
randf_range(world_w * 0.1, world_w * 0.9),
|
||||||
|
randf_range(world_h * 0.1, world_h * 0.9))
|
||||||
|
patrol_timer = randi_range(120, 300)
|
||||||
|
var angle: float = atan2(to_target.y, to_target.x)
|
||||||
|
var ad: float = fmod(angle - heading + PI * 3.0, TAU) - PI
|
||||||
|
heading += clamp(ad, -eff_turn * 0.8, eff_turn * 0.8)
|
||||||
|
vx += cos(heading) * eff_thrust * 0.6
|
||||||
|
vy += sin(heading) * eff_thrust * 0.6
|
||||||
|
|
||||||
|
func draw(canvas: CanvasItem) -> void:
|
||||||
|
if dead: return
|
||||||
|
var cos_h := cos(heading); var sin_h := sin(heading)
|
||||||
|
for i in trail.size():
|
||||||
|
var t_alpha := (1.0 - float(i) / float(TRAIL_LEN)) * 0.5
|
||||||
|
var tc := Color(palette["trail"].r, palette["trail"].g, palette["trail"].b, t_alpha)
|
||||||
|
canvas.draw_rect(Rect2(trail[i].x - 1, trail[i].y - 1, 2, 2), tc)
|
||||||
|
for px_data in HULL_PIXELS:
|
||||||
|
var lx: float = px_data[0]
|
||||||
|
var ly: float = px_data[1]
|
||||||
|
var col: Color = palette.get(px_data[2], Color.WHITE)
|
||||||
|
var rx := x + lx * cos_h - ly * sin_h
|
||||||
|
var ry := y + lx * sin_h + ly * cos_h
|
||||||
|
canvas.draw_rect(Rect2(rx - 1, ry - 1, 3, 3), col)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://fycowvc7jmfy
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
uid://dgjwy8o1rgfd7
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
extends CanvasLayer
|
||||||
|
|
||||||
|
const W := 400.0
|
||||||
|
const H := 300.0
|
||||||
|
|
||||||
|
var countdown_value: int = 0
|
||||||
|
var show_countdown_flag: bool = false
|
||||||
|
var score_data: Dictionary = {}
|
||||||
|
var is_gameover: bool = false
|
||||||
|
var blink_phase: float = 0.0
|
||||||
|
var gameover_blink: bool = true
|
||||||
|
var gameover_blink_timer: float = 0.0
|
||||||
|
var is_returning: bool = false
|
||||||
|
var wipe_warning: bool = false
|
||||||
|
var credits: int = 0
|
||||||
|
var lives: int = 3
|
||||||
|
var wave_number: int = 1
|
||||||
|
var wave_cleared: bool = false
|
||||||
|
|
||||||
|
# References to game_world for live data
|
||||||
|
var game_world: Node = null
|
||||||
|
|
||||||
|
# Name entry after game over
|
||||||
|
var awaiting_name: bool = false
|
||||||
|
var name_input: String = ""
|
||||||
|
var name_saved: bool = false
|
||||||
|
var _name_saved_timer: float = 0.0
|
||||||
|
var _pending_score: int = 0
|
||||||
|
var _pending_wave: int = 0
|
||||||
|
var _online_lb: Node = null
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var hud_draw := Node2D.new()
|
||||||
|
hud_draw.set_script(load("res://scripts/hud_draw.gd"))
|
||||||
|
hud_draw.name = "HUDDraw"
|
||||||
|
add_child(hud_draw)
|
||||||
|
hud_draw.hud = self
|
||||||
|
_online_lb = load("res://scripts/online_leaderboard.gd").new()
|
||||||
|
add_child(_online_lb)
|
||||||
|
|
||||||
|
func show_countdown(val: int) -> void:
|
||||||
|
show_countdown_flag = true
|
||||||
|
countdown_value = val
|
||||||
|
|
||||||
|
func show_gameover(data: Dictionary) -> void:
|
||||||
|
score_data = data
|
||||||
|
is_gameover = true
|
||||||
|
is_returning = false
|
||||||
|
var total := int(data.get("p1_time", 0.0)) + int(data.get("p1_kills", 0)) * 50 + int(data.get("p1_wipe", 0))
|
||||||
|
if data.get("multiplayer", false):
|
||||||
|
total += int(data.get("p2_time", 0.0)) + int(data.get("p2_kills", 0)) * 50 + int(data.get("p2_wipe", 0))
|
||||||
|
_pending_score = total
|
||||||
|
_pending_wave = wave_number
|
||||||
|
if total > 0:
|
||||||
|
awaiting_name = true
|
||||||
|
name_input = ""
|
||||||
|
name_saved = false
|
||||||
|
|
||||||
|
func _confirm_name() -> void:
|
||||||
|
var n := name_input.strip_edges()
|
||||||
|
if n.length() == 0:
|
||||||
|
return
|
||||||
|
Leaderboard.add_score(n, _pending_score, _pending_wave)
|
||||||
|
if _online_lb:
|
||||||
|
_online_lb.submit_score(n, _pending_score, _pending_wave)
|
||||||
|
name_saved = true
|
||||||
|
_name_saved_timer = 0.0
|
||||||
|
awaiting_name = false
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if not awaiting_name:
|
||||||
|
return
|
||||||
|
if not (event is InputEventKey and (event as InputEventKey).pressed):
|
||||||
|
return
|
||||||
|
var phk := (event as InputEventKey).physical_keycode
|
||||||
|
if phk == KEY_BACKSPACE or phk == KEY_DELETE:
|
||||||
|
if name_input.length() > 0:
|
||||||
|
name_input = name_input.left(name_input.length() - 1)
|
||||||
|
elif phk == KEY_ENTER or phk == KEY_KP_ENTER:
|
||||||
|
if name_input.strip_edges().length() > 0:
|
||||||
|
_confirm_name()
|
||||||
|
elif phk == KEY_ESCAPE:
|
||||||
|
awaiting_name = false
|
||||||
|
elif name_input.length() < 12:
|
||||||
|
var uc := (event as InputEventKey).unicode
|
||||||
|
if uc >= 32 and uc < 127:
|
||||||
|
name_input += char(uc)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
blink_phase += delta
|
||||||
|
gameover_blink_timer += delta
|
||||||
|
if gameover_blink_timer >= 0.5:
|
||||||
|
gameover_blink_timer = 0.0
|
||||||
|
gameover_blink = not gameover_blink
|
||||||
|
if name_saved:
|
||||||
|
_name_saved_timer += delta
|
||||||
|
if _name_saved_timer > 1.5:
|
||||||
|
name_saved = false
|
||||||
|
if game_world and game_world.big_wipe:
|
||||||
|
wipe_warning = game_world.big_wipe.is_active() and game_world.big_wipe.phase == BigWipe.Phase.COLLAPSE
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://kn3e6fiywxr7
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# Minimal cockpit HUD — maximum screen space for the game.
|
||||||
|
# During play: only 4 corner L-brackets + tiny top-right status line.
|
||||||
|
# On gameover: full terminal overlay.
|
||||||
|
|
||||||
|
var W: float = 960.0
|
||||||
|
var H: float = 600.0
|
||||||
|
|
||||||
|
# Cockpit palette
|
||||||
|
const COL_PRIMARY := Color(0.0, 1.0, 0.533, 0.55) # phosphor green, semi-dim
|
||||||
|
const COL_ACCENT := Color(0.27, 1.0, 0.8, 0.80)
|
||||||
|
const COL_DIM := Color(0.0, 0.27, 0.13, 0.45)
|
||||||
|
const COL_WARNING := Color(1.0, 0.67, 0.0, 0.90)
|
||||||
|
const COL_DANGER := Color(1.0, 0.2, 0.0, 0.90)
|
||||||
|
const COL_WHITE := Color(1.0, 1.0, 1.0, 0.90)
|
||||||
|
const BRACKET_LEN := 14.0
|
||||||
|
const BRACKET_W := 1.5
|
||||||
|
const MARGIN := 10.0
|
||||||
|
|
||||||
|
var hud: Node = null
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
if hud == null:
|
||||||
|
return
|
||||||
|
var vs: Vector2 = get_viewport_rect().size
|
||||||
|
W = vs.x; H = vs.y
|
||||||
|
var font := ThemeDB.fallback_font
|
||||||
|
var gw: Node = hud.game_world
|
||||||
|
|
||||||
|
# ── Countdown ────────────────────────────────────────────────────────────
|
||||||
|
if hud.show_countdown_flag and hud.countdown_value > 0:
|
||||||
|
_draw_corner_brackets()
|
||||||
|
var ct := str(hud.countdown_value)
|
||||||
|
_draw_text_centered(font, ct, W * 0.5, H * 0.5 - 36.0, 64, COL_ACCENT)
|
||||||
|
_draw_text_centered(font, Tr.t("hud_launching"), W * 0.5, H * 0.5 + 28.0, 10, COL_DIM)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not hud.is_gameover and gw == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Normal gameplay HUD ──────────────────────────────────────────────────
|
||||||
|
if not hud.is_gameover and gw != null:
|
||||||
|
_draw_corner_brackets()
|
||||||
|
|
||||||
|
# ── Wave timer bar ────────────────────────────────────────────────
|
||||||
|
var wave_dur: float = float(gw.get("wave_duration") if gw.get("wave_duration") != null else 60.0)
|
||||||
|
var wave_t: float = float(gw.get("wave_timer") if gw.get("wave_timer") != null else 0.0)
|
||||||
|
var wave_frac: float = clamp(wave_t / wave_dur, 0.0, 1.0)
|
||||||
|
var wave_remaining: float = max(0.0, wave_dur - wave_t)
|
||||||
|
var last_10: bool = wave_remaining <= 10.0 and gw.get("is_playing")
|
||||||
|
# Thin bar at top — shrinks from right as time passes
|
||||||
|
var bar_col: Color
|
||||||
|
if last_10:
|
||||||
|
bar_col = Color(COL_WARNING.r, COL_WARNING.g, COL_WARNING.b,
|
||||||
|
0.55 + 0.45 * sin(hud.blink_phase * 8.0))
|
||||||
|
else:
|
||||||
|
bar_col = Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.35)
|
||||||
|
draw_rect(Rect2(0.0, 0.0, W * (1.0 - wave_frac), 2.0), bar_col)
|
||||||
|
# Wave label top-left: "W1 0:42"
|
||||||
|
var w_mins: int = int(wave_remaining / 60.0)
|
||||||
|
var w_secs: int = int(wave_remaining) % 60
|
||||||
|
var wave_label := "W%d %d:%02d" % [hud.wave_number, w_mins, w_secs]
|
||||||
|
var wlabel_col: Color = COL_WARNING if last_10 else COL_DIM
|
||||||
|
_draw_text(font, wave_label, MARGIN, 10.0, 9, wlabel_col)
|
||||||
|
|
||||||
|
# ── Wave cleared overlay ──────────────────────────────────────────
|
||||||
|
if hud.wave_cleared:
|
||||||
|
var wc_a: float = 0.7 + 0.3 * sin(hud.blink_phase * 5.0)
|
||||||
|
_draw_text_centered(font, Tr.t("hud_wave_clear") % (hud.wave_number - 1),
|
||||||
|
W * 0.5, H * 0.5 - 22.0, 16, Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, wc_a))
|
||||||
|
_draw_text_centered(font, Tr.t("hud_to_shop"), W * 0.5, H * 0.5 + 8.0, 11,
|
||||||
|
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, wc_a * 0.7))
|
||||||
|
|
||||||
|
# Top-right micro status — lives dots + credits + time + kills
|
||||||
|
var players: Array = gw.players
|
||||||
|
var lives_str := ""
|
||||||
|
for li in 3:
|
||||||
|
lives_str += ("● " if li < hud.lives else "○ ")
|
||||||
|
var cr_str := "CR:%d" % hud.credits
|
||||||
|
if not gw.is_multiplayer:
|
||||||
|
if players.size() > 0:
|
||||||
|
var p = players[0]
|
||||||
|
var mins: int = int(p.survival_time / 60.0)
|
||||||
|
var secs: int = int(p.survival_time) % 60
|
||||||
|
var time_str := "%02d:%02d" % [mins, secs]
|
||||||
|
var shields_str := ""
|
||||||
|
for _si in p.current_shields:
|
||||||
|
shields_str += "▣"
|
||||||
|
var status_str := "%s %s %s %s x%d" % [lives_str, cr_str, shields_str, time_str, p.kills]
|
||||||
|
_draw_text(font, status_str, W - 200.0, 10.0, 9, COL_PRIMARY)
|
||||||
|
else:
|
||||||
|
if players.size() >= 1:
|
||||||
|
var p1 = players[0]
|
||||||
|
var t1 := "%02d:%02d" % [int(p1.survival_time / 60.0), int(p1.survival_time) % 60]
|
||||||
|
_draw_text(font, "P1 %s x%d %s" % [t1, p1.kills, cr_str], 10.0, 10.0, 9, COL_PRIMARY)
|
||||||
|
if players.size() >= 2:
|
||||||
|
var p2 = players[1]
|
||||||
|
var t2 := "%02d:%02d" % [int(p2.survival_time / 60.0), int(p2.survival_time) % 60]
|
||||||
|
_draw_text(font, "P2 %s x%d" % [t2, p2.kills], W - 110.0, 10.0, 9,
|
||||||
|
Color(0.27, 1.0, 0.67, 0.55))
|
||||||
|
|
||||||
|
# ── Boost-Cooldown-Leisten (nur Schiffe mit has_boost) ────────────
|
||||||
|
for player in players:
|
||||||
|
var sp: Spaceship = player
|
||||||
|
if sp == null or sp.dead: continue
|
||||||
|
if sp.stats == null or not sp.stats.has_boost: continue
|
||||||
|
var ratio: float = sp.boost_ratio()
|
||||||
|
var bar_w := 40.0; var bar_h := 4.0
|
||||||
|
var bx2: float = sp.x - bar_w * 0.5
|
||||||
|
var by2: float = sp.y + 18.0
|
||||||
|
# Background
|
||||||
|
draw_rect(Rect2(bx2, by2, bar_w, bar_h), Color(0.0, 0.04, 0.02, 0.7))
|
||||||
|
# Fill — remaining cooldown shown as shrinking bar (ratio 1 = full cd, 0 = ready)
|
||||||
|
var fill_w: float = bar_w * (1.0 - ratio)
|
||||||
|
var is_ready: bool = ratio <= 0.0
|
||||||
|
var fill_col: Color
|
||||||
|
if is_ready:
|
||||||
|
fill_col = Color(1.0, 0.9, 0.3, 0.7 + 0.3 * sin(hud.blink_phase * 6.0))
|
||||||
|
else:
|
||||||
|
fill_col = Color(COL_WARNING.r, COL_WARNING.g, COL_WARNING.b, 0.75)
|
||||||
|
draw_rect(Rect2(bx2, by2, fill_w, bar_h), fill_col)
|
||||||
|
# Border
|
||||||
|
draw_rect(Rect2(bx2, by2, bar_w, bar_h),
|
||||||
|
Color(COL_DIM.r, COL_DIM.g, COL_DIM.b, 0.7), false, 1.0)
|
||||||
|
|
||||||
|
# Big wipe warning — bold centred flash, no decoration
|
||||||
|
if hud.wipe_warning:
|
||||||
|
if sin(hud.blink_phase * 8.0) > 0.0:
|
||||||
|
_draw_text_centered(font, Tr.hint("hud_wipe_key"), W * 0.5, H * 0.5 - 8.0, 13, COL_WARNING)
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Gameover / Returning ─────────────────────────────────────────────────
|
||||||
|
if hud.is_gameover or hud.is_returning:
|
||||||
|
# Dim overlay
|
||||||
|
draw_rect(get_viewport_rect(), Color(0.0, 0.0, 0.04, 0.62))
|
||||||
|
|
||||||
|
var sd: Dictionary = hud.score_data
|
||||||
|
var p1_time: float = sd.get("p1_time", 0.0)
|
||||||
|
var p1_kills: int = sd.get("p1_kills", 0)
|
||||||
|
var total: int = int(p1_time) + p1_kills * 50 + int(sd.get("p1_wipe", 0))
|
||||||
|
if sd.get("multiplayer", false):
|
||||||
|
total += int(sd.get("p2_time", 0.0)) + int(sd.get("p2_kills", 0)) * 50 + int(sd.get("p2_wipe", 0))
|
||||||
|
|
||||||
|
# Central terminal box — taller when asking for name
|
||||||
|
var bx := W * 0.5 - 160.0
|
||||||
|
var by := H * 0.5 - 100.0
|
||||||
|
var bw := 320.0
|
||||||
|
var bh2 := 215.0 if hud.awaiting_name else 200.0
|
||||||
|
_draw_terminal_box(bx, by, bw, bh2, COL_PRIMARY)
|
||||||
|
|
||||||
|
_draw_text_centered(font, Tr.t("hud_mission_over"), W * 0.5, by + 22.0, 12, COL_ACCENT)
|
||||||
|
_draw_hline(bx + 16.0, bx + bw - 16.0, by + 42.0, COL_DIM)
|
||||||
|
|
||||||
|
_draw_text_centered(font, Tr.t("hud_score") % total, W * 0.5, by + 60.0, 18, COL_WHITE)
|
||||||
|
|
||||||
|
var mins1: int = int(p1_time / 60.0)
|
||||||
|
var secs1: int = int(p1_time) % 60
|
||||||
|
_draw_text_centered(font, "P1 %02d:%02d KILLS %d" % [mins1, secs1, p1_kills],
|
||||||
|
W * 0.5, by + 92.0, 10, COL_PRIMARY)
|
||||||
|
if sd.get("multiplayer", false):
|
||||||
|
var p2t: float = sd.get("p2_time", 0.0)
|
||||||
|
var mins2: int = int(p2t / 60.0); var secs2: int = int(p2t) % 60
|
||||||
|
_draw_text_centered(font, "P2 %02d:%02d KILLS %d" % [mins2, secs2, sd.get("p2_kills", 0)],
|
||||||
|
W * 0.5, by + 112.0, 10, Color(0.27, 1.0, 0.67, 0.8))
|
||||||
|
|
||||||
|
_draw_hline(bx + 16.0, bx + bw - 16.0, by + 135.0, COL_DIM)
|
||||||
|
|
||||||
|
if hud.is_returning:
|
||||||
|
_draw_text_centered(font, Tr.t("hud_transfer"), W * 0.5, by + 155.0, 10, COL_WARNING)
|
||||||
|
elif hud.awaiting_name:
|
||||||
|
_draw_text_centered(font, Tr.t("lb_enter_name"), W * 0.5, by + 148.0, 10, COL_DIM)
|
||||||
|
var cursor_vis: bool = fmod(hud.blink_phase, 1.0) < 0.6
|
||||||
|
var field_str: String = hud.name_input + ("_" if cursor_vis else " ")
|
||||||
|
var display_str: String = field_str if field_str.strip_edges().length() > 0 else "_"
|
||||||
|
_draw_text_centered(font, display_str, W * 0.5, by + 163.0, 13, COL_ACCENT)
|
||||||
|
_draw_text_centered(font, Tr.hint("lb_name_hint"), W * 0.5, by + 186.0, 8, COL_DIM)
|
||||||
|
elif hud.name_saved:
|
||||||
|
_draw_text_centered(font, Tr.t("lb_saved"), W * 0.5, by + 155.0, 11, COL_ACCENT)
|
||||||
|
elif hud.gameover_blink:
|
||||||
|
_draw_text_centered(font, Tr.hint("hud_press_key"), W * 0.5, by + 155.0, 11, COL_ACCENT)
|
||||||
|
|
||||||
|
# ── Drawing helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _draw_corner_brackets() -> void:
|
||||||
|
# Four L-shaped corner brackets as the only permanent chrome
|
||||||
|
var m := MARGIN
|
||||||
|
var bl := BRACKET_LEN
|
||||||
|
var bw := BRACKET_W
|
||||||
|
# Top-left
|
||||||
|
draw_line(Vector2(m, m), Vector2(m + bl, m), COL_DIM, bw)
|
||||||
|
draw_line(Vector2(m, m), Vector2(m, m + bl), COL_DIM, bw)
|
||||||
|
# Top-right
|
||||||
|
draw_line(Vector2(W-m, m), Vector2(W-m-bl, m), COL_DIM, bw)
|
||||||
|
draw_line(Vector2(W-m, m), Vector2(W-m, m+bl), COL_DIM, bw)
|
||||||
|
# Bottom-left
|
||||||
|
draw_line(Vector2(m, H-m), Vector2(m+bl, H-m), COL_DIM, bw)
|
||||||
|
draw_line(Vector2(m, H-m), Vector2(m, H-m-bl), COL_DIM, bw)
|
||||||
|
# Bottom-right
|
||||||
|
draw_line(Vector2(W-m, H-m), Vector2(W-m-bl, H-m), COL_DIM, bw)
|
||||||
|
draw_line(Vector2(W-m, H-m), Vector2(W-m, H-m-bl), COL_DIM, bw)
|
||||||
|
|
||||||
|
func _draw_terminal_box(bx: float, by: float, bw: float, bh2: float, col: Color) -> void:
|
||||||
|
# Filled dark panel
|
||||||
|
draw_rect(Rect2(bx, by, bw, bh2), Color(0.0, 0.04, 0.02, 0.88))
|
||||||
|
# Border lines
|
||||||
|
var bc := Color(col.r, col.g, col.b, col.a * 0.6)
|
||||||
|
draw_line(Vector2(bx, by), Vector2(bx+bw, by), bc, 1.0)
|
||||||
|
draw_line(Vector2(bx, by+bh2), Vector2(bx+bw, by+bh2), bc, 1.0)
|
||||||
|
draw_line(Vector2(bx, by), Vector2(bx, by+bh2), bc, 1.0)
|
||||||
|
draw_line(Vector2(bx+bw, by), Vector2(bx+bw, by+bh2), bc, 1.0)
|
||||||
|
# Corner L-brackets (bright)
|
||||||
|
var cl := 10.0
|
||||||
|
for cx in [bx, bx+bw]:
|
||||||
|
for cy in [by, by+bh2]:
|
||||||
|
var sx := 1.0 if cx == bx else -1.0
|
||||||
|
var sy := 1.0 if cy == by else -1.0
|
||||||
|
draw_line(Vector2(cx, cy), Vector2(cx + sx*cl, cy), col, 1.5)
|
||||||
|
draw_line(Vector2(cx, cy), Vector2(cx, cy + sy*cl), col, 1.5)
|
||||||
|
|
||||||
|
func _draw_hline(x1: float, x2: float, y: float, col: Color) -> void:
|
||||||
|
draw_line(Vector2(x1, y), Vector2(x2, y), col, 1.0)
|
||||||
|
|
||||||
|
func _draw_text(font: Font, text: String, x: float, y: float, size: int, col: Color) -> void:
|
||||||
|
draw_string(font, Vector2(x, y + size), text, HORIZONTAL_ALIGNMENT_LEFT, -1, size, col)
|
||||||
|
|
||||||
|
func _draw_text_centered(font: Font, text: String, x: float, y: float, size: int, col: Color) -> void:
|
||||||
|
var tw := font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, size).x
|
||||||
|
draw_string(font, Vector2(x - tw * 0.5, y + size), text, HORIZONTAL_ALIGNMENT_LEFT, -1, size, col)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dhqnupvfv7tnt
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
extends RefCounted
|
||||||
|
class_name ItemDB
|
||||||
|
|
||||||
|
# ─── Legacy-System (alter Shop / Enemy-Upgrades in game_world.gd) ──────────────
|
||||||
|
# Bleibt unverändert für Backward-Compat. Wird nur von game_world.gd für
|
||||||
|
# Enemy-Stat-Boosts ab Welle 10 verwendet.
|
||||||
|
|
||||||
|
enum Rarity { COMMON, UNCOMMON, RARE, EPIC }
|
||||||
|
|
||||||
|
const RARITY_WEIGHTS: Array = [60, 25, 12, 3]
|
||||||
|
|
||||||
|
const RARITY_COLORS: Dictionary = {
|
||||||
|
0: Color("#aaaaaa"), # COMMON
|
||||||
|
1: Color("#44ff88"), # UNCOMMON
|
||||||
|
2: Color("#4488ff"), # RARE
|
||||||
|
3: Color("#cc44ff") # EPIC
|
||||||
|
}
|
||||||
|
|
||||||
|
const RARITY_NAMES: Dictionary = {
|
||||||
|
0: "STANDARD",
|
||||||
|
1: "SELTEN",
|
||||||
|
2: "EPISCH",
|
||||||
|
3: "LEGENDÄR"
|
||||||
|
}
|
||||||
|
|
||||||
|
const ITEMS: Array = [
|
||||||
|
{
|
||||||
|
"id": "thrust_1", "name": "Schubverstärker", "rarity": 0, "cost": 50,
|
||||||
|
"desc": "+12% Geschwindigkeit",
|
||||||
|
"effects": { "speed_mult": 1.12 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "firerate_1", "name": "Schnellfeuer I", "rarity": 0, "cost": 50,
|
||||||
|
"desc": "+15% Feuerrate",
|
||||||
|
"effects": { "fire_rate_mult": 1.15 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "damage_1", "name": "Schwere Ladung I", "rarity": 0, "cost": 50,
|
||||||
|
"desc": "+15% Trefferzone",
|
||||||
|
"effects": { "damage_mult": 1.15 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shield_1", "name": "Panzerplatte", "rarity": 0, "cost": 55,
|
||||||
|
"desc": "+1 Schutzladung",
|
||||||
|
"effects": { "shield_charges": 1 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "firerate_2", "name": "Schnellfeuer II", "rarity": 1, "cost": 90,
|
||||||
|
"desc": "+22% Feuerrate",
|
||||||
|
"effects": { "fire_rate_mult": 1.22 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "damage_2", "name": "Schwere Ladung II", "rarity": 1, "cost": 100,
|
||||||
|
"desc": "+22% Trefferzone +10% Projektilgeschwindigkeit",
|
||||||
|
"effects": { "damage_mult": 1.22, "bullet_speed_mult": 1.10 }
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Weighted random roll — Legacy-Shop (wird nicht mehr für Werkstatt verwendet)
|
||||||
|
static func roll_shop(count: int, _exclude_ids: Array = []) -> Array:
|
||||||
|
var pool: Array = []
|
||||||
|
for item: Dictionary in ITEMS:
|
||||||
|
var w: int = RARITY_WEIGHTS[int(item["rarity"])]
|
||||||
|
for _i in w:
|
||||||
|
pool.append(item)
|
||||||
|
pool.shuffle()
|
||||||
|
var result: Array = []
|
||||||
|
var seen_ids: Dictionary = {}
|
||||||
|
for item: Dictionary in pool:
|
||||||
|
var iid: String = item["id"]
|
||||||
|
if iid in seen_ids: continue
|
||||||
|
seen_ids[iid] = true
|
||||||
|
result.append(item)
|
||||||
|
if result.size() >= count:
|
||||||
|
break
|
||||||
|
return result
|
||||||
|
|
||||||
|
# ─── Werkstatt-System (Plugin-basiert) ─────────────────────────────────────────
|
||||||
|
# Auto-Discovery: alle *.gd unter res://items/ die ItemDef erweitern werden
|
||||||
|
# beim ersten Zugriff registriert.
|
||||||
|
|
||||||
|
static var _registry: Array = []
|
||||||
|
static var _registry_loaded: bool = false
|
||||||
|
|
||||||
|
# Static preload list — DirAccess doesn't work in exported builds (compiled scripts
|
||||||
|
# aren't raw files in the PCK). preload() is resolved at compile time and always works.
|
||||||
|
const _ITEM_SCRIPTS: Array = [
|
||||||
|
preload("res://items/weapons/wk_burst.gd"),
|
||||||
|
preload("res://items/weapons/wk_charge.gd"),
|
||||||
|
preload("res://items/weapons/wk_ion.gd"),
|
||||||
|
preload("res://items/weapons/wk_laser.gd"),
|
||||||
|
preload("res://items/weapons/wk_plasma.gd"),
|
||||||
|
preload("res://items/weapons/wk_rail.gd"),
|
||||||
|
preload("res://items/weapons/wk_scatter.gd"),
|
||||||
|
preload("res://items/weapons/wk_shotgun.gd"),
|
||||||
|
preload("res://items/weapons/wk_sniper.gd"),
|
||||||
|
preload("res://items/hull/hull_giant.gd"),
|
||||||
|
preload("res://items/hull/hull_nullfeld.gd"),
|
||||||
|
preload("res://items/hull/hull_plating.gd"),
|
||||||
|
preload("res://items/hull/hull_reaktor.gd"),
|
||||||
|
preload("res://items/drives/drive_overdrive.gd"),
|
||||||
|
preload("res://items/drives/drive_quantum.gd"),
|
||||||
|
preload("res://items/drives/drive_steer.gd"),
|
||||||
|
preload("res://items/special/special_credit_mag.gd"),
|
||||||
|
preload("res://items/special/special_wipe_core.gd"),
|
||||||
|
]
|
||||||
|
|
||||||
|
static func _ensure_loaded() -> void:
|
||||||
|
if _registry_loaded: return
|
||||||
|
_registry_loaded = true
|
||||||
|
for script: Script in _ITEM_SCRIPTS:
|
||||||
|
if script == null or not script.can_instantiate(): continue
|
||||||
|
var inst = script.new()
|
||||||
|
if inst is ItemDef and (inst as ItemDef).id != "":
|
||||||
|
_registry.append(inst)
|
||||||
|
|
||||||
|
static func get_all_defs() -> Array:
|
||||||
|
_ensure_loaded()
|
||||||
|
return _registry
|
||||||
|
|
||||||
|
static func get_def_by_id(id: String) -> ItemDef:
|
||||||
|
_ensure_loaded()
|
||||||
|
for item in _registry:
|
||||||
|
if (item as ItemDef).id == id:
|
||||||
|
return item
|
||||||
|
return null
|
||||||
|
|
||||||
|
# Rolls `count` random items from the Werkstatt pool, excluding given IDs.
|
||||||
|
static func roll_werkstatt(count: int, exclude_ids: Array = []) -> Array:
|
||||||
|
_ensure_loaded()
|
||||||
|
var pool: Array = []
|
||||||
|
for item in _registry:
|
||||||
|
var def := item as ItemDef
|
||||||
|
if exclude_ids.has(def.id):
|
||||||
|
continue
|
||||||
|
pool.append(def)
|
||||||
|
pool.shuffle()
|
||||||
|
var n: int = min(count, pool.size())
|
||||||
|
return pool.slice(0, n)
|
||||||
|
|
||||||
|
# ─── Effect-Klassifizierung (für UI-Farbkodierung) ────────────────────────────
|
||||||
|
|
||||||
|
static func is_positive_effect(key: String, val) -> bool:
|
||||||
|
match key:
|
||||||
|
"speed_mult", "turn_mult", "fire_rate_mult", "damage_mult", \
|
||||||
|
"bullet_speed_mult", "invuln_mult", "credit_bonus":
|
||||||
|
return float(val) >= 1.0
|
||||||
|
"bullet_count", "shield_charges":
|
||||||
|
return int(val) > 0
|
||||||
|
"bh_resist":
|
||||||
|
return float(val) > 0.0
|
||||||
|
"wipe_mult":
|
||||||
|
return float(val) <= 1.0 # kleiner = schneller = besser
|
||||||
|
return true
|
||||||
|
|
||||||
|
# Menschlich lesbarer Stat-Name für die Vorschau.
|
||||||
|
static func stat_display_name(key: String) -> String:
|
||||||
|
match key:
|
||||||
|
"speed_mult": return "Speed"
|
||||||
|
"turn_mult": return "Wendung"
|
||||||
|
"fire_rate_mult": return "Feuerrate"
|
||||||
|
"damage_mult": return "Schaden"
|
||||||
|
"bullet_speed_mult": return "Proj.-Speed"
|
||||||
|
"bullet_count": return "Projektile"
|
||||||
|
"shield_charges": return "Schilde"
|
||||||
|
"invuln_mult": return "Unverwundbar"
|
||||||
|
"bh_resist": return "BH-Resistenz"
|
||||||
|
"wipe_mult": return "Wipe-Ladezeit"
|
||||||
|
"credit_bonus": return "Kreditgewinn"
|
||||||
|
return key
|
||||||
|
|
||||||
|
static func get_by_id(id: String) -> Dictionary:
|
||||||
|
for item: Dictionary in ITEMS:
|
||||||
|
if item["id"] == id:
|
||||||
|
return item
|
||||||
|
return {}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://sfb1ybt3r8ne
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
extends RefCounted
|
||||||
|
class_name ItemDef
|
||||||
|
|
||||||
|
# Base class for all Werkstatt items. Each item is its own .gd file in res://items/
|
||||||
|
# that extends ItemDef and fills the fields in _init().
|
||||||
|
# ItemDB auto-discovers all .gd files in res://items/ at first access.
|
||||||
|
|
||||||
|
# ─── Pflichtfelder ────────────────────────────────────────────────────────────
|
||||||
|
var id: String = ""
|
||||||
|
var name: String = ""
|
||||||
|
var name_en: String = ""
|
||||||
|
var desc: String = ""
|
||||||
|
var desc_en: String = ""
|
||||||
|
var category: String = "" # "WAFFENMODUL" | "HÜLLENMOD" | "ANTRIEBSMOD" | "SPEZIAL"
|
||||||
|
var category_en: String = "" # "WEAPON MOD" | "HULL MOD" | "DRIVE MOD" | "SPECIAL"
|
||||||
|
var icon: String = "◈"
|
||||||
|
var cost: int = 50
|
||||||
|
var rarity: int = 0 # 0=Common, 1=Uncommon, 2=Rare, 3=Epic
|
||||||
|
var effects: Dictionary = {} # ShipStats keys → multipliers/additives
|
||||||
|
|
||||||
|
# ─── Visuelle Anhänge am Schiff ───────────────────────────────────────────────
|
||||||
|
# Format: [[x_offset, y_offset, "color_key"], ...]
|
||||||
|
# color_key muss ein Schlüssel in der Schiffs-Palette sein (nose, bright, mid,
|
||||||
|
# dim, accent, edge, shadow) oder "white" / "red" / "yellow" / "green" als Fallback.
|
||||||
|
var visual_pixels: Array = []
|
||||||
|
|
||||||
|
# ─── Schiffswachstum ──────────────────────────────────────────────────────────
|
||||||
|
# Nur Items mit hull_size_bonus > 0 vergrößern das Schiff.
|
||||||
|
var hull_size_bonus: float = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cxa4d1ome3iap
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
const MAX_ENTRIES := 10
|
||||||
|
const SAVE_PATH := "user://leaderboard.cfg"
|
||||||
|
|
||||||
|
var _scores: Array = []
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_load()
|
||||||
|
|
||||||
|
func add_score(player_name: String, score: int, wave: int) -> void:
|
||||||
|
var entry := {
|
||||||
|
"name": player_name.strip_edges().left(12),
|
||||||
|
"score": score,
|
||||||
|
"wave": wave,
|
||||||
|
"date": Time.get_date_string_from_system()
|
||||||
|
}
|
||||||
|
_scores.append(entry)
|
||||||
|
_scores.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||||
|
return a["score"] > b["score"])
|
||||||
|
if _scores.size() > MAX_ENTRIES:
|
||||||
|
_scores.resize(MAX_ENTRIES)
|
||||||
|
_save()
|
||||||
|
|
||||||
|
func get_scores() -> Array:
|
||||||
|
return _scores
|
||||||
|
|
||||||
|
func is_highscore(score: int) -> bool:
|
||||||
|
if _scores.size() < MAX_ENTRIES:
|
||||||
|
return true
|
||||||
|
return score > (_scores[-1] as Dictionary).get("score", 0)
|
||||||
|
|
||||||
|
func _save() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
for i: int in _scores.size():
|
||||||
|
var e: Dictionary = _scores[i]
|
||||||
|
cfg.set_value("entry_%d" % i, "name", e.get("name", "???"))
|
||||||
|
cfg.set_value("entry_%d" % i, "score", e.get("score", 0))
|
||||||
|
cfg.set_value("entry_%d" % i, "wave", e.get("wave", 1))
|
||||||
|
cfg.set_value("entry_%d" % i, "date", e.get("date", ""))
|
||||||
|
cfg.save(SAVE_PATH)
|
||||||
|
|
||||||
|
func _load() -> void:
|
||||||
|
_scores = []
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
if cfg.load(SAVE_PATH) != OK:
|
||||||
|
return
|
||||||
|
var i := 0
|
||||||
|
while cfg.has_section("entry_%d" % i):
|
||||||
|
_scores.append({
|
||||||
|
"name": cfg.get_value("entry_%d" % i, "name", "???"),
|
||||||
|
"score": cfg.get_value("entry_%d" % i, "score", 0),
|
||||||
|
"wave": cfg.get_value("entry_%d" % i, "wave", 1),
|
||||||
|
"date": cfg.get_value("entry_%d" % i, "date", "")
|
||||||
|
})
|
||||||
|
i += 1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cs0jfxgljs4rl
|
||||||
+367
@@ -0,0 +1,367 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# ─── State Machine ───────────────────────────────────────────────────────────
|
||||||
|
enum State { MAIN_MENU, SELECT, SELECT_P2, LAUNCHING, PLAYING, RETURNING, WAVE_CLEAR, SHOP, GAMEOVER, PAUSED }
|
||||||
|
var state: State = State.SELECT
|
||||||
|
|
||||||
|
# ─── Ship data ───────────────────────────────────────────────────────────────
|
||||||
|
const SHIPS := [
|
||||||
|
{ "id": "classic", "name": "NOVA-1",
|
||||||
|
"nose": Color("#ffffff"), "bright": Color("#dddddd"), "mid": Color("#cccccc"),
|
||||||
|
"dim": Color("#aaaaaa"), "accent": Color("#88aaff"), "edge": Color("#888888"),
|
||||||
|
"shadow": Color("#666688"), "trail": Color(0.533,0.667,1.0,0.251),
|
||||||
|
"thrustHot": Color("#ffcc44"), "thrustCool": Color(1.0,0.533,0.267,0.533) },
|
||||||
|
{ "id": "inferno", "name": "INFERNO",
|
||||||
|
"nose": Color("#ffeecc"), "bright": Color("#ffaa66"), "mid": Color("#ee6633"),
|
||||||
|
"dim": Color("#aa3322"), "accent": Color("#ff4444"), "edge": Color("#882211"),
|
||||||
|
"shadow": Color("#551111"), "trail": Color(1.0,0.4,0.251,0.251),
|
||||||
|
"thrustHot": Color("#ffee44"), "thrustCool": Color(1.0,0.267,0.0,0.533) },
|
||||||
|
{ "id": "aurora", "name": "AURORA",
|
||||||
|
"nose": Color("#ddffee"), "bright": Color("#aaffcc"), "mid": Color("#66cc99"),
|
||||||
|
"dim": Color("#338866"), "accent": Color("#44ccff"), "edge": Color("#226655"),
|
||||||
|
"shadow": Color("#114433"), "trail": Color(0.267,0.8,1.0,0.251),
|
||||||
|
"thrustHot": Color("#aaffcc"), "thrustCool": Color(0.267,0.8,1.0,0.533) },
|
||||||
|
{ "id": "titan", "name": "TITAN",
|
||||||
|
"nose": Color("#ffffdd"), "bright": Color("#ddcc88"), "mid": Color("#aa9944"),
|
||||||
|
"dim": Color("#776633"), "accent": Color("#ffdd44"), "edge": Color("#554422"),
|
||||||
|
"shadow": Color("#332211"), "trail": Color(1.0,0.85,0.25,0.251),
|
||||||
|
"thrustHot": Color("#ffffff"), "thrustCool": Color(1.0,0.67,0.0,0.533) },
|
||||||
|
]
|
||||||
|
|
||||||
|
# ─── Game state ───────────────────────────────────────────────────────────────
|
||||||
|
var selected_ship_p1 := 0
|
||||||
|
var selected_ship_p2 := 0
|
||||||
|
var is_multiplayer := false
|
||||||
|
var launch_timer := 0.0
|
||||||
|
var return_timer := 0.0
|
||||||
|
var blink_phase := 0.0
|
||||||
|
|
||||||
|
# ─── Run state (roguelite) ────────────────────────────────────────────────────
|
||||||
|
const MAX_LIVES := 3
|
||||||
|
var lives_p1: int = MAX_LIVES
|
||||||
|
var credits_p1: int = 0
|
||||||
|
var stats_p1: ShipStats = null
|
||||||
|
var owned_items_p1: Array = []
|
||||||
|
var credits_p2: int = 0
|
||||||
|
var stats_p2: ShipStats = null
|
||||||
|
var owned_items_p2: Array = []
|
||||||
|
var wave_number: int = 1
|
||||||
|
var _shop_player: int = 1
|
||||||
|
# Reroll-Counter persistiert über den gesamten Run (nicht pro Shop zurückgesetzt).
|
||||||
|
# Jeder Reroll verteuert den nächsten auch welleübergreifend — Snowball-Schutz.
|
||||||
|
var reroll_count_p1: int = 0
|
||||||
|
var reroll_count_p2: int = 0
|
||||||
|
|
||||||
|
# ─── References ──────────────────────────────────────────────────────────────
|
||||||
|
@onready var game_world: Node2D = $GameWorld
|
||||||
|
@onready var ship_select_ui: Node2D = $ShipSelectUI
|
||||||
|
@onready var hud: CanvasLayer = $HUD
|
||||||
|
@onready var shop_ui: Node2D = $ShopUI
|
||||||
|
@onready var pause_menu: Node2D = $PauseMenu
|
||||||
|
@onready var main_menu: Node2D = $MainMenu
|
||||||
|
@onready var atlas_ui: Node2D = $AtlasUI
|
||||||
|
@onready var music_player: Node = $MusicPlayer
|
||||||
|
@onready var touch_controls: Node2D = $HUD/TouchControls
|
||||||
|
|
||||||
|
func start_boss_music(is_final: bool = false) -> void:
|
||||||
|
if music_player:
|
||||||
|
if is_final:
|
||||||
|
music_player.play_boss_leviathan()
|
||||||
|
else:
|
||||||
|
music_player.play_boss()
|
||||||
|
|
||||||
|
func stop_boss_music() -> void:
|
||||||
|
if music_player:
|
||||||
|
music_player.play_normal()
|
||||||
|
|
||||||
|
func boss_phase_changed(new_phase: int) -> void:
|
||||||
|
if music_player and new_phase == 2:
|
||||||
|
music_player.enter_phase2()
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
game_world.main_node = self
|
||||||
|
hud.game_world = game_world
|
||||||
|
shop_ui.shop_closed.connect(_on_shop_closed)
|
||||||
|
pause_menu.resume_requested.connect(_on_pause_resume)
|
||||||
|
pause_menu.quit_to_menu_requested.connect(_on_pause_quit_menu)
|
||||||
|
main_menu.mode_selected.connect(_on_mode_selected)
|
||||||
|
main_menu.atlas_requested.connect(_on_atlas_requested)
|
||||||
|
main_menu.quit_requested.connect(func(): get_tree().quit())
|
||||||
|
atlas_ui.closed.connect(_on_atlas_closed)
|
||||||
|
touch_controls.game_world = game_world
|
||||||
|
touch_controls.direct_touch_ui = shop_ui
|
||||||
|
stats_p1 = ShipStats.new()
|
||||||
|
stats_p2 = ShipStats.new()
|
||||||
|
_set_state(State.MAIN_MENU)
|
||||||
|
|
||||||
|
# Sets ship-specific base stats (e.g. TITAN starts slower + has boost).
|
||||||
|
# Only applied on fresh ShipStats (no previously-bought items yet).
|
||||||
|
static func _apply_ship_base_stats(stats: ShipStats, ship_id: String) -> void:
|
||||||
|
if stats == null: return
|
||||||
|
stats.ship_id = ship_id
|
||||||
|
match ship_id:
|
||||||
|
"classic":
|
||||||
|
stats.shield_charges = 1
|
||||||
|
"inferno":
|
||||||
|
stats.speed_mult = 1.28
|
||||||
|
stats.turn_mult = 0.72
|
||||||
|
stats.fire_rate_mult = 1.55
|
||||||
|
"aurora":
|
||||||
|
stats.speed_mult = 0.80
|
||||||
|
stats.turn_mult = 1.55
|
||||||
|
stats.invuln_mult = 1.80
|
||||||
|
stats.shield_charges = 2
|
||||||
|
stats.bh_resist = 0.55
|
||||||
|
"titan":
|
||||||
|
stats.speed_mult = 0.72
|
||||||
|
stats.turn_mult = 0.85
|
||||||
|
stats.has_boost = true
|
||||||
|
stats.boost_cooldown_max = 5.0
|
||||||
|
|
||||||
|
func _set_state(new_state: State) -> void:
|
||||||
|
state = new_state
|
||||||
|
match state:
|
||||||
|
State.MAIN_MENU:
|
||||||
|
main_menu.visible = true
|
||||||
|
ship_select_ui.visible = false
|
||||||
|
shop_ui.visible = false
|
||||||
|
pause_menu.visible = false
|
||||||
|
hud.visible = false
|
||||||
|
game_world.visible = true
|
||||||
|
game_world.init_menu_mode()
|
||||||
|
touch_controls.set_mode(touch_controls.Mode.MENU)
|
||||||
|
State.SELECT:
|
||||||
|
main_menu.visible = false
|
||||||
|
ship_select_ui.visible = true
|
||||||
|
shop_ui.visible = false
|
||||||
|
game_world.visible = true
|
||||||
|
hud.visible = false
|
||||||
|
game_world.init_menu_mode()
|
||||||
|
ship_select_ui.start_select(false, selected_ship_p1, SHIPS)
|
||||||
|
touch_controls.set_mode(touch_controls.Mode.MENU)
|
||||||
|
State.SELECT_P2:
|
||||||
|
ship_select_ui.start_select(true, selected_ship_p2, SHIPS)
|
||||||
|
touch_controls.set_mode(touch_controls.Mode.MENU)
|
||||||
|
State.LAUNCHING:
|
||||||
|
ship_select_ui.visible = false
|
||||||
|
shop_ui.visible = false
|
||||||
|
game_world.visible = true
|
||||||
|
hud.visible = true
|
||||||
|
hud.is_gameover = false
|
||||||
|
hud.is_returning = false
|
||||||
|
hud.show_countdown_flag = false
|
||||||
|
hud.credits = credits_p1
|
||||||
|
hud.lives = lives_p1
|
||||||
|
launch_timer = 3.0
|
||||||
|
# Apply ship-base-stats once on first launch (wave 1). Later waves
|
||||||
|
# preserve the stats the player earned from Werkstatt upgrades.
|
||||||
|
if wave_number == 1:
|
||||||
|
_apply_ship_base_stats(stats_p1, SHIPS[selected_ship_p1]["id"])
|
||||||
|
if is_multiplayer:
|
||||||
|
_apply_ship_base_stats(stats_p2, SHIPS[selected_ship_p2]["id"])
|
||||||
|
var s2 = SHIPS[selected_ship_p2] if is_multiplayer else null
|
||||||
|
game_world.init_world(SHIPS[selected_ship_p1], s2, is_multiplayer, stats_p1, stats_p2, wave_number)
|
||||||
|
touch_controls.set_mode(touch_controls.Mode.GAME)
|
||||||
|
State.PLAYING:
|
||||||
|
get_tree().paused = false
|
||||||
|
pause_menu.visible = false
|
||||||
|
game_world.start_playing()
|
||||||
|
touch_controls.set_mode(touch_controls.Mode.GAME)
|
||||||
|
State.PAUSED:
|
||||||
|
pause_menu.open()
|
||||||
|
touch_controls.set_mode(touch_controls.Mode.MENU)
|
||||||
|
State.RETURNING:
|
||||||
|
lives_p1 -= 1
|
||||||
|
hud.is_returning = true
|
||||||
|
hud.lives = lives_p1
|
||||||
|
return_timer = 0.85
|
||||||
|
game_world.on_player_died()
|
||||||
|
touch_controls.set_mode(touch_controls.Mode.HIDDEN)
|
||||||
|
State.WAVE_CLEAR:
|
||||||
|
hud.wave_cleared = true
|
||||||
|
return_timer = 2.0
|
||||||
|
game_world.on_player_died()
|
||||||
|
touch_controls.set_mode(touch_controls.Mode.HIDDEN)
|
||||||
|
State.SHOP:
|
||||||
|
ship_select_ui.visible = false
|
||||||
|
hud.visible = false
|
||||||
|
game_world.visible = true
|
||||||
|
shop_ui.visible = true
|
||||||
|
if _shop_player == 1:
|
||||||
|
shop_ui.open(lives_p1, credits_p1, stats_p1, owned_items_p1,
|
||||||
|
"SPIELER 1" if is_multiplayer else "",
|
||||||
|
SHIPS[selected_ship_p1], wave_number, reroll_count_p1, 1)
|
||||||
|
else:
|
||||||
|
shop_ui.open(lives_p1, credits_p2, stats_p2, owned_items_p2, "SPIELER 2",
|
||||||
|
SHIPS[selected_ship_p2], wave_number, reroll_count_p2, 2)
|
||||||
|
touch_controls.set_mode(touch_controls.Mode.MENU)
|
||||||
|
State.GAMEOVER:
|
||||||
|
shop_ui.visible = false
|
||||||
|
hud.show_gameover(game_world.get_score_data())
|
||||||
|
touch_controls.set_mode(touch_controls.Mode.HIDDEN)
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
blink_phase += delta
|
||||||
|
match state:
|
||||||
|
State.LAUNCHING:
|
||||||
|
launch_timer -= delta
|
||||||
|
hud.show_countdown(ceil(launch_timer))
|
||||||
|
if launch_timer <= 0.0:
|
||||||
|
_set_state(State.PLAYING)
|
||||||
|
State.RETURNING:
|
||||||
|
return_timer -= delta
|
||||||
|
if return_timer <= 0.0:
|
||||||
|
if lives_p1 > 0:
|
||||||
|
_set_state(State.SHOP)
|
||||||
|
else:
|
||||||
|
_set_state(State.GAMEOVER)
|
||||||
|
State.WAVE_CLEAR:
|
||||||
|
return_timer -= delta
|
||||||
|
if return_timer <= 0.0:
|
||||||
|
hud.wave_cleared = false
|
||||||
|
_set_state(State.SHOP)
|
||||||
|
|
||||||
|
# Detect which type of input device the player is currently using.
|
||||||
|
# Drives adaptive UI labels via Tr.hint().
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
|
||||||
|
if Settings.last_input_device != "pad":
|
||||||
|
Settings.last_input_device = "pad"
|
||||||
|
elif event is InputEventKey or event is InputEventMouseButton:
|
||||||
|
if Settings.last_input_device != "keyboard":
|
||||||
|
Settings.last_input_device = "keyboard"
|
||||||
|
# "touch" is set by touch_controls.gd when a screen touch arrives.
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
match state:
|
||||||
|
State.PLAYING:
|
||||||
|
if event.is_action_pressed("ui_cancel"):
|
||||||
|
get_tree().paused = true
|
||||||
|
_set_state(State.PAUSED)
|
||||||
|
return
|
||||||
|
State.SELECT:
|
||||||
|
if event.is_action_pressed("ui_left"):
|
||||||
|
selected_ship_p1 = (selected_ship_p1 - 1 + SHIPS.size()) % SHIPS.size()
|
||||||
|
ship_select_ui.set_selection(selected_ship_p1)
|
||||||
|
elif event.is_action_pressed("ui_right"):
|
||||||
|
selected_ship_p1 = (selected_ship_p1 + 1) % SHIPS.size()
|
||||||
|
ship_select_ui.set_selection(selected_ship_p1)
|
||||||
|
elif event.is_action_pressed("ui_accept"):
|
||||||
|
if is_multiplayer:
|
||||||
|
_set_state(State.SELECT_P2)
|
||||||
|
else:
|
||||||
|
_set_state(State.LAUNCHING)
|
||||||
|
elif event.is_action_pressed("ui_cancel"):
|
||||||
|
_set_state(State.MAIN_MENU)
|
||||||
|
State.SELECT_P2:
|
||||||
|
if event.is_action_pressed("ui_left"):
|
||||||
|
selected_ship_p2 = (selected_ship_p2 - 1 + SHIPS.size()) % SHIPS.size()
|
||||||
|
ship_select_ui.set_selection(selected_ship_p2)
|
||||||
|
elif event.is_action_pressed("ui_right"):
|
||||||
|
selected_ship_p2 = (selected_ship_p2 + 1) % SHIPS.size()
|
||||||
|
ship_select_ui.set_selection(selected_ship_p2)
|
||||||
|
elif event.is_action_pressed("ui_accept"):
|
||||||
|
_set_state(State.LAUNCHING)
|
||||||
|
elif event.is_action_pressed("ui_cancel"):
|
||||||
|
_set_state(State.SELECT)
|
||||||
|
State.GAMEOVER:
|
||||||
|
if event.is_pressed():
|
||||||
|
_reset_game()
|
||||||
|
|
||||||
|
func _reset_game() -> void:
|
||||||
|
stop_boss_music() # Boss-Musik stoppen falls aktiv
|
||||||
|
selected_ship_p1 = 0
|
||||||
|
selected_ship_p2 = 0
|
||||||
|
is_multiplayer = false
|
||||||
|
hud.is_gameover = false
|
||||||
|
hud.is_returning = false
|
||||||
|
hud.show_countdown_flag = false
|
||||||
|
# Reset roguelite run state
|
||||||
|
lives_p1 = MAX_LIVES
|
||||||
|
credits_p1 = 0
|
||||||
|
stats_p1 = ShipStats.new()
|
||||||
|
owned_items_p1 = []
|
||||||
|
credits_p2 = 0
|
||||||
|
stats_p2 = ShipStats.new()
|
||||||
|
owned_items_p2 = []
|
||||||
|
reroll_count_p1 = 0
|
||||||
|
reroll_count_p2 = 0
|
||||||
|
_shop_player = 1
|
||||||
|
wave_number = 1
|
||||||
|
hud.wave_number = 1
|
||||||
|
hud.wave_cleared = false
|
||||||
|
_set_state(State.MAIN_MENU)
|
||||||
|
|
||||||
|
func add_credits(player_idx: int, amount: int) -> void:
|
||||||
|
if player_idx == 0:
|
||||||
|
credits_p1 += amount
|
||||||
|
hud.credits = credits_p1
|
||||||
|
elif player_idx == 1:
|
||||||
|
credits_p2 += amount
|
||||||
|
|
||||||
|
func on_wave_complete() -> void:
|
||||||
|
_set_state(State.WAVE_CLEAR)
|
||||||
|
|
||||||
|
func _on_shop_closed(remaining_credits: int, new_stats: ShipStats, items: Array, reroll_count: int) -> void:
|
||||||
|
if _shop_player == 1:
|
||||||
|
credits_p1 = remaining_credits
|
||||||
|
stats_p1 = new_stats
|
||||||
|
owned_items_p1 = items
|
||||||
|
reroll_count_p1 = reroll_count
|
||||||
|
if is_multiplayer:
|
||||||
|
_shop_player = 2
|
||||||
|
_set_state(State.SHOP)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
credits_p2 = remaining_credits
|
||||||
|
stats_p2 = new_stats
|
||||||
|
owned_items_p2 = items
|
||||||
|
reroll_count_p2 = reroll_count
|
||||||
|
_shop_player = 1
|
||||||
|
wave_number += 1
|
||||||
|
hud.wave_number = wave_number
|
||||||
|
_set_state(State.LAUNCHING)
|
||||||
|
|
||||||
|
func _on_mode_selected(multi: bool) -> void:
|
||||||
|
is_multiplayer = multi
|
||||||
|
main_menu.visible = false
|
||||||
|
# Reset run state + apply fresh ship-base stats (TITAN starts slower etc.).
|
||||||
|
# The base stats are applied AFTER ship selection in _set_state(LAUNCHING).
|
||||||
|
lives_p1 = MAX_LIVES
|
||||||
|
credits_p1 = 0
|
||||||
|
stats_p1 = ShipStats.new()
|
||||||
|
owned_items_p1 = []
|
||||||
|
credits_p2 = 0
|
||||||
|
stats_p2 = ShipStats.new()
|
||||||
|
owned_items_p2 = []
|
||||||
|
reroll_count_p1 = 0
|
||||||
|
reroll_count_p2 = 0
|
||||||
|
_shop_player = 1
|
||||||
|
wave_number = 1
|
||||||
|
hud.wave_number = 1
|
||||||
|
_set_state(State.SELECT)
|
||||||
|
|
||||||
|
func _on_atlas_requested() -> void:
|
||||||
|
main_menu.visible = false
|
||||||
|
atlas_ui.visible = true
|
||||||
|
if atlas_ui.has_method("open"):
|
||||||
|
atlas_ui.open()
|
||||||
|
touch_controls.set_mode(touch_controls.Mode.MENU)
|
||||||
|
|
||||||
|
func _on_atlas_closed() -> void:
|
||||||
|
atlas_ui.visible = false
|
||||||
|
main_menu.visible = true
|
||||||
|
touch_controls.set_mode(touch_controls.Mode.MENU)
|
||||||
|
|
||||||
|
func _on_pause_resume() -> void:
|
||||||
|
get_tree().paused = false
|
||||||
|
state = State.PLAYING
|
||||||
|
|
||||||
|
func _on_pause_quit_menu() -> void:
|
||||||
|
get_tree().paused = false
|
||||||
|
_reset_game()
|
||||||
|
|
||||||
|
# Called by game_world when all players are dead
|
||||||
|
func on_game_over() -> void:
|
||||||
|
stop_boss_music() # Boss-Musik stoppen falls aktiv
|
||||||
|
_set_state(State.RETURNING)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ctbcvqj0aoo3v
|
||||||
@@ -0,0 +1,510 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# Hauptmenü — erscheint beim Start.
|
||||||
|
# Hintergrund-Simulation läuft durch (game_world im Menu-Modus).
|
||||||
|
|
||||||
|
signal mode_selected(is_multiplayer: bool)
|
||||||
|
signal atlas_requested
|
||||||
|
signal quit_requested
|
||||||
|
|
||||||
|
var W: float = 960.0
|
||||||
|
var H: float = 600.0
|
||||||
|
|
||||||
|
const COL_BG := Color(0.0, 0.0, 0.04, 0.65)
|
||||||
|
const COL_PRIMARY := Color(0.0, 1.0, 0.533, 1.0)
|
||||||
|
const COL_ACCENT := Color(0.27, 1.0, 0.8, 1.0)
|
||||||
|
const COL_DIM := Color(0.0, 0.27, 0.13, 0.55)
|
||||||
|
const COL_WHITE := Color(1.0, 1.0, 1.0, 0.90)
|
||||||
|
const COL_WARN := Color(1.0, 0.67, 0.0, 0.90)
|
||||||
|
|
||||||
|
# 0 = Hauptmenü, 1 = Optionen, 2 = Steuerung, 3 = Leaderboard
|
||||||
|
var _screen: int = 0
|
||||||
|
var _cursor: int = 0
|
||||||
|
var _blink: float = 0.0
|
||||||
|
var _opt_cursor: int = 0
|
||||||
|
var _ctrl_cursor: int = 0
|
||||||
|
var _ctrl_waiting: bool = false # wartet auf Taste-Drücken zum Binden
|
||||||
|
|
||||||
|
# Leaderboard state
|
||||||
|
var _lb_view: int = 0 # 0=lokal, 1=online
|
||||||
|
var _lb_online_data: Array = []
|
||||||
|
var _lb_loading: bool = false
|
||||||
|
var _lb_error: String = ""
|
||||||
|
var _online_lb_node: Node = null
|
||||||
|
|
||||||
|
# Abwärtskompatibilität
|
||||||
|
var _show_options: bool:
|
||||||
|
get: return _screen == 1
|
||||||
|
set(v): _screen = 1 if v else 0
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
func _items() -> Array:
|
||||||
|
return [Tr.t("menu_single"), Tr.t("menu_vs"), Tr.t("menu_leaderboard"), Tr.t("menu_options"), Tr.t("menu_atlas"), Tr.t("menu_quit")]
|
||||||
|
|
||||||
|
func _opt_items() -> Array:
|
||||||
|
return [Tr.t("opt_sfx"), Tr.t("opt_music"), Tr.t("opt_mute"), Tr.t("opt_fullscreen"),
|
||||||
|
Tr.t("opt_nebula"), Tr.t("opt_stars"), Tr.t("opt_language"), Tr.t("opt_touch"),
|
||||||
|
Tr.t("opt_controls"), Tr.t("opt_back")]
|
||||||
|
|
||||||
|
func _ctrl_labels() -> Array:
|
||||||
|
return [Tr.t("ctrl_thrust"), Tr.t("ctrl_left"), Tr.t("ctrl_right"),
|
||||||
|
Tr.t("ctrl_shoot"), Tr.t("ctrl_wipe")]
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if not visible: return
|
||||||
|
_blink += delta
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if not visible: return
|
||||||
|
match _screen:
|
||||||
|
0: _input_menu(event)
|
||||||
|
1: _input_options(event)
|
||||||
|
2: _input_controls(event)
|
||||||
|
3: _input_leaderboard(event)
|
||||||
|
|
||||||
|
func _input_menu(event: InputEvent) -> void:
|
||||||
|
var items := _items()
|
||||||
|
if event.is_action_pressed("ui_up"):
|
||||||
|
_cursor = (_cursor - 1 + items.size()) % items.size()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif event.is_action_pressed("ui_down"):
|
||||||
|
_cursor = (_cursor + 1) % items.size()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif event.is_action_pressed("ui_accept"):
|
||||||
|
# Consume the event FIRST so the same ENTER doesn't bubble up to
|
||||||
|
# main.gd._unhandled_input and auto-confirm the next state's UI.
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
match _cursor:
|
||||||
|
0: mode_selected.emit(false)
|
||||||
|
1: mode_selected.emit(true)
|
||||||
|
2: _screen = 3; _lb_view = 0; _lb_online_data = []; _lb_error = ""; _lb_loading = false
|
||||||
|
3: _screen = 1; _opt_cursor = 0
|
||||||
|
4: atlas_requested.emit()
|
||||||
|
5: quit_requested.emit()
|
||||||
|
|
||||||
|
func _input_options(event: InputEvent) -> void:
|
||||||
|
var opts := _opt_items()
|
||||||
|
if event.is_action_pressed("ui_up"):
|
||||||
|
_opt_cursor = (_opt_cursor - 1 + opts.size()) % opts.size()
|
||||||
|
elif event.is_action_pressed("ui_down"):
|
||||||
|
_opt_cursor = (_opt_cursor + 1) % opts.size()
|
||||||
|
elif event.is_action_pressed("ui_left"):
|
||||||
|
_change_option(_opt_cursor, -1)
|
||||||
|
elif event.is_action_pressed("ui_right"):
|
||||||
|
_change_option(_opt_cursor, 1)
|
||||||
|
elif event.is_action_pressed("ui_accept"):
|
||||||
|
if _opt_cursor == opts.size() - 1: # Back
|
||||||
|
_screen = 0
|
||||||
|
elif _opt_cursor == opts.size() - 2: # Controls
|
||||||
|
_screen = 2; _ctrl_cursor = 0; _ctrl_waiting = false
|
||||||
|
else:
|
||||||
|
_change_option(_opt_cursor, 1)
|
||||||
|
elif event.is_action_pressed("ui_cancel"):
|
||||||
|
_screen = 0
|
||||||
|
|
||||||
|
func _input_controls(event: InputEvent) -> void:
|
||||||
|
var labels := _ctrl_labels()
|
||||||
|
var total := labels.size() + 2 # actions + Reset + Back
|
||||||
|
var reset_idx := labels.size()
|
||||||
|
var back_idx := labels.size() + 1
|
||||||
|
|
||||||
|
if _ctrl_waiting:
|
||||||
|
if event is InputEventKey and event.pressed:
|
||||||
|
var kc: int = event.physical_keycode
|
||||||
|
# Ignore pure modifier keys
|
||||||
|
if kc in [KEY_SHIFT, KEY_CTRL, KEY_ALT, KEY_META, KEY_CAPSLOCK]:
|
||||||
|
return
|
||||||
|
if kc == KEY_ESCAPE:
|
||||||
|
_ctrl_waiting = false
|
||||||
|
return
|
||||||
|
_rebind_key(Settings.REBIND_ACTIONS[_ctrl_cursor], kc)
|
||||||
|
_ctrl_waiting = false
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif event is InputEventJoypadButton and event.pressed:
|
||||||
|
_rebind_joy(Settings.REBIND_ACTIONS[_ctrl_cursor], event.button_index)
|
||||||
|
_ctrl_waiting = false
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return # block all other input while waiting
|
||||||
|
|
||||||
|
if event.is_action_pressed("ui_up"):
|
||||||
|
_ctrl_cursor = (_ctrl_cursor - 1 + total) % total
|
||||||
|
elif event.is_action_pressed("ui_down"):
|
||||||
|
_ctrl_cursor = (_ctrl_cursor + 1) % total
|
||||||
|
elif event.is_action_pressed("ui_accept"):
|
||||||
|
if _ctrl_cursor == back_idx:
|
||||||
|
_screen = 1
|
||||||
|
elif _ctrl_cursor == reset_idx:
|
||||||
|
Settings.reset_key_bindings()
|
||||||
|
else:
|
||||||
|
_ctrl_waiting = true
|
||||||
|
elif event.is_action_pressed("ui_cancel"):
|
||||||
|
_screen = 1
|
||||||
|
|
||||||
|
func _input_leaderboard(event: InputEvent) -> void:
|
||||||
|
if _lb_loading:
|
||||||
|
return
|
||||||
|
if event.is_action_pressed("ui_cancel"):
|
||||||
|
_screen = 0
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
|
# 'O' key toggles online/local view
|
||||||
|
if event is InputEventKey and (event as InputEventKey).pressed:
|
||||||
|
var phk := (event as InputEventKey).physical_keycode
|
||||||
|
if phk == KEY_O:
|
||||||
|
if _lb_view == 0:
|
||||||
|
_fetch_online()
|
||||||
|
else:
|
||||||
|
_lb_view = 0
|
||||||
|
_lb_error = ""
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
func _fetch_online() -> void:
|
||||||
|
if _online_lb_node == null:
|
||||||
|
_online_lb_node = load("res://scripts/online_leaderboard.gd").new()
|
||||||
|
add_child(_online_lb_node)
|
||||||
|
_online_lb_node.scores_fetched.connect(_on_online_scores_fetched)
|
||||||
|
_lb_loading = true
|
||||||
|
_lb_error = ""
|
||||||
|
_online_lb_node.fetch_scores()
|
||||||
|
|
||||||
|
func _on_online_scores_fetched(scores: Array, error: String) -> void:
|
||||||
|
_lb_loading = false
|
||||||
|
if error != "":
|
||||||
|
_lb_error = Tr.t(error)
|
||||||
|
_lb_view = 1
|
||||||
|
else:
|
||||||
|
_lb_online_data = scores
|
||||||
|
_lb_view = 1
|
||||||
|
|
||||||
|
func _rebind_key(action: String, physical_keycode: int) -> void:
|
||||||
|
if not InputMap.has_action(action): return
|
||||||
|
# Remove existing keyboard events
|
||||||
|
for ev in InputMap.action_get_events(action):
|
||||||
|
if ev is InputEventKey:
|
||||||
|
InputMap.action_erase_event(action, ev)
|
||||||
|
var new_ev := InputEventKey.new()
|
||||||
|
new_ev.physical_keycode = physical_keycode
|
||||||
|
InputMap.action_add_event(action, new_ev)
|
||||||
|
Settings.key_bindings[action] = physical_keycode
|
||||||
|
Settings.save_settings()
|
||||||
|
|
||||||
|
func _rebind_joy(action: String, button_index: int) -> void:
|
||||||
|
if not InputMap.has_action(action): return
|
||||||
|
for ev in InputMap.action_get_events(action):
|
||||||
|
if ev is InputEventJoypadButton:
|
||||||
|
InputMap.action_erase_event(action, ev)
|
||||||
|
var new_ev := InputEventJoypadButton.new()
|
||||||
|
new_ev.button_index = button_index
|
||||||
|
InputMap.action_add_event(action, new_ev)
|
||||||
|
Settings.key_bindings[action + "_joy"] = button_index
|
||||||
|
Settings.save_settings()
|
||||||
|
|
||||||
|
func _change_option(idx: int, dir: int) -> void:
|
||||||
|
match idx:
|
||||||
|
0: # SFX
|
||||||
|
Settings.master_volume = clamp(Settings.master_volume + dir * 0.1, 0.0, 1.0)
|
||||||
|
Settings.apply_volume()
|
||||||
|
1: # Music
|
||||||
|
Settings.music_volume = clamp(Settings.music_volume + dir * 0.1, 0.0, 1.0)
|
||||||
|
Settings.apply_music_volume()
|
||||||
|
2: # Mute
|
||||||
|
Settings.sfx_muted = not Settings.sfx_muted
|
||||||
|
Settings.apply_volume()
|
||||||
|
3: # Fullscreen
|
||||||
|
Settings.fullscreen = not Settings.fullscreen
|
||||||
|
if Settings.fullscreen:
|
||||||
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
|
||||||
|
else:
|
||||||
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
||||||
|
4: # Nebula
|
||||||
|
Settings.nebula_enabled = not Settings.nebula_enabled
|
||||||
|
5: # Stars
|
||||||
|
Settings.star_density = (Settings.star_density + dir + 3) % 3
|
||||||
|
6: # Language
|
||||||
|
Settings.language = "en" if Settings.language == "de" else "de"
|
||||||
|
7: # Touch mode
|
||||||
|
Settings.touch_mode = (Settings.touch_mode + 1) % 3
|
||||||
|
# 8 = Controls → handled in _input_options (opens sub-screen)
|
||||||
|
Settings.save_settings()
|
||||||
|
|
||||||
|
# ── Drawing ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var vs: Vector2 = get_viewport_rect().size
|
||||||
|
W = vs.x; H = vs.y
|
||||||
|
draw_rect(get_viewport_rect(), COL_BG)
|
||||||
|
match _screen:
|
||||||
|
0: _draw_main()
|
||||||
|
1: _draw_options()
|
||||||
|
2: _draw_controls()
|
||||||
|
3: _draw_leaderboard()
|
||||||
|
|
||||||
|
func _draw_main() -> void:
|
||||||
|
# Title
|
||||||
|
var title_y := H * 0.22
|
||||||
|
_draw_text_c("S P A C E L", W * 0.5, title_y, 38, COL_PRIMARY)
|
||||||
|
var sub_alpha := 0.4 + 0.2 * sin(_blink * 1.5)
|
||||||
|
_draw_text_c(Tr.t("subtitle"), W * 0.5, title_y + 52.0, 9,
|
||||||
|
Color(COL_DIM.r, COL_DIM.g, COL_DIM.b, sub_alpha))
|
||||||
|
|
||||||
|
# Separator line
|
||||||
|
var lx1 := W * 0.5 - 120.0; var lx2 := W * 0.5 + 120.0
|
||||||
|
draw_line(Vector2(lx1, title_y + 70.0), Vector2(lx2, title_y + 70.0),
|
||||||
|
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.25), 1.0)
|
||||||
|
|
||||||
|
# Menu entries
|
||||||
|
var items := _items()
|
||||||
|
var menu_y := H * 0.50
|
||||||
|
for i in items.size():
|
||||||
|
var is_sel := i == _cursor
|
||||||
|
var pulse := 0.6 + 0.4 * sin(_blink * 3.0)
|
||||||
|
|
||||||
|
var col: Color
|
||||||
|
if i == items.size() - 1: # Quit in warning colour
|
||||||
|
col = Color(COL_WARN.r, COL_WARN.g, COL_WARN.b,
|
||||||
|
pulse if is_sel else 0.45)
|
||||||
|
elif is_sel:
|
||||||
|
col = Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse)
|
||||||
|
else:
|
||||||
|
col = Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.55)
|
||||||
|
|
||||||
|
var sz := 16 if is_sel else 13
|
||||||
|
var prefix := "▶ " if is_sel else " "
|
||||||
|
_draw_text_c(prefix + items[i], W * 0.5, menu_y + i * 42.0, sz, col)
|
||||||
|
|
||||||
|
# Corner brackets
|
||||||
|
_draw_brackets(12.0, 12.0, W - 12.0, H - 12.0, COL_DIM, 18.0)
|
||||||
|
|
||||||
|
# Footer — adapts to active input device
|
||||||
|
_draw_text_c(Tr.hint("footer_nav"), W * 0.5, H - 22.0, 8, COL_DIM)
|
||||||
|
|
||||||
|
func _draw_options() -> void:
|
||||||
|
var bw := 480.0; var bh := 380.0
|
||||||
|
var bx := (W - bw) * 0.5; var by := (H - bh) * 0.5
|
||||||
|
_draw_terminal_box(bx, by, bw, bh)
|
||||||
|
_draw_text_c(Tr.t("opt_title"), W * 0.5, by + 16.0, 10, COL_DIM)
|
||||||
|
|
||||||
|
var star_labels := [Tr.t("star_low"), Tr.t("star_mid"), Tr.t("star_high")]
|
||||||
|
var touch_labels := [Tr.t("touch_auto"), Tr.t("touch_on"), Tr.t("touch_off")]
|
||||||
|
var opts := _opt_items()
|
||||||
|
var values: Array = [
|
||||||
|
_volume_bar(Settings.master_volume),
|
||||||
|
_volume_bar(Settings.music_volume),
|
||||||
|
Tr.t("yes") if Settings.sfx_muted else Tr.t("no"),
|
||||||
|
Tr.t("yes") if Settings.fullscreen else Tr.t("no"),
|
||||||
|
Tr.t("yes") if Settings.nebula_enabled else Tr.t("no"),
|
||||||
|
star_labels[Settings.star_density],
|
||||||
|
Settings.language.to_upper(),
|
||||||
|
touch_labels[Settings.touch_mode],
|
||||||
|
"→", # Controls sub-screen
|
||||||
|
"" # Back (rendered specially)
|
||||||
|
]
|
||||||
|
|
||||||
|
var item_y := by + 52.0
|
||||||
|
for i in opts.size():
|
||||||
|
var is_sel := i == _opt_cursor
|
||||||
|
var pulse := 0.6 + 0.4 * sin(_blink * 3.0)
|
||||||
|
var row_y := item_y + i * 36.0
|
||||||
|
|
||||||
|
if i == opts.size() - 1: # Back
|
||||||
|
_draw_text_c("[ %s ]" % opts[i], W * 0.5, row_y, 12,
|
||||||
|
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b,
|
||||||
|
pulse if is_sel else 0.4))
|
||||||
|
else:
|
||||||
|
var label_col := Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b,
|
||||||
|
pulse) if is_sel else Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.70)
|
||||||
|
var val_col := Color(COL_WHITE.r, COL_WHITE.g, COL_WHITE.b,
|
||||||
|
0.90 if is_sel else 0.50)
|
||||||
|
var prefix := "▶ " if is_sel else " "
|
||||||
|
_draw_text(prefix + opts[i], bx + 28.0, row_y, 11, label_col)
|
||||||
|
if values[i] != "":
|
||||||
|
var val_str: String
|
||||||
|
if values[i] == "→":
|
||||||
|
val_str = "→" # Sub-screen indicator — no ◄► arrows
|
||||||
|
else:
|
||||||
|
val_str = "◄ %s ►" % values[i]
|
||||||
|
_draw_text(val_str, bx + bw - 28.0 - _text_w(val_str, 11), row_y, 11, val_col)
|
||||||
|
|
||||||
|
_draw_text_c(Tr.hint("opt_footer"), W * 0.5, by + bh - 20.0, 8, COL_DIM)
|
||||||
|
|
||||||
|
func _draw_controls() -> void:
|
||||||
|
var bw := 400.0; var bh := 310.0
|
||||||
|
var bx := (W - bw) * 0.5; var by := (H - bh) * 0.5
|
||||||
|
_draw_terminal_box(bx, by, bw, bh)
|
||||||
|
_draw_text_c(Tr.t("ctrl_title"), W * 0.5, by + 16.0, 10, COL_DIM)
|
||||||
|
|
||||||
|
var labels := _ctrl_labels()
|
||||||
|
var actions := Settings.REBIND_ACTIONS
|
||||||
|
var reset_idx := labels.size()
|
||||||
|
var back_idx := labels.size() + 1
|
||||||
|
var total := labels.size() + 2
|
||||||
|
|
||||||
|
var item_y := by + 52.0
|
||||||
|
for i in total:
|
||||||
|
var is_sel := i == _ctrl_cursor
|
||||||
|
var pulse := 0.6 + 0.4 * sin(_blink * 3.0)
|
||||||
|
var row_y := item_y + i * 36.0
|
||||||
|
|
||||||
|
if i == back_idx:
|
||||||
|
_draw_text_c("[ %s ]" % Tr.t("opt_back"), W * 0.5, row_y, 12,
|
||||||
|
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse if is_sel else 0.4))
|
||||||
|
elif i == reset_idx:
|
||||||
|
_draw_text_c("[ %s ]" % Tr.t("ctrl_reset"), W * 0.5, row_y, 12,
|
||||||
|
Color(COL_WARN.r, COL_WARN.g, COL_WARN.b, pulse if is_sel else 0.35))
|
||||||
|
else:
|
||||||
|
var label_col := Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b,
|
||||||
|
pulse) if is_sel else Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.70)
|
||||||
|
var val_col := Color(COL_WHITE.r, COL_WHITE.g, COL_WHITE.b,
|
||||||
|
0.90 if is_sel else 0.50)
|
||||||
|
var prefix := "▶ " if is_sel else " "
|
||||||
|
|
||||||
|
var key_str: String
|
||||||
|
if _ctrl_waiting and is_sel:
|
||||||
|
key_str = Tr.t("ctrl_waiting")
|
||||||
|
val_col = Color(COL_WARN.r, COL_WARN.g, COL_WARN.b,
|
||||||
|
0.5 + 0.5 * sin(_blink * 6.0))
|
||||||
|
else:
|
||||||
|
key_str = "[ %s ]" % _action_key_name(actions[i])
|
||||||
|
|
||||||
|
_draw_text(prefix + labels[i], bx + 28.0, row_y, 11, label_col)
|
||||||
|
_draw_text(key_str, bx + bw - 28.0 - _text_w(key_str, 11), row_y, 11, val_col)
|
||||||
|
|
||||||
|
_draw_text_c(Tr.hint("ctrl_footer"), W * 0.5, by + bh - 20.0, 8, COL_DIM)
|
||||||
|
|
||||||
|
func _draw_leaderboard() -> void:
|
||||||
|
var bw := 500.0; var bh := 420.0
|
||||||
|
var bx := (W - bw) * 0.5; var by := (H - bh) * 0.5
|
||||||
|
_draw_terminal_box(bx, by, bw, bh)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
var view_label := Tr.t("lb_online") if _lb_view == 1 else Tr.t("lb_local")
|
||||||
|
_draw_text_c(Tr.t("lb_title") + " " + view_label, W * 0.5, by + 16.0, 10, COL_DIM)
|
||||||
|
_draw_hline(bx + 16.0, bx + bw - 16.0, by + 38.0)
|
||||||
|
|
||||||
|
# Loading / error
|
||||||
|
if _lb_loading:
|
||||||
|
var pulse := 0.5 + 0.5 * sin(_blink * 4.0)
|
||||||
|
_draw_text_c(Tr.t("lb_loading"), W * 0.5, by + 200.0, 13,
|
||||||
|
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse))
|
||||||
|
_draw_text_c(Tr.hint("lb_footer"), W * 0.5, by + bh - 20.0, 8, COL_DIM)
|
||||||
|
return
|
||||||
|
|
||||||
|
if _lb_error != "":
|
||||||
|
_draw_text_c(_lb_error, W * 0.5, by + 200.0, 11, COL_WARN)
|
||||||
|
_draw_text_c(Tr.hint("lb_footer"), W * 0.5, by + bh - 20.0, 8, COL_DIM)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Column headers
|
||||||
|
var col_x_rank := bx + 30.0
|
||||||
|
var col_x_name := bx + 70.0
|
||||||
|
var col_x_score := bx + 260.0
|
||||||
|
var col_x_wave := bx + 390.0
|
||||||
|
var header_y := by + 50.0
|
||||||
|
var dim := Color(COL_DIM.r, COL_DIM.g, COL_DIM.b, 0.8)
|
||||||
|
_draw_text("#", col_x_rank, header_y, 9, dim)
|
||||||
|
_draw_text("NAME", col_x_name, header_y, 9, dim)
|
||||||
|
_draw_text("SCORE", col_x_score, header_y, 9, dim)
|
||||||
|
_draw_text("WAVE", col_x_wave, header_y, 9, dim)
|
||||||
|
_draw_hline(bx + 16.0, bx + bw - 16.0, by + 66.0)
|
||||||
|
|
||||||
|
# Rows
|
||||||
|
var scores: Array = Leaderboard.get_scores() if _lb_view == 0 else _lb_online_data
|
||||||
|
if scores.is_empty():
|
||||||
|
_draw_text_c(Tr.t("lb_empty"), W * 0.5, by + 200.0, 11,
|
||||||
|
Color(COL_DIM.r, COL_DIM.g, COL_DIM.b, 0.6))
|
||||||
|
else:
|
||||||
|
for i: int in min(scores.size(), 10):
|
||||||
|
var e: Dictionary = scores[i]
|
||||||
|
var row_y := by + 76.0 + i * 30.0
|
||||||
|
var row_col := COL_ACCENT if i == 0 else \
|
||||||
|
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.85 - i * 0.05)
|
||||||
|
_draw_text("%d." % (i + 1), col_x_rank, row_y, 10, row_col)
|
||||||
|
_draw_text(str(e.get("name", "???")), col_x_name, row_y, 10, row_col)
|
||||||
|
_draw_text(str(e.get("score", 0)), col_x_score, row_y, 10, row_col)
|
||||||
|
var wave_str := Tr.t("lb_wave") + str(e.get("wave", 1))
|
||||||
|
_draw_text(wave_str, col_x_wave, row_y, 10, row_col)
|
||||||
|
|
||||||
|
# Footer
|
||||||
|
_draw_hline(bx + 16.0, bx + bw - 16.0, by + bh - 36.0)
|
||||||
|
_draw_text_c(Tr.hint("lb_footer"), W * 0.5, by + bh - 20.0, 8, COL_DIM)
|
||||||
|
|
||||||
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _draw_hline(x1: float, x2: float, y: float) -> void:
|
||||||
|
draw_line(Vector2(x1, y), Vector2(x2, y),
|
||||||
|
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.25), 1.0)
|
||||||
|
|
||||||
|
# Returns a short human-readable name for the first keyboard or joypad event of an action.
|
||||||
|
func _action_key_name(action: String) -> String:
|
||||||
|
if not InputMap.has_action(action): return "?"
|
||||||
|
for ev in InputMap.action_get_events(action):
|
||||||
|
if ev is InputEventKey:
|
||||||
|
var kev := ev as InputEventKey
|
||||||
|
var kc: int = kev.physical_keycode if kev.physical_keycode != KEY_NONE else kev.keycode
|
||||||
|
return OS.get_keycode_string(kc)
|
||||||
|
elif ev is InputEventJoypadButton:
|
||||||
|
var jev := ev as InputEventJoypadButton
|
||||||
|
return _joy_btn_label(jev.button_index)
|
||||||
|
return "?"
|
||||||
|
|
||||||
|
func _joy_btn_label(idx: int) -> String:
|
||||||
|
match idx:
|
||||||
|
JOY_BUTTON_A: return "A"
|
||||||
|
JOY_BUTTON_B: return "B"
|
||||||
|
JOY_BUTTON_X: return "X"
|
||||||
|
JOY_BUTTON_Y: return "Y"
|
||||||
|
JOY_BUTTON_LEFT_SHOULDER: return "LB"
|
||||||
|
JOY_BUTTON_RIGHT_SHOULDER: return "RB"
|
||||||
|
JOY_BUTTON_LEFT_STICK: return "L3"
|
||||||
|
JOY_BUTTON_RIGHT_STICK: return "R3"
|
||||||
|
_: return "BTN%d" % idx
|
||||||
|
|
||||||
|
func _volume_bar(vol: float) -> String:
|
||||||
|
var filled := int(round(vol * 10.0))
|
||||||
|
var bar := ""
|
||||||
|
for i in 10:
|
||||||
|
bar += "█" if i < filled else "░"
|
||||||
|
return bar
|
||||||
|
|
||||||
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _draw_terminal_box(bx: float, by: float, bw: float, bh: float) -> void:
|
||||||
|
draw_rect(Rect2(bx, by, bw, bh), Color(0.0, 0.04, 0.02, 0.95))
|
||||||
|
var bc := Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.35)
|
||||||
|
draw_line(Vector2(bx, by), Vector2(bx+bw, by), bc, 1.0)
|
||||||
|
draw_line(Vector2(bx, by+bh), Vector2(bx+bw, by+bh), bc, 1.0)
|
||||||
|
draw_line(Vector2(bx, by), Vector2(bx, by+bh), bc, 1.0)
|
||||||
|
draw_line(Vector2(bx+bw, by), Vector2(bx+bw, by+bh), bc, 1.0)
|
||||||
|
var cl := 12.0
|
||||||
|
for cx: float in [bx, bx+bw]:
|
||||||
|
for cy: float in [by, by+bh]:
|
||||||
|
var sx := 1.0 if cx == bx else -1.0
|
||||||
|
var sy := 1.0 if cy == by else -1.0
|
||||||
|
draw_line(Vector2(cx, cy), Vector2(cx + sx*cl, cy), COL_PRIMARY, 1.5)
|
||||||
|
draw_line(Vector2(cx, cy), Vector2(cx, cy + sy*cl), COL_PRIMARY, 1.5)
|
||||||
|
|
||||||
|
func _draw_brackets(x1: float, y1: float, x2: float, y2: float, col: Color, arm: float) -> void:
|
||||||
|
var bw := 1.5
|
||||||
|
draw_line(Vector2(x1, y1), Vector2(x1+arm, y1), col, bw)
|
||||||
|
draw_line(Vector2(x1, y1), Vector2(x1, y1+arm), col, bw)
|
||||||
|
draw_line(Vector2(x2, y1), Vector2(x2-arm, y1), col, bw)
|
||||||
|
draw_line(Vector2(x2, y1), Vector2(x2, y1+arm), col, bw)
|
||||||
|
draw_line(Vector2(x1, y2), Vector2(x1+arm, y2), col, bw)
|
||||||
|
draw_line(Vector2(x1, y2), Vector2(x1, y2-arm), col, bw)
|
||||||
|
draw_line(Vector2(x2, y2), Vector2(x2-arm, y2), col, bw)
|
||||||
|
draw_line(Vector2(x2, y2), Vector2(x2, y2-arm), col, bw)
|
||||||
|
|
||||||
|
func _draw_text(text: String, x: float, y: float, sz: int, col: Color) -> void:
|
||||||
|
draw_string(ThemeDB.fallback_font, Vector2(x, y + sz),
|
||||||
|
text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz, col)
|
||||||
|
|
||||||
|
func _draw_text_c(text: String, x: float, y: float, sz: int, col: Color) -> void:
|
||||||
|
var tw := _text_w(text, sz)
|
||||||
|
draw_string(ThemeDB.fallback_font, Vector2(x - tw * 0.5, y + sz),
|
||||||
|
text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz, col)
|
||||||
|
|
||||||
|
func _text_w(text: String, sz: int) -> float:
|
||||||
|
return ThemeDB.fallback_font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cp36y6rjo8aqs
|
||||||
@@ -0,0 +1,623 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# "STELLAR DRIFT" · Normal-OST (A-Moll, 140 BPM, Am→G→F→Em)
|
||||||
|
# "CRITICAL MASS" · Boss-OST #1 WRAITH (A-Moll, 175 BPM, Super Hexagon)
|
||||||
|
# "EVENT HORIZON" · Boss-OST #2 LEVIATHAN (E-Phrygisch, 140 BPM, Cosmic Dread)
|
||||||
|
#
|
||||||
|
# Voices in einem AudioStreamPlayer auf dem "Music"-Bus:
|
||||||
|
# Voice 0 · Melodie · Triangle (normal/boss2), Square (boss1)
|
||||||
|
# Voice 1 · Arpeggio · Square (normal/boss1), Triangle leise (boss2)
|
||||||
|
# Voice 2 · Bass · Sawtooth (boss2: +detuneter 2. Layer für Schwebung)
|
||||||
|
# Voice 3 · Schlagzeug · Hi-Hat+Kick+Snare (normal/boss1) ·
|
||||||
|
# Kick+Floor-Tom+Mid-Tom+Reverse-Whoosh (boss2)
|
||||||
|
# Voice 4 · Phase-2-Layer · Triangle 1 Oktave über Melodie (nur boss2 Phase 2)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@export var volume_db: float = -8.0
|
||||||
|
@export var loop: bool = true
|
||||||
|
@export var autoplay: bool = true
|
||||||
|
|
||||||
|
const SAMPLE_RATE := 44100
|
||||||
|
const BUFFER_SIZE := 0.1
|
||||||
|
|
||||||
|
# ── Normale Musik ─────────────────────────────────────────────────────────
|
||||||
|
const BPM := 140.0
|
||||||
|
const BEAT := 60.0 / BPM
|
||||||
|
|
||||||
|
# ── Boss-Musik #1 (WRAITH) ────────────────────────────────────────────────
|
||||||
|
const BOSS_BPM := 175.0
|
||||||
|
const BOSS_BEAT := 60.0 / BOSS_BPM
|
||||||
|
|
||||||
|
# ── Boss-Musik #2 (LEVIATHAN) ─────────────────────────────────────────────
|
||||||
|
const BOSS2_BPM := 140.0
|
||||||
|
const BOSS2_BEAT := 60.0 / BOSS2_BPM
|
||||||
|
|
||||||
|
# ── Frequenztabelle ───────────────────────────────────────────────────────
|
||||||
|
const FREQ := {
|
||||||
|
"R" : 0.0,
|
||||||
|
"D2" : 73.42, "E2" : 82.41, "F2" : 87.31, "A2" : 110.00,
|
||||||
|
"C3" : 130.81, "D3" : 146.83, "E3" : 164.81,
|
||||||
|
"F3" : 174.61, "G3" : 196.00, "A3" : 220.00, "B3" : 246.94,
|
||||||
|
"C4" : 261.63, "D4" : 293.66, "E4" : 329.63,
|
||||||
|
"F4" : 349.23, "G4" : 392.00, "A4" : 440.00, "B4" : 493.88,
|
||||||
|
"C5" : 523.25, "D5" : 587.33, "E5" : 659.25,
|
||||||
|
"F5" : 698.46, "G5" : 783.99, "A5" : 880.00, "B5" : 987.77,
|
||||||
|
"C6" :1046.50, "E6" :1318.51,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Normal-Akkorde (chord: 0=Am 1=G 2=F 3=Em) ─────────────────────────
|
||||||
|
const ARP := {
|
||||||
|
0: ["A4","C5","E5","C5"],
|
||||||
|
1: ["G4","B4","D5","B4"],
|
||||||
|
2: ["F4","A4","C5","A4"],
|
||||||
|
3: ["E4","G4","B4","G4"],
|
||||||
|
}
|
||||||
|
const BASS := { 0:"A3", 1:"G3", 2:"F3", 3:"E3" }
|
||||||
|
|
||||||
|
# ── Boss-Akkorde (chord: 0=Am 1=Em) ─────────────────────────────────────
|
||||||
|
const BOSS_ARP := {
|
||||||
|
0: ["A4","E5","A5","E5"], # Am — Power-Arpeggio
|
||||||
|
1: ["E4","B4","E5","B4"], # Em — Moll-Arpeggio
|
||||||
|
}
|
||||||
|
const BOSS_BASS := { 0:"A2", 1:"E2" } # tiefer Sub-Bass
|
||||||
|
|
||||||
|
# ── Boss2-Akkorde (chord: 0=E 1=F 2=Dm) — E-Phrygisch ──────────────────
|
||||||
|
const BOSS2_ARP := {
|
||||||
|
0: ["E4","B4","E5","G4"], # E (phrygisch, mit kleiner Terz G)
|
||||||
|
1: ["F4","C5","F5","A4"], # F — der phrygische II-Akkord (F→E ist der Killer)
|
||||||
|
2: ["D4","A4","D5","F4"], # Dm — dunkler iv-Akkord
|
||||||
|
}
|
||||||
|
const BOSS2_BASS := { 0:"E2", 1:"F2", 2:"D2" } # Drone-Bass Grundtöne
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# NORMAL-SCORE — "STELLAR DRIFT"
|
||||||
|
# Format: [mel_note, beats, chord_idx, section]
|
||||||
|
# section 0=Intro 1=Build 2=Main
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
const SCORE: Array = [
|
||||||
|
# ═══ INTRO ═══
|
||||||
|
["A4", 2.0, 0, 0], ["E5", 2.0, 0, 0],
|
||||||
|
["C5", 2.0, 0, 0], ["A4", 2.0, 0, 0],
|
||||||
|
["E5", 1.0, 0, 0], ["A5", 1.0, 0, 0], ["E5", 1.0, 0, 0], ["C5", 1.0, 0, 0],
|
||||||
|
["A4", 4.0, 0, 0],
|
||||||
|
|
||||||
|
# ═══ BUILD ═══
|
||||||
|
["A4",0.5,0,1],["C5",0.25,0,1],["E5",0.25,0,1],
|
||||||
|
["A5",0.5,0,1],["G5",0.25,0,1],["E5",0.25,0,1],
|
||||||
|
["D5",0.5,0,1],["C5",0.25,0,1],["A4",0.25,0,1],
|
||||||
|
["C5",0.5,0,1],["R", 0.5, 0,1],
|
||||||
|
["C5",0.5,0,1],["E5",0.25,0,1],["A5",0.25,0,1],
|
||||||
|
["C6",0.5,0,1],["A5",0.25,0,1],["E5",0.25,0,1],
|
||||||
|
["G5",0.5,0,1],["E5",0.25,0,1],["C5",0.25,0,1],["A4",1.0,0,1],
|
||||||
|
|
||||||
|
["G4",0.5,1,1],["B4",0.25,1,1],["D5",0.25,1,1],
|
||||||
|
["G5",0.5,1,1],["D5",0.25,1,1],["B4",0.25,1,1],
|
||||||
|
["G4",0.5,1,1],["R", 0.25,1,1],["B4",0.25,1,1],
|
||||||
|
["D5",0.5,1,1],["G5",0.5,1,1],
|
||||||
|
|
||||||
|
["F4",0.5,2,1],["A4",0.25,2,1],["C5",0.25,2,1],
|
||||||
|
["F5",0.5,2,1],["C5",0.25,2,1],["A4",0.25,2,1],
|
||||||
|
["F5",0.5,2,1],["A5",0.25,2,1],["C6",0.25,2,1],
|
||||||
|
["A5",0.5,2,1],["F5",0.5, 2,1],
|
||||||
|
|
||||||
|
["E5",0.5,3,1],["G5",0.25,3,1],["B5",0.25,3,1],
|
||||||
|
["E6",0.5,3,1],["B5",0.25,3,1],["G5",0.25,3,1],
|
||||||
|
["A5",0.5,3,1],["G5",0.25,3,1],["E5",0.25,3,1],["B4",1.0,3,1],
|
||||||
|
|
||||||
|
# ═══ MAIN ═══
|
||||||
|
["A4",0.5,0,2],["C5",0.25,0,2],["E5",0.25,0,2],
|
||||||
|
["A5",0.5,0,2],["G5",0.25,0,2],["E5",0.25,0,2],
|
||||||
|
["D5",0.5,0,2],["C5",0.25,0,2],["A4",0.25,0,2],
|
||||||
|
["C5",0.5,0,2],["R", 0.5, 0,2],
|
||||||
|
["C5",0.5,0,2],["E5",0.25,0,2],["A5",0.25,0,2],
|
||||||
|
["C6",0.5,0,2],["A5",0.25,0,2],["E5",0.25,0,2],
|
||||||
|
["G5",0.5,0,2],["E5",0.25,0,2],["C5",0.25,0,2],["A4",1.0,0,2],
|
||||||
|
|
||||||
|
["G4",0.5,1,2],["B4",0.25,1,2],["D5",0.25,1,2],
|
||||||
|
["G5",0.5,1,2],["D5",0.25,1,2],["B4",0.25,1,2],
|
||||||
|
["G4",0.5,1,2],["R", 0.25,1,2],["B4",0.25,1,2],
|
||||||
|
["D5",0.5,1,2],["G5",0.5,1,2],
|
||||||
|
|
||||||
|
["F4",0.5,2,2],["A4",0.25,2,2],["C5",0.25,2,2],
|
||||||
|
["F5",0.5,2,2],["C5",0.25,2,2],["A4",0.25,2,2],
|
||||||
|
["F5",0.5,2,2],["A5",0.25,2,2],["C6",0.25,2,2],
|
||||||
|
["A5",0.5,2,2],["F5",0.5, 2,2],
|
||||||
|
|
||||||
|
["E5",0.5,3,2],["G5",0.25,3,2],["B5",0.25,3,2],
|
||||||
|
["E6",0.5,3,2],["B5",0.25,3,2],["G5",0.25,3,2],
|
||||||
|
["A5",0.5,3,2],["G5",0.25,3,2],["E5",0.25,3,2],["B4",1.0,3,2],
|
||||||
|
]
|
||||||
|
const LOOP_START := 9 # Loop ab BUILD (Intro überspringen)
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# BOSS-SCORE — "CRITICAL MASS"
|
||||||
|
# Format: [mel_note, beats, chord_idx, section]
|
||||||
|
# chord 0=Am 1=Em · section=3 (volle Intensität immer)
|
||||||
|
# BPM=175, Stil: Super Hexagon — stakkato, getrieben, 4-Bar-Loop
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
const BOSS_SCORE: Array = [
|
||||||
|
# ══ Bar 1 (Am) — Aufstiegs-Stabs ══
|
||||||
|
["E5", 0.25, 0, 3], ["R", 0.25, 0, 3], ["G5", 0.25, 0, 3], ["A5", 0.25, 0, 3],
|
||||||
|
["E5", 0.5, 0, 3], ["R", 0.25, 0, 3], ["C5", 0.25, 0, 3],
|
||||||
|
["E5", 0.25, 0, 3], ["R", 0.25, 0, 3], ["A4", 0.5, 0, 3],
|
||||||
|
["R", 1.0, 0, 3],
|
||||||
|
|
||||||
|
# ══ Bar 2 (Am) — Synkopierter Lauf ══
|
||||||
|
["G5", 0.25, 0, 3], ["A5", 0.25, 0, 3], ["G5", 0.25, 0, 3], ["E5", 0.25, 0, 3],
|
||||||
|
["C5", 0.25, 0, 3], ["E5", 0.25, 0, 3], ["G5", 0.5, 0, 3],
|
||||||
|
["A5", 0.25, 0, 3], ["R", 0.25, 0, 3], ["G5", 0.25, 0, 3], ["E5", 0.25, 0, 3],
|
||||||
|
["C5", 0.5, 0, 3], ["R", 0.5, 0, 3],
|
||||||
|
|
||||||
|
# ══ Bar 3 (Em) — Dunkle Dringlichkeit ══
|
||||||
|
["E5", 0.25, 1, 3], ["R", 0.25, 1, 3], ["B4", 0.25, 1, 3], ["R", 0.25, 1, 3],
|
||||||
|
["G5", 0.25, 1, 3], ["E5", 0.25, 1, 3], ["D5", 0.5, 1, 3],
|
||||||
|
["B4", 0.25, 1, 3], ["R", 0.25, 1, 3], ["G5", 0.5, 1, 3],
|
||||||
|
["E5", 0.25, 1, 3], ["D5", 0.25, 1, 3], ["B4", 0.5, 1, 3],
|
||||||
|
|
||||||
|
# ══ Bar 4 (Em) — Klimax ══
|
||||||
|
["G5", 0.25, 1, 3], ["B5", 0.25, 1, 3], ["A5", 0.25, 1, 3], ["G5", 0.25, 1, 3],
|
||||||
|
["E5", 0.5, 1, 3], ["R", 0.5, 1, 3],
|
||||||
|
["D5", 0.25, 1, 3], ["R", 0.25, 1, 3], ["B4", 0.25, 1, 3], ["R", 0.25, 1, 3],
|
||||||
|
["E5", 1.0, 1, 3],
|
||||||
|
]
|
||||||
|
const BOSS_LOOP_START := 0 # Voller Loop, kein Intro
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# BOSS2-SCORE — "EVENT HORIZON"
|
||||||
|
# Format: [mel_note, beats, chord_idx, section]
|
||||||
|
# chord 0=E 1=F 2=Dm · section=3 (Bass+Drums immer aktiv)
|
||||||
|
# BPM=140 · E-Phrygisch · 16-Bar-Struktur: 8 Bars Phase 1 + 8 Bars Phase 2
|
||||||
|
# Melodie: lange, sparsame Töne in Phase 1 — verdichtet sich in Phase 2
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
const BOSS2_SCORE: Array = [
|
||||||
|
# ══ PHASE 1 — Bars 1–8 (Index 0–14) ═══════════════════════════════════
|
||||||
|
|
||||||
|
# Bar 1 (E) — langer Tonika-Drone
|
||||||
|
["E5", 3.0, 0, 3], ["R", 1.0, 0, 3],
|
||||||
|
# Bar 2 (E) — leichte Bewegung
|
||||||
|
["G5", 2.0, 0, 3], ["E5", 2.0, 0, 3],
|
||||||
|
# Bar 3 (F) — phrygische Spannung bricht ein
|
||||||
|
["F5", 3.0, 1, 3], ["C5", 1.0, 1, 3],
|
||||||
|
# Bar 4 (F) — hält die Spannung
|
||||||
|
["F5", 2.0, 1, 3], ["A5", 2.0, 1, 3],
|
||||||
|
# Bar 5 (Dm) — dunkler Abstieg
|
||||||
|
["D5", 2.0, 2, 3], ["F5", 2.0, 2, 3],
|
||||||
|
# Bar 6 (Dm) — Weite, Atem
|
||||||
|
["A5", 3.0, 2, 3], ["R", 1.0, 2, 3],
|
||||||
|
# Bar 7 (E) — Rückkehr
|
||||||
|
["B4", 2.0, 0, 3], ["E5", 2.0, 0, 3],
|
||||||
|
# Bar 8 (E) — ganze Note als Übergang
|
||||||
|
["E5", 4.0, 0, 3],
|
||||||
|
|
||||||
|
# ══ PHASE 2 — Bars 9–16 (Index 15–47) ═════════════════════════════════
|
||||||
|
# Verdichtung: mehr Achtel, aufsteigende Linien, Klimax-Oktav
|
||||||
|
|
||||||
|
# Bar 9 (E) — Phrygisch-Lauf aufwärts
|
||||||
|
["E5", 1.0, 0, 3], ["G5", 1.0, 0, 3], ["B5", 1.0, 0, 3], ["G5", 1.0, 0, 3],
|
||||||
|
# Bar 10 (E) — Stakkato-Antwort
|
||||||
|
["E5", 0.5, 0, 3], ["R", 0.5, 0, 3], ["B4", 1.0, 0, 3], ["G5", 1.0, 0, 3], ["E5", 1.0, 0, 3],
|
||||||
|
# Bar 11 (F) — hochklettern, der F→E-Halbton beißt
|
||||||
|
["F5", 1.0, 1, 3], ["A5", 1.0, 1, 3], ["C6", 1.0, 1, 3], ["A5", 1.0, 1, 3],
|
||||||
|
# Bar 12 (F) — absteigende Variation
|
||||||
|
["F5", 0.5, 1, 3], ["R", 0.5, 1, 3], ["C5", 1.0, 1, 3], ["A5", 1.0, 1, 3], ["F5", 1.0, 1, 3],
|
||||||
|
# Bar 13 (Dm) — Dm-Arpeggio-Lick
|
||||||
|
["D5", 1.0, 2, 3], ["F5", 1.0, 2, 3], ["A5", 1.0, 2, 3], ["F5", 1.0, 2, 3],
|
||||||
|
# Bar 14 (Dm) — Dreiklangs-Dichte
|
||||||
|
["D5", 0.5, 2, 3], ["A4", 0.5, 2, 3], ["D5", 1.0, 2, 3], ["F5", 1.0, 2, 3], ["A5", 1.0, 2, 3],
|
||||||
|
# Bar 15 (E) — finaler Aufstieg
|
||||||
|
["B4", 1.0, 0, 3], ["E5", 1.0, 0, 3], ["G5", 1.0, 0, 3], ["B5", 1.0, 0, 3],
|
||||||
|
# Bar 16 (E) — Klimax-Oktavsprung E5→E4
|
||||||
|
["E5", 2.0, 0, 3], ["E4", 2.0, 0, 3],
|
||||||
|
]
|
||||||
|
const BOSS2_LOOP_START := 0 # bei Phase 1 loopt der ganze Track
|
||||||
|
const BOSS2_PHASE2_OFFSET := 15 # Index wo Bar 9 (Phase 2) beginnt
|
||||||
|
|
||||||
|
# ── Player-Zustand ────────────────────────────────────────────────────────
|
||||||
|
var _player: AudioStreamPlayer
|
||||||
|
var _generator: AudioStreamGenerator
|
||||||
|
var _playback: AudioStreamGeneratorPlayback
|
||||||
|
var _playing := false
|
||||||
|
|
||||||
|
# Modus: 0=normal, 1=WRAITH (CRITICAL MASS), 2=LEVIATHAN (EVENT HORIZON)
|
||||||
|
var _boss_mode: int = 0
|
||||||
|
var _cur_beat: float = BEAT
|
||||||
|
|
||||||
|
# Phase-2-Umschaltung (nur boss_mode=2)
|
||||||
|
var _phase2_active: bool = false
|
||||||
|
var _pending_phase2_jump: bool = false
|
||||||
|
|
||||||
|
# Melody
|
||||||
|
var _mel_idx := 0
|
||||||
|
var _mel_pos := 0
|
||||||
|
var _mel_samples := 0
|
||||||
|
var _mel_freq := 0.0
|
||||||
|
var _mel_phase := 0.0
|
||||||
|
|
||||||
|
# Arpeggio (16tel-Noten)
|
||||||
|
var _arp_step := 0
|
||||||
|
var _arp_pos := 0
|
||||||
|
var _arp_samples := 0
|
||||||
|
var _arp_freq := 0.0
|
||||||
|
var _arp_phase := 0.0
|
||||||
|
|
||||||
|
# Bass
|
||||||
|
var _bass_chord := -1
|
||||||
|
var _bass_freq := 0.0
|
||||||
|
var _bass_phase := 0.0
|
||||||
|
|
||||||
|
# Bass-Zweit-Oszillator (nur boss_mode=2, detunet für Schwebung)
|
||||||
|
var _bass2_phase := 0.0
|
||||||
|
|
||||||
|
# Globaler Zustand
|
||||||
|
var _section := 0
|
||||||
|
var _chord := 0
|
||||||
|
var _sample_g := 0
|
||||||
|
|
||||||
|
# ── Initialisierung ───────────────────────────────────────────────────────
|
||||||
|
func _ready() -> void:
|
||||||
|
var bus_idx := AudioServer.get_bus_index("Music")
|
||||||
|
if bus_idx == -1:
|
||||||
|
AudioServer.add_bus()
|
||||||
|
bus_idx = AudioServer.get_bus_count() - 1
|
||||||
|
AudioServer.set_bus_name(bus_idx, "Music")
|
||||||
|
AudioServer.set_bus_send(bus_idx, "Master")
|
||||||
|
AudioServer.set_bus_volume_db(bus_idx,
|
||||||
|
linear_to_db(Settings.music_volume) if Settings.music_volume > 0.0 else -80.0)
|
||||||
|
|
||||||
|
_generator = AudioStreamGenerator.new()
|
||||||
|
_generator.mix_rate = SAMPLE_RATE
|
||||||
|
_generator.buffer_length = BUFFER_SIZE
|
||||||
|
_player = AudioStreamPlayer.new()
|
||||||
|
_player.stream = _generator
|
||||||
|
_player.volume_db = volume_db
|
||||||
|
_player.bus = "Music"
|
||||||
|
add_child(_player)
|
||||||
|
if autoplay:
|
||||||
|
play()
|
||||||
|
|
||||||
|
func play() -> void:
|
||||||
|
_boss_mode = 0
|
||||||
|
_cur_beat = BEAT
|
||||||
|
_phase2_active = false; _pending_phase2_jump = false
|
||||||
|
_mel_idx = 0; _mel_pos = 0; _mel_phase = 0.0
|
||||||
|
_arp_step = 0; _arp_pos = 0; _arp_phase = 0.0
|
||||||
|
_bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.0
|
||||||
|
_sample_g = 0; _section = 0; _chord = 0
|
||||||
|
_playing = true
|
||||||
|
_player.play()
|
||||||
|
_playback = _player.get_stream_playback()
|
||||||
|
_load_mel()
|
||||||
|
|
||||||
|
func play_boss() -> void:
|
||||||
|
_boss_mode = 1
|
||||||
|
_cur_beat = BOSS_BEAT
|
||||||
|
_phase2_active = false; _pending_phase2_jump = false
|
||||||
|
_mel_idx = 0; _mel_pos = 0; _mel_phase = 0.0
|
||||||
|
_arp_step = 0; _arp_pos = 0; _arp_phase = 0.0
|
||||||
|
_bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.0
|
||||||
|
_sample_g = 0; _section = 3; _chord = 0
|
||||||
|
if not _playing:
|
||||||
|
_playing = true
|
||||||
|
_player.play()
|
||||||
|
_playback = _player.get_stream_playback()
|
||||||
|
_load_mel()
|
||||||
|
|
||||||
|
func play_boss_leviathan(phase: int = 1) -> void:
|
||||||
|
_boss_mode = 2
|
||||||
|
_cur_beat = BOSS2_BEAT
|
||||||
|
_phase2_active = (phase >= 2)
|
||||||
|
_pending_phase2_jump = false
|
||||||
|
_mel_idx = BOSS2_PHASE2_OFFSET if _phase2_active else 0
|
||||||
|
_mel_pos = 0; _mel_phase = 0.0
|
||||||
|
_arp_step = 0; _arp_pos = 0; _arp_phase = 0.0
|
||||||
|
_bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.0
|
||||||
|
_sample_g = 0; _section = 3; _chord = 0
|
||||||
|
if not _playing:
|
||||||
|
_playing = true
|
||||||
|
_player.play()
|
||||||
|
_playback = _player.get_stream_playback()
|
||||||
|
_load_mel()
|
||||||
|
|
||||||
|
# Wird vom game_world.gd aufgerufen, wenn LEVIATHAN Phase 2 erreicht (< 50% HP).
|
||||||
|
# Setzt nur ein Flag — der Sprung geschieht beim nächsten Note-Wechsel,
|
||||||
|
# damit der Rhythmus nicht bricht.
|
||||||
|
func enter_phase2() -> void:
|
||||||
|
if _boss_mode == 2 and not _phase2_active:
|
||||||
|
_pending_phase2_jump = true
|
||||||
|
|
||||||
|
func play_normal() -> void:
|
||||||
|
if _boss_mode == 0:
|
||||||
|
return # already playing normal — don't restart
|
||||||
|
_boss_mode = 0
|
||||||
|
_cur_beat = BEAT
|
||||||
|
_phase2_active = false; _pending_phase2_jump = false
|
||||||
|
_mel_idx = LOOP_START; _mel_pos = 0; _mel_phase = 0.0
|
||||||
|
_arp_step = 0; _arp_pos = 0; _arp_phase = 0.0
|
||||||
|
_bass_chord = -1; _bass_phase = 0.0; _bass2_phase = 0.0
|
||||||
|
_sample_g = 0
|
||||||
|
_load_mel()
|
||||||
|
|
||||||
|
func stop_music() -> void:
|
||||||
|
_playing = false
|
||||||
|
_player.stop()
|
||||||
|
|
||||||
|
func set_music_volume(linear: float) -> void:
|
||||||
|
var idx := AudioServer.get_bus_index("Music")
|
||||||
|
if idx != -1:
|
||||||
|
AudioServer.set_bus_volume_db(idx,
|
||||||
|
linear_to_db(linear) if linear > 0.0 else -80.0)
|
||||||
|
|
||||||
|
func set_muted(muted: bool) -> void:
|
||||||
|
var idx := AudioServer.get_bus_index("Music")
|
||||||
|
if idx != -1:
|
||||||
|
AudioServer.set_bus_mute(idx, muted)
|
||||||
|
|
||||||
|
# ── Hauptschleife ─────────────────────────────────────────────────────────
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if _playing:
|
||||||
|
_fill_buffer()
|
||||||
|
|
||||||
|
func _fill_buffer() -> void:
|
||||||
|
var frames := _playback.get_frames_available()
|
||||||
|
for _i in frames:
|
||||||
|
|
||||||
|
# ── Melodie-Note weiterschalten ──────────────────────────────
|
||||||
|
if _mel_pos >= _mel_samples:
|
||||||
|
# Phase-2-Sprung bei LEVIATHAN: smooth am Note-Ende
|
||||||
|
if _pending_phase2_jump and _boss_mode == 2:
|
||||||
|
_mel_idx = BOSS2_PHASE2_OFFSET
|
||||||
|
_phase2_active = true
|
||||||
|
_pending_phase2_jump = false
|
||||||
|
else:
|
||||||
|
_mel_idx += 1
|
||||||
|
|
||||||
|
var score: Array
|
||||||
|
var loop_start: int
|
||||||
|
match _boss_mode:
|
||||||
|
2:
|
||||||
|
score = BOSS2_SCORE
|
||||||
|
# Phase-2 loopt nur die zweite Hälfte (endlose Eskalation)
|
||||||
|
loop_start = BOSS2_PHASE2_OFFSET if _phase2_active else BOSS2_LOOP_START
|
||||||
|
1:
|
||||||
|
score = BOSS_SCORE
|
||||||
|
loop_start = BOSS_LOOP_START
|
||||||
|
_:
|
||||||
|
score = SCORE
|
||||||
|
loop_start = LOOP_START
|
||||||
|
|
||||||
|
if _mel_idx >= score.size():
|
||||||
|
if loop:
|
||||||
|
_mel_idx = loop_start
|
||||||
|
else:
|
||||||
|
_playing = false
|
||||||
|
_player.stop()
|
||||||
|
return
|
||||||
|
_mel_pos = 0
|
||||||
|
_load_mel()
|
||||||
|
|
||||||
|
# ── Arp-Note weiterschalten (16tel) ──────────────────────────
|
||||||
|
if _arp_pos >= _arp_samples:
|
||||||
|
_arp_step = (_arp_step + 1) % 4
|
||||||
|
_arp_pos = 0
|
||||||
|
_load_arp()
|
||||||
|
|
||||||
|
# ── Samples mischen ──────────────────────────────────────────
|
||||||
|
var s := 0.0
|
||||||
|
|
||||||
|
# Voice 0: Melodie
|
||||||
|
# Normal: Triangle warm · Boss1 (WRAITH): Square scharf · Boss2 (LEVIATHAN): Triangle mit langem Attack
|
||||||
|
if _mel_freq > 0.0:
|
||||||
|
var attack: int
|
||||||
|
var release: int
|
||||||
|
match _boss_mode:
|
||||||
|
2: attack = 800; release = 1500
|
||||||
|
1: attack = 100; release = 500
|
||||||
|
_: attack = 200; release = 1200
|
||||||
|
var env := _env(_mel_pos, _mel_samples, attack, release)
|
||||||
|
var ph := fmod(_mel_phase, 1.0)
|
||||||
|
if _boss_mode == 1:
|
||||||
|
# Square-Wave: schärfer, beißender Klang
|
||||||
|
var sq := 1.0 if ph < 0.5 else -1.0
|
||||||
|
s += sq * env * 0.26
|
||||||
|
else:
|
||||||
|
# Triangle-Wave: warm, vibraphone-artig (normal & boss2)
|
||||||
|
var tri := 1.0 - absf(4.0 * ph - 2.0)
|
||||||
|
var mel_gain := 0.32 if _boss_mode == 2 else 0.38
|
||||||
|
s += tri * env * mel_gain
|
||||||
|
# Phase-2-Oktav-Layer: zusätzliche Triangle eine Oktave höher
|
||||||
|
if _boss_mode == 2 and _phase2_active:
|
||||||
|
var ph2 := fmod(_mel_phase * 2.0, 1.0)
|
||||||
|
var tri2 := 1.0 - absf(4.0 * ph2 - 2.0)
|
||||||
|
s += tri2 * env * 0.14
|
||||||
|
_mel_phase += _mel_freq / SAMPLE_RATE
|
||||||
|
|
||||||
|
# Voice 1: Arpeggio
|
||||||
|
# Normal/Boss1: Square 50% · Boss2: leise Triangle (Sternengeflimmer)
|
||||||
|
if _arp_freq > 0.0:
|
||||||
|
var env := _env(_arp_pos, _arp_samples, 40, 400)
|
||||||
|
var ph := fmod(_arp_phase, 1.0)
|
||||||
|
if _boss_mode == 2:
|
||||||
|
var tri := 1.0 - absf(4.0 * ph - 2.0)
|
||||||
|
var gain := 0.09 if _phase2_active else 0.06
|
||||||
|
s += tri * env * gain
|
||||||
|
else:
|
||||||
|
var sq := 1.0 if ph < 0.5 else -1.0
|
||||||
|
var gain: float
|
||||||
|
if _section == 0:
|
||||||
|
gain = 0.05
|
||||||
|
elif _boss_mode == 1:
|
||||||
|
gain = 0.20
|
||||||
|
else:
|
||||||
|
gain = 0.11
|
||||||
|
s += sq * env * gain
|
||||||
|
_arp_phase += _arp_freq / SAMPLE_RATE
|
||||||
|
|
||||||
|
# Voice 2: Bass — Sawtooth (ab section 1)
|
||||||
|
# Boss2: zusätzlicher 2. Saw-Oszillator, detunet für Drone-Schwebung
|
||||||
|
if _section >= 1 and _bass_freq > 0.0:
|
||||||
|
var ph := fmod(_bass_phase, 1.0)
|
||||||
|
var bass_gain: float
|
||||||
|
if _boss_mode == 2:
|
||||||
|
bass_gain = 0.22
|
||||||
|
elif _boss_mode == 1:
|
||||||
|
bass_gain = 0.30
|
||||||
|
elif _section == 1:
|
||||||
|
bass_gain = 0.15
|
||||||
|
else:
|
||||||
|
bass_gain = 0.25
|
||||||
|
s += (2.0 * ph - 1.0) * bass_gain
|
||||||
|
_bass_phase += _bass_freq / SAMPLE_RATE
|
||||||
|
|
||||||
|
if _boss_mode == 2:
|
||||||
|
# 2. Oszillator detunet (~3‰ = leichte Schwebung bei ~82 Hz: ~0.25 Hz)
|
||||||
|
var ph2 := fmod(_bass2_phase, 1.0)
|
||||||
|
s += (2.0 * ph2 - 1.0) * 0.22
|
||||||
|
_bass2_phase += (_bass_freq * 1.003) / SAMPLE_RATE
|
||||||
|
|
||||||
|
# Voice 3: Schlagzeug (ab section 2 oder Boss-Modus)
|
||||||
|
if _section >= 2:
|
||||||
|
if _boss_mode == 2:
|
||||||
|
# ── LEVIATHAN: Tom-lastig, keine Hi-Hats ─────────────────
|
||||||
|
# Kick auf Beat 1 jedes Bars (in Phase 2 zusätzlich auf Beat 3)
|
||||||
|
var bar_period := int(4.0 * _cur_beat * SAMPLE_RATE)
|
||||||
|
var bar_pos := _sample_g % bar_period
|
||||||
|
var kick_len := 900
|
||||||
|
if bar_pos < kick_len:
|
||||||
|
var kick_env := 1.0 - float(bar_pos) / float(kick_len)
|
||||||
|
var kick_f := 1.0 - float(bar_pos) / float(kick_len)
|
||||||
|
s += sin(kick_f * kick_f * 55.0) * kick_env * 0.26
|
||||||
|
if _phase2_active:
|
||||||
|
var beat3_offset := int(2.0 * _cur_beat * SAMPLE_RATE)
|
||||||
|
var kick2_pos := (_sample_g + bar_period - beat3_offset) % bar_period
|
||||||
|
if kick2_pos < kick_len:
|
||||||
|
var kick2_env := 1.0 - float(kick2_pos) / float(kick_len)
|
||||||
|
var kick2_f := 1.0 - float(kick2_pos) / float(kick_len)
|
||||||
|
s += sin(kick2_f * kick2_f * 55.0) * kick2_env * 0.22
|
||||||
|
|
||||||
|
# Floor-Tom auf Beats 1 & 3 (period = 2 beats, tiefer Sinus mit Pitch-Drop)
|
||||||
|
var tom_period := int(2.0 * _cur_beat * SAMPLE_RATE)
|
||||||
|
var tom_pos := _sample_g % tom_period
|
||||||
|
var tom_len := 2200
|
||||||
|
if tom_pos < tom_len:
|
||||||
|
var tom_t := float(tom_pos) / float(tom_len)
|
||||||
|
var tom_env := 1.0 - tom_t
|
||||||
|
# Pitch-Envelope: startet bei 130 Hz, fällt auf 70 Hz
|
||||||
|
var tom_freq := 130.0 - 60.0 * tom_t
|
||||||
|
var tom_ph := float(tom_pos) / float(SAMPLE_RATE) * tom_freq
|
||||||
|
s += sin(tom_ph * TAU) * tom_env * 0.18
|
||||||
|
|
||||||
|
# Mid-Tom-Fill: Beat 4 jedes 4. Bars (= Ende jeder 4-Bar-Phrase)
|
||||||
|
var phrase_period := int(16.0 * _cur_beat * SAMPLE_RATE)
|
||||||
|
var phrase_pos := _sample_g % phrase_period
|
||||||
|
var mid_tom_start := int(15.0 * _cur_beat * SAMPLE_RATE)
|
||||||
|
var mid_tom_rel := phrase_pos - mid_tom_start
|
||||||
|
if mid_tom_rel >= 0:
|
||||||
|
# 4 schnelle Mid-Tom-Hits auf dem letzten Beat
|
||||||
|
var hit_len: int = int(_cur_beat * SAMPLE_RATE * 0.25)
|
||||||
|
@warning_ignore("integer_division")
|
||||||
|
var hit_idx: int = mid_tom_rel / hit_len
|
||||||
|
if hit_idx < 4:
|
||||||
|
var hit_pos := mid_tom_rel % hit_len
|
||||||
|
if hit_pos < 1400:
|
||||||
|
var mt_t := float(hit_pos) / 1400.0
|
||||||
|
var mt_env := 1.0 - mt_t
|
||||||
|
var mt_freq := 220.0 - 80.0 * mt_t
|
||||||
|
var mt_ph := float(hit_pos) / float(SAMPLE_RATE) * mt_freq
|
||||||
|
s += sin(mt_ph * TAU) * mt_env * 0.14
|
||||||
|
|
||||||
|
# Reverse-Whoosh alle 8 Bars: aufsteigende Noise-Envelope als Übergang
|
||||||
|
var whoosh_period := int(32.0 * _cur_beat * SAMPLE_RATE)
|
||||||
|
var whoosh_pos := _sample_g % whoosh_period
|
||||||
|
var whoosh_start := whoosh_period - int(2.0 * _cur_beat * SAMPLE_RATE)
|
||||||
|
if whoosh_pos >= whoosh_start:
|
||||||
|
var w_t := float(whoosh_pos - whoosh_start) / float(whoosh_period - whoosh_start)
|
||||||
|
var w_env := w_t * w_t # aufsteigend
|
||||||
|
s += randf_range(-1.0, 1.0) * w_env * 0.09
|
||||||
|
else:
|
||||||
|
# ── Normal & WRAITH: Hi-Hats + Kick + (Boss1) Snare ──────
|
||||||
|
var hat_beat_frac := 0.25 if _boss_mode == 1 else 0.5
|
||||||
|
var hat_period := int(hat_beat_frac * _cur_beat * SAMPLE_RATE)
|
||||||
|
var hat_pos := _sample_g % hat_period
|
||||||
|
var hat_len := 200 if _boss_mode == 1 else 600
|
||||||
|
if hat_pos < hat_len:
|
||||||
|
var hat_env := 1.0 - float(hat_pos) / float(hat_len)
|
||||||
|
var hat_vol := 0.08 if _boss_mode == 1 else 0.07
|
||||||
|
s += randf_range(-1.0, 1.0) * hat_env * hat_vol
|
||||||
|
|
||||||
|
var kick_period := int(_cur_beat * SAMPLE_RATE)
|
||||||
|
var kick_pos := _sample_g % kick_period
|
||||||
|
var kick_len := 750 if _boss_mode == 1 else 800
|
||||||
|
if kick_pos < kick_len:
|
||||||
|
var kick_env := 1.0 - float(kick_pos) / float(kick_len)
|
||||||
|
var kick_f := 1.0 - float(kick_pos) / float(kick_len)
|
||||||
|
var kick_vol := 0.22 if _boss_mode == 1 else 0.18
|
||||||
|
s += sin(kick_f * kick_f * 60.0) * kick_env * kick_vol
|
||||||
|
|
||||||
|
if _boss_mode == 1:
|
||||||
|
var snare_period := int(2.0 * _cur_beat * SAMPLE_RATE)
|
||||||
|
var snare_offset := int(1.0 * _cur_beat * SAMPLE_RATE)
|
||||||
|
var snare_pos := (_sample_g + snare_offset) % snare_period
|
||||||
|
if snare_pos < 340:
|
||||||
|
var snare_env := 1.0 - float(snare_pos) / 340.0
|
||||||
|
s += randf_range(-1.0, 1.0) * snare_env * 0.13
|
||||||
|
|
||||||
|
_playback.push_frame(Vector2(s, s))
|
||||||
|
_mel_pos += 1
|
||||||
|
_arp_pos += 1
|
||||||
|
_sample_g += 1
|
||||||
|
|
||||||
|
# ── Note laden ────────────────────────────────────────────────────────────
|
||||||
|
func _load_mel() -> void:
|
||||||
|
var score: Array
|
||||||
|
match _boss_mode:
|
||||||
|
2: score = BOSS2_SCORE
|
||||||
|
1: score = BOSS_SCORE
|
||||||
|
_: score = SCORE
|
||||||
|
var e: Array = score[_mel_idx]
|
||||||
|
_mel_freq = FREQ.get(e[0], 0.0)
|
||||||
|
_mel_samples = int(float(e[1]) * _cur_beat * SAMPLE_RATE)
|
||||||
|
_mel_phase = 0.0
|
||||||
|
_section = e[3]
|
||||||
|
|
||||||
|
if e[2] != _chord:
|
||||||
|
_chord = e[2]
|
||||||
|
_arp_step = 0
|
||||||
|
_arp_pos = 0
|
||||||
|
_load_arp()
|
||||||
|
if e[2] != _bass_chord:
|
||||||
|
_bass_chord = e[2]
|
||||||
|
var bass_dict: Dictionary
|
||||||
|
match _boss_mode:
|
||||||
|
2: bass_dict = BOSS2_BASS
|
||||||
|
1: bass_dict = BOSS_BASS
|
||||||
|
_: bass_dict = BASS
|
||||||
|
_bass_freq = FREQ.get(bass_dict[_bass_chord], 0.0)
|
||||||
|
_bass_phase = 0.0
|
||||||
|
_bass2_phase = 0.0
|
||||||
|
|
||||||
|
func _load_arp() -> void:
|
||||||
|
var arp_dict: Dictionary
|
||||||
|
var arp_step_count: int = 4
|
||||||
|
match _boss_mode:
|
||||||
|
2:
|
||||||
|
arp_dict = BOSS2_ARP
|
||||||
|
# Arp-Geschwindigkeit: Phase 1 = 8tel, Phase 2 = 16tel (verdichtet)
|
||||||
|
var arp_frac := 0.25 if _phase2_active else 0.5
|
||||||
|
_arp_samples = int(arp_frac * _cur_beat * SAMPLE_RATE)
|
||||||
|
1:
|
||||||
|
arp_dict = BOSS_ARP
|
||||||
|
_arp_samples = int(0.25 * _cur_beat * SAMPLE_RATE)
|
||||||
|
_:
|
||||||
|
arp_dict = ARP
|
||||||
|
_arp_samples = int(0.25 * _cur_beat * SAMPLE_RATE)
|
||||||
|
_arp_freq = FREQ.get(arp_dict[_chord][_arp_step % arp_step_count], 0.0)
|
||||||
|
_arp_phase = 0.0
|
||||||
|
|
||||||
|
# ── Hüllkurve ─────────────────────────────────────────────────────────────
|
||||||
|
func _env(pos: int, total: int, attack: int, release: int) -> float:
|
||||||
|
if pos < attack:
|
||||||
|
return float(pos) / maxf(attack, 1.0)
|
||||||
|
elif pos > total - release:
|
||||||
|
return float(total - pos) / maxf(release, 1.0)
|
||||||
|
return 1.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dxun4apx8cxf1
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
# Change SERVER_URL to your server address before deploying.
|
||||||
|
# Set the same HMAC_SECRET on the server via env var SPACEL_SECRET.
|
||||||
|
const SERVER_URL := "https://lb.alpacaman.de"
|
||||||
|
const HMAC_SECRET := "63f4945d921d599f27ae4fdf5bada3f1"
|
||||||
|
|
||||||
|
signal scores_fetched(scores: Array, error: String)
|
||||||
|
signal submit_done(ok: bool)
|
||||||
|
|
||||||
|
var _http: HTTPRequest
|
||||||
|
var _mode: String = ""
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_http = HTTPRequest.new()
|
||||||
|
add_child(_http)
|
||||||
|
_http.request_completed.connect(_on_completed)
|
||||||
|
|
||||||
|
func fetch_scores() -> void:
|
||||||
|
if _mode != "":
|
||||||
|
return
|
||||||
|
_mode = "fetch"
|
||||||
|
_http.request(SERVER_URL + "/scores")
|
||||||
|
|
||||||
|
func submit_score(player_name: String, score: int, wave: int) -> void:
|
||||||
|
if _mode != "":
|
||||||
|
return
|
||||||
|
var ts := int(Time.get_unix_time_from_system())
|
||||||
|
var raw := player_name + str(score) + str(wave) + str(ts)
|
||||||
|
var hmac := Crypto.new().hmac_digest(
|
||||||
|
HashingContext.HASH_SHA256,
|
||||||
|
HMAC_SECRET.to_utf8_buffer(),
|
||||||
|
raw.to_utf8_buffer()
|
||||||
|
).hex_encode()
|
||||||
|
var body := JSON.stringify({
|
||||||
|
"name": player_name,
|
||||||
|
"score": score,
|
||||||
|
"wave": wave,
|
||||||
|
"timestamp": ts,
|
||||||
|
"hmac": hmac
|
||||||
|
})
|
||||||
|
_mode = "submit"
|
||||||
|
_http.request(
|
||||||
|
SERVER_URL + "/scores",
|
||||||
|
PackedStringArray(["Content-Type: application/json"]),
|
||||||
|
HTTPClient.METHOD_POST,
|
||||||
|
body
|
||||||
|
)
|
||||||
|
|
||||||
|
func _on_completed(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||||
|
var ok := result == HTTPRequest.RESULT_SUCCESS and code == 200
|
||||||
|
if _mode == "fetch":
|
||||||
|
if ok:
|
||||||
|
var json := JSON.new()
|
||||||
|
if json.parse(body.get_string_from_utf8()) == OK:
|
||||||
|
var data = json.get_data()
|
||||||
|
if data is Array:
|
||||||
|
scores_fetched.emit(data as Array, "")
|
||||||
|
_mode = ""
|
||||||
|
return
|
||||||
|
scores_fetched.emit([], "lb_error")
|
||||||
|
elif _mode == "submit":
|
||||||
|
submit_done.emit(ok)
|
||||||
|
_mode = ""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bs401f5368qos
|
||||||
@@ -0,0 +1,380 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# Pause-Menü + Optionen-Menü im Cockpit-Stil.
|
||||||
|
# Node braucht process_mode = PROCESS_MODE_ALWAYS (wird in _ready gesetzt).
|
||||||
|
|
||||||
|
signal resume_requested
|
||||||
|
signal quit_to_menu_requested
|
||||||
|
|
||||||
|
var W: float = 960.0
|
||||||
|
var H: float = 600.0
|
||||||
|
|
||||||
|
const COL_BG := Color(0.0, 0.0, 0.04, 0.75)
|
||||||
|
const COL_PRIMARY := Color(0.0, 1.0, 0.533, 1.0)
|
||||||
|
const COL_ACCENT := Color(0.27, 1.0, 0.8, 1.0)
|
||||||
|
const COL_DIM := Color(0.0, 0.27, 0.13, 0.55)
|
||||||
|
const COL_WHITE := Color(1.0, 1.0, 1.0, 0.90)
|
||||||
|
const COL_WARN := Color(1.0, 0.67, 0.0, 0.90)
|
||||||
|
|
||||||
|
# 0=Pause, 1=Optionen, 2=Steuerung
|
||||||
|
var _screen: int = 0
|
||||||
|
var _cursor: int = 0
|
||||||
|
var _blink: float = 0.0
|
||||||
|
var _ctrl_cursor: int = 0
|
||||||
|
var _ctrl_waiting: bool = false
|
||||||
|
|
||||||
|
func _pause_items() -> Array:
|
||||||
|
return [Tr.t("pause_resume"), Tr.t("menu_options"), Tr.t("pause_main_menu"), Tr.t("menu_quit")]
|
||||||
|
|
||||||
|
func _opt_items() -> Array:
|
||||||
|
return [Tr.t("opt_sfx"), Tr.t("opt_music"), Tr.t("opt_mute"), Tr.t("opt_fullscreen"),
|
||||||
|
Tr.t("opt_nebula"), Tr.t("opt_stars"), Tr.t("opt_language"), Tr.t("opt_touch"),
|
||||||
|
Tr.t("opt_controls"), Tr.t("opt_back")]
|
||||||
|
|
||||||
|
func _ctrl_labels() -> Array:
|
||||||
|
return [Tr.t("ctrl_thrust"), Tr.t("ctrl_left"), Tr.t("ctrl_right"),
|
||||||
|
Tr.t("ctrl_shoot"), Tr.t("ctrl_wipe")]
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
|
||||||
|
func open() -> void:
|
||||||
|
_screen = 0
|
||||||
|
_cursor = 0
|
||||||
|
_blink = 0.0
|
||||||
|
_ctrl_waiting = false
|
||||||
|
visible = true
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if not visible: return
|
||||||
|
_blink += delta
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if not visible: return
|
||||||
|
match _screen:
|
||||||
|
0: _input_pause(event)
|
||||||
|
1: _input_options(event)
|
||||||
|
2: _input_controls(event)
|
||||||
|
|
||||||
|
func _input_pause(event: InputEvent) -> void:
|
||||||
|
var pitems := _pause_items()
|
||||||
|
if event.is_action_pressed("ui_up"):
|
||||||
|
_cursor = (_cursor - 1 + pitems.size()) % pitems.size()
|
||||||
|
elif event.is_action_pressed("ui_down"):
|
||||||
|
_cursor = (_cursor + 1) % pitems.size()
|
||||||
|
elif event.is_action_pressed("ui_accept"):
|
||||||
|
match _cursor:
|
||||||
|
0: _do_resume()
|
||||||
|
1: _screen = 1; _cursor = 0
|
||||||
|
2: _do_quit_menu()
|
||||||
|
3: get_tree().quit()
|
||||||
|
elif event.is_action_pressed("ui_cancel"):
|
||||||
|
_do_resume()
|
||||||
|
|
||||||
|
func _input_options(event: InputEvent) -> void:
|
||||||
|
var opts := _opt_items()
|
||||||
|
if event.is_action_pressed("ui_up"):
|
||||||
|
_cursor = (_cursor - 1 + opts.size()) % opts.size()
|
||||||
|
elif event.is_action_pressed("ui_down"):
|
||||||
|
_cursor = (_cursor + 1) % opts.size()
|
||||||
|
elif event.is_action_pressed("ui_left"):
|
||||||
|
_change_option(_cursor, -1)
|
||||||
|
elif event.is_action_pressed("ui_right"):
|
||||||
|
_change_option(_cursor, 1)
|
||||||
|
elif event.is_action_pressed("ui_accept"):
|
||||||
|
if _cursor == opts.size() - 1: # Back
|
||||||
|
_screen = 0; _cursor = 1
|
||||||
|
elif _cursor == opts.size() - 2: # Controls
|
||||||
|
_screen = 2; _ctrl_cursor = 0; _ctrl_waiting = false
|
||||||
|
else:
|
||||||
|
_change_option(_cursor, 1)
|
||||||
|
elif event.is_action_pressed("ui_cancel"):
|
||||||
|
_close_options()
|
||||||
|
|
||||||
|
func _input_controls(event: InputEvent) -> void:
|
||||||
|
var labels := _ctrl_labels()
|
||||||
|
var reset_idx := labels.size()
|
||||||
|
var back_idx := labels.size() + 1
|
||||||
|
var total := labels.size() + 2
|
||||||
|
|
||||||
|
if _ctrl_waiting:
|
||||||
|
if event is InputEventKey and event.pressed:
|
||||||
|
var kc: int = event.physical_keycode
|
||||||
|
if kc in [KEY_SHIFT, KEY_CTRL, KEY_ALT, KEY_META, KEY_CAPSLOCK]:
|
||||||
|
return
|
||||||
|
if kc == KEY_ESCAPE:
|
||||||
|
_ctrl_waiting = false
|
||||||
|
return
|
||||||
|
_rebind_key(Settings.REBIND_ACTIONS[_ctrl_cursor], kc)
|
||||||
|
_ctrl_waiting = false
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
elif event is InputEventJoypadButton and event.pressed:
|
||||||
|
_rebind_joy(Settings.REBIND_ACTIONS[_ctrl_cursor], event.button_index)
|
||||||
|
_ctrl_waiting = false
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
|
|
||||||
|
if event.is_action_pressed("ui_up"):
|
||||||
|
_ctrl_cursor = (_ctrl_cursor - 1 + total) % total
|
||||||
|
elif event.is_action_pressed("ui_down"):
|
||||||
|
_ctrl_cursor = (_ctrl_cursor + 1) % total
|
||||||
|
elif event.is_action_pressed("ui_accept"):
|
||||||
|
if _ctrl_cursor == back_idx:
|
||||||
|
_screen = 1
|
||||||
|
elif _ctrl_cursor == reset_idx:
|
||||||
|
Settings.reset_key_bindings()
|
||||||
|
else:
|
||||||
|
_ctrl_waiting = true
|
||||||
|
elif event.is_action_pressed("ui_cancel"):
|
||||||
|
_screen = 1
|
||||||
|
|
||||||
|
func _rebind_key(action: String, physical_keycode: int) -> void:
|
||||||
|
if not InputMap.has_action(action): return
|
||||||
|
for ev in InputMap.action_get_events(action):
|
||||||
|
if ev is InputEventKey:
|
||||||
|
InputMap.action_erase_event(action, ev)
|
||||||
|
var new_ev := InputEventKey.new()
|
||||||
|
new_ev.physical_keycode = physical_keycode
|
||||||
|
InputMap.action_add_event(action, new_ev)
|
||||||
|
Settings.key_bindings[action] = physical_keycode
|
||||||
|
Settings.save_settings()
|
||||||
|
|
||||||
|
func _rebind_joy(action: String, button_index: int) -> void:
|
||||||
|
if not InputMap.has_action(action): return
|
||||||
|
for ev in InputMap.action_get_events(action):
|
||||||
|
if ev is InputEventJoypadButton:
|
||||||
|
InputMap.action_erase_event(action, ev)
|
||||||
|
var new_ev := InputEventJoypadButton.new()
|
||||||
|
new_ev.button_index = button_index
|
||||||
|
InputMap.action_add_event(action, new_ev)
|
||||||
|
Settings.key_bindings[action + "_joy"] = button_index
|
||||||
|
Settings.save_settings()
|
||||||
|
|
||||||
|
func _change_option(idx: int, dir: int) -> void:
|
||||||
|
match idx:
|
||||||
|
0: # SFX volume
|
||||||
|
Settings.master_volume = clamp(Settings.master_volume + dir * 0.1, 0.0, 1.0)
|
||||||
|
Settings.apply_volume()
|
||||||
|
1: # Music volume
|
||||||
|
Settings.music_volume = clamp(Settings.music_volume + dir * 0.1, 0.0, 1.0)
|
||||||
|
Settings.apply_music_volume()
|
||||||
|
2: # Mute
|
||||||
|
Settings.sfx_muted = not Settings.sfx_muted
|
||||||
|
Settings.apply_volume()
|
||||||
|
3: # Fullscreen
|
||||||
|
Settings.fullscreen = not Settings.fullscreen
|
||||||
|
if Settings.fullscreen:
|
||||||
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
|
||||||
|
else:
|
||||||
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
||||||
|
4: # Nebula
|
||||||
|
Settings.nebula_enabled = not Settings.nebula_enabled
|
||||||
|
5: # Stars
|
||||||
|
Settings.star_density = (Settings.star_density + dir + 3) % 3
|
||||||
|
6: # Language
|
||||||
|
Settings.language = "en" if Settings.language == "de" else "de"
|
||||||
|
7: # Touch mode
|
||||||
|
Settings.touch_mode = (Settings.touch_mode + 1) % 3
|
||||||
|
# 8 = Controls → handled in _input_options
|
||||||
|
Settings.save_settings()
|
||||||
|
|
||||||
|
func _do_resume() -> void:
|
||||||
|
visible = false
|
||||||
|
resume_requested.emit()
|
||||||
|
|
||||||
|
func _close_options() -> void:
|
||||||
|
_screen = 0
|
||||||
|
_cursor = 1
|
||||||
|
|
||||||
|
func _do_quit_menu() -> void:
|
||||||
|
visible = false
|
||||||
|
quit_to_menu_requested.emit()
|
||||||
|
|
||||||
|
# ── Drawing ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var vs: Vector2 = get_viewport_rect().size
|
||||||
|
W = vs.x; H = vs.y
|
||||||
|
draw_rect(get_viewport_rect(), COL_BG)
|
||||||
|
match _screen:
|
||||||
|
0: _draw_pause()
|
||||||
|
1: _draw_options()
|
||||||
|
2: _draw_controls()
|
||||||
|
|
||||||
|
func _draw_pause() -> void:
|
||||||
|
var bw := 300.0; var bh := 255.0
|
||||||
|
var bx := (W - bw) * 0.5; var by := (H - bh) * 0.5
|
||||||
|
_draw_terminal_box(bx, by, bw, bh)
|
||||||
|
_draw_text_c(Tr.t("pause_title"), W * 0.5, by + 16.0, 10, COL_DIM)
|
||||||
|
|
||||||
|
var pitems := _pause_items()
|
||||||
|
var item_y := by + 55.0
|
||||||
|
for i in pitems.size():
|
||||||
|
var pulse := 0.6 + 0.4 * sin(_blink * 3.0)
|
||||||
|
var col: Color
|
||||||
|
if i == _cursor:
|
||||||
|
col = Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse)
|
||||||
|
else:
|
||||||
|
col = Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.65)
|
||||||
|
var prefix := "▶ " if i == _cursor else " "
|
||||||
|
_draw_text_c(prefix + pitems[i], W * 0.5, item_y + i * 40.0, 14, col)
|
||||||
|
|
||||||
|
_draw_text_c(Tr.hint("pause_footer"), W * 0.5, by + bh - 20.0, 8, COL_DIM)
|
||||||
|
|
||||||
|
func _draw_options() -> void:
|
||||||
|
var bw := 480.0; var bh := 380.0
|
||||||
|
var bx := (W - bw) * 0.5; var by := (H - bh) * 0.5
|
||||||
|
_draw_terminal_box(bx, by, bw, bh)
|
||||||
|
_draw_text_c(Tr.t("opt_title"), W * 0.5, by + 16.0, 10, COL_DIM)
|
||||||
|
|
||||||
|
var star_labels := [Tr.t("star_low"), Tr.t("star_mid"), Tr.t("star_high")]
|
||||||
|
var touch_labels := [Tr.t("touch_auto"), Tr.t("touch_on"), Tr.t("touch_off")]
|
||||||
|
var opts := _opt_items()
|
||||||
|
var values: Array = [
|
||||||
|
_volume_bar(Settings.master_volume),
|
||||||
|
_volume_bar(Settings.music_volume),
|
||||||
|
Tr.t("yes") if Settings.sfx_muted else Tr.t("no"),
|
||||||
|
Tr.t("yes") if Settings.fullscreen else Tr.t("no"),
|
||||||
|
Tr.t("yes") if Settings.nebula_enabled else Tr.t("no"),
|
||||||
|
star_labels[Settings.star_density],
|
||||||
|
Settings.language.to_upper(),
|
||||||
|
touch_labels[Settings.touch_mode],
|
||||||
|
"→", # Controls sub-screen
|
||||||
|
"" # Back (rendered specially)
|
||||||
|
]
|
||||||
|
|
||||||
|
var item_y := by + 52.0
|
||||||
|
for i in opts.size():
|
||||||
|
var is_sel := i == _cursor
|
||||||
|
var pulse := 0.6 + 0.4 * sin(_blink * 3.0)
|
||||||
|
var row_y := item_y + i * 36.0
|
||||||
|
|
||||||
|
if i == opts.size() - 1: # Back
|
||||||
|
var p := pulse if is_sel else 0.4
|
||||||
|
_draw_text_c("[ %s ]" % opts[i], W * 0.5, row_y, 12,
|
||||||
|
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, p))
|
||||||
|
else:
|
||||||
|
var label_col: Color
|
||||||
|
if is_sel:
|
||||||
|
label_col = Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse)
|
||||||
|
else:
|
||||||
|
label_col = Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.70)
|
||||||
|
var val_col := Color(COL_WHITE.r, COL_WHITE.g, COL_WHITE.b,
|
||||||
|
0.90 if is_sel else 0.50)
|
||||||
|
var prefix := "▶ " if is_sel else " "
|
||||||
|
_draw_text(prefix + opts[i], bx + 28.0, row_y, 11, label_col)
|
||||||
|
if values[i] != "":
|
||||||
|
var val_str: String
|
||||||
|
if values[i] == "→":
|
||||||
|
val_str = "→"
|
||||||
|
else:
|
||||||
|
val_str = "◄ %s ►" % values[i]
|
||||||
|
_draw_text(val_str,
|
||||||
|
bx + bw - 28.0 - _text_w(val_str, 11), row_y, 11, val_col)
|
||||||
|
|
||||||
|
_draw_text_c(Tr.hint("opt_footer"), W * 0.5, by + bh - 20.0, 8, COL_DIM)
|
||||||
|
|
||||||
|
func _draw_controls() -> void:
|
||||||
|
var bw := 400.0; var bh := 310.0
|
||||||
|
var bx := (W - bw) * 0.5; var by := (H - bh) * 0.5
|
||||||
|
_draw_terminal_box(bx, by, bw, bh)
|
||||||
|
_draw_text_c(Tr.t("ctrl_title"), W * 0.5, by + 16.0, 10, COL_DIM)
|
||||||
|
|
||||||
|
var labels := _ctrl_labels()
|
||||||
|
var actions := Settings.REBIND_ACTIONS
|
||||||
|
var reset_idx := labels.size()
|
||||||
|
var back_idx := labels.size() + 1
|
||||||
|
var total := labels.size() + 2
|
||||||
|
|
||||||
|
var item_y := by + 52.0
|
||||||
|
for i in total:
|
||||||
|
var is_sel := i == _ctrl_cursor
|
||||||
|
var pulse := 0.6 + 0.4 * sin(_blink * 3.0)
|
||||||
|
var row_y := item_y + i * 36.0
|
||||||
|
|
||||||
|
if i == back_idx:
|
||||||
|
_draw_text_c("[ %s ]" % Tr.t("opt_back"), W * 0.5, row_y, 12,
|
||||||
|
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse if is_sel else 0.4))
|
||||||
|
elif i == reset_idx:
|
||||||
|
_draw_text_c("[ %s ]" % Tr.t("ctrl_reset"), W * 0.5, row_y, 12,
|
||||||
|
Color(COL_WARN.r, COL_WARN.g, COL_WARN.b, pulse if is_sel else 0.35))
|
||||||
|
else:
|
||||||
|
var label_col := Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b,
|
||||||
|
pulse) if is_sel else Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.70)
|
||||||
|
var val_col := Color(COL_WHITE.r, COL_WHITE.g, COL_WHITE.b,
|
||||||
|
0.90 if is_sel else 0.50)
|
||||||
|
var prefix := "▶ " if is_sel else " "
|
||||||
|
|
||||||
|
var key_str: String
|
||||||
|
if _ctrl_waiting and is_sel:
|
||||||
|
key_str = Tr.t("ctrl_waiting")
|
||||||
|
val_col = Color(COL_WARN.r, COL_WARN.g, COL_WARN.b,
|
||||||
|
0.5 + 0.5 * sin(_blink * 6.0))
|
||||||
|
else:
|
||||||
|
key_str = "[ %s ]" % _action_key_name(actions[i])
|
||||||
|
|
||||||
|
_draw_text(prefix + labels[i], bx + 28.0, row_y, 11, label_col)
|
||||||
|
_draw_text(key_str, bx + bw - 28.0 - _text_w(key_str, 11), row_y, 11, val_col)
|
||||||
|
|
||||||
|
_draw_text_c(Tr.hint("ctrl_footer"), W * 0.5, by + bh - 20.0, 8, COL_DIM)
|
||||||
|
|
||||||
|
func _action_key_name(action: String) -> String:
|
||||||
|
if not InputMap.has_action(action): return "?"
|
||||||
|
for ev in InputMap.action_get_events(action):
|
||||||
|
if ev is InputEventKey:
|
||||||
|
var kev := ev as InputEventKey
|
||||||
|
var kc: int = kev.physical_keycode if kev.physical_keycode != KEY_NONE else kev.keycode
|
||||||
|
return OS.get_keycode_string(kc)
|
||||||
|
elif ev is InputEventJoypadButton:
|
||||||
|
var jev := ev as InputEventJoypadButton
|
||||||
|
return _joy_btn_label(jev.button_index)
|
||||||
|
return "?"
|
||||||
|
|
||||||
|
func _joy_btn_label(idx: int) -> String:
|
||||||
|
match idx:
|
||||||
|
JOY_BUTTON_A: return "A"
|
||||||
|
JOY_BUTTON_B: return "B"
|
||||||
|
JOY_BUTTON_X: return "X"
|
||||||
|
JOY_BUTTON_Y: return "Y"
|
||||||
|
JOY_BUTTON_LEFT_SHOULDER: return "LB"
|
||||||
|
JOY_BUTTON_RIGHT_SHOULDER: return "RB"
|
||||||
|
JOY_BUTTON_LEFT_STICK: return "L3"
|
||||||
|
JOY_BUTTON_RIGHT_STICK: return "R3"
|
||||||
|
_: return "BTN%d" % idx
|
||||||
|
|
||||||
|
func _volume_bar(vol: float) -> String:
|
||||||
|
var filled := int(round(vol * 10.0))
|
||||||
|
var bar := ""
|
||||||
|
for i in 10:
|
||||||
|
bar += "█" if i < filled else "░"
|
||||||
|
return bar
|
||||||
|
|
||||||
|
# ── Drawing helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _draw_terminal_box(bx: float, by: float, bw: float, bh: float) -> void:
|
||||||
|
draw_rect(Rect2(bx, by, bw, bh), Color(0.0, 0.04, 0.02, 0.95))
|
||||||
|
var bc := Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.35)
|
||||||
|
draw_line(Vector2(bx, by), Vector2(bx+bw, by), bc, 1.0)
|
||||||
|
draw_line(Vector2(bx, by+bh), Vector2(bx+bw, by+bh), bc, 1.0)
|
||||||
|
draw_line(Vector2(bx, by), Vector2(bx, by+bh), bc, 1.0)
|
||||||
|
draw_line(Vector2(bx+bw, by), Vector2(bx+bw, by+bh), bc, 1.0)
|
||||||
|
var cl := 12.0
|
||||||
|
for cx: float in [bx, bx+bw]:
|
||||||
|
for cy: float in [by, by+bh]:
|
||||||
|
var sx := 1.0 if cx == bx else -1.0
|
||||||
|
var sy := 1.0 if cy == by else -1.0
|
||||||
|
draw_line(Vector2(cx, cy), Vector2(cx + sx*cl, cy), COL_PRIMARY, 1.5)
|
||||||
|
draw_line(Vector2(cx, cy), Vector2(cx, cy + sy*cl), COL_PRIMARY, 1.5)
|
||||||
|
|
||||||
|
func _draw_text(text: String, x: float, y: float, sz: int, col: Color) -> void:
|
||||||
|
draw_string(ThemeDB.fallback_font, Vector2(x, y + sz),
|
||||||
|
text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz, col)
|
||||||
|
|
||||||
|
func _draw_text_c(text: String, x: float, y: float, sz: int, col: Color) -> void:
|
||||||
|
var tw := _text_w(text, sz)
|
||||||
|
draw_string(ThemeDB.fallback_font, Vector2(x - tw * 0.5, y + sz),
|
||||||
|
text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz, col)
|
||||||
|
|
||||||
|
func _text_w(text: String, sz: int) -> float:
|
||||||
|
return ThemeDB.fallback_font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://lmuic5md3b5p
|
||||||
@@ -0,0 +1,416 @@
|
|||||||
|
extends RefCounted
|
||||||
|
# Accessed as CosmicObjects.Planet via preload in cosmic_objects.gd.
|
||||||
|
# No class_name to avoid outer-scope shadowing inside CosmicObjects' inner classes.
|
||||||
|
|
||||||
|
enum PType { TERRESTRIAL, DESERT, GAS_GIANT, ICE, LAVA, TOXIC }
|
||||||
|
|
||||||
|
# Orbit / position
|
||||||
|
var orbit_cx: float; var orbit_cy: float
|
||||||
|
var orbit_radius: float; var orbit_angle: float; var orbit_speed: float
|
||||||
|
var x: float; var y: float; var radius: float
|
||||||
|
|
||||||
|
# Legacy public fields (kept for compatibility with game_world.gd / debris)
|
||||||
|
var color: Color
|
||||||
|
var ring: bool = false
|
||||||
|
var moons: Array = []
|
||||||
|
var alpha: float = 1.0
|
||||||
|
var dead: bool = false
|
||||||
|
|
||||||
|
# Capture / tidal state (unchanged)
|
||||||
|
var captured: bool = false
|
||||||
|
var capture_bh_x: float = 0.0; var capture_bh_y: float = 0.0
|
||||||
|
var capture_angle: float = 0.0; var capture_dist: float = 0.0
|
||||||
|
var capture_initial_dist: float = 0.0; var capture_timer: float = 0.0
|
||||||
|
var initial_radius: float = 0.0
|
||||||
|
var debris_trail: Array = []
|
||||||
|
const CAPTURE_DURATION := 1.2
|
||||||
|
const DEBRIS_MAX := 20
|
||||||
|
|
||||||
|
# New: type & palette
|
||||||
|
var ptype: int = PType.TERRESTRIAL
|
||||||
|
var palette: Array = [] # [base, accent, highlight]
|
||||||
|
|
||||||
|
# New: surface noise & animation
|
||||||
|
var surface_noise: FastNoiseLite
|
||||||
|
var cloud_noise: FastNoiseLite
|
||||||
|
var has_clouds: bool = false
|
||||||
|
var spin_angle: float = 0.0
|
||||||
|
var spin_speed: float = 0.25
|
||||||
|
var cloud_drift: float = 0.0
|
||||||
|
var cloud_speed: float = 0.5
|
||||||
|
|
||||||
|
# Gas giant spot
|
||||||
|
var has_spot: bool = false
|
||||||
|
var spot_longitude: float = 0.0 # 0..TAU
|
||||||
|
var spot_latitude: float = 0.0 # in -radius..+radius
|
||||||
|
var spot_radius: float = 2.5
|
||||||
|
var spot_band_offset: float = 0.0 # for band index match
|
||||||
|
|
||||||
|
# Rings (extended)
|
||||||
|
var ring_count: int = 0 # 0 = none
|
||||||
|
var ring_inner: float = 0.0
|
||||||
|
var ring_outer: float = 0.0
|
||||||
|
var ring_tilt: float = 0.25
|
||||||
|
var ring_colors: Array = []
|
||||||
|
var ring_gaps: Array = [] # indices of skipped "bands"
|
||||||
|
|
||||||
|
|
||||||
|
func init(cx: float, cy: float, _world_w: float, _world_h: float) -> void:
|
||||||
|
orbit_cx = cx; orbit_cy = cy
|
||||||
|
orbit_radius = randf_range(19.0, 72.0)
|
||||||
|
orbit_angle = randf() * TAU
|
||||||
|
orbit_speed = randf_range(0.001, 0.004) * (1.0 if randf() > 0.5 else -1.0)
|
||||||
|
var is_gas_giant := randf() < 0.35
|
||||||
|
radius = randf_range(12.0, 19.0) if is_gas_giant else randf_range(5.0, 10.0)
|
||||||
|
initial_radius = radius
|
||||||
|
|
||||||
|
# Type selection
|
||||||
|
if is_gas_giant:
|
||||||
|
ptype = PType.GAS_GIANT
|
||||||
|
else:
|
||||||
|
var r := randf()
|
||||||
|
if r < 0.35: ptype = PType.TERRESTRIAL
|
||||||
|
elif r < 0.65: ptype = PType.DESERT
|
||||||
|
elif r < 0.80: ptype = PType.ICE
|
||||||
|
elif r < 0.90: ptype = PType.LAVA
|
||||||
|
else: ptype = PType.TOXIC
|
||||||
|
|
||||||
|
_setup_palette()
|
||||||
|
_setup_noise()
|
||||||
|
_setup_animation()
|
||||||
|
_setup_rings(is_gas_giant)
|
||||||
|
_setup_moons(is_gas_giant)
|
||||||
|
|
||||||
|
color = palette[0] # debris / legacy compatibility
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_palette() -> void:
|
||||||
|
match ptype:
|
||||||
|
PType.TERRESTRIAL:
|
||||||
|
palette = [Color("#2a5aa0"), Color("#3a8a4a"), Color("#e8f0ff")]
|
||||||
|
PType.DESERT:
|
||||||
|
palette = [Color("#c87848"), Color("#7a3e25"), Color("#f0d8b0")]
|
||||||
|
PType.GAS_GIANT:
|
||||||
|
# Two palette variants — warm (Jupiter-like) or cool (Neptune-like)
|
||||||
|
if randf() < 0.6:
|
||||||
|
palette = [Color("#d8b078"), Color("#a06040"), Color("#f0d8a0")]
|
||||||
|
else:
|
||||||
|
palette = [Color("#88a8d8"), Color("#506090"), Color("#c8d8f0")]
|
||||||
|
PType.ICE:
|
||||||
|
palette = [Color("#b8d8f0"), Color("#5080a0"), Color("#ffffff")]
|
||||||
|
PType.LAVA:
|
||||||
|
palette = [Color("#2a0a00"), Color("#ff4408"), Color("#ffc040")]
|
||||||
|
PType.TOXIC:
|
||||||
|
palette = [Color("#4a8030"), Color("#d0d040"), Color("#204018")]
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_noise() -> void:
|
||||||
|
surface_noise = FastNoiseLite.new()
|
||||||
|
surface_noise.seed = randi()
|
||||||
|
surface_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
|
||||||
|
match ptype:
|
||||||
|
PType.TERRESTRIAL: surface_noise.frequency = 0.25
|
||||||
|
PType.DESERT: surface_noise.frequency = 0.22
|
||||||
|
PType.GAS_GIANT: surface_noise.frequency = 0.15
|
||||||
|
PType.ICE: surface_noise.frequency = 0.50
|
||||||
|
PType.LAVA: surface_noise.frequency = 0.35
|
||||||
|
PType.TOXIC: surface_noise.frequency = 0.18
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_animation() -> void:
|
||||||
|
spin_angle = randf() * TAU
|
||||||
|
spin_speed = randf_range(0.15, 0.55) * (1.0 if randf() > 0.5 else -1.0)
|
||||||
|
if ptype == PType.TERRESTRIAL and randf() < 0.7:
|
||||||
|
has_clouds = true
|
||||||
|
cloud_noise = FastNoiseLite.new()
|
||||||
|
cloud_noise.seed = randi()
|
||||||
|
cloud_noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
|
||||||
|
cloud_noise.frequency = 0.30
|
||||||
|
cloud_drift = randf() * 100.0
|
||||||
|
cloud_speed = randf_range(0.4, 0.9)
|
||||||
|
if ptype == PType.GAS_GIANT:
|
||||||
|
has_spot = randf() < 0.55
|
||||||
|
if has_spot:
|
||||||
|
spot_longitude = randf() * TAU
|
||||||
|
spot_latitude = randf_range(-radius * 0.4, radius * 0.4)
|
||||||
|
spot_radius = randf_range(2.0, 3.0)
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_rings(is_gas_giant: bool) -> void:
|
||||||
|
var has_ring := (is_gas_giant and randf() < 0.7) or (not is_gas_giant and randf() < 0.15)
|
||||||
|
if not has_ring:
|
||||||
|
ring = false
|
||||||
|
return
|
||||||
|
ring = true
|
||||||
|
ring_count = (randi() % 2) + 1 # 1..2 visible bands
|
||||||
|
ring_inner = radius + randf_range(3.0, 5.0)
|
||||||
|
ring_outer = ring_inner + randf_range(4.0, 8.0)
|
||||||
|
ring_tilt = randf_range(0.15, 0.35)
|
||||||
|
# Slight variation in ring color per band
|
||||||
|
ring_colors.clear()
|
||||||
|
for i in ring_count:
|
||||||
|
var base: Color = palette[0] if randf() < 0.5 else palette[2]
|
||||||
|
var shade: float = randf_range(0.65, 1.0)
|
||||||
|
ring_colors.append(Color(base.r * shade, base.g * shade, base.b * shade))
|
||||||
|
# Random gap bands (visual holes); we use radii, so gaps are "dark" arcs
|
||||||
|
ring_gaps.clear()
|
||||||
|
if randf() < 0.4:
|
||||||
|
ring_gaps.append(randf_range(ring_inner + 1.0, ring_outer - 1.0))
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_moons(is_gas_giant: bool) -> void:
|
||||||
|
var max_moons := 4 if is_gas_giant else 3
|
||||||
|
var moon_count := randi() % (max_moons + 1)
|
||||||
|
for _i in moon_count:
|
||||||
|
var mtype := randi() % 3
|
||||||
|
var mcolor: Color
|
||||||
|
match mtype:
|
||||||
|
0: mcolor = Color(0.75, 0.75, 0.78) # rocky grey
|
||||||
|
1: mcolor = Color(0.85, 0.92, 1.0) # icy
|
||||||
|
2: mcolor = Color(0.85, 0.35, 0.25) # volcanic
|
||||||
|
_: mcolor = Color(0.8, 0.8, 0.8)
|
||||||
|
var msize: float = randf_range(2.0, 6.0) if is_gas_giant else randf_range(2.0, 4.0)
|
||||||
|
moons.append({
|
||||||
|
"angle": randf() * TAU,
|
||||||
|
"dist": radius + randf_range(8.0, 22.0),
|
||||||
|
"speed": randf_range(0.03, 0.08),
|
||||||
|
"size": msize,
|
||||||
|
"color": mcolor,
|
||||||
|
"mtype": mtype,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
func start_capture(bh_x: float, bh_y: float) -> void:
|
||||||
|
captured = true
|
||||||
|
capture_bh_x = bh_x; capture_bh_y = bh_y
|
||||||
|
var dx := x - bh_x; var dy := y - bh_y
|
||||||
|
capture_dist = sqrt(dx * dx + dy * dy)
|
||||||
|
capture_initial_dist = maxf(capture_dist, 1.0)
|
||||||
|
capture_angle = atan2(dy, dx)
|
||||||
|
capture_timer = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func update(delta: float) -> void:
|
||||||
|
# Animation
|
||||||
|
spin_angle += spin_speed * delta
|
||||||
|
cloud_drift += cloud_speed * delta
|
||||||
|
|
||||||
|
if captured:
|
||||||
|
capture_timer += delta
|
||||||
|
var t: float = clampf(capture_timer / CAPTURE_DURATION, 0.0, 1.0)
|
||||||
|
capture_dist = capture_initial_dist * (1.0 - t * t)
|
||||||
|
var angular_speed: float = 4.0 + 10.0 * t * t
|
||||||
|
capture_angle += angular_speed * delta
|
||||||
|
x = capture_bh_x + cos(capture_angle) * capture_dist
|
||||||
|
y = capture_bh_y + sin(capture_angle) * capture_dist
|
||||||
|
radius = maxf(1.0, initial_radius * (1.0 - t * 0.8))
|
||||||
|
if int(capture_timer * 12.5) > int((capture_timer - delta) * 12.5):
|
||||||
|
var da: float = randf() * TAU
|
||||||
|
debris_trail.append({"x": x + cos(da) * radius,
|
||||||
|
"y": y + sin(da) * radius, "life": 0.5})
|
||||||
|
if debris_trail.size() > DEBRIS_MAX: debris_trail.pop_front()
|
||||||
|
for d: Dictionary in debris_trail:
|
||||||
|
d["life"] = float(d["life"]) - delta
|
||||||
|
debris_trail = debris_trail.filter(func(d): return float(d["life"]) > 0.0)
|
||||||
|
if t > 0.6:
|
||||||
|
alpha = maxf(0.0, 1.0 - (t - 0.6) / 0.4)
|
||||||
|
if capture_timer >= CAPTURE_DURATION:
|
||||||
|
dead = true
|
||||||
|
return
|
||||||
|
|
||||||
|
orbit_angle += orbit_speed
|
||||||
|
x = orbit_cx + cos(orbit_angle) * orbit_radius
|
||||||
|
y = orbit_cy + sin(orbit_angle) * orbit_radius * 0.4
|
||||||
|
for m: Dictionary in moons:
|
||||||
|
m["angle"] = float(m["angle"]) + float(m["speed"])
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Drawing ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func draw(canvas: CanvasItem) -> void:
|
||||||
|
# Back half of rings (behind planet)
|
||||||
|
if ring:
|
||||||
|
_draw_ring_half(canvas, true)
|
||||||
|
|
||||||
|
# Lava outer halo (drawn before body, under-glow)
|
||||||
|
if ptype == PType.LAVA:
|
||||||
|
var halo_r := int(ceil(radius + 3.0))
|
||||||
|
var pulse: float = 0.55 + sin(spin_angle * 3.0) * 0.15
|
||||||
|
var hc := Color(palette[1].r, palette[1].g, palette[1].b, alpha * 0.18 * pulse)
|
||||||
|
canvas.draw_rect(Rect2(x - halo_r, y - halo_r, halo_r * 2 + 1, halo_r * 2 + 1), hc)
|
||||||
|
|
||||||
|
# Planet body
|
||||||
|
_draw_body(canvas)
|
||||||
|
|
||||||
|
# Front half of rings (in front of planet)
|
||||||
|
if ring:
|
||||||
|
_draw_ring_half(canvas, false)
|
||||||
|
|
||||||
|
# Moons
|
||||||
|
for m: Dictionary in moons:
|
||||||
|
var mx: float = x + cos(float(m["angle"])) * float(m["dist"])
|
||||||
|
var my: float = y + sin(float(m["angle"])) * float(m["dist"]) * 0.5
|
||||||
|
_draw_moon(canvas, mx, my, m)
|
||||||
|
|
||||||
|
# Debris trail (tidal disruption)
|
||||||
|
for d: Dictionary in debris_trail:
|
||||||
|
var da: float = float(d["life"]) / 0.5
|
||||||
|
canvas.draw_rect(Rect2(float(d["x"]) - 1, float(d["y"]) - 1, 2, 2),
|
||||||
|
Color(palette[0].r, palette[0].g, palette[0].b, alpha * da * 0.7))
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_body(canvas: CanvasItem) -> void:
|
||||||
|
var ir := int(ceil(radius))
|
||||||
|
var r2 := radius * radius
|
||||||
|
for py in range(-ir, ir + 1):
|
||||||
|
for px in range(-ir, ir + 1):
|
||||||
|
if px * px + py * py > r2:
|
||||||
|
continue
|
||||||
|
var idx := _sample_pattern(px, py)
|
||||||
|
var c: Color = palette[idx]
|
||||||
|
# Spherical shading — light from upper-left
|
||||||
|
var nx := float(px) / radius
|
||||||
|
var ny := float(py) / radius
|
||||||
|
var light: float = clampf(0.65 - nx * 0.35 + ny * 0.35, 0.35, 1.0)
|
||||||
|
var out := Color(c.r * light, c.g * light, c.b * light, alpha)
|
||||||
|
canvas.draw_rect(Rect2(x + px, y + py, 1, 1), out)
|
||||||
|
|
||||||
|
# Cloud layer (terrestrial only)
|
||||||
|
if has_clouds and ptype == PType.TERRESTRIAL:
|
||||||
|
var cv: float = cloud_noise.get_noise_2d(
|
||||||
|
float(px) + cloud_drift + spin_angle * radius * 0.6,
|
||||||
|
float(py) * 1.2)
|
||||||
|
if cv > 0.35:
|
||||||
|
var cloud_a: float = alpha * 0.55 * clampf((cv - 0.35) / 0.25, 0.0, 1.0) * light
|
||||||
|
canvas.draw_rect(Rect2(x + px, y + py, 1, 1),
|
||||||
|
Color(1.0, 1.0, 1.0, cloud_a))
|
||||||
|
|
||||||
|
# Lava glow — small emissive bloom near glow pixels
|
||||||
|
if ptype == PType.LAVA and idx == 2:
|
||||||
|
var gc := Color(palette[2].r, palette[2].g, palette[2].b, alpha * 0.35)
|
||||||
|
canvas.draw_rect(Rect2(x + px - 1, y + py, 1, 1), gc)
|
||||||
|
canvas.draw_rect(Rect2(x + px + 1, y + py, 1, 1), gc)
|
||||||
|
canvas.draw_rect(Rect2(x + px, y + py - 1, 1, 1), gc)
|
||||||
|
canvas.draw_rect(Rect2(x + px, y + py + 1, 1, 1), gc)
|
||||||
|
|
||||||
|
|
||||||
|
func _sample_pattern(px: int, py: int) -> int:
|
||||||
|
var fpx: float = float(px)
|
||||||
|
var fpy: float = float(py)
|
||||||
|
var shifted_x: float = fpx + spin_angle * radius * 0.6
|
||||||
|
match ptype:
|
||||||
|
PType.TERRESTRIAL:
|
||||||
|
# Polar caps
|
||||||
|
if absf(fpy) / radius > 0.78:
|
||||||
|
return 2
|
||||||
|
var v: float = surface_noise.get_noise_2d(shifted_x, fpy * 1.1)
|
||||||
|
return 1 if v > 0.05 else 0
|
||||||
|
PType.DESERT:
|
||||||
|
if fpy / radius < -0.82:
|
||||||
|
return 2
|
||||||
|
var v2: float = surface_noise.get_noise_2d(shifted_x, fpy * 1.1)
|
||||||
|
return 1 if v2 > 0.15 else 0
|
||||||
|
PType.GAS_GIANT:
|
||||||
|
# Distorted banding; spot overrides band
|
||||||
|
if has_spot:
|
||||||
|
# Spot position rotates with spin
|
||||||
|
var spot_x: float = cos(spot_longitude + spin_angle) * radius * 0.6
|
||||||
|
# Only visible on front half (cos > 0)
|
||||||
|
var front: float = cos(spot_longitude + spin_angle)
|
||||||
|
if front > 0.0:
|
||||||
|
var dx: float = fpx - spot_x
|
||||||
|
var dy: float = fpy - spot_latitude
|
||||||
|
if dx * dx + dy * dy < spot_radius * spot_radius:
|
||||||
|
return 2
|
||||||
|
var warp: float = surface_noise.get_noise_2d(shifted_x * 0.5, fpy * 4.0) * 2.0
|
||||||
|
var band: int = int(floor((fpy + warp + radius) / 2.5))
|
||||||
|
var m: int = band % 3
|
||||||
|
if m < 0: m += 3
|
||||||
|
return m
|
||||||
|
PType.ICE:
|
||||||
|
var v3: float = surface_noise.get_noise_2d(shifted_x, fpy * 1.1)
|
||||||
|
if v3 > 0.55:
|
||||||
|
return 2
|
||||||
|
if absf(v3) < 0.08:
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
PType.LAVA:
|
||||||
|
var shift: float = sin(spin_angle * 3.0) * 0.03
|
||||||
|
var v4: float = surface_noise.get_noise_2d(shifted_x, fpy * 1.1)
|
||||||
|
if v4 > 0.45 + shift: return 2
|
||||||
|
if v4 > 0.15 + shift: return 1
|
||||||
|
return 0
|
||||||
|
PType.TOXIC:
|
||||||
|
var v5: float = surface_noise.get_noise_2d(shifted_x, fpy * 1.1)
|
||||||
|
var v6: float = surface_noise.get_noise_2d(shifted_x + v5 * 3.0, fpy * 1.1 + v5 * 3.0)
|
||||||
|
if v6 > 0.3: return 2
|
||||||
|
if v6 > 0.0: return 1
|
||||||
|
return 0
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_ring_half(canvas: CanvasItem, back: bool) -> void:
|
||||||
|
# Build band radii (each integer radius = one pixel band). Split by ring_count groups.
|
||||||
|
var band_width: float = (ring_outer - ring_inner) / float(maxi(ring_count, 1) * 2 + 1)
|
||||||
|
var a_start: float = PI
|
||||||
|
var a_end: float = TAU
|
||||||
|
if not back:
|
||||||
|
a_start = 0.0
|
||||||
|
a_end = PI
|
||||||
|
# For each band
|
||||||
|
for i in ring_count:
|
||||||
|
var inner: float = ring_inner + float(i * 2) * band_width
|
||||||
|
var outer: float = inner + band_width
|
||||||
|
var col: Color = ring_colors[i]
|
||||||
|
var rc := Color(col.r, col.g, col.b, alpha * 0.55)
|
||||||
|
# Sample arc points
|
||||||
|
var steps: int = int(clampf(outer * 1.6, 20.0, 64.0))
|
||||||
|
var prev: Vector2 = Vector2.ZERO
|
||||||
|
var have_prev: bool = false
|
||||||
|
for s in range(steps + 1):
|
||||||
|
var a: float = a_start + (a_end - a_start) * float(s) / float(steps)
|
||||||
|
# For each radius in [inner, outer]
|
||||||
|
var rad: float = (inner + outer) * 0.5
|
||||||
|
if _ring_gap_hit(rad):
|
||||||
|
have_prev = false
|
||||||
|
continue
|
||||||
|
var px: float = x + cos(a) * rad
|
||||||
|
var py: float = y + sin(a) * rad * ring_tilt
|
||||||
|
if have_prev:
|
||||||
|
canvas.draw_line(prev, Vector2(px, py), rc, maxf(1.0, band_width))
|
||||||
|
prev = Vector2(px, py)
|
||||||
|
have_prev = true
|
||||||
|
|
||||||
|
|
||||||
|
func _ring_gap_hit(rad: float) -> bool:
|
||||||
|
for g in ring_gaps:
|
||||||
|
if absf(rad - float(g)) < 0.6:
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_moon(canvas: CanvasItem, mx: float, my: float, m: Dictionary) -> void:
|
||||||
|
var mc: Color = m["color"]
|
||||||
|
var ms: float = float(m["size"])
|
||||||
|
var a_out := Color(mc.r, mc.g, mc.b, alpha)
|
||||||
|
# Solid body
|
||||||
|
canvas.draw_rect(Rect2(mx - ms * 0.5, my - ms * 0.5, ms, ms), a_out)
|
||||||
|
# Type-specific detail
|
||||||
|
var mtype: int = int(m["mtype"])
|
||||||
|
match mtype:
|
||||||
|
0:
|
||||||
|
# Rocky — darker crater pixel
|
||||||
|
var dark := Color(mc.r * 0.55, mc.g * 0.55, mc.b * 0.55, alpha)
|
||||||
|
canvas.draw_rect(Rect2(mx - ms * 0.5 + 1, my - ms * 0.5, 1, 1), dark)
|
||||||
|
if ms >= 4.0:
|
||||||
|
canvas.draw_rect(Rect2(mx + ms * 0.5 - 2, my + ms * 0.5 - 2, 1, 1), dark)
|
||||||
|
1:
|
||||||
|
# Icy — bright highlight
|
||||||
|
var hi := Color(1.0, 1.0, 1.0, alpha)
|
||||||
|
canvas.draw_rect(Rect2(mx - ms * 0.5, my - ms * 0.5, 1, 1), hi)
|
||||||
|
2:
|
||||||
|
# Volcanic — red/black speckle
|
||||||
|
var hot := Color(1.0, 0.6, 0.1, alpha)
|
||||||
|
var black := Color(0.1, 0.05, 0.05, alpha)
|
||||||
|
canvas.draw_rect(Rect2(mx - ms * 0.5 + 1, my - ms * 0.5 + 1, 1, 1), hot)
|
||||||
|
canvas.draw_rect(Rect2(mx + ms * 0.5 - 1, my - ms * 0.5, 1, 1), black)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b78slt2taoxyc
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
var master_volume: float = 1.0
|
||||||
|
var sfx_muted: bool = false
|
||||||
|
var music_volume: float = 0.8
|
||||||
|
var fullscreen: bool = false
|
||||||
|
var nebula_enabled: bool = true
|
||||||
|
var star_density: int = 1 # 0=low 1=mid 2=high
|
||||||
|
var language: String = "de" # "de" or "en"
|
||||||
|
var touch_mode: int = 0 # 0=auto, 1=always on, 2=always off
|
||||||
|
|
||||||
|
# Runtime-only — not saved. Set by input detection in main.gd / touch_controls.gd.
|
||||||
|
var last_input_device: String = "keyboard" # "keyboard" | "pad" | "touch"
|
||||||
|
|
||||||
|
# Rebindable gameplay actions (P1 only).
|
||||||
|
const REBIND_ACTIONS: Array[String] = ["p1_thrust", "p1_left", "p1_right", "p1_shoot", "p1_wipe"]
|
||||||
|
|
||||||
|
# Stores physical keycodes (int) for rebound keys. Empty = use project defaults.
|
||||||
|
var key_bindings: Dictionary = {}
|
||||||
|
|
||||||
|
const PATH := "user://settings.cfg"
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
load_settings()
|
||||||
|
apply_all()
|
||||||
|
|
||||||
|
func apply_all() -> void:
|
||||||
|
apply_volume()
|
||||||
|
apply_key_bindings()
|
||||||
|
# Mobile always runs fullscreen — ignore the saved setting.
|
||||||
|
if OS.has_feature("android") or OS.has_feature("ios"):
|
||||||
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
|
||||||
|
elif fullscreen:
|
||||||
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
|
||||||
|
else:
|
||||||
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
||||||
|
|
||||||
|
func apply_volume() -> void:
|
||||||
|
var db := linear_to_db(master_volume) if not sfx_muted else -80.0
|
||||||
|
AudioServer.set_bus_volume_db(0, db)
|
||||||
|
|
||||||
|
func apply_music_volume() -> void:
|
||||||
|
var bus_idx := AudioServer.get_bus_index("Music")
|
||||||
|
if bus_idx != -1:
|
||||||
|
AudioServer.set_bus_volume_db(bus_idx,
|
||||||
|
linear_to_db(music_volume) if music_volume > 0.0 else -80.0)
|
||||||
|
|
||||||
|
func apply_key_bindings() -> void:
|
||||||
|
for action: String in key_bindings:
|
||||||
|
var keycode: int = key_bindings[action]
|
||||||
|
if keycode > 0 and InputMap.has_action(action):
|
||||||
|
# Remove existing keyboard events for this action
|
||||||
|
var events := InputMap.action_get_events(action)
|
||||||
|
for ev in events:
|
||||||
|
if ev is InputEventKey:
|
||||||
|
InputMap.action_erase_event(action, ev)
|
||||||
|
# Add the custom key
|
||||||
|
var new_ev := InputEventKey.new()
|
||||||
|
new_ev.physical_keycode = keycode as Key
|
||||||
|
InputMap.action_add_event(action, new_ev)
|
||||||
|
|
||||||
|
func reset_key_bindings() -> void:
|
||||||
|
InputMap.load_from_project_settings()
|
||||||
|
key_bindings.clear()
|
||||||
|
save_settings()
|
||||||
|
|
||||||
|
func save_settings() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
cfg.set_value("sound", "volume", master_volume)
|
||||||
|
cfg.set_value("sound", "muted", sfx_muted)
|
||||||
|
cfg.set_value("sound", "music", music_volume)
|
||||||
|
cfg.set_value("graphics", "fullscreen", fullscreen)
|
||||||
|
cfg.set_value("graphics", "nebula", nebula_enabled)
|
||||||
|
cfg.set_value("graphics", "stars", star_density)
|
||||||
|
cfg.set_value("game", "language", language)
|
||||||
|
cfg.set_value("game", "touch_mode", touch_mode)
|
||||||
|
for action: String in REBIND_ACTIONS:
|
||||||
|
if key_bindings.has(action):
|
||||||
|
cfg.set_value("bindings", action, key_bindings[action])
|
||||||
|
cfg.save(PATH)
|
||||||
|
|
||||||
|
func load_settings() -> void:
|
||||||
|
var cfg := ConfigFile.new()
|
||||||
|
if cfg.load(PATH) != OK:
|
||||||
|
return
|
||||||
|
master_volume = cfg.get_value("sound", "volume", 1.0)
|
||||||
|
sfx_muted = cfg.get_value("sound", "muted", false)
|
||||||
|
music_volume = cfg.get_value("sound", "music", 0.8)
|
||||||
|
fullscreen = cfg.get_value("graphics", "fullscreen", false)
|
||||||
|
nebula_enabled = cfg.get_value("graphics", "nebula", true)
|
||||||
|
star_density = cfg.get_value("graphics", "stars", 1)
|
||||||
|
language = cfg.get_value("game", "language", "de")
|
||||||
|
touch_mode = cfg.get_value("game", "touch_mode", 0)
|
||||||
|
key_bindings.clear()
|
||||||
|
for action: String in REBIND_ACTIONS:
|
||||||
|
if cfg.has_section_key("bindings", action):
|
||||||
|
key_bindings[action] = cfg.get_value("bindings", action)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c1yb21fhib2c1
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
var W: float = 960.0
|
||||||
|
var H: float = 600.0
|
||||||
|
|
||||||
|
# Cockpit palette
|
||||||
|
const COL_BG := Color(0.0, 0.04, 0.02, 0.90)
|
||||||
|
const COL_PRIMARY := Color(0.0, 1.0, 0.533, 1.0) # phosphor green
|
||||||
|
const COL_ACCENT := Color(0.27, 1.0, 0.8, 1.0)
|
||||||
|
const COL_DIM := Color(0.0, 0.27, 0.13, 0.6) # lines / brackets only
|
||||||
|
const COL_TEXT := Color(0.3, 0.68, 0.46, 0.88) # readable secondary text
|
||||||
|
const COL_P2 := Color(0.27, 1.0, 0.67, 1.0)
|
||||||
|
const COL_OVERLAY := Color(0.0, 0.0, 0.04, 0.78)
|
||||||
|
|
||||||
|
var is_p2_mode: bool = false
|
||||||
|
var selected: int = 0
|
||||||
|
var ships: Array = []
|
||||||
|
var blink_phase: float = 0.0
|
||||||
|
var enter_blink: bool = true
|
||||||
|
var enter_timer: float = 0.0
|
||||||
|
|
||||||
|
func start_select(p2_mode: bool, start_sel: int, ship_data: Array) -> void:
|
||||||
|
is_p2_mode = p2_mode
|
||||||
|
selected = start_sel
|
||||||
|
ships = ship_data
|
||||||
|
visible = true
|
||||||
|
blink_phase = 0.0
|
||||||
|
|
||||||
|
func set_selection(idx: int) -> void:
|
||||||
|
selected = idx
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
blink_phase += delta
|
||||||
|
enter_timer += delta
|
||||||
|
if enter_timer >= 0.5:
|
||||||
|
enter_timer = 0.0
|
||||||
|
enter_blink = not enter_blink
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var vs: Vector2 = get_viewport_rect().size
|
||||||
|
W = vs.x; H = vs.y
|
||||||
|
# Background overlay so live simulation shows through
|
||||||
|
draw_rect(get_viewport_rect(), COL_OVERLAY)
|
||||||
|
|
||||||
|
# ── Header bar ───────────────────────────────────────────────────────────
|
||||||
|
draw_rect(Rect2(0, 0, W, 52.0), Color(0.0, 0.04, 0.02, 0.92))
|
||||||
|
draw_line(Vector2(0, 52), Vector2(W, 52), COL_DIM, 1.0)
|
||||||
|
|
||||||
|
var header_col := COL_PRIMARY if not is_p2_mode else COL_P2
|
||||||
|
_draw_text_centered(Tr.t("select_header"), W * 0.5, 14.0, 10, COL_TEXT)
|
||||||
|
var sub := Tr.t("select_pilot1") if not is_p2_mode else Tr.t("select_pilot2")
|
||||||
|
_draw_text_centered(sub, W * 0.5, 30.0, 13, header_col)
|
||||||
|
|
||||||
|
# Outer corner brackets
|
||||||
|
_draw_corner_brackets(8.0, 8.0, W - 8.0, H - 8.0, COL_DIM, 16.0)
|
||||||
|
|
||||||
|
# ── Ship boxes ───────────────────────────────────────────────────────────
|
||||||
|
# 2×2 Grid Layout (supports 4 ships, falls back for 3)
|
||||||
|
var n := ships.size()
|
||||||
|
var cols: int = 2 if n >= 4 else min(n, 3)
|
||||||
|
var box_w := 220.0; var box_h := 180.0
|
||||||
|
if cols == 3:
|
||||||
|
box_w = 210.0; box_h = 220.0
|
||||||
|
var row_h: float = box_h + 16.0
|
||||||
|
var col_space: float = (W - box_w * cols) / float(cols + 1)
|
||||||
|
var grid_start_y: float = 72.0
|
||||||
|
for i in n:
|
||||||
|
var col_i: int = i % cols
|
||||||
|
var row_i: int = int(i / cols)
|
||||||
|
var bx: float = col_space + col_i * (box_w + col_space)
|
||||||
|
var by: float = grid_start_y + row_i * row_h
|
||||||
|
var is_sel := i == selected
|
||||||
|
var pulse := 0.5 + 0.5 * sin(blink_phase * 3.0)
|
||||||
|
|
||||||
|
# Box background
|
||||||
|
draw_rect(Rect2(bx, by, box_w, box_h), Color(0.0, 0.04, 0.02, 0.72))
|
||||||
|
|
||||||
|
# Corner L-brackets instead of full border
|
||||||
|
var bracket_col: Color
|
||||||
|
if is_sel:
|
||||||
|
bracket_col = Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.6 + 0.4 * pulse)
|
||||||
|
else:
|
||||||
|
bracket_col = COL_DIM
|
||||||
|
_draw_corner_brackets(bx, by, bx + box_w, by + box_h, bracket_col, 16.0)
|
||||||
|
|
||||||
|
# Ship preview
|
||||||
|
var cx := bx + box_w * 0.5
|
||||||
|
var cy := by + box_h * 0.42
|
||||||
|
var preview_heading := blink_phase * 0.6 if is_sel else -PI * 0.5
|
||||||
|
_draw_ship_preview(cx, cy, preview_heading, ships[i])
|
||||||
|
|
||||||
|
# Ship name
|
||||||
|
var name_col := COL_ACCENT if is_sel else COL_TEXT
|
||||||
|
_draw_text_centered(ships[i]["name"], cx, by + box_h - 42.0, 12, name_col)
|
||||||
|
|
||||||
|
# Ship identifier or boost hint
|
||||||
|
var sub_str: String
|
||||||
|
match ships[i]["id"]:
|
||||||
|
"classic": sub_str = "1 SHIELD · BALANCED"
|
||||||
|
"inferno": sub_str = "SPEED+ · RAM DAMAGE"
|
||||||
|
"aurora": sub_str = "2 SHIELDS · BH RESIST"
|
||||||
|
"titan": sub_str = "[SHIFT] BOOST"
|
||||||
|
_: sub_str = ships[i]["id"].to_upper()
|
||||||
|
_draw_text_centered(sub_str, cx, by + box_h - 24.0, 8,
|
||||||
|
Color(COL_TEXT.r, COL_TEXT.g, COL_TEXT.b, 0.85))
|
||||||
|
|
||||||
|
# ── Footer ───────────────────────────────────────────────────────────────
|
||||||
|
draw_rect(Rect2(0, H - 48.0, W, 48.0), Color(0.0, 0.04, 0.02, 0.92))
|
||||||
|
draw_line(Vector2(0, H - 48.0), Vector2(W, H - 48.0), COL_DIM, 1.0)
|
||||||
|
|
||||||
|
_draw_text_centered(Tr.hint("select_choose"), W * 0.5, H - 44.0, 9, COL_TEXT)
|
||||||
|
|
||||||
|
if not is_p2_mode:
|
||||||
|
if enter_blink:
|
||||||
|
_draw_text_centered(Tr.hint("select_confirm"), W * 0.5, H - 26.0, 11, COL_PRIMARY)
|
||||||
|
else:
|
||||||
|
_draw_text_centered(Tr.hint("select_join"), W * 0.3, H - 26.0, 10,
|
||||||
|
COL_P2 if enter_blink else COL_DIM)
|
||||||
|
_draw_text_centered(Tr.hint("select_solo"), W * 0.7, H - 26.0, 10,
|
||||||
|
COL_PRIMARY if enter_blink else COL_DIM)
|
||||||
|
|
||||||
|
# ── Drawing helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func _draw_corner_brackets(x1: float, y1: float, x2: float, y2: float,
|
||||||
|
col: Color, arm: float) -> void:
|
||||||
|
# Top-left
|
||||||
|
draw_line(Vector2(x1, y1), Vector2(x1 + arm, y1), col, 1.5)
|
||||||
|
draw_line(Vector2(x1, y1), Vector2(x1, y1 + arm), col, 1.5)
|
||||||
|
# Top-right
|
||||||
|
draw_line(Vector2(x2, y1), Vector2(x2 - arm, y1), col, 1.5)
|
||||||
|
draw_line(Vector2(x2, y1), Vector2(x2, y1 + arm), col, 1.5)
|
||||||
|
# Bottom-left
|
||||||
|
draw_line(Vector2(x1, y2), Vector2(x1 + arm, y2), col, 1.5)
|
||||||
|
draw_line(Vector2(x1, y2), Vector2(x1, y2 - arm), col, 1.5)
|
||||||
|
# Bottom-right
|
||||||
|
draw_line(Vector2(x2, y2), Vector2(x2 - arm, y2), col, 1.5)
|
||||||
|
draw_line(Vector2(x2, y2), Vector2(x2, y2 - arm), col, 1.5)
|
||||||
|
|
||||||
|
func _draw_ship_preview(cx: float, cy: float, heading: float, ship: Dictionary) -> void:
|
||||||
|
var cos_h := cos(heading); var sin_h := sin(heading)
|
||||||
|
var preview_scale := 5.5
|
||||||
|
var hull_pixels := [
|
||||||
|
[3, 0, "nose"], [2, -1, "mid"], [2, 1, "mid"],
|
||||||
|
[1, 0, "dim"], [0, -2, "accent"],[0, 2, "accent"],
|
||||||
|
[0, 0, "bright"],[-1,-1, "dim"], [-1, 1, "dim"],
|
||||||
|
[-2,-2, "shadow"],[-2, 2, "shadow"],[-2, 0, "edge"],
|
||||||
|
]
|
||||||
|
for px_data in hull_pixels:
|
||||||
|
var lx: float = px_data[0] * preview_scale
|
||||||
|
var ly: float = px_data[1] * preview_scale
|
||||||
|
var col: Color = ship.get(px_data[2], Color.WHITE)
|
||||||
|
var rx := cx + lx * cos_h - ly * sin_h
|
||||||
|
var ry := cy + lx * sin_h + ly * cos_h
|
||||||
|
draw_rect(Rect2(rx - 2, ry - 2, 5, 5), col)
|
||||||
|
|
||||||
|
func _draw_text_centered(text: String, x: float, y: float, font_size: int, col: Color) -> void:
|
||||||
|
var font := ThemeDB.fallback_font
|
||||||
|
var tw := font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size).x
|
||||||
|
draw_string(font, Vector2(x - tw * 0.5, y + font_size), text,
|
||||||
|
HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, col)
|
||||||
|
|
||||||
|
func _draw_text(text: String, x: float, y: float, font_size: int, col: Color) -> void:
|
||||||
|
var font := ThemeDB.fallback_font
|
||||||
|
draw_string(font, Vector2(x, y + font_size), text,
|
||||||
|
HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, col)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bc1kys1vqyll6
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
extends RefCounted
|
||||||
|
class_name ShipStats
|
||||||
|
|
||||||
|
# All multiplicative / additive upgrades for one player's run.
|
||||||
|
# Stats stack: each item applies via apply_item_def(def) oder apply_item(effects).
|
||||||
|
|
||||||
|
var ship_id: String = "" # set at game start; used for per-ship runtime mechanics
|
||||||
|
var speed_mult: float = 1.0 # scales THRUST and MAX_SPEED
|
||||||
|
var turn_mult: float = 1.0 # scales TURN_SPEED
|
||||||
|
var fire_rate_mult: float = 1.0 # divides BULLET_COOLDOWN_MAX (higher = faster fire)
|
||||||
|
var damage_mult: float = 1.0 # scales bullet hit radius; >= 2.0 → pierce
|
||||||
|
var bullet_speed_mult: float = 1.0 # scales bullet travel speed
|
||||||
|
var bullet_count: int = 1 # shots per trigger press
|
||||||
|
var shield_charges: int = 0 # absorbs N hits before death
|
||||||
|
var invuln_mult: float = 1.0 # scales INVULN_FRAMES after hit
|
||||||
|
var bh_resist: float = 0.0 # 0.0–0.9: fraction of BH pull negated
|
||||||
|
var wipe_mult: float = 1.0 # scales BigWipe hold-time requirement
|
||||||
|
var credit_bonus: float = 1.0 # multiplier on all credits earned
|
||||||
|
|
||||||
|
# ─── Werkstatt-spezifische Felder ─────────────────────────────────────────────
|
||||||
|
var hull_scale: float = 3.0 # px per hull pixel (Basis 3.0, Max 4.5)
|
||||||
|
var owned_item_ids: Array = [] # IDs für visuelle Attachments am Schiff
|
||||||
|
var has_boost: bool = false # ist Boost-Taste aktiviert? (TITAN)
|
||||||
|
var boost_cooldown_max: float = 5.0 # Sekunden zwischen Boosts
|
||||||
|
|
||||||
|
# Wendet einen rohen effects-Dict an (Legacy-Shop-Path).
|
||||||
|
func apply_item(effects: Dictionary) -> void:
|
||||||
|
for key: String in effects:
|
||||||
|
var val = effects[key]
|
||||||
|
match key:
|
||||||
|
"speed_mult": speed_mult *= float(val)
|
||||||
|
"turn_mult": turn_mult *= float(val)
|
||||||
|
"fire_rate_mult": fire_rate_mult *= float(val)
|
||||||
|
"damage_mult": damage_mult *= float(val)
|
||||||
|
"bullet_speed_mult": bullet_speed_mult *= float(val)
|
||||||
|
"bullet_count": bullet_count += int(val)
|
||||||
|
"shield_charges": shield_charges = max(0, shield_charges + int(val))
|
||||||
|
"invuln_mult": invuln_mult *= float(val)
|
||||||
|
"bh_resist": bh_resist = min(0.9, bh_resist + float(val))
|
||||||
|
"wipe_mult": wipe_mult *= float(val)
|
||||||
|
"credit_bonus": credit_bonus *= float(val)
|
||||||
|
# Clamp bullet_count auf minimum 1
|
||||||
|
bullet_count = max(1, bullet_count)
|
||||||
|
|
||||||
|
# Wendet einen ItemDef an (neuer Werkstatt-Path).
|
||||||
|
func apply_item_def(def: ItemDef) -> void:
|
||||||
|
if def == null: return
|
||||||
|
apply_item(def.effects)
|
||||||
|
hull_scale = min(4.5, hull_scale + def.hull_size_bonus)
|
||||||
|
owned_item_ids.append(def.id)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c1b87vp4lr17r
|
||||||
@@ -0,0 +1,605 @@
|
|||||||
|
extends Node2D
|
||||||
|
|
||||||
|
# ─── Werkstatt UI ─────────────────────────────────────────────────────────────
|
||||||
|
# Zwei Phasen:
|
||||||
|
# PHASE_ATTR — 3 kostenlose Attribut-Upgrades (Pflicht, eins auswählen)
|
||||||
|
# PHASE_SHOP — Werkstatt: beliebig viele Items kaufen, SPACE beendet
|
||||||
|
#
|
||||||
|
# Items kommen aus ItemDB.roll_werkstatt() (Plugin-System unter res://items/).
|
||||||
|
# Gekaufte Items werden via stats.apply_item_def(def) angewendet und erscheinen
|
||||||
|
# visuell am Schiff-Preview im Zentrum.
|
||||||
|
# Emits: shop_closed(remaining_credits, updated_stats, owned_item_ids)
|
||||||
|
|
||||||
|
signal shop_closed(remaining_credits: int, new_stats: ShipStats, items: Array, reroll_count: int)
|
||||||
|
|
||||||
|
# ─── Konstanten ───────────────────────────────────────────────────────────────
|
||||||
|
const REROLL_BASE_COST := 60
|
||||||
|
const REROLL_STEP_COST := 42
|
||||||
|
const SHOP_CARD_COUNT := 4
|
||||||
|
const ATTR_CARD_COUNT := 3
|
||||||
|
|
||||||
|
# Cockpit palette
|
||||||
|
const COL_BG := Color(0.0, 0.0, 0.04, 0.92)
|
||||||
|
const COL_PRIMARY := Color(0.0, 1.0, 0.533, 1.0)
|
||||||
|
const COL_ACCENT := Color(0.27, 1.0, 0.8, 1.0)
|
||||||
|
const COL_DIM := Color(0.0, 0.27, 0.13, 0.55)
|
||||||
|
const COL_WHITE := Color(1.0, 1.0, 1.0, 0.90)
|
||||||
|
const COL_WARN := Color(1.0, 0.67, 0.0, 0.90)
|
||||||
|
const COL_POS := Color(0.3, 1.0, 0.4, 1.0)
|
||||||
|
const COL_NEG := Color(1.0, 0.35, 0.35, 1.0)
|
||||||
|
|
||||||
|
# ─── Attribut-Pool (Phase 1 — gratis, reine Buffs) ────────────────────────────
|
||||||
|
# Jedes Attribut ist ein Dictionary wie ein ItemDef-Effect.
|
||||||
|
const ATTR_POOL: Array = [
|
||||||
|
{ "id": "a_speed", "name": "Schubverstärker", "name_en": "Thruster", "desc": "+8% Geschwindigkeit", "desc_en": "+8% Speed", "effects": { "speed_mult": 1.08 } },
|
||||||
|
{ "id": "a_turn", "name": "Agilität", "name_en": "Agility", "desc": "+12% Wendigkeit", "desc_en": "+12% Handling", "effects": { "turn_mult": 1.12 } },
|
||||||
|
{ "id": "a_fire", "name": "Schnellfeuer", "name_en": "Rapid Fire", "desc": "+12% Feuerrate", "desc_en": "+12% Fire Rate", "effects": { "fire_rate_mult": 1.12 } },
|
||||||
|
{ "id": "a_damage", "name": "Schwere Ladung", "name_en": "Heavy Load", "desc": "+12% Schaden", "desc_en": "+12% Damage", "effects": { "damage_mult": 1.12 } },
|
||||||
|
{ "id": "a_proj", "name": "Lineare Ballistik","name_en": "Linear Ballistics", "desc": "+12% Projektilgeschw.", "desc_en": "+12% Projectile Speed", "effects": { "bullet_speed_mult": 1.12 } },
|
||||||
|
{ "id": "a_shield", "name": "Panzerplatte", "name_en": "Armor Plate", "desc": "+1 Schutzladung", "desc_en": "+1 Shield Charge", "effects": { "shield_charges": 1 } },
|
||||||
|
{ "id": "a_invuln", "name": "Schildgenerator", "name_en": "Shield Generator", "desc": "+18% Unverwundbarkeit", "desc_en": "+18% Invulnerability", "effects": { "invuln_mult": 1.18 } },
|
||||||
|
{ "id": "a_credits", "name": "Profitoptimierer", "name_en": "Profit Optimizer", "desc": "+8% Kreditgewinn", "desc_en": "+8% Credit Gain", "effects": { "credit_bonus": 1.08 } },
|
||||||
|
]
|
||||||
|
|
||||||
|
# ─── Phasen-State ─────────────────────────────────────────────────────────────
|
||||||
|
enum Phase { ATTR, SHOP }
|
||||||
|
var _phase: int = Phase.ATTR
|
||||||
|
|
||||||
|
# ─── Session-State ────────────────────────────────────────────────────────────
|
||||||
|
var _lives: int = 3
|
||||||
|
var _credits: int = 0
|
||||||
|
var _stats: ShipStats = null
|
||||||
|
var _owned_ids: Array = []
|
||||||
|
var _player_label: String = ""
|
||||||
|
var _ship_palette: Dictionary = {}
|
||||||
|
var _wave: int = 1
|
||||||
|
var _player_idx: int = 1 # 1 = P1 (Pfeiltasten/Enter/Space), 2 = P2 (ADWEF/Q)
|
||||||
|
|
||||||
|
# Phase 1
|
||||||
|
var _attr_choices: Array = [] # 3 zufällige Attribute
|
||||||
|
var _attr_cursor: int = 0
|
||||||
|
|
||||||
|
# Phase 2
|
||||||
|
var _shop_cards: Array = [] # aktuelle 4 ItemDef-Objekte
|
||||||
|
var _shop_cursor: int = 0
|
||||||
|
var _reroll_count: int = 0
|
||||||
|
|
||||||
|
# UI
|
||||||
|
var _blink: float = 0.0
|
||||||
|
var W: float = 960.0
|
||||||
|
var H: float = 600.0
|
||||||
|
|
||||||
|
# Touch state (direct InputEventScreenTouch handling)
|
||||||
|
var _tc_id: int = -1
|
||||||
|
var _tc_start: Vector2 = Vector2.ZERO
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
func open(lives: int, credits: int, stats: ShipStats, owned: Array,
|
||||||
|
player_label: String = "", ship_palette: Dictionary = {},
|
||||||
|
wave: int = 1, reroll_count: int = 0, player_idx: int = 1) -> void:
|
||||||
|
_lives = lives
|
||||||
|
_credits = credits
|
||||||
|
_stats = stats if stats != null else ShipStats.new()
|
||||||
|
_owned_ids = owned.duplicate()
|
||||||
|
_player_label = player_label
|
||||||
|
_ship_palette = ship_palette
|
||||||
|
_wave = wave
|
||||||
|
# Attribut-Phase nur in ungeraden Wellen (1, 3, 5, ...). Gerade Wellen
|
||||||
|
# starten direkt im Shop — langsamere Power-Kurve.
|
||||||
|
_phase = Phase.ATTR if (wave % 2) == 1 else Phase.SHOP
|
||||||
|
_attr_cursor = 0
|
||||||
|
_shop_cursor = 0
|
||||||
|
# Reroll-Counter persistiert über Wellen hinweg (von main.gd durchgereicht).
|
||||||
|
_reroll_count = reroll_count
|
||||||
|
_player_idx = player_idx
|
||||||
|
_blink = 0.0
|
||||||
|
if _phase == Phase.ATTR:
|
||||||
|
_roll_attr()
|
||||||
|
_roll_shop()
|
||||||
|
visible = true
|
||||||
|
|
||||||
|
func _roll_attr() -> void:
|
||||||
|
var pool := ATTR_POOL.duplicate()
|
||||||
|
pool.shuffle()
|
||||||
|
_attr_choices = pool.slice(0, ATTR_CARD_COUNT)
|
||||||
|
|
||||||
|
func _roll_shop() -> void:
|
||||||
|
# Exclude already-owned item IDs so you don't see the same thing twice in one roll
|
||||||
|
_shop_cards = ItemDB.roll_werkstatt(SHOP_CARD_COUNT, _owned_ids)
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if not visible: return
|
||||||
|
_blink += delta
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
# ─── Input ────────────────────────────────────────────────────────────────────
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if not visible: return
|
||||||
|
if not event is InputEventScreenTouch: return
|
||||||
|
if event.pressed:
|
||||||
|
if _tc_id == -1:
|
||||||
|
_tc_id = event.index
|
||||||
|
_tc_start = event.position
|
||||||
|
elif event.index == _tc_id:
|
||||||
|
_tc_id = -1
|
||||||
|
var d: Vector2 = event.position - _tc_start
|
||||||
|
if d.length() < 32.0:
|
||||||
|
_touch_tap(event.position)
|
||||||
|
elif abs(d.x) >= abs(d.y):
|
||||||
|
_touch_nav("ui_left" if d.x < 0 else "ui_right")
|
||||||
|
else:
|
||||||
|
_touch_nav("ui_up" if d.y < 0 else "ui_down")
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
func _touch_tap(p: Vector2) -> void:
|
||||||
|
if _phase == Phase.ATTR:
|
||||||
|
var card_w := 230.0; var card_h := 140.0
|
||||||
|
var n := _attr_choices.size()
|
||||||
|
var total_w: float = card_w * n + 20.0 * (n - 1)
|
||||||
|
var start_x: float = (W - total_w) * 0.5
|
||||||
|
var cy: float = H * 0.5 - 30.0 - card_h * 0.5 + 40.0
|
||||||
|
for i in n:
|
||||||
|
var cx: float = start_x + i * (card_w + 20.0)
|
||||||
|
if Rect2(cx, cy, card_w, card_h).has_point(p):
|
||||||
|
_attr_cursor = i
|
||||||
|
_confirm_attr()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
var card_w := 250.0; var card_h := 140.0
|
||||||
|
var card_pos: Array = [
|
||||||
|
Vector2(W * 0.5 - card_w - 120.0, H * 0.5 - card_h - 50.0),
|
||||||
|
Vector2(W * 0.5 + 120.0, H * 0.5 - card_h - 50.0),
|
||||||
|
Vector2(W * 0.5 - card_w - 120.0, H * 0.5 - 10.0),
|
||||||
|
Vector2(W * 0.5 + 120.0, H * 0.5 - 10.0),
|
||||||
|
]
|
||||||
|
for i in min(_shop_cards.size(), card_pos.size()):
|
||||||
|
if Rect2(card_pos[i].x, card_pos[i].y, card_w, card_h).has_point(p):
|
||||||
|
_shop_cursor = i
|
||||||
|
_try_buy(i)
|
||||||
|
return
|
||||||
|
if p.y >= H - 52.0:
|
||||||
|
if p.x < W * 0.5:
|
||||||
|
_try_reroll()
|
||||||
|
else:
|
||||||
|
_close()
|
||||||
|
|
||||||
|
func _touch_nav(action: String) -> void:
|
||||||
|
if _phase == Phase.ATTR:
|
||||||
|
match action:
|
||||||
|
"ui_left": _attr_cursor = (_attr_cursor - 1 + _attr_choices.size()) % _attr_choices.size()
|
||||||
|
"ui_right": _attr_cursor = (_attr_cursor + 1) % _attr_choices.size()
|
||||||
|
else:
|
||||||
|
match action:
|
||||||
|
"ui_left": _move_shop_cursor(-1)
|
||||||
|
"ui_right": _move_shop_cursor(1)
|
||||||
|
"ui_up": _move_shop_cursor(-2)
|
||||||
|
"ui_down": _move_shop_cursor(2)
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if not visible: return
|
||||||
|
if _phase == Phase.ATTR:
|
||||||
|
_handle_attr_input(event)
|
||||||
|
else:
|
||||||
|
_handle_shop_input(event)
|
||||||
|
|
||||||
|
func _handle_attr_input(event: InputEvent) -> void:
|
||||||
|
if _player_idx == 2:
|
||||||
|
if event.is_action_pressed("p2_left"):
|
||||||
|
_attr_cursor = (_attr_cursor - 1 + _attr_choices.size()) % _attr_choices.size()
|
||||||
|
elif event.is_action_pressed("p2_right"):
|
||||||
|
_attr_cursor = (_attr_cursor + 1) % _attr_choices.size()
|
||||||
|
elif event.is_action_pressed("p2_shoot"):
|
||||||
|
_confirm_attr()
|
||||||
|
else:
|
||||||
|
if event.is_action_pressed("ui_left"):
|
||||||
|
_attr_cursor = (_attr_cursor - 1 + _attr_choices.size()) % _attr_choices.size()
|
||||||
|
elif event.is_action_pressed("ui_right"):
|
||||||
|
_attr_cursor = (_attr_cursor + 1) % _attr_choices.size()
|
||||||
|
elif event is InputEventKey and event.pressed and event.keycode == KEY_SPACE:
|
||||||
|
_confirm_attr()
|
||||||
|
elif event.is_action_pressed("ui_accept"):
|
||||||
|
_confirm_attr()
|
||||||
|
|
||||||
|
func _handle_shop_input(event: InputEvent) -> void:
|
||||||
|
if _player_idx == 2:
|
||||||
|
if event.is_action_pressed("p2_wipe") or event.is_action_pressed("ui_cancel"):
|
||||||
|
_close()
|
||||||
|
elif event.is_action_pressed("p2_left"):
|
||||||
|
_move_shop_cursor(-1)
|
||||||
|
elif event.is_action_pressed("p2_right"):
|
||||||
|
_move_shop_cursor(1)
|
||||||
|
elif event.is_action_pressed("p2_thrust"):
|
||||||
|
_move_shop_cursor(-2)
|
||||||
|
elif event.is_action_pressed("p2_boost"):
|
||||||
|
_try_reroll()
|
||||||
|
elif event.is_action_pressed("p2_shoot"):
|
||||||
|
_try_buy(_shop_cursor)
|
||||||
|
else:
|
||||||
|
if event is InputEventKey and event.pressed:
|
||||||
|
if event.keycode == KEY_SPACE or event.keycode == KEY_ESCAPE:
|
||||||
|
_close()
|
||||||
|
return
|
||||||
|
elif event.keycode == KEY_ENTER:
|
||||||
|
_try_buy(_shop_cursor)
|
||||||
|
return
|
||||||
|
elif event.keycode == KEY_R:
|
||||||
|
_try_reroll()
|
||||||
|
return
|
||||||
|
if event.is_action_pressed("ui_cancel"):
|
||||||
|
_close()
|
||||||
|
elif event.is_action_pressed("ui_left"):
|
||||||
|
_move_shop_cursor(-1)
|
||||||
|
elif event.is_action_pressed("ui_right"):
|
||||||
|
_move_shop_cursor(1)
|
||||||
|
elif event.is_action_pressed("ui_up"):
|
||||||
|
_move_shop_cursor(-2)
|
||||||
|
elif event.is_action_pressed("ui_down"):
|
||||||
|
_move_shop_cursor(2)
|
||||||
|
elif event.is_action_pressed("ui_accept"):
|
||||||
|
_try_buy(_shop_cursor)
|
||||||
|
|
||||||
|
func _move_shop_cursor(delta: int) -> void:
|
||||||
|
if _shop_cards.is_empty(): return
|
||||||
|
_shop_cursor = (_shop_cursor + delta + _shop_cards.size()) % _shop_cards.size()
|
||||||
|
|
||||||
|
func _confirm_attr() -> void:
|
||||||
|
if _attr_cursor >= _attr_choices.size(): return
|
||||||
|
var choice: Dictionary = _attr_choices[_attr_cursor]
|
||||||
|
_stats.apply_item(choice["effects"])
|
||||||
|
_phase = Phase.SHOP
|
||||||
|
|
||||||
|
func _try_buy(idx: int) -> void:
|
||||||
|
if idx < 0 or idx >= _shop_cards.size(): return
|
||||||
|
var def: ItemDef = _shop_cards[idx]
|
||||||
|
if _credits < def.cost: return
|
||||||
|
_credits -= def.cost
|
||||||
|
_stats.apply_item_def(def)
|
||||||
|
_owned_ids.append(def.id)
|
||||||
|
_shop_cards.remove_at(idx)
|
||||||
|
if _shop_cards.is_empty():
|
||||||
|
_shop_cursor = 0
|
||||||
|
else:
|
||||||
|
_shop_cursor = min(_shop_cursor, _shop_cards.size() - 1)
|
||||||
|
|
||||||
|
func _try_reroll() -> void:
|
||||||
|
var cost: int = REROLL_BASE_COST + _reroll_count * REROLL_STEP_COST
|
||||||
|
if _credits < cost: return
|
||||||
|
_credits -= cost
|
||||||
|
_reroll_count += 1
|
||||||
|
_roll_shop()
|
||||||
|
_shop_cursor = 0
|
||||||
|
|
||||||
|
func _close() -> void:
|
||||||
|
visible = false
|
||||||
|
shop_closed.emit(_credits, _stats, _owned_ids, _reroll_count)
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Drawing
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
var vs: Vector2 = get_viewport_rect().size
|
||||||
|
W = vs.x; H = vs.y
|
||||||
|
draw_rect(get_viewport_rect(), COL_BG)
|
||||||
|
|
||||||
|
_draw_header()
|
||||||
|
_draw_brackets(8, 8, W - 8, H - 8, COL_DIM, 16)
|
||||||
|
|
||||||
|
if _phase == Phase.ATTR:
|
||||||
|
_draw_attr_phase()
|
||||||
|
else:
|
||||||
|
_draw_shop_phase()
|
||||||
|
|
||||||
|
_draw_footer()
|
||||||
|
|
||||||
|
# ── Header ────────────────────────────────────────────────────────────────────
|
||||||
|
func _draw_header() -> void:
|
||||||
|
draw_rect(Rect2(0, 0, W, 50.0), Color(0.0, 0.04, 0.02, 0.95))
|
||||||
|
draw_line(Vector2(0, 50), Vector2(W, 50), COL_DIM, 1.0)
|
||||||
|
|
||||||
|
var phase_key: String = "werk_phase_attr" if _phase == Phase.ATTR else "werk_phase_shop"
|
||||||
|
var wave_str := Tr.t("werk_wave") % _wave
|
||||||
|
if _player_label != "":
|
||||||
|
_draw_text_c(_player_label, W * 0.5, 10.0, 9, COL_DIM)
|
||||||
|
_draw_text_c(Tr.t(phase_key) + wave_str, W * 0.5, 26.0, 13, COL_PRIMARY)
|
||||||
|
|
||||||
|
# Lives + credits oben rechts
|
||||||
|
var lives_str := ""
|
||||||
|
for li in 3:
|
||||||
|
lives_str += ("●" if li < _lives else "○")
|
||||||
|
_draw_text(lives_str + " CR: %d" % _credits, W - 190.0, 14.0, 11, COL_PRIMARY)
|
||||||
|
|
||||||
|
# ── Attribut-Phase ────────────────────────────────────────────────────────────
|
||||||
|
func _draw_attr_phase() -> void:
|
||||||
|
var y_mid := H * 0.5 - 30.0
|
||||||
|
_draw_text_c(Tr.t("werk_attr_prompt"), W * 0.5, 76.0, 11, COL_ACCENT)
|
||||||
|
|
||||||
|
var card_w := 230.0; var card_h := 140.0
|
||||||
|
var n: int = _attr_choices.size()
|
||||||
|
var total_w: float = card_w * n + 20.0 * (n - 1)
|
||||||
|
var start_x: float = (W - total_w) * 0.5
|
||||||
|
|
||||||
|
for i in n:
|
||||||
|
var choice: Dictionary = _attr_choices[i]
|
||||||
|
var cx: float = start_x + i * (card_w + 20.0)
|
||||||
|
var cy: float = y_mid - card_h * 0.5 + 40.0
|
||||||
|
var is_sel: bool = i == _attr_cursor
|
||||||
|
var pulse: float = 0.5 + 0.5 * sin(_blink * 3.0)
|
||||||
|
|
||||||
|
draw_rect(Rect2(cx, cy, card_w, card_h),
|
||||||
|
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.10))
|
||||||
|
var bc: Color = Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.55 + 0.45 * pulse) if is_sel else COL_DIM
|
||||||
|
_draw_brackets(cx, cy, cx + card_w, cy + card_h, bc, 14)
|
||||||
|
|
||||||
|
_draw_text_c(Tr.t("werk_attr_tag"), cx + card_w * 0.5, cy + 10.0, 8,
|
||||||
|
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.6))
|
||||||
|
var _en := Settings.language == "en"
|
||||||
|
_draw_text_c(choice["name_en"] if _en else choice["name"], cx + card_w * 0.5, cy + 32.0, 13, COL_WHITE)
|
||||||
|
_draw_text_c(choice["desc_en"] if _en else choice["desc"], cx + card_w * 0.5, cy + 62.0, 10, COL_POS)
|
||||||
|
|
||||||
|
if is_sel:
|
||||||
|
var hint_col := Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.6 + 0.4 * pulse)
|
||||||
|
var pick_key: String = "werk_pick_p2" if _player_idx == 2 else "werk_pick"
|
||||||
|
_draw_text_c(Tr.hint(pick_key), cx + card_w * 0.5, cy + card_h - 24.0, 10, hint_col)
|
||||||
|
|
||||||
|
# ── Werkstatt-Phase ───────────────────────────────────────────────────────────
|
||||||
|
func _draw_shop_phase() -> void:
|
||||||
|
# Schiff-Preview in der Mitte (leicht nach oben verschoben)
|
||||||
|
_draw_ship_preview(W * 0.5, H * 0.5 - 15.0)
|
||||||
|
|
||||||
|
# 2×2 Karten um das Schiff herum — nach oben geschoben, damit Vorschau-Panel Platz hat
|
||||||
|
var card_w := 250.0; var card_h := 140.0
|
||||||
|
var positions: Array = [
|
||||||
|
Vector2(W * 0.5 - card_w - 120.0, H * 0.5 - card_h - 50.0), # Top-left y=110
|
||||||
|
Vector2(W * 0.5 + 120.0, H * 0.5 - card_h - 50.0), # Top-right y=110
|
||||||
|
Vector2(W * 0.5 - card_w - 120.0, H * 0.5 - 10.0), # Bottom-left y=290
|
||||||
|
Vector2(W * 0.5 + 120.0, H * 0.5 - 10.0), # Bottom-right y=290
|
||||||
|
]
|
||||||
|
|
||||||
|
for i in _shop_cards.size():
|
||||||
|
var def: ItemDef = _shop_cards[i]
|
||||||
|
var p: Vector2 = positions[i] if i < positions.size() else Vector2(20, 80 + i * 40)
|
||||||
|
_draw_shop_card(def, p.x, p.y, card_w, card_h, i == _shop_cursor)
|
||||||
|
|
||||||
|
# Vorschau-Panel — unterhalb der unteren Kartenreihe (y=430), mit 8px Abstand
|
||||||
|
if not _shop_cards.is_empty() and _shop_cursor < _shop_cards.size():
|
||||||
|
_draw_preview_panel(_shop_cards[_shop_cursor], W * 0.5, H - 119.0)
|
||||||
|
|
||||||
|
# Owned items Banner (oben unter dem Header)
|
||||||
|
if _owned_ids.size() > 0:
|
||||||
|
_draw_owned_banner(W * 0.5, 60.0)
|
||||||
|
|
||||||
|
func _draw_shop_card(def: ItemDef, x: float, y: float, w: float, h: float, is_sel: bool) -> void:
|
||||||
|
var pulse: float = 0.5 + 0.5 * sin(_blink * 3.0)
|
||||||
|
var can_afford: bool = _credits >= def.cost
|
||||||
|
var rarity_col: Color = ItemDB.RARITY_COLORS.get(def.rarity, COL_PRIMARY)
|
||||||
|
|
||||||
|
var bg_a: float = 0.18 if can_afford else 0.08
|
||||||
|
draw_rect(Rect2(x, y, w, h), Color(rarity_col.r, rarity_col.g, rarity_col.b, bg_a))
|
||||||
|
|
||||||
|
var border_col: Color
|
||||||
|
if is_sel:
|
||||||
|
border_col = Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.55 + 0.45 * pulse)
|
||||||
|
else:
|
||||||
|
border_col = Color(rarity_col.r, rarity_col.g, rarity_col.b, 0.35 if can_afford else 0.15)
|
||||||
|
_draw_brackets(x, y, x + w, y + h, border_col, 12)
|
||||||
|
|
||||||
|
# Icon + Kategorie
|
||||||
|
var _item_en := Settings.language == "en"
|
||||||
|
var disp_cat := def.category_en if _item_en and def.category_en != "" else def.category
|
||||||
|
var disp_name := def.name_en if _item_en and def.name_en != "" else def.name
|
||||||
|
_draw_text(def.icon + " " + disp_cat, x + 12.0, y + 8.0, 8,
|
||||||
|
Color(rarity_col.r, rarity_col.g, rarity_col.b, 0.7 if can_afford else 0.3))
|
||||||
|
|
||||||
|
# Name
|
||||||
|
var name_a: float = 0.9 if can_afford else 0.35
|
||||||
|
_draw_text(disp_name, x + 12.0, y + 26.0, 13, Color(COL_WHITE.r, COL_WHITE.g, COL_WHITE.b, name_a))
|
||||||
|
|
||||||
|
# Effekte (bis zu 4 Zeilen, + grün / − rot)
|
||||||
|
var line_y: float = y + 58.0
|
||||||
|
var count := 0
|
||||||
|
for key: String in def.effects:
|
||||||
|
if count >= 4: break
|
||||||
|
var val = def.effects[key]
|
||||||
|
var is_pos: bool = ItemDB.is_positive_effect(key, val)
|
||||||
|
var sign_str: String = "+ " if is_pos else "− "
|
||||||
|
var col: Color = COL_POS if is_pos else COL_NEG
|
||||||
|
if not can_afford:
|
||||||
|
col = Color(col.r, col.g, col.b, 0.35)
|
||||||
|
var line: String = "%s%s %s" % [sign_str, ItemDB.stat_display_name(key), _fmt_val(key, val)]
|
||||||
|
_draw_text(line, x + 16.0, line_y, 9, col)
|
||||||
|
line_y += 13.0
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
# Cost unten rechts
|
||||||
|
var cost_col: Color = COL_WARN if not can_afford else Color(1.0, 1.0, 0.6, 0.95)
|
||||||
|
_draw_text("%d CR" % def.cost, x + w - 60.0, y + h - 20.0, 12, cost_col)
|
||||||
|
|
||||||
|
# Hint auf aktiver Karte
|
||||||
|
if is_sel:
|
||||||
|
var hint_col: Color = Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.6 + 0.4 * pulse)
|
||||||
|
var hint: String = Tr.hint("werk_buy_hint_p2" if _player_idx == 2 else "werk_buy_hint_p1") if can_afford else Tr.t("shop_no_credits")
|
||||||
|
_draw_text(hint, x + 12.0, y + h - 20.0, 9, hint_col)
|
||||||
|
|
||||||
|
# ── Preview-Panel: Vorher/Nachher-Diff ────────────────────────────────────────
|
||||||
|
func _draw_preview_panel(def: ItemDef, cx: float, cy: float) -> void:
|
||||||
|
var pw := 420.0; var ph := 86.0
|
||||||
|
var px := cx - pw * 0.5; var py := cy - ph * 0.5
|
||||||
|
draw_rect(Rect2(px, py, pw, ph), Color(0.0, 0.04, 0.02, 0.9))
|
||||||
|
_draw_brackets(px, py, px + pw, py + ph, COL_DIM, 10)
|
||||||
|
|
||||||
|
_draw_text_c(Tr.t("werk_preview") + " · " + def.name,
|
||||||
|
cx, py + 6.0, 9, COL_ACCENT)
|
||||||
|
|
||||||
|
# Liste: key: before → after
|
||||||
|
var col_x: float = px + 14.0
|
||||||
|
var line_y: float = py + 26.0
|
||||||
|
var count := 0
|
||||||
|
for key: String in def.effects:
|
||||||
|
if count >= 3: break
|
||||||
|
var val = def.effects[key]
|
||||||
|
var before: float = _stat_value(key)
|
||||||
|
var after: float = _project_stat(key, val)
|
||||||
|
var pos: bool = ItemDB.is_positive_effect(key, val)
|
||||||
|
var col: Color = COL_POS if pos else COL_NEG
|
||||||
|
var line := "%s: %.2f → %.2f" % [ItemDB.stat_display_name(key), before, after]
|
||||||
|
_draw_text(line, col_x, line_y, 10, col)
|
||||||
|
line_y += 14.0
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
# Hull-Size Warnung
|
||||||
|
if def.hull_size_bonus > 0.0:
|
||||||
|
_draw_text(Tr.t("werk_grows"), px + pw - 150.0, py + ph - 22.0, 9, COL_WARN)
|
||||||
|
|
||||||
|
func _stat_value(key: String) -> float:
|
||||||
|
match key:
|
||||||
|
"speed_mult": return _stats.speed_mult
|
||||||
|
"turn_mult": return _stats.turn_mult
|
||||||
|
"fire_rate_mult": return _stats.fire_rate_mult
|
||||||
|
"damage_mult": return _stats.damage_mult
|
||||||
|
"bullet_speed_mult": return _stats.bullet_speed_mult
|
||||||
|
"bullet_count": return float(_stats.bullet_count)
|
||||||
|
"shield_charges": return float(_stats.shield_charges)
|
||||||
|
"invuln_mult": return _stats.invuln_mult
|
||||||
|
"bh_resist": return _stats.bh_resist
|
||||||
|
"wipe_mult": return _stats.wipe_mult
|
||||||
|
"credit_bonus": return _stats.credit_bonus
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
func _project_stat(key: String, val) -> float:
|
||||||
|
match key:
|
||||||
|
"speed_mult","turn_mult","fire_rate_mult","damage_mult", \
|
||||||
|
"bullet_speed_mult","invuln_mult","wipe_mult","credit_bonus":
|
||||||
|
return _stat_value(key) * float(val)
|
||||||
|
"bullet_count","shield_charges":
|
||||||
|
return _stat_value(key) + float(val)
|
||||||
|
"bh_resist":
|
||||||
|
return min(0.9, _stat_value(key) + float(val))
|
||||||
|
return _stat_value(key)
|
||||||
|
|
||||||
|
func _fmt_val(key: String, val) -> String:
|
||||||
|
match key:
|
||||||
|
"bullet_count": return "+%d" % int(val)
|
||||||
|
"shield_charges": return ("+%d" % int(val)) if int(val) >= 0 else "%d" % int(val)
|
||||||
|
"bh_resist": return "+%.0f%%" % (float(val) * 100.0)
|
||||||
|
_:
|
||||||
|
var f: float = float(val)
|
||||||
|
var pct: float = (f - 1.0) * 100.0
|
||||||
|
return "%+.0f%%" % pct
|
||||||
|
|
||||||
|
# ── Ship preview im Zentrum ───────────────────────────────────────────────────
|
||||||
|
func _draw_ship_preview(cx: float, cy: float) -> void:
|
||||||
|
var bw := 180.0; var bh := 180.0
|
||||||
|
draw_rect(Rect2(cx - bw * 0.5, cy - bh * 0.5, bw, bh),
|
||||||
|
Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.05))
|
||||||
|
_draw_brackets(cx - bw * 0.5, cy - bh * 0.5, cx + bw * 0.5, cy + bh * 0.5,
|
||||||
|
COL_DIM, 14)
|
||||||
|
|
||||||
|
_draw_text_c(Tr.t("werk_ship_preview"), cx, cy - bh * 0.5 + 6.0, 8,
|
||||||
|
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.55))
|
||||||
|
|
||||||
|
# Schiff zeichnen — gleich wie Spaceship.HULL_PIXELS, skaliert
|
||||||
|
var hs: float = _stats.hull_scale if _stats != null else 3.0
|
||||||
|
var preview_scale: float = 3.5 # Extra-Zoom für Preview
|
||||||
|
var off_mul: float = hs / 3.0 * preview_scale
|
||||||
|
var rect_sz: float = hs * preview_scale * 0.9
|
||||||
|
var rect_half: float = rect_sz * 0.5
|
||||||
|
var heading: float = -PI * 0.5 # Schiff zeigt nach oben
|
||||||
|
var cos_h: float = cos(heading); var sin_h: float = sin(heading)
|
||||||
|
|
||||||
|
for px_data in Spaceship.HULL_PIXELS:
|
||||||
|
var lx: float = px_data[0] * off_mul
|
||||||
|
var ly: float = px_data[1] * off_mul
|
||||||
|
var col: Color = _ship_palette.get(px_data[2], COL_WHITE)
|
||||||
|
var rx: float = cx + lx * cos_h - ly * sin_h
|
||||||
|
var ry: float = cy + lx * sin_h + ly * cos_h
|
||||||
|
draw_rect(Rect2(rx - rect_half, ry - rect_half, rect_sz, rect_sz), col)
|
||||||
|
|
||||||
|
# Attachments aus owned_items
|
||||||
|
var seen: Dictionary = {}
|
||||||
|
for id: String in _owned_ids:
|
||||||
|
var def: ItemDef = ItemDB.get_def_by_id(id)
|
||||||
|
if def == null or def.visual_pixels.is_empty(): continue
|
||||||
|
var stack: int = int(seen.get(id, 0))
|
||||||
|
seen[id] = stack + 1
|
||||||
|
var stack_off: float = float(stack) * 0.6
|
||||||
|
for vpx in def.visual_pixels:
|
||||||
|
var lx: float = (float(vpx[0]) + stack_off) * off_mul
|
||||||
|
var ly: float = float(vpx[1]) * off_mul
|
||||||
|
var col: Color = _ship_palette.get(vpx[2], COL_WHITE)
|
||||||
|
var rx: float = cx + lx * cos_h - ly * sin_h
|
||||||
|
var ry: float = cy + lx * sin_h + ly * cos_h
|
||||||
|
draw_rect(Rect2(rx - rect_half, ry - rect_half, rect_sz, rect_sz), col)
|
||||||
|
|
||||||
|
func _draw_owned_banner(cx: float, cy: float) -> void:
|
||||||
|
var names: Array = []
|
||||||
|
for id: String in _owned_ids:
|
||||||
|
var def: ItemDef = ItemDB.get_def_by_id(id)
|
||||||
|
if def != null:
|
||||||
|
names.append(def.name)
|
||||||
|
if names.is_empty(): return
|
||||||
|
var line := Tr.t("shop_owned") + " " + " · ".join(names)
|
||||||
|
if line.length() > 100:
|
||||||
|
line = line.substr(0, 97) + "…"
|
||||||
|
_draw_text_c(line, cx, cy, 9, Color(COL_PRIMARY.r, COL_PRIMARY.g, COL_PRIMARY.b, 0.5))
|
||||||
|
|
||||||
|
# ── Footer ────────────────────────────────────────────────────────────────────
|
||||||
|
func _draw_footer() -> void:
|
||||||
|
draw_rect(Rect2(0, H - 52.0, W, 52.0), Color(0.0, 0.04, 0.02, 0.95))
|
||||||
|
draw_line(Vector2(0, H - 52.0), Vector2(W, H - 52.0), COL_DIM, 1.0)
|
||||||
|
|
||||||
|
var p2: bool = _player_idx == 2
|
||||||
|
var is_touch: bool = Settings.last_input_device == "touch"
|
||||||
|
var pulse: float = 0.5 + 0.4 * sin(_blink * 2.5)
|
||||||
|
if _phase == Phase.ATTR:
|
||||||
|
if is_touch:
|
||||||
|
_draw_text_c("● TAP", W * 0.5, H - 26.0, 11,
|
||||||
|
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse))
|
||||||
|
else:
|
||||||
|
_draw_text_c(Tr.hint("werk_attr_footer_p2" if p2 else "werk_attr_footer"), W * 0.5, H - 48.0, 9, COL_DIM)
|
||||||
|
_draw_text_c(Tr.hint("werk_confirm_p2" if p2 else "werk_confirm"), W * 0.5, H - 26.0, 11,
|
||||||
|
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse))
|
||||||
|
else:
|
||||||
|
var reroll_cost: int = REROLL_BASE_COST + _reroll_count * REROLL_STEP_COST
|
||||||
|
var reroll_col: Color = COL_DIM if _credits < reroll_cost else COL_PRIMARY
|
||||||
|
if is_touch:
|
||||||
|
# Touch: left half = REROLL tap zone, right half = SKIP tap zone
|
||||||
|
draw_rect(Rect2(0, H - 52.0, W * 0.5 - 1, 52.0),
|
||||||
|
Color(reroll_col.r, reroll_col.g, reroll_col.b, 0.06))
|
||||||
|
draw_rect(Rect2(W * 0.5 + 1, H - 52.0, W * 0.5 - 1, 52.0),
|
||||||
|
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, 0.06))
|
||||||
|
var reroll_str: String = ("REROLL %d CR" if Settings.language == "en" else "REROLL %d CR") % reroll_cost
|
||||||
|
_draw_text_c(reroll_str, W * 0.25, H - 26.0, 10, reroll_col)
|
||||||
|
var skip_str: String = "SKIP ►" if Settings.language == "en" else "WEITER ►"
|
||||||
|
_draw_text_c(skip_str, W * 0.75, H - 26.0, 11,
|
||||||
|
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse))
|
||||||
|
else:
|
||||||
|
_draw_text_c(Tr.hint("werk_shop_footer_p2" if p2 else "werk_shop_footer"), W * 0.5, H - 48.0, 9, COL_DIM)
|
||||||
|
var reroll_key: String = "Q" if p2 else "R"
|
||||||
|
_draw_text_c(Tr.t("werk_reroll") % [reroll_key, reroll_cost], W * 0.30, H - 26.0, 10, reroll_col)
|
||||||
|
_draw_text_c(Tr.hint("werk_continue_p2" if p2 else "shop_continue"), W * 0.72, H - 26.0, 11,
|
||||||
|
Color(COL_ACCENT.r, COL_ACCENT.g, COL_ACCENT.b, pulse))
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Text + Shape Helpers
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
func _draw_text(text: String, x: float, y: float, sz: int, col: Color) -> void:
|
||||||
|
var font := ThemeDB.fallback_font
|
||||||
|
draw_string(font, Vector2(x, y + sz), text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz, col)
|
||||||
|
|
||||||
|
func _draw_text_c(text: String, x: float, y: float, sz: int, col: Color) -> void:
|
||||||
|
var font := ThemeDB.fallback_font
|
||||||
|
var tw: float = font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x
|
||||||
|
draw_string(font, Vector2(x - tw * 0.5, y + sz), text, HORIZONTAL_ALIGNMENT_LEFT, -1, sz, col)
|
||||||
|
|
||||||
|
func _draw_brackets(x1: float, y1: float, x2: float, y2: float, col: Color, arm: float) -> void:
|
||||||
|
var bw: float = 1.5
|
||||||
|
draw_line(Vector2(x1, y1), Vector2(x1 + arm, y1), col, bw)
|
||||||
|
draw_line(Vector2(x1, y1), Vector2(x1, y1 + arm), col, bw)
|
||||||
|
draw_line(Vector2(x2, y1), Vector2(x2 - arm, y1), col, bw)
|
||||||
|
draw_line(Vector2(x2, y1), Vector2(x2, y1 + arm), col, bw)
|
||||||
|
draw_line(Vector2(x1, y2), Vector2(x1 + arm, y2), col, bw)
|
||||||
|
draw_line(Vector2(x1, y2), Vector2(x1, y2 - arm), col, bw)
|
||||||
|
draw_line(Vector2(x2, y2), Vector2(x2 - arm, y2), col, bw)
|
||||||
|
draw_line(Vector2(x2, y2), Vector2(x2, y2 - arm), col, bw)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://18s6j8gme2ne
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
# SoundManager - Autoload singleton for all SFX
|
||||||
|
|
||||||
|
var _bus_sfx := 0
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_bus_sfx = AudioServer.get_bus_index("Master")
|
||||||
|
|
||||||
|
func play_sfx(sfx_name: String) -> void:
|
||||||
|
match sfx_name:
|
||||||
|
"player_shoot": _play_tone(880.0, 0.07, "sine", -18.0, 440.0)
|
||||||
|
"enemy_shoot": _play_tone(220.0, 0.09, "saw", -20.0, 110.0)
|
||||||
|
"enemy_die": _play_noise(0.2, -16.0); _play_tone(180.0, 0.15, "square", -18.0, 60.0)
|
||||||
|
"player_die": _play_noise(0.8, -14.0); _play_tone(120.0, 0.8, "sine", -16.0, 30.0)
|
||||||
|
"antimatter_hit": _play_tone(1200.0, 0.3, "saw", -16.0, 300.0); _play_noise(0.2, -18.0)
|
||||||
|
"bh_swallow": _play_tone(200.0, 0.22, "sine", -20.0, 40.0)
|
||||||
|
"wipe_start": _play_tone(35.0, 2.3, "sine", -22.0, 35.0)
|
||||||
|
"wipe_flash": _play_noise(0.4, -16.0); _play_tone(80.0, 0.6, "sine", -18.0, 20.0)
|
||||||
|
"wipe_survived": _play_chord_fanfare()
|
||||||
|
"smbh_spawn": _play_tone(60.0, 1.0, "sine", -18.0, 20.0); _play_noise(0.5, -16.0)
|
||||||
|
"quasar_boost": _play_tone(280.0, 0.45, "sine", -15.0, 1500.0); _play_noise(0.15, -22.0)
|
||||||
|
"antimatter_swarm":_play_noise(0.28, -18.0)
|
||||||
|
|
||||||
|
func _play_tone(freq: float, duration: float, wave: String, vol_db: float, end_freq: float) -> void:
|
||||||
|
var gen := AudioStreamGenerator.new()
|
||||||
|
gen.mix_rate = 44100.0
|
||||||
|
gen.buffer_length = duration
|
||||||
|
var player := AudioStreamPlayer.new()
|
||||||
|
player.stream = gen
|
||||||
|
player.volume_db = vol_db
|
||||||
|
player.autoplay = false
|
||||||
|
add_child(player)
|
||||||
|
player.play()
|
||||||
|
var playback: AudioStreamGeneratorPlayback = player.get_stream_playback()
|
||||||
|
var frames := int(44100.0 * duration)
|
||||||
|
var phase := 0.0
|
||||||
|
for i in frames:
|
||||||
|
var t := float(i) / float(frames)
|
||||||
|
var cur_freq := freq + (end_freq - freq) * t
|
||||||
|
phase += cur_freq / 44100.0
|
||||||
|
var s := 0.0
|
||||||
|
match wave:
|
||||||
|
"sine": s = sin(phase * TAU)
|
||||||
|
"saw": s = fmod(phase, 1.0) * 2.0 - 1.0
|
||||||
|
"square": s = 1.0 if sin(phase * TAU) > 0 else -1.0
|
||||||
|
var env := 1.0 - t
|
||||||
|
playback.push_frame(Vector2(s * env, s * env))
|
||||||
|
var timer := get_tree().create_timer(duration + 0.1)
|
||||||
|
timer.timeout.connect(player.queue_free)
|
||||||
|
|
||||||
|
func _play_noise(duration: float, vol_db: float) -> void:
|
||||||
|
var gen := AudioStreamGenerator.new()
|
||||||
|
gen.mix_rate = 44100.0
|
||||||
|
gen.buffer_length = duration
|
||||||
|
var player := AudioStreamPlayer.new()
|
||||||
|
player.stream = gen
|
||||||
|
player.volume_db = vol_db
|
||||||
|
add_child(player)
|
||||||
|
player.play()
|
||||||
|
var playback: AudioStreamGeneratorPlayback = player.get_stream_playback()
|
||||||
|
var frames := int(44100.0 * duration)
|
||||||
|
for i in frames:
|
||||||
|
var t := float(i) / float(frames)
|
||||||
|
var s := randf_range(-1.0, 1.0) * (1.0 - t)
|
||||||
|
playback.push_frame(Vector2(s, s))
|
||||||
|
var timer := get_tree().create_timer(duration + 0.1)
|
||||||
|
timer.timeout.connect(player.queue_free)
|
||||||
|
|
||||||
|
func _play_chord_fanfare() -> void:
|
||||||
|
var freqs: Array[float] = [261.6, 329.6, 392.0]
|
||||||
|
for i in freqs.size():
|
||||||
|
var delay_timer := get_tree().create_timer(i * 0.06)
|
||||||
|
var f: float = freqs[i]
|
||||||
|
delay_timer.timeout.connect(func(): _play_tone(f, 0.4, "sine", -16.0, f))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bivpkig7j7mor
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user