Add Diablo-style combat: player health/attack/click-move, enemies, tile-based arenas
Player (player/player.gd, shared by both island.tscn's real Player and the player/player.tscn debug prototype via the same script): - Health (max_health/health, take_damage, heal_full, death via change_scene_to_file to a new Game Over screen). - Left-click move-to-point layered onto existing WASD (WASD cancels an active click-target and takes priority; click-move re-derives the existing screen-relative animation logic from the world-space direction instead of duplicating it). New "attack" input action (Space) does a simple range check against everything in the "enemies" group. Runs via _unhandled_input so an open interaction menu naturally takes priority (button consumes the event first). New systems: - world/enemy/enemy.gd + .tscn: direct-chase CharacterBody3D (no pathfinding), contact damage with its own cooldown, reports death to ArenaManager rather than freeing itself (which owns the live count). - world/arena/tiles/: 6 placeholder tile scenes (4 plain floor colors, 2 with a simple obstacle pillar) — pure static geometry, same unshaded-flat-color convention as terrain.gd. - autoload/arena_manager.gd (new singleton): assembles a 3x3 grid of random tiles (center forced to plain floor) under a persistent ArenaRoot positioned far below the island (0,-300,0, group "arena_root"), rebuilding it fresh on every trigger since only one arena is ever active. Adds 4 axis-aligned invisible walls (same idea as terrain.gd's _build_edge_barrier, no rotation needed for a square). Spawns 3 enemies, teleports the player in. On the last enemy's death: calls the POI's mark_visited(), reuses InteractionManager.open_message() (existing, previously-unused code) for a "Sieg!" message, then teleports the player back after a timer. - world/poi/poi_arena.gd + .tscn: same Area3D/sprite shell as poi.gd, but interacting starts an arena directly instead of opening the generic menu (same divergence pattern poi_gate.gd already established). Exposes the same duck-typed `visited` field, so poi_gate.gd's region-gate group check needed zero changes. - boot/game_over_screen.gd + .tscn: same Control/Panel/Button structure as main_menu.tscn, one button back to the main menu. - world/ui/player_hud.gd + .tscn: minimal CanvasLayer HP label. Content pass: poi.gd gained `event_text` (falls back to the old flat placeholder if unset); interaction_manager.gd's "explore" case uses it and "rest" now actually calls player.heal_full(). Applied to region 1: Ort1/Ort4/Ort6 became arena POIs, Ort2/Ort3/Ort5 got distinct event text — first concrete proof of the "mix of events and arenas" pattern, replicating to regions 2-6 is a fast follow-up. Verified headless via direct method calls (confirmed limitation: synthetic clicks/keypresses don't reliably drive _unhandled_input in this harness): start_arena produces 9 tiles + a wall body + 3 enemies and teleports the player near y=-300; killing all 3 enemies flips the POI's visited flag and opens the win message; completing the win flow returns the player next to the original POI; marking the other 5 region-1 POIs visited then correctly unlocks GateR1toR2 (proving the arena-POI/gate integration without touching poi_gate.gd); player take_damage(max_health) triggers is_dead and a scene change to game_over_screen.tscn (confirmed after allowing deferred frames to process). Also verified an enemy actually closes distance on the player and lands contact damage over a live physics simulation, and that the player's attack respects both damage and cooldown. Not verifiable headlessly, needs in-editor testing: real click-to-move and Space-bar attack end-to-end, combat feel/balance, arena tile visuals.
This commit is contained in:
parent
6eb6758216
commit
9ea573d681
111
autoload/arena_manager.gd
Normal file
111
autoload/arena_manager.gd
Normal file
@ -0,0 +1,111 @@
|
||||
extends Node
|
||||
|
||||
const TILE_SIZE := 8.0
|
||||
const GRID_SIZE := 3
|
||||
const ENEMY_COUNT := 3
|
||||
const WIN_MESSAGE_SECONDS := 1.5
|
||||
const WALL_HEIGHT := 6.0
|
||||
const WALL_THICKNESS := 1.0
|
||||
|
||||
const PLAIN_TILES := [
|
||||
preload("res://world/arena/tiles/arena_tile_plain_a.tscn"),
|
||||
preload("res://world/arena/tiles/arena_tile_plain_b.tscn"),
|
||||
preload("res://world/arena/tiles/arena_tile_plain_c.tscn"),
|
||||
preload("res://world/arena/tiles/arena_tile_plain_d.tscn"),
|
||||
]
|
||||
const OBSTACLE_TILES := [
|
||||
preload("res://world/arena/tiles/arena_tile_obstacle_a.tscn"),
|
||||
preload("res://world/arena/tiles/arena_tile_obstacle_b.tscn"),
|
||||
]
|
||||
const ENEMY_SCENE := preload("res://world/enemy/enemy.tscn")
|
||||
|
||||
var _current_poi: Node = null
|
||||
var _live_enemies: int = 0
|
||||
|
||||
# Baut die Arena bei jedem Aufruf komplett neu (nur eine gleichzeitig aktiv) und
|
||||
# teleportiert den Spieler hinein. 3x3-Raster aus den Kacheln, Mittelfeld immer
|
||||
# ein Boden-Stück (damit niemand auf einem Hindernis landet).
|
||||
func start_arena(poi: Node) -> void:
|
||||
_current_poi = poi
|
||||
var arena_root := get_tree().get_first_node_in_group("arena_root")
|
||||
if arena_root == null:
|
||||
push_error("ArenaRoot nicht gefunden! Gruppe 'arena_root' gesetzt?")
|
||||
return
|
||||
|
||||
for child in arena_root.get_children():
|
||||
child.queue_free()
|
||||
|
||||
_build_walls(arena_root)
|
||||
|
||||
var half := float(GRID_SIZE) * TILE_SIZE * 0.5
|
||||
var center_index := GRID_SIZE / 2
|
||||
var spawn_cells: Array[Vector3] = []
|
||||
|
||||
for gx in range(GRID_SIZE):
|
||||
for gz in range(GRID_SIZE):
|
||||
var is_center := gx == center_index and gz == center_index
|
||||
var palette := PLAIN_TILES if is_center else (PLAIN_TILES + OBSTACLE_TILES)
|
||||
var tile_scene: PackedScene = palette[randi() % palette.size()]
|
||||
var tile := tile_scene.instantiate()
|
||||
var local_pos := Vector3(
|
||||
gx * TILE_SIZE - half + TILE_SIZE * 0.5,
|
||||
0.0,
|
||||
gz * TILE_SIZE - half + TILE_SIZE * 0.5
|
||||
)
|
||||
tile.position = local_pos
|
||||
arena_root.add_child(tile)
|
||||
if not is_center:
|
||||
spawn_cells.append(local_pos)
|
||||
|
||||
spawn_cells.shuffle()
|
||||
_live_enemies = 0
|
||||
for i in range(min(ENEMY_COUNT, spawn_cells.size())):
|
||||
var enemy := ENEMY_SCENE.instantiate()
|
||||
arena_root.add_child(enemy)
|
||||
enemy.global_position = arena_root.global_position + spawn_cells[i] + Vector3(0, 1.0, 0)
|
||||
_live_enemies += 1
|
||||
|
||||
var player := get_tree().get_first_node_in_group("player")
|
||||
if player:
|
||||
player.global_position = arena_root.global_position + Vector3(0, 1.0, 0)
|
||||
|
||||
func on_enemy_died(enemy: Node) -> void:
|
||||
enemy.queue_free()
|
||||
_live_enemies -= 1
|
||||
if _live_enemies <= 0:
|
||||
_on_win()
|
||||
|
||||
func _on_win() -> void:
|
||||
var poi := _current_poi
|
||||
if poi and poi.has_method("mark_visited"):
|
||||
poi.mark_visited()
|
||||
InteractionManager.open_message(poi, "Sieg!", "Die Gegner wurden besiegt.")
|
||||
var timer := get_tree().create_timer(WIN_MESSAGE_SECONDS)
|
||||
timer.timeout.connect(_on_win_message_done)
|
||||
|
||||
func _on_win_message_done() -> void:
|
||||
InteractionManager.close()
|
||||
var player := get_tree().get_first_node_in_group("player")
|
||||
if player and _current_poi:
|
||||
player.global_position = _current_poi.global_position + Vector3(0, 0.5, 0.6)
|
||||
_current_poi = null
|
||||
|
||||
func _build_walls(arena_root: Node3D) -> void:
|
||||
var half := float(GRID_SIZE) * TILE_SIZE * 0.5
|
||||
var barrier := StaticBody3D.new()
|
||||
barrier.name = "ArenaWalls"
|
||||
arena_root.add_child(barrier)
|
||||
|
||||
var configs := [
|
||||
{"size": Vector3(WALL_THICKNESS, WALL_HEIGHT, half * 2.0), "pos": Vector3(half, WALL_HEIGHT * 0.5, 0)},
|
||||
{"size": Vector3(WALL_THICKNESS, WALL_HEIGHT, half * 2.0), "pos": Vector3(-half, WALL_HEIGHT * 0.5, 0)},
|
||||
{"size": Vector3(half * 2.0, WALL_HEIGHT, WALL_THICKNESS), "pos": Vector3(0, WALL_HEIGHT * 0.5, half)},
|
||||
{"size": Vector3(half * 2.0, WALL_HEIGHT, WALL_THICKNESS), "pos": Vector3(0, WALL_HEIGHT * 0.5, -half)},
|
||||
]
|
||||
for cfg in configs:
|
||||
var box := BoxShape3D.new()
|
||||
box.size = cfg["size"]
|
||||
var cs := CollisionShape3D.new()
|
||||
cs.shape = box
|
||||
cs.position = cfg["pos"]
|
||||
barrier.add_child(cs)
|
||||
1
autoload/arena_manager.gd.uid
Normal file
1
autoload/arena_manager.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dmo00p6ex24u8
|
||||
@ -21,9 +21,14 @@ func _get_main_options() -> Array:
|
||||
func handle_selection(action: String) -> void:
|
||||
match action:
|
||||
"explore":
|
||||
# Platzhalter, bis echte Erkunden-Events pro POI existieren
|
||||
InteractionUI.show_text("Das wurde besucht.")
|
||||
var text := "Das wurde besucht."
|
||||
if current_poi and current_poi.has_method("get_event_text"):
|
||||
text = current_poi.get_event_text()
|
||||
InteractionUI.show_text(text)
|
||||
"rest":
|
||||
var player := get_tree().get_first_node_in_group("player")
|
||||
if player and player.has_method("heal_full"):
|
||||
player.heal_full()
|
||||
InteractionUI.show_text("Du rastest eine Weile...\nDu fühlst dich erholt.")
|
||||
"close":
|
||||
close()
|
||||
|
||||
6
boot/game_over_screen.gd
Normal file
6
boot/game_over_screen.gd
Normal file
@ -0,0 +1,6 @@
|
||||
extends Control
|
||||
|
||||
@onready var menu_button: Button = $Panel/ButtonList/MenuButton
|
||||
|
||||
func _ready() -> void:
|
||||
menu_button.pressed.connect(func(): get_tree().change_scene_to_file("res://boot/main_menu.tscn"))
|
||||
1
boot/game_over_screen.gd.uid
Normal file
1
boot/game_over_screen.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://gjcsth4d4ey7
|
||||
42
boot/game_over_screen.tscn
Normal file
42
boot/game_over_screen.tscn
Normal file
@ -0,0 +1,42 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://boot/game_over_screen.gd" id="1_gameover"]
|
||||
|
||||
[node name="GameOverScreen" type="Control"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_gameover")
|
||||
|
||||
[node name="Panel" type="Panel" parent="."]
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -160.0
|
||||
offset_top = -110.0
|
||||
offset_right = 160.0
|
||||
offset_bottom = 110.0
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="Panel"]
|
||||
anchor_right = 1.0
|
||||
offset_top = 10.0
|
||||
offset_bottom = 60.0
|
||||
text = "Du bist gestorben"
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="ButtonList" type="VBoxContainer" parent="Panel"]
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 20.0
|
||||
offset_top = 70.0
|
||||
offset_right = -20.0
|
||||
offset_bottom = -20.0
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="MenuButton" type="Button" parent="Panel/ButtonList"]
|
||||
layout_mode = 2
|
||||
text = "Zurück zum Hauptmenü"
|
||||
@ -2,8 +2,20 @@ extends CharacterBody3D
|
||||
|
||||
@export var move_speed: float = 5.0
|
||||
@export var gravity: float = 9.8
|
||||
@export var max_health: int = 10
|
||||
@export var attack_damage: int = 1
|
||||
@export var attack_range: float = 1.0
|
||||
@export var attack_cooldown: float = 0.5
|
||||
|
||||
signal health_changed(current: int, max: int)
|
||||
|
||||
var camera_rig: Node3D
|
||||
var health: int
|
||||
var is_dead: bool = false
|
||||
|
||||
var _attack_cooldown_left: float = 0.0
|
||||
var _click_move_target: Vector3 = Vector3.ZERO
|
||||
var _click_move_active: bool = false
|
||||
|
||||
@onready var sprite: AnimatedSprite3D = $AnimatedSprite3D
|
||||
# Nur in world/island.tscn vorhanden, nicht in diesem Debug-Prototyp -> optional
|
||||
@ -13,12 +25,68 @@ func _ready() -> void:
|
||||
camera_rig = get_tree().get_first_node_in_group("camera_rig")
|
||||
if camera_rig == null:
|
||||
push_error("CameraRig nicht gefunden! Gruppe 'camera_rig' gesetzt?")
|
||||
health = max_health
|
||||
|
||||
# Diablo-artig: Linksklick bewegt zusätzlich zu WASD, Leertaste greift an.
|
||||
# Läuft über _unhandled_input, damit ein offenes Interaktions-Menü (Buttons
|
||||
# verbrauchen den Klick/die Leertaste zuerst) automatisch Vorrang hat.
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if InteractionManager.active:
|
||||
return
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
|
||||
_try_click_move(event.position)
|
||||
elif event.is_action_pressed("attack"):
|
||||
_try_attack()
|
||||
|
||||
func _try_click_move(screen_pos: Vector2) -> void:
|
||||
if camera_rig == null:
|
||||
return
|
||||
var cam: Camera3D = camera_rig.camera
|
||||
var from := cam.project_ray_origin(screen_pos)
|
||||
var dir := cam.project_ray_normal(screen_pos)
|
||||
var space_state := get_world_3d().direct_space_state
|
||||
var query := PhysicsRayQueryParameters3D.create(from, from + dir * 1000.0)
|
||||
query.exclude = [get_rid()]
|
||||
var result := space_state.intersect_ray(query)
|
||||
if result:
|
||||
_click_move_target = result.position
|
||||
_click_move_active = true
|
||||
|
||||
func _try_attack() -> void:
|
||||
if _attack_cooldown_left > 0.0:
|
||||
return
|
||||
_attack_cooldown_left = attack_cooldown
|
||||
for enemy in get_tree().get_nodes_in_group("enemies"):
|
||||
if global_position.distance_to(enemy.global_position) <= attack_range:
|
||||
enemy.take_damage(attack_damage)
|
||||
|
||||
func take_damage(amount: int) -> void:
|
||||
if is_dead:
|
||||
return
|
||||
health = max(0, health - amount)
|
||||
health_changed.emit(health, max_health)
|
||||
if health <= 0:
|
||||
_die()
|
||||
|
||||
func heal_full() -> void:
|
||||
health = max_health
|
||||
health_changed.emit(health, max_health)
|
||||
|
||||
func _die() -> void:
|
||||
is_dead = true
|
||||
get_tree().change_scene_to_file("res://boot/game_over_screen.tscn")
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
# Bewegung sperren während Interaction
|
||||
if InteractionManager.active:
|
||||
velocity = Vector3.ZERO
|
||||
return
|
||||
if is_dead:
|
||||
return
|
||||
|
||||
if _attack_cooldown_left > 0.0:
|
||||
_attack_cooldown_left -= delta
|
||||
|
||||
# Schwerkraft
|
||||
if not is_on_floor():
|
||||
velocity.y -= gravity * delta
|
||||
@ -30,6 +98,7 @@ func _physics_process(delta: float) -> void:
|
||||
)
|
||||
|
||||
if input_dir != Vector2.ZERO:
|
||||
_click_move_active = false # WASD hat Vorrang vor einem laufenden Klick-Ziel
|
||||
var cam_basis = camera_rig.global_transform.basis
|
||||
var forward = -cam_basis.z
|
||||
var right = cam_basis.x
|
||||
@ -43,6 +112,29 @@ func _physics_process(delta: float) -> void:
|
||||
velocity.z = move_dir.z * move_speed
|
||||
|
||||
_update_animation(input_dir)
|
||||
elif _click_move_active:
|
||||
var to_target = _click_move_target - global_position
|
||||
to_target.y = 0
|
||||
if to_target.length() < 0.15:
|
||||
_click_move_active = false
|
||||
velocity.x = move_toward(velocity.x, 0, move_speed)
|
||||
velocity.z = move_toward(velocity.z, 0, move_speed)
|
||||
sprite.play("idle")
|
||||
else:
|
||||
var move_dir = to_target.normalized()
|
||||
velocity.x = move_dir.x * move_speed
|
||||
velocity.z = move_dir.z * move_speed
|
||||
# Weltrichtung zurück in die bestehende bildschirmrelative
|
||||
# Animationslogik übersetzen, statt eine zweite Logik zu bauen.
|
||||
var cam_basis = camera_rig.global_transform.basis
|
||||
var forward = -cam_basis.z
|
||||
var right = cam_basis.x
|
||||
forward.y = 0
|
||||
right.y = 0
|
||||
forward = forward.normalized()
|
||||
right = right.normalized()
|
||||
var screen_input := Vector2(move_dir.dot(right), -move_dir.dot(forward))
|
||||
_update_animation(screen_input)
|
||||
else:
|
||||
velocity.x = move_toward(velocity.x, 0, move_speed)
|
||||
velocity.z = move_toward(velocity.z, 0, move_speed)
|
||||
|
||||
@ -19,6 +19,7 @@ config/features=PackedStringArray("4.6", "GL Compatibility")
|
||||
InteractionManager="*uid://cbg1abspfwgxo"
|
||||
InteractionUI="*uid://cmg2008xfel2v"
|
||||
SaveManager="*res://autoload/save_manager.gd"
|
||||
ArenaManager="*res://autoload/arena_manager.gd"
|
||||
|
||||
[display]
|
||||
|
||||
@ -87,6 +88,12 @@ interact={
|
||||
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":0,"pressure":0.0,"pressed":false,"script":null)
|
||||
]
|
||||
}
|
||||
attack={
|
||||
"deadzone": 0.2,
|
||||
"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":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":2,"pressure":0.0,"pressed":false,"script":null)
|
||||
]
|
||||
}
|
||||
cancel={
|
||||
"deadzone": 0.2,
|
||||
"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":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
|
||||
41
world/arena/tiles/arena_tile_obstacle_a.tscn
Normal file
41
world/arena/tiles/arena_tile_obstacle_a.tscn
Normal file
@ -0,0 +1,41 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_floor"]
|
||||
size = Vector3(8, 0.5, 8)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_floor"]
|
||||
shading_mode = 0
|
||||
albedo_color = Color(0.45, 0.42, 0.38, 1)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_floor"]
|
||||
size = Vector3(8, 0.5, 8)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_pillar"]
|
||||
size = Vector3(1.5, 2, 1.5)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_pillar"]
|
||||
shading_mode = 0
|
||||
albedo_color = Color(0.3, 0.24, 0.2, 1)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_pillar"]
|
||||
size = Vector3(1.5, 2, 1.5)
|
||||
|
||||
[node name="ArenaTileObstacleA" type="StaticBody3D"]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0)
|
||||
mesh = SubResource("BoxMesh_floor")
|
||||
surface_material_override/0 = SubResource("StandardMaterial3D_floor")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0)
|
||||
shape = SubResource("BoxShape3D_floor")
|
||||
|
||||
[node name="Pillar" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.0, 0)
|
||||
mesh = SubResource("BoxMesh_pillar")
|
||||
surface_material_override/0 = SubResource("StandardMaterial3D_pillar")
|
||||
|
||||
[node name="PillarCollision" type="CollisionShape3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.0, 0)
|
||||
shape = SubResource("BoxShape3D_pillar")
|
||||
41
world/arena/tiles/arena_tile_obstacle_b.tscn
Normal file
41
world/arena/tiles/arena_tile_obstacle_b.tscn
Normal file
@ -0,0 +1,41 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_floor"]
|
||||
size = Vector3(8, 0.5, 8)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_floor"]
|
||||
shading_mode = 0
|
||||
albedo_color = Color(0.4, 0.38, 0.42, 1)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_floor"]
|
||||
size = Vector3(8, 0.5, 8)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_pillar"]
|
||||
size = Vector3(1.5, 2, 1.5)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_pillar"]
|
||||
shading_mode = 0
|
||||
albedo_color = Color(0.22, 0.22, 0.26, 1)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_pillar"]
|
||||
size = Vector3(1.5, 2, 1.5)
|
||||
|
||||
[node name="ArenaTileObstacleB" type="StaticBody3D"]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0)
|
||||
mesh = SubResource("BoxMesh_floor")
|
||||
surface_material_override/0 = SubResource("StandardMaterial3D_floor")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0)
|
||||
shape = SubResource("BoxShape3D_floor")
|
||||
|
||||
[node name="Pillar" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.0, 0)
|
||||
mesh = SubResource("BoxMesh_pillar")
|
||||
surface_material_override/0 = SubResource("StandardMaterial3D_pillar")
|
||||
|
||||
[node name="PillarCollision" type="CollisionShape3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.0, 0)
|
||||
shape = SubResource("BoxShape3D_pillar")
|
||||
22
world/arena/tiles/arena_tile_plain_a.tscn
Normal file
22
world/arena/tiles/arena_tile_plain_a.tscn
Normal file
@ -0,0 +1,22 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_floor"]
|
||||
size = Vector3(8, 0.5, 8)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_floor"]
|
||||
shading_mode = 0
|
||||
albedo_color = Color(0.45, 0.42, 0.38, 1)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_floor"]
|
||||
size = Vector3(8, 0.5, 8)
|
||||
|
||||
[node name="ArenaTilePlainA" type="StaticBody3D"]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0)
|
||||
mesh = SubResource("BoxMesh_floor")
|
||||
surface_material_override/0 = SubResource("StandardMaterial3D_floor")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0)
|
||||
shape = SubResource("BoxShape3D_floor")
|
||||
22
world/arena/tiles/arena_tile_plain_b.tscn
Normal file
22
world/arena/tiles/arena_tile_plain_b.tscn
Normal file
@ -0,0 +1,22 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_floor"]
|
||||
size = Vector3(8, 0.5, 8)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_floor"]
|
||||
shading_mode = 0
|
||||
albedo_color = Color(0.4, 0.38, 0.42, 1)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_floor"]
|
||||
size = Vector3(8, 0.5, 8)
|
||||
|
||||
[node name="ArenaTilePlainB" type="StaticBody3D"]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0)
|
||||
mesh = SubResource("BoxMesh_floor")
|
||||
surface_material_override/0 = SubResource("StandardMaterial3D_floor")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0)
|
||||
shape = SubResource("BoxShape3D_floor")
|
||||
22
world/arena/tiles/arena_tile_plain_c.tscn
Normal file
22
world/arena/tiles/arena_tile_plain_c.tscn
Normal file
@ -0,0 +1,22 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_floor"]
|
||||
size = Vector3(8, 0.5, 8)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_floor"]
|
||||
shading_mode = 0
|
||||
albedo_color = Color(0.42, 0.4, 0.32, 1)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_floor"]
|
||||
size = Vector3(8, 0.5, 8)
|
||||
|
||||
[node name="ArenaTilePlainC" type="StaticBody3D"]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0)
|
||||
mesh = SubResource("BoxMesh_floor")
|
||||
surface_material_override/0 = SubResource("StandardMaterial3D_floor")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0)
|
||||
shape = SubResource("BoxShape3D_floor")
|
||||
22
world/arena/tiles/arena_tile_plain_d.tscn
Normal file
22
world/arena/tiles/arena_tile_plain_d.tscn
Normal file
@ -0,0 +1,22 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_floor"]
|
||||
size = Vector3(8, 0.5, 8)
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_floor"]
|
||||
shading_mode = 0
|
||||
albedo_color = Color(0.38, 0.36, 0.36, 1)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_floor"]
|
||||
size = Vector3(8, 0.5, 8)
|
||||
|
||||
[node name="ArenaTilePlainD" type="StaticBody3D"]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0)
|
||||
mesh = SubResource("BoxMesh_floor")
|
||||
surface_material_override/0 = SubResource("StandardMaterial3D_floor")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.25, 0)
|
||||
shape = SubResource("BoxShape3D_floor")
|
||||
55
world/enemy/enemy.gd
Normal file
55
world/enemy/enemy.gd
Normal file
@ -0,0 +1,55 @@
|
||||
extends CharacterBody3D
|
||||
|
||||
@export var max_health: int = 3
|
||||
@export var move_speed: float = 3.5
|
||||
@export var aggro_range: float = 30.0
|
||||
@export var contact_damage: int = 1
|
||||
@export var contact_range: float = 0.7
|
||||
@export var contact_cooldown: float = 1.0
|
||||
@export var gravity: float = 9.8
|
||||
|
||||
var health: int
|
||||
var _contact_timer: float = 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
health = max_health
|
||||
add_to_group("enemies")
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not is_on_floor():
|
||||
velocity.y -= gravity * delta
|
||||
else:
|
||||
velocity.y = 0.0
|
||||
|
||||
if _contact_timer > 0.0:
|
||||
_contact_timer -= delta
|
||||
|
||||
var player := get_tree().get_first_node_in_group("player")
|
||||
if player:
|
||||
var to_player: Vector3 = player.global_position - global_position
|
||||
to_player.y = 0.0
|
||||
var dist := to_player.length()
|
||||
if dist <= aggro_range and dist > 0.01:
|
||||
var dir := to_player.normalized()
|
||||
velocity.x = dir.x * move_speed
|
||||
velocity.z = dir.z * move_speed
|
||||
else:
|
||||
velocity.x = 0.0
|
||||
velocity.z = 0.0
|
||||
|
||||
if dist <= contact_range and _contact_timer <= 0.0 and player.has_method("take_damage"):
|
||||
player.take_damage(contact_damage)
|
||||
_contact_timer = contact_cooldown
|
||||
else:
|
||||
velocity.x = 0.0
|
||||
velocity.z = 0.0
|
||||
|
||||
move_and_slide()
|
||||
|
||||
func take_damage(amount: int) -> void:
|
||||
if health <= 0:
|
||||
return
|
||||
health -= amount
|
||||
if health <= 0:
|
||||
health = 0
|
||||
ArenaManager.on_enemy_died(self)
|
||||
1
world/enemy/enemy.gd.uid
Normal file
1
world/enemy/enemy.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dc3e4i0qsn8kp
|
||||
31
world/enemy/enemy.tscn
Normal file
31
world/enemy/enemy.tscn
Normal file
@ -0,0 +1,31 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/enemy/enemy.gd" id="1_enemy"]
|
||||
[ext_resource type="Texture2D" uid="uid://c3ajv7vb8c4fb" path="res://assets/debug/checkerboard.png" id="2_enemy"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_enemy"]
|
||||
radius = 0.25
|
||||
height = 0.6
|
||||
|
||||
[sub_resource type="SpriteFrames" id="SpriteFrames_enemy"]
|
||||
animations = [{
|
||||
"frames": [{
|
||||
"duration": 1.0,
|
||||
"texture": ExtResource("2_enemy")
|
||||
}],
|
||||
"loop": true,
|
||||
"name": &"default",
|
||||
"speed": 5.0
|
||||
}]
|
||||
|
||||
[node name="Enemy" type="CharacterBody3D"]
|
||||
script = ExtResource("1_enemy")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("CapsuleShape3D_enemy")
|
||||
|
||||
[node name="AnimatedSprite3D" type="AnimatedSprite3D" parent="."]
|
||||
billboard = 2
|
||||
pixel_size = 0.003
|
||||
modulate = Color(1, 0.2, 0.2, 1)
|
||||
sprite_frames = SubResource("SpriteFrames_enemy")
|
||||
@ -7,6 +7,8 @@
|
||||
[ext_resource type="Script" path="res://player/equipment/equipment.gd" id="4_equip"]
|
||||
[ext_resource type="PackedScene" uid="uid://be6ufrc3dsjge" path="res://poi.tscn" id="5_lquwl"]
|
||||
[ext_resource type="PackedScene" path="res://world/poi/poi_gate.tscn" id="5_gate"]
|
||||
[ext_resource type="PackedScene" path="res://world/poi/poi_arena.tscn" id="5_arena"]
|
||||
[ext_resource type="PackedScene" path="res://world/ui/player_hud.tscn" id="9_hud"]
|
||||
[ext_resource type="PackedScene" uid="uid://dmhs1oy1kh6hi" path="res://assets/models/mountain.glb" id="6_7mycd"]
|
||||
[ext_resource type="PackedScene" uid="uid://gl4qpk4wto1e" path="res://assets/models/mountain2v5.glb" id="7_272bh"]
|
||||
[ext_resource type="PackedScene" uid="uid://be2uq7ixlbv6b" path="res://assets/models/forest.glb" id="8_5vw27"]
|
||||
@ -210,34 +212,37 @@ far = 500.0
|
||||
[node name="POI" parent="." unique_id=1770000468 instance=ExtResource("5_lquwl")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 0.5, 2)
|
||||
|
||||
[node name="Ort1" parent="." instance=ExtResource("5_lquwl") groups=["region1_pois"]]
|
||||
[node name="Ort1" parent="." instance=ExtResource("5_arena") groups=["region1_pois"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 24.15, -1.214, 6.47)
|
||||
poi_name = "Ort 1"
|
||||
poi_name = "Verlassenes Schlachtfeld"
|
||||
marker_color = Color(1, 0.2, 0.2, 1)
|
||||
|
||||
[node name="Ort2" parent="." instance=ExtResource("5_lquwl") groups=["region1_pois"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 17.68, -1.214, 17.68)
|
||||
poi_name = "Ort 2"
|
||||
marker_color = Color(1, 0.6, 0.1, 1)
|
||||
event_text = "Alte Fußspuren führen ins Nichts. Wer auch immer hier war, ist längst weitergezogen."
|
||||
|
||||
[node name="Ort3" parent="." instance=ExtResource("5_lquwl") groups=["region1_pois"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 37.17, -3.443, 7.9)
|
||||
poi_name = "Ort 3"
|
||||
marker_color = Color(1, 0.95, 0.2, 1)
|
||||
event_text = "Ein verwittertes Wegzeichen weist auf ein Gebiet, das es nicht mehr gibt."
|
||||
|
||||
[node name="Ort4" parent="." instance=ExtResource("5_lquwl") groups=["region1_pois"]]
|
||||
[node name="Ort4" parent="." instance=ExtResource("5_arena") groups=["region1_pois"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 25.43, -3.443, 28.24)
|
||||
poi_name = "Ort 4"
|
||||
poi_name = "Wachpfostenruine"
|
||||
marker_color = Color(0.2, 0.9, 0.3, 1)
|
||||
|
||||
[node name="Ort5" parent="." instance=ExtResource("5_lquwl") groups=["region1_pois"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 19.05, -0.7, 11.0)
|
||||
poi_name = "Ort 5"
|
||||
marker_color = Color(0.2, 0.5, 1, 1)
|
||||
event_text = "Der Wind trägt eine Melodie herbei, die du nicht greifen kannst."
|
||||
|
||||
[node name="Ort6" parent="." instance=ExtResource("5_lquwl") groups=["region1_pois"]]
|
||||
[node name="Ort6" parent="." instance=ExtResource("5_arena") groups=["region1_pois"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 28.37, -3.1, 22.17)
|
||||
poi_name = "Ort 6"
|
||||
poi_name = "Vergessener Hinterhalt"
|
||||
marker_color = Color(0.7, 0.3, 0.9, 1)
|
||||
|
||||
[node name="GateR1toR2" parent="." instance=ExtResource("5_gate")]
|
||||
@ -517,3 +522,8 @@ shape = SubResource("CylinderShape3D_kek77")
|
||||
[node name="CollisionShape3D3" type="CollisionShape3D" parent="Node3D3/forest2" unique_id=1876581014]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.084690094, -0.5371094, 0.40002155)
|
||||
shape = SubResource("CylinderShape3D_kek77")
|
||||
|
||||
[node name="ArenaRoot" type="Node3D" parent="." groups=["arena_root"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -300, 0)
|
||||
|
||||
[node name="PlayerHUD" parent="." instance=ExtResource("9_hud")]
|
||||
|
||||
@ -2,6 +2,7 @@ extends Area3D
|
||||
|
||||
@export var poi_name: String = "Ruinen"
|
||||
@export var marker_color: Color = Color.WHITE
|
||||
@export var event_text: String = ""
|
||||
|
||||
@onready var sprite: AnimatedSprite3D = $AnimatedSprite3D
|
||||
@onready var name_label: Label3D = $Billboard/NameLabel
|
||||
@ -45,6 +46,9 @@ func _update_visuals() -> void:
|
||||
sprite.modulate = marker_color * Color(0.1, 0.1, 0.1, 1.0)
|
||||
name_label.modulate = Color(0.6, 0.6, 0.6, 1.0)
|
||||
|
||||
func get_event_text() -> String:
|
||||
return event_text if event_text != "" else "Das wurde besucht."
|
||||
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
if body.is_in_group("player"):
|
||||
player_nearby = true
|
||||
|
||||
60
world/poi/poi_arena.gd
Normal file
60
world/poi/poi_arena.gd
Normal file
@ -0,0 +1,60 @@
|
||||
extends Area3D
|
||||
|
||||
@export var poi_name: String = "Kampfarena"
|
||||
@export var marker_color: Color = Color.WHITE
|
||||
|
||||
@onready var sprite: AnimatedSprite3D = $AnimatedSprite3D
|
||||
@onready var name_label: Label3D = $Billboard/NameLabel
|
||||
@onready var prompt: Label3D = $Billboard/InteractionPrompt
|
||||
|
||||
var player_nearby: bool = false
|
||||
var visited: bool = false
|
||||
|
||||
func _ready() -> void:
|
||||
prompt.visible = false
|
||||
prompt.text = "[F] Interact"
|
||||
_update_visuals()
|
||||
body_entered.connect(_on_body_entered)
|
||||
body_exited.connect(_on_body_exited)
|
||||
# Terrain-Kollision braucht mind. einen Physik-Tick um registriert zu sein
|
||||
await get_tree().physics_frame
|
||||
_snap_to_terrain()
|
||||
|
||||
func _snap_to_terrain() -> void:
|
||||
var space_state := get_world_3d().direct_space_state
|
||||
var from := global_position + Vector3(0, 20, 0)
|
||||
var to := global_position + Vector3(0, -20, 0)
|
||||
var query := PhysicsRayQueryParameters3D.create(from, to)
|
||||
query.exclude = [get_rid(), $StaticBody3D.get_rid()]
|
||||
var result := space_state.intersect_ray(query)
|
||||
if result:
|
||||
global_position.y = result.position.y + 0.5
|
||||
|
||||
func _update_visuals() -> void:
|
||||
if visited:
|
||||
name_label.text = poi_name
|
||||
sprite.modulate = marker_color
|
||||
else:
|
||||
name_label.text = "???"
|
||||
sprite.modulate = marker_color * Color(0.4, 0.4, 0.4, 1.0)
|
||||
|
||||
func mark_visited() -> void:
|
||||
visited = true
|
||||
_update_visuals()
|
||||
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
if body.is_in_group("player"):
|
||||
player_nearby = true
|
||||
prompt.visible = true
|
||||
|
||||
func _on_body_exited(body: Node3D) -> void:
|
||||
if body.is_in_group("player"):
|
||||
player_nearby = false
|
||||
prompt.visible = false
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if player_nearby and Input.is_action_just_pressed("interact"):
|
||||
if not visited:
|
||||
ArenaManager.start_arena(self)
|
||||
else:
|
||||
InteractionManager.open_message(self, poi_name, "Bereits bezwungen.")
|
||||
1
world/poi/poi_arena.gd.uid
Normal file
1
world/poi/poi_arena.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bltg23248s2y7
|
||||
53
world/poi/poi_arena.tscn
Normal file
53
world/poi/poi_arena.tscn
Normal file
@ -0,0 +1,53 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/poi/poi_arena.gd" id="1_arena"]
|
||||
[ext_resource type="Texture2D" uid="uid://c3ajv7vb8c4fb" path="res://assets/debug/checkerboard.png" id="2_arena"]
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_arena"]
|
||||
radius = 0.6
|
||||
|
||||
[sub_resource type="SpriteFrames" id="SpriteFrames_arena"]
|
||||
animations = [{
|
||||
"frames": [{
|
||||
"duration": 1.0,
|
||||
"texture": ExtResource("2_arena")
|
||||
}],
|
||||
"loop": true,
|
||||
"name": &"default",
|
||||
"speed": 5.0
|
||||
}]
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_arena"]
|
||||
radius = 0.4
|
||||
height = 1.0
|
||||
|
||||
[node name="POIArena" type="Area3D"]
|
||||
script = ExtResource("1_arena")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("SphereShape3D_arena")
|
||||
|
||||
[node name="AnimatedSprite3D" type="AnimatedSprite3D" parent="."]
|
||||
billboard = 2
|
||||
pixel_size = 0.004
|
||||
sprite_frames = SubResource("SpriteFrames_arena")
|
||||
|
||||
[node name="StaticBody3D" type="StaticBody3D" parent="."]
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D"]
|
||||
shape = SubResource("CylinderShape3D_arena")
|
||||
|
||||
[node name="Billboard" type="Node3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)
|
||||
|
||||
[node name="NameLabel" type="Label3D" parent="Billboard"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.7, 0)
|
||||
billboard = 1
|
||||
text = "???"
|
||||
font_size = 16
|
||||
|
||||
[node name="InteractionPrompt" type="Label3D" parent="Billboard"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.55, 0)
|
||||
visible = false
|
||||
billboard = 1
|
||||
font_size = 12
|
||||
12
world/ui/player_hud.gd
Normal file
12
world/ui/player_hud.gd
Normal file
@ -0,0 +1,12 @@
|
||||
extends CanvasLayer
|
||||
|
||||
@onready var label: Label = $Label
|
||||
|
||||
func _ready() -> void:
|
||||
var player := get_tree().get_first_node_in_group("player")
|
||||
if player and player.has_signal("health_changed"):
|
||||
player.health_changed.connect(_on_health_changed)
|
||||
_on_health_changed(player.health, player.max_health)
|
||||
|
||||
func _on_health_changed(current: int, max_hp: int) -> void:
|
||||
label.text = "HP: %d / %d" % [current, max_hp]
|
||||
1
world/ui/player_hud.gd.uid
Normal file
1
world/ui/player_hud.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://capiuygufek2t
|
||||
13
world/ui/player_hud.tscn
Normal file
13
world/ui/player_hud.tscn
Normal file
@ -0,0 +1,13 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/ui/player_hud.gd" id="1_hud"]
|
||||
|
||||
[node name="PlayerHUD" type="CanvasLayer"]
|
||||
script = ExtResource("1_hud")
|
||||
|
||||
[node name="Label" type="Label" parent="."]
|
||||
offset_left = 16.0
|
||||
offset_top = 16.0
|
||||
offset_right = 200.0
|
||||
offset_bottom = 46.0
|
||||
text = "HP: 10 / 10"
|
||||
Loading…
x
Reference in New Issue
Block a user