Recommiting glbs for LFS
Some checks failed
Deploy Promiscuity Auth API / deploy (push) Successful in 1m59s
Deploy Promiscuity Character API / deploy (push) Successful in 1m16s
Deploy Promiscuity Inventory API / deploy (push) Has been cancelled
Deploy Promiscuity Locations API / deploy (push) Has been cancelled
Deploy Promiscuity Mail API / deploy (push) Has been cancelled
Deploy Promiscuity World API / deploy (push) Has been cancelled
Deploy Promiscuity Crafting API / deploy (push) Has been cancelled
k8s smoke test / test (push) Has been cancelled
4
.gitattributes
vendored
@ -1,6 +1,5 @@
|
|||||||
# Set the default behavior, in case people don't have core.autocrlf set.
|
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||||
* text=auto
|
* text=auto
|
||||||
|
|
||||||
# Explicitly declare text files you want to always be normalized and converted
|
# Explicitly declare text files you want to always be normalized and converted
|
||||||
# to native line endings on checkout.
|
# to native line endings on checkout.
|
||||||
*.cs text diff=csharp
|
*.cs text diff=csharp
|
||||||
@ -14,11 +13,9 @@
|
|||||||
*.yaml text
|
*.yaml text
|
||||||
*.csproj text
|
*.csproj text
|
||||||
*.sln text
|
*.sln text
|
||||||
|
|
||||||
# Declare files that will always have CRLF line endings on checkout.
|
# Declare files that will always have CRLF line endings on checkout.
|
||||||
*.bat text eol=crlf
|
*.bat text eol=crlf
|
||||||
*.ps1 text eol=crlf
|
*.ps1 text eol=crlf
|
||||||
|
|
||||||
# Denote all files that are truly binary and should not be modified.
|
# Denote all files that are truly binary and should not be modified.
|
||||||
*.png binary
|
*.png binary
|
||||||
*.jpg binary
|
*.jpg binary
|
||||||
@ -40,3 +37,4 @@
|
|||||||
*.7z binary
|
*.7z binary
|
||||||
*.import binary
|
*.import binary
|
||||||
*.uid binary
|
*.uid binary
|
||||||
|
*.glb filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|||||||
@ -18,5 +18,7 @@
|
|||||||
- Sprint: Shift
|
- Sprint: Shift
|
||||||
- Interact (enter/exit car): E
|
- Interact (enter/exit car): E
|
||||||
- Flashlight: F
|
- Flashlight: F
|
||||||
|
- Radial command menu: Q
|
||||||
|
- Fireball: Shift + B
|
||||||
- Phone: Tab
|
- Phone: Tab
|
||||||
- Pause menu: Esc
|
- Pause menu: Esc
|
||||||
|
|||||||
BIN
art/character/Ashling Swarmer/ashling-swarmer-reference.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
BIN
art/character/Ember/ember-reference-tpose-only.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
art/character/Ember/ember-reference.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
art/character/Ember/ember-t-pose-ref.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
art/character/Ember/ember.glb
(Stored with Git LFS)
Normal file
BIN
game/addons/godot_mcp/.DS_Store
vendored
Normal file
2582
game/addons/godot_mcp/command_handler.gd
Normal file
1
game/addons/godot_mcp/command_handler.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://mcipi7y664aq
|
||||||
7
game/addons/godot_mcp/plugin.cfg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[plugin]
|
||||||
|
|
||||||
|
name="Godot MCP"
|
||||||
|
description="A plugin to enable communication between Godot Editor and Model Context Protocol (MCP) clients."
|
||||||
|
author="Your Name"
|
||||||
|
version="0.1.0"
|
||||||
|
script="plugin.gd"
|
||||||
140
game/addons/godot_mcp/plugin.gd
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
# Structure for addons/godot_mcp/plugin.gd
|
||||||
|
@tool
|
||||||
|
extends EditorPlugin
|
||||||
|
|
||||||
|
const SERVER_PORT = 6400
|
||||||
|
var server: TCPServer = null
|
||||||
|
var active_connections = []
|
||||||
|
var command_handler
|
||||||
|
|
||||||
|
func _enter_tree():
|
||||||
|
# Initialize the plugin
|
||||||
|
print("Godot MCP Plugin activated")
|
||||||
|
|
||||||
|
# Create command handler
|
||||||
|
command_handler = preload("res://addons/godot_mcp/command_handler.gd").new()
|
||||||
|
command_handler.set_editor_plugin(self)
|
||||||
|
|
||||||
|
# Start the TCP server
|
||||||
|
server = TCPServer.new()
|
||||||
|
var error = server.listen(SERVER_PORT)
|
||||||
|
if error != OK:
|
||||||
|
push_error("Failed to start Godot MCP Server on port %d: %s" % [SERVER_PORT, error])
|
||||||
|
return
|
||||||
|
|
||||||
|
print("Godot MCP Server listening on port %d" % SERVER_PORT)
|
||||||
|
|
||||||
|
# Add UI
|
||||||
|
add_control_to_bottom_panel(
|
||||||
|
preload("res://addons/godot_mcp/ui/mcp_panel.tscn").instantiate(),
|
||||||
|
"MCP"
|
||||||
|
)
|
||||||
|
|
||||||
|
func _exit_tree():
|
||||||
|
# Clean up the plugin when disabled
|
||||||
|
if server:
|
||||||
|
server.stop()
|
||||||
|
server = null
|
||||||
|
|
||||||
|
for connection in active_connections:
|
||||||
|
if connection.get_status() == StreamPeerTCP.STATUS_CONNECTED:
|
||||||
|
connection.disconnect_from_host()
|
||||||
|
|
||||||
|
active_connections.clear()
|
||||||
|
|
||||||
|
# Remove UI
|
||||||
|
remove_control_from_bottom_panel(get_editor_interface().get_base_control().get_node("MCPPanel"))
|
||||||
|
print("Godot MCP Plugin deactivated")
|
||||||
|
|
||||||
|
func _process(delta):
|
||||||
|
# Check for new connections
|
||||||
|
if server and server.is_connection_available():
|
||||||
|
var connection = server.take_connection()
|
||||||
|
if connection:
|
||||||
|
active_connections.append(connection)
|
||||||
|
print("New MCP connection established")
|
||||||
|
|
||||||
|
# Process existing connections
|
||||||
|
var i = 0
|
||||||
|
while i < active_connections.size():
|
||||||
|
var connection = active_connections[i]
|
||||||
|
|
||||||
|
# Check connection status
|
||||||
|
if connection.get_status() != StreamPeerTCP.STATUS_CONNECTED:
|
||||||
|
active_connections.remove_at(i)
|
||||||
|
print("MCP connection closed")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check for incoming messages
|
||||||
|
if connection.get_available_bytes() > 0:
|
||||||
|
var data = _read_message(connection)
|
||||||
|
if data.size() > 0:
|
||||||
|
# Process the command
|
||||||
|
var response = _process_command(data)
|
||||||
|
|
||||||
|
# Send the response
|
||||||
|
_send_message(connection, response)
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
func _read_message(connection):
|
||||||
|
# Read data from the connection
|
||||||
|
var data = PackedByteArray()
|
||||||
|
var bytes_available = connection.get_available_bytes()
|
||||||
|
|
||||||
|
if bytes_available > 0:
|
||||||
|
data = connection.get_data(bytes_available)[1]
|
||||||
|
|
||||||
|
# Attempt to parse as JSON
|
||||||
|
var json_string = data.get_string_from_utf8()
|
||||||
|
var json = JSON.new()
|
||||||
|
var error = json.parse(json_string)
|
||||||
|
|
||||||
|
if error == OK:
|
||||||
|
return json.get_data()
|
||||||
|
else:
|
||||||
|
print("Failed to parse JSON: ", json.get_error_message())
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
func _send_message(connection, data):
|
||||||
|
# Convert to JSON and send
|
||||||
|
var json_string = JSON.stringify(data)
|
||||||
|
connection.put_data(json_string.to_utf8_buffer())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _process_command(data):
|
||||||
|
# Process the command and return a response
|
||||||
|
if data == null or typeof(data) != TYPE_DICTIONARY:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": "Invalid command format. Expected a dictionary."
|
||||||
|
}
|
||||||
|
|
||||||
|
if not data.has("type") or not data.has("params"):
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": "Invalid command format. Expected 'type' and 'params' fields."
|
||||||
|
}
|
||||||
|
|
||||||
|
var command_type = data["type"]
|
||||||
|
var params = data["params"]
|
||||||
|
|
||||||
|
if command_type == "ping":
|
||||||
|
return {"status": "success", "result": {"message": "pong"}}
|
||||||
|
|
||||||
|
# Forward to command handler
|
||||||
|
var result = command_handler.handle_command(command_type, params)
|
||||||
|
|
||||||
|
# Check if result is valid
|
||||||
|
if result == null:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": "Command handler returned null result"
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.has("error"):
|
||||||
|
return {"status": "error", "error": result.error}
|
||||||
|
else:
|
||||||
|
return {"status": "success", "result": result}
|
||||||
1
game/addons/godot_mcp/plugin.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://ddfhkiyge45tn
|
||||||
77
game/addons/godot_mcp/ui/mcp_panel.gd
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# addons/godot_mcp/ui/mcp_panel.gd
|
||||||
|
@tool
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
var status_label: Label
|
||||||
|
var port_field: SpinBox
|
||||||
|
var start_button: Button
|
||||||
|
var stop_button: Button
|
||||||
|
var log_display: TextEdit
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
# Set up references to UI elements
|
||||||
|
status_label = $VBoxContainer/StatusPanel/StatusLabel
|
||||||
|
port_field = $VBoxContainer/ConfigPanel/PortField
|
||||||
|
start_button = $VBoxContainer/ButtonPanel/StartButton
|
||||||
|
stop_button = $VBoxContainer/ButtonPanel/StopButton
|
||||||
|
log_display = $VBoxContainer/LogPanel/LogDisplay
|
||||||
|
|
||||||
|
# Initialize UI
|
||||||
|
port_field.value = 6400 # Default port
|
||||||
|
start_button.disabled = false
|
||||||
|
stop_button.disabled = true
|
||||||
|
|
||||||
|
# Connect signals
|
||||||
|
start_button.pressed.connect(_on_start_button_pressed)
|
||||||
|
stop_button.pressed.connect(_on_stop_button_pressed)
|
||||||
|
|
||||||
|
# Set initial status
|
||||||
|
update_status("Not running")
|
||||||
|
add_log_message("Godot MCP Plugin initialized")
|
||||||
|
|
||||||
|
func update_status(status_text: String, is_error: bool = false):
|
||||||
|
status_label.text = "Status: " + status_text
|
||||||
|
if is_error:
|
||||||
|
status_label.add_theme_color_override("font_color", Color(1, 0.3, 0.3))
|
||||||
|
else:
|
||||||
|
status_label.remove_theme_color_override("font_color")
|
||||||
|
|
||||||
|
func add_log_message(message: String):
|
||||||
|
var timestamp = Time.get_datetime_string_from_system()
|
||||||
|
log_display.text += "[" + timestamp + "] " + message + "\n"
|
||||||
|
log_display.scroll_vertical = log_display.get_line_count()
|
||||||
|
|
||||||
|
func _on_start_button_pressed():
|
||||||
|
# This function will be called from the plugin.gd script
|
||||||
|
# when the server is actually started
|
||||||
|
update_status("Running on port " + str(port_field.value))
|
||||||
|
start_button.disabled = true
|
||||||
|
stop_button.disabled = false
|
||||||
|
add_log_message("MCP Server started on port " + str(port_field.value))
|
||||||
|
|
||||||
|
func _on_stop_button_pressed():
|
||||||
|
# This function will be called from the plugin.gd script
|
||||||
|
# when the server is actually stopped
|
||||||
|
update_status("Stopped")
|
||||||
|
start_button.disabled = false
|
||||||
|
stop_button.disabled = true
|
||||||
|
add_log_message("MCP Server stopped")
|
||||||
|
|
||||||
|
# Function to be called from plugin.gd when a client connects
|
||||||
|
func on_client_connected():
|
||||||
|
add_log_message("Client connected")
|
||||||
|
update_status("Client connected")
|
||||||
|
|
||||||
|
# Function to be called from plugin.gd when a client disconnects
|
||||||
|
func on_client_disconnected():
|
||||||
|
add_log_message("Client disconnected")
|
||||||
|
update_status("Running (no clients)")
|
||||||
|
|
||||||
|
# Function to be called from plugin.gd when a command is received
|
||||||
|
func on_command_received(command_type, params):
|
||||||
|
add_log_message("Command received: " + command_type)
|
||||||
|
|
||||||
|
# Function to be called from plugin.gd when a response is sent
|
||||||
|
func on_response_sent(command_type, success):
|
||||||
|
var status = "Success" if success else "Failed"
|
||||||
|
add_log_message("Response sent for " + command_type + ": " + status)
|
||||||
1
game/addons/godot_mcp/ui/mcp_panel.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://bqabyd7ce60u0
|
||||||
69
game/addons/godot_mcp/ui/mcp_panel.tscn
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://dxvt86ck6b2a4"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://addons/godot_mcp/ui/mcp_panel.gd" id="1_4g23r"]
|
||||||
|
|
||||||
|
[node name="MCPPanel" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_4g23r")
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="StatusPanel" type="PanelContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="StatusLabel" type="Label" parent="VBoxContainer/StatusPanel"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Status: Not running"
|
||||||
|
|
||||||
|
[node name="ConfigPanel" type="PanelContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/ConfigPanel"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="PortLabel" type="Label" parent="VBoxContainer/ConfigPanel/HBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Port:"
|
||||||
|
|
||||||
|
[node name="PortField" type="SpinBox" parent="VBoxContainer/ConfigPanel"]
|
||||||
|
layout_mode = 2
|
||||||
|
min_value = 1024.0
|
||||||
|
max_value = 65535.0
|
||||||
|
value = 6400.0
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="ButtonPanel" type="PanelContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/ButtonPanel"]
|
||||||
|
layout_mode = 2
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="StartButton" type="Button" parent="VBoxContainer/ButtonPanel"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Start Server"
|
||||||
|
|
||||||
|
[node name="StopButton" type="Button" parent="VBoxContainer/ButtonPanel"]
|
||||||
|
layout_mode = 2
|
||||||
|
disabled = true
|
||||||
|
text = "Stop Server"
|
||||||
|
|
||||||
|
[node name="LogPanel" type="PanelContainer" parent="VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="LogDisplay" type="TextEdit" parent="VBoxContainer/LogPanel"]
|
||||||
|
layout_mode = 2
|
||||||
|
editable = false
|
||||||
|
wrap_mode = 1
|
||||||
BIN
game/assets/models/AshlingSwarmer.glb
(Stored with Git LFS)
Normal file
42
game/assets/models/AshlingSwarmer.glb.import
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://bi13nqi11oqhx"
|
||||||
|
path="res://.godot/imported/AshlingSwarmer.glb-69896cc28ddf5352ad63aa98f43893d5.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/models/AshlingSwarmer.glb"
|
||||||
|
dest_files=["res://.godot/imported/AshlingSwarmer.glb-69896cc28ddf5352ad63aa98f43893d5.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
BIN
game/assets/models/AshlingSwarmer.pre_anim.glb
(Stored with Git LFS)
Normal file
42
game/assets/models/AshlingSwarmer.pre_anim.glb.import
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://db8yuf2l8eavk"
|
||||||
|
path="res://.godot/imported/AshlingSwarmer.pre_anim.glb-81e6d03699494e78a323ca2a801ae146.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/models/AshlingSwarmer.pre_anim.glb"
|
||||||
|
dest_files=["res://.godot/imported/AshlingSwarmer.pre_anim.glb-81e6d03699494e78a323ca2a801ae146.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
BIN
game/assets/models/AshlingSwarmer.pre_rig.glb
(Stored with Git LFS)
Normal file
42
game/assets/models/AshlingSwarmer.pre_rig.glb.import
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://b3jmkpa8mjqo3"
|
||||||
|
path="res://.godot/imported/AshlingSwarmer.pre_rig.glb-f99517ddbdeb482c37550a37bcb3352f.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/models/AshlingSwarmer.pre_rig.glb"
|
||||||
|
dest_files=["res://.godot/imported/AshlingSwarmer.pre_rig.glb-f99517ddbdeb482c37550a37bcb3352f.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
BIN
game/assets/models/AshlingSwarmer.previous.glb
(Stored with Git LFS)
Normal file
42
game/assets/models/AshlingSwarmer.previous.glb.import
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="scene"
|
||||||
|
importer_version=1
|
||||||
|
type="PackedScene"
|
||||||
|
uid="uid://gd2yk5hi4u30"
|
||||||
|
path="res://.godot/imported/AshlingSwarmer.previous.glb-13f621ed6557d1df82d0fc05a90344cf.scn"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/models/AshlingSwarmer.previous.glb"
|
||||||
|
dest_files=["res://.godot/imported/AshlingSwarmer.previous.glb-13f621ed6557d1df82d0fc05a90344cf.scn"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
nodes/root_type=""
|
||||||
|
nodes/root_name=""
|
||||||
|
nodes/root_script=null
|
||||||
|
nodes/apply_root_scale=true
|
||||||
|
nodes/root_scale=1.0
|
||||||
|
nodes/import_as_skeleton_bones=false
|
||||||
|
nodes/use_name_suffixes=true
|
||||||
|
nodes/use_node_type_suffixes=true
|
||||||
|
meshes/ensure_tangents=true
|
||||||
|
meshes/generate_lods=true
|
||||||
|
meshes/create_shadow_meshes=true
|
||||||
|
meshes/light_baking=1
|
||||||
|
meshes/lightmap_texel_size=0.2
|
||||||
|
meshes/force_disable_compression=false
|
||||||
|
skins/use_named_skins=true
|
||||||
|
animation/import=true
|
||||||
|
animation/fps=30
|
||||||
|
animation/trimming=false
|
||||||
|
animation/remove_immutable_tracks=true
|
||||||
|
animation/import_rest_as_RESET=false
|
||||||
|
import_script/path=""
|
||||||
|
materials/extract=0
|
||||||
|
materials/extract_format=0
|
||||||
|
materials/extract_path=""
|
||||||
|
_subresources={}
|
||||||
|
gltf/naming_version=2
|
||||||
|
gltf/embedded_image_handling=1
|
||||||
BIN
game/assets/textures/ashling_swarmer/ashling_swarmer_palette.png
Normal file
|
After Width: | Height: | Size: 399 B |
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://bpcj2xno5aykg"
|
||||||
|
path="res://.godot/imported/ashling_swarmer_palette.png-c771be7c592755dce8c857fa3b76a603.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/textures/ashling_swarmer/ashling_swarmer_palette.png"
|
||||||
|
dest_files=["res://.godot/imported/ashling_swarmer_palette.png-c771be7c592755dce8c857fa3b76a603.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@ -56,6 +56,7 @@ CharacterService="*res://scenes/UI/character_service.gd"
|
|||||||
SelectedCharacter="*res://scenes/UI/selected_character.gd"
|
SelectedCharacter="*res://scenes/UI/selected_character.gd"
|
||||||
DialogSystem="*res://scenes/UI/dialog_system.gd"
|
DialogSystem="*res://scenes/UI/dialog_system.gd"
|
||||||
QuestManager="*res://scenes/Quests/quest_manager.gd"
|
QuestManager="*res://scenes/Quests/quest_manager.gd"
|
||||||
|
TeleportState="*res://scenes/Interaction/teleport_state.gd"
|
||||||
SimpleGrass="*res://addons/simplegrasstextured/singleton.tscn"
|
SimpleGrass="*res://addons/simplegrasstextured/singleton.tscn"
|
||||||
|
|
||||||
[dotnet]
|
[dotnet]
|
||||||
@ -64,7 +65,7 @@ project/assembly_name="Promiscuity"
|
|||||||
|
|
||||||
[editor_plugins]
|
[editor_plugins]
|
||||||
|
|
||||||
enabled=PackedStringArray("res://addons/simplegrasstextured/plugin.cfg")
|
enabled=PackedStringArray("res://addons/godot_mcp/plugin.cfg", "res://addons/simplegrasstextured/plugin.cfg")
|
||||||
|
|
||||||
[input]
|
[input]
|
||||||
|
|
||||||
|
|||||||
110
game/scenes/Characters/ashling_swarmer.gd
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
@export var model_path: NodePath = NodePath("AshlingSwarmerModel")
|
||||||
|
@export var default_animation: StringName = &"Idle"
|
||||||
|
@export var autoplay: bool = true
|
||||||
|
|
||||||
|
var _animation_player: AnimationPlayer
|
||||||
|
var _model_node: Node3D
|
||||||
|
var _current_animation: StringName = &""
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
call_deferred("_setup_animation")
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_animation() -> void:
|
||||||
|
_model_node = get_node_or_null(model_path) as Node3D
|
||||||
|
_animation_player = _find_animation_player(_model_node)
|
||||||
|
if _animation_player == null:
|
||||||
|
if autoplay:
|
||||||
|
push_warning("AshlingSwarmer: no AnimationPlayer found under %s." % model_path)
|
||||||
|
return
|
||||||
|
if autoplay:
|
||||||
|
play_animation(default_animation)
|
||||||
|
|
||||||
|
|
||||||
|
func play_animation(animation_name: StringName = &"") -> void:
|
||||||
|
if _animation_player == null:
|
||||||
|
_animation_player = _find_animation_player(get_node_or_null(model_path))
|
||||||
|
if _animation_player == null:
|
||||||
|
return
|
||||||
|
var requested_animation := animation_name if animation_name != StringName() else default_animation
|
||||||
|
var resolved_animation := _resolve_animation_name(requested_animation)
|
||||||
|
if resolved_animation == StringName():
|
||||||
|
push_warning("AshlingSwarmer: animation '%s' not found. Available animations: %s" % [requested_animation, _animation_player.get_animation_list()])
|
||||||
|
return
|
||||||
|
var animation := _animation_player.get_animation(resolved_animation)
|
||||||
|
if animation != null:
|
||||||
|
animation.loop_mode = Animation.LOOP_LINEAR
|
||||||
|
_animation_player.play(resolved_animation)
|
||||||
|
_current_animation = _classify_animation(resolved_animation)
|
||||||
|
|
||||||
|
|
||||||
|
func play_idle() -> void:
|
||||||
|
play_animation(&"Idle")
|
||||||
|
|
||||||
|
|
||||||
|
func play_run() -> void:
|
||||||
|
play_animation(&"Run")
|
||||||
|
|
||||||
|
|
||||||
|
func play_attack() -> void:
|
||||||
|
play_animation(&"Attack_Leap")
|
||||||
|
|
||||||
|
|
||||||
|
func play_hit() -> void:
|
||||||
|
play_animation(&"Hit")
|
||||||
|
|
||||||
|
|
||||||
|
func play_death() -> void:
|
||||||
|
play_animation(&"Death_Explode")
|
||||||
|
|
||||||
|
|
||||||
|
func _resolve_animation_name(animation_name: StringName) -> StringName:
|
||||||
|
if _animation_player.has_animation(animation_name):
|
||||||
|
return animation_name
|
||||||
|
var desired := String(animation_name).to_lower()
|
||||||
|
for candidate in _animation_player.get_animation_list():
|
||||||
|
var candidate_text := String(candidate)
|
||||||
|
if candidate_text.to_lower() == desired:
|
||||||
|
return StringName(candidate)
|
||||||
|
for candidate in _animation_player.get_animation_list():
|
||||||
|
var candidate_text := String(candidate)
|
||||||
|
if candidate_text.to_lower().contains(desired):
|
||||||
|
return StringName(candidate)
|
||||||
|
for candidate in _animation_player.get_animation_list():
|
||||||
|
var candidate_text := String(candidate)
|
||||||
|
if candidate_text.to_lower().contains("idle"):
|
||||||
|
return StringName(candidate)
|
||||||
|
var animations := _animation_player.get_animation_list()
|
||||||
|
if animations.size() > 0:
|
||||||
|
return StringName(animations[0])
|
||||||
|
return StringName()
|
||||||
|
|
||||||
|
|
||||||
|
func _classify_animation(animation_name: StringName) -> StringName:
|
||||||
|
var text := String(animation_name).to_lower()
|
||||||
|
if text.contains("run"):
|
||||||
|
return &"Run"
|
||||||
|
if text.contains("idle"):
|
||||||
|
return &"Idle"
|
||||||
|
if text.contains("attack"):
|
||||||
|
return &"Attack_Leap"
|
||||||
|
if text.contains("hit"):
|
||||||
|
return &"Hit"
|
||||||
|
if text.contains("death"):
|
||||||
|
return &"Death_Explode"
|
||||||
|
return animation_name
|
||||||
|
|
||||||
|
|
||||||
|
func _find_animation_player(root: Node) -> AnimationPlayer:
|
||||||
|
if root == null:
|
||||||
|
return null
|
||||||
|
if root is AnimationPlayer:
|
||||||
|
return root
|
||||||
|
for child in root.get_children():
|
||||||
|
var found := _find_animation_player(child)
|
||||||
|
if found != null:
|
||||||
|
return found
|
||||||
|
return null
|
||||||
1
game/scenes/Characters/ashling_swarmer.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://dptxcarifu7e1
|
||||||
36
game/scenes/Characters/ashling_swarmer.tscn
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
[gd_scene load_steps=5 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scenes/Characters/ashling_swarmer.gd" id="1_script"]
|
||||||
|
[ext_resource type="PackedScene" path="res://assets/models/AshlingSwarmer.glb" id="2_model"]
|
||||||
|
[ext_resource type="Script" path="res://scenes/Interaction/dialog_trigger_area.gd" id="3_dialog"]
|
||||||
|
|
||||||
|
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_body"]
|
||||||
|
radius = 0.45
|
||||||
|
height = 1.6
|
||||||
|
|
||||||
|
[sub_resource type="SphereShape3D" id="SphereShape3D_interact"]
|
||||||
|
radius = 1.8
|
||||||
|
|
||||||
|
[node name="AshlingSwarmer" type="Node3D"]
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
autoplay = false
|
||||||
|
|
||||||
|
[node name="Body" type="StaticBody3D" parent="."]
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Body"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
|
||||||
|
shape = SubResource("CapsuleShape3D_body")
|
||||||
|
|
||||||
|
[node name="AshlingSwarmerModel" parent="." instance=ExtResource("2_model")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
|
||||||
|
|
||||||
|
[node name="InteractArea" type="Area3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
|
||||||
|
script = ExtResource("3_dialog")
|
||||||
|
collision_layer = 2
|
||||||
|
collision_mask = 1
|
||||||
|
prompt_text = "Press E to inspect"
|
||||||
|
dialog_text = "Ashling Swarmer: A small ember-born creature watches you."
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="InteractArea"]
|
||||||
|
shape = SubResource("SphereShape3D_interact")
|
||||||
@ -21,8 +21,10 @@ func _toggle_menu():
|
|||||||
_animate_menu(false)
|
_animate_menu(false)
|
||||||
|
|
||||||
func _arrange_buttons():
|
func _arrange_buttons():
|
||||||
var buttons = get_children()
|
var buttons = get_children().filter(func(child): return child is Control)
|
||||||
var count = buttons.size()
|
var count = buttons.size()
|
||||||
|
if count == 0:
|
||||||
|
return
|
||||||
|
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
# Calculate the angle for each button in radians
|
# Calculate the angle for each button in radians
|
||||||
@ -36,7 +38,7 @@ func _arrange_buttons():
|
|||||||
|
|
||||||
func _animate_menu(opening: bool):
|
func _animate_menu(opening: bool):
|
||||||
var tween = create_tween().set_parallel(true)
|
var tween = create_tween().set_parallel(true)
|
||||||
for button in get_children():
|
for button in get_children().filter(func(child): return child is Control):
|
||||||
var final_scale = Vector2.ONE if opening else Vector2.ZERO
|
var final_scale = Vector2.ONE if opening else Vector2.ZERO
|
||||||
tween.tween_property(button, "scale", final_scale, transition_speed).from(Vector2.ZERO if opening else Vector2.ONE)
|
tween.tween_property(button, "scale", final_scale, transition_speed).from(Vector2.ZERO if opening else Vector2.ONE)
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,21 @@ extends Node3D
|
|||||||
one_shot = value
|
one_shot = value
|
||||||
_sync_teleporter()
|
_sync_teleporter()
|
||||||
|
|
||||||
|
@export var target_spawn_name: StringName = &"":
|
||||||
|
set(value):
|
||||||
|
target_spawn_name = value
|
||||||
|
_sync_teleporter()
|
||||||
|
|
||||||
|
@export var quest_event_name: StringName = &"":
|
||||||
|
set(value):
|
||||||
|
quest_event_name = value
|
||||||
|
_sync_teleporter()
|
||||||
|
|
||||||
|
@export var quest_id_filter: String = "":
|
||||||
|
set(value):
|
||||||
|
quest_id_filter = value
|
||||||
|
_sync_teleporter()
|
||||||
|
|
||||||
@onready var teleporter: Area3D = $Teleporter
|
@onready var teleporter: Area3D = $Teleporter
|
||||||
|
|
||||||
|
|
||||||
@ -29,3 +44,6 @@ func _sync_teleporter() -> void:
|
|||||||
teleporter.set("target_scene_path", target_scene_path)
|
teleporter.set("target_scene_path", target_scene_path)
|
||||||
teleporter.set("target_group", target_group)
|
teleporter.set("target_group", target_group)
|
||||||
teleporter.set("one_shot", one_shot)
|
teleporter.set("one_shot", one_shot)
|
||||||
|
teleporter.set("target_spawn_name", target_spawn_name)
|
||||||
|
teleporter.set("quest_event_name", quest_event_name)
|
||||||
|
teleporter.set("quest_id_filter", quest_id_filter)
|
||||||
|
|||||||
@ -3,6 +3,9 @@ extends Area3D
|
|||||||
@export_file("*.tscn") var target_scene_path := "res://scenes/Levels/transportation_level.tscn"
|
@export_file("*.tscn") var target_scene_path := "res://scenes/Levels/transportation_level.tscn"
|
||||||
@export var target_group: StringName = &"player"
|
@export var target_group: StringName = &"player"
|
||||||
@export var one_shot := true
|
@export var one_shot := true
|
||||||
|
@export var target_spawn_name: StringName = &""
|
||||||
|
@export var quest_event_name: StringName = &""
|
||||||
|
@export var quest_id_filter: String = ""
|
||||||
|
|
||||||
var _is_transitioning := false
|
var _is_transitioning := false
|
||||||
|
|
||||||
@ -22,6 +25,9 @@ func _on_body_entered(body: Node) -> void:
|
|||||||
if not ResourceLoader.exists(target_scene_path):
|
if not ResourceLoader.exists(target_scene_path):
|
||||||
push_warning("Teleporter target scene does not exist: %s" % target_scene_path)
|
push_warning("Teleporter target scene does not exist: %s" % target_scene_path)
|
||||||
return
|
return
|
||||||
|
_emit_quest_event(body)
|
||||||
|
if TeleportState != null:
|
||||||
|
TeleportState.request_spawn(target_spawn_name)
|
||||||
|
|
||||||
_is_transitioning = true
|
_is_transitioning = true
|
||||||
if one_shot:
|
if one_shot:
|
||||||
@ -29,6 +35,17 @@ func _on_body_entered(body: Node) -> void:
|
|||||||
call_deferred("_deferred_change_scene")
|
call_deferred("_deferred_change_scene")
|
||||||
|
|
||||||
|
|
||||||
|
func _emit_quest_event(body: Node) -> void:
|
||||||
|
if quest_event_name == StringName() or QuestManager == null:
|
||||||
|
return
|
||||||
|
if quest_id_filter.strip_edges() != "" and QuestManager.get_active_quest_id() != StringName(quest_id_filter):
|
||||||
|
return
|
||||||
|
QuestManager.emit_event(quest_event_name, {
|
||||||
|
"body": body,
|
||||||
|
"source": self,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
func _deferred_change_scene() -> void:
|
func _deferred_change_scene() -> void:
|
||||||
var err := get_tree().change_scene_to_file(target_scene_path)
|
var err := get_tree().change_scene_to_file(target_scene_path)
|
||||||
if err == OK:
|
if err == OK:
|
||||||
|
|||||||
13
game/scenes/Interaction/teleport_state.gd
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
var _requested_spawn_name: StringName = &""
|
||||||
|
|
||||||
|
|
||||||
|
func request_spawn(spawn_name: StringName) -> void:
|
||||||
|
_requested_spawn_name = spawn_name
|
||||||
|
|
||||||
|
|
||||||
|
func consume_spawn_name() -> StringName:
|
||||||
|
var spawn_name := _requested_spawn_name
|
||||||
|
_requested_spawn_name = &""
|
||||||
|
return spawn_name
|
||||||
1
game/scenes/Interaction/teleport_state.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://ds35gqf0obsxf
|
||||||
@ -21,8 +21,10 @@ func _toggle_menu():
|
|||||||
_animate_menu(false)
|
_animate_menu(false)
|
||||||
|
|
||||||
func _arrange_buttons():
|
func _arrange_buttons():
|
||||||
var buttons = get_children()
|
var buttons = get_children().filter(func(child): return child is Control)
|
||||||
var count = buttons.size()
|
var count = buttons.size()
|
||||||
|
if count == 0:
|
||||||
|
return
|
||||||
|
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
# Calculate the angle for each button in radians
|
# Calculate the angle for each button in radians
|
||||||
@ -36,6 +38,6 @@ func _arrange_buttons():
|
|||||||
|
|
||||||
func _animate_menu(opening: bool):
|
func _animate_menu(opening: bool):
|
||||||
var tween = create_tween().set_parallel(true)
|
var tween = create_tween().set_parallel(true)
|
||||||
for button in get_children():
|
for button in get_children().filter(func(child): return child is Control):
|
||||||
var final_scale = Vector2.ONE if opening else Vector2.ZERO
|
var final_scale = Vector2.ONE if opening else Vector2.ZERO
|
||||||
tween.tween_property(button, "scale", final_scale, transition_speed).from(Vector2.ZERO if opening else Vector2.ONE)
|
tween.tween_property(button, "scale", final_scale, transition_speed).from(Vector2.ZERO if opening else Vector2.ONE)
|
||||||
|
|||||||
77
game/scenes/Levels/ashling_level.gd
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
@export var player_spawn_position := Vector3(0.0, 0.0, 0.0)
|
||||||
|
@export var day_length := 120.0
|
||||||
|
@export var start_light_angle := -90.0
|
||||||
|
|
||||||
|
@onready var _player: RigidBody3D = get_node_or_null("Player") as RigidBody3D
|
||||||
|
@onready var _sun: DirectionalLight3D = $DirectionalLight3D
|
||||||
|
@onready var _quest_text: RichTextLabel = get_node_or_null("PhoneUI/Control/PhoneFrame/QuestText") as RichTextLabel
|
||||||
|
|
||||||
|
var _time := 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_move_player_to_spawn()
|
||||||
|
_setup_quest_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
_update_day_night(delta)
|
||||||
|
|
||||||
|
|
||||||
|
func _move_player_to_spawn() -> void:
|
||||||
|
if _player == null:
|
||||||
|
return
|
||||||
|
var spawn_marker := _consume_spawn_marker()
|
||||||
|
if spawn_marker != null:
|
||||||
|
_player.call("teleport_to_spawn", spawn_marker.global_transform)
|
||||||
|
else:
|
||||||
|
_player.global_position = player_spawn_position
|
||||||
|
_player.linear_velocity = Vector3.ZERO
|
||||||
|
_player.angular_velocity = Vector3.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
func _consume_spawn_marker() -> Node3D:
|
||||||
|
if TeleportState == null:
|
||||||
|
return null
|
||||||
|
var spawn_name: StringName = TeleportState.consume_spawn_name()
|
||||||
|
if spawn_name == StringName():
|
||||||
|
return null
|
||||||
|
return find_child(String(spawn_name), true, false) as Node3D
|
||||||
|
|
||||||
|
|
||||||
|
func _update_day_night(delta: float) -> void:
|
||||||
|
if _sun == null or day_length <= 0.0:
|
||||||
|
return
|
||||||
|
_time = fmod(_time + delta, day_length)
|
||||||
|
var t: float = _time / day_length
|
||||||
|
var angle: float = lerp(start_light_angle, start_light_angle + 360.0, t)
|
||||||
|
_sun.rotation_degrees.x = angle
|
||||||
|
var energy_curve: float = -sin((t * TAU) + (start_light_angle * PI / 180.0))
|
||||||
|
_sun.light_energy = clamp((energy_curve * 1.0) + 0.2, 0.0, 1.2)
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_quest_ui() -> void:
|
||||||
|
if QuestManager == null:
|
||||||
|
return
|
||||||
|
if not QuestManager.is_connected("quest_state_changed", Callable(self, "_refresh_quest_ui")):
|
||||||
|
QuestManager.quest_state_changed.connect(_refresh_quest_ui)
|
||||||
|
_refresh_quest_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_quest_ui() -> void:
|
||||||
|
if _quest_text == null or QuestManager == null:
|
||||||
|
return
|
||||||
|
var state: Dictionary = QuestManager.get_active_quest_state()
|
||||||
|
if not bool(state.get("active", false)):
|
||||||
|
_quest_text.text = "No active quest."
|
||||||
|
return
|
||||||
|
var title := String(state.get("title", "Quest"))
|
||||||
|
if bool(state.get("completed", false)):
|
||||||
|
_quest_text.text = "[b]%s[/b]\nComplete." % title
|
||||||
|
return
|
||||||
|
var step_index: int = int(state.get("current_step_index", 0))
|
||||||
|
var total_steps: int = int(state.get("total_steps", 0))
|
||||||
|
var step_text := String(state.get("current_step_text", ""))
|
||||||
|
_quest_text.text = "[b]%s[/b]\nStep %d/%d\n%s" % [title, step_index + 1, total_steps, step_text]
|
||||||
1
game/scenes/Levels/ashling_level.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://dgqayogt2d14g
|
||||||
134
game/scenes/Levels/ashling_level.tscn
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
[gd_scene load_steps=11 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scenes/Levels/ashling_level.gd" id="1_level"]
|
||||||
|
[ext_resource type="Script" path="res://scenes/player.gd" id="2_player"]
|
||||||
|
[ext_resource type="PackedScene" path="res://assets/models/TestCharAnimated.glb" id="3_model"]
|
||||||
|
[ext_resource type="PackedScene" path="res://scenes/Interaction/prototype_gateway.tscn" id="4_teleporter"]
|
||||||
|
[ext_resource type="Material" path="res://assets/materials/kenney_prototype_ground_green.tres" id="5_ground_mat"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dp6jk0k3o4v1u" path="res://scenes/UI/pause_menu.tscn" id="6_pause_menu"]
|
||||||
|
[ext_resource type="PackedScene" path="res://scenes/Characters/ashling_swarmer.tscn" id="7_ashling"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://bnwpu7p8sbsfa" path="res://scenes/Interaction/RadialCommandMenu.tscn" id="8_radial_menu"]
|
||||||
|
|
||||||
|
[sub_resource type="SphereShape3D" id="SphereShape3D_player"]
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_ground"]
|
||||||
|
size = Vector3(1080, 2, 1080)
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_ground"]
|
||||||
|
material = ExtResource("5_ground_mat")
|
||||||
|
size = Vector3(1080, 2, 1080)
|
||||||
|
|
||||||
|
[node name="AshlingLevel" type="Node3D"]
|
||||||
|
script = ExtResource("1_level")
|
||||||
|
|
||||||
|
[node name="Ground" type="StaticBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Ground"]
|
||||||
|
shape = SubResource("BoxShape3D_ground")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="Ground"]
|
||||||
|
mesh = SubResource("BoxMesh_ground")
|
||||||
|
|
||||||
|
[node name="Player" type="RigidBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
|
||||||
|
script = ExtResource("2_player")
|
||||||
|
camera_path = NodePath("Camera3D")
|
||||||
|
phone_path = NodePath("../PhoneUI")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
|
||||||
|
shape = SubResource("SphereShape3D_player")
|
||||||
|
|
||||||
|
[node name="TestCharAnimated" parent="Player" instance=ExtResource("3_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="Player"]
|
||||||
|
transform = Transform3D(0.9989785, -4.651856e-10, -0.045188628, 0.006969331, 0.9880354, 0.15407, 0.044647958, -0.15422754, 0.9870261, 0.22036135, 1.8988357, 0.64972365)
|
||||||
|
current = true
|
||||||
|
fov = 49.0
|
||||||
|
|
||||||
|
[node name="SpotLight3D" type="SpotLight3D" parent="Player"]
|
||||||
|
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
|
||||||
|
|
||||||
|
[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="EntrySpawn" type="Marker3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2)
|
||||||
|
|
||||||
|
[node name="AshlingSwarmer" parent="." instance=ExtResource("7_ashling")]
|
||||||
|
transform = Transform3D(0.8660254, 0, -0.5, 0, 1, 0, 0.5, 0, 0.8660254, 0, 0, -4)
|
||||||
|
|
||||||
|
[node name="Label3D" type="Label3D" parent="AshlingSwarmer"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.965926, -0.258819, 0, 0.258819, 0.965926, 0, 2.6, 0)
|
||||||
|
billboard = 1
|
||||||
|
no_depth_test = true
|
||||||
|
pixel_size = 0.012
|
||||||
|
text = "ASHLING"
|
||||||
|
|
||||||
|
[node name="ReturnTeleporter" parent="." instance=ExtResource("4_teleporter")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 5.5)
|
||||||
|
target_scene_path = "res://scenes/Levels/level.tscn"
|
||||||
|
target_spawn_name = &"AshlingReturnSpawn"
|
||||||
|
|
||||||
|
[node name="Label3D" type="Label3D" parent="ReturnTeleporter"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.965926, -0.258819, 0, 0.258819, 0.965926, 0, 5.4, 0)
|
||||||
|
billboard = 1
|
||||||
|
no_depth_test = true
|
||||||
|
pixel_size = 0.012
|
||||||
|
text = "PLAYGROUND"
|
||||||
|
|
||||||
|
[node name="PauseMenu" parent="." instance=ExtResource("6_pause_menu")]
|
||||||
|
|
||||||
|
[node name="PhoneUI" type="CanvasLayer" parent="."]
|
||||||
|
layer = 5
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="Control" type="Control" parent="PhoneUI"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="PhoneFrame" type="ColorRect" parent="PhoneUI/Control"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -180.0
|
||||||
|
offset_top = -320.0
|
||||||
|
offset_right = 180.0
|
||||||
|
offset_bottom = 320.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
color = Color(0.08, 0.08, 0.1, 1)
|
||||||
|
|
||||||
|
[node name="QuestTitle" type="Label" parent="PhoneUI/Control/PhoneFrame"]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 18.0
|
||||||
|
offset_top = 18.0
|
||||||
|
offset_right = 150.0
|
||||||
|
offset_bottom = 41.0
|
||||||
|
text = "Quest Log"
|
||||||
|
|
||||||
|
[node name="QuestText" type="RichTextLabel" parent="PhoneUI/Control/PhoneFrame"]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 18.0
|
||||||
|
offset_top = 52.0
|
||||||
|
offset_right = 344.0
|
||||||
|
offset_bottom = 613.0
|
||||||
|
bbcode_enabled = true
|
||||||
|
text = "No active quest."
|
||||||
|
scroll_active = false
|
||||||
|
|
||||||
|
[node name="RadialCommandMenu" parent="PhoneUI/Control" instance=ExtResource("8_radial_menu")]
|
||||||
|
layout_mode = 1
|
||||||
@ -22,9 +22,9 @@ const FIRST_QUEST := {
|
|||||||
"description": "Get familiar with movement and vehicles.",
|
"description": "Get familiar with movement and vehicles.",
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"id": "enter_vehicle",
|
"id": "enter_car_feature",
|
||||||
"text": "Get in the car (E).",
|
"text": "Walk through the car teleporter.",
|
||||||
"complete_event": "entered_vehicle",
|
"complete_event": "entered_car_feature",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "reach_checkpoint",
|
"id": "reach_checkpoint",
|
||||||
@ -35,6 +35,7 @@ const FIRST_QUEST := {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
_move_player_to_requested_spawn()
|
||||||
_apply_sun_state(0.0)
|
_apply_sun_state(0.0)
|
||||||
_setup_quests()
|
_setup_quests()
|
||||||
if _should_show_spawn_dialog() and DialogSystem and DialogSystem.has_method("show_text"):
|
if _should_show_spawn_dialog() and DialogSystem and DialogSystem.has_method("show_text"):
|
||||||
@ -45,7 +46,6 @@ func _ready() -> void:
|
|||||||
await get_tree().create_timer(spawn_dialog_auto_close_seconds).timeout
|
await get_tree().create_timer(spawn_dialog_auto_close_seconds).timeout
|
||||||
if DialogSystem and DialogSystem.has_method("close_if_text"):
|
if DialogSystem and DialogSystem.has_method("close_if_text"):
|
||||||
DialogSystem.close_if_text(spawn_dialog_text)
|
DialogSystem.close_if_text(spawn_dialog_text)
|
||||||
_show_quest_intro_dialog()
|
|
||||||
|
|
||||||
func _process(delta):
|
func _process(delta):
|
||||||
time = fmod((time + delta), day_length)
|
time = fmod((time + delta), day_length)
|
||||||
@ -132,3 +132,23 @@ func _mark_spawn_dialog_shown() -> void:
|
|||||||
if QuestManager == null:
|
if QuestManager == null:
|
||||||
return
|
return
|
||||||
QuestManager.set_meta(SPAWN_DIALOG_META_KEY, true)
|
QuestManager.set_meta(SPAWN_DIALOG_META_KEY, true)
|
||||||
|
|
||||||
|
|
||||||
|
func _move_player_to_requested_spawn() -> void:
|
||||||
|
if _player == null or TeleportState == null:
|
||||||
|
return
|
||||||
|
var spawn_name: StringName = TeleportState.consume_spawn_name()
|
||||||
|
if spawn_name == StringName():
|
||||||
|
return
|
||||||
|
var spawn_marker := find_child(String(spawn_name), true, false) as Node3D
|
||||||
|
if spawn_marker == null:
|
||||||
|
return
|
||||||
|
if _player.has_method("teleport_to_spawn"):
|
||||||
|
_player.call("teleport_to_spawn", spawn_marker.global_transform)
|
||||||
|
elif _player is RigidBody3D:
|
||||||
|
var player_body := _player as RigidBody3D
|
||||||
|
player_body.global_transform = spawn_marker.global_transform
|
||||||
|
player_body.linear_velocity = Vector3.ZERO
|
||||||
|
player_body.angular_velocity = Vector3.ZERO
|
||||||
|
else:
|
||||||
|
(_player as Node3D).global_transform = spawn_marker.global_transform
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
[ext_resource type="Script" uid="uid://bpxggc8nr6tf6" path="res://scenes/player.gd" id="1_muv8p"]
|
[ext_resource type="Script" uid="uid://bpxggc8nr6tf6" path="res://scenes/player.gd" id="1_muv8p"]
|
||||||
[ext_resource type="PackedScene" uid="uid://c5of6aaxop1hl" path="res://scenes/block.tscn" id="2_tc7dm"]
|
[ext_resource type="PackedScene" uid="uid://c5of6aaxop1hl" path="res://scenes/block.tscn" id="2_tc7dm"]
|
||||||
[ext_resource type="PackedScene" uid="uid://dp6jk0k3o4v1u" path="res://scenes/UI/pause_menu.tscn" id="3_pause_menu"]
|
[ext_resource type="PackedScene" uid="uid://dp6jk0k3o4v1u" path="res://scenes/UI/pause_menu.tscn" id="3_pause_menu"]
|
||||||
[ext_resource type="PackedScene" path="res://scenes/Characters/repo_bot.tscn" id="4_repo"]
|
|
||||||
[ext_resource type="PackedScene" path="res://scenes/Vehicles/car.tscn" id="5_car"]
|
[ext_resource type="PackedScene" path="res://scenes/Vehicles/car.tscn" id="5_car"]
|
||||||
[ext_resource type="PackedScene" uid="uid://bnqaqbgynoyys" path="res://assets/models/TestCharAnimated.glb" id="5_fi66n"]
|
[ext_resource type="PackedScene" uid="uid://bnqaqbgynoyys" path="res://assets/models/TestCharAnimated.glb" id="5_fi66n"]
|
||||||
[ext_resource type="Script" uid="uid://bk53njt7i3kmv" path="res://scenes/Interaction/dialog_trigger_area.gd" id="6_dialog"]
|
[ext_resource type="Script" uid="uid://bk53njt7i3kmv" path="res://scenes/Interaction/dialog_trigger_area.gd" id="6_dialog"]
|
||||||
@ -15,7 +14,6 @@
|
|||||||
[ext_resource type="Material" path="res://assets/materials/kenney_prototype_ground_green.tres" id="9_ground_mat"]
|
[ext_resource type="Material" path="res://assets/materials/kenney_prototype_ground_green.tres" id="9_ground_mat"]
|
||||||
[ext_resource type="Texture2D" uid="uid://c4ggdp0kg5wjk" path="res://addons/simplegrasstextured/textures/grassbushcc008.png" id="10_loupo"]
|
[ext_resource type="Texture2D" uid="uid://c4ggdp0kg5wjk" path="res://addons/simplegrasstextured/textures/grassbushcc008.png" id="10_loupo"]
|
||||||
[ext_resource type="Script" uid="uid://2juaclm8gc1n" path="res://addons/simplegrasstextured/grass.gd" id="11_1meta"]
|
[ext_resource type="Script" uid="uid://2juaclm8gc1n" path="res://addons/simplegrasstextured/grass.gd" id="11_1meta"]
|
||||||
[ext_resource type="Script" uid="uid://bv2ei5rg0dn3d" path="res://scenes/Levels/RadialCommandMenu.gd" id="15_18iqp"]
|
|
||||||
[ext_resource type="Texture2D" uid="uid://b2dmnrm3ubjon" path="res://assets/textures/stolenFire.png" id="16_i35yb"]
|
[ext_resource type="Texture2D" uid="uid://b2dmnrm3ubjon" path="res://assets/textures/stolenFire.png" id="16_i35yb"]
|
||||||
[ext_resource type="PackedScene" uid="uid://bnwpu7p8sbsfa" path="res://scenes/Interaction/RadialCommandMenu.tscn" id="16_px5jg"]
|
[ext_resource type="PackedScene" uid="uid://bnwpu7p8sbsfa" path="res://scenes/Interaction/RadialCommandMenu.tscn" id="16_px5jg"]
|
||||||
|
|
||||||
@ -401,10 +399,6 @@ script = ExtResource("1_a4mo8")
|
|||||||
|
|
||||||
[node name="human" parent="." unique_id=333471061 instance=ExtResource("1_eg4yq")]
|
[node name="human" parent="." unique_id=333471061 instance=ExtResource("1_eg4yq")]
|
||||||
|
|
||||||
[node name="RepoBot" parent="." unique_id=1519051606 instance=ExtResource("4_repo")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.9426608, 0, -4.4451966)
|
|
||||||
look_target_path = NodePath("../Player")
|
|
||||||
|
|
||||||
[node name="Thing" type="RigidBody3D" parent="." unique_id=32794772]
|
[node name="Thing" type="RigidBody3D" parent="." unique_id=32794772]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -3.7986288)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -3.7986288)
|
||||||
physics_material_override = SubResource("PhysicsMaterial_2q6dc")
|
physics_material_override = SubResource("PhysicsMaterial_2q6dc")
|
||||||
@ -450,48 +444,6 @@ root_motion_track = NodePath("Armature/Skeleton3D:mixamorig_Hips")
|
|||||||
tree_root = SubResource("AnimationNodeStateMachine_bwsrl")
|
tree_root = SubResource("AnimationNodeStateMachine_bwsrl")
|
||||||
anim_player = NodePath("../TestCharAnimated/AnimationPlayer")
|
anim_player = NodePath("../TestCharAnimated/AnimationPlayer")
|
||||||
|
|
||||||
[node name="Car" parent="." unique_id=1231424399 instance=ExtResource("5_car")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, -3)
|
|
||||||
|
|
||||||
[node name="DialogZone" type="Area3D" parent="." unique_id=616412893]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.5, 0, -2.5)
|
|
||||||
script = ExtResource("6_dialog")
|
|
||||||
prompt_text = "Press E to inspect area"
|
|
||||||
dialog_text = "Dialog trigger area"
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="DialogZone" unique_id=1201310304]
|
|
||||||
shape = SubResource("SphereShape3D_dialog_zone")
|
|
||||||
|
|
||||||
[node name="Visual" type="MeshInstance3D" parent="DialogZone" unique_id=851990198]
|
|
||||||
mesh = SubResource("SphereMesh_dialog_zone")
|
|
||||||
|
|
||||||
[node name="AutoDialogZone" type="Area3D" parent="." unique_id=123053838]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4, 0, -6.5)
|
|
||||||
script = ExtResource("6_dialog")
|
|
||||||
dialog_text = "Auto dialog trigger"
|
|
||||||
auto_popup = true
|
|
||||||
auto_popup_close_on_exit = true
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="AutoDialogZone" unique_id=54253856]
|
|
||||||
shape = SubResource("SphereShape3D_dialog_zone")
|
|
||||||
|
|
||||||
[node name="Visual" type="MeshInstance3D" parent="AutoDialogZone" unique_id=1933827323]
|
|
||||||
transform = Transform3D(0.8, 0, 0, 0, 0.8, 0, 0, 0, 0.8, 0, 0, 0)
|
|
||||||
mesh = SubResource("SphereMesh_dialog_zone")
|
|
||||||
|
|
||||||
[node name="QuestCheckpoint" type="Area3D" parent="." unique_id=1060088011]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 9, 0, -10)
|
|
||||||
script = ExtResource("7_qtrigger")
|
|
||||||
event_name = &"reach_checkpoint"
|
|
||||||
target_group = &"vehicle"
|
|
||||||
quest_id_filter = "first_drive"
|
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="QuestCheckpoint" unique_id=1399159753]
|
|
||||||
shape = SubResource("SphereShape3D_checkpoint")
|
|
||||||
|
|
||||||
[node name="Visual" type="MeshInstance3D" parent="QuestCheckpoint" unique_id=1488541765]
|
|
||||||
mesh = SubResource("SphereMesh_checkpoint")
|
|
||||||
|
|
||||||
[node name="Ground" type="StaticBody3D" parent="." unique_id=1718381675]
|
[node name="Ground" type="StaticBody3D" parent="." unique_id=1718381675]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0)
|
||||||
|
|
||||||
@ -548,7 +500,6 @@ anchor_right = 1.0
|
|||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
script = ExtResource("15_18iqp")
|
|
||||||
|
|
||||||
[node name="PhoneFrame" type="ColorRect" parent="PhoneUI/Control" unique_id=1204675817]
|
[node name="PhoneFrame" type="ColorRect" parent="PhoneUI/Control" unique_id=1204675817]
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
@ -589,8 +540,66 @@ layout_mode = 1
|
|||||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1808155101]
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1808155101]
|
||||||
environment = SubResource("Environment_a4mo8")
|
environment = SubResource("Environment_a4mo8")
|
||||||
|
|
||||||
[node name="LevelExitTeleporter" parent="." unique_id=1508738453 instance=ExtResource("8_teleporter")]
|
[node name="CarTeleporter" parent="." unique_id=1508738453 instance=ExtResource("8_teleporter")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.5, 0, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.5, 0, 0)
|
||||||
|
target_spawn_name = &"EntrySpawn"
|
||||||
|
quest_event_name = &"entered_car_feature"
|
||||||
|
quest_id_filter = "first_drive"
|
||||||
|
|
||||||
|
[node name="Label3D" type="Label3D" parent="CarTeleporter"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.965926, -0.258819, 0, 0.258819, 0.965926, 0, 5.4, 0)
|
||||||
|
billboard = 1
|
||||||
|
no_depth_test = true
|
||||||
|
pixel_size = 0.012
|
||||||
|
text = "CAR"
|
||||||
|
|
||||||
|
[node name="TriggerZonesTeleporter" parent="." instance=ExtResource("8_teleporter")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.5, 0, 6)
|
||||||
|
target_scene_path = "res://scenes/Levels/trigger_zones_level.tscn"
|
||||||
|
target_spawn_name = &"EntrySpawn"
|
||||||
|
|
||||||
|
[node name="Label3D" type="Label3D" parent="TriggerZonesTeleporter"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.965926, -0.258819, 0, 0.258819, 0.965926, 0, 5.4, 0)
|
||||||
|
billboard = 1
|
||||||
|
no_depth_test = true
|
||||||
|
pixel_size = 0.012
|
||||||
|
text = "TRIGGERS"
|
||||||
|
|
||||||
|
[node name="RepoBotTeleporter" parent="." instance=ExtResource("8_teleporter")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.5, 0, 12)
|
||||||
|
target_scene_path = "res://scenes/Levels/repo_bot_level.tscn"
|
||||||
|
target_spawn_name = &"EntrySpawn"
|
||||||
|
|
||||||
|
[node name="Label3D" type="Label3D" parent="RepoBotTeleporter"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.965926, -0.258819, 0, 0.258819, 0.965926, 0, 5.4, 0)
|
||||||
|
billboard = 1
|
||||||
|
no_depth_test = true
|
||||||
|
pixel_size = 0.012
|
||||||
|
text = "REPO BOT"
|
||||||
|
|
||||||
|
[node name="AshlingTeleporter" parent="." instance=ExtResource("8_teleporter")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.5, 0, 18)
|
||||||
|
target_scene_path = "res://scenes/Levels/ashling_level.tscn"
|
||||||
|
target_spawn_name = &"EntrySpawn"
|
||||||
|
|
||||||
|
[node name="Label3D" type="Label3D" parent="AshlingTeleporter"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.965926, -0.258819, 0, 0.258819, 0.965926, 0, 5.4, 0)
|
||||||
|
billboard = 1
|
||||||
|
no_depth_test = true
|
||||||
|
pixel_size = 0.012
|
||||||
|
text = "ASHLING"
|
||||||
|
|
||||||
|
[node name="CarReturnSpawn" type="Marker3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.5, 0, -3)
|
||||||
|
|
||||||
|
[node name="TriggerZonesReturnSpawn" type="Marker3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.5, 0, 3)
|
||||||
|
|
||||||
|
[node name="RepoBotReturnSpawn" type="Marker3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.5, 0, 9)
|
||||||
|
|
||||||
|
[node name="AshlingReturnSpawn" type="Marker3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.5, 0, 15)
|
||||||
|
|
||||||
[node name="GPUParticles3D" type="GPUParticles3D" parent="." unique_id=2107882789]
|
[node name="GPUParticles3D" type="GPUParticles3D" parent="." unique_id=2107882789]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.8036127, 1.8337743, -6.1764746)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.8036127, 1.8337743, -6.1764746)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ extends Node3D
|
|||||||
const CHARACTER_API_URL := "https://pchar.ranaze.com/api/Characters"
|
const CHARACTER_API_URL := "https://pchar.ranaze.com/api/Characters"
|
||||||
const LOCATION_API_URL := "https://ploc.ranaze.com/api/Locations"
|
const LOCATION_API_URL := "https://ploc.ranaze.com/api/Locations"
|
||||||
const INVENTORY_API_URL := "https://pinv.ranaze.com/api/inventory"
|
const INVENTORY_API_URL := "https://pinv.ranaze.com/api/inventory"
|
||||||
|
const CRAFTING_API_URL := "https://pcraft.ranaze.com/api/crafting"
|
||||||
const MAIL_API_URL := "https://pmail.ranaze.com/api/mail"
|
const MAIL_API_URL := "https://pmail.ranaze.com/api/mail"
|
||||||
const WORLD_API_URL := "https://pworld.ranaze.com/api/world"
|
const WORLD_API_URL := "https://pworld.ranaze.com/api/world"
|
||||||
const START_SCREEN_SCENE := "res://scenes/UI/start_screen.tscn"
|
const START_SCREEN_SCENE := "res://scenes/UI/start_screen.tscn"
|
||||||
@ -34,6 +35,7 @@ const WORLD_CYCLE_REFRESH_INTERVAL := 15.0
|
|||||||
@onready var _inventory_location_label: Label = $InventoryMenu/MarginContainer/Panel/VBoxContainer/CurrentLocationLabel
|
@onready var _inventory_location_label: Label = $InventoryMenu/MarginContainer/Panel/VBoxContainer/CurrentLocationLabel
|
||||||
@onready var _character_items_list: ItemList = $InventoryMenu/MarginContainer/Panel/VBoxContainer/Columns/CharacterPanel/VBoxContainer/CharacterItems
|
@onready var _character_items_list: ItemList = $InventoryMenu/MarginContainer/Panel/VBoxContainer/Columns/CharacterPanel/VBoxContainer/CharacterItems
|
||||||
@onready var _ground_items_list: ItemList = $InventoryMenu/MarginContainer/Panel/VBoxContainer/Columns/GroundPanel/VBoxContainer/GroundItems
|
@onready var _ground_items_list: ItemList = $InventoryMenu/MarginContainer/Panel/VBoxContainer/Columns/GroundPanel/VBoxContainer/GroundItems
|
||||||
|
@onready var _recipes_list: ItemList = $InventoryMenu/MarginContainer/Panel/VBoxContainer/Columns/RecipesPanel/VBoxContainer/RecipesList
|
||||||
@onready var _target_slot_spin_box: SpinBox = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ControlsRow/TargetSlotSpinBox
|
@onready var _target_slot_spin_box: SpinBox = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ControlsRow/TargetSlotSpinBox
|
||||||
@onready var _quantity_spin_box: SpinBox = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ControlsRow/QuantitySpinBox
|
@onready var _quantity_spin_box: SpinBox = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ControlsRow/QuantitySpinBox
|
||||||
@onready var _inventory_status_label: Label = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/StatusLabel
|
@onready var _inventory_status_label: Label = $InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/StatusLabel
|
||||||
@ -67,8 +69,10 @@ var _queued_locations_refresh := false
|
|||||||
var _interact_in_flight := false
|
var _interact_in_flight := false
|
||||||
var _inventory_request_in_flight := false
|
var _inventory_request_in_flight := false
|
||||||
var _character_inventory_items: Array = []
|
var _character_inventory_items: Array = []
|
||||||
|
var _crafting_recipes: Array = []
|
||||||
var _selected_character_item_id := ""
|
var _selected_character_item_id := ""
|
||||||
var _selected_ground_item_id := ""
|
var _selected_ground_item_id := ""
|
||||||
|
var _selected_recipe_key := ""
|
||||||
var _visible_characters_in_flight := false
|
var _visible_characters_in_flight := false
|
||||||
var _heartbeat_in_flight := false
|
var _heartbeat_in_flight := false
|
||||||
var _visible_character_refresh_elapsed := 0.0
|
var _visible_character_refresh_elapsed := 0.0
|
||||||
@ -264,8 +268,10 @@ func _close_inventory_menu() -> void:
|
|||||||
_inventory_status_label.text = ""
|
_inventory_status_label.text = ""
|
||||||
_selected_character_item_id = ""
|
_selected_character_item_id = ""
|
||||||
_selected_ground_item_id = ""
|
_selected_ground_item_id = ""
|
||||||
|
_selected_recipe_key = ""
|
||||||
_character_items_list.deselect_all()
|
_character_items_list.deselect_all()
|
||||||
_ground_items_list.deselect_all()
|
_ground_items_list.deselect_all()
|
||||||
|
_recipes_list.deselect_all()
|
||||||
_set_player_menu_lock(false)
|
_set_player_menu_lock(false)
|
||||||
|
|
||||||
|
|
||||||
@ -647,6 +653,7 @@ func _refresh_inventory_menu_data_async() -> void:
|
|||||||
_inventory_request_in_flight = true
|
_inventory_request_in_flight = true
|
||||||
_update_inventory_location_label()
|
_update_inventory_location_label()
|
||||||
_character_inventory_items = await _fetch_character_inventory()
|
_character_inventory_items = await _fetch_character_inventory()
|
||||||
|
_crafting_recipes = await _fetch_available_recipes()
|
||||||
var location_id := _get_current_location_id()
|
var location_id := _get_current_location_id()
|
||||||
if not location_id.is_empty():
|
if not location_id.is_empty():
|
||||||
await _refresh_location_inventory(location_id)
|
await _refresh_location_inventory(location_id)
|
||||||
@ -657,6 +664,7 @@ func _refresh_inventory_menu_data_async() -> void:
|
|||||||
func _render_inventory_menu() -> void:
|
func _render_inventory_menu() -> void:
|
||||||
var current_character_selection := _selected_character_item_id
|
var current_character_selection := _selected_character_item_id
|
||||||
var current_ground_selection := _selected_ground_item_id
|
var current_ground_selection := _selected_ground_item_id
|
||||||
|
var current_recipe_selection := _selected_recipe_key
|
||||||
|
|
||||||
_character_items_list.clear()
|
_character_items_list.clear()
|
||||||
var slot_map := {}
|
var slot_map := {}
|
||||||
@ -698,13 +706,36 @@ func _render_inventory_menu() -> void:
|
|||||||
if not current_ground_selection.is_empty() and String(floor_item.get("itemId", floor_item.get("id", ""))).strip_edges() == current_ground_selection:
|
if not current_ground_selection.is_empty() and String(floor_item.get("itemId", floor_item.get("id", ""))).strip_edges() == current_ground_selection:
|
||||||
_ground_items_list.select(index)
|
_ground_items_list.select(index)
|
||||||
|
|
||||||
|
_recipes_list.clear()
|
||||||
|
for index in range(_crafting_recipes.size()):
|
||||||
|
var recipe_entry_variant: Variant = _crafting_recipes[index]
|
||||||
|
if typeof(recipe_entry_variant) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var recipe_entry := recipe_entry_variant as Dictionary
|
||||||
|
var recipe: Dictionary = recipe_entry.get("recipe", {})
|
||||||
|
var recipe_key := String(recipe.get("recipeKey", "")).strip_edges()
|
||||||
|
var marker := "[OK]" if bool(recipe_entry.get("canCraft", false)) else "[--]"
|
||||||
|
var recipe_text := "%s %s: %s -> %s" % [
|
||||||
|
marker,
|
||||||
|
String(recipe.get("name", recipe_key)).strip_edges(),
|
||||||
|
_format_ingredients(recipe.get("inputs", [])),
|
||||||
|
_format_ingredients(recipe.get("outputs", []))
|
||||||
|
]
|
||||||
|
_recipes_list.add_item(recipe_text)
|
||||||
|
var recipe_list_index := _recipes_list.get_item_count() - 1
|
||||||
|
_recipes_list.set_item_metadata(recipe_list_index, recipe_entry)
|
||||||
|
if not current_recipe_selection.is_empty() and recipe_key == current_recipe_selection:
|
||||||
|
_recipes_list.select(recipe_list_index)
|
||||||
|
|
||||||
_update_inventory_controls()
|
_update_inventory_controls()
|
||||||
|
|
||||||
|
|
||||||
func _update_inventory_controls() -> void:
|
func _update_inventory_controls() -> void:
|
||||||
var selected_item := _get_selected_inventory_item()
|
var selected_item := _get_selected_inventory_item()
|
||||||
var max_quantity := 1
|
var max_quantity := 1
|
||||||
if not selected_item.is_empty():
|
if not _selected_recipe_key.is_empty():
|
||||||
|
max_quantity = 999
|
||||||
|
elif not selected_item.is_empty():
|
||||||
max_quantity = max(1, int(selected_item.get("quantity", 1)))
|
max_quantity = max(1, int(selected_item.get("quantity", 1)))
|
||||||
if _selected_ground_item_id == String(selected_item.get("itemId", selected_item.get("id", ""))).strip_edges():
|
if _selected_ground_item_id == String(selected_item.get("itemId", selected_item.get("id", ""))).strip_edges():
|
||||||
_target_slot_spin_box.value = float(_default_slot_for_item(selected_item))
|
_target_slot_spin_box.value = float(_default_slot_for_item(selected_item))
|
||||||
@ -733,6 +764,43 @@ func _find_item_by_id(items: Array, item_id: String) -> Dictionary:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
func _find_recipe_entry(recipe_key: String) -> Dictionary:
|
||||||
|
for recipe_entry_variant in _crafting_recipes:
|
||||||
|
if typeof(recipe_entry_variant) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var recipe_entry := recipe_entry_variant as Dictionary
|
||||||
|
var recipe: Dictionary = recipe_entry.get("recipe", {})
|
||||||
|
if String(recipe.get("recipeKey", "")).strip_edges() == recipe_key:
|
||||||
|
return recipe_entry
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
func _format_ingredients(value: Variant) -> String:
|
||||||
|
if typeof(value) != TYPE_ARRAY:
|
||||||
|
return "-"
|
||||||
|
var parts := PackedStringArray()
|
||||||
|
for ingredient_variant in value:
|
||||||
|
if typeof(ingredient_variant) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var ingredient := ingredient_variant as Dictionary
|
||||||
|
var item_key := String(ingredient.get("itemKey", "")).strip_edges()
|
||||||
|
if item_key.is_empty():
|
||||||
|
continue
|
||||||
|
parts.append("%s x%d" % [item_key, int(ingredient.get("quantity", 0))])
|
||||||
|
return ", ".join(parts) if parts.size() > 0 else "-"
|
||||||
|
|
||||||
|
|
||||||
|
func _format_messages(value: Variant, fallback: String) -> String:
|
||||||
|
if typeof(value) != TYPE_ARRAY:
|
||||||
|
return fallback
|
||||||
|
var parts := PackedStringArray()
|
||||||
|
for message_variant in value:
|
||||||
|
var message := String(message_variant).strip_edges()
|
||||||
|
if not message.is_empty():
|
||||||
|
parts.append(message)
|
||||||
|
return ", ".join(parts) if parts.size() > 0 else fallback
|
||||||
|
|
||||||
|
|
||||||
func _get_current_location_id() -> String:
|
func _get_current_location_id() -> String:
|
||||||
var location_data := _get_location_data(_center_coord)
|
var location_data := _get_location_data(_center_coord)
|
||||||
return String(location_data.get("id", "")).strip_edges()
|
return String(location_data.get("id", "")).strip_edges()
|
||||||
@ -774,7 +842,9 @@ func _first_open_character_slot() -> int:
|
|||||||
func _on_character_items_selected(index: int) -> void:
|
func _on_character_items_selected(index: int) -> void:
|
||||||
var metadata: Variant = _character_items_list.get_item_metadata(index)
|
var metadata: Variant = _character_items_list.get_item_metadata(index)
|
||||||
_selected_ground_item_id = ""
|
_selected_ground_item_id = ""
|
||||||
|
_selected_recipe_key = ""
|
||||||
_ground_items_list.deselect_all()
|
_ground_items_list.deselect_all()
|
||||||
|
_recipes_list.deselect_all()
|
||||||
if typeof(metadata) != TYPE_DICTIONARY or (metadata as Dictionary).is_empty():
|
if typeof(metadata) != TYPE_DICTIONARY or (metadata as Dictionary).is_empty():
|
||||||
_selected_character_item_id = ""
|
_selected_character_item_id = ""
|
||||||
else:
|
else:
|
||||||
@ -787,7 +857,9 @@ func _on_character_items_selected(index: int) -> void:
|
|||||||
func _on_ground_items_selected(index: int) -> void:
|
func _on_ground_items_selected(index: int) -> void:
|
||||||
var metadata: Variant = _ground_items_list.get_item_metadata(index)
|
var metadata: Variant = _ground_items_list.get_item_metadata(index)
|
||||||
_selected_character_item_id = ""
|
_selected_character_item_id = ""
|
||||||
|
_selected_recipe_key = ""
|
||||||
_character_items_list.deselect_all()
|
_character_items_list.deselect_all()
|
||||||
|
_recipes_list.deselect_all()
|
||||||
if typeof(metadata) != TYPE_DICTIONARY or (metadata as Dictionary).is_empty():
|
if typeof(metadata) != TYPE_DICTIONARY or (metadata as Dictionary).is_empty():
|
||||||
_selected_ground_item_id = ""
|
_selected_ground_item_id = ""
|
||||||
else:
|
else:
|
||||||
@ -797,6 +869,22 @@ func _on_ground_items_selected(index: int) -> void:
|
|||||||
_update_inventory_controls()
|
_update_inventory_controls()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_recipe_selected(index: int) -> void:
|
||||||
|
var metadata: Variant = _recipes_list.get_item_metadata(index)
|
||||||
|
_selected_character_item_id = ""
|
||||||
|
_selected_ground_item_id = ""
|
||||||
|
_character_items_list.deselect_all()
|
||||||
|
_ground_items_list.deselect_all()
|
||||||
|
if typeof(metadata) != TYPE_DICTIONARY or (metadata as Dictionary).is_empty():
|
||||||
|
_selected_recipe_key = ""
|
||||||
|
else:
|
||||||
|
var recipe_entry := metadata as Dictionary
|
||||||
|
var recipe: Dictionary = recipe_entry.get("recipe", {})
|
||||||
|
_selected_recipe_key = String(recipe.get("recipeKey", "")).strip_edges()
|
||||||
|
_inventory_status_label.text = _format_messages(recipe_entry.get("missingRequirements", []), "Recipe selected.")
|
||||||
|
_update_inventory_controls()
|
||||||
|
|
||||||
|
|
||||||
func _on_inventory_move_pressed() -> void:
|
func _on_inventory_move_pressed() -> void:
|
||||||
if _selected_character_item_id.is_empty():
|
if _selected_character_item_id.is_empty():
|
||||||
_inventory_status_label.text = "Select a character item first."
|
_inventory_status_label.text = "Select a character item first."
|
||||||
@ -830,6 +918,20 @@ func _on_inventory_pickup_pressed() -> void:
|
|||||||
_transfer_item_async(_selected_ground_item_id, "location", location_id, "character", _character_id, null, quantity, "Picked up item.")
|
_transfer_item_async(_selected_ground_item_id, "location", location_id, "character", _character_id, null, quantity, "Picked up item.")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_inventory_craft_pressed() -> void:
|
||||||
|
if _selected_recipe_key.is_empty():
|
||||||
|
_inventory_status_label.text = "Select a recipe first."
|
||||||
|
return
|
||||||
|
var recipe_entry := _find_recipe_entry(_selected_recipe_key)
|
||||||
|
if recipe_entry.is_empty():
|
||||||
|
_inventory_status_label.text = "Selected recipe is no longer available."
|
||||||
|
return
|
||||||
|
if not bool(recipe_entry.get("canCraft", false)):
|
||||||
|
_inventory_status_label.text = _format_messages(recipe_entry.get("missingRequirements", []), "Recipe requirements are missing.")
|
||||||
|
return
|
||||||
|
_craft_recipe_async(_selected_recipe_key, int(_quantity_spin_box.value))
|
||||||
|
|
||||||
|
|
||||||
func _on_inventory_refresh_pressed() -> void:
|
func _on_inventory_refresh_pressed() -> void:
|
||||||
_inventory_status_label.text = "Refreshing..."
|
_inventory_status_label.text = "Refreshing..."
|
||||||
_refresh_inventory_menu_data()
|
_refresh_inventory_menu_data()
|
||||||
@ -922,6 +1024,88 @@ func _handle_inventory_mutation_response(result: Array, success_message: String)
|
|||||||
_refresh_inventory_menu_data()
|
_refresh_inventory_menu_data()
|
||||||
|
|
||||||
|
|
||||||
|
func _craft_recipe_async(recipe_key: String, quantity: int) -> void:
|
||||||
|
if _inventory_request_in_flight:
|
||||||
|
return
|
||||||
|
_inventory_request_in_flight = true
|
||||||
|
_inventory_status_label.text = "Crafting..."
|
||||||
|
|
||||||
|
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)
|
||||||
|
headers.append("Content-Type: application/json")
|
||||||
|
|
||||||
|
var body := JSON.stringify({
|
||||||
|
"recipeKey": recipe_key,
|
||||||
|
"quantity": max(1, quantity),
|
||||||
|
"locationId": _get_current_location_id()
|
||||||
|
})
|
||||||
|
var err := request.request("%s/characters/%s/craft" % [CRAFTING_API_URL, _character_id], headers, HTTPClient.METHOD_POST, body)
|
||||||
|
if err != OK:
|
||||||
|
request.queue_free()
|
||||||
|
_inventory_status_label.text = "Craft request failed."
|
||||||
|
_inventory_request_in_flight = false
|
||||||
|
return
|
||||||
|
|
||||||
|
var result: Array = await request.request_completed
|
||||||
|
request.queue_free()
|
||||||
|
_inventory_request_in_flight = false
|
||||||
|
|
||||||
|
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:
|
||||||
|
var parsed_error: Variant = JSON.parse_string(response_body)
|
||||||
|
if typeof(parsed_error) == TYPE_DICTIONARY:
|
||||||
|
_inventory_status_label.text = _format_messages((parsed_error as Dictionary).get("missingRequirements", []), "Crafting failed.")
|
||||||
|
else:
|
||||||
|
_inventory_status_label.text = "Crafting failed."
|
||||||
|
push_warning("Crafting failed (%s/%s): %s" % [result_code, response_code, response_body])
|
||||||
|
_refresh_inventory_menu_data()
|
||||||
|
return
|
||||||
|
|
||||||
|
var parsed: Variant = JSON.parse_string(response_body)
|
||||||
|
var produced_text := "Crafted."
|
||||||
|
if typeof(parsed) == TYPE_DICTIONARY:
|
||||||
|
produced_text = "Crafted %s." % _format_ingredients((parsed as Dictionary).get("produced", []))
|
||||||
|
_inventory_status_label.text = produced_text
|
||||||
|
_refresh_inventory_menu_data()
|
||||||
|
|
||||||
|
|
||||||
|
func _fetch_available_recipes() -> Array:
|
||||||
|
if _character_id.is_empty():
|
||||||
|
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/characters/%s/available-recipes" % [CRAFTING_API_URL, _character_id], headers, HTTPClient.METHOD_GET)
|
||||||
|
if err != OK:
|
||||||
|
request.queue_free()
|
||||||
|
push_warning("Failed to request crafting recipes: %s" % err)
|
||||||
|
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 crafting recipes (%s/%s): %s" % [result_code, response_code, response_body])
|
||||||
|
return []
|
||||||
|
|
||||||
|
var parsed: Variant = JSON.parse_string(response_body)
|
||||||
|
return parsed if typeof(parsed) == TYPE_ARRAY else []
|
||||||
|
|
||||||
|
|
||||||
func _fetch_character_inventory() -> Array:
|
func _fetch_character_inventory() -> Array:
|
||||||
var request := HTTPRequest.new()
|
var request := HTTPRequest.new()
|
||||||
add_child(request)
|
add_child(request)
|
||||||
|
|||||||
@ -165,6 +165,26 @@ layout_mode = 2
|
|||||||
size_flags_vertical = 3
|
size_flags_vertical = 3
|
||||||
select_mode = 0
|
select_mode = 0
|
||||||
|
|
||||||
|
[node name="RecipesPanel" type="PanelContainer" parent="InventoryMenu/MarginContainer/Panel/VBoxContainer/Columns"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="InventoryMenu/MarginContainer/Panel/VBoxContainer/Columns/RecipesPanel"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="RecipesLabel" type="Label" parent="InventoryMenu/MarginContainer/Panel/VBoxContainer/Columns/RecipesPanel/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Crafting"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
|
||||||
|
[node name="RecipesList" type="ItemList" parent="InventoryMenu/MarginContainer/Panel/VBoxContainer/Columns/RecipesPanel/VBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(0, 240)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
select_mode = 0
|
||||||
|
|
||||||
[node name="ControlsPanel" type="PanelContainer" parent="InventoryMenu/MarginContainer/Panel/VBoxContainer"]
|
[node name="ControlsPanel" type="PanelContainer" parent="InventoryMenu/MarginContainer/Panel/VBoxContainer"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
||||||
@ -220,6 +240,11 @@ layout_mode = 2
|
|||||||
theme = ExtResource("4_button_theme")
|
theme = ExtResource("4_button_theme")
|
||||||
text = "PICK UP"
|
text = "PICK UP"
|
||||||
|
|
||||||
|
[node name="CraftButton" type="Button" parent="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme = ExtResource("4_button_theme")
|
||||||
|
text = "CRAFT"
|
||||||
|
|
||||||
[node name="RefreshButton" type="Button" parent="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow"]
|
[node name="RefreshButton" type="Button" parent="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme = ExtResource("4_button_theme")
|
theme = ExtResource("4_button_theme")
|
||||||
@ -377,9 +402,11 @@ text = ""
|
|||||||
[connection signal="pressed" from="PauseMenu/CenterContainer/Panel/VBoxContainer/MainMenuButton" to="." method="_on_pause_main_menu_pressed"]
|
[connection signal="pressed" from="PauseMenu/CenterContainer/Panel/VBoxContainer/MainMenuButton" to="." method="_on_pause_main_menu_pressed"]
|
||||||
[connection signal="item_selected" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/Columns/CharacterPanel/VBoxContainer/CharacterItems" to="." method="_on_character_items_selected"]
|
[connection signal="item_selected" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/Columns/CharacterPanel/VBoxContainer/CharacterItems" to="." method="_on_character_items_selected"]
|
||||||
[connection signal="item_selected" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/Columns/GroundPanel/VBoxContainer/GroundItems" to="." method="_on_ground_items_selected"]
|
[connection signal="item_selected" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/Columns/GroundPanel/VBoxContainer/GroundItems" to="." method="_on_ground_items_selected"]
|
||||||
|
[connection signal="item_selected" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/Columns/RecipesPanel/VBoxContainer/RecipesList" to="." method="_on_recipe_selected"]
|
||||||
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/MoveButton" to="." method="_on_inventory_move_pressed"]
|
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/MoveButton" to="." method="_on_inventory_move_pressed"]
|
||||||
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/DropButton" to="." method="_on_inventory_drop_pressed"]
|
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/DropButton" to="." method="_on_inventory_drop_pressed"]
|
||||||
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/PickupButton" to="." method="_on_inventory_pickup_pressed"]
|
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/PickupButton" to="." method="_on_inventory_pickup_pressed"]
|
||||||
|
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/CraftButton" to="." method="_on_inventory_craft_pressed"]
|
||||||
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/RefreshButton" to="." method="_on_inventory_refresh_pressed"]
|
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/RefreshButton" to="." method="_on_inventory_refresh_pressed"]
|
||||||
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/CloseButton" to="." method="_on_inventory_close_pressed"]
|
[connection signal="pressed" from="InventoryMenu/MarginContainer/Panel/VBoxContainer/ControlsPanel/VBoxContainer/ActionRow/CloseButton" to="." method="_on_inventory_close_pressed"]
|
||||||
[connection signal="item_selected" from="MailMenu/MarginContainer/Panel/VBoxContainer/Columns/InboxPanel/VBoxContainer/InboxItems" to="." method="_on_mail_inbox_selected"]
|
[connection signal="item_selected" from="MailMenu/MarginContainer/Panel/VBoxContainer/Columns/InboxPanel/VBoxContainer/InboxItems" to="." method="_on_mail_inbox_selected"]
|
||||||
|
|||||||
77
game/scenes/Levels/repo_bot_level.gd
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
@export var player_spawn_position := Vector3(0.0, 0.0, 0.0)
|
||||||
|
@export var day_length := 120.0
|
||||||
|
@export var start_light_angle := -90.0
|
||||||
|
|
||||||
|
@onready var _player: RigidBody3D = get_node_or_null("Player") as RigidBody3D
|
||||||
|
@onready var _sun: DirectionalLight3D = $DirectionalLight3D
|
||||||
|
@onready var _quest_text: RichTextLabel = get_node_or_null("PhoneUI/Control/PhoneFrame/QuestText") as RichTextLabel
|
||||||
|
|
||||||
|
var _time := 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_move_player_to_spawn()
|
||||||
|
_setup_quest_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
_update_day_night(delta)
|
||||||
|
|
||||||
|
|
||||||
|
func _move_player_to_spawn() -> void:
|
||||||
|
if _player == null:
|
||||||
|
return
|
||||||
|
var spawn_marker := _consume_spawn_marker()
|
||||||
|
if spawn_marker != null:
|
||||||
|
_player.call("teleport_to_spawn", spawn_marker.global_transform)
|
||||||
|
else:
|
||||||
|
_player.global_position = player_spawn_position
|
||||||
|
_player.linear_velocity = Vector3.ZERO
|
||||||
|
_player.angular_velocity = Vector3.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
func _consume_spawn_marker() -> Node3D:
|
||||||
|
if TeleportState == null:
|
||||||
|
return null
|
||||||
|
var spawn_name: StringName = TeleportState.consume_spawn_name()
|
||||||
|
if spawn_name == StringName():
|
||||||
|
return null
|
||||||
|
return find_child(String(spawn_name), true, false) as Node3D
|
||||||
|
|
||||||
|
|
||||||
|
func _update_day_night(delta: float) -> void:
|
||||||
|
if _sun == null or day_length <= 0.0:
|
||||||
|
return
|
||||||
|
_time = fmod(_time + delta, day_length)
|
||||||
|
var t: float = _time / day_length
|
||||||
|
var angle: float = lerp(start_light_angle, start_light_angle + 360.0, t)
|
||||||
|
_sun.rotation_degrees.x = angle
|
||||||
|
var energy_curve: float = -sin((t * TAU) + (start_light_angle * PI / 180.0))
|
||||||
|
_sun.light_energy = clamp((energy_curve * 1.0) + 0.2, 0.0, 1.2)
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_quest_ui() -> void:
|
||||||
|
if QuestManager == null:
|
||||||
|
return
|
||||||
|
if not QuestManager.is_connected("quest_state_changed", Callable(self, "_refresh_quest_ui")):
|
||||||
|
QuestManager.quest_state_changed.connect(_refresh_quest_ui)
|
||||||
|
_refresh_quest_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_quest_ui() -> void:
|
||||||
|
if _quest_text == null or QuestManager == null:
|
||||||
|
return
|
||||||
|
var state: Dictionary = QuestManager.get_active_quest_state()
|
||||||
|
if not bool(state.get("active", false)):
|
||||||
|
_quest_text.text = "No active quest."
|
||||||
|
return
|
||||||
|
var title := String(state.get("title", "Quest"))
|
||||||
|
if bool(state.get("completed", false)):
|
||||||
|
_quest_text.text = "[b]%s[/b]\nComplete." % title
|
||||||
|
return
|
||||||
|
var step_index: int = int(state.get("current_step_index", 0))
|
||||||
|
var total_steps: int = int(state.get("total_steps", 0))
|
||||||
|
var step_text := String(state.get("current_step_text", ""))
|
||||||
|
_quest_text.text = "[b]%s[/b]\nStep %d/%d\n%s" % [title, step_index + 1, total_steps, step_text]
|
||||||
1
game/scenes/Levels/repo_bot_level.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://cy2vwcmtqsr4o
|
||||||
135
game/scenes/Levels/repo_bot_level.tscn
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
[gd_scene load_steps=11 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scenes/Levels/repo_bot_level.gd" id="1_level"]
|
||||||
|
[ext_resource type="Script" path="res://scenes/player.gd" id="2_player"]
|
||||||
|
[ext_resource type="PackedScene" path="res://assets/models/TestCharAnimated.glb" id="3_model"]
|
||||||
|
[ext_resource type="PackedScene" path="res://scenes/Interaction/prototype_gateway.tscn" id="4_teleporter"]
|
||||||
|
[ext_resource type="Material" path="res://assets/materials/kenney_prototype_ground_green.tres" id="5_ground_mat"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dp6jk0k3o4v1u" path="res://scenes/UI/pause_menu.tscn" id="6_pause_menu"]
|
||||||
|
[ext_resource type="PackedScene" path="res://scenes/Characters/repo_bot.tscn" id="7_repo_bot"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://bnwpu7p8sbsfa" path="res://scenes/Interaction/RadialCommandMenu.tscn" id="8_radial_menu"]
|
||||||
|
|
||||||
|
[sub_resource type="SphereShape3D" id="SphereShape3D_player"]
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_ground"]
|
||||||
|
size = Vector3(1080, 2, 1080)
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_ground"]
|
||||||
|
material = ExtResource("5_ground_mat")
|
||||||
|
size = Vector3(1080, 2, 1080)
|
||||||
|
|
||||||
|
[node name="RepoBotLevel" type="Node3D"]
|
||||||
|
script = ExtResource("1_level")
|
||||||
|
|
||||||
|
[node name="Ground" type="StaticBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Ground"]
|
||||||
|
shape = SubResource("BoxShape3D_ground")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="Ground"]
|
||||||
|
mesh = SubResource("BoxMesh_ground")
|
||||||
|
|
||||||
|
[node name="Player" type="RigidBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
|
||||||
|
script = ExtResource("2_player")
|
||||||
|
camera_path = NodePath("Camera3D")
|
||||||
|
phone_path = NodePath("../PhoneUI")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
|
||||||
|
shape = SubResource("SphereShape3D_player")
|
||||||
|
|
||||||
|
[node name="TestCharAnimated" parent="Player" instance=ExtResource("3_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="Player"]
|
||||||
|
transform = Transform3D(0.9989785, -4.651856e-10, -0.045188628, 0.006969331, 0.9880354, 0.15407, 0.044647958, -0.15422754, 0.9870261, 0.22036135, 1.8988357, 0.64972365)
|
||||||
|
current = true
|
||||||
|
fov = 49.0
|
||||||
|
|
||||||
|
[node name="SpotLight3D" type="SpotLight3D" parent="Player"]
|
||||||
|
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
|
||||||
|
|
||||||
|
[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="EntrySpawn" type="Marker3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2)
|
||||||
|
|
||||||
|
[node name="RepoBot" parent="." instance=ExtResource("7_repo_bot")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -4)
|
||||||
|
look_target_path = NodePath("../Player")
|
||||||
|
|
||||||
|
[node name="Label3D" type="Label3D" parent="RepoBot"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.965926, -0.258819, 0, 0.258819, 0.965926, 0, 2.0, 0)
|
||||||
|
billboard = 1
|
||||||
|
no_depth_test = true
|
||||||
|
pixel_size = 0.012
|
||||||
|
text = "REPO BOT"
|
||||||
|
|
||||||
|
[node name="ReturnTeleporter" parent="." instance=ExtResource("4_teleporter")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 5.5)
|
||||||
|
target_scene_path = "res://scenes/Levels/level.tscn"
|
||||||
|
target_spawn_name = &"RepoBotReturnSpawn"
|
||||||
|
|
||||||
|
[node name="Label3D" type="Label3D" parent="ReturnTeleporter"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.965926, -0.258819, 0, 0.258819, 0.965926, 0, 5.4, 0)
|
||||||
|
billboard = 1
|
||||||
|
no_depth_test = true
|
||||||
|
pixel_size = 0.012
|
||||||
|
text = "PLAYGROUND"
|
||||||
|
|
||||||
|
[node name="PauseMenu" parent="." instance=ExtResource("6_pause_menu")]
|
||||||
|
|
||||||
|
[node name="PhoneUI" type="CanvasLayer" parent="."]
|
||||||
|
layer = 5
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="Control" type="Control" parent="PhoneUI"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="PhoneFrame" type="ColorRect" parent="PhoneUI/Control"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -180.0
|
||||||
|
offset_top = -320.0
|
||||||
|
offset_right = 180.0
|
||||||
|
offset_bottom = 320.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
color = Color(0.08, 0.08, 0.1, 1)
|
||||||
|
|
||||||
|
[node name="QuestTitle" type="Label" parent="PhoneUI/Control/PhoneFrame"]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 18.0
|
||||||
|
offset_top = 18.0
|
||||||
|
offset_right = 150.0
|
||||||
|
offset_bottom = 41.0
|
||||||
|
text = "Quest Log"
|
||||||
|
|
||||||
|
[node name="QuestText" type="RichTextLabel" parent="PhoneUI/Control/PhoneFrame"]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 18.0
|
||||||
|
offset_top = 52.0
|
||||||
|
offset_right = 344.0
|
||||||
|
offset_bottom = 613.0
|
||||||
|
bbcode_enabled = true
|
||||||
|
text = "No active quest."
|
||||||
|
scroll_active = false
|
||||||
|
|
||||||
|
[node name="RadialCommandMenu" parent="PhoneUI/Control" instance=ExtResource("8_radial_menu")]
|
||||||
|
layout_mode = 1
|
||||||
@ -6,12 +6,14 @@ extends Node3D
|
|||||||
|
|
||||||
@onready var _player: RigidBody3D = get_node_or_null("Player") as RigidBody3D
|
@onready var _player: RigidBody3D = get_node_or_null("Player") as RigidBody3D
|
||||||
@onready var _sun: DirectionalLight3D = $DirectionalLight3D
|
@onready var _sun: DirectionalLight3D = $DirectionalLight3D
|
||||||
|
@onready var _quest_text: RichTextLabel = get_node_or_null("PhoneUI/Control/PhoneFrame/QuestText") as RichTextLabel
|
||||||
|
|
||||||
var _time := 0.0
|
var _time := 0.0
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_move_player_to_spawn()
|
_move_player_to_spawn()
|
||||||
|
_setup_quest_ui()
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
@ -21,11 +23,24 @@ func _process(delta: float) -> void:
|
|||||||
func _move_player_to_spawn() -> void:
|
func _move_player_to_spawn() -> void:
|
||||||
if _player == null:
|
if _player == null:
|
||||||
return
|
return
|
||||||
|
var spawn_marker := _consume_spawn_marker()
|
||||||
|
if spawn_marker != null:
|
||||||
|
_player.call("teleport_to_spawn", spawn_marker.global_transform)
|
||||||
|
else:
|
||||||
_player.global_position = player_spawn_position
|
_player.global_position = player_spawn_position
|
||||||
_player.linear_velocity = Vector3.ZERO
|
_player.linear_velocity = Vector3.ZERO
|
||||||
_player.angular_velocity = Vector3.ZERO
|
_player.angular_velocity = Vector3.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
func _consume_spawn_marker() -> Node3D:
|
||||||
|
if TeleportState == null:
|
||||||
|
return null
|
||||||
|
var spawn_name: StringName = TeleportState.consume_spawn_name()
|
||||||
|
if spawn_name == StringName():
|
||||||
|
return null
|
||||||
|
return find_child(String(spawn_name), true, false) as Node3D
|
||||||
|
|
||||||
|
|
||||||
func _update_day_night(delta: float) -> void:
|
func _update_day_night(delta: float) -> void:
|
||||||
if _sun == null or day_length <= 0.0:
|
if _sun == null or day_length <= 0.0:
|
||||||
return
|
return
|
||||||
@ -35,3 +50,28 @@ func _update_day_night(delta: float) -> void:
|
|||||||
_sun.rotation_degrees.x = angle
|
_sun.rotation_degrees.x = angle
|
||||||
var energy_curve: float = -sin((t * TAU) + (start_light_angle * PI / 180.0))
|
var energy_curve: float = -sin((t * TAU) + (start_light_angle * PI / 180.0))
|
||||||
_sun.light_energy = clamp((energy_curve * 1.0) + 0.2, 0.0, 1.2)
|
_sun.light_energy = clamp((energy_curve * 1.0) + 0.2, 0.0, 1.2)
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_quest_ui() -> void:
|
||||||
|
if QuestManager == null:
|
||||||
|
return
|
||||||
|
if not QuestManager.is_connected("quest_state_changed", Callable(self, "_refresh_quest_ui")):
|
||||||
|
QuestManager.quest_state_changed.connect(_refresh_quest_ui)
|
||||||
|
_refresh_quest_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_quest_ui() -> void:
|
||||||
|
if _quest_text == null or QuestManager == null:
|
||||||
|
return
|
||||||
|
var state: Dictionary = QuestManager.get_active_quest_state()
|
||||||
|
if not bool(state.get("active", false)):
|
||||||
|
_quest_text.text = "No active quest."
|
||||||
|
return
|
||||||
|
var title := String(state.get("title", "Quest"))
|
||||||
|
if bool(state.get("completed", false)):
|
||||||
|
_quest_text.text = "[b]%s[/b]\nComplete." % title
|
||||||
|
return
|
||||||
|
var step_index: int = int(state.get("current_step_index", 0))
|
||||||
|
var total_steps: int = int(state.get("total_steps", 0))
|
||||||
|
var step_text := String(state.get("current_step_text", ""))
|
||||||
|
_quest_text.text = "[b]%s[/b]\nStep %d/%d\n%s" % [title, step_index + 1, total_steps, step_text]
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
[gd_scene load_steps=9 format=3 uid="uid://b7p7k1i4t0m2l"]
|
[gd_scene load_steps=15 format=3 uid="uid://b7p7k1i4t0m2l"]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://scenes/Levels/transportation_level.gd" id="1_6y4q1"]
|
[ext_resource type="Script" path="res://scenes/Levels/transportation_level.gd" id="1_6y4q1"]
|
||||||
[ext_resource type="Script" path="res://scenes/player.gd" id="2_player"]
|
[ext_resource type="Script" path="res://scenes/player.gd" id="2_player"]
|
||||||
@ -6,6 +6,10 @@
|
|||||||
[ext_resource type="PackedScene" path="res://scenes/Interaction/prototype_gateway.tscn" id="4_teleporter"]
|
[ext_resource type="PackedScene" path="res://scenes/Interaction/prototype_gateway.tscn" id="4_teleporter"]
|
||||||
[ext_resource type="Material" path="res://assets/materials/kenney_prototype_ground_green.tres" id="5_ground_mat"]
|
[ext_resource type="Material" path="res://assets/materials/kenney_prototype_ground_green.tres" id="5_ground_mat"]
|
||||||
[ext_resource type="PackedScene" uid="uid://dp6jk0k3o4v1u" path="res://scenes/UI/pause_menu.tscn" id="6_pause_menu"]
|
[ext_resource type="PackedScene" uid="uid://dp6jk0k3o4v1u" path="res://scenes/UI/pause_menu.tscn" id="6_pause_menu"]
|
||||||
|
[ext_resource type="PackedScene" path="res://scenes/Vehicles/car.tscn" id="7_car"]
|
||||||
|
[ext_resource type="Script" uid="uid://cshtdpjp4xy2f" path="res://scenes/Quests/quest_trigger_area.gd" id="8_qtrigger"]
|
||||||
|
[ext_resource type="Material" path="res://assets/materials/kenney_prototype_prop_red.tres" id="9_checkpoint_mat"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://bnwpu7p8sbsfa" path="res://scenes/Interaction/RadialCommandMenu.tscn" id="10_radial_menu"]
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_player"]
|
[sub_resource type="SphereShape3D" id="SphereShape3D_player"]
|
||||||
|
|
||||||
@ -16,6 +20,13 @@ size = Vector3(1080, 2, 1080)
|
|||||||
material = ExtResource("5_ground_mat")
|
material = ExtResource("5_ground_mat")
|
||||||
size = Vector3(1080, 2, 1080)
|
size = Vector3(1080, 2, 1080)
|
||||||
|
|
||||||
|
[sub_resource type="SphereShape3D" id="SphereShape3D_checkpoint"]
|
||||||
|
|
||||||
|
[sub_resource type="SphereMesh" id="SphereMesh_checkpoint"]
|
||||||
|
material = ExtResource("9_checkpoint_mat")
|
||||||
|
radius = 1.2
|
||||||
|
height = 2.4
|
||||||
|
|
||||||
[node name="TransportationLevel" type="Node3D"]
|
[node name="TransportationLevel" type="Node3D"]
|
||||||
script = ExtResource("1_6y4q1")
|
script = ExtResource("1_6y4q1")
|
||||||
|
|
||||||
@ -33,6 +44,7 @@ mesh = SubResource("BoxMesh_ground")
|
|||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
|
||||||
script = ExtResource("2_player")
|
script = ExtResource("2_player")
|
||||||
camera_path = NodePath("Camera3D")
|
camera_path = NodePath("Camera3D")
|
||||||
|
phone_path = NodePath("../PhoneUI")
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
|
||||||
@ -56,8 +68,81 @@ spot_angle = 25.0
|
|||||||
transform = Transform3D(1, 0, 0, 0, 0.819152, 0.573576, 0, -0.573576, 0.819152, 0, 6, 0)
|
transform = Transform3D(1, 0, 0, 0, 0.819152, 0.573576, 0, -0.573576, 0.819152, 0, 6, 0)
|
||||||
shadow_enabled = true
|
shadow_enabled = true
|
||||||
|
|
||||||
|
[node name="EntrySpawn" type="Marker3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2)
|
||||||
|
|
||||||
|
[node name="Car" parent="." instance=ExtResource("7_car")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -6, 0, -3)
|
||||||
|
|
||||||
|
[node name="QuestCheckpoint" type="Area3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 9, 0, -10)
|
||||||
|
script = ExtResource("8_qtrigger")
|
||||||
|
event_name = &"reach_checkpoint"
|
||||||
|
target_group = &"vehicle"
|
||||||
|
quest_id_filter = "first_drive"
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="QuestCheckpoint"]
|
||||||
|
shape = SubResource("SphereShape3D_checkpoint")
|
||||||
|
|
||||||
|
[node name="Visual" type="MeshInstance3D" parent="QuestCheckpoint"]
|
||||||
|
mesh = SubResource("SphereMesh_checkpoint")
|
||||||
|
|
||||||
[node name="ReturnTeleporter" parent="." instance=ExtResource("4_teleporter")]
|
[node name="ReturnTeleporter" parent="." instance=ExtResource("4_teleporter")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5, 0, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.5, 0, 0)
|
||||||
target_scene_path = "res://scenes/Levels/level.tscn"
|
target_scene_path = "res://scenes/Levels/level.tscn"
|
||||||
|
target_spawn_name = &"CarReturnSpawn"
|
||||||
|
|
||||||
|
[node name="Label3D" type="Label3D" parent="ReturnTeleporter"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.965926, -0.258819, 0, 0.258819, 0.965926, 0, 5.4, 0)
|
||||||
|
pixel_size = 0.012
|
||||||
|
text = "PLAYGROUND"
|
||||||
|
|
||||||
[node name="PauseMenu" parent="." instance=ExtResource("6_pause_menu")]
|
[node name="PauseMenu" parent="." instance=ExtResource("6_pause_menu")]
|
||||||
|
|
||||||
|
[node name="PhoneUI" type="CanvasLayer" parent="."]
|
||||||
|
layer = 5
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="Control" type="Control" parent="PhoneUI"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="PhoneFrame" type="ColorRect" parent="PhoneUI/Control"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -180.0
|
||||||
|
offset_top = -320.0
|
||||||
|
offset_right = 180.0
|
||||||
|
offset_bottom = 320.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
color = Color(0.08, 0.08, 0.1, 1)
|
||||||
|
|
||||||
|
[node name="QuestTitle" type="Label" parent="PhoneUI/Control/PhoneFrame"]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 18.0
|
||||||
|
offset_top = 18.0
|
||||||
|
offset_right = 150.0
|
||||||
|
offset_bottom = 41.0
|
||||||
|
text = "Quest Log"
|
||||||
|
|
||||||
|
[node name="QuestText" type="RichTextLabel" parent="PhoneUI/Control/PhoneFrame"]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 18.0
|
||||||
|
offset_top = 52.0
|
||||||
|
offset_right = 344.0
|
||||||
|
offset_bottom = 613.0
|
||||||
|
bbcode_enabled = true
|
||||||
|
text = "No active quest."
|
||||||
|
scroll_active = false
|
||||||
|
|
||||||
|
[node name="RadialCommandMenu" parent="PhoneUI/Control" instance=ExtResource("10_radial_menu")]
|
||||||
|
layout_mode = 1
|
||||||
|
|||||||
121
game/scenes/Levels/trigger_zones_level.gd
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
@export var player_spawn_position := Vector3(0.0, 0.0, 0.0)
|
||||||
|
@export var day_length := 120.0
|
||||||
|
@export var start_light_angle := -90.0
|
||||||
|
|
||||||
|
@onready var _player: RigidBody3D = get_node_or_null("Player") as RigidBody3D
|
||||||
|
@onready var _sun: DirectionalLight3D = $DirectionalLight3D
|
||||||
|
@onready var _quest_text: RichTextLabel = get_node_or_null("PhoneUI/Control/PhoneFrame/QuestText") as RichTextLabel
|
||||||
|
|
||||||
|
const FIRST_QUEST_ID := "first_drive"
|
||||||
|
const QUEST_PROMPT_META_PREFIX := "quest_intro_prompt_shown_"
|
||||||
|
const FIRST_QUEST := {
|
||||||
|
"id": FIRST_QUEST_ID,
|
||||||
|
"title": "RepoBot's First Task",
|
||||||
|
"description": "Get familiar with movement and vehicles.",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": "enter_car_feature",
|
||||||
|
"text": "Walk through the car teleporter.",
|
||||||
|
"complete_event": "entered_car_feature",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "reach_checkpoint",
|
||||||
|
"text": "Drive the car into the checkpoint marker.",
|
||||||
|
"complete_event": "reach_checkpoint",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
var _time := 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_move_player_to_spawn()
|
||||||
|
_setup_quests()
|
||||||
|
_show_quest_intro_dialog()
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
_update_day_night(delta)
|
||||||
|
|
||||||
|
|
||||||
|
func _move_player_to_spawn() -> void:
|
||||||
|
if _player == null:
|
||||||
|
return
|
||||||
|
var spawn_marker := _consume_spawn_marker()
|
||||||
|
if spawn_marker != null:
|
||||||
|
_player.call("teleport_to_spawn", spawn_marker.global_transform)
|
||||||
|
else:
|
||||||
|
_player.global_position = player_spawn_position
|
||||||
|
_player.linear_velocity = Vector3.ZERO
|
||||||
|
_player.angular_velocity = Vector3.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
func _consume_spawn_marker() -> Node3D:
|
||||||
|
if TeleportState == null:
|
||||||
|
return null
|
||||||
|
var spawn_name: StringName = TeleportState.consume_spawn_name()
|
||||||
|
if spawn_name == StringName():
|
||||||
|
return null
|
||||||
|
return find_child(String(spawn_name), true, false) as Node3D
|
||||||
|
|
||||||
|
|
||||||
|
func _update_day_night(delta: float) -> void:
|
||||||
|
if _sun == null or day_length <= 0.0:
|
||||||
|
return
|
||||||
|
_time = fmod(_time + delta, day_length)
|
||||||
|
var t: float = _time / day_length
|
||||||
|
var angle: float = lerp(start_light_angle, start_light_angle + 360.0, t)
|
||||||
|
_sun.rotation_degrees.x = angle
|
||||||
|
var energy_curve: float = -sin((t * TAU) + (start_light_angle * PI / 180.0))
|
||||||
|
_sun.light_energy = clamp((energy_curve * 1.0) + 0.2, 0.0, 1.2)
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_quests() -> void:
|
||||||
|
if QuestManager == null:
|
||||||
|
return
|
||||||
|
if not QuestManager.has_quest(FIRST_QUEST_ID):
|
||||||
|
QuestManager.register_quest(FIRST_QUEST)
|
||||||
|
if not QuestManager.is_active_quest(FIRST_QUEST_ID) and not QuestManager.is_quest_completed(FIRST_QUEST_ID):
|
||||||
|
QuestManager.start_quest(FIRST_QUEST_ID)
|
||||||
|
if not QuestManager.is_connected("quest_state_changed", Callable(self, "_refresh_quest_ui")):
|
||||||
|
QuestManager.quest_state_changed.connect(_refresh_quest_ui)
|
||||||
|
_refresh_quest_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_quest_ui() -> void:
|
||||||
|
if _quest_text == null or QuestManager == null:
|
||||||
|
return
|
||||||
|
var state: Dictionary = QuestManager.get_active_quest_state()
|
||||||
|
if not bool(state.get("active", false)):
|
||||||
|
_quest_text.text = "No active quest."
|
||||||
|
return
|
||||||
|
var title := String(state.get("title", "Quest"))
|
||||||
|
if bool(state.get("completed", false)):
|
||||||
|
_quest_text.text = "[b]%s[/b]\nComplete." % title
|
||||||
|
return
|
||||||
|
var step_index: int = int(state.get("current_step_index", 0))
|
||||||
|
var total_steps: int = int(state.get("total_steps", 0))
|
||||||
|
var step_text := String(state.get("current_step_text", ""))
|
||||||
|
_quest_text.text = "[b]%s[/b]\nStep %d/%d\n%s" % [title, step_index + 1, total_steps, step_text]
|
||||||
|
|
||||||
|
|
||||||
|
func _show_quest_intro_dialog() -> void:
|
||||||
|
if QuestManager == null:
|
||||||
|
return
|
||||||
|
var state: Dictionary = QuestManager.get_active_quest_state()
|
||||||
|
if not bool(state.get("active", false)) or bool(state.get("completed", false)):
|
||||||
|
return
|
||||||
|
var quest_id := String(state.get("quest_id", "")).strip_edges()
|
||||||
|
var step_id := String(state.get("current_step_id", "")).strip_edges()
|
||||||
|
var step_text := String(state.get("current_step_text", ""))
|
||||||
|
if quest_id.is_empty() or step_id.is_empty() or step_text.is_empty():
|
||||||
|
return
|
||||||
|
var prompt_key := "%s%s_%s" % [QUEST_PROMPT_META_PREFIX, quest_id, step_id]
|
||||||
|
if QuestManager.has_meta(prompt_key) and bool(QuestManager.get_meta(prompt_key)):
|
||||||
|
return
|
||||||
|
if DialogSystem and DialogSystem.has_method("show_text"):
|
||||||
|
DialogSystem.show_text("RepoBot: New task assigned.\n\n%s" % step_text)
|
||||||
|
QuestManager.set_meta(prompt_key, true)
|
||||||
1
game/scenes/Levels/trigger_zones_level.gd.uid
Normal file
@ -0,0 +1 @@
|
|||||||
|
uid://br28dq2hwo7sc
|
||||||
177
game/scenes/Levels/trigger_zones_level.tscn
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
[gd_scene load_steps=14 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://scenes/Levels/trigger_zones_level.gd" id="1_level"]
|
||||||
|
[ext_resource type="Script" path="res://scenes/player.gd" id="2_player"]
|
||||||
|
[ext_resource type="PackedScene" path="res://assets/models/TestCharAnimated.glb" id="3_model"]
|
||||||
|
[ext_resource type="PackedScene" path="res://scenes/Interaction/prototype_gateway.tscn" id="4_teleporter"]
|
||||||
|
[ext_resource type="Material" path="res://assets/materials/kenney_prototype_ground_green.tres" id="5_ground_mat"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dp6jk0k3o4v1u" path="res://scenes/UI/pause_menu.tscn" id="6_pause_menu"]
|
||||||
|
[ext_resource type="Script" uid="uid://bk53njt7i3kmv" path="res://scenes/Interaction/dialog_trigger_area.gd" id="7_dialog"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://bnwpu7p8sbsfa" path="res://scenes/Interaction/RadialCommandMenu.tscn" id="8_radial_menu"]
|
||||||
|
|
||||||
|
[sub_resource type="SphereShape3D" id="SphereShape3D_player"]
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_ground"]
|
||||||
|
size = Vector3(1080, 2, 1080)
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_ground"]
|
||||||
|
material = ExtResource("5_ground_mat")
|
||||||
|
size = Vector3(1080, 2, 1080)
|
||||||
|
|
||||||
|
[sub_resource type="SphereShape3D" id="SphereShape3D_dialog_zone"]
|
||||||
|
radius = 2.5
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_dialog_zone"]
|
||||||
|
transparency = 1
|
||||||
|
cull_mode = 2
|
||||||
|
shading_mode = 0
|
||||||
|
albedo_color = Color(0.2, 0.8, 0.35, 0.18)
|
||||||
|
|
||||||
|
[sub_resource type="SphereMesh" id="SphereMesh_dialog_zone"]
|
||||||
|
material = SubResource("StandardMaterial3D_dialog_zone")
|
||||||
|
radius = 2.5
|
||||||
|
height = 5.0
|
||||||
|
|
||||||
|
[node name="TriggerZonesLevel" type="Node3D"]
|
||||||
|
script = ExtResource("1_level")
|
||||||
|
|
||||||
|
[node name="Ground" type="StaticBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Ground"]
|
||||||
|
shape = SubResource("BoxShape3D_ground")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="Ground"]
|
||||||
|
mesh = SubResource("BoxMesh_ground")
|
||||||
|
|
||||||
|
[node name="Player" type="RigidBody3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
|
||||||
|
script = ExtResource("2_player")
|
||||||
|
camera_path = NodePath("Camera3D")
|
||||||
|
phone_path = NodePath("../PhoneUI")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Player"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
|
||||||
|
shape = SubResource("SphereShape3D_player")
|
||||||
|
|
||||||
|
[node name="TestCharAnimated" parent="Player" instance=ExtResource("3_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="Player"]
|
||||||
|
transform = Transform3D(0.9989785, -4.651856e-10, -0.045188628, 0.006969331, 0.9880354, 0.15407, 0.044647958, -0.15422754, 0.9870261, 0.22036135, 1.8988357, 0.64972365)
|
||||||
|
current = true
|
||||||
|
fov = 49.0
|
||||||
|
|
||||||
|
[node name="SpotLight3D" type="SpotLight3D" parent="Player"]
|
||||||
|
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
|
||||||
|
|
||||||
|
[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="EntrySpawn" type="Marker3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2)
|
||||||
|
|
||||||
|
[node name="ManualDialogZone" type="Area3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4, 0, -3)
|
||||||
|
script = ExtResource("7_dialog")
|
||||||
|
prompt_text = "Press E to inspect area"
|
||||||
|
dialog_text = "Manual dialog trigger zone"
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="ManualDialogZone"]
|
||||||
|
shape = SubResource("SphereShape3D_dialog_zone")
|
||||||
|
|
||||||
|
[node name="Visual" type="MeshInstance3D" parent="ManualDialogZone"]
|
||||||
|
mesh = SubResource("SphereMesh_dialog_zone")
|
||||||
|
|
||||||
|
[node name="Label3D" type="Label3D" parent="ManualDialogZone"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.965926, -0.258819, 0, 0.258819, 0.965926, 0, 3.0, 0)
|
||||||
|
billboard = 1
|
||||||
|
no_depth_test = true
|
||||||
|
pixel_size = 0.012
|
||||||
|
text = "PRESS E"
|
||||||
|
|
||||||
|
[node name="AutoDialogZone" type="Area3D" parent="."]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4, 0, -3)
|
||||||
|
script = ExtResource("7_dialog")
|
||||||
|
dialog_text = "Auto dialog trigger zone"
|
||||||
|
auto_popup = true
|
||||||
|
auto_popup_close_on_exit = true
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="AutoDialogZone"]
|
||||||
|
shape = SubResource("SphereShape3D_dialog_zone")
|
||||||
|
|
||||||
|
[node name="Visual" type="MeshInstance3D" parent="AutoDialogZone"]
|
||||||
|
mesh = SubResource("SphereMesh_dialog_zone")
|
||||||
|
|
||||||
|
[node name="Label3D" type="Label3D" parent="AutoDialogZone"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.965926, -0.258819, 0, 0.258819, 0.965926, 0, 3.0, 0)
|
||||||
|
billboard = 1
|
||||||
|
no_depth_test = true
|
||||||
|
pixel_size = 0.012
|
||||||
|
text = "AUTO"
|
||||||
|
|
||||||
|
[node name="ReturnTeleporter" parent="." instance=ExtResource("4_teleporter")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 5.5)
|
||||||
|
target_scene_path = "res://scenes/Levels/level.tscn"
|
||||||
|
target_spawn_name = &"TriggerZonesReturnSpawn"
|
||||||
|
|
||||||
|
[node name="Label3D" type="Label3D" parent="ReturnTeleporter"]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 0.965926, -0.258819, 0, 0.258819, 0.965926, 0, 5.4, 0)
|
||||||
|
billboard = 1
|
||||||
|
no_depth_test = true
|
||||||
|
pixel_size = 0.012
|
||||||
|
text = "PLAYGROUND"
|
||||||
|
|
||||||
|
[node name="PauseMenu" parent="." instance=ExtResource("6_pause_menu")]
|
||||||
|
|
||||||
|
[node name="PhoneUI" type="CanvasLayer" parent="."]
|
||||||
|
layer = 5
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="Control" type="Control" parent="PhoneUI"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="PhoneFrame" type="ColorRect" parent="PhoneUI/Control"]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 8
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
offset_left = -180.0
|
||||||
|
offset_top = -320.0
|
||||||
|
offset_right = 180.0
|
||||||
|
offset_bottom = 320.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
color = Color(0.08, 0.08, 0.1, 1)
|
||||||
|
|
||||||
|
[node name="QuestTitle" type="Label" parent="PhoneUI/Control/PhoneFrame"]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 18.0
|
||||||
|
offset_top = 18.0
|
||||||
|
offset_right = 150.0
|
||||||
|
offset_bottom = 41.0
|
||||||
|
text = "Quest Log"
|
||||||
|
|
||||||
|
[node name="QuestText" type="RichTextLabel" parent="PhoneUI/Control/PhoneFrame"]
|
||||||
|
layout_mode = 0
|
||||||
|
offset_left = 18.0
|
||||||
|
offset_top = 52.0
|
||||||
|
offset_right = 344.0
|
||||||
|
offset_bottom = 613.0
|
||||||
|
bbcode_enabled = true
|
||||||
|
text = "No active quest."
|
||||||
|
scroll_active = false
|
||||||
|
|
||||||
|
[node name="RadialCommandMenu" parent="PhoneUI/Control" instance=ExtResource("8_radial_menu")]
|
||||||
|
layout_mode = 1
|
||||||
@ -271,6 +271,18 @@ func exit_vehicle(exit_point: Node3D, vehicle_camera: Camera3D) -> void:
|
|||||||
vehicle_exited.emit(null)
|
vehicle_exited.emit(null)
|
||||||
|
|
||||||
|
|
||||||
|
func teleport_to_spawn(spawn_transform: Transform3D) -> void:
|
||||||
|
global_transform = spawn_transform
|
||||||
|
linear_velocity = Vector3.ZERO
|
||||||
|
angular_velocity = Vector3.ZERO
|
||||||
|
sleeping = false
|
||||||
|
_camera_yaw = global_transform.basis.get_euler().y
|
||||||
|
if cam:
|
||||||
|
var target_basis := Basis(Vector3.UP, _camera_yaw)
|
||||||
|
cam.global_position = global_position + (target_basis * _camera_offset_local)
|
||||||
|
cam.global_rotation = Vector3(_camera_pitch, _camera_yaw, 0.0)
|
||||||
|
|
||||||
|
|
||||||
func is_in_vehicle() -> bool:
|
func is_in_vehicle() -> bool:
|
||||||
return _in_vehicle
|
return _in_vehicle
|
||||||
|
|
||||||
|
|||||||
@ -89,6 +89,7 @@ public class CraftingController : ControllerBase
|
|||||||
CraftingStore.CraftStatus.RecipeNotFound => NotFound("Recipe not found"),
|
CraftingStore.CraftStatus.RecipeNotFound => NotFound("Recipe not found"),
|
||||||
CraftingStore.CraftStatus.MissingStation => Conflict("Required crafting station is not available"),
|
CraftingStore.CraftStatus.MissingStation => Conflict("Required crafting station is not available"),
|
||||||
CraftingStore.CraftStatus.MissingInputs => Conflict(new { missingRequirements = result.MissingRequirements }),
|
CraftingStore.CraftStatus.MissingInputs => Conflict(new { missingRequirements = result.MissingRequirements }),
|
||||||
|
CraftingStore.CraftStatus.InventoryFull => Conflict(new { missingRequirements = result.MissingRequirements }),
|
||||||
CraftingStore.CraftStatus.InvalidRecipe => BadRequest(new { missingRequirements = result.MissingRequirements }),
|
CraftingStore.CraftStatus.InvalidRecipe => BadRequest(new { missingRequirements = result.MissingRequirements }),
|
||||||
_ => Ok(new CraftRecipeResponse
|
_ => Ok(new CraftRecipeResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@ -99,7 +99,7 @@ public class CraftingStore
|
|||||||
{
|
{
|
||||||
var recipes = await _recipes.Find(r => r.Enabled).SortBy(r => r.Category).ThenBy(r => r.Name).ToListAsync();
|
var recipes = await _recipes.Find(r => r.Enabled).SortBy(r => r.Category).ThenBy(r => r.Name).ToListAsync();
|
||||||
var items = await GetCharacterItemsAsync(character.CharacterId);
|
var items = await GetCharacterItemsAsync(character.CharacterId);
|
||||||
var itemTotals = items.GroupBy(i => i.ItemKey).ToDictionary(g => g.Key, g => g.Sum(x => x.Quantity), StringComparer.Ordinal);
|
var itemTotals = GetCraftableItemTotals(items);
|
||||||
var location = await GetLocationAtCoordAsync(character.CoordX, character.CoordY);
|
var location = await GetLocationAtCoordAsync(character.CoordX, character.CoordY);
|
||||||
|
|
||||||
return recipes.Select(recipe =>
|
return recipes.Select(recipe =>
|
||||||
@ -126,7 +126,7 @@ public class CraftingStore
|
|||||||
return new CraftAttemptResult { Status = CraftStatus.MissingStation };
|
return new CraftAttemptResult { Status = CraftStatus.MissingStation };
|
||||||
|
|
||||||
var items = await GetCharacterItemsAsync(character.CharacterId);
|
var items = await GetCharacterItemsAsync(character.CharacterId);
|
||||||
var totals = items.GroupBy(i => i.ItemKey).ToDictionary(g => g.Key, g => g.Sum(x => x.Quantity), StringComparer.Ordinal);
|
var totals = GetCraftableItemTotals(items);
|
||||||
var missing = GetMissingRequirements(recipe, totals, location, craftCount);
|
var missing = GetMissingRequirements(recipe, totals, location, craftCount);
|
||||||
if (missing.Count > 0)
|
if (missing.Count > 0)
|
||||||
return new CraftAttemptResult { Status = CraftStatus.MissingInputs, MissingRequirements = missing };
|
return new CraftAttemptResult { Status = CraftStatus.MissingInputs, MissingRequirements = missing };
|
||||||
@ -139,6 +139,10 @@ public class CraftingStore
|
|||||||
return new CraftAttemptResult { Status = CraftStatus.InvalidRecipe, MissingRequirements = [$"Missing item definition for output '{output.ItemKey}'"] };
|
return new CraftAttemptResult { Status = CraftStatus.InvalidRecipe, MissingRequirements = [$"Missing item definition for output '{output.ItemKey}'"] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var capacityError = GetOutputCapacityError(recipe, craftCount, items, definitionMap);
|
||||||
|
if (capacityError is not null)
|
||||||
|
return new CraftAttemptResult { Status = CraftStatus.InventoryFull, MissingRequirements = [capacityError] };
|
||||||
|
|
||||||
foreach (var input in recipe.Inputs)
|
foreach (var input in recipe.Inputs)
|
||||||
await ConsumeItemKeyAsync(character, input.ItemKey, input.Quantity * craftCount);
|
await ConsumeItemKeyAsync(character, input.ItemKey, input.Quantity * craftCount);
|
||||||
|
|
||||||
@ -157,6 +161,12 @@ public class CraftingStore
|
|||||||
private async Task<List<InventoryItemDocument>> GetCharacterItemsAsync(string characterId) =>
|
private async Task<List<InventoryItemDocument>> GetCharacterItemsAsync(string characterId) =>
|
||||||
await _items.Find(i => i.OwnerType == CharacterOwnerType && i.OwnerId == characterId).SortBy(i => i.Slot).ThenBy(i => i.ItemKey).ToListAsync();
|
await _items.Find(i => i.OwnerType == CharacterOwnerType && i.OwnerId == characterId).SortBy(i => i.Slot).ThenBy(i => i.ItemKey).ToListAsync();
|
||||||
|
|
||||||
|
private static Dictionary<string, int> GetCraftableItemTotals(IEnumerable<InventoryItemDocument> items) =>
|
||||||
|
items
|
||||||
|
.Where(i => i.EquippedSlot is null)
|
||||||
|
.GroupBy(i => i.ItemKey)
|
||||||
|
.ToDictionary(g => g.Key, g => g.Sum(x => x.Quantity), StringComparer.Ordinal);
|
||||||
|
|
||||||
private async Task<LocationDocument?> GetLocationAtCoordAsync(int x, int y) =>
|
private async Task<LocationDocument?> GetLocationAtCoordAsync(int x, int y) =>
|
||||||
await _locations.Find(l => l.Coord.X == x && l.Coord.Y == y).FirstOrDefaultAsync();
|
await _locations.Find(l => l.Coord.X == x && l.Coord.Y == y).FirstOrDefaultAsync();
|
||||||
|
|
||||||
@ -216,6 +226,74 @@ public class CraftingStore
|
|||||||
return string.Equals(recipe.StationType, stationType, StringComparison.Ordinal);
|
return string.Equals(recipe.StationType, stationType, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? GetOutputCapacityError(CraftingRecipe recipe, int craftCount, List<InventoryItemDocument> currentItems, IReadOnlyDictionary<string, ItemDefinitionDocument> definitions)
|
||||||
|
{
|
||||||
|
var simulatedItems = currentItems
|
||||||
|
.Select(item => new SimulatedInventoryItem
|
||||||
|
{
|
||||||
|
ItemKey = item.ItemKey,
|
||||||
|
Quantity = item.Quantity,
|
||||||
|
Slot = item.Slot,
|
||||||
|
EquippedSlot = item.EquippedSlot,
|
||||||
|
CreatedUtc = item.CreatedUtc
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var input in recipe.Inputs)
|
||||||
|
SimulateConsume(simulatedItems, input.ItemKey, input.Quantity * craftCount);
|
||||||
|
|
||||||
|
var usedSlots = simulatedItems
|
||||||
|
.Where(i => i.Slot.HasValue)
|
||||||
|
.Select(i => i.Slot!.Value)
|
||||||
|
.ToHashSet();
|
||||||
|
var openSlots = Enumerable.Range(0, InventorySlotCount).Count(slot => !usedSlots.Contains(slot));
|
||||||
|
|
||||||
|
foreach (var outputGroup in recipe.Outputs.GroupBy(output => output.ItemKey, StringComparer.Ordinal))
|
||||||
|
{
|
||||||
|
var itemKey = outputGroup.Key;
|
||||||
|
var requiredQuantity = outputGroup.Sum(output => output.Quantity * craftCount);
|
||||||
|
var definition = definitions[itemKey];
|
||||||
|
|
||||||
|
if (definition.Stackable)
|
||||||
|
{
|
||||||
|
var existingSpace = simulatedItems
|
||||||
|
.Where(i => i.EquippedSlot is null && i.Slot.HasValue && i.ItemKey == itemKey)
|
||||||
|
.Sum(i => Math.Max(0, definition.MaxStackSize - i.Quantity));
|
||||||
|
var remaining = Math.Max(0, requiredQuantity - existingSpace);
|
||||||
|
var slotsNeeded = (int)Math.Ceiling(remaining / (double)Math.Max(1, definition.MaxStackSize));
|
||||||
|
if (slotsNeeded > openSlots)
|
||||||
|
return $"Not enough inventory space for output '{itemKey}'";
|
||||||
|
openSlots -= slotsNeeded;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiredQuantity > openSlots)
|
||||||
|
return $"Not enough inventory space for output '{itemKey}'";
|
||||||
|
openSlots -= requiredQuantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SimulateConsume(List<SimulatedInventoryItem> items, string itemKey, int quantity)
|
||||||
|
{
|
||||||
|
var remaining = quantity;
|
||||||
|
foreach (var item in items
|
||||||
|
.Where(i => i.EquippedSlot is null && i.ItemKey == itemKey)
|
||||||
|
.OrderBy(i => i.Slot)
|
||||||
|
.ThenBy(i => i.CreatedUtc))
|
||||||
|
{
|
||||||
|
if (remaining <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var consumed = Math.Min(remaining, item.Quantity);
|
||||||
|
item.Quantity -= consumed;
|
||||||
|
remaining -= consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.RemoveAll(i => i.Quantity <= 0);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ConsumeItemKeyAsync(CharacterAccessResult character, string itemKey, int quantity)
|
private async Task ConsumeItemKeyAsync(CharacterAccessResult character, string itemKey, int quantity)
|
||||||
{
|
{
|
||||||
var remaining = quantity;
|
var remaining = quantity;
|
||||||
@ -302,7 +380,7 @@ public class CraftingStore
|
|||||||
{
|
{
|
||||||
var items = await GetCharacterItemsAsync(characterId);
|
var items = await GetCharacterItemsAsync(characterId);
|
||||||
var used = items.Where(i => i.Slot.HasValue).Select(i => i.Slot!.Value).ToHashSet();
|
var used = items.Where(i => i.Slot.HasValue).Select(i => i.Slot!.Value).ToHashSet();
|
||||||
for (var slot = 0; slot < 6; slot++)
|
for (var slot = 0; slot < InventorySlotCount; slot++)
|
||||||
{
|
{
|
||||||
if (!used.Contains(slot))
|
if (!used.Contains(slot))
|
||||||
return slot;
|
return slot;
|
||||||
@ -344,7 +422,23 @@ public class CraftingStore
|
|||||||
RecipeNotFound,
|
RecipeNotFound,
|
||||||
MissingInputs,
|
MissingInputs,
|
||||||
MissingStation,
|
MissingStation,
|
||||||
InvalidRecipe
|
InvalidRecipe,
|
||||||
|
InventoryFull
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int InventorySlotCount = 6;
|
||||||
|
|
||||||
|
private class SimulatedInventoryItem
|
||||||
|
{
|
||||||
|
public string ItemKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int Quantity { get; set; }
|
||||||
|
|
||||||
|
public int? Slot { get; set; }
|
||||||
|
|
||||||
|
public string? EquippedSlot { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedUtc { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[BsonIgnoreExtraElements]
|
[BsonIgnoreExtraElements]
|
||||||
|
|||||||