Add scaling Alien Grunt boss waves and native simulation; publish Web release
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
extends "res://scripts/zombie.gd"
|
||||
## One Alien Grunt replaces every tenth wave. Uses the shared office navigation.
|
||||
const Hornet = preload("res://scripts/boss_hornet.gd")
|
||||
var rank := 1
|
||||
var max_hp := 800.0
|
||||
var melee_damage := 28.0
|
||||
var ranged_damage := 8.0
|
||||
var attack_interval := 2.8
|
||||
var reward := 1000
|
||||
var attack_lock := 0.0
|
||||
var pain_cooldown := 0.0
|
||||
var model: Node3D
|
||||
|
||||
static func stats_for_wave(number: int) -> Dictionary:
|
||||
var grade := maxi(1, floori(number / 10.0))
|
||||
return {
|
||||
"rank": grade, "hp": 800.0 + 400.0 * (grade - 1),
|
||||
"speed": minf(3.4, 2.0 + 0.12 * (grade - 1)),
|
||||
"melee": 28.0 + 6.0 * (grade - 1), "ranged": 8.0 + 2.0 * (grade - 1),
|
||||
"interval": maxf(1.3, 2.8 - 0.18 * (grade - 1)), "reward": grade * 1000,
|
||||
}
|
||||
|
||||
func configure(number: int) -> void:
|
||||
var stats := stats_for_wave(number)
|
||||
rank = stats.rank
|
||||
max_hp = stats.hp
|
||||
hp = max_hp
|
||||
speed = stats.speed
|
||||
melee_damage = stats.melee
|
||||
ranged_damage = stats.ranged
|
||||
attack_interval = stats.interval
|
||||
reward = stats.reward
|
||||
|
||||
func _ready() -> void:
|
||||
name = "AlienGruntBoss"
|
||||
add_to_group("live_zombies") # bullets, blast, target-counting use the same interface
|
||||
add_to_group("bosses")
|
||||
collision_layer = 0 # no hits or obstruction during materialization
|
||||
collision_mask = 1 | 4
|
||||
var shape := CollisionShape3D.new()
|
||||
var cap := CapsuleShape3D.new()
|
||||
cap.radius = 0.45
|
||||
cap.height = 2.4
|
||||
shape.shape = cap
|
||||
shape.position.y = 1.2
|
||||
add_child(shape)
|
||||
model = preload("res://assets/boss/agrunt_textured.scn").instantiate()
|
||||
model.scale = Vector3.ONE * 1.1
|
||||
add_child(model)
|
||||
anim = model.get_node("AnimationPlayer")
|
||||
for mesh in model.find_children("*", "MeshInstance3D", true, false):
|
||||
mesh.layers = 2
|
||||
for area in model.find_children("*", "Area3D", true, false):
|
||||
area.collision_layer = 0
|
||||
area.collision_mask = 0
|
||||
model.hide()
|
||||
# Residual Xen glow keeps the boss readable after the portal flash has faded.
|
||||
var glow := OmniLight3D.new()
|
||||
glow.name = "XenGlow"
|
||||
glow.light_color = Color(0.5, 1.0, 0.68)
|
||||
glow.light_energy = 0.65
|
||||
glow.light_specular = 0.3
|
||||
glow.omni_range = 3.5
|
||||
glow.shadow_enabled = false
|
||||
glow.position = Vector3(0, 1.8, -1.1)
|
||||
glow.visible = false
|
||||
add_child(glow)
|
||||
cooldown = 0.8
|
||||
_repath = 0
|
||||
|
||||
func materialize() -> void:
|
||||
if dead or not is_instance_valid(player) or player.hp <= 0:
|
||||
return
|
||||
model.show()
|
||||
get_node("XenGlow").show()
|
||||
collision_layer = 4
|
||||
entering = false
|
||||
anim.play("threat", 0.1)
|
||||
attack_lock = 0.7
|
||||
Audio.play(self, "agrunt/ag_alert1.wav", true, -8)
|
||||
|
||||
func _physics_process(dt: float) -> void:
|
||||
if dead or entering or not is_instance_valid(player) or player.hp <= 0:
|
||||
return
|
||||
cooldown -= dt
|
||||
attack_lock -= dt
|
||||
pain_cooldown -= dt
|
||||
voice_timer -= dt
|
||||
if voice_timer <= 0:
|
||||
voice_timer = 7.0
|
||||
Audio.play(self, "agrunt/ag_idle1.wav", true, -14)
|
||||
var delta := player.global_position - global_position
|
||||
delta.y = 0
|
||||
var distance := delta.length()
|
||||
if distance > 0.01:
|
||||
rotation.y = atan2(-delta.x, -delta.z)
|
||||
var motion := Vector3.ZERO
|
||||
if attack_lock <= 0:
|
||||
if distance <= 2.2 and _attack_clear() and cooldown <= 0:
|
||||
_start_melee()
|
||||
elif distance > 2.2 and distance < 14 and cooldown <= 0 and _ranged_clear():
|
||||
_start_burst()
|
||||
elif distance > 2.0 or not _attack_clear():
|
||||
motion = _pursuit_direction(dt) * speed
|
||||
if motion.length_squared() > 0.01:
|
||||
rotation.y = atan2(-motion.x, -motion.z)
|
||||
if anim.current_animation != "walk":
|
||||
anim.play("walk", 0.12)
|
||||
elif anim.current_animation != "idle1":
|
||||
anim.play("idle1", 0.12)
|
||||
velocity.x = motion.x
|
||||
velocity.z = motion.z
|
||||
velocity.y = -1.0 if is_on_floor() else velocity.y - 20 * dt
|
||||
move_and_slide()
|
||||
|
||||
func _ranged_clear() -> bool:
|
||||
var from := global_position + Vector3.UP * 1.5
|
||||
var to := player.global_position + Vector3.UP * 1.0
|
||||
var query := PhysicsRayQueryParameters3D.create(from, to, 1)
|
||||
return get_world_3d().direct_space_state.intersect_ray(query).is_empty()
|
||||
|
||||
func _start_melee() -> void:
|
||||
cooldown = attack_interval
|
||||
attack_lock = 0.85
|
||||
anim.play("mattack2", 0.1)
|
||||
Audio.play(self, "agrunt/ag_attack1.wav", true, -9)
|
||||
get_tree().create_timer(0.45).timeout.connect(_attack)
|
||||
|
||||
func _attack() -> void:
|
||||
if not dead and not entering and is_instance_valid(player) and player.hp > 0 \
|
||||
and global_position.distance_to(player.global_position) < 2.7 and _attack_clear():
|
||||
player.take_damage(melee_damage)
|
||||
|
||||
func _start_burst() -> void:
|
||||
cooldown = attack_interval
|
||||
attack_lock = 0.9
|
||||
anim.play("hornetattack", 0.08)
|
||||
# Telegraph the attack, then fire three visible dodgeable hornets.
|
||||
for i in range(3):
|
||||
get_tree().create_timer(0.3 + i * 0.18).timeout.connect(_fire_hornet)
|
||||
|
||||
func _fire_hornet() -> void:
|
||||
if dead or entering or not is_instance_valid(player) or player.hp <= 0:
|
||||
return
|
||||
var from := global_position + Vector3.UP * 1.5
|
||||
var direction := (player.global_position + Vector3.UP - from).normalized()
|
||||
var projectile := Hornet.new()
|
||||
projectile.player = player
|
||||
projectile.shooter = self
|
||||
projectile.damage = ranged_damage
|
||||
projectile.direction = direction
|
||||
get_parent().add_child(projectile)
|
||||
# Begin at the chest, not beyond it: an offset could skip a nearby thin wall.
|
||||
projectile.global_position = from
|
||||
projectile.look_at(projectile.global_position + direction)
|
||||
Audio.play(self, "agrunt/ag_fire1.wav", true, -13)
|
||||
|
||||
func take_damage(amount: float, hit_y: float) -> void:
|
||||
if dead or entering or amount <= 0:
|
||||
return
|
||||
var headshot := hit_y - global_position.y > 1.95
|
||||
hp = maxf(0, hp - amount * (1.5 if headshot else 1.0))
|
||||
if hp > 0:
|
||||
if pain_cooldown <= 0:
|
||||
pain_cooldown = 0.7
|
||||
Audio.play(self, "agrunt/ag_pain1.wav", true, -12)
|
||||
return
|
||||
dead = true
|
||||
get_node("XenGlow").hide()
|
||||
remove_from_group("live_zombies")
|
||||
remove_from_group("bosses")
|
||||
add_to_group("boss_corpses")
|
||||
velocity = Vector3.ZERO
|
||||
collision_layer = 0
|
||||
collision_mask = 0
|
||||
for speaker in get_children():
|
||||
if speaker is AudioStreamPlayer3D:
|
||||
speaker.stop()
|
||||
speaker.queue_free()
|
||||
for projectile in get_tree().get_nodes_in_group("boss_projectiles"):
|
||||
if projectile.shooter == self:
|
||||
projectile.queue_free()
|
||||
Audio.play(self, "agrunt/ag_die1.wav", true, -8)
|
||||
anim.play("diehead" if headshot else "diesimple", 0.1)
|
||||
killed.emit(headshot)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dgjgrwojm7qea
|
||||
@@ -0,0 +1,55 @@
|
||||
extends Node3D
|
||||
## Visible, dodgeable Alien Grunt projectile. Swept ray prevents tunnelling.
|
||||
const SPEED := 8.0
|
||||
const LIFETIME := 3.0
|
||||
var direction := Vector3.FORWARD
|
||||
var damage := 8.0
|
||||
var shooter: CharacterBody3D
|
||||
var player: CharacterBody3D
|
||||
var age := 0.0
|
||||
var spent := false
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("boss_projectiles")
|
||||
var model: Node3D = preload("res://assets/boss/hornet_textured.scn").instantiate()
|
||||
add_child(model)
|
||||
for area in model.find_children("*", "Area3D", true, false):
|
||||
area.collision_layer = 0
|
||||
area.collision_mask = 0
|
||||
var anim := model.get_node_or_null("AnimationPlayer") as AnimationPlayer
|
||||
if anim != null:
|
||||
# The exported native scene already marks idle as looping.
|
||||
anim.play("idle")
|
||||
var mesh := SphereMesh.new()
|
||||
mesh.radius = 0.055
|
||||
mesh.height = 0.11
|
||||
var glow := StandardMaterial3D.new()
|
||||
glow.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
glow.albedo_color = Color(1, 0.7, 0.1)
|
||||
mesh.material = glow
|
||||
var marker := MeshInstance3D.new()
|
||||
marker.mesh = mesh
|
||||
marker.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
add_child(marker)
|
||||
if direction.length_squared() > 0:
|
||||
look_at(global_position + direction)
|
||||
|
||||
func _physics_process(dt: float) -> void:
|
||||
if spent:
|
||||
return
|
||||
age += dt
|
||||
if age >= LIFETIME or not is_instance_valid(shooter) or shooter.dead \
|
||||
or not is_instance_valid(player) or player.hp <= 0:
|
||||
queue_free()
|
||||
return
|
||||
var next := global_position + direction * SPEED * dt
|
||||
var query := PhysicsRayQueryParameters3D.create(global_position, next, 1 | 2)
|
||||
query.exclude = [shooter.get_rid()]
|
||||
var hit := get_world_3d().direct_space_state.intersect_ray(query)
|
||||
if not hit.is_empty():
|
||||
spent = true
|
||||
if hit.collider == player:
|
||||
player.take_damage(damage)
|
||||
queue_free()
|
||||
else:
|
||||
global_position = next
|
||||
@@ -0,0 +1 @@
|
||||
uid://0yeh2nqrpj3o
|
||||
@@ -0,0 +1,83 @@
|
||||
extends Node3D
|
||||
## Cheap Compatibility-safe Xen arrival: rotating rings, CPU wisps, cyan flash.
|
||||
signal materialized
|
||||
const Audio = preload("res://scripts/hl_audio.gd")
|
||||
const ARRIVAL_TIME := 1.5
|
||||
var elapsed := 0.0
|
||||
var announced := false
|
||||
var rings: Array[MeshInstance3D] = []
|
||||
var light: OmniLight3D
|
||||
|
||||
func _ready() -> void:
|
||||
name = "BossTeleport"
|
||||
Audio.play(self, "ambience/port_suckin1.wav", true, -9)
|
||||
var material := StandardMaterial3D.new()
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
material.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
|
||||
material.albedo_color = Color(0.12, 1.0, 0.65, 0.8)
|
||||
for i in range(3):
|
||||
var ring := MeshInstance3D.new()
|
||||
ring.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
var mesh := TorusMesh.new()
|
||||
mesh.inner_radius = 0.91
|
||||
mesh.outer_radius = 1.0
|
||||
mesh.rings = 32
|
||||
mesh.ring_segments = 8
|
||||
mesh.material = material
|
||||
ring.mesh = mesh
|
||||
ring.position.y = 0.10 + i * 0.72
|
||||
add_child(ring)
|
||||
rings.append(ring)
|
||||
light = OmniLight3D.new()
|
||||
light.light_color = Color(0.12, 1.0, 0.65)
|
||||
light.light_energy = 3
|
||||
light.omni_range = 6
|
||||
light.shadow_enabled = false
|
||||
light.position.y = 1.1
|
||||
add_child(light)
|
||||
var particles := CPUParticles3D.new()
|
||||
particles.amount = 40
|
||||
particles.lifetime = 1.0
|
||||
particles.emitting = true
|
||||
particles.emission_shape = CPUParticles3D.EMISSION_SHAPE_SPHERE
|
||||
particles.emission_sphere_radius = 0.9
|
||||
particles.position.y = 0.5
|
||||
particles.direction = Vector3.UP
|
||||
particles.spread = 20
|
||||
particles.initial_velocity_min = 1
|
||||
particles.initial_velocity_max = 2.2
|
||||
particles.gravity = Vector3.ZERO
|
||||
var quad := QuadMesh.new()
|
||||
quad.size = Vector2(0.22, 0.4)
|
||||
var mist := material.duplicate() as StandardMaterial3D
|
||||
mist.billboard_mode = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
mist.albedo_texture = preload("res://assets/effects/flame.png")
|
||||
quad.material = mist
|
||||
particles.mesh = quad
|
||||
var colors := Gradient.new()
|
||||
colors.set_color(0, Color(0.1, 1, 0.65, 0.8))
|
||||
colors.set_color(1, Color(0.05, 0.3, 0.25, 0))
|
||||
particles.color_ramp = colors
|
||||
add_child(particles)
|
||||
get_tree().create_timer(ARRIVAL_TIME).timeout.connect(func():
|
||||
particles.emitting = false)
|
||||
|
||||
func _process(dt: float) -> void:
|
||||
elapsed += dt
|
||||
for i in rings.size():
|
||||
var size := maxf(0.05, sin(minf(elapsed / 2.2, 1.0) * PI))
|
||||
rings[i].scale = Vector3.ONE * size * (1.0 + 0.12 * sin(elapsed * 11 + i))
|
||||
rings[i].rotation = Vector3(sin(elapsed * 4 + i) * 0.2, elapsed * 3, 0)
|
||||
light.light_energy = 3 * maxf(0, 1.0 - maxf(0, elapsed - ARRIVAL_TIME) / 0.7)
|
||||
if elapsed >= ARRIVAL_TIME and not announced:
|
||||
announced = true
|
||||
Audio.play(self, "ambience/port_suckout1.wav", true, -9)
|
||||
materialized.emit()
|
||||
if elapsed >= 2.2:
|
||||
# Sound players belong to this node; leave them time to finish.
|
||||
for ring in rings:
|
||||
ring.hide()
|
||||
light.hide()
|
||||
set_process(false)
|
||||
get_tree().create_timer(3).timeout.connect(queue_free)
|
||||
@@ -0,0 +1 @@
|
||||
uid://h5wtakci76b2
|
||||
@@ -9,6 +9,9 @@ var wave_value: Label
|
||||
var grenade_value: Label
|
||||
var help: Label
|
||||
var root: Control
|
||||
var boss_panel: VBoxContainer
|
||||
var boss_name: Label
|
||||
var boss_health: ProgressBar
|
||||
|
||||
func _ready() -> void:
|
||||
layer = 2
|
||||
@@ -44,6 +47,31 @@ func _ready() -> void:
|
||||
help.offset_right = 420
|
||||
help.offset_top = -30
|
||||
help.offset_bottom = -8
|
||||
boss_panel = VBoxContainer.new()
|
||||
boss_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
root.add_child(boss_panel)
|
||||
boss_panel.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP)
|
||||
boss_panel.offset_left = -180
|
||||
boss_panel.offset_right = 180
|
||||
boss_panel.offset_top = 76
|
||||
boss_name = _label(boss_panel, "ALIEN GRUNT", 17, Vector2.ZERO)
|
||||
boss_name.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
boss_health = ProgressBar.new()
|
||||
boss_health.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
boss_health.custom_minimum_size = Vector2(360, 18)
|
||||
boss_health.show_percentage = false
|
||||
boss_health.step = 0.0 # explosion falloff produces fractional health values
|
||||
boss_health.rounded = false
|
||||
var fill := StyleBoxFlat.new()
|
||||
fill.bg_color = Color(0.14, 0.85, 0.5)
|
||||
boss_health.add_theme_stylebox_override("fill", fill)
|
||||
var background := StyleBoxFlat.new()
|
||||
background.bg_color = Color(0.02, 0.05, 0.03, 0.9)
|
||||
background.border_color = AMBER
|
||||
background.set_border_width_all(1)
|
||||
boss_health.add_theme_stylebox_override("background", background)
|
||||
boss_panel.add_child(boss_health)
|
||||
boss_panel.hide()
|
||||
|
||||
func _panel(label: String, right: bool) -> Panel:
|
||||
var panel := Panel.new()
|
||||
@@ -76,6 +104,15 @@ func _label(parent: Control, text: String, size: int, pos: Vector2) -> Label:
|
||||
parent.add_child(label)
|
||||
return label
|
||||
|
||||
func update_boss(boss: Node) -> void:
|
||||
boss_panel.visible = is_instance_valid(boss) and not boss.dead
|
||||
if not boss_panel.visible:
|
||||
return
|
||||
boss_name.text = "ALIEN GRUNT — GRADO %d%s" % [boss.rank,
|
||||
" — ARRIVO" if boss.entering else " | %d / %d" % [ceili(boss.hp), ceili(boss.max_hp)]]
|
||||
boss_health.max_value = boss.max_hp
|
||||
boss_health.value = boss.hp
|
||||
|
||||
func update_status(player: Node, wave: int, score: int) -> void:
|
||||
health_value.text = str(maxi(0, ceili(player.hp)))
|
||||
health_value.add_theme_color_override("font_color", DANGER if player.hp <= 25 else AMBER)
|
||||
|
||||
+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()
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ var damage_feedback: CanvasLayer
|
||||
const DamageFeedback = preload("res://scripts/damage_feedback.gd")
|
||||
const MAX_HP := 100.0
|
||||
var hp := MAX_HP
|
||||
var invulnerable := false # only enabled by the explicit native boss simulation flag
|
||||
var _regen_elapsed := 0.0
|
||||
|
||||
## Discreto: +1 HP per secondo di gioco, mai oltre il massimo o dopo morte.
|
||||
@@ -215,7 +216,7 @@ func throw_grenade() -> RigidBody3D:
|
||||
return grenade
|
||||
|
||||
func take_damage(amount: float) -> void:
|
||||
if death_started or amount <= 0:
|
||||
if death_started or invulnerable or amount <= 0:
|
||||
return
|
||||
hp = maxf(0, hp - amount)
|
||||
if hp <= 0:
|
||||
|
||||
Reference in New Issue
Block a user