Health and attacks!

Dashfix
Twirpytherobot 3 weeks ago
parent 1d63abc581
commit bc30cfd6ca
  1. 12
      .godot/editor/editor_layout.cfg
  2. BIN
      .godot/editor/editor_script_doc_cache.res
  3. 28
      .godot/editor/filesystem_cache10
  4. 3
      .godot/editor/filesystem_update4
  5. 2
      .godot/editor/project_metadata.cfg
  6. 30
      .godot/editor/script_editor_cache.cfg
  7. 10
      .godot/global_script_class_cache.cfg
  8. BIN
      .godot/uid_cache.bin
  9. 111
      CLAUDE.md
  10. 117
      level/scripts/base_unit.gd
  11. 1
      level/scripts/base_unit.gd.uid
  12. 211
      level/scripts/player.gd
  13. 3
      level/scripts/spring_arm_offset.gd
  14. 5
      project.godot

@ -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]

@ -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<><>::

@ -1 +1,2 @@
res://level/scenes/player.tscn
res://level/scripts/player.gd
res://level/scripts/spring_arm_offset.gd

@ -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]

@ -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"
}

@ -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,

Binary file not shown.

@ -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
```

@ -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

@ -0,0 +1 @@
uid://nhw7amcyksft

@ -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!")

@ -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)

@ -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]

Loading…
Cancel
Save