Audio (all CC0 — Kenney packs + OpenGameArt, see assets/sounds/SOURCES.md): - Replace every procedural synth WAV with forged sounds (ffmpeg pitch/layer mixes): distinct fire sounds per weapon with 2-3 variations, explosions, footsteps x5, land/jump/dash/vault, knife swing, hit/kill confirms, bullet impacts, reload, flight loops, UI hover/click/confirm/error, match jingles, menu music - AudioManager.stream_for(): AudioStreamRandomizer per sound id — every weapon now gets variation + pitch randomization on each shot - Explosions and bullet impacts play positional audio; landing has its own sound instead of a pitched footstep; ambient wind bed on every map Visuals: - ExplosionVFX: shared cel-shaded burst (white-hot stepped core, ink shockwave ring, star spikes, flat smoke puffs) replaces the orange sphere in both local and remote-replay paths - Comic star muzzle flashes on all weapons; unified cel tracer bolts with ink outlines across hitscan/shotgun/remote paths - Impact decals: hard-stepped ink-splat gradients instead of soft airbrush - Toon shading on first-person view models + arms, third-person weapons, and the procedural humanoid fallback (no hull outlines on FBX weapons — their hard normals tear the inverted hull) - Fix: giant soft "blob" highlight on floors — toon rim/specular disabled on level-geometry materials (pre-existing artifact since the cel commit) - Fix: own third-person weapon rendered into the first-person camera (shadows-only now covers first_person_mode) UI: - UITheme: comic theme on the root window — Bangers display font (OFL), paper panels with thick ink borders + hard drop shadows, papaya accent, themed buttons/inputs/popups; hover/click sounds on all menu buttons - Main menu: tilted comic wordmark, sunset toon diorama, looping menu music - Match HUD: themed scoreboard/killfeed/kill counter Viewports: - 4x MSAA project-wide + viewmodel/diorama viewports (crisp ink lines) - Viewmodel camera near plane 0.01; third-person camera FOV syncs settings - debug/visual_capture.gd: dev tool to screenshot menu + level for review Verified: 11/11 movement tests, spawn smoke test 0 failures, before/after screenshot comparison of menu and level. Co-Authored-By: Claude Fable 5 <[email protected]>
219 lines
6.9 KiB
GDScript
219 lines
6.9 KiB
GDScript
extends BaseProjectileWeapon
|
|
class_name RocketSwarm
|
|
|
|
var is_targeting: bool = false
|
|
var homing_target_pos: Vector3 = Vector3.ZERO
|
|
var targeting_laser: MeshInstance3D
|
|
|
|
func _init() -> void:
|
|
weapon_name = "Rocket Swarm"
|
|
fire_rate = 1.0 # 1 click fires all 8
|
|
max_ammo = 4 # 4 swarms
|
|
reload_time = 3.0
|
|
base_damage = 35.0 # Per mini rocket
|
|
min_damage = 20.0
|
|
falloff_start = 0.0
|
|
max_range = 300.0
|
|
automatic = false
|
|
spread_angle = 0.15 # Wide spread out of the barrel
|
|
|
|
projectile_speed = 25.0 # Slower so they have time to home
|
|
|
|
func _build_model() -> void:
|
|
position = Vector3(0.4, -0.4, -0.656)
|
|
|
|
var model_scene = load("res://assets/weapons/swarm launcher/source/LOD_0.fbx")
|
|
if model_scene:
|
|
var model = model_scene.instantiate()
|
|
model_root.add_child(model)
|
|
model.scale = Vector3(1.0, 1.0, 1.0)
|
|
model.rotation_degrees.y = -90
|
|
model.position.y -= 0.6 # Move model down without moving arms
|
|
|
|
# Strip lights
|
|
for child in model.find_children("*", "Light3D", true, false):
|
|
child.queue_free()
|
|
|
|
# Laser
|
|
targeting_laser = MeshInstance3D.new()
|
|
var l_mesh = CylinderMesh.new()
|
|
l_mesh.top_radius = 0.02 # Make it thicker and more visible
|
|
l_mesh.bottom_radius = 0.02
|
|
l_mesh.height = 300.0
|
|
var l_mat = StandardMaterial3D.new()
|
|
l_mat.albedo_color = Color(1.0, 0.0, 0.0, 1.0)
|
|
l_mat.emission_enabled = true
|
|
l_mat.emission = Color(2.0, 0.0, 0.0) # Bright red
|
|
l_mesh.material = l_mat
|
|
targeting_laser.mesh = l_mesh
|
|
targeting_laser.rotation.x = deg_to_rad(90)
|
|
targeting_laser.position = Vector3(0, 0, -234.375)
|
|
targeting_laser.visible = false
|
|
model_root.add_child(targeting_laser)
|
|
|
|
# Audio
|
|
fire_sound = AudioStreamPlayer3D.new()
|
|
fire_sound.bus = "SFX"
|
|
fire_sound.stream = AudioManager.stream_for("swarm_fire", "res://assets/sounds/swarm_fire.wav")
|
|
model_root.add_child(fire_sound)
|
|
|
|
reload_sound = AudioStreamPlayer3D.new()
|
|
reload_sound.bus = "SFX"
|
|
reload_sound.stream = AudioManager.stream_for("shotgun_reload", "res://assets/sounds/shotgun_reload.wav")
|
|
model_root.add_child(reload_sound)
|
|
|
|
func _process(delta: float) -> void:
|
|
super._process(delta)
|
|
|
|
if not visible:
|
|
return
|
|
|
|
if camera:
|
|
is_targeting = Input.is_action_pressed("fire_alt")
|
|
targeting_laser.visible = is_targeting
|
|
|
|
if is_targeting:
|
|
var space_state = get_world_3d().direct_space_state
|
|
var origin = camera.global_position
|
|
var target = origin - camera.global_transform.basis.z * 300.0
|
|
var query = PhysicsRayQueryParameters3D.create(origin, target)
|
|
if player:
|
|
query.exclude = [player.get_rid()]
|
|
|
|
var result = space_state.intersect_ray(query)
|
|
if result:
|
|
homing_target_pos = result.position
|
|
# Shrink laser to hit point
|
|
var dist = origin.distance_to(result.position)
|
|
(targeting_laser.mesh as CylinderMesh).height = dist
|
|
targeting_laser.position.z = -dist / 2.0
|
|
else:
|
|
homing_target_pos = target
|
|
(targeting_laser.mesh as CylinderMesh).height = 300.0
|
|
targeting_laser.position.z = -150.0
|
|
|
|
if player and player.is_multiplayer_authority():
|
|
player.synced_is_targeting = true
|
|
player.synced_homing_target_pos = homing_target_pos
|
|
else:
|
|
if player and player.is_multiplayer_authority():
|
|
player.synced_is_targeting = false
|
|
|
|
func unequip() -> void:
|
|
is_targeting = false
|
|
targeting_laser.visible = false
|
|
if player and player.is_multiplayer_authority():
|
|
player.synced_is_targeting = false
|
|
|
|
# Override fire to shoot 8 rockets at once
|
|
func _fire() -> void:
|
|
if current_ammo <= 0 and not reloading:
|
|
_start_reload()
|
|
return
|
|
|
|
if not camera: return
|
|
|
|
current_ammo -= 1
|
|
fire_cooldown = fire_rate
|
|
|
|
if fire_sound:
|
|
fire_sound.play()
|
|
|
|
if muzzle_flash:
|
|
muzzle_flash.light_energy = 2.0
|
|
var tween = create_tween()
|
|
tween.tween_property(muzzle_flash, "light_energy", 0.0, 0.1)
|
|
|
|
var _origin = camera.global_position + camera.global_transform.basis * Vector3(0.4, -0.2, -0.938)
|
|
var base_dir = -camera.global_transform.basis.z
|
|
|
|
_current_aim_dir = base_dir
|
|
|
|
for i in range(8):
|
|
var right = camera.global_transform.basis.x
|
|
var up = camera.global_transform.basis.y
|
|
|
|
var fraction = float(i) / 7.0 # 0.0 to 1.0 (left to right)
|
|
|
|
# Offset from left (-0.1) to right (0.1) on the top edge of the launcher
|
|
var local_x = lerpf(-0.1, 0.1, fraction)
|
|
var local_offset = Vector3(local_x, 0.1, 0.0)
|
|
|
|
var launcher_center_local = Vector3(0.4, -0.4, -0.6)
|
|
var proj_origin = camera.global_position + camera.global_transform.basis * (launcher_center_local + local_offset)
|
|
|
|
# Fire direction: radial fan out
|
|
# They shoot mostly forward, but fan out horizontally, and slightly arch upwards radially
|
|
var angle = lerpf(-PI/3.0, PI/3.0, fraction) # -60 deg to +60 deg fan
|
|
var x_dir = sin(angle) * 0.6
|
|
var y_dir = cos(angle) * 0.8 # Reduced upward angle so it's not mostly straight up
|
|
|
|
var fire_dir = (base_dir + right * x_dir + up * y_dir).normalized()
|
|
_spawn_custom_projectile(proj_origin, fire_dir)
|
|
|
|
if multiplayer.has_multiplayer_peer() and not(multiplayer.multiplayer_peer is OfflineMultiplayerPeer):
|
|
player.server_play_fire_effects.rpc_id(1, proj_origin, fire_dir, weapon_name, false)
|
|
|
|
var _current_aim_dir: Vector3 = Vector3.ZERO
|
|
|
|
func _spawn_custom_projectile(origin: Vector3, fire_dir: Vector3) -> void:
|
|
var proj = load("res://weapons/homing_projectile.gd").new()
|
|
|
|
# Mesh
|
|
var mesh_instance = MeshInstance3D.new()
|
|
var p_mesh = CylinderMesh.new()
|
|
p_mesh.top_radius = 0.0
|
|
p_mesh.bottom_radius = 0.02
|
|
p_mesh.height = 0.1
|
|
var mat = StandardMaterial3D.new()
|
|
mat.albedo_color = Color(0.8, 0.8, 0.1) # Yellow mini rocket
|
|
p_mesh.material = mat
|
|
mesh_instance.mesh = p_mesh
|
|
mesh_instance.rotation.x = deg_to_rad(-90)
|
|
proj.add_child(mesh_instance)
|
|
|
|
# Rotation
|
|
var up_vec = Vector3.UP
|
|
if abs(fire_dir.dot(up_vec)) > 0.99:
|
|
up_vec = Vector3.RIGHT
|
|
proj.transform.basis = Basis.looking_at(fire_dir, up_vec)
|
|
|
|
# Logic
|
|
proj.weapon_source = self
|
|
proj.turn_speed = 6.5 # Slower turn radius so they sweep nicely
|
|
proj.position = origin # Start at origin to spread outward around camera
|
|
proj.logical_source = origin
|
|
|
|
# Initial pop-up speed
|
|
proj.velocity = fire_dir * 5.0
|
|
if player:
|
|
proj.velocity += fire_dir * max(0.0, player.velocity.dot(fire_dir))
|
|
|
|
var aim_dir = _current_aim_dir
|
|
# If remote player, use the synced head rotation to determine forward direction
|
|
if player and not player.is_multiplayer_authority():
|
|
var head = player.get_node_or_null("HeadPivot")
|
|
if head:
|
|
aim_dir = -head.global_transform.basis.z.normalized()
|
|
else:
|
|
aim_dir = fire_dir # Fallback
|
|
|
|
proj.initial_aim_dir = aim_dir
|
|
proj.cruise_speed = 65.0
|
|
proj.ignition_timer = randf_range(0.3, 0.6) # Staggered ignition
|
|
|
|
proj.damage = base_damage
|
|
proj.min_damage = min_damage
|
|
proj.falloff_start = falloff_start
|
|
proj.max_range = max_range
|
|
proj.drop_gravity = 5.0 # Slight arc before ignition
|
|
proj.owner_player = player
|
|
|
|
# Explosive stats
|
|
proj.explosion_radius = 1.5 # Tiny AoE effect
|
|
proj.explosion_knockback = 5.0
|
|
proj.can_self_damage = false
|
|
proj.explosion_pitch = 1.2 # Less deep but still explosion
|
|
|
|
get_tree().current_scene.add_child(proj)
|