Release alpha 14 menu and developer test tools

This commit is contained in:
Emporex committed 2026-10-11 20:21:59 +02:00
1 parent 026d0bad58
commit a536b698f7
5 files changed
+464 -76

No files matched your search

+377 -49
View File
@@ -3,7 +3,9 @@ extends Node2D
const SCREEN := Vector2(1600.0, 900.0) const SCREEN := Vector2(1600.0, 900.0)
const ENGLISH_UI := { const ENGLISH_UI := {
"PROFIL": "PROFILE", "EINSATZKARTE WÄHLEN • GESPERRTE SEKTOREN IM SHOP FREISCHALTEN": "SELECT A MAP • UNLOCK SECTORS IN THE SHOP", "PROFIL": "PROFILE", "EINSATZKARTE WÄHLEN • GESPERRTE SEKTOREN IM SHOP FREISCHALTEN": "SELECT A MAP • UNLOCK SECTORS IN THE SHOP",
"GESPERRT": "LOCKED", "Wähle einen Sektor, um Schwierigkeit und Missionsstart festzulegen.": "Select a sector to choose difficulty and start the mission.", "EINSATZSEKTOREN": "DEPLOYMENT SECTORS", "KARTE WÄHLEN • GESPERRTE SEKTOREN IM SHOP FREISCHALTEN": "CHOOSE A MAP • UNLOCK LOCKED SECTORS IN THE SHOP",
"AUSGEWÄHLT": "SELECTED", "KOMMUNITY": "COMMUNITY", "HANGAR & KONTO": "HANGAR & ACCOUNT", "SYSTEM": "SYSTEM",
"GESPERRT": "LOCKED", "Wähle einen Sektor. Danach legst du Schwierigkeit und Missionsstart fest.": "Choose a sector, then set the difficulty and start your mission.",
"< KARTE": "< MAP", "SCHWIERIGKEIT WÄHLEN": "SELECT DIFFICULTY", "LEICHT": "EASY", "MITTEL": "MEDIUM", "SCHWER": "HARD", "< KARTE": "< MAP", "SCHWIERIGKEIT WÄHLEN": "SELECT DIFFICULTY", "LEICHT": "EASY", "MITTEL": "MEDIUM", "SCHWER": "HARD",
"MISSION STARTEN": "START MISSION", "BESTENLISTE": "LEADERBOARD", "ARSENAL / SHOP": "ARSENAL / SHOP", "EINSTELLUNGEN": "SETTINGS", "MISSION STARTEN": "START MISSION", "BESTENLISTE": "LEADERBOARD", "ARSENAL / SHOP": "ARSENAL / SHOP", "EINSTELLUNGEN": "SETTINGS",
"CODE EINLÖSEN": "REDEEM CODE", "SPIEL BEENDEN": "QUIT GAME", "MULTIPLAYER": "MULTIPLAYER", "SCHLIESSEN": "CLOSE", "CODE EINLÖSEN": "REDEEM CODE", "SPIEL BEENDEN": "QUIT GAME", "MULTIPLAYER": "MULTIPLAYER", "SCHLIESSEN": "CLOSE",
@@ -33,6 +35,7 @@ const ENGLISH_UI := {
"Biodom über dem Nebel": "Biodome above the nebula", "Eisige Raffinerie": "Frozen refinery", "Schmiede am Gasriesen": "Forge by the gas giant", "Biodom über dem Nebel": "Biodome above the nebula", "Eisige Raffinerie": "Frozen refinery", "Schmiede am Gasriesen": "Forge by the gas giant",
"Verlassene Megastruktur": "Abandoned megastructure", "VERKAUFEN": "SELL", "MAX. STUFE": "MAX LEVEL", "FREIGESCHALTET": "UNLOCKED", "Verlassene Megastruktur": "Abandoned megastructure", "VERKAUFEN": "SELL", "MAX. STUFE": "MAX LEVEL", "FREIGESCHALTET": "UNLOCKED",
"GESPERRT • %d KERNKRISTALLE": "LOCKED • %d CORE CRYSTALS", "KARTENBONUS %d • BESTE WELLE %02d • KERNKRISTALLE %04d": "MAP BONUS %d • BEST WAVE %02d • CORE CRYSTALS %04d", "GESPERRT • %d KERNKRISTALLE": "LOCKED • %d CORE CRYSTALS", "KARTENBONUS %d • BESTE WELLE %02d • KERNKRISTALLE %04d": "MAP BONUS %d • BEST WAVE %02d • CORE CRYSTALS %04d",
"KARTE %02d VON %02d": "MAP %02d OF %02d", "KARTE IM SHOP FREISCHALTEN": "UNLOCK MAP IN SHOP",
"Bisheriges Balancing • 20 Reaktor • Kartenbonus 5": "Classic balance • 20 reactor health • map bonus 5", "Bisheriges Balancing • 20 Reaktor • Kartenbonus 5": "Classic balance • 20 reactor health • map bonus 5",
"+32 % Gegnerhülle • +24 % Gegner • Kartenbonus 7": "+32% enemy armor • +24% enemies • map bonus 7", "+32 % Gegnerhülle • +24 % Gegner • Kartenbonus 7": "+32% enemy armor • +24% enemies • map bonus 7",
"+72 % Grundhülle • stärkere Spätwellen • Start 340 • Kartenbonus 12": "+72% base armor • stronger late waves • start 340 • map bonus 12", "+72 % Grundhülle • stärkere Spätwellen • Start 340 • Kartenbonus 12": "+72% base armor • stronger late waves • start 340 • map bonus 12",
@@ -195,17 +198,18 @@ const DIFFICULTY_DESCRIPTIONS := [
"+72 % Grundhülle • stärkere Spätwellen • Start 340 • Kartenbonus 12" "+72 % Grundhülle • stärkere Spätwellen • Start 340 • Kartenbonus 12"
] ]
const TOWER_DATA := [ var TOWER_DATA := [
{"name": "KINETIK", "cost": 120, "damage": 22.0, "range": 190.0, "rate": 0.55, "shot_speed": 620.0, "damage_type": "EINZELZIEL", "target_class": "ground", "color": Color("7de7ff"), "unlock_cost": 0, "description": "Zuverlässig gegen Bodenziele."}, {"name": "KINETIK", "cost": 120, "damage": 22.0, "range": 190.0, "rate": 0.55, "shot_speed": 620.0, "damage_type": "EINZELZIEL", "target_class": "ground", "color": Color("7de7ff"), "unlock_cost": 0, "description": "Zuverlässig gegen Bodenziele."},
{"name": "LASER", "cost": 180, "damage": 12.0, "range": 165.0, "rate": 0.18, "shot_speed": 1100.0, "damage_type": "EINZELZIEL", "target_class": "both", "both_damage_scale": 0.82, "color": Color("62fff1"), "unlock_cost": 0, "description": "Mehrzwecklaser; durchdringt Schilde von Luft- und Bodenzielen."}, {"name": "LASER", "cost": 180, "damage": 12.0, "range": 165.0, "rate": 0.18, "shot_speed": 1100.0, "damage_type": "EINZELZIEL", "target_class": "both", "both_damage_scale": 0.82, "color": Color("62fff1"), "unlock_cost": 0, "description": "Mehrzwecklaser; durchdringt Schilde von Luft- und Bodenzielen."},
{"name": "RAKETE", "cost": 250, "damage": 65.0, "range": 260.0, "rate": 1.40, "shot_speed": 360.0, "damage_type": "FLÄCHE 76", "target_class": "ground", "color": Color("ffb35a"), "unlock_cost": 0, "description": "Bodenraketen treffen Gegnergruppen."}, {"name": "RAKETE", "cost": 250, "damage": 65.0, "range": 260.0, "rate": 1.40, "shot_speed": 360.0, "damage_type": "FLÄCHE 76", "target_class": "ground", "color": Color("ffb35a"), "unlock_cost": 0, "description": "Bodenraketen treffen Gegnergruppen."},
{"name": "PLASMA", "cost": 230, "damage": 34.0, "range": 240.0, "rate": 1.05, "shot_speed": 480.0, "damage_type": "PLASMA-FLÄCHE 92", "target_class": "both", "both_damage_scale": 0.72, "color": Color("e27bff"), "unlock_cost": 18, "description": "Mehrzweckwaffe mit reduziertem Schaden."}, {"name": "PLASMA", "cost": 230, "damage": 34.0, "range": 240.0, "rate": 1.05, "shot_speed": 480.0, "damage_type": "PLASMA-FLÄCHE 92", "target_class": "both", "both_damage_scale": 0.72, "color": Color("e27bff"), "unlock_cost": 18, "description": "Mehrzweckwaffe mit reduziertem Schaden."},
{"name": "EMP-PULS", "cost": 210, "damage": 20.0, "range": 215.0, "rate": 0.92, "shot_speed": 540.0, "damage_type": "EMP-FLÄCHE 118 · ALLE", "target_class": "both", "both_damage_scale": 1.0, "color": Color("77e8ff"), "unlock_cost": 24, "description": "Impuls trifft alle Luft- und Bodenziele im Radius mit 20 Schaden."}, {"name": "EMP-PULS", "cost": 210, "damage": 20.0, "range": 215.0, "rate": 0.92, "shot_speed": 540.0, "damage_type": "EMP-FLÄCHE 118 · ALLE", "target_class": "both", "both_damage_scale": 1.0, "color": Color("77e8ff"), "unlock_cost": 24, "description": "Impuls trifft alle Luft- und Bodenziele im Radius mit 20 Schaden."},
{"name": "RAILGUN", "cost": 300, "damage": 77.0, "range": 330.0, "rate": 1.85, "shot_speed": 1450.0, "damage_type": "PIERCING-STRAHL", "target_class": "both", "both_damage_scale": 0.70, "color": Color("c86bff"), "unlock_cost": 30, "description": "Lila Durchschlagsstrahl trifft alle Gegner auf seiner Schusslinie."}, {"name": "RAILGUN", "cost": 300, "damage": 77.0, "range": 330.0, "rate": 1.85, "shot_speed": 1450.0, "damage_type": "PIERCING-STRAHL", "target_class": "both", "both_damage_scale": 0.70, "color": Color("c86bff"), "unlock_cost": 30, "description": "Lila Durchschlagsstrahl trifft alle Gegner auf seiner Schusslinie."},
{"name": "FLAK", "cost": 210, "damage": 22.0, "range": 230.0, "rate": 0.42, "shot_speed": 920.0, "damage_type": "LUFT-EINZELZIEL", "target_class": "air", "color": Color("ff737c"), "unlock_cost": 0, "description": "Schnelle Abfangkanone gegen gegnerische Luftschiffe."} {"name": "FLAK", "cost": 210, "damage": 22.0, "range": 230.0, "rate": 0.42, "shot_speed": 920.0, "damage_type": "LUFT-EINZELZIEL", "target_class": "air", "color": Color("ff737c"), "unlock_cost": 0, "description": "Schnelle Abfangkanone gegen gegnerische Luftschiffe."},
{"name": "ADMIN-TURM", "cost": 0, "damage": 1000000.0, "range": 3000.0, "rate": 0.08, "shot_speed": 3000.0, "damage_type": "TESTMODUS", "target_class": "both", "color": Color("fff2a1"), "unlock_cost": 0, "description": "Entwicklertest: besiegt jedes Ziel mit einem Schuss."}
] ]
const ENEMY_DATA := [ var ENEMY_DATA := [
{"name": "JAEGER", "category": "air", "hp": 70.0, "speed": 105.0, "reward": 18, "size": 58.0, "breach_damage": 1, "shield_resistance": 0.28}, {"name": "JAEGER", "category": "air", "hp": 70.0, "speed": 105.0, "reward": 18, "size": 58.0, "breach_damage": 1, "shield_resistance": 0.28},
{"name": "FREGATTE", "category": "air", "hp": 185.0, "speed": 72.0, "reward": 34, "size": 78.0, "breach_damage": 2, "shield_resistance": 0.28}, {"name": "FREGATTE", "category": "air", "hp": 185.0, "speed": 72.0, "reward": 34, "size": 78.0, "breach_damage": 2, "shield_resistance": 0.28},
{"name": "KREUZER", "category": "air", "hp": 520.0, "speed": 43.0, "reward": 78, "size": 112.0, "breach_damage": 3, "shield_resistance": 0.28}, {"name": "KREUZER", "category": "air", "hp": 520.0, "speed": 43.0, "reward": 78, "size": 112.0, "breach_damage": 3, "shield_resistance": 0.28},
@@ -267,6 +271,17 @@ var wave_spawn_index := 0
var online_wave_types_remaining: Array[int] = [] var online_wave_types_remaining: Array[int] = []
var enemy_strength_multiplier := 1.0 var enemy_strength_multiplier := 1.0
var developer_panel_open := false var developer_panel_open := false
const DEVELOPER_PASSWORD := "Spring.3"
const ADMIN_TOWER_TYPE := 7
const DEVELOPER_AUTH_OP := 15
const DEVELOPER_ENERGY_OP := 16
var developer_unlocked_this_round := false
var developer_server_authorized := false
var developer_password_prompt_open := false
var developer_password_line_edit: LineEdit
var developer_password_dialog_rect := Rect2(510, 330, 580, 235)
var developer_password_input_rect := Rect2(570, 410, 460, 54)
var developer_password_cancel_button := Rect2(670, 488, 260, 48)
var status_text := "" var status_text := ""
var status_timer := 0.0 var status_timer := 0.0
var elapsed := 0.0 var elapsed := 0.0
@@ -283,8 +298,8 @@ var next_spawn_route := 0
var start_button := Rect2(625, 674, 350, 58) var start_button := Rect2(625, 674, 350, 58)
var map_buttons := [ var map_buttons := [
Rect2(30, 390, 340, 130), Rect2(430, 390, 340, 130), Rect2(830, 390, 340, 130), Rect2(1230, 390, 340, 130), Rect2(55, 312, 350, 150), Rect2(435, 312, 350, 150), Rect2(815, 312, 350, 150), Rect2(1195, 312, 350, 150),
Rect2(255, 540, 340, 130), Rect2(630, 540, 340, 130), Rect2(1005, 540, 340, 130) Rect2(245, 492, 350, 150), Rect2(625, 492, 350, 150), Rect2(1005, 492, 350, 150)
] ]
var difficulty_buttons := [Rect2(535, 520, 165, 50), Rect2(718, 520, 165, 50), Rect2(901, 520, 165, 50)] var difficulty_buttons := [Rect2(535, 520, 165, 50), Rect2(718, 520, 165, 50), Rect2(901, 520, 165, 50)]
var wave_button := Rect2(1216, 10, 359, 46) var wave_button := Rect2(1216, 10, 359, 46)
@@ -293,11 +308,14 @@ var speed_button := Rect2(1070, 14, 62, 54)
var auto_wave_button := Rect2(1216, 58, 359, 19) var auto_wave_button := Rect2(1216, 58, 359, 19)
var wave_current_preview_rect := Rect2(220, 8, 258, 66) var wave_current_preview_rect := Rect2(220, 8, 258, 66)
var wave_next_preview_rect := Rect2(486, 8, 258, 66) var wave_next_preview_rect := Rect2(486, 8, 258, 66)
var developer_panel_rect := Rect2(1060, 104, 515, 78) var developer_panel_rect := Rect2(920, 104, 660, 112)
var developer_strength_down_button := Rect2(1074, 140, 42, 30) var developer_strength_down_button := Rect2(934, 140, 42, 28)
var developer_strength_up_button := Rect2(1260, 140, 42, 30) var developer_strength_up_button := Rect2(1110, 140, 42, 28)
var developer_crystals_down_button := Rect2(1320, 140, 50, 30) var developer_crystals_down_button := Rect2(1170, 140, 50, 28)
var developer_crystals_up_button := Rect2(1512, 140, 50, 30) var developer_crystals_up_button := Rect2(1412, 140, 50, 28)
var developer_energy_down_button := Rect2(934, 176, 78, 28)
var developer_energy_up_button := Rect2(1018, 176, 78, 28)
var developer_admin_tower_button := Rect2(1370, 174, 195, 32)
var tower_buttons := [ var tower_buttons := [
Rect2(13, 778, 218, 112), Rect2(238, 778, 218, 112), Rect2(463, 778, 218, 112), Rect2(13, 778, 218, 112), Rect2(238, 778, 218, 112), Rect2(463, 778, 218, 112),
Rect2(688, 778, 218, 112), Rect2(913, 778, 218, 112), Rect2(1138, 778, 218, 112), Rect2(688, 778, 218, 112), Rect2(913, 778, 218, 112), Rect2(1138, 778, 218, 112),
@@ -314,8 +332,8 @@ var target_priority_button := Rect2(666, 698, 300, 44)
var endless_button := Rect2(545, 515, 510, 70) var endless_button := Rect2(545, 515, 510, 70)
var finish_mission_button := Rect2(625, 610, 350, 62) var finish_mission_button := Rect2(625, 610, 350, 62)
var profile_button := Rect2(1280, 25, 270, 52) var profile_button := Rect2(1280, 25, 270, 52)
var leaderboard_button := Rect2(40, 790, 200, 54) var leaderboard_button := Rect2(40, 772, 245, 62)
var shop_button := Rect2(568, 790, 200, 54) var shop_button := Rect2(550, 772, 245, 62)
var map_step_back_button := Rect2(55, 320, 170, 50) var map_step_back_button := Rect2(55, 320, 170, 50)
var shop_back_button := Rect2(55, 35, 185, 54) var shop_back_button := Rect2(55, 35, 185, 54)
var shop_purchase_buttons := [Rect2(190, 545, 360, 62), Rect2(620, 545, 360, 62), Rect2(1050, 545, 360, 62)] var shop_purchase_buttons := [Rect2(190, 545, 360, 62), Rect2(620, 545, 360, 62), Rect2(1050, 545, 360, 62)]
@@ -339,7 +357,7 @@ var leaderboard_difficulty := 1
var leaderboard_loading := false var leaderboard_loading := false
var leaderboard_message := "" var leaderboard_message := ""
var profile_back_button := Rect2(55, 35, 185, 54) var profile_back_button := Rect2(55, 35, 185, 54)
var multiplayer_button := Rect2(304, 790, 200, 54) var multiplayer_button := Rect2(295, 772, 245, 62)
var multiplayer_back_button := Rect2(55, 35, 185, 54) var multiplayer_back_button := Rect2(55, 35, 185, 54)
var quick_match_button := Rect2(125, 665, 300, 64) var quick_match_button := Rect2(125, 665, 300, 64)
var friend_match_button := Rect2(475, 665, 300, 64) var friend_match_button := Rect2(475, 665, 300, 64)
@@ -380,9 +398,9 @@ var available_update: Dictionary = {}
var update_notice_visible := false var update_notice_visible := false
var audio_settings_open := false var audio_settings_open := false
var master_volume := 0.65 var master_volume := 0.65
var audio_settings_button := Rect2(832, 790, 200, 54) var audio_settings_button := Rect2(805, 772, 245, 62)
var promo_code_button := Rect2(1096, 790, 200, 54) var promo_code_button := Rect2(1060, 772, 245, 62)
var exit_button := Rect2(1360, 790, 200, 54) var exit_button := Rect2(1315, 772, 245, 62)
var promo_code_input_rect := Rect2(500, 335, 600, 64) var promo_code_input_rect := Rect2(500, 335, 600, 64)
var promo_code_redeem_button := Rect2(650, 460, 300, 58) var promo_code_redeem_button := Rect2(650, 460, 300, 58)
var promo_code_cancel_button := Rect2(55, 35, 185, 54) var promo_code_cancel_button := Rect2(55, 35, 185, 54)
@@ -430,9 +448,32 @@ func _ready() -> void:
online_manager.match_invite_received.connect(_on_match_invite_received) online_manager.match_invite_received.connect(_on_match_invite_received)
_create_promo_code_input() _create_promo_code_input()
_create_friend_name_input() _create_friend_name_input()
_create_developer_password_input()
queue_redraw() queue_redraw()
func _create_developer_password_input() -> void:
developer_password_line_edit = LineEdit.new()
developer_password_line_edit.position = developer_password_input_rect.position + Vector2(12, 4)
developer_password_line_edit.size = developer_password_input_rect.size - Vector2(24, 8)
developer_password_line_edit.max_length = 40
developer_password_line_edit.secret = true
developer_password_line_edit.placeholder_text = "PASSWORT"
developer_password_line_edit.alignment = HORIZONTAL_ALIGNMENT_CENTER
developer_password_line_edit.add_theme_color_override("font_color", Color("e8fbff"))
developer_password_line_edit.add_theme_color_override("font_placeholder_color", Color("7699a8"))
var input_style := StyleBoxFlat.new()
input_style.bg_color = Color("081a27")
input_style.border_color = Color("ffad62")
input_style.set_border_width_all(1)
input_style.set_content_margin_all(10)
developer_password_line_edit.add_theme_stylebox_override("normal", input_style)
developer_password_line_edit.add_theme_stylebox_override("focus", input_style)
developer_password_line_edit.text_submitted.connect(func(_value: String) -> void: _submit_developer_password())
add_child(developer_password_line_edit)
developer_password_line_edit.hide()
func _create_friend_name_input() -> void: func _create_friend_name_input() -> void:
friend_name_line_edit = LineEdit.new() friend_name_line_edit = LineEdit.new()
friend_name_line_edit.position = friend_name_rect.position + Vector2(10, 3) friend_name_line_edit.position = friend_name_rect.position + Vector2(10, 3)
@@ -571,7 +612,7 @@ func _save_balance_cache(config: Dictionary) -> void:
func _apply_balance_config(config: Dictionary) -> bool: func _apply_balance_config(config: Dictionary) -> bool:
var towers_config = config.get("towers", []) var towers_config = config.get("towers", [])
var enemies_config = config.get("enemies", []) var enemies_config = config.get("enemies", [])
if not towers_config is Array or towers_config.size() != TOWER_DATA.size() or not enemies_config is Array or enemies_config.size() != ENEMY_DATA.size(): if not towers_config is Array or towers_config.size() != TOWER_DATA.size() - 1 or not enemies_config is Array or enemies_config.size() != ENEMY_DATA.size():
return false return false
for values in towers_config: for values in towers_config:
if not values is Dictionary or str(values.get("targetClass", "both")) not in ["air", "ground", "both"]: if not values is Dictionary or str(values.get("targetClass", "both")) not in ["air", "ground", "both"]:
@@ -579,7 +620,7 @@ func _apply_balance_config(config: Dictionary) -> bool:
for values in enemies_config: for values in enemies_config:
if not values is Dictionary: if not values is Dictionary:
return false return false
for index in TOWER_DATA.size(): for index in TOWER_DATA.size() - 1:
var values: Dictionary = towers_config[index] var values: Dictionary = towers_config[index]
var tower: Dictionary = TOWER_DATA[index] var tower: Dictionary = TOWER_DATA[index]
tower["cost"] = clampi(int(values.get("cost", tower.cost)), 10, 50000) tower["cost"] = clampi(int(values.get("cost", tower.cost)), 10, 50000)
@@ -603,7 +644,7 @@ func _apply_balance_config(config: Dictionary) -> bool:
func _apply_promo_unlocks(entitlements: Dictionary) -> void: func _apply_promo_unlocks(entitlements: Dictionary) -> void:
if bool(entitlements.get("all_towers", false)): if bool(entitlements.get("all_towers", false)):
unlocked_towers = [] unlocked_towers = []
for tower_type in TOWER_DATA.size(): for tower_type in TOWER_DATA.size() - 1:
unlocked_towers.append(tower_type) unlocked_towers.append(tower_type)
if bool(entitlements.get("all_maps", false)): if bool(entitlements.get("all_maps", false)):
unlocked_maps = [] unlocked_maps = []
@@ -806,7 +847,7 @@ func _tower_target_class(tower_type: int) -> String:
func _tower_display_order() -> Array[int]: func _tower_display_order() -> Array[int]:
var ordered_types: Array[int] = [] var ordered_types: Array[int] = []
for target_class in ["air", "ground", "both"]: for target_class in ["air", "ground", "both"]:
for tower_type in TOWER_DATA.size(): for tower_type in TOWER_DATA.size() - 1:
if _tower_target_class(tower_type) == target_class: if _tower_target_class(tower_type) == target_class:
ordered_types.append(tower_type) ordered_types.append(tower_type)
return ordered_types return ordered_types
@@ -945,7 +986,10 @@ func _fire_tower(tower: Dictionary, target: Dictionary) -> void:
var damage := _tower_damage(tower.type, tower.level) var damage := _tower_damage(tower.type, tower.level)
if _tower_target_class(tower.type) == "both": if _tower_target_class(tower.type) == "both":
damage *= float(data.get("both_damage_scale", 0.7)) damage *= float(data.get("both_damage_scale", 0.7))
if tower.type == 1: if tower.type == ADMIN_TOWER_TYPE:
_apply_damage(target, damage, int(tower.get("owner", 0)), tower.type)
beams.append({"from": tower.pos, "to": target.pos, "ttl": 0.16, "color": data.color, "piercing": false})
elif tower.type == 1:
_apply_damage(target, damage, int(tower.get("owner", 0)), tower.type) _apply_damage(target, damage, int(tower.get("owner", 0)), tower.type)
beams.append({"from": tower.pos, "to": target.pos, "ttl": 0.09, "color": data.color}) beams.append({"from": tower.pos, "to": target.pos, "ttl": 0.09, "color": data.color})
elif tower.type == 5: elif tower.type == 5:
@@ -1055,7 +1099,21 @@ func _update_effects(delta: float) -> void:
func _unhandled_input(event: InputEvent) -> void: func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_F9 and game_mode in ["playing", "paused"]: if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_F9 and game_mode in ["playing", "paused"]:
developer_panel_open = not developer_panel_open if developer_password_prompt_open:
developer_password_prompt_open = false
developer_password_line_edit.hide()
elif developer_unlocked_this_round:
developer_panel_open = not developer_panel_open
else:
developer_password_prompt_open = true
developer_password_line_edit.clear()
developer_password_line_edit.show()
developer_password_line_edit.grab_focus()
queue_redraw()
return
if developer_password_prompt_open and event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
developer_password_prompt_open = false
developer_password_line_edit.hide()
queue_redraw() queue_redraw()
return return
if event is InputEventKey and event.pressed and not event.echo and event.keycode in [KEY_ESCAPE, KEY_BACK, KEY_Q]: if event is InputEventKey and event.pressed and not event.echo and event.keycode in [KEY_ESCAPE, KEY_BACK, KEY_Q]:
@@ -1131,6 +1189,12 @@ func _unhandled_input(event: InputEvent) -> void:
func _handle_pointer(position: Vector2, is_touch := false) -> void: func _handle_pointer(position: Vector2, is_touch := false) -> void:
_play_ui_sound() _play_ui_sound()
if developer_password_prompt_open:
if developer_password_cancel_button.has_point(position):
developer_password_prompt_open = false
developer_password_line_edit.hide()
queue_redraw()
return
if developer_panel_open and game_mode in ["playing", "paused"]: if developer_panel_open and game_mode in ["playing", "paused"]:
if developer_strength_down_button.has_point(position): if developer_strength_down_button.has_point(position):
_set_enemy_strength_multiplier(enemy_strength_multiplier - 0.1) _set_enemy_strength_multiplier(enemy_strength_multiplier - 0.1)
@@ -1144,6 +1208,21 @@ func _handle_pointer(position: Vector2, is_touch := false) -> void:
if developer_crystals_up_button.has_point(position): if developer_crystals_up_button.has_point(position):
_adjust_core_crystals(25) _adjust_core_crystals(25)
return return
if developer_energy_down_button.has_point(position):
_adjust_developer_energy(-500)
return
if developer_energy_up_button.has_point(position):
_adjust_developer_energy(500)
return
if developer_admin_tower_button.has_point(position):
if coop_online_mode and not developer_server_authorized:
_show_status("ONLINE-TESTMODUS WIRD NOCH AUTORISIERT")
return
selected_tower_type = ADMIN_TOWER_TYPE
selected_tower_index = -1
_show_status("ADMIN-TURM GEWÄHLT • BAUPLATZ AUF DER KARTE ANKLICKEN")
queue_redraw()
return
if developer_panel_rect.has_point(position): if developer_panel_rect.has_point(position):
return return
if audio_settings_open: if audio_settings_open:
@@ -1160,6 +1239,9 @@ func _handle_pointer(position: Vector2, is_touch := false) -> void:
queue_redraw() queue_redraw()
return return
if game_mode == "menu": if game_mode == "menu":
if _is_mobile_menu_layout():
_handle_mobile_menu_pointer(position)
return
if not pending_match_invite.is_empty() and Rect2(470, 226, 660, 54).has_point(position): if not pending_match_invite.is_empty() and Rect2(470, 226, 660, 54).has_point(position):
room_code_input = String(pending_match_invite.get("room_code", "")) room_code_input = String(pending_match_invite.get("room_code", ""))
pending_match_invite.clear() pending_match_invite.clear()
@@ -1549,6 +1631,7 @@ func _toggle_coop_side() -> void:
func _start_coop_ai_game() -> void: func _start_coop_ai_game() -> void:
online_manager.set_presence_status("playing") online_manager.set_presence_status("playing")
_reset_developer_access()
coop_mode = true coop_mode = true
coop_ai_mode = true coop_ai_mode = true
coop_online_mode = false coop_online_mode = false
@@ -1585,6 +1668,7 @@ func _start_coop_ai_game() -> void:
func _start_coop_online_game(player_slot: int) -> void: func _start_coop_online_game(player_slot: int) -> void:
online_manager.set_presence_status("playing") online_manager.set_presence_status("playing")
_reset_developer_access()
local_player_slot = clampi(player_slot, 0, 1) local_player_slot = clampi(player_slot, 0, 1)
coop_mode = true coop_mode = true
coop_ai_mode = false coop_ai_mode = false
@@ -1751,6 +1835,7 @@ func _refresh_leaderboard() -> void:
func _start_game() -> void: func _start_game() -> void:
online_manager.set_presence_status("playing") online_manager.set_presence_status("playing")
_reset_developer_access()
coop_online_mode = false coop_online_mode = false
credits = int(_difficulty().start_credits) credits = int(_difficulty().start_credits)
reactor_lives = int(_difficulty().lives) reactor_lives = int(_difficulty().lives)
@@ -1781,6 +1866,7 @@ func _start_game() -> void:
func _return_to_menu() -> void: func _return_to_menu() -> void:
online_manager.set_presence_status("menu") online_manager.set_presence_status("menu")
_reset_developer_access()
if coop_online_mode: if coop_online_mode:
online_manager.leave_current_match() online_manager.leave_current_match()
game_mode = "menu" game_mode = "menu"
@@ -1805,6 +1891,58 @@ func _return_to_menu() -> void:
queue_redraw() queue_redraw()
func _reset_developer_access() -> void:
developer_panel_open = false
developer_unlocked_this_round = false
developer_server_authorized = false
developer_password_prompt_open = false
if developer_password_line_edit:
developer_password_line_edit.clear()
developer_password_line_edit.hide()
func _submit_developer_password() -> void:
if developer_password_line_edit == null:
return
if developer_password_line_edit.text == DEVELOPER_PASSWORD:
developer_unlocked_this_round = true
developer_panel_open = true
developer_password_prompt_open = false
developer_password_line_edit.hide()
if coop_online_mode:
developer_server_authorized = false
online_manager.send_match_command(DEVELOPER_AUTH_OP, {"password": developer_password_line_edit.text})
_show_status("TESTMODUS FREI • ONLINE-SERVER WIRD AUTORISIERT")
else:
developer_server_authorized = true
_show_status("TESTMODUS FÜR DIESE RUNDE FREIGESCHALTET")
else:
developer_password_line_edit.clear()
_show_status("PASSWORT FALSCH")
developer_password_line_edit.grab_focus()
queue_redraw()
func _adjust_developer_energy(amount: int) -> void:
if amount not in [-500, 500]:
return
if coop_online_mode:
if not developer_server_authorized:
_show_status("ONLINE-TESTMODUS WIRD NOCH AUTORISIERT")
return
online_manager.send_match_command(DEVELOPER_ENERGY_OP, {"amount": amount})
_show_status("ENERGIEÄNDERUNG AN SERVER GESENDET")
return
if coop_mode:
var current_energy := int(multiplayer_manager.player_credits[local_player_slot])
multiplayer_manager.player_credits[local_player_slot] = maxi(0, current_energy + amount)
credits = int(multiplayer_manager.player_credits[0])
else:
credits = maxi(0, credits + amount)
_show_status("TESTENERGIE %s500" % ("+" if amount > 0 else "−"))
queue_redraw()
func _start_wave() -> void: func _start_wave() -> void:
if wave_active: if wave_active:
_show_status("WELLE LÄUFT BEREITS") _show_status("WELLE LÄUFT BEREITS")
@@ -1973,15 +2111,19 @@ func _complete_tutorial() -> void:
func _place_tower(position: Vector2) -> void: func _place_tower(position: Vector2) -> void:
position = _snap_to_grid(position) position = _snap_to_grid(position)
if selected_tower_type < 0 or not unlocked_towers.has(selected_tower_type): var is_admin_tower := selected_tower_type == ADMIN_TOWER_TYPE
if is_admin_tower and (not developer_unlocked_this_round or (coop_online_mode and not developer_server_authorized)):
_show_status("ADMIN-TURM IST NOCH NICHT FREIGESCHALTET")
return
if selected_tower_type < 0 or (not is_admin_tower and not unlocked_towers.has(selected_tower_type)):
_show_status("TURM ZUERST IM ARSENAL FREISCHALTEN") _show_status("TURM ZUERST IM ARSENAL FREISCHALTEN")
return return
var data: Dictionary = TOWER_DATA[selected_tower_type] var data: Dictionary = TOWER_DATA[selected_tower_type]
var credit_slot := _local_credit_slot() var credit_slot := _local_credit_slot()
if _player_credits(credit_slot) < data.cost: if not is_admin_tower and _player_credits(credit_slot) < data.cost:
_show_status("NICHT GENUG ENERGIE") _show_status("NICHT GENUG ENERGIE")
return return
if not _is_valid_build_position(position, credit_slot): if not _is_valid_build_position(position, credit_slot, is_admin_tower):
_show_status("BAUPLATZ BLOCKIERT") _show_status("BAUPLATZ BLOCKIERT")
return return
if coop_online_mode: if coop_online_mode:
@@ -1990,15 +2132,16 @@ func _place_tower(position: Vector2) -> void:
pending_touch_build_position = Vector2(-1, -1) pending_touch_build_position = Vector2(-1, -1)
_show_status("BAUAUFTRAG AN SERVER GESENDET") _show_status("BAUAUFTRAG AN SERVER GESENDET")
return return
_spend_credits(0, int(data.cost)) if not is_admin_tower:
towers_built_this_run += 1 _spend_credits(0, int(data.cost))
towers_built_this_run += 1
towers.append({"pos": position, "type": selected_tower_type, "level": 1, "cooldown": 0.15, "angle": 0.0, "owner": 0, "target_priority": "shield" if selected_tower_type == 1 else "first"}) towers.append({"pos": position, "type": selected_tower_type, "level": 1, "cooldown": 0.15, "angle": 0.0, "owner": 0, "target_priority": "shield" if selected_tower_type == 1 else "first"})
selected_tower_index = towers.size() - 1 selected_tower_index = towers.size() - 1
selected_tower_type = -1 selected_tower_type = -1
pending_touch_build_position = Vector2(-1, -1) pending_touch_build_position = Vector2(-1, -1)
if not tutorial_completed and tutorial_step == 1: if not tutorial_completed and tutorial_step == 1:
tutorial_step = 2 tutorial_step = 2
_spawn_burst(position, Color("65eaff"), 12) _spawn_burst(position, Color("fff2a1") if is_admin_tower else Color("65eaff"), 12)
func _upgrade_selected() -> void: func _upgrade_selected() -> void:
@@ -2215,7 +2358,7 @@ func _ai_priority_category() -> String:
func _ai_choose_tower_type(category: String) -> int: func _ai_choose_tower_type(category: String) -> int:
var best_type := 1 var best_type := 1
var best_score := -INF var best_score := -INF
for tower_type in TOWER_DATA.size(): for tower_type in TOWER_DATA.size() - 1:
var target_class := _tower_target_class(tower_type) var target_class := _tower_target_class(tower_type)
if target_class != category and target_class != "both": if target_class != category and target_class != "both":
continue continue
@@ -2380,7 +2523,7 @@ func _apply_online_snapshot(snapshot: Dictionary) -> void:
var shot_from := Vector2(float(fired_shot.get("from_x", 0.0)), float(fired_shot.get("from_y", 0.0))) var shot_from := Vector2(float(fired_shot.get("from_x", 0.0)), float(fired_shot.get("from_y", 0.0)))
var shot_to := Vector2(float(fired_shot.get("to_x", 0.0)), float(fired_shot.get("to_y", 0.0))) var shot_to := Vector2(float(fired_shot.get("to_x", 0.0)), float(fired_shot.get("to_y", 0.0)))
var shot_color: Color = TOWER_DATA[tower_type].color var shot_color: Color = TOWER_DATA[tower_type].color
if tower_type in [1, 5]: if tower_type in [1, 5, ADMIN_TOWER_TYPE]:
beams.append({"from": shot_from, "to": shot_to, "ttl": 0.12 if tower_type == 1 else 0.16, "color": shot_color, "piercing": tower_type == 5}) beams.append({"from": shot_from, "to": shot_to, "ttl": 0.12 if tower_type == 1 else 0.16, "color": shot_color, "piercing": tower_type == 5})
else: else:
online_projectiles.append({"pos": shot_from, "target": shot_to, "speed": float(TOWER_DATA[tower_type].shot_speed), "type": tower_type, "color": shot_color}) online_projectiles.append({"pos": shot_from, "target": shot_to, "speed": float(TOWER_DATA[tower_type].shot_speed), "type": tower_type, "color": shot_color})
@@ -2414,6 +2557,14 @@ func _handle_online_event(event: Dictionary) -> void:
if not coop_online_mode: if not coop_online_mode:
return return
match String(event.get("type", "")): match String(event.get("type", "")):
"developer_authorized":
if int(event.get("slot", -1)) == local_player_slot:
developer_server_authorized = true
_show_status("ONLINE-TESTMODUS AUF DEM SERVER FREIGESCHALTET")
"developer_auth_rejected":
if int(event.get("slot", -1)) == local_player_slot:
developer_server_authorized = false
_show_status("SERVER HAT DAS TESTPASSWORT ABGELEHNT")
"wave_complete": "wave_complete":
_show_status("WELLE %d ABGEWEHRT +%d" % [int(event.get("wave", wave)), int(event.get("bonus", 0))]) _show_status("WELLE %d ABGEWEHRT +%d" % [int(event.get("wave", wave)), int(event.get("bonus", 0))])
if int(event.get("wave", wave)) >= MAX_WAVES: if int(event.get("wave", wave)) >= MAX_WAVES:
@@ -2620,6 +2771,8 @@ func _draw() -> void:
_draw_endless_offer() _draw_endless_offer()
elif game_mode == "endless_result": elif game_mode == "endless_result":
_draw_endless_result() _draw_endless_result()
if developer_password_prompt_open:
_draw_developer_password_prompt()
func _active_world_width() -> float: func _active_world_width() -> float:
@@ -2825,7 +2978,12 @@ func _draw_tower(tower: Dictionary) -> void:
draw_arc(tower.pos, size * 0.54, 0.0, TAU, 28, Color(1.0, 0.70, 0.34, 0.72), 2.0) draw_arc(tower.pos, size * 0.54, 0.0, TAU, 28, Color(1.0, 0.70, 0.34, 0.72), 2.0)
draw_set_transform(tower.pos - camera_offset, tower.angle, Vector2.ONE) draw_set_transform(tower.pos - camera_offset, tower.angle, Vector2.ONE)
var destination := Rect2(-size * 0.5, -size * 0.5, size, size) var destination := Rect2(-size * 0.5, -size * 0.5, size, size)
if tower.type < 3: if tower.type == ADMIN_TOWER_TYPE:
draw_circle(Vector2.ZERO, size * 0.42, Color("504621"))
draw_arc(Vector2.ZERO, size * 0.47, 0.0, TAU, 40, Color("fff2a1"), 4.0)
draw_circle(Vector2.ZERO, size * 0.20, Color("fff2a1"))
draw_string(ThemeDB.fallback_font, Vector2(-10, 5), "A", HORIZONTAL_ALIGNMENT_CENTER, 20, 14, Color("201a0b"))
elif tower.type < 3:
var source_width: float = TOWER_TEXTURE.get_width() / 3.0 var source_width: float = TOWER_TEXTURE.get_width() / 3.0
draw_texture_rect_region(TOWER_TEXTURE, destination, Rect2(source_width * tower.type, 0, source_width, TOWER_TEXTURE.get_height())) draw_texture_rect_region(TOWER_TEXTURE, destination, Rect2(source_width * tower.type, 0, source_width, TOWER_TEXTURE.get_height()))
elif tower.type == 6: elif tower.type == 6:
@@ -2944,18 +3102,33 @@ func _draw_hud() -> void:
if developer_panel_open: if developer_panel_open:
draw_rect(developer_panel_rect, Color(0.008, 0.024, 0.045, 0.96), true) draw_rect(developer_panel_rect, Color(0.008, 0.024, 0.045, 0.96), true)
draw_rect(developer_panel_rect, Color("ffad62"), false, 1.5) draw_rect(developer_panel_rect, Color("ffad62"), false, 1.5)
draw_string(font, developer_panel_rect.position + Vector2(12, 22), "TESTMODUS • F9 ZUM SCHLIESSEN", HORIZONTAL_ALIGNMENT_LEFT, 490, 11, Color("ffc277")) draw_string(font, developer_panel_rect.position + Vector2(12, 22), "TESTMODUS • F9 ZUM SCHLIESSEN", HORIZONTAL_ALIGNMENT_LEFT, 620, 11, Color("ffc277"))
_draw_button(developer_strength_down_button, "−", false, Color("44333a")) _draw_button(developer_strength_down_button, "−", false, Color("44333a"))
draw_string(font, developer_panel_rect.position + Vector2(58, 57), "GEGNER ×%.1f" % enemy_strength_multiplier, HORIZONTAL_ALIGNMENT_CENTER, 128, 13, Color.WHITE) draw_string(font, developer_panel_rect.position + Vector2(58, 57), "GEGNER ×%.1f" % enemy_strength_multiplier, HORIZONTAL_ALIGNMENT_CENTER, 128, 13, Color.WHITE)
_draw_button(developer_strength_up_button, "+", false, Color("44333a")) _draw_button(developer_strength_up_button, "+", false, Color("44333a"))
_draw_button(developer_crystals_down_button, "−25", false, Color("44333a")) _draw_button(developer_crystals_down_button, "−25", false, Color("44333a"))
draw_string(font, developer_panel_rect.position + Vector2(310, 57), "KRISTALLE %04d" % core_crystals, HORIZONTAL_ALIGNMENT_CENTER, 136, 13, Color("8ff4ff")) draw_string(font, developer_panel_rect.position + Vector2(298, 57), "KRISTALLE %04d" % core_crystals, HORIZONTAL_ALIGNMENT_CENTER, 136, 13, Color("8ff4ff"))
_draw_button(developer_crystals_up_button, "+25", false, Color("44333a")) _draw_button(developer_crystals_up_button, "+25", false, Color("44333a"))
_draw_button(developer_energy_down_button, "−500", false, Color("44333a"))
_draw_button(developer_energy_up_button, "+500", false, Color("44333a"))
draw_string(font, Vector2(1102, 195), "ENERGIE %04d" % _player_credits(_local_credit_slot()), HORIZONTAL_ALIGNMENT_CENTER, 230, 11, Color("8ff4ff"))
_draw_button(developer_admin_tower_button, "ADMIN-TURM BAUEN", false, Color("5c4d2e" if (not coop_online_mode or developer_server_authorized) else "34404b"))
if status_timer > 0.0: if status_timer > 0.0:
draw_rect(Rect2(560, 108, 480, 44), Color(0.01, 0.035, 0.07, 0.88), true) draw_rect(Rect2(560, 108, 480, 44), Color(0.01, 0.035, 0.07, 0.88), true)
draw_string(font, Vector2(570, 138), status_text, HORIZONTAL_ALIGNMENT_CENTER, 460, 18, Color("a9efff")) draw_string(font, Vector2(570, 138), status_text, HORIZONTAL_ALIGNMENT_CENTER, 460, 18, Color("a9efff"))
func _draw_developer_password_prompt() -> void:
var font := ThemeDB.fallback_font
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.0, 0.01, 0.03, 0.76), true)
draw_rect(developer_password_dialog_rect, Color("071725"), true)
draw_rect(developer_password_dialog_rect, Color("ffad62"), false, 2.0)
draw_string(font, Vector2(developer_password_dialog_rect.position.x, 375), "ENTWICKLERMODUS", HORIZONTAL_ALIGNMENT_CENTER, developer_password_dialog_rect.size.x, 25, Color("ffc277"))
draw_string(font, Vector2(developer_password_dialog_rect.position.x + 28, 399), "Passwort einmal für diese Runde eingeben", HORIZONTAL_ALIGNMENT_CENTER, developer_password_dialog_rect.size.x - 56, 13, Color("b8d4df"))
draw_rect(developer_password_input_rect, Color("4ab8d1"), false, 1.0)
_draw_button(developer_password_cancel_button, "ABBRECHEN", false, Color("26394a"))
func _draw_wave_preview_panel(rect: Rect2, heading: String, for_wave: int) -> void: func _draw_wave_preview_panel(rect: Rect2, heading: String, for_wave: int) -> void:
var font := ThemeDB.fallback_font var font := ThemeDB.fallback_font
var summary := _wave_preview_summary(for_wave) var summary := _wave_preview_summary(for_wave)
@@ -3147,28 +3320,42 @@ func _draw_button(rect: Rect2, text: String, active: bool, base := Color("173c55
func _draw_menu() -> void: func _draw_menu() -> void:
if _is_mobile_menu_layout():
_draw_mobile_menu()
return
var font := ThemeDB.fallback_font var font := ThemeDB.fallback_font
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.52)) draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.66), true)
draw_string(font, Vector2(0, 145), "VOID BASTION", HORIZONTAL_ALIGNMENT_CENTER, 1600, 64, Color("c7f5ff")) draw_string(font, Vector2(2, 147), "VOID BASTION", HORIZONTAL_ALIGNMENT_CENTER, 1600, 64, Color(0.0, 0.0, 0.0, 0.62))
draw_string(font, Vector2(0, 202), "ORBITAL DEFENSE PROTOCOL", HORIZONTAL_ALIGNMENT_CENTER, 1600, 23, Color("56cce9")) draw_string(font, Vector2(0, 143), "VOID BASTION", HORIZONTAL_ALIGNMENT_CENTER, 1600, 64, Color("d9f9ff"))
draw_string(font, Vector2(0, 200), "ORBITAL DEFENSE PROTOCOL", HORIZONTAL_ALIGNMENT_CENTER, 1600, 21, Color("56cce9"))
if not pending_match_invite.is_empty(): if not pending_match_invite.is_empty():
_draw_button(Rect2(470, 226, 660, 54), _ui("EINLADUNG VON %s ANNEHMEN") % String(pending_match_invite.get("from", "MITSPIELER")), true, Color("1d665b")) _draw_button(Rect2(470, 226, 660, 54), _ui("EINLADUNG VON %s ANNEHMEN") % String(pending_match_invite.get("from", "MITSPIELER")), true, Color("1d665b"))
_draw_button(profile_button, "%s • %s" % [account_manager.display_name, _ui("PROFIL")], false, Color("233f57")) _draw_button(profile_button, "%s • %s" % [account_manager.display_name, _ui("PROFIL")], false, Color("233f57"))
if menu_step == "map": if menu_step == "map":
draw_string(font, Vector2(0, 300), _ui("EINSATZKARTE WÄHLEN • GESPERRTE SEKTOREN IM SHOP FREISCHALTEN"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 20, Color("8feeff")) draw_rect(Rect2(34, 252, 1532, 414), Color(0.004, 0.018, 0.032, 0.32), true)
draw_rect(Rect2(34, 252, 1532, 414), Color(0.18, 0.55, 0.67, 0.42), false, 1.0)
draw_string(font, Vector2(0, 280), _ui("EINSATZSEKTOREN"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 17, Color("84eaff"))
draw_string(font, Vector2(0, 301), _ui("KARTE WÄHLEN • GESPERRTE SEKTOREN IM SHOP FREISCHALTEN"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 13, Color("98b8c7"))
for index in map_buttons.size(): for index in map_buttons.size():
var card: Rect2 = map_buttons[index] var card: Rect2 = map_buttons[index]
var map_index := index var map_index := index
var is_unlocked := unlocked_maps.has(map_index) var is_unlocked := unlocked_maps.has(map_index)
draw_texture_rect(MAP_TEXTURES[map_index], card, false, Color(0.82, 0.88, 0.94, 1.0) if is_unlocked else Color(0.38, 0.46, 0.54, 1.0)) var is_selected := selected_map == map_index and is_unlocked
draw_rect(card, Color(0.005, 0.015, 0.035, 0.22 if is_unlocked else 0.48), true) draw_rect(card.grow(4.0), Color(0.0, 0.0, 0.0, 0.4), true)
draw_rect(card, Color("70eaff") if selected_map == map_index and is_unlocked else Color("436574"), false, 4.0 if selected_map == map_index and is_unlocked else 2.0) draw_texture_rect(MAP_TEXTURES[map_index], card, false, Color(0.96, 0.99, 1.0, 1.0) if is_unlocked else Color(0.38, 0.46, 0.54, 1.0))
var footer_y := card.size.y - 52.0 draw_rect(card, Color(0.005, 0.015, 0.035, 0.08 if is_unlocked else 0.38), true)
draw_rect(Rect2(card.position + Vector2(0, footer_y), Vector2(card.size.x, 52)), Color(0.01, 0.025, 0.05, 0.88), true) var footer := Rect2(card.position.x, card.end.y - 54.0, card.size.x, 54.0)
draw_string(font, card.position + Vector2(0, footer_y + 21), _ui(MAP_NAMES[map_index]), HORIZONTAL_ALIGNMENT_CENTER, card.size.x, 14, Color.WHITE) draw_rect(footer, Color(0.005, 0.018, 0.032, 0.9), true)
draw_rect(card, Color("70eaff") if is_selected else (Color("d99d55") if not is_unlocked else Color("345769")), false, 3.0 if is_selected else 1.5)
var badge_text := _ui("AUSGEWÄHLT") if is_selected else (_ui("GESPERRT") if not is_unlocked else "")
if not badge_text.is_empty():
var badge := Rect2(card.position + Vector2(10, 10), Vector2(116, 25))
draw_rect(badge, Color("116c83", 0.96) if is_selected else Color("80562d", 0.96), true)
draw_string(font, badge.position + Vector2(0, 17), badge_text, HORIZONTAL_ALIGNMENT_CENTER, badge.size.x, 10, Color.WHITE)
draw_string(font, card.position + Vector2(0, card.size.y - 33), _ui(MAP_NAMES[map_index]), HORIZONTAL_ALIGNMENT_CENTER, card.size.x, 16, Color.WHITE)
var description: String = _ui(MAP_DESCRIPTIONS[map_index]) if is_unlocked else _ui("GESPERRT • %d KERNKRISTALLE") % int(MAP_UNLOCK_COSTS[map_index]) var description: String = _ui(MAP_DESCRIPTIONS[map_index]) if is_unlocked else _ui("GESPERRT • %d KERNKRISTALLE") % int(MAP_UNLOCK_COSTS[map_index])
draw_string(font, card.position + Vector2(0, footer_y + 42), description, HORIZONTAL_ALIGNMENT_CENTER, card.size.x, 11, Color("8fddef") if is_unlocked else Color("ffcf7a")) draw_string(font, card.position + Vector2(0, card.size.y - 11), description, HORIZONTAL_ALIGNMENT_CENTER, card.size.x, 12, Color("8fddef") if is_unlocked else Color("ffcf7a"))
draw_string(font, Vector2(0, 720), _ui("Wähle einen Sektor, um Schwierigkeit und Missionsstart festzulegen."), HORIZONTAL_ALIGNMENT_CENTER, 1600, 16, Color("d7e2ea")) draw_string(font, Vector2(0, 698), _ui("Wähle einen Sektor. Danach legst du Schwierigkeit und Missionsstart fest."), HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, Color("d7e2ea"))
else: else:
_draw_button(map_step_back_button, "< KARTE", false, Color("26394a")) _draw_button(map_step_back_button, "< KARTE", false, Color("26394a"))
var selected_card := Rect2(500, 315, 600, 145) var selected_card := Rect2(500, 315, 600, 145)
@@ -3184,13 +3371,154 @@ func _draw_menu() -> void:
draw_string(font, Vector2(0, 603), _ui(DIFFICULTY_DESCRIPTIONS[selected_difficulty]), HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("ffcf7a")) draw_string(font, Vector2(0, 603), _ui(DIFFICULTY_DESCRIPTIONS[selected_difficulty]), HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("ffcf7a"))
_draw_button(start_button, "MISSION STARTEN", false, Color("18556f")) _draw_button(start_button, "MISSION STARTEN", false, Color("18556f"))
draw_string(font, Vector2(0, 755), _ui("KARTENBONUS %d • BESTE WELLE %02d • KERNKRISTALLE %04d") % [int(_difficulty().crystals), best_wave, core_crystals], HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, Color("86c8d8")) draw_string(font, Vector2(0, 755), _ui("KARTENBONUS %d • BESTE WELLE %02d • KERNKRISTALLE %04d") % [int(_difficulty().crystals), best_wave, core_crystals], HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, Color("86c8d8"))
draw_line(Vector2(40, 744), Vector2(1560, 744), Color("448094", 0.68), 1.0)
draw_string(font, Vector2(40, 761), _ui("KOMMUNITY"), HORIZONTAL_ALIGNMENT_CENTER, 500, 10, Color("7fa8b8"))
draw_string(font, Vector2(550, 761), _ui("HANGAR & KONTO"), HORIZONTAL_ALIGNMENT_CENTER, 755, 10, Color("7fa8b8"))
draw_string(font, Vector2(1315, 761), _ui("SYSTEM"), HORIZONTAL_ALIGNMENT_CENTER, 245, 10, Color("7fa8b8"))
_draw_button(leaderboard_button, "BESTENLISTE", false, Color("24475b")) _draw_button(leaderboard_button, "BESTENLISTE", false, Color("24475b"))
_draw_button(multiplayer_button, "MULTIPLAYER", false, Color("254f75")) _draw_button(multiplayer_button, "MULTIPLAYER", false, Color("254f75"))
_draw_button(shop_button, "ARSENAL / SHOP", false, Color("24516a")) _draw_button(shop_button, "ARSENAL / SHOP", false, Color("24516a"))
_draw_button(audio_settings_button, "EINSTELLUNGEN", false, Color("24516a")) _draw_button(audio_settings_button, "EINSTELLUNGEN", false, Color("24516a"))
_draw_button(promo_code_button, "CODE EINLÖSEN", false, Color("34576b")) _draw_button(promo_code_button, "CODE EINLÖSEN", false, Color("34576b"))
_draw_button(exit_button, "SPIEL BEENDEN", false, Color("5c3342")) _draw_button(exit_button, "SPIEL BEENDEN", false, Color("5c3342"))
draw_string(font, Vector2(0, 860), _ui("VERSION %s • GERGAMES-ONLINEKONTO • GLOBALE BESTENLISTEN") % _app_version(), HORIZONTAL_ALIGNMENT_CENTER, 1600, 14, Color("688293")) draw_string(font, Vector2(0, 867), _ui("VERSION %s • GERGAMES-ONLINEKONTO • GLOBALE BESTENLISTEN") % _app_version(), HORIZONTAL_ALIGNMENT_CENTER, 1600, 12, Color("688293"))
if update_notice_visible:
_draw_button(update_button, "UPDATE %s VERFÜGBAR" % str(available_update.get("version", "")), true, Color("1f5d4d"))
_draw_button(update_dismiss_button, "×", false, Color("3d4650"))
func _is_mobile_menu_layout() -> bool:
if OS.has_feature("mobile") or OS.get_cmdline_user_args().has("--mobile-menu-preview"):
return true
var viewport_size := get_viewport_rect().size
return viewport_size.x < 1180.0 or viewport_size.aspect() < 1.15
func _mobile_map_card_rect() -> Rect2:
return Rect2(240, 154, 1120, 300)
func _mobile_map_previous_rect() -> Rect2:
return Rect2(148, 268, 72, 72)
func _mobile_map_next_rect() -> Rect2:
return Rect2(1380, 268, 72, 72)
func _mobile_invite_rect() -> Rect2:
return Rect2(470, 112, 660, 42)
func _mobile_difficulty_rect(index: int) -> Rect2:
return Rect2(330 + index * 315, 535, 300, 62)
func _mobile_start_rect() -> Rect2:
return Rect2(350, 645, 900, 76)
func _mobile_nav_rect(index: int) -> Rect2:
return Rect2(50 + (index % 3) * 500, 758 + floori(float(index) / 3.0) * 62, 470, 54)
func _handle_mobile_menu_pointer(position: Vector2) -> void:
if not pending_match_invite.is_empty() and _mobile_invite_rect().has_point(position):
room_code_input = String(pending_match_invite.get("room_code", ""))
pending_match_invite.clear()
_join_entered_room()
return
if update_notice_visible and update_button.has_point(position):
OS.shell_open(str(available_update.get("url", "https://void-bastion.gergames.de/")))
return
if update_notice_visible and update_dismiss_button.has_point(position):
update_notice_visible = false
queue_redraw()
return
if profile_button.has_point(position):
game_mode = "profile"
queue_redraw()
return
for index in range(6):
if not _mobile_nav_rect(index).has_point(position):
continue
match index:
0:
_open_leaderboard()
1:
game_mode = "multiplayer_menu"
2:
store_return_mode = "menu"
store_tab = "towers"
game_mode = "store"
3:
audio_settings_open = true
4:
game_mode = "promo_code"
promo_code_message = ""
promo_code_line_edit.show()
promo_code_line_edit.grab_focus()
5:
get_tree().quit()
queue_redraw()
return
if _mobile_map_previous_rect().has_point(position):
_select_map(wrapi(selected_map - 1, 0, MAP_NAMES.size()))
menu_step = "map"
queue_redraw()
return
if _mobile_map_next_rect().has_point(position):
_select_map(wrapi(selected_map + 1, 0, MAP_NAMES.size()))
menu_step = "map"
queue_redraw()
return
for index in difficulty_buttons.size():
if _mobile_difficulty_rect(index).has_point(position):
selected_difficulty = index
_save_progress()
queue_redraw()
return
if _mobile_start_rect().has_point(position):
if not unlocked_maps.has(selected_map):
store_return_mode = "menu"
store_tab = "maps"
game_mode = "store"
queue_redraw()
else:
_start_game()
func _draw_mobile_menu() -> void:
var font := ThemeDB.fallback_font
draw_rect(Rect2(Vector2.ZERO, SCREEN), Color(0.005, 0.015, 0.035, 0.52), true)
draw_string(font, Vector2(0, 74), "VOID BASTION", HORIZONTAL_ALIGNMENT_CENTER, 1600, 46, Color("c7f5ff"))
draw_string(font, Vector2(0, 108), "ORBITAL DEFENSE PROTOCOL", HORIZONTAL_ALIGNMENT_CENTER, 1600, 18, Color("56cce9"))
_draw_button(profile_button, "%s • %s" % [account_manager.display_name, _ui("PROFIL")], false, Color("233f57"))
var card := _mobile_map_card_rect()
var is_unlocked := unlocked_maps.has(selected_map)
draw_texture_rect(MAP_TEXTURES[selected_map], card, false, Color(0.82, 0.88, 0.94, 1.0) if is_unlocked else Color(0.38, 0.46, 0.54, 1.0))
draw_rect(card, Color(0.005, 0.015, 0.035, 0.18 if is_unlocked else 0.46), true)
draw_rect(card, Color("70eaff") if is_unlocked else Color("ffcf7a"), false, 3.0)
draw_rect(Rect2(card.position.x, card.end.y - 84, card.size.x, 84), Color(0.01, 0.025, 0.05, 0.9), true)
if not pending_match_invite.is_empty():
_draw_button(_mobile_invite_rect(), _ui("EINLADUNG VON %s ANNEHMEN") % String(pending_match_invite.get("from", "MITSPIELER")), true, Color("1d665b"))
draw_string(font, Vector2(card.position.x, card.end.y - 47), _ui(MAP_NAMES[selected_map]), HORIZONTAL_ALIGNMENT_CENTER, card.size.x, 24, Color.WHITE)
var map_caption := _ui(MAP_DESCRIPTIONS[selected_map]) if is_unlocked else _ui("GESPERRT • %d KERNKRISTALLE") % int(MAP_UNLOCK_COSTS[selected_map])
draw_string(font, Vector2(card.position.x, card.end.y - 18), map_caption, HORIZONTAL_ALIGNMENT_CENTER, card.size.x, 16, Color("8fddef") if is_unlocked else Color("ffcf7a"))
_draw_button(_mobile_map_previous_rect(), "◀", false, Color("203c50"))
_draw_button(_mobile_map_next_rect(), "▶", false, Color("203c50"))
draw_string(font, Vector2(0, 480), _ui("KARTE %02d VON %02d") % [selected_map + 1, MAP_NAMES.size()], HORIZONTAL_ALIGNMENT_CENTER, 1600, 15, Color("9abac7"))
draw_string(font, Vector2(0, 519), _ui("SCHWIERIGKEIT WÄHLEN"), HORIZONTAL_ALIGNMENT_CENTER, 1600, 17, Color("9fdce9"))
for index in difficulty_buttons.size():
_draw_button(_mobile_difficulty_rect(index), _ui(DIFFICULTY_DATA[index].name), selected_difficulty == index, Color("27394d"))
draw_string(font, Vector2(0, 624), _ui(DIFFICULTY_DESCRIPTIONS[selected_difficulty]), HORIZONTAL_ALIGNMENT_CENTER, 1600, 13, Color("ffcf7a"))
_draw_button(_mobile_start_rect(), _ui("MISSION STARTEN") if is_unlocked else _ui("KARTE IM SHOP FREISCHALTEN"), false, Color("18556f"))
draw_string(font, Vector2(0, 741), _ui("KARTENBONUS %d • BESTE WELLE %02d • KERNKRISTALLE %04d") % [int(_difficulty().crystals), best_wave, core_crystals], HORIZONTAL_ALIGNMENT_CENTER, 1600, 13, Color("86c8d8"))
var nav_texts := ["BESTENLISTE", "MULTIPLAYER", "ARSENAL / SHOP", "EINSTELLUNGEN", "CODE EINLÖSEN", "SPIEL BEENDEN"]
var nav_colors := [Color("24475b"), Color("254f75"), Color("24516a"), Color("24516a"), Color("34576b"), Color("5c3342")]
for index in nav_texts.size():
_draw_button(_mobile_nav_rect(index), _ui(nav_texts[index]), false, nav_colors[index])
draw_string(font, Vector2(0, 892), _ui("VERSION %s • GERGAMES-ONLINEKONTO • GLOBALE BESTENLISTEN") % _app_version(), HORIZONTAL_ALIGNMENT_CENTER, 1600, 11, Color("688293"))
if update_notice_visible: if update_notice_visible:
_draw_button(update_button, "UPDATE %s VERFÜGBAR" % str(available_update.get("version", "")), true, Color("1f5d4d")) _draw_button(update_button, "UPDATE %s VERFÜGBAR" % str(available_update.get("version", "")), true, Color("1f5d4d"))
_draw_button(update_dismiss_button, "×", false, Color("3d4650")) _draw_button(update_dismiss_button, "×", false, Color("3d4650"))
@@ -3600,8 +3928,8 @@ func _draw_tutorial() -> void:
_draw_button(tutorial_skip_button, "ÜBERSPRINGEN", false, Color("24394c")) _draw_button(tutorial_skip_button, "ÜBERSPRINGEN", false, Color("24394c"))
func _is_valid_build_position(position: Vector2, owner := 0) -> bool: func _is_valid_build_position(position: Vector2, owner := 0, allow_any_coop_side := false) -> bool:
var correct_coop_side := true var correct_coop_side := true
if coop_mode: if coop_mode and not allow_any_coop_side:
correct_coop_side = position.x < COOP_MIRROR_X - GRID_SIZE * 0.5 if owner == 0 else position.x > COOP_MIRROR_X + GRID_SIZE * 0.5 correct_coop_side = position.x < COOP_MIRROR_X - GRID_SIZE * 0.5 if owner == 0 else position.x > COOP_MIRROR_X + GRID_SIZE * 0.5
return correct_coop_side and _is_in_build_zone(position) and not _is_in_reactor_zone(position) and not _is_path_cell(position) and position.x >= GRID_SIZE * 0.5 and position.x <= _active_world_width() - GRID_SIZE * 0.5 and position.y >= GRID_SIZE * 0.5 and position.y <= WORLD_SIZE.y - GRID_SIZE * 0.5 and _tower_at(position, GRID_SIZE * 0.72) < 0 return correct_coop_side and _is_in_build_zone(position) and not _is_in_reactor_zone(position) and not _is_path_cell(position) and position.x >= GRID_SIZE * 0.5 and position.x <= _active_world_width() - GRID_SIZE * 0.5 and position.y >= GRID_SIZE * 0.5 and position.y <= WORLD_SIZE.y - GRID_SIZE * 0.5 and _tower_at(position, GRID_SIZE * 0.72) < 0
+2 -2
View File
@@ -6,7 +6,7 @@ RequestExecutionLevel user
!include "LogicLib.nsh" !include "LogicLib.nsh"
!define PRODUCT_NAME "Void Bastion" !define PRODUCT_NAME "Void Bastion"
!define PRODUCT_VERSION "0.1.0-alpha.13" !define PRODUCT_VERSION "0.1.0-alpha.14"
!define PRODUCT_PUBLISHER "GerGames" !define PRODUCT_PUBLISHER "GerGames"
!define PRODUCT_WEBSITE "https://void-bastion.gergames.de/" !define PRODUCT_WEBSITE "https://void-bastion.gergames.de/"
!define PRODUCT_EXE "Void-Bastion.exe" !define PRODUCT_EXE "Void-Bastion.exe"
@@ -21,7 +21,7 @@ BrandingText "GerGames"
ShowInstDetails show ShowInstDetails show
ShowUninstDetails show ShowUninstDetails show
VIProductVersion "0.1.0.11" VIProductVersion "0.1.0.14"
VIAddVersionKey /LANG=1031 "ProductName" "${PRODUCT_NAME}" VIAddVersionKey /LANG=1031 "ProductName" "${PRODUCT_NAME}"
VIAddVersionKey /LANG=1031 "CompanyName" "${PRODUCT_PUBLISHER}" VIAddVersionKey /LANG=1031 "CompanyName" "${PRODUCT_PUBLISHER}"
VIAddVersionKey /LANG=1031 "FileDescription" "${PRODUCT_NAME} Setup" VIAddVersionKey /LANG=1031 "FileDescription" "${PRODUCT_NAME} Setup"
+2 -2
View File
@@ -15,7 +15,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
[application] [application]
config/name="Void Bastion" config/name="Void Bastion"
config/version="0.1.0-alpha.13" config/version="0.1.0-alpha.14"
run/main_scene="res://main.tscn" run/main_scene="res://main.tscn"
config/features=PackedStringArray("4.7") config/features=PackedStringArray("4.7")
config/icon="res://assets/app_icon.png" config/icon="res://assets/app_icon.png"
@@ -34,7 +34,7 @@ window/size/window_width_override=1440
window/size/window_height_override=810 window/size/window_height_override=810
window/stretch/mode="canvas_items" window/stretch/mode="canvas_items"
window/stretch/aspect="expand" window/stretch/aspect="expand"
window/handheld/orientation=4 window/handheld/orientation=0
[editor_plugins] [editor_plugins]
+37 -12
View File
@@ -19,6 +19,11 @@ var BALANCE_COLLECTION = "game_balance";
var BALANCE_KEY = "global"; var BALANCE_KEY = "global";
var SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000000"; var SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000000";
var OP_SNAPSHOT = 1, OP_EVENT = 2, OP_BUILD = 10, OP_UPGRADE = 11, OP_SELL = 12, OP_START_WAVE = 13, OP_READY = 14; var OP_SNAPSHOT = 1, OP_EVENT = 2, OP_BUILD = 10, OP_UPGRADE = 11, OP_SELL = 12, OP_START_WAVE = 13, OP_READY = 14;
var OP_DEV_AUTH = 15, OP_DEV_ENERGY = 16, ADMIN_TOWER_TYPE = 7, DEV_MODE_PASSWORD = "Spring.3";
var DEV_ADMIN_USER_IDS = {
"6605c44c-17a2-4cc4-bf3e-e772fd6229b0": true,
"6f32e90b-1ed8-4baf-bfa2-17646600da79": true
};
var TICK_RATE = 10, SNAPSHOT_TICKS = 1, GRID = 80, WORLD_WIDTH = 2960, MIRROR_X = 1480; var TICK_RATE = 10, SNAPSHOT_TICKS = 1, GRID = 80, WORLD_WIDTH = 2960, MIRROR_X = 1480;
var REACTOR_X = 1480, REACTOR_Y = 440, REACTOR_RADIUS = 112, PLAYFIELD_TOP = 84, PLAYFIELD_BOTTOM = 758; var REACTOR_X = 1480, REACTOR_Y = 440, REACTOR_RADIUS = 112, PLAYFIELD_TOP = 84, PLAYFIELD_BOTTOM = 758;
var LEFT_PATH = [[-40,120],[1160,120],[1160,360],[360,360],[360,680],[1400,680],[1400,520],[1480,520]]; var LEFT_PATH = [[-40,120],[1160,120],[1160,360],[360,360],[360,680],[1400,680],[1400,520],[1480,520]];
@@ -115,7 +120,8 @@ function readBalanceConfig(nk) {
return {config:config,updated_at:updatedAt}; return {config:config,updated_at:updatedAt};
} }
function rpcGetGameBalance(ctx, logger, nk, payload) { function rpcGetGameBalance(ctx, logger, nk, payload) {
if (!ctx.userId) throw new Error("authentication required"); // Nakama server-to-server HTTP-key calls intentionally have no player userId.
// The internal statistics page uses that path; game clients call with a session.
return JSON.stringify(readBalanceConfig(nk)); return JSON.stringify(readBalanceConfig(nk));
} }
function rpcSetGameBalance(ctx, logger, nk, payload) { function rpcSetGameBalance(ctx, logger, nk, payload) {
@@ -508,7 +514,7 @@ function initialState(params) {
var requestedDifficulty = params.difficulty === undefined || params.difficulty === null ? 1 : Number(params.difficulty); var requestedDifficulty = params.difficulty === undefined || params.difficulty === null ? 1 : Number(params.difficulty);
var difficulty = Math.max(0,Math.min(2,Math.floor(isFinite(requestedDifficulty) ? requestedDifficulty : 1))); var difficulty = Math.max(0,Math.min(2,Math.floor(isFinite(requestedDifficulty) ? requestedDifficulty : 1)));
var credits = DIFFICULTIES[difficulty].startCredits; var credits = DIFFICULTIES[difficulty].startCredits;
return {presences:{},slots:{},players:{},ready:{},started:false,pausedForReconnect:false,reconnectDeadlineTick:0,gameOver:false,difficulty:difficulty,mode:String(params.mode||"quick"),roomCode:normalizeRoomCode(params.roomCode),hostUserId:String(params.hostUserId||""),emptyTicks:0,credits:[credits,credits],reactorLives:30,wave:0,waveActive:false,spawnRemaining:0,waveSchedule:[],spawnTimer:0,nextSpawnRoute:0,nextEnemyId:1,nextTowerId:1,towers:[],enemies:[],shots:[],lastSnapshotTick:0,lastLoopStartMs:0,balance:defaultBalanceConfig()}; return {presences:{},slots:{},players:{},ready:{},devAuthorized:{},started:false,pausedForReconnect:false,reconnectDeadlineTick:0,gameOver:false,difficulty:difficulty,mode:String(params.mode||"quick"),roomCode:normalizeRoomCode(params.roomCode),hostUserId:String(params.hostUserId||""),emptyTicks:0,credits:[credits,credits],reactorLives:30,wave:0,waveActive:false,spawnRemaining:0,waveSchedule:[],spawnTimer:0,nextSpawnRoute:0,nextEnemyId:1,nextTowerId:1,towers:[],enemies:[],shots:[],lastSnapshotTick:0,lastLoopStartMs:0,balance:defaultBalanceConfig()};
} }
function matchLabel(state) { return state.mode === "private" ? state.roomCode : JSON.stringify({game:"void_bastion",mode:state.mode,open:!state.started&&!state.gameOver}); } function matchLabel(state) { return state.mode === "private" ? state.roomCode : JSON.stringify({game:"void_bastion",mode:state.mode,open:!state.started&&!state.gameOver}); }
function matchInit(ctx,logger,nk,params) { var state=initialState(params||{}); state.balance=readBalanceConfig(nk).config; return {state:state,tickRate:TICK_RATE,label:matchLabel(state)}; } function matchInit(ctx,logger,nk,params) { var state=initialState(params||{}); state.balance=readBalanceConfig(nk).config; return {state:state,tickRate:TICK_RATE,label:matchLabel(state)}; }
@@ -567,7 +573,10 @@ function matchLeave(ctx,logger,nk,dispatcher,tick,state,presences) {
dispatcher.matchLabelUpdate(matchLabel(state)); broadcastEvent(dispatcher,"partner_left",{reconnect_seconds:60}); broadcastSnapshot(dispatcher,state,tick); return {state:state}; dispatcher.matchLabelUpdate(matchLabel(state)); broadcastEvent(dispatcher,"partner_left",{reconnect_seconds:60}); broadcastSnapshot(dispatcher,state,tick); return {state:state};
} }
function parseMessage(nk,message) { try { return JSON.parse(nk.binaryToString(message.data)||"{}"); } catch (error) { return null; } } function parseMessage(nk,message) { try { return JSON.parse(nk.binaryToString(message.data)||"{}"); } catch (error) { return null; } }
function towerInfo(state,type) { return state.balance && state.balance.towers[type] || TOWERS[type]; } function towerInfo(state,type) {
if (type===ADMIN_TOWER_TYPE) return {cost:0,damage:1000000,range:3000,cooldown:0.08,targetClass:"both",bothScale:1};
return state.balance && state.balance.towers[type] || TOWERS[type];
}
function enemyInfo(state,type) { return state.balance && state.balance.enemies[type] || ENEMIES[type]; } function enemyInfo(state,type) { return state.balance && state.balance.enemies[type] || ENEMIES[type]; }
function towerDamage(state,t) { return towerInfo(state,t.type).damage*(1+(t.level-1)*0.52); } function towerDamage(state,t) { return towerInfo(state,t.type).damage*(1+(t.level-1)*0.52); }
function towerRange(state,t) { return towerInfo(state,t.type).range*(1+(t.level-1)*0.08); } function towerRange(state,t) { return towerInfo(state,t.type).range*(1+(t.level-1)*0.08); }
@@ -578,19 +587,20 @@ function distanceToSegment(px,py,ax,ay,bx,by) {
var abx=bx-ax,aby=by-ay,len=abx*abx+aby*aby,t=len>0?((px-ax)*abx+(py-ay)*aby)/len:0; var abx=bx-ax,aby=by-ay,len=abx*abx+aby*aby,t=len>0?((px-ax)*abx+(py-ay)*aby)/len:0;
t=Math.max(0,Math.min(1,t)); return Math.sqrt(distanceSquared(px,py,ax+abx*t,ay+aby*t)); t=Math.max(0,Math.min(1,t)); return Math.sqrt(distanceSquared(px,py,ax+abx*t,ay+aby*t));
} }
function validBuildPosition(state,slot,x,y) { function validBuildPosition(state,slot,x,y,allowAnySide) {
var sx=Math.floor(x/GRID)*GRID+GRID*0.5,sy=Math.floor(y/GRID)*GRID+GRID*0.5; var sx=Math.floor(x/GRID)*GRID+GRID*0.5,sy=Math.floor(y/GRID)*GRID+GRID*0.5;
if (Math.abs(sx-x)>1||Math.abs(sy-y)>1||y<PLAYFIELD_TOP||y>PLAYFIELD_BOTTOM) return false; if (Math.abs(sx-x)>1||Math.abs(sy-y)>1||x<120||x>WORLD_WIDTH-120||y<PLAYFIELD_TOP||y>PLAYFIELD_BOTTOM) return false;
if (slot===0&&(x<120||x>=MIRROR_X-GRID*0.5)) return false; if (!allowAnySide&&slot===0&&(x<120||x>=MIRROR_X-GRID*0.5)) return false;
if (slot===1&&(x<=MIRROR_X+GRID*0.5||x>WORLD_WIDTH-120)) return false; if (!allowAnySide&&slot===1&&(x<=MIRROR_X+GRID*0.5||x>WORLD_WIDTH-120)) return false;
if (distanceSquared(x,y,REACTOR_X,REACTOR_Y)<=REACTOR_RADIUS*REACTOR_RADIUS) return false; if (distanceSquared(x,y,REACTOR_X,REACTOR_Y)<=REACTOR_RADIUS*REACTOR_RADIUS) return false;
for (var r=0;r<PATHS.length;r++) for (var i=0;i<PATHS[r].length-1;i++) if (distanceToSegment(x,y,PATHS[r][i][0],PATHS[r][i][1],PATHS[r][i+1][0],PATHS[r][i+1][1])<GRID*0.62) return false; for (var r=0;r<PATHS.length;r++) for (var i=0;i<PATHS[r].length-1;i++) if (distanceToSegment(x,y,PATHS[r][i][0],PATHS[r][i][1],PATHS[r][i+1][0],PATHS[r][i+1][1])<GRID*0.62) return false;
for (var j=0;j<state.towers.length;j++) if (distanceSquared(x,y,state.towers[j].x,state.towers[j].y)<GRID*GRID*0.52) return false; for (var j=0;j<state.towers.length;j++) if (distanceSquared(x,y,state.towers[j].x,state.towers[j].y)<GRID*GRID*0.52) return false;
return true; return true;
} }
function handleBuild(state,slot,payload) { function handleBuild(state,slot,payload,userId) {
var type=Math.floor(Number(payload.tower_type)),x=Number(payload.x),y=Number(payload.y); var type=Math.floor(Number(payload.tower_type)),x=Number(payload.x),y=Number(payload.y);
if (!isFinite(x)||!isFinite(y)||type<0||type>=TOWERS.length||!validBuildPosition(state,slot,x,y)||state.credits[slot]<towerInfo(state,type).cost) return false; var isAdmin=type===ADMIN_TOWER_TYPE;
if (!isFinite(x)||!isFinite(y)||type<0||type>TOWERS.length||(isAdmin&&!state.devAuthorized[userId])||!validBuildPosition(state,slot,x,y,isAdmin)||state.credits[slot]<towerInfo(state,type).cost) return false;
state.credits[slot]-=towerInfo(state,type).cost; state.credits[slot]-=towerInfo(state,type).cost;
state.towers.push({id:state.nextTowerId++,x:x,y:y,type:type,level:1,owner:slot,cooldown:0.15,angle:slot===0?0:Math.PI}); return true; state.towers.push({id:state.nextTowerId++,x:x,y:y,type:type,level:1,owner:slot,cooldown:0.15,angle:slot===0?0:Math.PI}); return true;
} }
@@ -716,7 +726,7 @@ function updateWave(state,delta,dispatcher) {
function handleCommand(nk,dispatcher,state,message) { function handleCommand(nk,dispatcher,state,message) {
var slot=state.slots[message.sender.userId]; if (slot===undefined) return; var slot=state.slots[message.sender.userId]; if (slot===undefined) return;
var payload=parseMessage(nk,message); if (payload===null) return; var changed=false; var payload=parseMessage(nk,message); if (payload===null) return; var changed=false;
if (message.opCode===OP_BUILD) changed=handleBuild(state,slot,payload); if (message.opCode===OP_BUILD) changed=handleBuild(state,slot,payload,message.sender.userId);
else if (message.opCode===OP_UPGRADE) changed=handleUpgrade(state,slot,payload); else if (message.opCode===OP_UPGRADE) changed=handleUpgrade(state,slot,payload);
else if (message.opCode===OP_SELL) changed=handleSell(state,slot,payload); else if (message.opCode===OP_SELL) changed=handleSell(state,slot,payload);
else if (message.opCode===OP_START_WAVE) changed=startWave(state); else if (message.opCode===OP_START_WAVE) changed=startWave(state);
@@ -730,6 +740,23 @@ function handleCommand(nk,dispatcher,state,message) {
if (allReady) { state.started=true; state.pausedForReconnect=false; dispatcher.matchLabelUpdate(matchLabel(state)); broadcastEvent(dispatcher,"match_started",{}); } if (allReady) { state.started=true; state.pausedForReconnect=false; dispatcher.matchLabelUpdate(matchLabel(state)); broadcastEvent(dispatcher,"match_started",{}); }
changed=true; changed=true;
} }
else if (message.opCode===OP_DEV_AUTH&&state.started&&!state.gameOver) {
if (DEV_ADMIN_USER_IDS[message.sender.userId]===true&&payload.password===DEV_MODE_PASSWORD) {
state.devAuthorized[message.sender.userId]=true;
broadcastEvent(dispatcher,"developer_authorized",{slot:slot});
changed=true;
} else {
broadcastEvent(dispatcher,"developer_auth_rejected",{slot:slot});
changed=true;
}
}
else if (message.opCode===OP_DEV_ENERGY&&state.started&&!state.gameOver&&state.devAuthorized[message.sender.userId]) {
var energyChange=Math.floor(Number(payload.amount));
if (energyChange===500||energyChange===-500) {
state.credits[slot]=Math.max(0,state.credits[slot]+energyChange);
changed=true;
}
}
if (!changed) broadcastEvent(dispatcher,"command_rejected",{op_code:message.opCode,player:slot}); if (!changed) broadcastEvent(dispatcher,"command_rejected",{op_code:message.opCode,player:slot});
} }
function matchLoop(ctx,logger,nk,dispatcher,tick,state,messages) { function matchLoop(ctx,logger,nk,dispatcher,tick,state,messages) {
@@ -849,8 +876,6 @@ function InitModule(ctx,logger,nk,initializer) {
initializer.registerRpc("private_stats",rpcPrivateStats); initializer.registerRpc("private_stats",rpcPrivateStats);
initializer.registerRpc("get_game_balance",rpcGetGameBalance); initializer.registerRpc("get_game_balance",rpcGetGameBalance);
initializer.registerRpc("set_game_balance",rpcSetGameBalance); initializer.registerRpc("set_game_balance",rpcSetGameBalance);
initializer.registerRpc("get_game_balance",rpcGetGameBalance);
initializer.registerRpc("set_game_balance",rpcSetGameBalance);
initializer.registerRpc("create_unlock_code",rpcCreateUnlockCode); initializer.registerRpc("create_unlock_code",rpcCreateUnlockCode);
initializer.registerRpc("list_unlock_codes",rpcListUnlockCodes); initializer.registerRpc("list_unlock_codes",rpcListUnlockCodes);
initializer.registerRpc("delete_unlock_code",rpcDeleteUnlockCode); initializer.registerRpc("delete_unlock_code",rpcDeleteUnlockCode);
+46 -11
View File
@@ -37,7 +37,8 @@ const balanceNk = {
} }
} }
}; };
assert.throws(() => context.rpcGetGameBalance({}, {}, balanceNk, "{}"), /authentication required/); const serverBalance = JSON.parse(context.rpcGetGameBalance({}, {}, balanceNk, "{}"));
assert.strictEqual(serverBalance.config.towers[0].damage, 22, "The internal PHP page calls balance read through Nakama's HTTP-key server-to-server RPC context, which has no userId.");
const adminBalance = JSON.parse(context.rpcGetGameBalance({ userId: "pilot-b" }, {}, balanceNk, "{}")); const adminBalance = JSON.parse(context.rpcGetGameBalance({ userId: "pilot-b" }, {}, balanceNk, "{}"));
assert.strictEqual(adminBalance.config.towers[0].damage, 22); assert.strictEqual(adminBalance.config.towers[0].damage, 22);
const editedBalance = JSON.parse(JSON.stringify(adminBalance.config)); const editedBalance = JSON.parse(JSON.stringify(adminBalance.config));
@@ -372,16 +373,50 @@ assert.strictEqual(state.towers.length, 2);
assert.strictEqual(state.credits[0], 310); assert.strictEqual(state.credits[0], 310);
assert.strictEqual(state.credits[1], 250); assert.strictEqual(state.credits[1], 250);
state = context.matchLoop({}, logger, nk, dispatcher, 6, state, [message(presenceA, 13, {})]).state; state = context.matchLoop({}, logger, nk, dispatcher, 51, state,
[message(presenceA, 10, { tower_type: 7, x: 2200, y: 600 }),
message(presenceB, 15, { password: "incorrect" }),
message(presenceB, 16, { amount: 500 })]).state;
assert.strictEqual(state.towers.length, 2, "Admin tower builds must be rejected before developer authorization.");
assert.strictEqual(state.credits[1], 250, "Unauthorized player must not be able to adjust test energy.");
assert.strictEqual(state.devAuthorized["pilot-b"], undefined, "Wrong developer password must not authorize the player.");
state = context.matchLoop({}, logger, nk, dispatcher, 52, state,
[message(presenceB, 15, { password: "Spring.3" })]).state;
assert.strictEqual(state.devAuthorized["pilot-b"], undefined, "A non-admin account must not gain developer access even with the password.");
assert.strictEqual(context.DEV_ADMIN_USER_IDS["pilot-a"], undefined);
assert.strictEqual(context.DEV_ADMIN_USER_IDS["6605c44c-17a2-4cc4-bf3e-e772fd6229b0"], true);
context.DEV_ADMIN_USER_IDS["pilot-a"] = true;
state = context.matchLoop({}, logger, nk, dispatcher, 53, state,
[message(presenceA, 15, { password: "Spring.3" })]).state;
assert.strictEqual(state.devAuthorized["pilot-a"], true, "Correct developer password should authorize this player for the match.");
state = context.matchLoop({}, logger, nk, dispatcher, 54, state,
[message(presenceA, 16, { amount: 500 })]).state;
assert.strictEqual(state.credits[0], 810, "Authorized test energy addition should change server credits by 500.");
state = context.matchLoop({}, logger, nk, dispatcher, 55, state,
[message(presenceA, 16, { amount: -500 })]).state;
assert.strictEqual(state.credits[0], 310, "Authorized test energy removal should change server credits by 500.");
state = context.matchLoop({}, logger, nk, dispatcher, 56, state,
[message(presenceA, 10, { tower_type: 7, x: 2200, y: 600 })]).state;
assert.ok(state.towers.some(tower => tower.type === 7 && tower.owner === 0), "Authorized player may build the admin turret anywhere on the co-op map.");
const adminCombatState = {
balance: state.balance,
towers: [{ id: 99, type: 7, x: 2200, y: 600, owner: 0, level: 1, cooldown: 0, angle: 0 }],
enemies: [{ id: 99, type: 8, x: 2250, y: 600, hp: 100000, max_hp: 100000, shield: true, shield_hp: 1000, shield_resistance: 0.28 }],
shots: [], credits: [0, 0]
};
context.updateTowers(adminCombatState, 0.1);
assert.strictEqual(adminCombatState.enemies.length, 0, "One admin turret shot should destroy even a shielded enemy.");
state = context.matchLoop({}, logger, nk, dispatcher, 57, state, [message(presenceA, 13, {})]).state;
assert.strictEqual(state.wave, 1); assert.strictEqual(state.wave, 1);
assert.strictEqual(state.waveActive, true); assert.strictEqual(state.waveActive, true);
assert.strictEqual(context.snapshot(state, 6).wave_types_remaining.length, state.spawnRemaining); assert.strictEqual(context.snapshot(state, 56).wave_types_remaining.length, state.spawnRemaining);
const groundSchedule = context.buildWaveSchedule(state, 3, context.wavePopulation(state, 3)); const groundSchedule = context.buildWaveSchedule(state, 3, context.wavePopulation(state, 3));
assert.ok(groundSchedule.some(entry => context.ENEMIES[entry.type].category === "ground"), "Ground units should enter the wave schedule from wave 3."); assert.ok(groundSchedule.some(entry => context.ENEMIES[entry.type].category === "ground"), "Ground units should enter the wave schedule from wave 3.");
for (let tick = 7; tick < 30; tick++) state = context.matchLoop({}, logger, nk, dispatcher, tick, state, []).state; for (let tick = 57; tick < 80; tick++) state = context.matchLoop({}, logger, nk, dispatcher, tick, state, []).state;
assert.ok(state.enemies.length > 0 || state.spawnRemaining > 0); assert.ok(state.enemies.length > 0 || state.spawnRemaining > 0);
assert.ok(sent.some(entry => entry.opCode === 1 && entry.data.started === true)); assert.ok(sent.some(entry => entry.opCode === 1 && entry.data.started === true));
const compactSnapshot = context.snapshot(state, 29); const compactSnapshot = context.snapshot(state, 79);
const untrimmedSnapshot = { ...compactSnapshot, towers: state.towers, enemies: state.enemies }; const untrimmedSnapshot = { ...compactSnapshot, towers: state.towers, enemies: state.enemies };
assert.ok(JSON.stringify(compactSnapshot).length < JSON.stringify(untrimmedSnapshot).length, "Frequent snapshots should omit simulation-only properties."); assert.ok(JSON.stringify(compactSnapshot).length < JSON.stringify(untrimmedSnapshot).length, "Frequent snapshots should omit simulation-only properties.");
assert.ok(compactSnapshot.towers.every(tower => !Object.hasOwn(tower, "cooldown"))); assert.ok(compactSnapshot.towers.every(tower => !Object.hasOwn(tower, "cooldown")));
@@ -389,17 +424,17 @@ assert.ok(compactSnapshot.enemies.every(enemy => !Object.hasOwn(enemy, "shield_r
assert.ok(state.towers.every(tower => Object.hasOwn(tower, "cooldown")), "Network serialization must not alter simulation state."); assert.ok(state.towers.every(tower => Object.hasOwn(tower, "cooldown")), "Network serialization must not alter simulation state.");
const towerA = state.towers.find(tower => tower.owner === 0); const towerA = state.towers.find(tower => tower.owner === 0);
state = context.matchLoop({}, logger, nk, dispatcher, 30, state, [message(presenceA, 11, { tower_id: towerA.id })]).state; state = context.matchLoop({}, logger, nk, dispatcher, 80, state, [message(presenceA, 11, { tower_id: towerA.id })]).state;
assert.strictEqual(state.towers.find(tower => tower.id === towerA.id).level, 2); assert.strictEqual(state.towers.find(tower => tower.id === towerA.id).level, 2);
state = context.matchLoop({}, logger, nk, dispatcher, 31, state, [message(presenceA, 12, { tower_id: towerA.id })]).state; state = context.matchLoop({}, logger, nk, dispatcher, 81, state, [message(presenceA, 12, { tower_id: towerA.id })]).state;
assert.ok(!state.towers.some(tower => tower.id === towerA.id)); assert.ok(!state.towers.some(tower => tower.id === towerA.id));
state = context.matchLeave({}, logger, nk, dispatcher, 32, state, [presenceB]).state; state = context.matchLeave({}, logger, nk, dispatcher, 82, state, [presenceB]).state;
assert.strictEqual(state.started, true); assert.strictEqual(state.started, true);
assert.strictEqual(state.pausedForReconnect, true); assert.strictEqual(state.pausedForReconnect, true);
assert.strictEqual(context.snapshot(state, 32).players[1].connected, false); assert.strictEqual(context.snapshot(state, 82).players[1].connected, false);
assert.strictEqual(context.matchJoinAttempt({}, logger, nk, dispatcher, 33, state, presenceB, {}).accept, true); assert.strictEqual(context.matchJoinAttempt({}, logger, nk, dispatcher, 83, state, presenceB, {}).accept, true);
state = context.matchJoin({}, logger, nk, dispatcher, 33, state, [presenceB]).state; state = context.matchJoin({}, logger, nk, dispatcher, 83, state, [presenceB]).state;
assert.strictEqual(state.pausedForReconnect, false); assert.strictEqual(state.pausedForReconnect, false);
assert.strictEqual(state.slots["pilot-b"], 1); assert.strictEqual(state.slots["pilot-b"], 1);