diff --git a/.godot/editor/editor_layout.cfg b/.godot/editor/editor_layout.cfg index ec1a4ab..987aaf2 100644 --- a/.godot/editor/editor_layout.cfg +++ b/.godot/editor/editor_layout.cfg @@ -32,8 +32,8 @@ open_scenes=PackedStringArray("res://level/scenes/level.tscn", "res://level/scen current_scene="res://level/scenes/player.tscn" center_split_offset=0 selected_default_debugger_tab_idx=1 -selected_main_editor_idx=3 -selected_bottom_panel_item=0 +selected_main_editor_idx=2 +selected_bottom_panel_item=1 [EditorWindow] @@ -44,8 +44,8 @@ size=Vector2i(1152, 648) [ScriptEditor] -open_scripts=["res://level/scripts/3d_godot_robot.gd", "res://level/scripts/level.gd", "res://level/scripts/player.gd", "res://README.md"] -selected_script="res://level/scripts/player.gd" +open_scripts=["res://level/scripts/3d_godot_robot.gd", "res://level/scripts/level.gd", "res://level/scripts/player.gd", "res://README.md", "res://level/scripts/spring_arm_offset.gd"] +selected_script="res://level/scripts/spring_arm_offset.gd" open_help=[] script_split_offset=200 list_split_offset=0 @@ -53,8 +53,8 @@ zoom_factor=1.0 [GameView] -floating_window_rect=Rect2i(634, 315, 1292, 767) -floating_window_screen=1 +floating_window_rect=Rect2i(2677, 131, 1292, 767) +floating_window_screen=0 [ShaderEditor] diff --git a/.godot/editor/editor_script_doc_cache.res b/.godot/editor/editor_script_doc_cache.res deleted file mode 100644 index 78533a8..0000000 Binary files a/.godot/editor/editor_script_doc_cache.res and /dev/null differ diff --git a/.godot/editor/filesystem_cache10 b/.godot/editor/filesystem_cache10 index 5ed7cb9..b783b8e 100644 --- a/.godot/editor/filesystem_cache10 +++ b/.godot/editor/filesystem_cache10 @@ -1,30 +1,32 @@ 63f7b34db8d8cdea90c76aacccf841ec -::res://::1762971203 +::res://::1762974779 +CLAUDE.md::TextFile::-1::1762974557::0::1::::<><><>0<>0<><>:: CONTRIBUTING.md::TextFile::-1::1762703890::0::1::::<><><>0<>0<><>:: icon.png::CompressedTexture2D::7745181525272711891::1762703890::1762703997::1::::<><><>0<>0<>896be222f4526152412fc7150ec81337<>res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex:: -README.md::TextFile::-1::1762703890::0::1::::<><><>0<>0<><>:: -::res://assets/::1762703936 -::res://assets/characters/::1762972117 -::res://assets/characters/Lobster/::1762972124 +README.md::TextFile::-1::1762972834::0::1::::<><><>0<>0<><>:: +::res://assets/::1762972860 +::res://assets/characters/::1762972860 +::res://assets/characters/Lobster/::1762972860 10029_Lobster_v1_Diffuse.jpg::CompressedTexture2D::1636629097341229697::1762972122::1762972126::1::::<><><>0<>0<>6340ceb996fa69b121c26459d93e8863<>res://.godot/imported/10029_Lobster_v1_Diffuse.jpg-042296e567cd8e99b847d125c0bf3e99.s3tc.ctex:: 10029_Lobster_v1_iterations-2.obj::Mesh::8031617903903645591::1762972122::1762972124::1::::<><><>0<>0<>6f735946f3e6cee1022fc19030e5e510<>res://.godot/imported/10029_Lobster_v1_iterations-2.obj-007e872ca27f7329d9bece1ae9a73118.mesh<*>res://.godot/imported/10029_Lobster_v1_iterations-2.obj-007e872ca27f7329d9bece1ae9a73118.mesh::uid://xk2boxo0jwrp::::res://assets/characters/Lobster/10029_Lobster_v1_Diffuse.jpg -::res://assets/characters/player/::1762704000 +::res://assets/characters/player/::1762972860 3DGodotRobot.glb::PackedScene::5656896519777153630::1762703890::1762704000::1::::<><><>0<>0<>bb220f20c8949e4b43553be149732094<>res://.godot/imported/3DGodotRobot.glb-c7060093cec96d07cab7f0bfb35f5061.scn::uid://bdg48m6l86q8i::::res://assets/characters/player/3DGodotRobot_GodotPalette.png 3DGodotRobot_GodotPalette.png::CompressedTexture2D::2611193883834911314::1762703890::1762703998::1::::<><><>0<>0<>a2b865fe920c4004f33acb6ec9f0a050<>res://.godot/imported/3DGodotRobot_GodotPalette.png-db2cb37c015bbbe580a404413e47cf86.s3tc.ctex:: -::res://assets/characters/player/GodotRobotPaletteSwap/::1762703998 +::res://assets/characters/player/GodotRobotPaletteSwap/::1762972860 GodotGreenPalette.png::CompressedTexture2D::2473362712891021041::1762703890::1762703998::1::::<><><>0<>0<>b098d3d8ca31773160ee1f4035b79a4e<>res://.godot/imported/GodotGreenPalette.png-8c760c09d8ace0593bb63002a2ed8eae.s3tc.ctex:: GodotPalette.png::CompressedTexture2D::382661063308374971::1762703890::1762703998::1::::<><><>0<>0<>2bdb257df9dcc0460d586ea92204ceb3<>res://.godot/imported/GodotPalette.png-a2d735eec81ad2647941b719336144cd.s3tc.ctex:: GodotRedPalette.png::CompressedTexture2D::3612187353401932242::1762703890::1762703996::1::::<><><>0<>0<>12aa761020ebe4c596572a3c09f2ee0b<>res://.godot/imported/GodotRedPalette.png-08bd2ae0a0cfe45c50f42f0c3a248d1b.ctex:: GodotYellowPalette.png::CompressedTexture2D::6381944650169859036::1762703890::1762703996::1::::<><><>0<>0<>96b97d4593ec92e6955954d301295c0a<>res://.godot/imported/GodotYellowPalette.png-827e66defded795634af82b4a85f17de.ctex:: -::res://assets/fonts/::1762703995 +::res://assets/fonts/::1762972860 Kurland.ttf::FontFile::7721683626151354491::1762703891::1762703995::1::::<><><>0<>0<>dacc2e7578d27658799cf58f30bac804<>res://.godot/imported/Kurland.ttf-14f9cbbd8657b37f475042f6a32feeab.fontdata:: -::res://level/::1762703936 -::res://level/scenes/::1762703936 +::res://level/::1762972860 +::res://level/scenes/::1762972860 level.tscn::PackedScene::8575440581694956823::1762703891::0::1::::<><><>0<>0<><>::uid://d0dgljwwl463n::::res://level/scripts/level.gd<>uid://cffjduipbb3s5::::res://level/scenes/player.tscn<>uid://diapabmalpcrj::::res://assets/fonts/Kurland.ttf -player.tscn::PackedScene::5134660348171653994::1762703891::0::1::::<><><>0<>0<><>::uid://c2si8gkbnde0c::::res://level/scripts/player.gd<>uid://bdg48m6l86q8i::::res://assets/characters/player/3DGodotRobot_GodotPalette.png<>uid://v8j54rbc0ik::::res://level/scripts/3d_godot_robot.gd<>uid://cw6pxwst2gh85::::res://assets/characters/player/GodotRobotPaletteSwap/GodotYellowPalette.png<>uid://bbid6mowxhd5b::::res://assets/characters/player/GodotRobotPaletteSwap/GodotGreenPalette.png<>uid://fpmmv2oxjcdv::::res://assets/characters/player/GodotRobotPaletteSwap/GodotPalette.png<>uid://bj7yrijm7bppq::::res://level/scripts/spring_arm_offset.gd<>uid://brp1gy30s4rks::::res://assets/characters/player/GodotRobotPaletteSwap/GodotRedPalette.png -::res://level/scripts/::1762703936 +player.tscn::PackedScene::5134660348171653994::1762972234::0::1::::<><><>0<>0<><>::uid://c2si8gkbnde0c::::res://level/scripts/player.gd<>uid://bdg48m6l86q8i::::res://assets/characters/player/3DGodotRobot_GodotPalette.png<>uid://v8j54rbc0ik::::res://level/scripts/3d_godot_robot.gd<>uid://cw6pxwst2gh85::::res://assets/characters/player/GodotRobotPaletteSwap/GodotYellowPalette.png<>uid://bbid6mowxhd5b::::res://assets/characters/player/GodotRobotPaletteSwap/GodotGreenPalette.png<>uid://fpmmv2oxjcdv::::res://assets/characters/player/GodotRobotPaletteSwap/GodotPalette.png<>uid://bj7yrijm7bppq::::res://level/scripts/spring_arm_offset.gd<>uid://brp1gy30s4rks::::res://assets/characters/player/GodotRobotPaletteSwap/GodotRedPalette.png<>uid://dmottq5u3my52::::res://assets/characters/Lobster/10029_Lobster_v1_iterations-2.obj +::res://level/scripts/::1762974651 3d_godot_robot.gd::GDScript::45373287015371390::1762703891::0::1::::Body<>Node3D<><>0<>0<><>:: +base_unit.gd::GDScript::928298549479668245::1762974583::0::1::::BaseUnit<>CharacterBody3D<><>0<>0<><>:: level.gd::GDScript::8920560728693148825::1762703891::0::1::::<>Node3D<><>0<>0<><>:: network.gd::GDScript::5061006158354274844::1762703891::0::1::::<>Node<><>0<>0<><>:: -player.gd::GDScript::6705643942978221932::1762703891::0::1::::Character<>CharacterBody3D<><>0<>0<><>:: +player.gd::GDScript::6705643942978221932::1762974651::0::1::::Character<>BaseUnit<><>0<>0<><>:: spring_arm_offset.gd::GDScript::3085668365566169618::1762703891::0::1::::SpringArmCharacter<>Node3D<><>0<>0<><>:: diff --git a/.godot/editor/filesystem_update4 b/.godot/editor/filesystem_update4 index 34ead9e..cd834bc 100644 --- a/.godot/editor/filesystem_update4 +++ b/.godot/editor/filesystem_update4 @@ -1 +1,2 @@ -res://level/scenes/player.tscn +res://level/scripts/player.gd +res://level/scripts/spring_arm_offset.gd diff --git a/.godot/editor/project_metadata.cfg b/.godot/editor/project_metadata.cfg index d2886f3..3db8370 100644 --- a/.godot/editor/project_metadata.cfg +++ b/.godot/editor/project_metadata.cfg @@ -10,7 +10,7 @@ executable_path="C:/Users/James/Desktop/Godot_v4.5-stable_win64.exe" [recent_files] scenes=["res://level/scenes/player.tscn", "res://level/scenes/level.tscn"] -scripts=["res://README.md", "res://level/scripts/level.gd", "res://level/scripts/3d_godot_robot.gd", "res://level/scripts/player.gd"] +scripts=["res://level/scripts/spring_arm_offset.gd", "res://README.md", "res://level/scripts/level.gd", "res://level/scripts/3d_godot_robot.gd", "res://level/scripts/player.gd"] [debug_options] diff --git a/.godot/editor/script_editor_cache.cfg b/.godot/editor/script_editor_cache.cfg index db4929c..6eb38c5 100644 --- a/.godot/editor/script_editor_cache.cfg +++ b/.godot/editor/script_editor_cache.cfg @@ -3,12 +3,16 @@ state={ "bookmarks": PackedInt32Array(), "breakpoints": PackedInt32Array(), -"column": 73, +"column": 18, "folded_lines": Array[int]([]), "h_scroll_position": 0, -"row": 112, -"scroll_position": 90.99999999999999, -"selection": false, +"row": 169, +"scroll_position": 169.0, +"selection": true, +"selection_from_column": 18, +"selection_from_line": 169, +"selection_to_column": 48, +"selection_to_line": 174, "syntax_highlighter": "GDScript" } @@ -45,11 +49,25 @@ state={ state={ "bookmarks": PackedInt32Array(), "breakpoints": PackedInt32Array(), -"column": 98, +"column": 18, "folded_lines": Array[int]([]), "h_scroll_position": 0, -"row": 22, +"row": 2, "scroll_position": 0.0, "selection": false, "syntax_highlighter": "Markdown" } + +[res://level/scripts/spring_arm_offset.gd] + +state={ +"bookmarks": PackedInt32Array(), +"breakpoints": PackedInt32Array(), +"column": 0, +"folded_lines": Array[int]([]), +"h_scroll_position": 0, +"row": 10, +"scroll_position": 0.0, +"selection": false, +"syntax_highlighter": "GDScript" +} diff --git a/.godot/global_script_class_cache.cfg b/.godot/global_script_class_cache.cfg index db39352..f90308e 100644 --- a/.godot/global_script_class_cache.cfg +++ b/.godot/global_script_class_cache.cfg @@ -1,4 +1,12 @@ list=[{ +"base": &"CharacterBody3D", +"class": &"BaseUnit", +"icon": "", +"is_abstract": false, +"is_tool": false, +"language": &"GDScript", +"path": "res://level/scripts/base_unit.gd" +}, { "base": &"Node3D", "class": &"Body", "icon": "", @@ -7,7 +15,7 @@ list=[{ "language": &"GDScript", "path": "res://level/scripts/3d_godot_robot.gd" }, { -"base": &"CharacterBody3D", +"base": &"BaseUnit", "class": &"Character", "icon": "", "is_abstract": false, diff --git a/.godot/uid_cache.bin b/.godot/uid_cache.bin index 19a0547..0e8512f 100644 Binary files a/.godot/uid_cache.bin and b/.godot/uid_cache.bin differ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..94c732d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,111 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a 3D multiplayer fighting game built in Godot 4.5 using an inheritance-based architecture. The game uses ENet for client-server networking with the Network singleton (autoload) managing all multiplayer connections. + +## Key Commands + +### Running the Game +- Open the project in Godot Editor and press F5 to run +- Or use: `godot --path . res://level/scenes/level.tscn` + +### Testing Multiplayer Locally +1. Run the main scene (level.tscn) +2. Click "Host" on one instance to create a server (default port 8080) +3. Run another instance and click "Join" to connect as a client + +## Architecture + +### Inheritance Structure +The codebase follows an inheritance-based design pattern: +- **BaseUnit** (base class) - Common functionality for all game entities that can take damage/have health +- **Character** extends **CharacterBody3D** - Player character class (class_name: Character) + - Currently implements movement, jumping, sprinting, respawning, and skin customization + - Uses multiplayer authority for input handling (only the owner processes input) + +### Networking (Network.gd) +- **Type**: Autoload singleton (accessible via `Network` globally) +- **Protocol**: ENet (UDP-based) +- **Architecture**: Client-Server (one host, multiple clients up to MAX_PLAYERS=10) +- **Player Data**: Stored in `Network.players` dictionary keyed by peer_id + - Contains: `{"nick": String, "skin": Character.SkinColor}` + +### RPC Patterns +The project uses Godot 4's RPC system for multiplayer synchronization: +- Use `@rpc("any_peer", "reliable")` for critical data (health, damage) +- Use `@rpc("any_peer", "unreliable")` for frequent position updates +- The server (multiplayer.is_server()) is authoritative for game state +- Use `multiplayer.get_remote_sender_id()` to identify RPC sender +- Use `rpc_id(peer_id, "method_name")` for targeted RPCs + +### Scene Structure +- **level/scenes/level.tscn** - Main scene with menu UI, chat, and PlayersContainer + - level.gd spawns players dynamically when they connect +- **level/scenes/player.tscn** - Player character scene + - Instantiated by server when players connect + - Named with peer_id for easy lookup + +### Physics Layers +- Layer 1: "player" - Player collision +- Layer 2: "world" - Environment collision + +### Input Actions +Defined in project.godot: +- `move_left`, `move_right`, `move_forward`, `move_backward` (WASD) +- `jump` (Space) +- `shift` (Left Shift for sprinting) +- `quit` (Escape) +- `toggle_chat` (Enter) + +## Development Guidelines + +### Adding New Components +1. Create scripts in `level/scripts/` +2. For multiplayer-synchronized components: + - Always check `is_multiplayer_authority()` before processing input + - Use RPC for state changes that need to replicate + - Server should validate all important state changes + +### Adding New Unit Types (Enemies, NPCs) +1. Create a new class that extends BaseUnit +2. Implement required virtual methods +3. Add any unique behavior in _physics_process/_process +4. Ensure multiplayer authority is set correctly + +### Testing Multiplayer +- Always test with at least 2 instances (1 host, 1 client) +- Test edge cases: late joins, disconnections, packet loss +- Verify state is synchronized correctly across all clients + +## Common Patterns + +### Spawning Networked Entities +```gdscript +# Server-side only +if multiplayer.is_server(): + var entity = entity_scene.instantiate() + entity.name = str(unique_id) # Name with ID for easy lookup + entity.set_multiplayer_authority(owner_peer_id) + container.add_child(entity, true) # true = force readable name +``` + +### Multiplayer Authority Check +```gdscript +func _physics_process(delta): + if not is_multiplayer_authority(): + return + # Only the authority processes input/logic +``` + +### RPC for State Changes +```gdscript +@rpc("any_peer", "reliable") +func take_damage(amount: int, attacker_id: int): + if not multiplayer.is_server(): + return # Server validates + # Apply damage logic + rpc("sync_health", current_health) # Broadcast to all +``` diff --git a/level/scripts/base_unit.gd b/level/scripts/base_unit.gd new file mode 100644 index 0000000..c3556bd --- /dev/null +++ b/level/scripts/base_unit.gd @@ -0,0 +1,117 @@ +extends CharacterBody3D +class_name BaseUnit + +## Base class for all units (players, enemies, NPCs) in the game +## Provides common functionality like health management and taking damage + +signal health_changed(old_health: float, new_health: float) +signal died(killer_id: int) +signal respawned() + +@export var max_health: float = 100.0 +@export var can_respawn: bool = true +@export var respawn_delay: float = 3.0 + +var current_health: float = 100.0 +var is_dead: bool = false +var _respawn_point: Vector3 = Vector3.ZERO + +func _ready(): + current_health = max_health + _respawn_point = global_position + +func _enter_tree(): + set_multiplayer_authority(str(name).to_int()) + +## Take damage from an attacker +## Should only be called on the server for authority +@rpc("any_peer", "reliable") +func take_damage(amount: float, attacker_id: int = -1): + # Only server can process damage + if not multiplayer.is_server(): + return + + if is_dead: + return + + var old_health = current_health + current_health = max(0, current_health - amount) + + # Broadcast health change to all clients + rpc("sync_health", current_health) + health_changed.emit(old_health, current_health) + + if current_health <= 0: + _die(attacker_id) + +## Heal the unit +@rpc("any_peer", "reliable") +func heal(amount: float): + if not multiplayer.is_server(): + return + + if is_dead: + return + + var old_health = current_health + current_health = min(max_health, current_health + amount) + + rpc("sync_health", current_health) + health_changed.emit(old_health, current_health) + +## Sync health across all clients +@rpc("any_peer", "call_local", "reliable") +func sync_health(new_health: float): + var old_health = current_health + current_health = new_health + health_changed.emit(old_health, current_health) + +## Handle death +func _die(killer_id: int): + if is_dead: + return + + is_dead = true + died.emit(killer_id) + rpc("sync_death", killer_id) + + if can_respawn and multiplayer.is_server(): + await get_tree().create_timer(respawn_delay).timeout + _respawn() + +## Sync death state to all clients +@rpc("any_peer", "call_local", "reliable") +func sync_death(killer_id: int): + is_dead = true + died.emit(killer_id) + # Subclasses should override to add visual effects, disable collision, etc. + +## Respawn the unit +func _respawn(): + if not multiplayer.is_server(): + return + + is_dead = false + current_health = max_health + global_position = _respawn_point + velocity = Vector3.ZERO + + rpc("sync_respawn", _respawn_point) + respawned.emit() + +## Sync respawn to all clients +@rpc("any_peer", "call_local", "reliable") +func sync_respawn(spawn_pos: Vector3): + is_dead = false + current_health = max_health + global_position = spawn_pos + velocity = Vector3.ZERO + respawned.emit() + +## Set the respawn point +func set_respawn_point(point: Vector3): + _respawn_point = point + +## Get health percentage (0.0 to 1.0) +func get_health_percent() -> float: + return current_health / max_health if max_health > 0 else 0.0 diff --git a/level/scripts/base_unit.gd.uid b/level/scripts/base_unit.gd.uid new file mode 100644 index 0000000..16e56c7 --- /dev/null +++ b/level/scripts/base_unit.gd.uid @@ -0,0 +1 @@ +uid://nhw7amcyksft diff --git a/level/scripts/player.gd b/level/scripts/player.gd index 0f044db..54f4ded 100644 --- a/level/scripts/player.gd +++ b/level/scripts/player.gd @@ -1,4 +1,4 @@ -extends CharacterBody3D +extends BaseUnit class_name Character const NORMAL_SPEED = 6.0 @@ -8,6 +8,7 @@ const JUMP_VELOCITY = 10 enum SkinColor { BLUE, YELLOW, GREEN, RED } @onready var nickname: Label3D = $PlayerNick/Nickname +@onready var health_label: Label3D = null # Optional 3D health label @export_category("Objects") @export var _body: Node3D = null @@ -25,18 +26,45 @@ enum SkinColor { BLUE, YELLOW, GREEN, RED } @onready var _limbs_head_mesh: MeshInstance3D = get_node("3DGodotRobot/RobotArmature/Skeleton3D/Llimbs and head") var _current_speed: float -var _respawn_point = Vector3(0, 5, 0) var gravity = ProjectSettings.get_setting("physics/3d/default_gravity") +# Attack system +@export var attack_damage: float = 10.0 +@export var attack_range: float = 3.0 +@export var attack_cooldown: float = 0.5 +var _attack_timer: float = 0.0 + func _enter_tree(): - set_multiplayer_authority(str(name).to_int()) + super._enter_tree() $SpringArmOffset/SpringArm3D/Camera3D.current = is_multiplayer_authority() func _ready(): - return + super._ready() + set_respawn_point(Vector3(0, 5, 0)) + + # Try to get optional health label + if has_node("PlayerNick/HealthLabel"): + health_label = get_node("PlayerNick/HealthLabel") + + # Connect health signals + health_changed.connect(_on_health_changed) + died.connect(_on_died) + respawned.connect(_on_respawned) + + # Update health display + _update_health_display() + + # Create 2D UI health bar for local player + if is_multiplayer_authority(): + _create_health_ui() func _physics_process(delta): - if not is_multiplayer_authority(): return + # Check if multiplayer is ready + if multiplayer.multiplayer_peer == null: + return + + if not is_multiplayer_authority(): + return var current_scene = get_tree().get_current_scene() if current_scene and current_scene.has_method("is_chat_visible") and current_scene.is_chat_visible() and is_on_floor(): @@ -57,10 +85,24 @@ func _physics_process(delta): move_and_slide() _body.animate(velocity) -func _process(_delta): - if not is_multiplayer_authority(): return +func _process(delta): + # Check if multiplayer is ready + if multiplayer.multiplayer_peer == null: + return + + if not is_multiplayer_authority(): + return + _check_fall_and_respawn() + # Update attack cooldown + if _attack_timer > 0: + _attack_timer -= delta + + # Handle attack input + if Input.is_action_just_pressed("attack") and _attack_timer <= 0 and not is_dead: + _perform_attack() + func freeze(): velocity.x = 0 velocity.z = 0 @@ -98,13 +140,10 @@ func is_running() -> bool: return false func _check_fall_and_respawn(): - if global_transform.origin.y < -15.0: + if global_transform.origin.y < -15.0 and multiplayer.is_server(): + # Use BaseUnit's respawn system _respawn() -func _respawn(): - global_transform.origin = _respawn_point - velocity = Vector3.ZERO - @rpc("any_peer", "reliable") func change_nick(new_nick: String): if nickname: @@ -134,3 +173,151 @@ func set_mesh_texture(mesh_instance: MeshInstance3D, texture: CompressedTexture2 var new_material := material new_material.albedo_texture = texture mesh_instance.set_surface_override_material(0, new_material) + +## Attack system +func _perform_attack(): + if not is_multiplayer_authority() or is_dead: + return + + _attack_timer = attack_cooldown + + # Find nearest enemy in range + var space_state = get_world_3d().direct_space_state + var query = PhysicsShapeQueryParameters3D.new() + var sphere = SphereShape3D.new() + sphere.radius = attack_range + query.shape = sphere + query.transform = global_transform + query.collision_mask = 1 # Player layer + + var results = space_state.intersect_shape(query) + + for result in results: + var hit_body = result["collider"] + if hit_body != self and hit_body is BaseUnit: + var attacker_id = multiplayer.get_unique_id() + + # If we're the server, apply damage directly + if multiplayer.is_server(): + _server_apply_damage(hit_body.name, attack_damage, attacker_id) + else: + # Otherwise, request server to apply damage + rpc_id(1, "_server_apply_damage", hit_body.name, attack_damage, attacker_id) + break # Only hit one target per attack + +## Server-side damage application +@rpc("any_peer", "reliable") +func _server_apply_damage(target_name: String, damage: float, attacker_id: int): + if not multiplayer.is_server(): + return + + # Get the target from the players container + var level = get_tree().get_current_scene() + if not level or not level.has_node("PlayersContainer"): + return + + var players_container = level.get_node("PlayersContainer") + if not players_container.has_node(target_name): + return + + var target = players_container.get_node(target_name) + if target and target is BaseUnit: + target.take_damage(damage, attacker_id) + +## Health display and callbacks +func _create_health_ui(): + # Create a 2D UI for the local player's health + var canvas = CanvasLayer.new() + canvas.name = "HealthUI" + add_child(canvas) + + # Health bar background + var health_bg = ColorRect.new() + health_bg.name = "HealthBG" + health_bg.color = Color(0.2, 0.2, 0.2, 0.8) + health_bg.position = Vector2(20, 20) + health_bg.size = Vector2(200, 30) + canvas.add_child(health_bg) + + # Health bar (current health) + var health_bar = ColorRect.new() + health_bar.name = "HealthBar" + health_bar.color = Color(0.0, 0.8, 0.0, 1.0) # Green + health_bar.position = Vector2(22, 22) + health_bar.size = Vector2(196, 26) + canvas.add_child(health_bar) + + # Health text + var health_text = Label.new() + health_text.name = "HealthText" + health_text.position = Vector2(20, 20) + health_text.size = Vector2(200, 30) + health_text.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + health_text.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + health_text.add_theme_color_override("font_color", Color.WHITE) + health_text.add_theme_color_override("font_outline_color", Color.BLACK) + health_text.add_theme_constant_override("outline_size", 2) + health_text.text = "HP: %d/%d" % [int(current_health), int(max_health)] + canvas.add_child(health_text) + +func _update_health_display(): + # Update 3D label if it exists + if health_label: + health_label.text = "HP: %d/%d" % [int(current_health), int(max_health)] + + # Update 2D UI for local player + if is_multiplayer_authority() and has_node("HealthUI"): + var health_bar = get_node_or_null("HealthUI/HealthBar") + var health_text = get_node_or_null("HealthUI/HealthText") + + if health_bar: + var health_percent = get_health_percent() + health_bar.size.x = 196 * health_percent + + # Change color based on health + if health_percent > 0.6: + health_bar.color = Color(0.0, 0.8, 0.0) # Green + elif health_percent > 0.3: + health_bar.color = Color(1.0, 0.8, 0.0) # Yellow + else: + health_bar.color = Color(0.8, 0.0, 0.0) # Red + + if health_text: + health_text.text = "HP: %d/%d" % [int(current_health), int(max_health)] + +func _on_health_changed(_old_health: float, _new_health: float): + _update_health_display() + +func _on_died(killer_id: int): + # Disable player when dead + set_physics_process(false) + set_process(false) + + # Visual feedback - could add death animation here + if _body: + _body.visible = false + + # Print death message + var killer_name = "Unknown" + if Network.players.has(killer_id): + killer_name = Network.players[killer_id]["nick"] + + if is_multiplayer_authority(): + print("You were killed by ", killer_name) + + # Show death message on UI + if has_node("HealthUI/HealthText"): + get_node("HealthUI/HealthText").text = "DEAD - Respawning..." + +func _on_respawned(): + # Re-enable player + set_physics_process(true) + set_process(true) + + if _body: + _body.visible = true + + _update_health_display() + + if is_multiplayer_authority(): + print("You respawned!") diff --git a/level/scripts/spring_arm_offset.gd b/level/scripts/spring_arm_offset.gd index 5b6cff5..9f6e061 100644 --- a/level/scripts/spring_arm_offset.gd +++ b/level/scripts/spring_arm_offset.gd @@ -7,6 +7,9 @@ const MOUSE_SENSIBILITY: float = 0.005 @export var _spring_arm: SpringArm3D = null func _unhandled_input(_event) -> void: + # Check if multiplayer is ready + if multiplayer.multiplayer_peer == null: + return if (_event is InputEventMouseMotion) and is_multiplayer_authority(): rotate_y(-_event.relative.x * MOUSE_SENSIBILITY) diff --git a/project.godot b/project.godot index 7cf2670..64464c6 100644 --- a/project.godot +++ b/project.godot @@ -68,6 +68,11 @@ toggle_chat={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +attack={ +"deadzone": 0.5, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null) +] +} [layer_names]