Phase 7: Windows client export, preset fix, code fixes
- Fix: export_presets.cfg — platform='Windows Desktop' (not 'Windows'), Windows as [preset.0] - Fix: plugin_manager.gd — removed class_name (autoload conflict), fixed multiline % formatting - Fix: Disabled GDExtension temporarily for clean export - Add: Windows PE32+ x86_64 client binary (109MB) at build/tactical-shooter-windows-x86_64/ - Add: tactical-shooter-windows.zip (portable zip package) - Build: Linux server binary (78MB) rebuilt
This commit is contained in:
@@ -0,0 +1 @@
|
||||
uid://bjtf5jiedmwx8
|
||||
@@ -92,6 +92,9 @@ var _input_dict: Dictionary = {}
|
||||
var _mouse_captured: bool = false
|
||||
var _mouse_clicked_this_frame: bool = false
|
||||
|
||||
## Weapon manager reference.
|
||||
var _weapon: WeaponManager = null
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Node references (set in _ready)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -152,6 +155,12 @@ func _ready() -> void:
|
||||
_crouch_current = 0.0
|
||||
_update_crouch(0.0)
|
||||
|
||||
# Find weapon manager child
|
||||
for child in get_children():
|
||||
if child is WeaponManager:
|
||||
_weapon = child
|
||||
break
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
# Mouse look
|
||||
@@ -227,6 +236,19 @@ func _physics_process(delta: float) -> void:
|
||||
if _camera:
|
||||
_camera.rotation.x = _pitch
|
||||
|
||||
# 5b. Weapon fire with rate limiting via WeaponManager
|
||||
var should_fire: bool = false
|
||||
var time_since_engine_start: float = Engine.get_process_ticks() * get_physics_process_delta_time()
|
||||
if _weapon:
|
||||
should_fire = _weapon.try_fire(time_since_engine_start)
|
||||
elif shoot_pressed:
|
||||
# No weapon manager — fire every tick (unlimited)
|
||||
should_fire = true
|
||||
|
||||
if _server != null:
|
||||
if should_fire:
|
||||
_server.fire_weapon(entity_id)
|
||||
|
||||
# 6. Send input to simulation server
|
||||
if _server != null and entity_id >= 0:
|
||||
# Build input for SimulationServer (mutate cached dict to avoid alloc)
|
||||
@@ -236,14 +258,18 @@ func _physics_process(delta: float) -> void:
|
||||
_input_dict["jump"] = jump_pressed
|
||||
_input_dict["crouch"] = _crouch_active
|
||||
_input_dict["sprint"] = _sprint_active
|
||||
_input_dict["shoot"] = shoot_pressed
|
||||
_input_dict["shoot"] = should_fire
|
||||
_input_dict["aim"] = aim_pressed
|
||||
_input_dict["input_sequence"] = _input_sequence
|
||||
|
||||
_server.apply_input(entity_id, _input_dict)
|
||||
|
||||
if shoot_pressed:
|
||||
_server.fire_weapon(entity_id)
|
||||
# Check for hit feedback from last tick
|
||||
if Engine.has_singleton("SimulationServer") or _server:
|
||||
var hit_result: Dictionary = _server.get_last_hit_result()
|
||||
if hit_result.get("hit", false) and _weapon:
|
||||
var hit_pos := Vector3.ZERO # approximate — we don't have exact world hit position from server yet
|
||||
_weapon.on_hit_confirmed(hit_pos, hit_result.get("damage", 0.0), hit_result.get("killed", false))
|
||||
|
||||
_input_sequence += 1
|
||||
|
||||
@@ -399,3 +425,15 @@ func reset_pose() -> void:
|
||||
_crouch_current = 0.0
|
||||
_crouch_target = 0.0
|
||||
_update_crouch(0.0)
|
||||
|
||||
|
||||
## Get the look direction as a normalized Vector3 in world space.
|
||||
## Uses the current yaw/pitch to compute a forward vector.
|
||||
func get_look_direction() -> Vector3:
|
||||
var yaw_rad: float = _yaw
|
||||
var pitch_rad: float = _pitch
|
||||
return Vector3(
|
||||
cos(pitch_rad) * sin(yaw_rad),
|
||||
-sin(pitch_rad),
|
||||
cos(pitch_rad) * cos(yaw_rad)
|
||||
).normalized()
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://cpbvk1itjonm
|
||||
@@ -0,0 +1 @@
|
||||
uid://bvkhxr1bv6lth
|
||||
@@ -0,0 +1,205 @@
|
||||
## WeaponManager — hitscan weapon system for tactical FPS.
|
||||
##
|
||||
## Manages a single hitscan weapon: fire rate limiting, ammo, reload.
|
||||
## Communicates with SimulationServer for server-authoritative hit detection.
|
||||
##
|
||||
## Architecture:
|
||||
## - Client-side: fire rate limiting, muzzle VFX, hit marker feedback
|
||||
## - Server-side (via SimulationServer): hit detection, damage, lag compensation
|
||||
## - Both sides: ammo tracking (server-authoritative, client-predicted)
|
||||
##
|
||||
## Usage (attach as child of FPSCharacterController):
|
||||
##
|
||||
## FPSCharacterController
|
||||
## ├── WeaponManager — fire rate, ammo, VFX
|
||||
## │ ├── MuzzleFlash — instantiated VFX
|
||||
## │ └── HitMarker — instantiated VFX
|
||||
## └── FpsCamera
|
||||
##
|
||||
class_name WeaponManager
|
||||
extends Node
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Signals
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Emitted when the weapon fires (for visual feedback).
|
||||
signal weapon_fired(origin: Vector3, direction: Vector3)
|
||||
|
||||
## Emitted when this weapon hits an entity.
|
||||
signal hit_marked(hit_pos: Vector3, damage: float, killed: bool)
|
||||
|
||||
## Emitted when ammo changes.
|
||||
signal ammo_changed(current: int, max: int)
|
||||
|
||||
## Emitted when player is killed.
|
||||
signal player_killed()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exports — Assault Rifle Config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Maximum ammo capacity.
|
||||
@export var max_ammo: int = 30
|
||||
|
||||
## Starting ammo (including one magazine).
|
||||
@export var start_ammo: int = 90
|
||||
|
||||
## Time between shots (seconds). 10 Hz = 0.1s between shots.
|
||||
@export var fire_rate: float = 0.1
|
||||
|
||||
## Reload time in seconds.
|
||||
@export var reload_time: float = 2.0
|
||||
|
||||
## Spread increases while firing (degrees).
|
||||
@export var spread_per_shot: float = 0.5
|
||||
|
||||
## Max spread when firing continuously (degrees).
|
||||
@export var max_spread: float = 4.0
|
||||
|
||||
## Spread recovery per second (degrees/s).
|
||||
@export var spread_recovery: float = 8.0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# State
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var current_ammo: int = 30
|
||||
var reserve_ammo: int = 90
|
||||
var is_reloading: bool = false
|
||||
var _last_shot_time: float = 0.0
|
||||
var _current_spread: float = 0.0
|
||||
|
||||
## Reference to the parent FPSCharacterController (set in _ready).
|
||||
var _controller: FPSCharacterController = null
|
||||
|
||||
## Reference to SimulationServer (if available).
|
||||
var _server: Object = null
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func _ready() -> void:
|
||||
_controller = get_parent() as FPSCharacterController
|
||||
if _controller == null:
|
||||
push_warning("WeaponManager: Parent is not FPSCharacterController. Disabled.")
|
||||
set_process(false)
|
||||
return
|
||||
|
||||
# Find SimulationServer singleton
|
||||
if Engine.has_singleton("SimulationServer"):
|
||||
_server = Engine.get_singleton("SimulationServer")
|
||||
elif Engine.get_main_loop() and Engine.get_main_loop().root:
|
||||
# Try finding it in the scene tree
|
||||
var game_server = Engine.get_main_loop().root.find_child("GameServer", true, false)
|
||||
if game_server and game_server.has_method(&"get_simulation_server"):
|
||||
_server = game_server.get_simulation_server()
|
||||
|
||||
# Initialize ammo
|
||||
current_ammo = max_ammo
|
||||
reserve_ammo = start_ammo
|
||||
|
||||
# Signal that ammo changed
|
||||
ammo_changed.emit(current_ammo, max_ammo)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main loop — called from FPSCharacterController._physics_process per tick
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
## Try to fire the weapon. Returns true if a shot was fired.
|
||||
## Called by the controller when shoot input is detected.
|
||||
func try_fire(time: float) -> bool:
|
||||
# Check fire rate
|
||||
if time - _last_shot_time < fire_rate:
|
||||
return false
|
||||
|
||||
# Check ammo
|
||||
if current_ammo <= 0:
|
||||
# Auto-reload on empty — or play empty-click sound
|
||||
if not is_reloading:
|
||||
start_reload()
|
||||
return false
|
||||
|
||||
# Check reloading
|
||||
if is_reloading:
|
||||
return false
|
||||
|
||||
# Deduct ammo
|
||||
current_ammo -= 1
|
||||
_last_shot_time = time
|
||||
|
||||
# Increase spread
|
||||
_current_spread = min(_current_spread + spread_per_shot, max_spread)
|
||||
|
||||
# Emit fire signal (for visual/hit marker)
|
||||
# Direction comes from the controller's look direction
|
||||
if _controller:
|
||||
var origin: Vector3 = _controller.global_position
|
||||
origin.y += _controller.eye_height_stand
|
||||
var direction: Vector3 = _controller.get_look_direction()
|
||||
weapon_fired.emit(origin, direction)
|
||||
|
||||
ammo_changed.emit(current_ammo, max_ammo)
|
||||
return true
|
||||
|
||||
## Called every frame to recover spread and handle spread animation.
|
||||
func _process(delta: float) -> void:
|
||||
# Recover spread
|
||||
if _current_spread > 0.0:
|
||||
_current_spread = max(0.0, _current_spread - spread_recovery * delta)
|
||||
|
||||
## Start reloading.
|
||||
func start_reload() -> void:
|
||||
if is_reloading or current_ammo >= max_ammo or reserve_ammo <= 0:
|
||||
return
|
||||
|
||||
is_reloading = true
|
||||
await get_tree().create_timer(reload_time).timeout
|
||||
|
||||
# Reload logic
|
||||
var needed: int = max_ammo - current_ammo
|
||||
var from_reserve: int = min(needed, reserve_ammo)
|
||||
current_ammo += from_reserve
|
||||
reserve_ammo -= from_reserve
|
||||
is_reloading = false
|
||||
|
||||
ammo_changed.emit(current_ammo, max_ammo)
|
||||
|
||||
## Called when a hit is confirmed by the server.
|
||||
func on_hit_confirmed(hit_pos: Vector3, damage: float, killed: bool) -> void:
|
||||
hit_marked.emit(hit_pos, damage, killed)
|
||||
if killed:
|
||||
player_killed.emit()
|
||||
|
||||
## Get current spread (degrees).
|
||||
func get_current_spread() -> float:
|
||||
return _current_spread
|
||||
|
||||
## Get the current weapon fire direction including spread cone.
|
||||
func get_spread_direction(base_direction: Vector3) -> Vector3:
|
||||
if _current_spread <= 0.001:
|
||||
return base_direction
|
||||
|
||||
var spread_rad: float = deg_to_rad(_current_spread)
|
||||
var theta: float = randf() * TAU
|
||||
var phi: float = randf() * spread_rad
|
||||
|
||||
# Create a random perpendicular vector
|
||||
var up := Vector3.UP
|
||||
if abs(base_direction.dot(up)) > 0.99:
|
||||
up = Vector3.RIGHT
|
||||
var right := base_direction.cross(up).normalized()
|
||||
up = right.cross(base_direction).normalized()
|
||||
|
||||
var offset := right * sin(theta) * sin(phi) + up * cos(theta) * sin(phi)
|
||||
return (base_direction + offset).normalized()
|
||||
|
||||
## Reset weapon state (for respawn).
|
||||
func reset() -> void:
|
||||
current_ammo = max_ammo
|
||||
reserve_ammo = start_ammo
|
||||
is_reloading = false
|
||||
_current_spread = 0.0
|
||||
_last_shot_time = 0.0
|
||||
ammo_changed.emit(current_ammo, max_ammo)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dywlbha8xivx4
|
||||
Reference in New Issue
Block a user