Release Void Bastion alpha.4
No files matched your search
@@ -2,6 +2,12 @@
|
||||
|
||||
Erzeugt mit dem integrierten OpenAI-Bildgenerator. Die Sprite-Sheets besitzen echte Transparenz.
|
||||
|
||||
## Zusätzliche Shop-Karten (Alpha 0.1.0-alpha.4 Entwicklung)
|
||||
|
||||
Die vier neuen Hintergründe `orbital_biodome_bg.png`, `cryo_refinery_bg.png`, `solar_forge_bg.png` und `vestige_megastructure_bg.png` wurden als eigenständige, straßenfreie 16:9-Karten erzeugt. Der Reaktor liegt jeweils rechts ungefähr auf Höhe der Spielfeldmitte. Die Fahrbahn, Gegnerroute und Bausperre werden zur Laufzeit ausschließlich aus den gemeinsamen 80-Pixel-Raster-Wegpunkten aufgebaut.
|
||||
|
||||
Gemeinsame Prompt-Vorgabe: realistische, hochauflösende, direkt von oben gesehene Science-Fiction-Plattform mit freier Deckfläche, gut lesbarer Umgebung und sichtbarem Reaktor am rechten Rand; kein eingebauter Weg, keine Fahrbahnmarkierung, kein Raster, keine Bauplätze, keine Einheiten, Türme, UI, Schrift oder Logos. Die Varianten unterscheiden sich thematisch durch Biodom und blauen Nebel, eisige Raffinerie, orangefarbene Sonnen-Schmiede sowie eine verlassene Megastruktur im violetten All.
|
||||
|
||||
## orbital_platform.png
|
||||
|
||||
```text
|
||||
|
||||
@@ -4,7 +4,8 @@ Ein plattformübergreifender 2D-Tower-Defense-Prototyp für Godot 4. Der aktuell
|
||||
|
||||
## Enthalten
|
||||
|
||||
- drei auswählbare realistische Weltraumkarten im Querformat
|
||||
- sieben auswählbare realistische Weltraumkarten im Querformat; vier zusätzliche Sektoren werden im internen Shop mit Kernkristallen freigeschaltet
|
||||
- Kartenkatalog mit vier Sektoren pro Seite, persistent gespeicherten Freischaltungen und direktem Shop-Einstieg bei gesperrten Karten
|
||||
- vollständig sichtbare Einsatzkarten ohne Scrollen, Minikarte oder Richtungstasten
|
||||
- hellere, kontrastreichere Stationskarten und eigens gestaltete modulare Metallfahrbahnen mit realistischen Materialtexturen auf Geraden und Kurven, Führungsschienen, gebürsteten Paneelen, cyanfarbenen Energieleitungen, farbigen Reflexionen, Schraubpunkten, warmen Positionslichtern und dezenten Lichtkegeln
|
||||
- feste Benutzeroberfläche mit 1600 × 900 Referenzauflösung
|
||||
@@ -14,7 +15,7 @@ Ein plattformübergreifender 2D-Tower-Defense-Prototyp für Godot 4. Der aktuell
|
||||
- sichtbares 80-Pixel-Bauraster mit automatischem Einrasten der Türme
|
||||
- jede geeignete Plattformzelle ist bebaubar; ausschließlich Wegzellen, Reaktoren und Bereiche außerhalb der Plattform sind gesperrt
|
||||
- neu erzeugte, straßenfreie HD-Kartenbasen mit 3344 × 1882 Pixeln und Mipmaps für eine schärfere Windows-Darstellung
|
||||
- drei Türme: Kinetik, Laser und Rakete
|
||||
- sechs Türme: Kinetik, Laser, Rakete, Plasma, EMP-Puls und Railgun
|
||||
- drei Gegnerklassen mit unterschiedlichen Werten
|
||||
- 15 automatisch skalierende Missionswellen
|
||||
- drei wählbare Schwierigkeitsgrade: Leicht entspricht dem bisherigen Balancing, Mittel ist der neue Standard und Schwer erhöht Gegnerhülle, Tempo und Anzahl deutlich
|
||||
@@ -36,6 +37,7 @@ Ein plattformübergreifender 2D-Tower-Defense-Prototyp für Godot 4. Der aktuell
|
||||
- feste Reaktor-Sperrzonen auf allen Karten; auf Reaktoren und offenen Reaktorkammern wird kein Bauraster angezeigt und es können dort keine Türme platziert werden
|
||||
- auf eine Rasterzelle verkleinerte Turmdarstellung, damit angrenzende Türme nicht in die Fahrbahn hineinragen
|
||||
- dauerhaft gespeicherte Kernkristalle als spätere Freischaltwährung
|
||||
- Nova-Arkologie (24), Cryo-Schleuse (32), Solarforge (40) und Vestige-9 (48) als kaufbare Einsatzsektoren
|
||||
- dauerhaftes lokales Gastprofil mit stabiler Spieler-ID als Grundlage für Apple-, Google- und Windows-Anmeldung
|
||||
- eigene Profilansicht mit lokalen Missions- und Endlosbestwerten pro Karte
|
||||
- frei wählbarer Spielername mit identischer Prüfung im Spiel und auf dem Server
|
||||
@@ -58,7 +60,7 @@ Ein plattformübergreifender 2D-Tower-Defense-Prototyp für Godot 4. Der aktuell
|
||||
- Abschussbelohnungen und Baukosten werden dem jeweiligen Spieler beziehungsweise KI-Partner getrennt zugerechnet
|
||||
- farblich getrennte Bauflächen und markierte KI-Türme zur schnellen Zuordnung im Gefecht
|
||||
- fünf Kernkristalle als Belohnung für jede erfolgreich abgeschlossene Karte
|
||||
- normale und doppelte Spielgeschwindigkeit
|
||||
- auswählbare Spielgeschwindigkeit bis 5× sowie automatischer Start der nächsten Welle
|
||||
- Pause, Missionssieg, Niederlage und lokaler Wellenrekord
|
||||
- jederzeit sichtbarer Zurück-Button zum Abbrechen der laufenden Runde und Wechseln ins Kartenmenü
|
||||
- lokaler Speicherstand für Rekord und Tutorialstatus
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Void Bastion 0.1.0-alpha.4
|
||||
|
||||
Diese Windows-Testversion erweitert die Kartenauswahl und verbessert die Wegdarstellung.
|
||||
|
||||
## Neu
|
||||
|
||||
- vier neue Einsatzkarten: Nova-Arkologie, Cryo-Schleuse, Solarforge und Vestige-9
|
||||
- alle sieben Karten werden gleichzeitig im Hauptmenü angezeigt
|
||||
- neue Karten lassen sich im internen Shop dauerhaft mit Kernkristallen freischalten
|
||||
- die Freischaltungen bleiben lokal im Profil gespeichert
|
||||
|
||||
## Verbessert
|
||||
|
||||
- alle Kartenwege folgen dem gemeinsamen 80-Pixel-Raster und enden im sichtbaren Reaktor
|
||||
- sich kreuzende Wegsegmente auf Nova-Arkologie sowie zusätzliche problematische Kreuzungen auf Cryo-Schleuse und Solarforge wurden entfernt
|
||||
- die letzten zwei unnötigen Bögen bei der Reaktoreinfahrt auf Pyrax wurden entfernt
|
||||
- Währungsanzeigen verwenden durchgängig die Bezeichnung „Kernkristalle“
|
||||
- die Profilübersicht und Kartenfilter unterstützen alle sieben Sektoren
|
||||
|
||||
## Hinweis
|
||||
|
||||
Void Bastion befindet sich weiterhin in einer frühen Alpha-Phase. Unter Windows erscheint keine Werbung. Updates werden im Spiel angekündigt, aber niemals ungefragt installiert. Rückmeldungen und Fehlerberichte bitte an support@gergames.de senden.
|
||||
@@ -3,11 +3,9 @@ extends Node
|
||||
|
||||
signal energy_reward_granted(amount: int)
|
||||
signal ad_unavailable(message: String)
|
||||
signal victory_interstitial_finished(was_shown: bool)
|
||||
|
||||
const SAVE_PATH := "user://void_bastion_ads.json"
|
||||
const ENERGY_REWARD := 150
|
||||
const MISSIONS_PER_INTERSTITIAL := 3
|
||||
const INTERSTITIAL_COOLDOWN_SECONDS := 15 * 60
|
||||
const ANDROID_TEST_APP_ID := "ca-app-pub-3940256099942544~3347511713"
|
||||
const IOS_TEST_APP_ID := "ca-app-pub-3940256099942544~1458002511"
|
||||
|
||||
@@ -16,15 +14,13 @@ var initialized := false
|
||||
var rewarded_ready := false
|
||||
var interstitial_ready := false
|
||||
var reward_used_this_mission := false
|
||||
var missions_since_interstitial := 0
|
||||
var last_interstitial_time := 0
|
||||
var _victory_interstitial_pending := false
|
||||
|
||||
var _admob: Node
|
||||
var _initialization_requested := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_load_state()
|
||||
mobile_ads_supported = OS.has_feature("android") or OS.has_feature("ios")
|
||||
if not mobile_ads_supported:
|
||||
return
|
||||
@@ -55,6 +51,7 @@ func _create_admob_node() -> void:
|
||||
_admob.interstitial_ad_loaded.connect(_on_interstitial_ad_loaded)
|
||||
_admob.interstitial_ad_failed_to_load.connect(_on_interstitial_ad_failed_to_load)
|
||||
_admob.interstitial_ad_dismissed_full_screen_content.connect(_on_interstitial_ad_dismissed)
|
||||
_admob.interstitial_ad_failed_to_show_full_screen_content.connect(_on_interstitial_ad_failed_to_show)
|
||||
add_child(_admob)
|
||||
|
||||
|
||||
@@ -76,31 +73,23 @@ func request_rewarded_energy() -> void:
|
||||
_admob.show_rewarded_ad()
|
||||
|
||||
|
||||
func record_mission_finished() -> void:
|
||||
missions_since_interstitial += 1
|
||||
_save_state()
|
||||
if is_interstitial_due() and interstitial_ready:
|
||||
_show_interstitial()
|
||||
elif is_interstitial_due() and initialized:
|
||||
func show_victory_interstitial() -> void:
|
||||
if _victory_interstitial_pending:
|
||||
return
|
||||
if not mobile_ads_supported or not initialized:
|
||||
victory_interstitial_finished.emit(false)
|
||||
return
|
||||
_victory_interstitial_pending = true
|
||||
if not interstitial_ready:
|
||||
_admob.load_interstitial_ad()
|
||||
return
|
||||
_display_pending_victory_interstitial()
|
||||
|
||||
|
||||
func is_interstitial_due(now_unix: int = -1) -> bool:
|
||||
if missions_since_interstitial < MISSIONS_PER_INTERSTITIAL:
|
||||
return false
|
||||
if last_interstitial_time <= 0:
|
||||
return true
|
||||
var now := int(Time.get_unix_time_from_system()) if now_unix < 0 else now_unix
|
||||
return now - last_interstitial_time >= INTERSTITIAL_COOLDOWN_SECONDS
|
||||
|
||||
|
||||
func _show_interstitial() -> void:
|
||||
if not mobile_ads_supported or not initialized or not interstitial_ready:
|
||||
func _display_pending_victory_interstitial() -> void:
|
||||
if not _victory_interstitial_pending or not interstitial_ready:
|
||||
return
|
||||
interstitial_ready = false
|
||||
missions_since_interstitial = 0
|
||||
last_interstitial_time = int(Time.get_unix_time_from_system())
|
||||
_save_state()
|
||||
_admob.show_interstitial_ad()
|
||||
|
||||
|
||||
@@ -171,33 +160,29 @@ func _on_rewarded_ad_dismissed(_ad_info) -> void:
|
||||
|
||||
func _on_interstitial_ad_loaded(_ad_info, _response_info) -> void:
|
||||
interstitial_ready = true
|
||||
_display_pending_victory_interstitial()
|
||||
|
||||
|
||||
func _on_interstitial_ad_failed_to_load(_ad_info, _error) -> void:
|
||||
interstitial_ready = false
|
||||
_finish_victory_interstitial(false)
|
||||
|
||||
|
||||
func _on_interstitial_ad_dismissed(_ad_info) -> void:
|
||||
interstitial_ready = false
|
||||
_finish_victory_interstitial(true)
|
||||
_admob.load_interstitial_ad()
|
||||
|
||||
|
||||
func _save_state() -> void:
|
||||
var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
|
||||
if file:
|
||||
file.store_string(JSON.stringify({
|
||||
"missions_since_interstitial": missions_since_interstitial,
|
||||
"last_interstitial_time": last_interstitial_time
|
||||
}))
|
||||
func _on_interstitial_ad_failed_to_show(_ad_info, _error) -> void:
|
||||
interstitial_ready = false
|
||||
_finish_victory_interstitial(false)
|
||||
if initialized:
|
||||
_admob.load_interstitial_ad()
|
||||
|
||||
|
||||
func _load_state() -> void:
|
||||
if not FileAccess.file_exists(SAVE_PATH):
|
||||
func _finish_victory_interstitial(was_shown: bool) -> void:
|
||||
if not _victory_interstitial_pending:
|
||||
return
|
||||
var file := FileAccess.open(SAVE_PATH, FileAccess.READ)
|
||||
if not file:
|
||||
return
|
||||
var data = JSON.parse_string(file.get_as_text())
|
||||
if data is Dictionary:
|
||||
missions_since_interstitial = maxi(0, int(data.get("missions_since_interstitial", 0)))
|
||||
last_interstitial_time = maxi(0, int(data.get("last_interstitial_time", 0)))
|
||||
_victory_interstitial_pending = false
|
||||
victory_interstitial_finished.emit(was_shown)
|
||||
|
After Width: | Height: | Size: 3.1 MiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c6qskqc282rpg"
|
||||
path="res://.godot/imported/cryo_refinery_bg.png-b0439eb485200ef8da0459147d987794.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/cryo_refinery_bg.png"
|
||||
dest_files=["res://.godot/imported/cryo_refinery_bg.png-b0439eb485200ef8da0459147d987794.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
|
||||
|
After Width: | Height: | Size: 3.0 MiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bkc4l5ax4o761"
|
||||
path="res://.godot/imported/orbital_biodome_bg.png-9073720249bcf29936435b3b814e9686.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/orbital_biodome_bg.png"
|
||||
dest_files=["res://.godot/imported/orbital_biodome_bg.png-9073720249bcf29936435b3b814e9686.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
|
||||
|
After Width: | Height: | Size: 2.7 MiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dxy2tscacupix"
|
||||
path="res://.godot/imported/solar_forge_bg.png-d2e9c29bfbb0f929e05570a5a5d0a90b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/solar_forge_bg.png"
|
||||
dest_files=["res://.godot/imported/solar_forge_bg.png-d2e9c29bfbb0f929e05570a5a5d0a90b.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
|
||||
|
After Width: | Height: | Size: 2.2 MiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://da66x5legbfd3"
|
||||
path="res://.godot/imported/tower_emp.png-0c5406d0107cf3a00de9de8b95603513.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/tower_emp.png"
|
||||
dest_files=["res://.godot/imported/tower_emp.png-0c5406d0107cf3a00de9de8b95603513.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
|
||||
|
After Width: | Height: | Size: 1.8 MiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dcfoc8ioji2jg"
|
||||
path="res://.godot/imported/tower_plasma.png-6c4277ea9427fade2f842c305b0946df.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/tower_plasma.png"
|
||||
dest_files=["res://.godot/imported/tower_plasma.png-6c4277ea9427fade2f842c305b0946df.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
|
||||
|
After Width: | Height: | Size: 1.0 MiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bbba7b3ds0q1s"
|
||||
path="res://.godot/imported/tower_railgun.png-05e293f5b98edc1a6a0a91aff3e87037.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/tower_railgun.png"
|
||||
dest_files=["res://.godot/imported/tower_railgun.png-05e293f5b98edc1a6a0a91aff3e87037.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
|
||||
|
After Width: | Height: | Size: 2.8 MiB |
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c18bfpv8ad25r"
|
||||
path="res://.godot/imported/vestige_megastructure_bg.png-cdef57a31190ae339ecfafd3efc0bafe.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://assets/vestige_megastructure_bg.png"
|
||||
dest_files=["res://.godot/imported/vestige_megastructure_bg.png-cdef57a31190ae339ecfafd3efc0bafe.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
|
||||
@@ -24,8 +24,8 @@ application/modify_resources=true
|
||||
application/icon="res://assets/app_icon.png"
|
||||
application/console_wrapper_icon=""
|
||||
application/icon_interpolation=4
|
||||
application/file_version="0.1.0.3"
|
||||
application/product_version="0.1.0.3"
|
||||
application/file_version="0.1.0.4"
|
||||
application/product_version="0.1.0.4"
|
||||
application/company_name="GerGames"
|
||||
application/product_name="Void Bastion"
|
||||
application/file_description="Void Bastion – Alpha-Testversion"
|
||||
@@ -34,3 +34,90 @@ application/trademarks=""
|
||||
ssh_remote_deploy/enabled=false
|
||||
texture_format/s3tc_bptc=true
|
||||
texture_format/etc2_astc=false
|
||||
|
||||
[preset.1]
|
||||
|
||||
name="Android Test APK"
|
||||
platform="Android"
|
||||
runnable=true
|
||||
advanced_options=false
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=".mcp.json,*.md,tests/*,server/*,tools/*,packaging/*,addons/agent_tools/*,build/*,dist/*"
|
||||
export_path="build/android/Void-Bastion-Alpha.apk"
|
||||
patches=PackedStringArray()
|
||||
|
||||
[preset.1.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
gradle_build/use_gradle_build=true
|
||||
gradle_build/export_format=0
|
||||
gradle_build/min_sdk="24"
|
||||
architectures/arm64-v8a=true
|
||||
architectures/armeabi-v7a=true
|
||||
architectures/x86=false
|
||||
architectures/x86_64=false
|
||||
package/name="Void Bastion"
|
||||
package/unique_name="de.gergames.voidbastion"
|
||||
package/signed=true
|
||||
screen/immersive_mode=true
|
||||
permissions/internet=true
|
||||
|
||||
[preset.3]
|
||||
|
||||
name="iOS (Xcode Projekt)"
|
||||
platform="iOS"
|
||||
runnable=false
|
||||
advanced_options=false
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=".mcp.json,*.md,tests/*,server/*,tools/*,packaging/*,addons/agent_tools/*,build/*,dist/*"
|
||||
export_path="build/ios/Void-Bastion"
|
||||
patches=PackedStringArray()
|
||||
|
||||
[preset.3.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
application/app_store_team_id=""
|
||||
application/bundle_identifier="de.gergames.voidbastion"
|
||||
application/export_project_only=true
|
||||
application/min_ios_version="14.3"
|
||||
application/short_version="0.1.0"
|
||||
application/version="1.0"
|
||||
|
||||
[preset.2]
|
||||
|
||||
name="Android Studio Emulator x86_64"
|
||||
platform="Android"
|
||||
runnable=true
|
||||
advanced_options=false
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=".mcp.json,*.md,tests/*,server/*,tools/*,packaging/*,addons/agent_tools/*,build/*,dist/*"
|
||||
export_path="build/android/Void-Bastion-Emulator-x64.apk"
|
||||
patches=PackedStringArray()
|
||||
|
||||
[preset.2.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
gradle_build/use_gradle_build=true
|
||||
gradle_build/export_format=0
|
||||
gradle_build/min_sdk="24"
|
||||
architectures/arm64-v8a=false
|
||||
architectures/armeabi-v7a=false
|
||||
architectures/x86=false
|
||||
architectures/x86_64=true
|
||||
package/name="Void Bastion"
|
||||
package/unique_name="de.gergames.voidbastion"
|
||||
package/signed=true
|
||||
screen/immersive_mode=true
|
||||
permissions/internet=true
|
||||
@@ -24,3 +24,9 @@ script = ExtResource("5")
|
||||
|
||||
[node name="UpdateManager" type="Node" parent="."]
|
||||
script = ExtResource("6")
|
||||
|
||||
[node name="MusicPlayer" type="AudioStreamPlayer" parent="."]
|
||||
bus = &"Master"
|
||||
|
||||
[node name="UiSoundPlayer" type="AudioStreamPlayer" parent="."]
|
||||
bus = &"Master"
|
||||
@@ -422,7 +422,7 @@ func is_online() -> bool:
|
||||
|
||||
func make_leaderboard_id(map_index: int, difficulty_index: int, mode: String) -> String:
|
||||
var safe_mode := mode if mode in ["mission", "endless", "coop_ai", "coop_online"] else "mission"
|
||||
return "%s_map%d_difficulty%d" % [safe_mode, clampi(map_index, 0, 2), clampi(difficulty_index, 0, 2)]
|
||||
return "%s_map%d_difficulty%d" % [safe_mode, clampi(map_index, 0, 6), clampi(difficulty_index, 0, 2)]
|
||||
|
||||
|
||||
func _parse_rpc_result(result) -> Dictionary:
|
||||
|
||||
@@ -6,7 +6,7 @@ RequestExecutionLevel user
|
||||
!include "LogicLib.nsh"
|
||||
|
||||
!define PRODUCT_NAME "Void Bastion"
|
||||
!define PRODUCT_VERSION "0.1.0-alpha.3"
|
||||
!define PRODUCT_VERSION "0.1.0-alpha.4"
|
||||
!define PRODUCT_PUBLISHER "GerGames"
|
||||
!define PRODUCT_WEBSITE "https://void-bastion.gergames.de/"
|
||||
!define PRODUCT_EXE "Void-Bastion.exe"
|
||||
@@ -21,7 +21,7 @@ BrandingText "GerGames"
|
||||
ShowInstDetails show
|
||||
ShowUninstDetails show
|
||||
|
||||
VIProductVersion "0.1.0.3"
|
||||
VIProductVersion "0.1.0.4"
|
||||
VIAddVersionKey /LANG=1031 "ProductName" "${PRODUCT_NAME}"
|
||||
VIAddVersionKey /LANG=1031 "CompanyName" "${PRODUCT_PUBLISHER}"
|
||||
VIAddVersionKey /LANG=1031 "FileDescription" "${PRODUCT_NAME} Setup"
|
||||
|
||||
@@ -15,7 +15,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
||||
[application]
|
||||
|
||||
config/name="Void Bastion"
|
||||
config/version="0.1.0-alpha.3"
|
||||
config/version="0.1.0-alpha.4"
|
||||
run/main_scene="res://main.tscn"
|
||||
config/features=PackedStringArray("4.7")
|
||||
config/icon="res://assets/app_icon.png"
|
||||
@@ -29,10 +29,12 @@ Nakama="*res://addons/com.heroiclabs.nakama/Nakama.gd"
|
||||
|
||||
window/size/viewport_width=1600
|
||||
window/size/viewport_height=900
|
||||
window/size/mode=3
|
||||
window/size/window_width_override=1440
|
||||
window/size/window_height_override=810
|
||||
window/stretch/mode="canvas_items"
|
||||
window/stretch/aspect="expand"
|
||||
window/handheld/orientation=4
|
||||
|
||||
[editor_plugins]
|
||||
|
||||
@@ -42,3 +44,4 @@ enabled=PackedStringArray("res://addons/agent_tools/plugin.cfg", "res://addons/A
|
||||
|
||||
renderer/rendering_method="gl_compatibility"
|
||||
renderer/rendering_method.mobile="gl_compatibility"
|
||||
textures/vram_compression/import_etc2_astc=true
|
||||
@@ -1,5 +1,5 @@
|
||||
var GAME_VERSION = "0.23";
|
||||
var MAP_COUNT = 3;
|
||||
var MAP_COUNT = 7;
|
||||
var DIFFICULTY_COUNT = 3;
|
||||
var VALID_MODES = ["mission", "endless", "coop_ai", "coop_online"];
|
||||
var MATCH_MODULE = "void_bastion_coop";
|
||||
@@ -19,7 +19,10 @@ var PATHS = [LEFT_PATH, RIGHT_PATH];
|
||||
var TOWERS = [
|
||||
{cost:120,damage:22,range:190,cooldown:0.55},
|
||||
{cost:180,damage:12,range:165,cooldown:0.18},
|
||||
{cost:250,damage:65,range:260,cooldown:1.40}
|
||||
{cost:250,damage:65,range:260,cooldown:1.40},
|
||||
{cost:230,damage:48,range:240,cooldown:1.05},
|
||||
{cost:210,damage:34,range:215,cooldown:0.92},
|
||||
{cost:300,damage:110,range:330,cooldown:1.85}
|
||||
];
|
||||
var ENEMIES = [{hp:70,speed:105,reward:18},{hp:185,speed:72,reward:34},{hp:520,speed:43,reward:78}];
|
||||
var DIFFICULTIES = [
|
||||
@@ -335,12 +338,13 @@ function updateTowers(state,delta) {
|
||||
var t=state.towers[i]; t.cooldown-=delta; var range=towerRange(t),targetIndex=-1,best=-1;
|
||||
for (var j=0;j<state.enemies.length;j++) if (distanceSquared(t.x,t.y,state.enemies[j].x,state.enemies[j].y)<=range*range) { var p=enemyProgress(state.enemies[j]); if (p>best) { best=p; targetIndex=j; } }
|
||||
if (targetIndex<0) continue;
|
||||
var target=state.enemies[targetIndex]; t.angle=Math.atan2(target.y-t.y,target.x-t.x)+(t.type===0?Math.PI:0);
|
||||
var target=state.enemies[targetIndex]; t.angle=Math.atan2(target.y-t.y,target.x-t.x)+([0,3,5].indexOf(t.type)>=0?Math.PI:0);
|
||||
if (t.cooldown>0) continue;
|
||||
var damage=towerDamage(t);
|
||||
if (t.type===2) {
|
||||
if (t.type===2||t.type===3||t.type===4) {
|
||||
var radius=t.type===2?76:(t.type===3?92:118),splash=t.type===2?0.45:(t.type===3?0.62:0.38);
|
||||
var tx=target.x,ty=target.y,targetId=target.id;
|
||||
for (var k=state.enemies.length-1;k>=0;k--) if (distanceSquared(state.enemies[k].x,state.enemies[k].y,tx,ty)<=76*76) { state.enemies[k].hp-=state.enemies[k].id===targetId?damage:damage*0.45; if (state.enemies[k].hp<=0) removeEnemy(state,k,t.owner); }
|
||||
for (var k=state.enemies.length-1;k>=0;k--) if (distanceSquared(state.enemies[k].x,state.enemies[k].y,tx,ty)<=radius*radius) { state.enemies[k].hp-=state.enemies[k].id===targetId?damage:damage*splash; if (state.enemies[k].hp<=0) removeEnemy(state,k,t.owner); }
|
||||
} else { target.hp-=damage; if (target.hp<=0) removeEnemy(state,targetIndex,t.owner); }
|
||||
t.cooldown=towerCooldown(t);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,51 @@ func _run() -> void:
|
||||
var ads_manager = game.get_node("AdsManager")
|
||||
var account_manager = game.get_node("AccountManager")
|
||||
var multiplayer_manager = game.get_node("MultiplayerManager")
|
||||
assert(game.get("MAP_NAMES").size() == 7, "Es müssen sieben Karten vorhanden sein.")
|
||||
assert(game.get("MAP_TEXTURES").size() == 7, "Jede Karte benötigt einen eigenen Hintergrund.")
|
||||
assert(game.get("map_buttons").size() == 7, "Alle sieben Kartenvorschauen müssen gleichzeitig auswählbar sein.")
|
||||
game.set("unlocked_maps", [0, 1, 2])
|
||||
assert(game.get("TOWER_DATA").size() == 6, "Das Arsenal muss sechs unterschiedliche Turmtypen enthalten.")
|
||||
assert(game.get("tower_buttons").size() == 6, "Alle sechs Türme müssen gleichzeitig in der unteren Baumenüleiste sichtbar sein.")
|
||||
game.call("_handle_pointer", Vector2(760, 815), false)
|
||||
assert(game.get("game_mode") == "store", "Der interne Shop muss direkt aus dem Hauptmenü erreichbar sein.")
|
||||
await process_frame
|
||||
game.set("core_crystals", 24)
|
||||
game.call("_handle_pointer", game.get("store_map_tab_button").get_center(), false)
|
||||
assert(game.get("store_tab") == "maps", "Der Shop muss eine eigene Kartenseite haben.")
|
||||
game.call("_handle_pointer", game.get("map_shop_purchase_buttons")[0].get_center(), false)
|
||||
assert(game.get("unlocked_maps").has(3) and game.get("core_crystals") == 0, "Neue Karten müssen dauerhaft mit Kernkristallen freischaltbar sein.")
|
||||
game.set("unlocked_maps", [0, 1, 2])
|
||||
game.set("core_crystals", 0)
|
||||
game.call("_handle_pointer", Vector2(140, 60), false)
|
||||
assert(game.get("game_mode") == "menu", "Der Shop benötigt einen funktionierenden Zurück-Button.")
|
||||
game.call("_handle_pointer", game.get("map_buttons")[3].get_center(), false)
|
||||
assert(game.get("game_mode") == "store" and game.get("store_tab") == "maps", "Eine gesperrte Karte muss direkt den Karten-Shop öffnen.")
|
||||
game.set("core_crystals", 24)
|
||||
game.call("_handle_pointer", game.get("map_shop_purchase_buttons")[0].get_center(), false)
|
||||
assert(game.get("unlocked_maps").has(3), "Die erste zusätzliche Karte muss kaufbar sein.")
|
||||
game.call("_handle_pointer", game.get("shop_back_button").get_center(), false)
|
||||
assert(game.get("game_mode") == "menu", "Nach dem Kartenkauf muss die Kartenauswahl erreichbar bleiben.")
|
||||
game.call("_handle_pointer", game.get("map_buttons")[3].get_center(), false)
|
||||
assert(game.get("menu_step") == "difficulty" and game.get("selected_map") == 3, "Eine freigeschaltete Karte muss danach spielbar sein.")
|
||||
game.call("_handle_pointer", game.get("map_step_back_button").get_center(), false)
|
||||
game.set("unlocked_maps", [0, 1, 2])
|
||||
game.set("core_crystals", 0)
|
||||
game.call("_select_map", 0)
|
||||
game.call("_handle_pointer", game.get("start_button").get_center(), false)
|
||||
assert(game.get("game_mode") == "menu", "Die Mission darf erst nach Auswahl einer Karte gestartet werden.")
|
||||
var menu_map_index: int = game.get("selected_map")
|
||||
game.call("_handle_pointer", game.get("map_buttons")[menu_map_index].get_center(), false)
|
||||
assert(game.get("menu_step") == "difficulty", "Nach Auswahl einer Karte muss die Schwierigkeitsauswahl erscheinen.")
|
||||
game.call("_handle_pointer", game.get("map_step_back_button").get_center(), false)
|
||||
assert(game.get("menu_step") == "map", "Die Missionsvorbereitung benötigt eine Rückkehr zur Kartenauswahl.")
|
||||
game.set("unlocked_towers", [0, 1, 2])
|
||||
game.set("game_mode", "playing")
|
||||
game.call("_handle_pointer", game.get("tower_buttons")[3].get_center(), false)
|
||||
assert(game.get("game_mode") == "store", "Ein gesperrter Turm muss den Shop auch aus einer laufenden Mission öffnen.")
|
||||
game.call("_handle_pointer", game.get("shop_back_button").get_center(), false)
|
||||
assert(game.get("game_mode") == "playing", "Der Shop-Zurück-Button muss in die laufende Mission zurückführen.")
|
||||
game.set("game_mode", "menu")
|
||||
multiplayer_manager.reset_coop_session(500, 30)
|
||||
assert(multiplayer_manager.player_credits == [500, 500], "Beide Koop-Spieler benötigen getrennte, gleich hohe Startressourcen.")
|
||||
assert(multiplayer_manager.spend_player_credits(0, 120), "Spieler 1 muss aus seinem eigenen Guthaben bauen können.")
|
||||
@@ -42,7 +87,7 @@ func _run() -> void:
|
||||
assert(multiplayer_manager.matchmaking_state == "waiting_for_service", "Die Spielersuche benötigt einen sichtbaren vorbereiteten Zustand.")
|
||||
multiplayer_manager.cancel_search()
|
||||
assert(multiplayer_manager.matchmaking_state == "idle", "Eine laufende Spielersuche muss abgebrochen werden können.")
|
||||
game.call("_handle_pointer", Vector2(1100, 730), false)
|
||||
game.call("_handle_pointer", Vector2(500, 815), false)
|
||||
assert(game.get("game_mode") == "multiplayer_menu", "Der Multiplayerbereich muss direkt aus dem Hauptmenü erreichbar sein.")
|
||||
game.call("_handle_pointer", Vector2(400, 695), false)
|
||||
assert(game.get("game_mode") == "matchmaking", "Schnelles Spiel muss in den vorbereiteten Suchzustand wechseln.")
|
||||
@@ -51,7 +96,7 @@ func _run() -> void:
|
||||
game.call("_handle_pointer", Vector2(140, 60), false)
|
||||
assert(game.get("game_mode") == "menu", "Der Multiplayerbereich benötigt einen funktionierenden Zurück-Button.")
|
||||
game.set("selected_difficulty", 0)
|
||||
game.call("_handle_pointer", Vector2(1100, 730), false)
|
||||
game.call("_handle_pointer", Vector2(500, 815), false)
|
||||
game.call("_handle_pointer", Vector2(1300, 695), false)
|
||||
assert(game.get("game_mode") == "playing" and game.get("coop_mode") and game.get("coop_ai_mode"), "Der KI-Koop-Modus muss direkt spielbar gestartet werden können.")
|
||||
assert(game.get("coop_paths").size() == 2, "Die Koop-Arena benötigt zwei gespiegelte Gegnerwege.")
|
||||
@@ -60,6 +105,7 @@ func _run() -> void:
|
||||
assert(game.get("towers").size() >= 2, "Der Computergegner muss selbstständig eine Startverteidigung bauen.")
|
||||
for ai_tower in game.get("towers"):
|
||||
assert(int(ai_tower.get("owner", -1)) == 1 and ai_tower.pos.x > 1480.0, "KI-Türme dürfen nur auf der vollständigen gespiegelten KI-Seite stehen.")
|
||||
assert(game.call("_distance_to_route", ai_tower.pos, game.get("coop_paths")[1]) <= 200.0, "KI-Türme müssen innerhalb ihrer effektiven Reichweite am Gegnerweg stehen.")
|
||||
assert(multiplayer_manager.player_credits[0] > multiplayer_manager.player_credits[1], "Die KI muss ihre Baukosten ausschließlich aus dem eigenen Guthaben bezahlen.")
|
||||
assert(game.call("_is_valid_build_position", Vector2(440, 200), 0), "Der Spieler benötigt gültige Bauplätze auf der linken Kartenhälfte.")
|
||||
assert(game.call("_is_valid_build_position", Vector2(1240, 200), 0), "Die eigene Koop-Seite muss nahezu die volle Breite einer Einzelspielerkarte bieten.")
|
||||
@@ -77,11 +123,11 @@ func _run() -> void:
|
||||
game.call("_process", 0.1)
|
||||
assert(game.get("next_enemy_id") > 3, "Beide Koop-Routen müssen auch während der laufenden Simulation Gegner erzeugen.")
|
||||
game.call("_return_to_menu")
|
||||
game.call("_handle_pointer", Vector2(400, 735), false)
|
||||
game.call("_handle_pointer", Vector2(225, 815), false)
|
||||
assert(game.get("game_mode") == "leaderboard", "Die globale Bestenliste muss direkt aus dem Hauptmenü erreichbar sein.")
|
||||
var leaderboard_map_before: int = game.get("leaderboard_map")
|
||||
game.call("_handle_pointer", Vector2(750, 175), false)
|
||||
assert(game.get("leaderboard_map") == wrapi(leaderboard_map_before + 1, 0, 3), "Die Rangliste muss direkt nach Karte filterbar sein.")
|
||||
assert(game.get("leaderboard_map") == wrapi(leaderboard_map_before + 1, 0, game.get("MAP_NAMES").size()), "Die Rangliste muss direkt nach Karte filterbar sein.")
|
||||
var leaderboard_difficulty_before: int = game.get("leaderboard_difficulty")
|
||||
game.call("_handle_pointer", Vector2(1225, 175), false)
|
||||
assert(game.get("leaderboard_difficulty") == wrapi(leaderboard_difficulty_before + 1, 0, 3), "Die Rangliste muss direkt nach Schwierigkeitsgrad filterbar sein.")
|
||||
@@ -89,10 +135,10 @@ func _run() -> void:
|
||||
assert(game.get("game_mode") == "menu", "Die Bestenliste benötigt einen funktionierenden Zurück-Button.")
|
||||
assert(not account_manager.player_id.is_empty(), "Beim ersten Start muss automatisch ein stabiles Gastprofil angelegt werden.")
|
||||
assert(not account_manager.display_name.is_empty(), "Das Gastprofil benötigt einen sichtbaren Pilotennamen.")
|
||||
assert(game.call("_app_version") == "0.1.0-alpha.3", "Profil, Installer und Online-Übertragung müssen dieselbe Alpha-Version verwenden.")
|
||||
assert(game.call("_app_version") == "0.1.0-alpha.4", "Profil, Installer und Online-Übertragung müssen dieselbe Alpha-Version verwenden.")
|
||||
var support_url: String = game.call("_support_email_url")
|
||||
assert(support_url.begins_with("mailto:support@gergames.de?"), "Die Feedback-Schaltfläche muss die öffentliche Supportadresse verwenden.")
|
||||
assert(support_url.contains("0.1.0-alpha.3") and support_url.contains(account_manager.short_id()), "Die Support-Mail muss Version und gekürzte Spieler-ID zur Fehlerzuordnung enthalten.")
|
||||
assert(support_url.contains("0.1.0-alpha.4") and support_url.contains(account_manager.short_id()), "Die Support-Mail muss Version und gekürzte Spieler-ID zur Fehlerzuordnung enthalten.")
|
||||
account_manager.storage_enabled = false
|
||||
var original_display_name: String = account_manager.display_name
|
||||
assert(account_manager.validate_display_name("Kommandant Ägir").is_empty(), "Ein normaler internationaler Spielername muss gültig sein.")
|
||||
@@ -105,23 +151,15 @@ func _run() -> void:
|
||||
assert(account_manager.record_run(0, 0, false, 3, previous_test_score + 1000), "Ein neuer lokaler Highscore muss gespeichert werden.")
|
||||
assert(account_manager.get_map_best_wave(0, false) >= 3, "Die Profilansicht muss den gespeicherten Missionsbestwert lesen können.")
|
||||
assert(not ads_manager.mobile_ads_supported, "Auf Windows müssen Anzeigen vollständig deaktiviert bleiben.")
|
||||
ads_manager.missions_since_interstitial = 0
|
||||
ads_manager.last_interstitial_time = 0
|
||||
assert(not ads_manager.is_interstitial_due(1000), "Vor drei Missionen darf keine Vollbildwerbung fällig sein.")
|
||||
ads_manager.missions_since_interstitial = 3
|
||||
assert(ads_manager.is_interstitial_due(1000), "Frühestens nach drei Missionen darf eine Vollbildwerbung fällig sein.")
|
||||
ads_manager.last_interstitial_time = 800
|
||||
assert(not ads_manager.is_interstitial_due(1000), "Die 15-Minuten-Sperre zwischen Vollbildanzeigen muss greifen.")
|
||||
ads_manager.last_interstitial_time = 0
|
||||
var update_manager = game.get_node("UpdateManager")
|
||||
assert(update_manager.call("_is_newer_version", "0.1.0-alpha.3", "0.1.0-alpha.2"), "Eine neuere Alpha-Version muss als Update erkannt werden.")
|
||||
assert(update_manager.call("_is_newer_version", "0.1.0-alpha.4", "0.1.0-alpha.3"), "Eine neuere Alpha-Version muss als Update erkannt werden.")
|
||||
assert(update_manager.call("_is_newer_version", "0.1.0", "0.1.0-alpha.2"), "Eine stabile Version muss neuer als ihre Alpha-Version sein.")
|
||||
assert(not update_manager.call("_is_newer_version", "0.1.0-alpha.3", "0.1.0-alpha.3"), "Die installierte Version darf nicht als Update angeboten werden.")
|
||||
assert(not update_manager.call("_is_newer_version", "0.1.0-alpha.4", "0.1.0-alpha.4"), "Die installierte Version darf nicht als Update angeboten werden.")
|
||||
var announced_update := {}
|
||||
update_manager.update_available.connect(func(info: Dictionary): announced_update.merge(info, true))
|
||||
var fake_release_body := JSON.stringify([{"tag_name": "v0.1.0-alpha.4", "name": "Alpha.4", "draft": false, "assets": [{"name": "Void-Bastion-Setup-x64.exe", "browser_download_url": "https://git.dominicspringer.de/public-test.exe"}]}]).to_utf8_buffer()
|
||||
var fake_release_body := JSON.stringify([{"tag_name": "v0.1.0-alpha.5", "name": "Alpha.5", "draft": false, "assets": [{"name": "Void-Bastion-Setup-x64.exe", "browser_download_url": "https://git.dominicspringer.de/public-test.exe"}]}]).to_utf8_buffer()
|
||||
update_manager.call("_on_request_completed", HTTPRequest.RESULT_SUCCESS, 200, PackedStringArray(), fake_release_body)
|
||||
assert(announced_update.get("version") == "0.1.0-alpha.4", "Die Update-Prüfung muss eine neuere öffentliche Version ankündigen.")
|
||||
assert(announced_update.get("version") == "0.1.0-alpha.5", "Die Update-Prüfung muss eine neuere öffentliche Version ankündigen.")
|
||||
assert(str(announced_update.get("url", "")).begins_with("https://git.dominicspringer.de/"), "Der Update-Hinweis darf nur einen öffentlichen Download-Link öffnen.")
|
||||
assert(game.get("path_points").size() == 9, "Bastion Prime sollte mit einer eigenen Einfahrt im Reaktor enden.")
|
||||
_assert_grid_aligned_path(game.get("path_points"), "Bastion Prime")
|
||||
@@ -149,15 +187,29 @@ func _run() -> void:
|
||||
assert(not game.call("_is_valid_build_position", Vector2(600, 680)), "Auf der unteren Helios-Fahrbahn darf nicht gebaut werden.")
|
||||
assert(game.call("_is_in_reactor_zone", Vector2(1480, 440)), "Der Helios-Reaktor muss als Sperrzone erkannt werden.")
|
||||
assert(not game.call("_is_valid_build_position", Vector2(1480, 440)), "Auf dem Helios-Reaktor darf nicht gebaut werden.")
|
||||
for map_index in game.get("MAP_NAMES").size():
|
||||
game.call("_select_map", map_index)
|
||||
var current_path: PackedVector2Array = game.get("path_points")
|
||||
_assert_grid_aligned_path(current_path, str(game.get("MAP_NAMES")[map_index]))
|
||||
assert(game.call("_validate_route_ends_at_reactor", current_path, map_index), "Jede neue Karte muss exakt am sichtbaren Reaktor enden.")
|
||||
assert(current_path.size() >= 7, "Jede Karte braucht einen ausreichend langen, gestalteten Laufweg.")
|
||||
assert(game.call("_is_in_reactor_zone", current_path[-1]), "Der Zielpunkt muss in der Reaktorsperrzone liegen.")
|
||||
for cell in game.get("path_cells"):
|
||||
var cell_position: Vector2i = cell
|
||||
var neighbors := 0
|
||||
for direction in [Vector2i.LEFT, Vector2i.RIGHT, Vector2i.UP, Vector2i.DOWN]:
|
||||
if game.get("path_cells").has(cell_position + direction):
|
||||
neighbors += 1
|
||||
assert(neighbors <= 2, "%s hat an Rasterzelle %s eine Wegkreuzung." % [game.get("MAP_NAMES")[map_index], cell_position])
|
||||
game.call("_select_map", 0)
|
||||
assert(game.call("_is_in_reactor_zone", Vector2(1480, 440)), "Der Bastion-Reaktor muss als Sperrzone erkannt werden.")
|
||||
assert(not game.call("_is_valid_build_position", Vector2(1480, 440)), "Auf dem Bastion-Reaktor darf nicht gebaut werden.")
|
||||
assert(not game.call("_is_valid_build_position", Vector2(40, 680)), "Im freien Weltraum darf kein Bauplatz liegen.")
|
||||
game.call("_select_map", 2)
|
||||
assert(game.get("path_points").size() == 8, "Pyrax sollte eine klar sichtbare Reaktoreinfahrt besitzen.")
|
||||
assert(game.get("path_points").size() == 7, "Pyrax sollte ohne die beiden unnötigen letzten Bögen in den Reaktor führen.")
|
||||
_assert_grid_aligned_path(game.get("path_points"), "Pyrax-Raffinerie")
|
||||
assert(game.call("_validate_route_ends_at_reactor", game.get("path_points"), 2), "Die Pyrax-Route muss exakt im Reaktormittelpunkt enden.")
|
||||
assert(_path_length(game.get("path_points")) >= 2480.0, "Die Pyrax-Route sollte den Bauraum weiträumig erschließen.")
|
||||
assert(_path_length(game.get("path_points")) >= 2300.0, "Die Pyrax-Route sollte den Bauraum weiträumig erschließen.")
|
||||
assert(game.call("_is_in_reactor_zone", Vector2(1440, 360)), "Der Pyrax-Hauptreaktor muss als Sperrzone erkannt werden.")
|
||||
assert(game.call("_is_in_reactor_zone", Vector2(680, 360)), "Auch die offene Pyrax-Reaktorkammer muss gesperrt sein.")
|
||||
assert(not game.call("_is_valid_build_position", Vector2(1440, 360)), "Auf dem Pyrax-Reaktor darf nicht gebaut werden.")
|
||||
@@ -170,6 +222,15 @@ func _run() -> void:
|
||||
game.set("selected_difficulty", 0)
|
||||
game.call("_start_game")
|
||||
assert(game.get("credits") == 450 and game.get("reactor_lives") == 20, "Leicht muss dem bisherigen Balancing entsprechen.")
|
||||
game.call("_handle_pointer", Vector2(1180, 45), false)
|
||||
assert(game.get("speed_dropdown_open"), "Die Spielgeschwindigkeit muss über eine Auswahl bedienbar sein.")
|
||||
game.call("_handle_pointer", Vector2(1190, 198), false)
|
||||
assert(is_equal_approx(game.get("game_speed"), 5.0), "Die Geschwindigkeitsauswahl muss bis 5× reichen.")
|
||||
game.set("game_speed", 1.0)
|
||||
game.call("_handle_pointer", Vector2(1330, 82), false)
|
||||
assert(game.get("auto_start_waves"), "Der automatische Wellenstart muss per Checkbox aktivierbar sein.")
|
||||
game.call("_handle_pointer", Vector2(1330, 82), false)
|
||||
assert(not game.get("auto_start_waves"), "Der automatische Wellenstart muss per Checkbox abschaltbar sein.")
|
||||
game.call("_handle_pointer", Vector2(1110, 45), false)
|
||||
assert(game.get("game_mode") == "menu", "Der Zurück-Button muss eine laufende Runde abbrechen und zum Kartenmenü führen.")
|
||||
assert(game.get("towers").is_empty() and game.get("enemies").is_empty(), "Beim Abbruch müssen alle Einheiten der laufenden Runde entfernt werden.")
|
||||
@@ -207,6 +268,28 @@ func _run() -> void:
|
||||
game.set("enemies_to_spawn", 2)
|
||||
game.call("_spawn_enemy")
|
||||
assert(game.get("enemies")[-1].max_hp > 100.0, "Schwer muss die Gegnerhülle deutlich verstärken.")
|
||||
assert(game.call("_enemy_health_scale", 4, false) > game.call("_enemy_health_scale", 3, false), "Die Gegnerhülle muss ab Welle 4 stärker skalieren.")
|
||||
game.set("wave", 3)
|
||||
game.call("_start_wave")
|
||||
assert(game.get("enemies_to_spawn") > 15, "Ab Welle 4 muss die Gegnerzahl merklich steigen.")
|
||||
game.get("enemies").clear()
|
||||
var shielded_enemy := {"hp": 1000.0, "max_hp": 1000.0, "flash": 0.0, "id": 98001, "type": 0, "pos": Vector2.ZERO, "speed": 1.0, "reward": 1, "boss": false, "size_scale": 1.0, "shield": true}
|
||||
game.get("enemies").append(shielded_enemy)
|
||||
game.call("_apply_damage", shielded_enemy, 100.0, 0, 0)
|
||||
assert(is_equal_approx(shielded_enemy.hp, 972.0), "Schilde müssen kinetischen Schaden deutlich reduzieren, aber nicht vollständig blockieren.")
|
||||
game.call("_apply_damage", shielded_enemy, 100.0, 0, 1)
|
||||
assert(is_equal_approx(shielded_enemy.hp, 872.0), "Laser müssen Schildgegner effektiv beschädigen.")
|
||||
game.get("enemies").clear()
|
||||
game.set("wave", 5)
|
||||
game.set("reactor_lives", 12)
|
||||
game.set("credits", 300)
|
||||
game.set("towers_built_this_run", 4)
|
||||
var lean_score: int = game.call("_calculate_run_score")
|
||||
game.set("towers_built_this_run", 14)
|
||||
assert(game.call("_calculate_run_score") < lean_score, "Zusätzliche Bauvorgänge müssen den Effizienz-Multiplikator senken, auch wenn Türme später verkauft werden.")
|
||||
game.set("towers_built_this_run", 4)
|
||||
game.set("credits", 500)
|
||||
assert(game.call("_calculate_run_score") > lean_score, "Verbleibende Energie muss die Highscore-Punkte erhöhen.")
|
||||
game.set("selected_difficulty", 0)
|
||||
game.call("_start_game")
|
||||
game.set("wave", 4)
|
||||
@@ -225,6 +308,10 @@ func _run() -> void:
|
||||
game.call("_update_wave", 0.1)
|
||||
assert(game.get("game_mode") == "endless_offer", "Nach Welle 15 muss die Wahl zum Endlosmodus erscheinen.")
|
||||
assert(game.get("core_crystals") == crystals_before + 5, "Eine abgeschlossene Karte muss Kernkristalle vergeben.")
|
||||
assert(not game.get("victory_reward_pending"), "Unter Windows ohne Anzeigen muss die Missionsbelohnung nicht blockiert werden.")
|
||||
game.call("_mission_complete")
|
||||
game.call("_handle_pointer", game.get("start_button").get_center(), false)
|
||||
assert(game.get("game_mode") == "menu", "Der Abschluss einer 15-Wellen-Mission muss aus dem Siegbildschirm zurück ins Hauptmenü führen, nicht neu starten.")
|
||||
game.call("_start_endless_mode")
|
||||
assert(game.get("endless_mode") and game.get("game_mode") == "playing", "Der Endlosmodus muss freiwillig startbar sein.")
|
||||
game.call("_game_over")
|
||||
|
||||