Add scaling Alien Grunt boss waves and native simulation; publish Web release
This commit is contained in:
+145
-3
@@ -1,6 +1,8 @@
|
||||
extends Node3D
|
||||
const PlayerScript = preload("res://scripts/player.gd")
|
||||
const ZombieScript = preload("res://scripts/zombie.gd")
|
||||
const BossScript = preload("res://scripts/alien_grunt.gd")
|
||||
const BossTeleport = preload("res://scripts/boss_teleport.gd")
|
||||
const RoomScript = preload("res://scripts/room.gd")
|
||||
const LeaderboardScript = preload("res://scripts/leaderboard.gd")
|
||||
const StartScreenScript = preload("res://scripts/start_screen.gd")
|
||||
@@ -26,15 +28,28 @@ var leaderboard: Node
|
||||
var start_screen: CanvasLayer
|
||||
var profile := {}
|
||||
var score_submitted := false
|
||||
var boss: CharacterBody3D
|
||||
var boss_simulation := false
|
||||
var simulation_wave := 0
|
||||
var simulation_god := false
|
||||
var simulation_finished := false
|
||||
var simulation_restarting := false
|
||||
var simulation_label: Label
|
||||
var simulation_retry: Button
|
||||
|
||||
func _ready() -> void:
|
||||
smoke = "--smoke" in OS.get_cmdline_user_args()
|
||||
profile = Profile.load_profile()
|
||||
if not _configure_simulation():
|
||||
return
|
||||
# A simulation must never read/write the real player's profile or public scores.
|
||||
profile = {"name": "SIMULAZIONE", "player_id": "boss-simulation", "best": 0} \
|
||||
if boss_simulation else Profile.load_profile()
|
||||
room = Node3D.new()
|
||||
room.set_script(RoomScript)
|
||||
add_child(room)
|
||||
player = CharacterBody3D.new()
|
||||
player.set_script(PlayerScript)
|
||||
player.invulnerable = boss_simulation and simulation_god
|
||||
player.position = Vector3(0, 0.1, 0)
|
||||
add_child(player)
|
||||
var envnode := WorldEnvironment.new()
|
||||
@@ -89,6 +104,12 @@ func _ready() -> void:
|
||||
leaderboard.score_submitted.connect(_on_score_submitted)
|
||||
leaderboard.submit_failed.connect(_on_submit_failed)
|
||||
player.died.connect(_on_player_died)
|
||||
if boss_simulation:
|
||||
leaderboard.disabled = true
|
||||
leaderboard.base_url = ""
|
||||
_setup_simulation_ui()
|
||||
call_deferred("_start_simulation")
|
||||
return
|
||||
start_screen = CanvasLayer.new()
|
||||
start_screen.set_script(StartScreenScript)
|
||||
start_screen.player = player
|
||||
@@ -112,7 +133,7 @@ func _on_start_pressed(player_name: String) -> void:
|
||||
|
||||
func start_game(player_name: String, save_profile := true) -> void:
|
||||
profile["name"] = player_name
|
||||
if save_profile:
|
||||
if save_profile and not boss_simulation:
|
||||
Profile.save_profile(profile)
|
||||
if not player.touch_mode:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
@@ -126,6 +147,11 @@ func start_game(player_name: String, save_profile := true) -> void:
|
||||
|
||||
## Alla morte si invia UNA volta il punteggio finale con nome e id del giocatore.
|
||||
func _on_player_died() -> void:
|
||||
if boss_simulation:
|
||||
simulation_finished = false
|
||||
simulation_retry.hide()
|
||||
player.set_death_note("SIMULAZIONE — R / RICOMINCIA per riprovare; nessun punteggio inviato")
|
||||
return
|
||||
if score_submitted:
|
||||
return
|
||||
score_submitted = true
|
||||
@@ -199,9 +225,13 @@ func _restart_music() -> void:
|
||||
func _process(_dt: float) -> void:
|
||||
if hud:
|
||||
hud.update_status(player, wave, score)
|
||||
hud.update_boss(boss)
|
||||
|
||||
static func is_boss_wave(number: int) -> bool:
|
||||
return number > 0 and number % 10 == 0
|
||||
|
||||
func zombies_for_wave(number: int) -> int:
|
||||
return 4 + number * 2
|
||||
return 0 if is_boss_wave(number) else 4 + number * 2
|
||||
|
||||
func _next_wave() -> void:
|
||||
wave_pending = false
|
||||
@@ -209,6 +239,12 @@ func _next_wave() -> void:
|
||||
return
|
||||
wave += 1
|
||||
spawning = true
|
||||
if is_boss_wave(wave):
|
||||
_spawn_boss()
|
||||
spawning = false
|
||||
_check_wave()
|
||||
return
|
||||
boss = null
|
||||
for i in zombies_for_wave(wave):
|
||||
if player.hp <= 0:
|
||||
break
|
||||
@@ -265,4 +301,110 @@ func _spawn() -> void:
|
||||
func _check_wave() -> void:
|
||||
if alive == 0 and not spawning and not wave_pending and player.hp > 0:
|
||||
wave_pending = true
|
||||
if boss_simulation:
|
||||
simulation_finished = true
|
||||
simulation_label.text = "BOSS SCONFITTO — ondata %d | R o RIPROVA per ricominciare" % wave
|
||||
simulation_retry.show()
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
return
|
||||
get_tree().create_timer(3).timeout.connect(_next_wave)
|
||||
|
||||
func _boss_spawn_position() -> Vector3:
|
||||
var candidates := [Vector3(0, 0.05, -6), Vector3(6, 0.05, 0),
|
||||
Vector3(0, 0.05, 6), Vector3(-6, 0.05, 0), Vector3(6, 0.05, 6),
|
||||
Vector3(-6, 0.05, -6), Vector3(6, 0.05, -6), Vector3(-6, 0.05, 6)]
|
||||
candidates.shuffle()
|
||||
var capsule := CapsuleShape3D.new()
|
||||
capsule.radius = 0.5
|
||||
capsule.height = 2.5
|
||||
for candidate: Vector3 in candidates:
|
||||
if candidate.distance_to(player.global_position) < 5:
|
||||
continue
|
||||
var query := PhysicsShapeQueryParameters3D.new()
|
||||
query.shape = capsule
|
||||
query.transform = Transform3D(Basis.IDENTITY, candidate + Vector3.UP * 1.25)
|
||||
query.collision_mask = 1 | 2 | 4
|
||||
if get_world_3d().direct_space_state.intersect_shape(query, 1).is_empty() \
|
||||
and navigation.route(candidate, player.global_position).size() > 1:
|
||||
return candidate
|
||||
# Fixed office layout has clear candidates; never silently spawn inside a prop.
|
||||
return Vector3.INF
|
||||
|
||||
func _spawn_boss() -> void:
|
||||
var at := _boss_spawn_position()
|
||||
if not at.is_finite():
|
||||
push_error("Nessun punto libero per il boss")
|
||||
return
|
||||
boss = BossScript.new()
|
||||
boss.configure(wave)
|
||||
boss.player = player
|
||||
boss.navigation = navigation
|
||||
boss.position = at
|
||||
var instance := boss
|
||||
alive += 1
|
||||
boss.killed.connect(func(_headshot: bool):
|
||||
score += instance.reward
|
||||
alive -= 1
|
||||
_check_wave())
|
||||
add_child(boss)
|
||||
var portal := BossTeleport.new()
|
||||
portal.position = at
|
||||
portal.materialized.connect(func():
|
||||
if is_instance_valid(instance):
|
||||
instance.materialize())
|
||||
add_child(portal)
|
||||
|
||||
func _configure_simulation() -> bool:
|
||||
if OS.has_feature("web"):
|
||||
return true
|
||||
for argument in OS.get_cmdline_user_args():
|
||||
if argument.begins_with("--boss-wave="):
|
||||
var value := argument.substr(12)
|
||||
if not value.is_valid_int() or not is_boss_wave(int(value)):
|
||||
push_error("--boss-wave richiede un multiplo positivo di 10: 10, 20, 30...")
|
||||
get_tree().quit(2)
|
||||
return false
|
||||
simulation_wave = int(value)
|
||||
boss_simulation = true
|
||||
simulation_god = "--boss-god" in OS.get_cmdline_user_args()
|
||||
return true
|
||||
|
||||
func _start_simulation() -> void:
|
||||
# Wait for static physics shapes/navigation to be registered before choosing a portal.
|
||||
await get_tree().physics_frame
|
||||
wave = simulation_wave - 1
|
||||
start_game("SIMULAZIONE", false)
|
||||
print("BOSS SIMULATION: wave=", simulation_wave, " god=", simulation_god,
|
||||
" | public scores/profile writes disabled")
|
||||
|
||||
func _setup_simulation_ui() -> void:
|
||||
var canvas := CanvasLayer.new()
|
||||
canvas.layer = 6
|
||||
add_child(canvas)
|
||||
simulation_label = Label.new()
|
||||
simulation_label.text = "SIMULAZIONE BOSS — ONDATA %d%s | nessun punteggio online" % [
|
||||
simulation_wave, " | INVULNERABILE" if simulation_god else ""]
|
||||
simulation_label.position = Vector2(18, 125)
|
||||
simulation_label.add_theme_font_size_override("font_size", 18)
|
||||
simulation_label.add_theme_color_override("font_color", Color(0.2, 1, 0.7))
|
||||
simulation_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
canvas.add_child(simulation_label)
|
||||
simulation_retry = Button.new()
|
||||
simulation_retry.text = "RIPROVA IL BOSS"
|
||||
simulation_retry.position = Vector2(510, 520)
|
||||
simulation_retry.size = Vector2(260, 60)
|
||||
simulation_retry.pressed.connect(_restart_simulation)
|
||||
simulation_retry.hide()
|
||||
canvas.add_child(simulation_retry)
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
# Only the completed native simulation has this shortcut. No menu click-to-start.
|
||||
if boss_simulation and simulation_finished and event.is_action_pressed("reload"):
|
||||
get_viewport().set_input_as_handled()
|
||||
_restart_simulation()
|
||||
|
||||
func _restart_simulation() -> void:
|
||||
if simulation_restarting:
|
||||
return
|
||||
simulation_restarting = true
|
||||
get_tree().reload_current_scene()
|
||||
|
||||
Reference in New Issue
Block a user