b0c83af092
Complete replacement of the tactical-shooter project with the netfox-cs-sample (MIT) — a CS 1.6 inspired multiplayer FPS built with Godot 4 and netfox. ## What's new - Full CS-style gameplay: teams (T/CT), rounds, economy, buy menu - 6 weapons: Knife, Glock, USP, AK-47, M4A1, AWP - Bomb plant/defuse with 2 bombsites - Flashbang & smoke grenades - Proper netfox rollback netcode at 64 tick - Network popup UI for host/join - HUD, crosshair, round timer, scoreboard - All netfox singletons registered as autoloads (works in exported builds) ## Architecture - Listen-server (host from client, no dedicated server binary) - Multiplayer-fps game lives at examples/multiplayer-fps/ - Netfox addons registered as autoloads for exported build compat - Godot 4.7 with Forward+ renderer ## Removed - Old headless-server architecture (client_main, server_main, player.gd, etc.) - Custom netfox bootstrap with ENet fallback - Old ChaffGames FPS template (2,420 lines, 844 KB) - SimulationServer GDExtension stub - Godot-jolt physics (netfox sample uses default Godot physics) - Duplicate weapon_data.gd, anti_cheat.gd, round_manager.gd, etc. - Server browser API Python venv (87 MB) - test_range map and modular assets ## Preserved - Git history - Server config at config/default_server_config.cfg - Windows export preset - Build directory (gitignored) Co-authored-by: naxIO <naxIO@users.noreply.github.com>
161 lines
4.4 KiB
GDScript
161 lines
4.4 KiB
GDScript
extends Node3D
|
|
class_name Grenade
|
|
|
|
enum GrenadeType { FLASH, SMOKE }
|
|
|
|
@export var grenade_type: GrenadeType = GrenadeType.FLASH
|
|
@export var fuse_time: float = 1.5
|
|
@export var throw_force: float = 20.0
|
|
@export var gravity: float = 15.0
|
|
@export var bounce_factor: float = 0.4
|
|
|
|
var velocity: Vector3 = Vector3.ZERO
|
|
var _fuse_timer: float = 0.0
|
|
var _detonated := false
|
|
var _owner_id: int = -1
|
|
|
|
func setup(origin: Vector3, direction: Vector3, owner_id: int, type: GrenadeType):
|
|
global_position = origin
|
|
grenade_type = type
|
|
_owner_id = owner_id
|
|
velocity = direction * throw_force + Vector3.UP * 5.0
|
|
|
|
if type == GrenadeType.FLASH:
|
|
fuse_time = 1.5
|
|
else:
|
|
fuse_time = 2.0
|
|
|
|
func _physics_process(delta: float):
|
|
if _detonated:
|
|
return
|
|
|
|
# Simple physics
|
|
velocity.y -= gravity * delta
|
|
var new_pos := global_position + velocity * delta
|
|
|
|
# Basic bounce off floor
|
|
if new_pos.y < 0.1:
|
|
new_pos.y = 0.1
|
|
velocity.y = -velocity.y * bounce_factor
|
|
velocity.x *= bounce_factor
|
|
velocity.z *= bounce_factor
|
|
|
|
global_position = new_pos
|
|
|
|
# Fuse countdown
|
|
_fuse_timer += delta
|
|
if _fuse_timer >= fuse_time:
|
|
_detonate()
|
|
|
|
func _detonate():
|
|
if _detonated:
|
|
return
|
|
_detonated = true
|
|
|
|
if not multiplayer.is_server():
|
|
return
|
|
|
|
match grenade_type:
|
|
GrenadeType.FLASH:
|
|
_flash_effect()
|
|
GrenadeType.SMOKE:
|
|
_smoke_effect()
|
|
|
|
func _flash_effect():
|
|
var space := get_world_3d().direct_space_state
|
|
var flash_pos := global_position
|
|
|
|
# Check all players for flash
|
|
var spawner := get_tree().current_scene.get_node_or_null("Network/Player Spawner")
|
|
if spawner == null:
|
|
queue_free()
|
|
return
|
|
|
|
for peer_id in spawner.avatars:
|
|
var avatar = spawner.avatars[peer_id] as CharacterBody3D
|
|
if avatar == null or avatar.is_dead:
|
|
continue
|
|
|
|
var head_pos: Vector3 = avatar.global_position + Vector3.UP * 0.7
|
|
var distance := flash_pos.distance_to(head_pos)
|
|
if distance > 30.0:
|
|
continue
|
|
|
|
# LOS check
|
|
var query := PhysicsRayQueryParameters3D.create(flash_pos, head_pos)
|
|
query.exclude = [avatar.get_rid()]
|
|
var result := space.intersect_ray(query)
|
|
|
|
if result.is_empty():
|
|
# Direct line of sight — calculate flash duration
|
|
var duration := _calculate_flash_duration(distance, avatar, head_pos, flash_pos)
|
|
if duration > 0.0:
|
|
_apply_flash.rpc_id(peer_id, duration)
|
|
|
|
# Cleanup
|
|
call_deferred("queue_free")
|
|
|
|
func _calculate_flash_duration(distance: float, avatar: CharacterBody3D, head_pos: Vector3, flash_pos: Vector3) -> float:
|
|
# Base duration: closer = longer flash
|
|
var base := lerpf(3.5, 0.5, clampf(distance / 25.0, 0.0, 1.0))
|
|
|
|
# Facing check: looking at flash = full duration, away = reduced
|
|
var head := avatar.get_node_or_null("Head") as Node3D
|
|
if head:
|
|
var to_flash := (flash_pos - head_pos).normalized()
|
|
var facing := -head.global_transform.basis.z
|
|
var dot := facing.dot(to_flash)
|
|
if dot < 0.0:
|
|
base *= 0.3 # Looking away
|
|
else:
|
|
base *= lerpf(0.5, 1.0, dot)
|
|
|
|
return base
|
|
|
|
func _smoke_effect():
|
|
# Spawn smoke zone at detonation position
|
|
_spawn_smoke.rpc(global_position)
|
|
# Smoke grenade stays alive for smoke duration, then frees itself
|
|
set_physics_process(false)
|
|
var smoke_duration := 18.0
|
|
get_tree().create_timer(smoke_duration).timeout.connect(queue_free)
|
|
|
|
@rpc("authority", "call_local", "reliable")
|
|
func _apply_flash(duration: float) -> void:
|
|
# Client-side: white screen effect
|
|
var canvas := CanvasLayer.new()
|
|
canvas.layer = 100
|
|
var rect := ColorRect.new()
|
|
rect.color = Color(1, 1, 1, 1)
|
|
rect.set_anchors_preset(Control.PRESET_FULL_RECT)
|
|
canvas.add_child(rect)
|
|
get_tree().current_scene.add_child(canvas)
|
|
|
|
# Fade out
|
|
var tween := get_tree().create_tween()
|
|
tween.tween_property(rect, "color:a", 0.0, duration)
|
|
tween.tween_callback(canvas.queue_free)
|
|
|
|
@rpc("authority", "call_local", "reliable")
|
|
func _spawn_smoke(pos: Vector3) -> void:
|
|
# Client-side: visual smoke particles
|
|
var smoke_node := Node3D.new()
|
|
smoke_node.name = "SmokeCloud"
|
|
smoke_node.global_position = pos
|
|
get_tree().current_scene.add_child(smoke_node)
|
|
|
|
# Simple visual: grey sphere mesh as placeholder
|
|
var mesh_inst := MeshInstance3D.new()
|
|
var sphere := SphereMesh.new()
|
|
sphere.radius = 3.0
|
|
sphere.height = 4.0
|
|
var mat := StandardMaterial3D.new()
|
|
mat.albedo_color = Color(0.6, 0.6, 0.6, 0.5)
|
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
|
sphere.material = mat
|
|
mesh_inst.mesh = sphere
|
|
smoke_node.add_child(mesh_inst)
|
|
|
|
get_tree().create_timer(18.0).timeout.connect(smoke_node.queue_free)
|