A raging quest

This commit is contained in:
Zeeshaun 2026-02-25 19:46:10 -06:00
parent 2670392e2d
commit bf1ac4406c
9 changed files with 345 additions and 22 deletions

View File

@ -27,6 +27,7 @@ AuthState="*res://scenes/UI/auth_state.gd"
CharacterService="*res://scenes/UI/character_service.gd"
SelectedCharacter="*res://scenes/UI/selected_character.gd"
DialogSystem="*res://scenes/UI/dialog_system.gd"
QuestManager="*res://scenes/Quests/quest_manager.gd"
[dotnet]

View File

@ -1,24 +1,47 @@
extends Node3D
@export var day_length := 120.0 # seconds for full rotation
@export var start_light_angle := -90.0
@export var show_spawn_dialog := true
@export_multiline var spawn_dialog_text := "Welcome to Promiscuity.\n\nPress E to close this message."
@export var spawn_dialog_auto_close_seconds := 4.0
var end_light_angle = start_light_angle + 360.0
var start_radians = start_light_angle * PI / 180
var time := 0.0
@export var day_length := 120.0 # seconds for full rotation
@export var start_light_angle := -90.0
@export var show_spawn_dialog := true
@export_multiline var spawn_dialog_text := "Welcome to Promiscuity.\n\nPress E to close this message."
@export var spawn_dialog_auto_close_seconds := 4.0
var end_light_angle = start_light_angle + 360.0
var start_radians = start_light_angle * PI / 180
var time := 0.0
@onready var sun := $DirectionalLight3D
@onready var _player: Node = $Player
@onready var _quest_text: RichTextLabel = $PhoneUI/Control/PhoneFrame/QuestText
func _ready() -> void:
if show_spawn_dialog and DialogSystem and DialogSystem.has_method("show_text"):
await get_tree().process_frame
DialogSystem.show_text(spawn_dialog_text)
if spawn_dialog_auto_close_seconds > 0.0:
await get_tree().create_timer(spawn_dialog_auto_close_seconds).timeout
if DialogSystem and DialogSystem.has_method("close_if_text"):
DialogSystem.close_if_text(spawn_dialog_text)
const FIRST_QUEST_ID := "first_drive"
const FIRST_QUEST := {
"id": FIRST_QUEST_ID,
"title": "RepoBot's First Task",
"description": "Get familiar with movement and vehicles.",
"steps": [
{
"id": "enter_vehicle",
"text": "Get in the car (E).",
"complete_event": "entered_vehicle",
},
{
"id": "reach_checkpoint",
"text": "Drive the car into the checkpoint marker.",
"complete_event": "reach_checkpoint",
},
],
}
func _ready() -> void:
_setup_quests()
if show_spawn_dialog and DialogSystem and DialogSystem.has_method("show_text"):
await get_tree().process_frame
DialogSystem.show_text(spawn_dialog_text)
if spawn_dialog_auto_close_seconds > 0.0:
await get_tree().create_timer(spawn_dialog_auto_close_seconds).timeout
if DialogSystem and DialogSystem.has_method("close_if_text"):
DialogSystem.close_if_text(spawn_dialog_text)
_show_quest_intro_dialog()
func _process(delta):
time = fmod((time + delta), day_length)
@ -32,3 +55,53 @@ func _process(delta):
var curSin = -sin((t * TAU) + start_radians)
var energy = clamp((curSin * 1.0) + 0.2, 0.0, 1.2)
sun.light_energy = energy
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)
if _player and _player.has_signal("vehicle_entered"):
if not _player.is_connected("vehicle_entered", Callable(self, "_on_player_vehicle_entered")):
_player.vehicle_entered.connect(_on_player_vehicle_entered)
_refresh_quest_ui()
func _on_player_vehicle_entered(_vehicle: Node) -> void:
if QuestManager:
QuestManager.emit_event(&"entered_vehicle")
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 step_text := String(state.get("current_step_text", ""))
if step_text.is_empty():
return
if DialogSystem and DialogSystem.has_method("show_text"):
DialogSystem.show_text("RepoBot: New task assigned.\n\n%s" % step_text)

View File

@ -1,4 +1,4 @@
[gd_scene load_steps=61 format=3 uid="uid://dchj6g2i8ebph"]
[gd_scene load_steps=65 format=3 uid="uid://dchj6g2i8ebph"]
[ext_resource type="Script" uid="uid://brgmxhhhtakja" path="res://scenes/Levels/level.gd" id="1_a4mo8"]
[ext_resource type="PackedScene" uid="uid://bb6hj6l23043x" path="res://assets/models/human.blend" id="1_eg4yq"]
@ -9,6 +9,7 @@
[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="Script" uid="uid://bk53njt7i3kmv" path="res://scenes/Interaction/dialog_trigger_area.gd" id="6_dialog"]
[ext_resource type="Script" path="res://scenes/Quests/quest_trigger_area.gd" id="7_qtrigger"]
[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_2q6dc"]
bounce = 0.5
@ -200,6 +201,20 @@ material = SubResource("StandardMaterial3D_dialog_zone")
radius = 2.5
height = 5.0
[sub_resource type="SphereShape3D" id="SphereShape3D_checkpoint"]
radius = 3.0
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_checkpoint"]
transparency = 1
cull_mode = 2
shading_mode = 0
albedo_color = Color(1, 0.804, 0.204, 0.2)
[sub_resource type="SphereMesh" id="SphereMesh_checkpoint"]
material = SubResource("StandardMaterial3D_checkpoint")
radius = 3.0
height = 6.0
[sub_resource type="BoxShape3D" id="BoxShape3D_2q6dc"]
size = Vector3(1080, 2, 1080)
@ -287,6 +302,19 @@ shape = SubResource("SphereShape3D_dialog_zone")
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="."]
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"]
shape = SubResource("SphereShape3D_checkpoint")
[node name="Visual" type="MeshInstance3D" parent="QuestCheckpoint"]
mesh = SubResource("SphereMesh_checkpoint")
[node name="Ground" type="StaticBody3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0)
@ -377,6 +405,24 @@ 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
scroll_active = false
text = "No active quest."
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("Environment_a4mo8")

View File

@ -0,0 +1,158 @@
extends Node
signal quest_registered(quest_id: StringName)
signal quest_started(quest_id: StringName)
signal quest_step_advanced(quest_id: StringName, step_index: int, step_text: String)
signal quest_completed(quest_id: StringName)
signal quest_state_changed
var _quests: Dictionary = {}
var _active_quest_id: StringName = &""
var _active_step_index: int = -1
var _completed_quests: Dictionary = {}
func register_quest(definition: Dictionary) -> bool:
var raw_id := String(definition.get("id", "")).strip_edges()
if raw_id.is_empty():
push_warning("Quest definition is missing an id.")
return false
var quest_id: StringName = StringName(raw_id)
if _quests.has(quest_id):
return true
var raw_steps: Variant = definition.get("steps", [])
if typeof(raw_steps) != TYPE_ARRAY:
push_warning("Quest '%s' has invalid steps." % raw_id)
return false
var normalized_steps: Array[Dictionary] = []
var idx := 0
for raw_step in raw_steps:
if typeof(raw_step) != TYPE_DICTIONARY:
continue
var step_dict: Dictionary = raw_step
var step := step_dict.duplicate(true)
step["id"] = String(step.get("id", "step_%d" % idx)).strip_edges()
step["text"] = String(step.get("text", "")).strip_edges()
step["complete_event"] = StringName(String(step.get("complete_event", "")).strip_edges())
normalized_steps.append(step)
idx += 1
if normalized_steps.is_empty():
push_warning("Quest '%s' has no usable steps." % raw_id)
return false
var quest := definition.duplicate(true)
quest["id"] = raw_id
quest["title"] = String(definition.get("title", raw_id))
quest["description"] = String(definition.get("description", ""))
quest["steps"] = normalized_steps
_quests[quest_id] = quest
quest_registered.emit(quest_id)
quest_state_changed.emit()
return true
func has_quest(quest_id: String) -> bool:
return _quests.has(StringName(quest_id))
func start_quest(quest_id: String) -> bool:
var id := StringName(quest_id)
if not _quests.has(id):
push_warning("Cannot start missing quest '%s'." % quest_id)
return false
_active_quest_id = id
_active_step_index = 0
quest_started.emit(_active_quest_id)
var step := _get_active_step()
if not step.is_empty():
quest_step_advanced.emit(_active_quest_id, _active_step_index, String(step.get("text", "")))
quest_state_changed.emit()
return true
func emit_event(event_name: StringName, payload: Dictionary = {}) -> void:
if event_name == StringName():
return
if _active_quest_id == StringName():
return
if is_quest_completed(String(_active_quest_id)):
return
var active_step := _get_active_step()
if active_step.is_empty():
return
var expected: StringName = active_step.get("complete_event", StringName())
if expected != event_name:
return
_advance_active_step(payload)
func is_quest_completed(quest_id: String) -> bool:
return bool(_completed_quests.get(StringName(quest_id), false))
func is_active_quest(quest_id: String) -> bool:
return _active_quest_id == StringName(quest_id)
func get_active_quest_id() -> StringName:
return _active_quest_id
func get_active_quest_state() -> Dictionary:
if _active_quest_id == StringName() or not _quests.has(_active_quest_id):
return {
"active": false,
}
var quest: Dictionary = _quests[_active_quest_id]
var steps: Array = quest.get("steps", [])
var completed: bool = is_quest_completed(String(_active_quest_id))
var step_text := ""
var step_id := ""
if not completed and _active_step_index >= 0 and _active_step_index < steps.size():
var step: Dictionary = steps[_active_step_index]
step_text = String(step.get("text", ""))
step_id = String(step.get("id", ""))
return {
"active": true,
"quest_id": String(_active_quest_id),
"title": String(quest.get("title", "")),
"description": String(quest.get("description", "")),
"current_step_index": _active_step_index,
"total_steps": steps.size(),
"current_step_id": step_id,
"current_step_text": step_text,
"completed": completed,
}
func _get_active_step() -> Dictionary:
if _active_quest_id == StringName():
return {}
if not _quests.has(_active_quest_id):
return {}
var quest: Dictionary = _quests[_active_quest_id]
var steps: Array = quest.get("steps", [])
if _active_step_index < 0 or _active_step_index >= steps.size():
return {}
return steps[_active_step_index]
func _advance_active_step(_payload: Dictionary) -> void:
if _active_quest_id == StringName():
return
var quest: Dictionary = _quests.get(_active_quest_id, {})
var steps: Array = quest.get("steps", [])
_active_step_index += 1
if _active_step_index >= steps.size():
_completed_quests[_active_quest_id] = true
quest_completed.emit(_active_quest_id)
quest_state_changed.emit()
return
var step: Dictionary = steps[_active_step_index]
quest_step_advanced.emit(_active_quest_id, _active_step_index, String(step.get("text", "")))
quest_state_changed.emit()

View File

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

View File

@ -0,0 +1,37 @@
extends Area3D
@export var event_name: StringName = &""
@export var target_group: StringName = &"player"
@export var quest_id_filter: String = ""
@export var one_shot: bool = true
var _triggered := false
func _ready() -> void:
body_entered.connect(_on_body_entered)
func reset_trigger() -> void:
_triggered = false
set_deferred("monitoring", true)
func _on_body_entered(body: Node) -> void:
if one_shot and _triggered:
return
if target_group != StringName() and not body.is_in_group(target_group):
return
if event_name == StringName():
return
if quest_id_filter.strip_edges() != "":
if QuestManager == null or QuestManager.get_active_quest_id() != StringName(quest_id_filter):
return
if QuestManager:
QuestManager.emit_event(event_name, {
"body": body,
"source": self,
})
if one_shot:
_triggered = true
set_deferred("monitoring", false)

View File

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

View File

@ -22,6 +22,7 @@ var _nearby_driver: Node = null
var _driver: Node = null
func _ready() -> void:
add_to_group("vehicle")
if interact_area:
interact_area.collision_layer = 2
interact_area.collision_mask = 1

View File

@ -2,6 +2,8 @@ extends RigidBody3D
# Initially I used a CharacterBody3D, however, I wanted the player to bounce off
# other objects in the environment and that would have required manual handling
# of collisions. So that's why we're using a RigidBody3D instead.
signal vehicle_entered(vehicle: Node)
signal vehicle_exited(vehicle: Node)
const MOVE_SPEED := 8.0
const SPRINT_MOVE_SPEED :=13
@ -94,11 +96,6 @@ func _integrate_forces(state):
# Input as 2D vector
var input2v := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
if Input.is_action_just_pressed("player_phone"):
phone_visible = !phone_visible
if phone:
phone.visible = phone_visible
# Camera based movement
var forward := Vector3.FORWARD * -1.0
var right := Vector3.RIGHT
@ -157,6 +154,12 @@ func _integrate_forces(state):
_jump_triggered = false
func _input(event):
if event.is_action_pressed("player_phone"):
phone_visible = !phone_visible
if phone:
phone.visible = phone_visible
return
if _in_vehicle:
return
if event is InputEventMouseButton:
@ -226,6 +229,7 @@ func enter_vehicle(_vehicle: Node, seat: Node3D, vehicle_camera: Camera3D) -> vo
cam.current = false
if vehicle_camera:
vehicle_camera.current = true
vehicle_entered.emit(_vehicle)
func exit_vehicle(exit_point: Node3D, vehicle_camera: Camera3D) -> void:
_in_vehicle = false
@ -245,3 +249,4 @@ func exit_vehicle(exit_point: Node3D, vehicle_camera: Camera3D) -> void:
vehicle_camera.current = false
if cam:
cam.current = true
vehicle_exited.emit(null)