Adding endpoint to only get locations visible to character
All checks were successful
All checks were successful
This commit is contained in:
parent
4ba06bf7e0
commit
84f8087647
@ -0,0 +1,8 @@
|
|||||||
|
[gd_resource type="StandardMaterial3D" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Texture2D" path="res://assets/textures/kenney_prototype/PNG/Orange/texture_08.png" id="1_gateway"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
albedo_texture = ExtResource("1_gateway")
|
||||||
|
uv1_triplanar = true
|
||||||
|
uv1_scale = Vector3(1, 1, 1)
|
||||||
249
game/scenes/Characters/location_player.gd
Normal file
249
game/scenes/Characters/location_player.gd
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
extends RigidBody3D
|
||||||
|
# Initially I used a CharacterBody3D, however, I wanted the player to bounce off
|
||||||
|
# other objects in the environment and that would have required manual handling
|
||||||
|
# of collisions. So that's why we're using a RigidBody3D instead.
|
||||||
|
signal vehicle_entered(vehicle: Node)
|
||||||
|
signal vehicle_exited(vehicle: Node)
|
||||||
|
|
||||||
|
const MOVE_SPEED := 8.0
|
||||||
|
const SPRINT_MOVE_SPEED :=13
|
||||||
|
const ACCELLERATION := 30.0
|
||||||
|
const DECELLERATION := 40.0
|
||||||
|
const JUMP_SPEED := 4.0
|
||||||
|
const MAX_NUMBER_OF_JUMPS := 2
|
||||||
|
const MIN_FOV := 10.0
|
||||||
|
const MAX_FOV := 179.0
|
||||||
|
const ZOOM_FACTOR := 1.1 # Zoom out when >1, in when < 1
|
||||||
|
var mouse_sensitivity := 0.005
|
||||||
|
var rotation_x := 0.0
|
||||||
|
var rotation_y := 0.0
|
||||||
|
var cameraMoveMode := false
|
||||||
|
var current_number_of_jumps := 0
|
||||||
|
var _pending_mouse_delta := Vector2.ZERO
|
||||||
|
var _last_move_forward := Vector3(0, 0, 1)
|
||||||
|
var _last_move_right := Vector3(1, 0, 0)
|
||||||
|
var _camera_offset_local := Vector3.ZERO
|
||||||
|
var _camera_yaw := 0.0
|
||||||
|
var _camera_pitch := 0.0
|
||||||
|
var _in_vehicle := false
|
||||||
|
var _vehicle_collision_layer := 0
|
||||||
|
var _vehicle_collision_mask := 0
|
||||||
|
var _vehicle_original_parent: Node = null
|
||||||
|
var _light_was_on := false
|
||||||
|
var _jump_triggered := false
|
||||||
|
@onready var _flashlight: SpotLight3D = $SpotLight3D
|
||||||
|
@onready var _anim_player: AnimationPlayer = find_child("AnimationPlayer", true, false) as AnimationPlayer
|
||||||
|
@onready var _anim_tree: AnimationTree = find_child("AnimationTree", true, false) as AnimationTree
|
||||||
|
@onready var _model_root: Node3D = find_child("TestCharAnimated", true, false) as Node3D
|
||||||
|
|
||||||
|
@export var camera_follow_speed := 10.0
|
||||||
|
@export var anim_idle_name := "Idle"
|
||||||
|
@export var anim_walk_name := "Walk"
|
||||||
|
@export var anim_jump_name := "Jump"
|
||||||
|
@export var anim_run_name := "Run"
|
||||||
|
@export var anim_walk_speed_threshold := 0.25
|
||||||
|
@export var anim_sprint_speed_threshold := 10.0
|
||||||
|
|
||||||
|
var jump_sound = preload("res://assets/audio/jump.ogg")
|
||||||
|
var audio_player = AudioStreamPlayer.new()
|
||||||
|
|
||||||
|
@export var camera_path: NodePath
|
||||||
|
@onready var cam: Camera3D = get_node(camera_path) if camera_path != NodePath("") else null
|
||||||
|
@export var phone_path: NodePath
|
||||||
|
@onready var phone: CanvasLayer = get_node(phone_path) if phone_path != NodePath("") else null
|
||||||
|
var phone_visible := false
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
add_to_group("player")
|
||||||
|
if _anim_tree:
|
||||||
|
_anim_tree.active = false
|
||||||
|
axis_lock_angular_x = true
|
||||||
|
axis_lock_angular_z = true
|
||||||
|
angular_damp = 6.0
|
||||||
|
contact_monitor = true
|
||||||
|
max_contacts_reported = 4
|
||||||
|
add_child(audio_player)
|
||||||
|
audio_player.stream = jump_sound
|
||||||
|
audio_player.volume_db = -20
|
||||||
|
if cam:
|
||||||
|
_camera_offset_local = cam.transform.origin
|
||||||
|
_camera_pitch = cam.rotation.x
|
||||||
|
_camera_yaw = global_transform.basis.get_euler().y
|
||||||
|
cam.set_as_top_level(true)
|
||||||
|
cam.global_position = global_position + (Basis(Vector3.UP, _camera_yaw) * _camera_offset_local)
|
||||||
|
cam.global_rotation = Vector3(_camera_pitch, _camera_yaw, 0.0)
|
||||||
|
var move_basis := cam.global_transform.basis if cam else global_transform.basis
|
||||||
|
var forward := move_basis.z
|
||||||
|
var right := move_basis.x
|
||||||
|
forward.y = 0.0
|
||||||
|
right.y = 0.0
|
||||||
|
if forward.length() > 0.0001:
|
||||||
|
_last_move_forward = forward.normalized()
|
||||||
|
if right.length() > 0.0001:
|
||||||
|
_last_move_right = right.normalized()
|
||||||
|
_vehicle_collision_layer = collision_layer
|
||||||
|
_vehicle_collision_mask = collision_mask
|
||||||
|
|
||||||
|
func _integrate_forces(state):
|
||||||
|
if _in_vehicle:
|
||||||
|
linear_velocity = Vector3.ZERO
|
||||||
|
return
|
||||||
|
if cameraMoveMode and _pending_mouse_delta != Vector2.ZERO:
|
||||||
|
rotation_x -= _pending_mouse_delta.y * mouse_sensitivity
|
||||||
|
rotation_y -= _pending_mouse_delta.x * mouse_sensitivity
|
||||||
|
rotation_x = clamp(rotation_x, deg_to_rad(-90), deg_to_rad(90))
|
||||||
|
_camera_pitch = rotation_x
|
||||||
|
rotation.y = rotation_y
|
||||||
|
_pending_mouse_delta = Vector2.ZERO
|
||||||
|
|
||||||
|
var input2v := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
|
||||||
|
var forward := Vector3.FORWARD * -1.0
|
||||||
|
var right := Vector3.RIGHT
|
||||||
|
if cam:
|
||||||
|
forward = cam.global_transform.basis.z
|
||||||
|
right = cam.global_transform.basis.x
|
||||||
|
forward.y = 0.0
|
||||||
|
right.y = 0.0
|
||||||
|
if forward.length() > 0.0001:
|
||||||
|
forward = forward.normalized()
|
||||||
|
_last_move_forward = forward
|
||||||
|
else:
|
||||||
|
forward = _last_move_forward
|
||||||
|
if right.length() > 0.0001:
|
||||||
|
right = right.normalized()
|
||||||
|
_last_move_right = right
|
||||||
|
else:
|
||||||
|
right = _last_move_right
|
||||||
|
|
||||||
|
var dir := (right * input2v.x + forward * input2v.y).normalized()
|
||||||
|
var target_v := dir * MOVE_SPEED
|
||||||
|
if Input.is_key_pressed(KEY_SHIFT):
|
||||||
|
target_v = dir * SPRINT_MOVE_SPEED
|
||||||
|
|
||||||
|
var ax := ACCELLERATION if dir != Vector3.ZERO else DECELLERATION
|
||||||
|
linear_velocity.x = move_toward(linear_velocity.x, target_v.x, ax * state.step)
|
||||||
|
linear_velocity.z = move_toward(linear_velocity.z, target_v.z, ax * state.step)
|
||||||
|
|
||||||
|
var on_floor = false
|
||||||
|
for i in state.get_contact_count():
|
||||||
|
var normal = state.get_contact_local_normal(i)
|
||||||
|
if normal.y > 0.5:
|
||||||
|
on_floor = true
|
||||||
|
break
|
||||||
|
|
||||||
|
if Input.is_action_just_pressed("ui_accept") and (on_floor or current_number_of_jumps == 1):
|
||||||
|
current_number_of_jumps = (current_number_of_jumps + 1) % 2
|
||||||
|
linear_velocity.y = JUMP_SPEED
|
||||||
|
audio_player.play()
|
||||||
|
_jump_triggered = true
|
||||||
|
|
||||||
|
if cam:
|
||||||
|
var target_yaw := global_transform.basis.get_euler().y
|
||||||
|
_camera_yaw = lerp_angle(_camera_yaw, target_yaw, camera_follow_speed * state.step)
|
||||||
|
var target_basis := Basis(Vector3.UP, _camera_yaw)
|
||||||
|
var target_pos := global_position + (target_basis * _camera_offset_local)
|
||||||
|
cam.global_position = cam.global_position.lerp(target_pos, camera_follow_speed * state.step)
|
||||||
|
cam.global_rotation = Vector3(_camera_pitch, _camera_yaw, 0.0)
|
||||||
|
|
||||||
|
_update_animation(on_floor, state.linear_velocity)
|
||||||
|
_jump_triggered = false
|
||||||
|
|
||||||
|
func _input(event):
|
||||||
|
if event.is_action_pressed("player_phone"):
|
||||||
|
phone_visible = !phone_visible
|
||||||
|
if phone:
|
||||||
|
phone.visible = phone_visible
|
||||||
|
return
|
||||||
|
|
||||||
|
if _in_vehicle:
|
||||||
|
return
|
||||||
|
if event is InputEventMouseButton:
|
||||||
|
if event.button_index == MOUSE_BUTTON_MIDDLE:
|
||||||
|
if event.pressed:
|
||||||
|
cameraMoveMode = true
|
||||||
|
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||||
|
else:
|
||||||
|
cameraMoveMode = false
|
||||||
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||||
|
|
||||||
|
if event is InputEventMouseMotion and cameraMoveMode:
|
||||||
|
_pending_mouse_delta += event.relative
|
||||||
|
|
||||||
|
if event is InputEventMouseButton and event.pressed:
|
||||||
|
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
||||||
|
zoom_camera(1.0 / ZOOM_FACTOR)
|
||||||
|
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||||
|
zoom_camera(ZOOM_FACTOR)
|
||||||
|
|
||||||
|
if event.is_action_pressed("player_light"):
|
||||||
|
_flashlight.visible = !_flashlight.visible
|
||||||
|
|
||||||
|
func zoom_camera(factor):
|
||||||
|
if cam == null:
|
||||||
|
return
|
||||||
|
var new_fov = cam.fov * factor
|
||||||
|
cam.fov = clamp(new_fov, MIN_FOV, MAX_FOV)
|
||||||
|
|
||||||
|
func _update_animation(on_floor: bool, velocity: Vector3) -> void:
|
||||||
|
if _anim_player == null:
|
||||||
|
return
|
||||||
|
var horizontal_speed := Vector3(velocity.x, 0.0, velocity.z).length()
|
||||||
|
if _jump_triggered and _anim_player.has_animation(anim_jump_name):
|
||||||
|
if _anim_player.current_animation != anim_jump_name:
|
||||||
|
_anim_player.play(anim_jump_name)
|
||||||
|
return
|
||||||
|
if not on_floor and _anim_player.has_animation(anim_jump_name):
|
||||||
|
if _anim_player.current_animation != anim_jump_name:
|
||||||
|
_anim_player.play(anim_jump_name)
|
||||||
|
return
|
||||||
|
if on_floor and horizontal_speed > anim_sprint_speed_threshold and _anim_player.has_animation(anim_run_name):
|
||||||
|
if _anim_player.current_animation != anim_run_name:
|
||||||
|
_anim_player.play(anim_run_name)
|
||||||
|
return
|
||||||
|
if horizontal_speed > anim_walk_speed_threshold and _anim_player.has_animation(anim_walk_name):
|
||||||
|
if _anim_player.current_animation != anim_walk_name:
|
||||||
|
_anim_player.play(anim_walk_name)
|
||||||
|
return
|
||||||
|
if _anim_player.has_animation(anim_idle_name):
|
||||||
|
if _anim_player.current_animation != anim_idle_name:
|
||||||
|
_anim_player.play(anim_idle_name)
|
||||||
|
|
||||||
|
func enter_vehicle(_vehicle: Node, seat: Node3D, vehicle_camera: Camera3D) -> void:
|
||||||
|
_in_vehicle = true
|
||||||
|
freeze = true
|
||||||
|
sleeping = true
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 0
|
||||||
|
_vehicle_original_parent = get_parent()
|
||||||
|
_light_was_on = _flashlight.visible
|
||||||
|
_flashlight.visible = false
|
||||||
|
if _model_root:
|
||||||
|
_model_root.visible = false
|
||||||
|
if seat:
|
||||||
|
reparent(seat, true)
|
||||||
|
global_transform = seat.global_transform
|
||||||
|
if cam:
|
||||||
|
cam.current = false
|
||||||
|
if vehicle_camera:
|
||||||
|
vehicle_camera.current = true
|
||||||
|
vehicle_entered.emit(_vehicle)
|
||||||
|
|
||||||
|
func exit_vehicle(exit_point: Node3D, vehicle_camera: Camera3D) -> void:
|
||||||
|
_in_vehicle = false
|
||||||
|
freeze = false
|
||||||
|
sleeping = false
|
||||||
|
collision_layer = _vehicle_collision_layer
|
||||||
|
collision_mask = _vehicle_collision_mask
|
||||||
|
if _vehicle_original_parent:
|
||||||
|
reparent(_vehicle_original_parent, true)
|
||||||
|
_vehicle_original_parent = null
|
||||||
|
_flashlight.visible = _light_was_on
|
||||||
|
if _model_root:
|
||||||
|
_model_root.visible = true
|
||||||
|
if exit_point:
|
||||||
|
global_transform = exit_point.global_transform
|
||||||
|
if vehicle_camera:
|
||||||
|
vehicle_camera.current = false
|
||||||
|
if cam:
|
||||||
|
cam.current = true
|
||||||
|
vehicle_exited.emit(null)
|
||||||
1
game/scenes/Characters/location_player.gd.uid
Normal file
1
game/scenes/Characters/location_player.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://kgqaeqappow3
|
||||||
28
game/scenes/Characters/location_player.tscn
Normal file
28
game/scenes/Characters/location_player.tscn
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
[gd_scene load_steps=4 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scenes/Characters/location_player.gd" id="1_player_script"]
|
||||||
|
[ext_resource type="PackedScene" path="res://assets/models/TestCharAnimated.glb" id="2_model"]
|
||||||
|
|
||||||
|
[sub_resource type="SphereShape3D" id="SphereShape3D_player"]
|
||||||
|
|
||||||
|
[node name="LocationPlayer" type="RigidBody3D"]
|
||||||
|
script = ExtResource("1_player_script")
|
||||||
|
camera_path = NodePath("Camera3D")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
|
||||||
|
shape = SubResource("SphereShape3D_player")
|
||||||
|
|
||||||
|
[node name="TestCharAnimated" parent="." instance=ExtResource("2_model")]
|
||||||
|
transform = Transform3D(-0.9998549, 0, 0.01703362, 0, 1, 0, -0.01703362, 0, -0.9998549, 0, 0, 0)
|
||||||
|
|
||||||
|
[node name="Camera3D" type="Camera3D" parent="."]
|
||||||
|
transform = Transform3D(0.9998477, 0, -0.017452406, 0.0066714617, 0.9238795, 0.38262552, 0.016124869, -0.38268343, 0.92373866, 0, 6, 10)
|
||||||
|
current = true
|
||||||
|
fov = 49.0
|
||||||
|
|
||||||
|
[node name="SpotLight3D" type="SpotLight3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.906308, -0.422618, 0, 0.422618, 0.906308, 0, 1.7, -0.35)
|
||||||
|
visible = false
|
||||||
|
spot_range = 30.0
|
||||||
|
spot_angle = 25.0
|
||||||
31
game/scenes/Interaction/prototype_gateway.gd
Normal file
31
game/scenes/Interaction/prototype_gateway.gd
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
@tool
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
@export_file("*.tscn") var target_scene_path := "res://scenes/Levels/transportation_level.tscn":
|
||||||
|
set(value):
|
||||||
|
target_scene_path = value
|
||||||
|
_sync_teleporter()
|
||||||
|
|
||||||
|
@export var target_group: StringName = &"player":
|
||||||
|
set(value):
|
||||||
|
target_group = value
|
||||||
|
_sync_teleporter()
|
||||||
|
|
||||||
|
@export var one_shot := true:
|
||||||
|
set(value):
|
||||||
|
one_shot = value
|
||||||
|
_sync_teleporter()
|
||||||
|
|
||||||
|
@onready var teleporter: Area3D = $Teleporter
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_sync_teleporter()
|
||||||
|
|
||||||
|
|
||||||
|
func _sync_teleporter() -> void:
|
||||||
|
if teleporter == null:
|
||||||
|
return
|
||||||
|
teleporter.set("target_scene_path", target_scene_path)
|
||||||
|
teleporter.set("target_group", target_group)
|
||||||
|
teleporter.set("one_shot", one_shot)
|
||||||
1
game/scenes/Interaction/prototype_gateway.gd.uid
Normal file
1
game/scenes/Interaction/prototype_gateway.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://1c0reto6vt6m
|
||||||
117
game/scenes/Interaction/prototype_gateway.tscn
Normal file
117
game/scenes/Interaction/prototype_gateway.tscn
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
[gd_scene load_steps=6 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scenes/Interaction/prototype_gateway.gd" id="1_gateway_script"]
|
||||||
|
[ext_resource type="PackedScene" path="res://scenes/Interaction/scene_teleporter.tscn" id="2_teleporter"]
|
||||||
|
[ext_resource type="Material" path="res://assets/materials/kenney_prototype_gateway_orange.tres" id="3_gateway_mat"]
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_gateway"]
|
||||||
|
size = Vector3(1, 1, 1)
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_gateway"]
|
||||||
|
material = ExtResource("3_gateway_mat")
|
||||||
|
size = Vector3(1, 1, 1)
|
||||||
|
|
||||||
|
[node name="PrototypeGateway" type="Node3D"]
|
||||||
|
script = ExtResource("1_gateway_script")
|
||||||
|
|
||||||
|
[node name="LeftBase" type="StaticBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 0.5, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="LeftBase"]
|
||||||
|
shape = SubResource("BoxShape3D_gateway")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="LeftBase"]
|
||||||
|
mesh = SubResource("BoxMesh_gateway")
|
||||||
|
|
||||||
|
[node name="LeftMidLow" type="StaticBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 1.5, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="LeftMidLow"]
|
||||||
|
shape = SubResource("BoxShape3D_gateway")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="LeftMidLow"]
|
||||||
|
mesh = SubResource("BoxMesh_gateway")
|
||||||
|
|
||||||
|
[node name="LeftMidHigh" type="StaticBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 2.5, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="LeftMidHigh"]
|
||||||
|
shape = SubResource("BoxShape3D_gateway")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="LeftMidHigh"]
|
||||||
|
mesh = SubResource("BoxMesh_gateway")
|
||||||
|
|
||||||
|
[node name="LeftTop" type="StaticBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 3.5, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="LeftTop"]
|
||||||
|
shape = SubResource("BoxShape3D_gateway")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="LeftTop"]
|
||||||
|
mesh = SubResource("BoxMesh_gateway")
|
||||||
|
|
||||||
|
[node name="RightBase" type="StaticBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 0.5, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="RightBase"]
|
||||||
|
shape = SubResource("BoxShape3D_gateway")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="RightBase"]
|
||||||
|
mesh = SubResource("BoxMesh_gateway")
|
||||||
|
|
||||||
|
[node name="RightMidLow" type="StaticBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 1.5, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="RightMidLow"]
|
||||||
|
shape = SubResource("BoxShape3D_gateway")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="RightMidLow"]
|
||||||
|
mesh = SubResource("BoxMesh_gateway")
|
||||||
|
|
||||||
|
[node name="RightMidHigh" type="StaticBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 2.5, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="RightMidHigh"]
|
||||||
|
shape = SubResource("BoxShape3D_gateway")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="RightMidHigh"]
|
||||||
|
mesh = SubResource("BoxMesh_gateway")
|
||||||
|
|
||||||
|
[node name="RightTop" type="StaticBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 3.5, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="RightTop"]
|
||||||
|
shape = SubResource("BoxShape3D_gateway")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="RightTop"]
|
||||||
|
mesh = SubResource("BoxMesh_gateway")
|
||||||
|
|
||||||
|
[node name="TopLeft" type="StaticBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 4.5, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="TopLeft"]
|
||||||
|
shape = SubResource("BoxShape3D_gateway")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="TopLeft"]
|
||||||
|
mesh = SubResource("BoxMesh_gateway")
|
||||||
|
|
||||||
|
[node name="TopCenter" type="StaticBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.5, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="TopCenter"]
|
||||||
|
shape = SubResource("BoxShape3D_gateway")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="TopCenter"]
|
||||||
|
mesh = SubResource("BoxMesh_gateway")
|
||||||
|
|
||||||
|
[node name="TopRight" type="StaticBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 4.5, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="TopRight"]
|
||||||
|
shape = SubResource("BoxShape3D_gateway")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="TopRight"]
|
||||||
|
mesh = SubResource("BoxMesh_gateway")
|
||||||
|
|
||||||
|
[node name="Teleporter" parent="." instance=ExtResource("2_teleporter")]
|
||||||
|
transform = Transform3D(1.15, 0, 0, 0, 1.33, 0, 0, 0, 0.7, 0, 1.5, 0)
|
||||||
258
game/scenes/Levels/location_level.gd
Normal file
258
game/scenes/Levels/location_level.gd
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
const CHARACTER_API_URL := "https://pchar.ranaze.com/api/Characters"
|
||||||
|
|
||||||
|
@export var tile_size := 4.0
|
||||||
|
@export var block_height := 1.0
|
||||||
|
@export_range(1, 8, 1) var tile_radius := 3
|
||||||
|
@export var tracked_node_path: NodePath
|
||||||
|
@export var player_spawn_height := 2.0
|
||||||
|
@export var border_color: Color = Color(0.05, 0.05, 0.05, 1.0)
|
||||||
|
@export var border_height_bias := 0.005
|
||||||
|
@export var show_tile_labels := true
|
||||||
|
@export var tile_label_height := 0.01
|
||||||
|
@export var tile_label_color: Color = Color(1, 1, 1, 1)
|
||||||
|
|
||||||
|
@onready var _block: MeshInstance3D = $TerrainBlock
|
||||||
|
@onready var _player: RigidBody3D = $Player
|
||||||
|
@onready var _camera: Camera3D = $Player/Camera3D
|
||||||
|
|
||||||
|
var _center_coord := Vector2i.ZERO
|
||||||
|
var _tiles_root: Node3D
|
||||||
|
var _tracked_node: Node3D
|
||||||
|
var _tile_nodes: Dictionary = {}
|
||||||
|
var _camera_start_offset := Vector3(0.0, 6.0, 10.0)
|
||||||
|
var _border_material: StandardMaterial3D
|
||||||
|
var _known_locations: Dictionary = {}
|
||||||
|
var _locations_loaded := false
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_tiles_root = Node3D.new()
|
||||||
|
_tiles_root.name = "GeneratedTiles"
|
||||||
|
add_child(_tiles_root)
|
||||||
|
|
||||||
|
if _camera:
|
||||||
|
_camera_start_offset = _camera.position
|
||||||
|
|
||||||
|
_tracked_node = get_node_or_null(tracked_node_path) as Node3D
|
||||||
|
if _tracked_node == null:
|
||||||
|
_tracked_node = _player
|
||||||
|
|
||||||
|
var start_coord := SelectedCharacter.get_coord()
|
||||||
|
_center_coord = Vector2i(roundi(start_coord.x), roundi(start_coord.y))
|
||||||
|
|
||||||
|
_block.visible = false
|
||||||
|
await _load_existing_locations()
|
||||||
|
_ensure_selected_location_exists(_center_coord)
|
||||||
|
_rebuild_tiles(_center_coord)
|
||||||
|
_move_player_to_coord(_center_coord)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if not _locations_loaded:
|
||||||
|
return
|
||||||
|
var target_world_pos := _get_stream_position()
|
||||||
|
var target_coord := _world_to_coord(target_world_pos)
|
||||||
|
if target_coord == _center_coord:
|
||||||
|
return
|
||||||
|
_center_coord = target_coord
|
||||||
|
_rebuild_tiles(_center_coord)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_stream_position() -> Vector3:
|
||||||
|
if _tracked_node:
|
||||||
|
return _tracked_node.global_position
|
||||||
|
return _coord_to_world(_center_coord)
|
||||||
|
|
||||||
|
|
||||||
|
func _world_to_coord(world_pos: Vector3) -> Vector2i:
|
||||||
|
return Vector2i(
|
||||||
|
roundi(world_pos.x / tile_size),
|
||||||
|
roundi(world_pos.z / tile_size)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _coord_to_world(coord: Vector2i) -> Vector3:
|
||||||
|
return Vector3(coord.x * tile_size, block_height * 0.5, coord.y * tile_size)
|
||||||
|
|
||||||
|
|
||||||
|
func _move_player_to_coord(coord: Vector2i) -> void:
|
||||||
|
if _player == null:
|
||||||
|
return
|
||||||
|
_player.global_position = Vector3(coord.x * tile_size, player_spawn_height, coord.y * tile_size)
|
||||||
|
_player.linear_velocity = Vector3.ZERO
|
||||||
|
_player.angular_velocity = Vector3.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
func _rebuild_tiles(center: Vector2i) -> void:
|
||||||
|
var wanted_keys: Dictionary = {}
|
||||||
|
for x in range(center.x - tile_radius, center.x + tile_radius + 1):
|
||||||
|
for y in range(center.y - tile_radius, center.y + tile_radius + 1):
|
||||||
|
var coord := Vector2i(x, y)
|
||||||
|
if not _known_locations.has(coord):
|
||||||
|
continue
|
||||||
|
wanted_keys[coord] = true
|
||||||
|
if _tile_nodes.has(coord):
|
||||||
|
continue
|
||||||
|
_spawn_tile(coord, String(_known_locations[coord]))
|
||||||
|
|
||||||
|
for key in _tile_nodes.keys():
|
||||||
|
if wanted_keys.has(key):
|
||||||
|
continue
|
||||||
|
var tile_node := _tile_nodes[key] as Node3D
|
||||||
|
if tile_node:
|
||||||
|
tile_node.queue_free()
|
||||||
|
_tile_nodes.erase(key)
|
||||||
|
|
||||||
|
|
||||||
|
func _spawn_tile(coord: Vector2i, location_name: String) -> void:
|
||||||
|
var tile_root := Node3D.new()
|
||||||
|
tile_root.name = "Tile_%d_%d" % [coord.x, coord.y]
|
||||||
|
tile_root.position = _coord_to_world(coord)
|
||||||
|
_tiles_root.add_child(tile_root)
|
||||||
|
|
||||||
|
var tile_body := StaticBody3D.new()
|
||||||
|
tile_body.name = "TileBody"
|
||||||
|
tile_body.scale = Vector3(tile_size, block_height, tile_size)
|
||||||
|
tile_root.add_child(tile_body)
|
||||||
|
|
||||||
|
var collision_shape := CollisionShape3D.new()
|
||||||
|
collision_shape.name = "CollisionShape3D"
|
||||||
|
collision_shape.shape = BoxShape3D.new()
|
||||||
|
tile_body.add_child(collision_shape)
|
||||||
|
|
||||||
|
var tile := _block.duplicate() as MeshInstance3D
|
||||||
|
tile.name = "TileMesh"
|
||||||
|
tile.visible = true
|
||||||
|
tile_body.add_child(tile)
|
||||||
|
|
||||||
|
tile.add_child(_create_tile_border())
|
||||||
|
if show_tile_labels:
|
||||||
|
tile_root.add_child(_create_tile_label(location_name))
|
||||||
|
|
||||||
|
_tile_nodes[coord] = tile_root
|
||||||
|
|
||||||
|
|
||||||
|
func _create_tile_border() -> MeshInstance3D:
|
||||||
|
var top_y := 0.5 + border_height_bias
|
||||||
|
var corners := [
|
||||||
|
Vector3(-0.5, top_y, -0.5),
|
||||||
|
Vector3(0.5, top_y, -0.5),
|
||||||
|
Vector3(0.5, top_y, 0.5),
|
||||||
|
Vector3(-0.5, top_y, 0.5),
|
||||||
|
]
|
||||||
|
|
||||||
|
var border_mesh := ImmediateMesh.new()
|
||||||
|
border_mesh.surface_begin(Mesh.PRIMITIVE_LINES, _get_border_material())
|
||||||
|
for idx in range(corners.size()):
|
||||||
|
var current: Vector3 = corners[idx]
|
||||||
|
var next: Vector3 = corners[(idx + 1) % corners.size()]
|
||||||
|
border_mesh.surface_add_vertex(current)
|
||||||
|
border_mesh.surface_add_vertex(next)
|
||||||
|
border_mesh.surface_end()
|
||||||
|
|
||||||
|
var border := MeshInstance3D.new()
|
||||||
|
border.name = "TileBorder"
|
||||||
|
border.mesh = border_mesh
|
||||||
|
return border
|
||||||
|
|
||||||
|
|
||||||
|
func _get_border_material() -> StandardMaterial3D:
|
||||||
|
if _border_material:
|
||||||
|
return _border_material
|
||||||
|
|
||||||
|
_border_material = StandardMaterial3D.new()
|
||||||
|
_border_material.albedo_color = border_color
|
||||||
|
_border_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
_border_material.disable_receive_shadows = true
|
||||||
|
_border_material.no_depth_test = true
|
||||||
|
return _border_material
|
||||||
|
|
||||||
|
|
||||||
|
func _create_tile_label(location_name: String) -> Label3D:
|
||||||
|
var label := Label3D.new()
|
||||||
|
label.name = "LocationNameLabel"
|
||||||
|
label.text = location_name
|
||||||
|
label.position = Vector3(0.0, (block_height * 0.5) + border_height_bias + tile_label_height, 0.0)
|
||||||
|
label.rotation_degrees = Vector3(-90.0, 0.0, 0.0)
|
||||||
|
label.billboard = BaseMaterial3D.BILLBOARD_DISABLED
|
||||||
|
label.modulate = tile_label_color
|
||||||
|
label.pixel_size = 0.01
|
||||||
|
label.outline_size = 12
|
||||||
|
label.no_depth_test = false
|
||||||
|
return label
|
||||||
|
|
||||||
|
|
||||||
|
func _ensure_selected_location_exists(coord: Vector2i) -> void:
|
||||||
|
if _known_locations.has(coord):
|
||||||
|
return
|
||||||
|
_known_locations[coord] = _selected_location_name(coord)
|
||||||
|
|
||||||
|
|
||||||
|
func _selected_location_name(coord: Vector2i) -> String:
|
||||||
|
var selected_name := String(SelectedCharacter.character.get("locationName", "")).strip_edges()
|
||||||
|
if not selected_name.is_empty():
|
||||||
|
return selected_name
|
||||||
|
var character_name := String(SelectedCharacter.character.get("name", "")).strip_edges()
|
||||||
|
if not character_name.is_empty():
|
||||||
|
return "%s's Location" % character_name
|
||||||
|
return "Location %d,%d" % [coord.x, coord.y]
|
||||||
|
|
||||||
|
|
||||||
|
func _load_existing_locations() -> void:
|
||||||
|
_locations_loaded = false
|
||||||
|
_known_locations.clear()
|
||||||
|
|
||||||
|
var character_id := String(SelectedCharacter.character.get("id", SelectedCharacter.character.get("Id", ""))).strip_edges()
|
||||||
|
if character_id.is_empty():
|
||||||
|
push_warning("Selected character is missing an id; cannot load visible locations.")
|
||||||
|
_locations_loaded = true
|
||||||
|
return
|
||||||
|
|
||||||
|
var request := HTTPRequest.new()
|
||||||
|
add_child(request)
|
||||||
|
|
||||||
|
var headers := PackedStringArray()
|
||||||
|
if not AuthState.access_token.is_empty():
|
||||||
|
headers.append("Authorization: Bearer %s" % AuthState.access_token)
|
||||||
|
|
||||||
|
var err := request.request("%s/%s/visible-locations" % [CHARACTER_API_URL, character_id], headers, HTTPClient.METHOD_GET)
|
||||||
|
if err != OK:
|
||||||
|
push_warning("Failed to request visible locations: %s" % err)
|
||||||
|
request.queue_free()
|
||||||
|
_locations_loaded = true
|
||||||
|
return
|
||||||
|
|
||||||
|
var result: Array = await request.request_completed
|
||||||
|
request.queue_free()
|
||||||
|
|
||||||
|
var result_code: int = result[0]
|
||||||
|
var response_code: int = result[1]
|
||||||
|
var response_body: String = result[3].get_string_from_utf8()
|
||||||
|
if result_code != HTTPRequest.RESULT_SUCCESS or response_code < 200 or response_code >= 300:
|
||||||
|
push_warning("Failed to load visible locations (%s/%s): %s" % [result_code, response_code, response_body])
|
||||||
|
_locations_loaded = true
|
||||||
|
return
|
||||||
|
|
||||||
|
var parsed: Variant = JSON.parse_string(response_body)
|
||||||
|
if typeof(parsed) != TYPE_ARRAY:
|
||||||
|
push_warning("Visible locations response was not an array.")
|
||||||
|
_locations_loaded = true
|
||||||
|
return
|
||||||
|
|
||||||
|
for item in parsed:
|
||||||
|
if typeof(item) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var location := item as Dictionary
|
||||||
|
var coord_variant: Variant = location.get("coord", {})
|
||||||
|
if typeof(coord_variant) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var coord_dict := coord_variant as Dictionary
|
||||||
|
var coord := Vector2i(int(coord_dict.get("x", 0)), int(coord_dict.get("y", 0)))
|
||||||
|
var location_name := String(location.get("name", "")).strip_edges()
|
||||||
|
if location_name.is_empty():
|
||||||
|
location_name = "Location %d,%d" % [coord.x, coord.y]
|
||||||
|
_known_locations[coord] = location_name
|
||||||
|
|
||||||
|
_locations_loaded = true
|
||||||
1
game/scenes/Levels/location_level.gd.uid
Normal file
1
game/scenes/Levels/location_level.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://ctbyn1gws2ahj
|
||||||
32
game/scenes/Levels/location_level.tscn
Normal file
32
game/scenes/Levels/location_level.tscn
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
[gd_scene load_steps=8 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scenes/Levels/location_level.gd" id="1_level_script"]
|
||||||
|
[ext_resource type="PackedScene" path="res://scenes/Characters/location_player.tscn" id="2_player_scene"]
|
||||||
|
[ext_resource type="Material" path="res://assets/materials/kenney_prototype_block_dark.tres" id="3_block_mat"]
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_tile"]
|
||||||
|
material = ExtResource("3_block_mat")
|
||||||
|
size = Vector3(1, 1, 1)
|
||||||
|
|
||||||
|
[sub_resource type="Environment" id="Environment_location"]
|
||||||
|
background_mode = 1
|
||||||
|
background_color = Color(0.55, 0.72, 0.92, 1)
|
||||||
|
ambient_light_source = 2
|
||||||
|
ambient_light_color = Color(1, 1, 1, 1)
|
||||||
|
ambient_light_energy = 0.8
|
||||||
|
|
||||||
|
[node name="LocationLevel" type="Node3D"]
|
||||||
|
script = ExtResource("1_level_script")
|
||||||
|
tracked_node_path = NodePath("Player")
|
||||||
|
|
||||||
|
[node name="TerrainBlock" type="MeshInstance3D" parent="."]
|
||||||
|
mesh = SubResource("BoxMesh_tile")
|
||||||
|
|
||||||
|
[node name="Player" parent="." instance=ExtResource("2_player_scene")]
|
||||||
|
|
||||||
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.819152, 0.573576, 0, -0.573576, 0.819152, 0, 6, 0)
|
||||||
|
shadow_enabled = true
|
||||||
|
|
||||||
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||||
|
environment = SubResource("Environment_location")
|
||||||
@ -19,10 +19,10 @@ public class CharactersController : ControllerBase
|
|||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Authorize(Roles = "USER,SUPER")]
|
[Authorize(Roles = "USER,SUPER")]
|
||||||
public async Task<IActionResult> Create([FromBody] CreateCharacterRequest req)
|
public async Task<IActionResult> Create([FromBody] CreateCharacterRequest req)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(req.Name))
|
if (string.IsNullOrWhiteSpace(req.Name))
|
||||||
return BadRequest("Name required");
|
return BadRequest("Name required");
|
||||||
|
|
||||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
if (string.IsNullOrWhiteSpace(userId))
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
@ -33,6 +33,7 @@ public class CharactersController : ControllerBase
|
|||||||
OwnerUserId = userId,
|
OwnerUserId = userId,
|
||||||
Name = req.Name.Trim(),
|
Name = req.Name.Trim(),
|
||||||
Coord = new Coord { X = 0, Y = 0 },
|
Coord = new Coord { X = 0, Y = 0 },
|
||||||
|
VisionRadius = 3,
|
||||||
CreatedUtc = DateTime.UtcNow
|
CreatedUtc = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -42,19 +43,36 @@ public class CharactersController : ControllerBase
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Authorize(Roles = "USER,SUPER")]
|
[Authorize(Roles = "USER,SUPER")]
|
||||||
public async Task<IActionResult> ListMine()
|
public async Task<IActionResult> ListMine()
|
||||||
{
|
{
|
||||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
if (string.IsNullOrWhiteSpace(userId))
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
var characters = await _characters.GetForOwnerAsync(userId);
|
var characters = await _characters.GetForOwnerAsync(userId);
|
||||||
return Ok(characters);
|
return Ok(characters);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("{id}")]
|
[HttpGet("{id}/visible-locations")]
|
||||||
[Authorize(Roles = "USER,SUPER")]
|
[Authorize(Roles = "USER,SUPER")]
|
||||||
public async Task<IActionResult> Delete(string id)
|
public async Task<IActionResult> VisibleLocations(string id)
|
||||||
|
{
|
||||||
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
|
return Unauthorized();
|
||||||
|
|
||||||
|
var allowAnyOwner = User.IsInRole("SUPER");
|
||||||
|
var character = await _characters.GetForOwnerByIdAsync(id, userId, allowAnyOwner);
|
||||||
|
if (character is null)
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var locations = await _characters.GetVisibleLocationsAsync(character);
|
||||||
|
return Ok(locations);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
[Authorize(Roles = "USER,SUPER")]
|
||||||
|
public async Task<IActionResult> Delete(string id)
|
||||||
{
|
{
|
||||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
if (string.IsNullOrWhiteSpace(userId))
|
if (string.IsNullOrWhiteSpace(userId))
|
||||||
|
|||||||
@ -12,16 +12,32 @@ Inbound JSON documents
|
|||||||
```
|
```
|
||||||
|
|
||||||
Stored documents (MongoDB)
|
Stored documents (MongoDB)
|
||||||
- Character
|
- Character
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"id": "string (ObjectId)",
|
"id": "string (ObjectId)",
|
||||||
"ownerUserId": "string",
|
"ownerUserId": "string",
|
||||||
"name": "string",
|
"name": "string",
|
||||||
"coord": {
|
"coord": {
|
||||||
"x": "number",
|
"x": "number",
|
||||||
"y": "number"
|
"y": "number"
|
||||||
},
|
},
|
||||||
"createdUtc": "string (ISO-8601 datetime)"
|
"visionRadius": "number",
|
||||||
}
|
"createdUtc": "string (ISO-8601 datetime)"
|
||||||
```
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Outbound JSON documents
|
||||||
|
- VisibleLocation (`GET /api/characters/{id}/visible-locations`)
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "string (ObjectId)",
|
||||||
|
"name": "string",
|
||||||
|
"coord": {
|
||||||
|
"x": "number",
|
||||||
|
"y": "number"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|||||||
@ -15,5 +15,7 @@ public class Character
|
|||||||
|
|
||||||
public Coord Coord { get; set; } = new();
|
public Coord Coord { get; set; } = new();
|
||||||
|
|
||||||
|
public int VisionRadius { get; set; } = 3;
|
||||||
|
|
||||||
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
|
|||||||
17
microservices/CharacterApi/Models/VisibleLocation.cs
Normal file
17
microservices/CharacterApi/Models/VisibleLocation.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
using MongoDB.Bson;
|
||||||
|
using MongoDB.Bson.Serialization.Attributes;
|
||||||
|
|
||||||
|
namespace CharacterApi.Models;
|
||||||
|
|
||||||
|
public class VisibleLocation
|
||||||
|
{
|
||||||
|
[BsonId]
|
||||||
|
[BsonRepresentation(BsonType.ObjectId)]
|
||||||
|
public string? Id { get; set; }
|
||||||
|
|
||||||
|
[BsonElement("name")]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[BsonElement("coord")]
|
||||||
|
public Coord Coord { get; set; } = new();
|
||||||
|
}
|
||||||
@ -6,4 +6,5 @@ See `DOCUMENTS.md` for request payloads and stored document shapes.
|
|||||||
## Endpoints
|
## Endpoints
|
||||||
- `POST /api/characters` Create a character.
|
- `POST /api/characters` Create a character.
|
||||||
- `GET /api/characters` List characters for the current user.
|
- `GET /api/characters` List characters for the current user.
|
||||||
|
- `GET /api/characters/{id}/visible-locations` List locations visible to that owned character.
|
||||||
- `DELETE /api/characters/{id}` Delete a character owned by the current user.
|
- `DELETE /api/characters/{id}` Delete a character owned by the current user.
|
||||||
|
|||||||
@ -3,31 +3,65 @@ using MongoDB.Driver;
|
|||||||
|
|
||||||
namespace CharacterApi.Services;
|
namespace CharacterApi.Services;
|
||||||
|
|
||||||
public class CharacterStore
|
public class CharacterStore
|
||||||
{
|
{
|
||||||
private readonly IMongoCollection<Character> _col;
|
private readonly IMongoCollection<Character> _col;
|
||||||
|
private readonly IMongoCollection<VisibleLocation> _locations;
|
||||||
public CharacterStore(IConfiguration cfg)
|
|
||||||
{
|
public CharacterStore(IConfiguration cfg)
|
||||||
var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017";
|
{
|
||||||
var dbName = cfg["MongoDB:DatabaseName"] ?? "GameDb";
|
var cs = cfg["MongoDB:ConnectionString"] ?? "mongodb://127.0.0.1:27017";
|
||||||
var client = new MongoClient(cs);
|
var dbName = cfg["MongoDB:DatabaseName"] ?? "GameDb";
|
||||||
var db = client.GetDatabase(dbName);
|
var client = new MongoClient(cs);
|
||||||
_col = db.GetCollection<Character>("Characters");
|
var db = client.GetDatabase(dbName);
|
||||||
|
_col = db.GetCollection<Character>("Characters");
|
||||||
var ownerIndex = Builders<Character>.IndexKeys.Ascending(c => c.OwnerUserId);
|
_locations = db.GetCollection<VisibleLocation>("Locations");
|
||||||
_col.Indexes.CreateOne(new CreateIndexModel<Character>(ownerIndex));
|
|
||||||
}
|
var ownerIndex = Builders<Character>.IndexKeys.Ascending(c => c.OwnerUserId);
|
||||||
|
_col.Indexes.CreateOne(new CreateIndexModel<Character>(ownerIndex));
|
||||||
|
}
|
||||||
|
|
||||||
public Task CreateAsync(Character character) => _col.InsertOneAsync(character);
|
public Task CreateAsync(Character character) => _col.InsertOneAsync(character);
|
||||||
|
|
||||||
public Task<List<Character>> GetForOwnerAsync(string ownerUserId) =>
|
public Task<List<Character>> GetForOwnerAsync(string ownerUserId) =>
|
||||||
_col.Find(c => c.OwnerUserId == ownerUserId).ToListAsync();
|
_col.Find(c => c.OwnerUserId == ownerUserId).ToListAsync();
|
||||||
|
|
||||||
public async Task<bool> DeleteForOwnerAsync(string id, string ownerUserId, bool allowAnyOwner)
|
public async Task<Character?> GetForOwnerByIdAsync(string id, string ownerUserId, bool allowAnyOwner)
|
||||||
{
|
{
|
||||||
var filter = Builders<Character>.Filter.Eq(c => c.Id, id);
|
var filter = Builders<Character>.Filter.Eq(c => c.Id, id);
|
||||||
if (!allowAnyOwner)
|
if (!allowAnyOwner)
|
||||||
|
{
|
||||||
|
filter = Builders<Character>.Filter.And(
|
||||||
|
filter,
|
||||||
|
Builders<Character>.Filter.Eq(c => c.OwnerUserId, ownerUserId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _col.Find(filter).FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<List<VisibleLocation>> GetVisibleLocationsAsync(Character character)
|
||||||
|
{
|
||||||
|
var radius = character.VisionRadius > 0 ? character.VisionRadius : 3;
|
||||||
|
var minX = character.Coord.X - radius;
|
||||||
|
var maxX = character.Coord.X + radius;
|
||||||
|
var minY = character.Coord.Y - radius;
|
||||||
|
var maxY = character.Coord.Y + radius;
|
||||||
|
|
||||||
|
var filter = Builders<VisibleLocation>.Filter.And(
|
||||||
|
Builders<VisibleLocation>.Filter.Gte(l => l.Coord.X, minX),
|
||||||
|
Builders<VisibleLocation>.Filter.Lte(l => l.Coord.X, maxX),
|
||||||
|
Builders<VisibleLocation>.Filter.Gte(l => l.Coord.Y, minY),
|
||||||
|
Builders<VisibleLocation>.Filter.Lte(l => l.Coord.Y, maxY)
|
||||||
|
);
|
||||||
|
|
||||||
|
return _locations.Find(filter).ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteForOwnerAsync(string id, string ownerUserId, bool allowAnyOwner)
|
||||||
|
{
|
||||||
|
var filter = Builders<Character>.Filter.Eq(c => c.Id, id);
|
||||||
|
if (!allowAnyOwner)
|
||||||
{
|
{
|
||||||
filter = Builders<Character>.Filter.And(
|
filter = Builders<Character>.Filter.And(
|
||||||
filter,
|
filter,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user