Compare commits
6 Commits
4a8d5b879e
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| cd466c73b5 | |||
| acb96933a7 | |||
| d895bc0a47 | |||
| 4f9e22f0b9 | |||
| d83e276c52 | |||
| 0bb00735b2 |
@@ -0,0 +1,13 @@
|
||||
# Instructions
|
||||
|
||||
- Never do any commits.
|
||||
- Keep answers short and concise.
|
||||
- Don't do huge comments. Make them very short and only when necessary.
|
||||
- Any comment that doesn't add more context than the code already provides should be omitted.
|
||||
- Match the comment style of the rest of the project.
|
||||
- Instead of commenting a block about what a function does, use short one-liner comments over sections of code within it. For example, where there are nested for loops and if statements, have one comment saying `# Find node based on criteria`.
|
||||
- Always check syntax with the game engine.
|
||||
- Read logs to confirm expected behavior when possible.
|
||||
|
||||
## About the project
|
||||
- All players will always play as clients. The server will be headless.
|
||||
@@ -103,10 +103,14 @@ func _populate_world_if_owner() -> void:
|
||||
layout.queue_free()
|
||||
_remove_authored(authored)
|
||||
NetworkManager.log_line("Populating world: %d stations, %d items" % [stations.size(), items.size()])
|
||||
# One frame between spawns - MultiplayerSpawner corrupts replication when
|
||||
# several nodes register for sync in the same frame (godotengine/godot#96914).
|
||||
for d in stations:
|
||||
NetworkManager.spawn_item(d["scene"], d["xform"], d["name"], d["props"])
|
||||
await get_tree().process_frame
|
||||
for d in items:
|
||||
NetworkManager.spawn_item(d["scene"], d["xform"], d["name"], d["props"])
|
||||
await get_tree().process_frame
|
||||
NetworkManager.log_line("World populated")
|
||||
|
||||
|
||||
|
||||
@@ -18,10 +18,14 @@ func _on_game_state_changed(new_state: GameManager.GameState) -> void:
|
||||
_set_stations_enabled(true)
|
||||
|
||||
|
||||
# Only Station-derived stations expose `enabled` (Table runs its own FSM).
|
||||
# Only single-snap_zone Station-derived stations use the generic `enabled`
|
||||
# toggle here. Table is Station-derived too now (SharedInventoryStation), but
|
||||
# it has multiple snap_zones and manages their enabling itself via its own
|
||||
# FSM (_set_snap_zones_enabled) - toggling only the base `snap_zone` here
|
||||
# would desync it from the rest of Table's zones, so it's excluded.
|
||||
func _set_stations_enabled(value: bool) -> void:
|
||||
for station in get_tree().get_nodes_in_group("station"):
|
||||
if station is Station:
|
||||
if station is Station and not station is Table:
|
||||
station.enabled = value
|
||||
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
[ext_resource type="Sky" uid="uid://c1abtpwhdm2d5" path="res://textures/sky/NightSkyHDRI009_16K.tres" id="5_c3xgf"]
|
||||
[ext_resource type="Script" uid="uid://fyd2rjramhbb" path="res://prefabs/kitchen_instantiator.gd" id="6_pw2j5"]
|
||||
[ext_resource type="PackedScene" uid="uid://damrxtlt7uswf" path="res://content/station_layouts/small_line.tscn" id="7_0sjqq"]
|
||||
[ext_resource type="PackedScene" uid="uid://j7caslh27nor" path="res://stations/Hob_old.tscn" id="8_0sjqq"]
|
||||
[ext_resource type="PackedScene" uid="uid://dm70ynyuw1a5u" path="res://prefabs/day_controller.tscn" id="9_5wx0o"]
|
||||
[ext_resource type="PackedScene" uid="uid://bnwb7imcotkod" path="res://prefabs/build_mode_controller.tscn" id="10_464r1"]
|
||||
[ext_resource type="PackedScene" uid="uid://3i1xb74cfsh5" path="res://test/vr_spectator_camera.tscn" id="11_5wx0o"]
|
||||
[ext_resource type="PackedScene" uid="uid://dxe05wp60jg3l" path="res://scenes/queue_controller.tscn" id="11_de1dy"]
|
||||
[ext_resource type="Script" uid="uid://biu4qr3nr1eny" path="res://test/mp_test_driver.gd" id="99_mptst"]
|
||||
|
||||
@@ -31,6 +31,9 @@ sdfgi_enabled = true
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_arao0"]
|
||||
size = Vector3(15, 20, 0.1)
|
||||
|
||||
[sub_resource type="Resource" id="Resource_464r1"]
|
||||
metadata/__load_path__ = "res://stations/Hob_old.tscn"
|
||||
|
||||
[node name="Main" type="Node3D" unique_id=1312265607 node_paths=PackedStringArray("items_spawner", "players_spawner")]
|
||||
script = ExtResource("1_kdan8")
|
||||
items_spawner = NodePath("ItemsSpawner")
|
||||
@@ -63,7 +66,7 @@ environment = SubResource("Environment_bvwq1")
|
||||
spawn_path = NodePath("../WorldContent")
|
||||
|
||||
[node name="PlayersSpawner" type="MultiplayerSpawner" parent="." unique_id=106645565]
|
||||
_spawnable_scenes = PackedStringArray("res://Player/net_player.tscn")
|
||||
_spawnable_scenes = PackedStringArray("uid://c58ns7csdahjy")
|
||||
spawn_path = NodePath("../Players")
|
||||
|
||||
[node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=4404969]
|
||||
@@ -91,7 +94,7 @@ script = ExtResource("99_mptst")
|
||||
transform = Transform3D(1, 0, -1.7484555e-07, 0, 1, 0, 1.7484555e-07, 0, 1, 0, 0, -1.2)
|
||||
script = ExtResource("6_pw2j5")
|
||||
kitchen_scene = ExtResource("7_0sjqq")
|
||||
hob_scene = ExtResource("8_0sjqq")
|
||||
hob_scene = SubResource("Resource_464r1")
|
||||
|
||||
[node name="DayController" parent="." unique_id=1418639156 instance=ExtResource("9_5wx0o")]
|
||||
|
||||
@@ -99,3 +102,5 @@ hob_scene = ExtResource("8_0sjqq")
|
||||
|
||||
[node name="QueueController" parent="." unique_id=326512876 instance=ExtResource("11_de1dy")]
|
||||
transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0, 0, 7.2000003)
|
||||
|
||||
[node name="VRSpectatorCamera" parent="." unique_id=310823561 instance=ExtResource("11_5wx0o")]
|
||||
|
||||
+63
-36
@@ -1,5 +1,5 @@
|
||||
class_name Table
|
||||
extends StaticBody3D
|
||||
extends SharedInventoryStation
|
||||
|
||||
@export var thinking_duration: float = 3.0
|
||||
@export var ordering_duration: float = 50.0
|
||||
@@ -32,22 +32,22 @@ enum TableState {
|
||||
EATING
|
||||
}
|
||||
|
||||
## State is server-authoritative and synced (see table.tscn's Sync node);
|
||||
## state transitions only ever run where NetworkManager.owns_world() is true
|
||||
## (the whole station's _process is gated off elsewhere for non-owners, see
|
||||
## NetworkManager._gate_station). The setters below just refresh the display,
|
||||
## so both the server (via _set_state) and clients (via incoming sync) show
|
||||
## the same text. Use _set_state(), never assign _state directly.
|
||||
@export var _state: TableState = TableState.EMPTY
|
||||
@export var _state_time: float = 0.0
|
||||
@export var _state_duration: float = 0.0 # set in _set_state_time
|
||||
|
||||
var _state: TableState = TableState.EMPTY
|
||||
var _state_time: float = 0.0
|
||||
var _state_duration: float = 0.0
|
||||
var _original_orders: Array[String] = []
|
||||
@export var _unsatisfied_orders: Array[String] = []
|
||||
var _unsatisfied_orders: Array[String] = []
|
||||
var _players_count: int = 0
|
||||
var _is_leader: bool = true # Only the group leader orchestrates the state and shows its own visuals; see _on_group_changed().
|
||||
|
||||
|
||||
func place_order() -> void:
|
||||
SweetLogger.debug("->[]")
|
||||
if not NetworkManager.owns_world():
|
||||
server_place_order.rpc_id(1)
|
||||
SweetLogger.debug(" []-> rpc")
|
||||
return
|
||||
var new_orders: Array[String] = _unsatisfied_orders.duplicate()
|
||||
# for _i in range(0, randi_range(1, 2)):
|
||||
# new_orders.append(GameManager.get_random_meal())
|
||||
@@ -56,9 +56,12 @@ func place_order() -> void:
|
||||
new_orders.append(GameManager.get_random_meal())
|
||||
_unsatisfied_orders = new_orders
|
||||
_original_orders = _unsatisfied_orders.duplicate()
|
||||
_set_state(TableState.WAITING_PRIMARY)
|
||||
SweetLogger.info("Placed order, orders: {0}", [_unsatisfied_orders])
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable")
|
||||
func server_place_order() -> void:
|
||||
SweetLogger.debug("->[]")
|
||||
if NetworkManager.owns_world():
|
||||
place_order()
|
||||
|
||||
@@ -73,6 +76,34 @@ func absorb_items():
|
||||
_absorb_item_if_correct(held_object)
|
||||
|
||||
|
||||
# Flattens items held across all of this table's snap zones
|
||||
func get_exposed_food_items() -> Array[FoodItem]:
|
||||
var items: Array[FoodItem] = []
|
||||
for zone in snap_zones:
|
||||
var held_object = zone.picked_up_object
|
||||
if not held_object:
|
||||
continue
|
||||
var plate: PlateController = _get_plate_controller_from_item(held_object)
|
||||
if plate:
|
||||
items.append_array(plate.container.contained_items)
|
||||
continue
|
||||
var food_item: FoodItem = Helper.find_food_item(held_object)
|
||||
if food_item:
|
||||
items.append(food_item)
|
||||
return items
|
||||
|
||||
|
||||
# Only the group leader orchestrates the FSM and shows its visuals
|
||||
func _on_group_changed() -> void:
|
||||
_is_leader = is_group_leader()
|
||||
if label_3d:
|
||||
label_3d.visible = _is_leader
|
||||
if label_3d_time:
|
||||
label_3d_time.visible = _is_leader
|
||||
if progress_bar:
|
||||
progress_bar.set_bar_visible(_is_leader and progress_bar.is_bar_visible())
|
||||
|
||||
|
||||
func satisfyAllOrders() -> void:
|
||||
SweetLogger.debug("->[]")
|
||||
_unsatisfied_orders.clear()
|
||||
@@ -109,7 +140,18 @@ func try_consume_customer() -> bool:
|
||||
return false
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
super._enter_tree()
|
||||
var properties: Array[NodePath] = [".:_state_duration", ".:_state", ".:_state_time", ".:_unsatisfied_orders", "Customers:visible"]
|
||||
for property_path in properties:
|
||||
sync_config.add_property(property_path)
|
||||
sync_config.property_set_spawn(property_path, false)
|
||||
sync_config.property_set_replication_mode(property_path, SceneReplicationConfig.REPLICATION_MODE_ON_CHANGE)
|
||||
SweetLogger.info("DEBUG sync_config properties: {0} | root_path: {1} | authority: {2} | peer: {3}", [sync_config.get_properties(), synchronizer.root_path, get_multiplayer_authority(), multiplayer.get_unique_id()]) # temp diagnostic
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
super.ready()
|
||||
if not label_3d:
|
||||
SweetLogger.warning("{0} missing label_3d reference", [name])
|
||||
if not label_3d_time:
|
||||
@@ -137,13 +179,6 @@ func _ready() -> void:
|
||||
for snap_zone_node in snap_zones:
|
||||
snap_zone_node.has_picked_up.connect(_on_object_picked_up)
|
||||
snap_zone_node.has_dropped.connect(_on_object_dropped)
|
||||
# Never on a client: the state machine is server-authoritative and _state is
|
||||
# synced down. NetworkManager._gate_station() already turned processing off,
|
||||
# but a spawned station is gated BEFORE its _ready() runs, so an unconditional
|
||||
# set_process(true) here would quietly re-arm the FSM on every client — and
|
||||
# _state_end(EATING) re-enables the snap zones, which then fight the server
|
||||
# for items sitting on the table.
|
||||
set_process(NetworkManager.owns_world())
|
||||
_set_state(TableState.EMPTY)
|
||||
|
||||
|
||||
@@ -173,6 +208,8 @@ func _on_object_dropped(_item) -> void:
|
||||
func _on_player_enter(_body):
|
||||
_players_count += 1
|
||||
SweetLogger.debug("Player enter, players_count: {0}", [_players_count])
|
||||
if _is_leader and _state == TableState.ORDERING:
|
||||
place_order()
|
||||
|
||||
|
||||
func _on_player_exit(_body):
|
||||
@@ -180,18 +217,6 @@ func _on_player_exit(_body):
|
||||
SweetLogger.debug("Player exit, players_count: {0}", [_players_count])
|
||||
|
||||
|
||||
func _place_order_if_player():
|
||||
if _state != TableState.ORDERING:
|
||||
return
|
||||
if _players_count > 0:
|
||||
place_order()
|
||||
_set_state(TableState.WAITING_PRIMARY)
|
||||
|
||||
|
||||
# func _get_plate_controller_from_item(_item: Node) -> PlateController:
|
||||
# return _item.get_children().filter(func(c): return c is PlateController).front() as PlateController
|
||||
|
||||
|
||||
func _get_plate_controller_from_item(_item: Node) -> PlateController:
|
||||
if not _item:
|
||||
SweetLogger.debug("Item is null")
|
||||
@@ -271,9 +296,9 @@ func _collect_money_from_food():
|
||||
func _set_snap_zones_enabled(value: bool) -> void:
|
||||
# Enabling is the world owner's call only — a client's zones stay gated no
|
||||
# matter what state its copy of the FSM thinks it is in.
|
||||
var enabled := value and NetworkManager.owns_world()
|
||||
var is_enabled := value and NetworkManager.owns_world()
|
||||
for zone in snap_zones:
|
||||
zone.enabled = enabled
|
||||
zone.enabled = is_enabled
|
||||
|
||||
|
||||
## Server-only state transition: sets the new state's timer and assigns
|
||||
@@ -302,7 +327,7 @@ func _set_state(newState: TableState) -> void:
|
||||
_state_time = primary_duration
|
||||
_state_duration = primary_duration
|
||||
_state = newState
|
||||
# Absorm meals that were already in the table when the state starts
|
||||
# Absorm meals that were already in the table when the state starts (We missed the pickup event)
|
||||
for zone in snap_zones:
|
||||
_absorb_item_if_correct(zone.picked_up_object)
|
||||
TableState.WAITING_FRIEND:
|
||||
@@ -383,12 +408,14 @@ func _refresh_display() -> void:
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
super.process(delta)
|
||||
#SweetLogger.debug("State {0} time: {1} duration: {2} is_leader: {3}", [TableState.keys()[_state], _state_time, _state_duration, _is_leader])
|
||||
_refresh_display()
|
||||
_place_order_if_player()
|
||||
#SweetLogger.error("State {0} time: {1} duration: {2}", [TableState.keys()[_state], _state_time, _state_duration])
|
||||
if not _is_leader:
|
||||
return # Follower: the group leader orchestrates the FSM, we just mirror its synced state.
|
||||
|
||||
if not NetworkManager.owns_world():
|
||||
SweetLogger.debug("Not server, skipping state update")
|
||||
# SweetLogger.debug("Not server, skipping state update")
|
||||
return
|
||||
|
||||
if GameManager.game_state == GameManager.GameState.GAME_OVER:
|
||||
|
||||
+76
-62
@@ -1,37 +1,14 @@
|
||||
[gd_scene format=3 uid="uid://caf0xanmxbshy"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://caeikc7e3igkd" path="res://stations/table.gd" id="1_2vcpj"]
|
||||
[ext_resource type="PackedScene" uid="uid://cvucwxibtol5f" path="res://stations/StationMovement.tscn" id="1_5mi7y"]
|
||||
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="1_8j1nt"]
|
||||
[ext_resource type="PackedScene" uid="uid://cbs8jiqe8rcmn" path="res://abstract/station.tscn" id="1_station"]
|
||||
[ext_resource type="AudioStream" uid="uid://ck72h06t8hyyk" path="res://sounds/money.mp3" id="2_jslhm"]
|
||||
[ext_resource type="AudioStream" uid="uid://dllgyc8jh83an" path="res://sounds/220195__gameaudio__click-wooden-1.wav" id="3_0s6ir"]
|
||||
[ext_resource type="PackedScene" uid="uid://bxbocxdaayvwx" path="res://UI/progress_bar.tscn" id="3_kjf1i"]
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_table"]
|
||||
properties/0/path = NodePath(".:position")
|
||||
properties/0/spawn = false
|
||||
properties/0/replication_mode = 1
|
||||
properties/1/path = NodePath(".:rotation")
|
||||
properties/1/spawn = false
|
||||
properties/1/replication_mode = 1
|
||||
properties/2/path = NodePath(".:visible")
|
||||
properties/2/spawn = false
|
||||
properties/2/replication_mode = 1
|
||||
properties/3/path = NodePath(".:_state_duration")
|
||||
properties/3/spawn = false
|
||||
properties/3/replication_mode = 1
|
||||
properties/4/path = NodePath(".:_state")
|
||||
properties/4/spawn = false
|
||||
properties/4/replication_mode = 1
|
||||
properties/5/path = NodePath(".:_state_time")
|
||||
properties/5/spawn = false
|
||||
properties/5/replication_mode = 1
|
||||
properties/6/path = NodePath(".:_unsatisfied_orders")
|
||||
properties/6/spawn = false
|
||||
properties/6/replication_mode = 1
|
||||
properties/7/path = NodePath("Customers:visible")
|
||||
properties/7/spawn = false
|
||||
properties/7/replication_mode = 1
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_probe"]
|
||||
size = Vector3(0.2, 0.3, 0.2)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_24d3s"]
|
||||
size = Vector3(0.7983472, 0.060000002, 0.59873295)
|
||||
@@ -45,28 +22,65 @@ albedo_color = Color(0.31, 0.21576, 0.1333, 1)
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"]
|
||||
size = Vector3(2.1, 0.5, 2.1)
|
||||
|
||||
[node name="Table" type="Node3D" unique_id=987495548]
|
||||
[node name="Table" unique_id=987495548 instance=ExtResource("1_station")]
|
||||
|
||||
[node name="StationMovement" parent="." unique_id=946873975 node_paths=PackedStringArray("station") instance=ExtResource("1_5mi7y")]
|
||||
[node name="StationMovement" parent="." index="0" unique_id=946873975 node_paths=PackedStringArray("station")]
|
||||
station = NodePath("../Table")
|
||||
|
||||
[node name="MultiplayerSyncronizer" type="MultiplayerSynchronizer" parent="." unique_id=2000411505]
|
||||
root_path = NodePath("../Table")
|
||||
replication_config = SubResource("SceneReplicationConfig_np_table")
|
||||
[node name="ProgressBar3D" parent="." index="4" unique_id=654673176]
|
||||
visible = false
|
||||
|
||||
[node name="Table" type="StaticBody3D" parent="." unique_id=1863572470 groups=["station", "table"]]
|
||||
[node name="SnapZone" parent="." index="5" unique_id=1315859105]
|
||||
enabled = false
|
||||
|
||||
[node name="Table" type="StaticBody3D" parent="." index="6" unique_id=1863572470 node_paths=PackedStringArray("notification_audio", "ambient_audio", "snap_zone", "synchronizer") groups=["station", "table"]]
|
||||
script = ExtResource("1_2vcpj")
|
||||
money_sound = ExtResource("2_jslhm")
|
||||
probe_paths = Array[NodePath]([NodePath("ProbeForward"), NodePath("ProbeRight"), NodePath("ProbeBack"), NodePath("ProbeLeft")])
|
||||
pickup_sound = ExtResource("3_0s6ir")
|
||||
drop_sound = ExtResource("3_0s6ir")
|
||||
notification_audio = NodePath("../AudioStreamPlayer3DNotification")
|
||||
ambient_audio = NodePath("../AudioStreamPlayer3DAmbient")
|
||||
snap_zone = NodePath("XRToolsSnapZone")
|
||||
synchronizer = NodePath("../MultiplayerSynchronizer")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Table" unique_id=228053941]
|
||||
[node name="ProbeForward" type="Area3D" parent="Table" index="0" unique_id=-1294555795]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, -0.45)
|
||||
collision_layer = 512
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Table/ProbeForward" index="0" unique_id=-1294555794]
|
||||
shape = SubResource("BoxShape3D_probe")
|
||||
|
||||
[node name="ProbeRight" type="Area3D" parent="Table" index="1" unique_id=-1294555793]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.45, 0.5, 0)
|
||||
collision_layer = 512
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Table/ProbeRight" index="0" unique_id=-1294555792]
|
||||
shape = SubResource("BoxShape3D_probe")
|
||||
|
||||
[node name="ProbeBack" type="Area3D" parent="Table" index="2" unique_id=-1294555791]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0.45)
|
||||
collision_layer = 512
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Table/ProbeBack" index="0" unique_id=-1294555790]
|
||||
shape = SubResource("BoxShape3D_probe")
|
||||
|
||||
[node name="ProbeLeft" type="Area3D" parent="Table" index="3" unique_id=-1294555789]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.45, 0.5, 0)
|
||||
collision_layer = 512
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Table/ProbeLeft" index="0" unique_id=-1294555788]
|
||||
shape = SubResource("BoxShape3D_probe")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Table" index="4" unique_id=228053941]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.50155246, 0)
|
||||
shape = SubResource("BoxShape3D_24d3s")
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="Table" unique_id=276384063]
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="Table" index="5" unique_id=276384063]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.094791204, 0.9715525, 0.0012244582)
|
||||
shape = SubResource("BoxShape3D_24d3s")
|
||||
|
||||
[node name="XRToolsSnapZone" type="Area3D" parent="Table" unique_id=1947858647 groups=["station_zone"]]
|
||||
[node name="XRToolsSnapZone" type="Area3D" parent="Table" index="6" unique_id=1947858647 groups=["station_zone"]]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.0285525, 0)
|
||||
collision_layer = 65536
|
||||
collision_mask = 65540
|
||||
@@ -75,90 +89,90 @@ stash_sound = ExtResource("3_0s6ir")
|
||||
snap_mode = 1
|
||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="Table/XRToolsSnapZone" unique_id=1176887393]
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="Table/XRToolsSnapZone" index="0" unique_id=1176887393]
|
||||
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
||||
shape = SubResource("SphereShape3D_dlkho")
|
||||
|
||||
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="Table/XRToolsSnapZone" unique_id=2135860710]
|
||||
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="Table/XRToolsSnapZone" index="1" unique_id=2135860710]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.013531357, -0.015141487, 0.012176305)
|
||||
|
||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="Table" unique_id=2085635675]
|
||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="Table" index="7" unique_id=2085635675]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.50155246, 0)
|
||||
|
||||
[node name="CSGBox3D" type="CSGBox3D" parent="Table/CSGCombiner3D" unique_id=411048765]
|
||||
[node name="CSGBox3D" type="CSGBox3D" parent="Table/CSGCombiner3D" index="0" unique_id=411048765]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.46137518, 0)
|
||||
size = Vector3(0.6, 0.077, 0.6)
|
||||
material = SubResource("StandardMaterial3D_24d3s")
|
||||
|
||||
[node name="CSGBox3D2" type="CSGBox3D" parent="Table/CSGCombiner3D" unique_id=2093582402]
|
||||
[node name="CSGBox3D2" type="CSGBox3D" parent="Table/CSGCombiner3D" index="1" unique_id=2093582402]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.02685462, 0)
|
||||
size = Vector3(0.1, 0.9258911, 0.1)
|
||||
material = SubResource("StandardMaterial3D_24d3s")
|
||||
|
||||
[node name="CSGBox3D4" type="CSGBox3D" parent="Table/CSGCombiner3D" unique_id=345709075]
|
||||
[node name="CSGBox3D4" type="CSGBox3D" parent="Table/CSGCombiner3D" index="2" unique_id=345709075]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.41408914, 0)
|
||||
size = Vector3(0.1, 0.15142211, 0.5341797)
|
||||
material = SubResource("StandardMaterial3D_24d3s")
|
||||
|
||||
[node name="CSGBox3D5" type="CSGBox3D" parent="Table/CSGCombiner3D" unique_id=550923256]
|
||||
[node name="CSGBox3D5" type="CSGBox3D" parent="Table/CSGCombiner3D" index="3" unique_id=550923256]
|
||||
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 0, -0.41408914, 0)
|
||||
size = Vector3(0.1, 0.15142211, 0.5341797)
|
||||
material = SubResource("StandardMaterial3D_24d3s")
|
||||
|
||||
[node name="Customers" type="Node3D" parent="Table" unique_id=2147179512]
|
||||
[node name="Customers" type="Node3D" parent="Table" index="8" unique_id=2147179512]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.50155246, 0)
|
||||
|
||||
[node name="Customer" type="Node3D" parent="Table/Customers" unique_id=1265943831]
|
||||
[node name="Customer" type="Node3D" parent="Table/Customers" index="0" unique_id=1265943831]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.016780734, -0.08218986, 0)
|
||||
|
||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="Table/Customers/Customer" unique_id=1211819222]
|
||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="Table/Customers/Customer" index="0" unique_id=1211819222]
|
||||
transform = Transform3D(1.095, 0, 0, 0, 1.095, 0, 0, 0, 1.095, 0.08442788, -0.19989291, 0)
|
||||
|
||||
[node name="CSGSphere3D" type="CSGSphere3D" parent="Table/Customers/Customer/CSGCombiner3D" unique_id=2104132980]
|
||||
[node name="CSGSphere3D" type="CSGSphere3D" parent="Table/Customers/Customer/CSGCombiner3D" index="0" unique_id=2104132980]
|
||||
transform = Transform3D(0.9999998, 0, 0, 0, 0.9999998, 0, 0, 0, 0.9999998, -0.7149929, 1.2762557, 0)
|
||||
radius = 0.17520222
|
||||
|
||||
[node name="CSGSphere3D2" type="CSGSphere3D" parent="Table/Customers/Customer/CSGCombiner3D" unique_id=808477724]
|
||||
[node name="CSGSphere3D2" type="CSGSphere3D" parent="Table/Customers/Customer/CSGCombiner3D" index="1" unique_id=808477724]
|
||||
transform = Transform3D(0.9999998, 0, 0, 0, 1.7476468, 0, 0, 0, 0.9999998, -0.7366721, 0.8531885, 0)
|
||||
radius = 0.17520222
|
||||
|
||||
[node name="Seats" type="Node3D" parent="Table" unique_id=1101423789]
|
||||
[node name="Seats" type="Node3D" parent="Table" index="9" unique_id=1101423789]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.50155246, 0)
|
||||
|
||||
[node name="Seat" type="Node3D" parent="Table/Seats" unique_id=1951252898]
|
||||
[node name="Seat" type="Node3D" parent="Table/Seats" index="0" unique_id=1951252898]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.9079602, -0.37259835, 0)
|
||||
|
||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="Table/Seats/Seat" unique_id=384912485]
|
||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="Table/Seats/Seat" index="0" unique_id=384912485]
|
||||
|
||||
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="Table/Seats/Seat/CSGCombiner3D" unique_id=170426769]
|
||||
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="Table/Seats/Seat/CSGCombiner3D" index="0" unique_id=170426769]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.18637085, 0.51674986, 0)
|
||||
radius = 0.25634766
|
||||
height = 0.051940918
|
||||
sides = 16
|
||||
|
||||
[node name="CSGCylinder3D2" type="CSGCylinder3D" parent="Table/Seats/Seat/CSGCombiner3D" unique_id=1294605073]
|
||||
[node name="CSGCylinder3D2" type="CSGCylinder3D" parent="Table/Seats/Seat/CSGCombiner3D" index="1" unique_id=1294605073]
|
||||
transform = Transform3D(0.24355066, 0.98373884, 0, -1.3339849, 0.1796049, 0, 0, 0, 0.8365013, -0.05247748, 0.8547088, 0)
|
||||
radius = 0.25634766
|
||||
height = 0.051940918
|
||||
sides = 16
|
||||
|
||||
[node name="CSGBox3D" type="CSGBox3D" parent="Table/Seats/Seat/CSGCombiner3D" unique_id=1761839440]
|
||||
[node name="CSGBox3D" type="CSGBox3D" parent="Table/Seats/Seat/CSGCombiner3D" index="2" unique_id=1761839440]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.030369163, 0.18746406, 0.1341562)
|
||||
size = Vector3(0.05, 0.6713196, 0.05)
|
||||
|
||||
[node name="CSGBox3D2" type="CSGBox3D" parent="Table/Seats/Seat/CSGCombiner3D" unique_id=1395371703]
|
||||
[node name="CSGBox3D2" type="CSGBox3D" parent="Table/Seats/Seat/CSGCombiner3D" index="3" unique_id=1395371703]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.030369163, 0.18746406, -0.15065354)
|
||||
size = Vector3(0.05, 0.6713196, 0.05)
|
||||
|
||||
[node name="CSGBox3D3" type="CSGBox3D" parent="Table/Seats/Seat/CSGCombiner3D" unique_id=96608061]
|
||||
[node name="CSGBox3D3" type="CSGBox3D" parent="Table/Seats/Seat/CSGCombiner3D" index="4" unique_id=96608061]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.33084005, 0.18746406, 0.1341562)
|
||||
size = Vector3(0.05, 0.6713196, 0.05)
|
||||
|
||||
[node name="CSGBox3D4" type="CSGBox3D" parent="Table/Seats/Seat/CSGCombiner3D" unique_id=1684738537]
|
||||
[node name="CSGBox3D4" type="CSGBox3D" parent="Table/Seats/Seat/CSGCombiner3D" index="5" unique_id=1684738537]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.33084005, 0.18746406, -0.15065354)
|
||||
size = Vector3(0.05, 0.6713196, 0.05)
|
||||
|
||||
[node name="Label3D" type="Label3D" parent="Table" unique_id=662960977]
|
||||
[node name="Label3D" type="Label3D" parent="Table" index="10" unique_id=662960977]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.8263302, 0)
|
||||
pixel_size = 0.003
|
||||
billboard = 2
|
||||
@@ -166,26 +180,26 @@ text = "table state"
|
||||
vertical_alignment = 0
|
||||
line_spacing = -15.0
|
||||
|
||||
[node name="Label3DTime" type="Label3D" parent="Table" unique_id=1534849507]
|
||||
[node name="Label3DTime" type="Label3D" parent="Table" index="11" unique_id=1534849507]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.0147986, 0)
|
||||
visible = false
|
||||
pixel_size = 0.003
|
||||
billboard = 2
|
||||
text = "20.1s"
|
||||
|
||||
[node name="ProgressBar3D" parent="Table" unique_id=654673176 instance=ExtResource("3_kjf1i")]
|
||||
[node name="ProgressBar3D" parent="Table" index="12" unique_id=600000001 instance=ExtResource("3_kjf1i")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.8692299, 0)
|
||||
y_billboard = true
|
||||
|
||||
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="Table" unique_id=724610642]
|
||||
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="Table" index="13" unique_id=724610642]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.7950532, 0)
|
||||
|
||||
[node name="PlayerDetectArea3D" type="Area3D" parent="Table" unique_id=913445150]
|
||||
[node name="PlayerDetectArea3D" type="Area3D" parent="Table" index="14" unique_id=913445150]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.50155246, 0)
|
||||
collision_layer = 0
|
||||
collision_mask = 524288
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Table/PlayerDetectArea3D" unique_id=1538221021]
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Table/PlayerDetectArea3D" index="0" unique_id=1538221021]
|
||||
shape = SubResource("BoxShape3D_vlqg6")
|
||||
|
||||
[connection signal="body_entered" from="Table/PlayerDetectArea3D" to="Table" method="_on_player_enter"]
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
class_name SharedInventoryStation
|
||||
extends Station
|
||||
|
||||
### Abstract 'class', should never be instantiated ###
|
||||
# Backbone for stations that need to know about neighboring stations of the
|
||||
# same kind (tables, belts).
|
||||
|
||||
## Path to one Area3D probe per direction this station should watch for a
|
||||
## same-kind neighbor. Wired explicitly per concrete scene (e.g. a belt
|
||||
## wires one path, a table wires four).
|
||||
@export var probe_paths: Array[NodePath] = []
|
||||
|
||||
## Resolved from probe_paths in ready().
|
||||
var probes: Array[Area3D] = []
|
||||
|
||||
## Area3D probe -> the SharedInventoryStation neighbor detected through it
|
||||
## (or null if that direction is currently unoccupied).
|
||||
var neighbors: Dictionary = {}
|
||||
|
||||
|
||||
# The child station has to call super._enter_tree() for this to be called
|
||||
func _enter_tree() -> void:
|
||||
super._enter_tree()
|
||||
var properties: Array[NodePath] = [".:position", ".:rotation", ".:visible"]
|
||||
for property_path in properties:
|
||||
sync_config.add_property(property_path)
|
||||
sync_config.property_set_spawn(property_path, false)
|
||||
sync_config.property_set_replication_mode(property_path, SceneReplicationConfig.REPLICATION_MODE_ON_CHANGE)
|
||||
|
||||
|
||||
# The child station has to call super.ready() for this to be called
|
||||
func ready() -> void:
|
||||
super.ready()
|
||||
# Find probes
|
||||
for path in probe_paths:
|
||||
var probe := get_node_or_null(path) as Area3D
|
||||
if probe:
|
||||
probes.append(probe)
|
||||
else:
|
||||
SweetLogger.warning("{0} could not resolve neighbor probe at path {1}", [name, path])
|
||||
if probes.is_empty():
|
||||
SweetLogger.warning("{0} has no neighbor probes wired", [name])
|
||||
for probe in probes:
|
||||
neighbors[probe] = null
|
||||
probe.body_entered.connect(_on_probe_body_entered.bind(probe))
|
||||
probe.body_exited.connect(_on_probe_body_exited.bind(probe))
|
||||
# Init leader state before any probe signal has a chance to fire
|
||||
_broadcast_group_changed.call_deferred()
|
||||
|
||||
|
||||
## Cycle-safe search of the neighbor graph (handles circular belt/table loops)
|
||||
func get_connected_group() -> Array[SharedInventoryStation]:
|
||||
var visited: Dictionary = {get_instance_id(): true}
|
||||
var queue: Array[SharedInventoryStation] = [self]
|
||||
var group: Array[SharedInventoryStation] = []
|
||||
while not queue.is_empty():
|
||||
var current: SharedInventoryStation = queue.pop_front()
|
||||
group.append(current)
|
||||
for neighbor in current.neighbors.values():
|
||||
if neighbor and not visited.has(neighbor.get_instance_id()):
|
||||
visited[neighbor.get_instance_id()] = true
|
||||
queue.append(neighbor)
|
||||
return group
|
||||
|
||||
|
||||
## Deterministic leader: smallest grid position, computed the same on every peer
|
||||
func is_group_leader() -> bool:
|
||||
var leader: SharedInventoryStation = self
|
||||
for member in get_connected_group():
|
||||
if _grid_sort_key(member) < _grid_sort_key(leader):
|
||||
leader = member
|
||||
return leader == self
|
||||
|
||||
|
||||
static func _grid_sort_key(station: SharedInventoryStation) -> Vector2i:
|
||||
var pos := station.global_position
|
||||
return Vector2i(roundi(pos.x / Helper.SNAP_GRID_SIZE), roundi(pos.z / Helper.SNAP_GRID_SIZE))
|
||||
|
||||
|
||||
## Food items this station holds, exposed for neighbors to read; override for multi-zone stations
|
||||
func get_exposed_food_items() -> Array[FoodItem]:
|
||||
var items: Array[FoodItem] = []
|
||||
if snap_zone and snap_zone.picked_up_object:
|
||||
var food_item := Helper.find_food_item(snap_zone.picked_up_object)
|
||||
if food_item:
|
||||
items.append(food_item)
|
||||
return items
|
||||
|
||||
|
||||
## Every food item held anywhere in this station's connected group
|
||||
func get_group_food_items() -> Array[FoodItem]:
|
||||
var items: Array[FoodItem] = []
|
||||
for member in get_connected_group():
|
||||
items.append_array(member.get_exposed_food_items())
|
||||
return items
|
||||
|
||||
|
||||
func _on_probe_body_entered(body: Node3D, probe: Area3D) -> void:
|
||||
var station := body as SharedInventoryStation
|
||||
if not station or station == self:
|
||||
return
|
||||
neighbors[probe] = station
|
||||
_broadcast_group_changed()
|
||||
|
||||
|
||||
func _on_probe_body_exited(body: Node3D, probe: Area3D) -> void:
|
||||
var former: SharedInventoryStation = neighbors.get(probe)
|
||||
if not former or body != former:
|
||||
return
|
||||
neighbors[probe] = null
|
||||
# former is no longer reachable from self, so it needs telling separately
|
||||
former._broadcast_group_changed()
|
||||
_broadcast_group_changed()
|
||||
|
||||
|
||||
## Notifies every member of this station's current connected group (not just the two that changed)
|
||||
func _broadcast_group_changed() -> void:
|
||||
for member in get_connected_group():
|
||||
member._on_group_changed()
|
||||
|
||||
|
||||
## Virtual hook: override to react to this station's group membership/leadership changing
|
||||
func _on_group_changed() -> void:
|
||||
pass
|
||||
@@ -0,0 +1 @@
|
||||
uid://bciln4f4tjwgy
|
||||
+11
-6
@@ -25,6 +25,17 @@ var enabled: bool: # When disabled the station only updated display and sounds.
|
||||
snap_zone.set_process(p_enabled)
|
||||
|
||||
|
||||
# Godot requires replication_config to be fully built before _ready. So in _enter_tree
|
||||
# The child station has to call super._enter_tree() for this to be called.
|
||||
func _enter_tree() -> void:
|
||||
# Always a fresh config - abstract/station.tscn's config sub-resource is shared
|
||||
# in memory across every station scene that instances it, so reusing it here
|
||||
# would pile every station type's properties onto the same shared object.
|
||||
synchronizer.root_path = get_path()
|
||||
synchronizer.replication_config = SceneReplicationConfig.new()
|
||||
sync_config = synchronizer.replication_config
|
||||
|
||||
|
||||
# The child station has to call super.ready() for this to be called
|
||||
func ready() -> void:
|
||||
SweetLogger.debug("Station {0} ready", [name])
|
||||
@@ -43,12 +54,6 @@ func ready() -> void:
|
||||
snap_zone.has_picked_up.connect(_on_object_picked_up_handler)
|
||||
snap_zone.has_dropped.connect(_on_object_dropped_handler)
|
||||
|
||||
# Configure Multiplayer Syncronizer
|
||||
# This overwrites any changes made in the inspector.
|
||||
synchronizer.root_path = get_path()
|
||||
synchronizer.replication_config = SceneReplicationConfig.new()
|
||||
sync_config = synchronizer.replication_config
|
||||
|
||||
# The child station has to call super.process(delta) for this to be called
|
||||
func process(_delta: float) -> void:
|
||||
refresh_display()
|
||||
|
||||
@@ -19,21 +19,22 @@ var current_work: float # How far a FoodItem conversion is toward completion
|
||||
var max_work: float # How much work has to be acheived to trigger conversion
|
||||
var result_id: String # Station active if not empty. What FoodItem id the conversion turns the current FoodItem into.
|
||||
|
||||
# The child station has to call super.ready() for this to be called
|
||||
func ready() -> void:
|
||||
super.ready()
|
||||
if not progress_bar:
|
||||
SweetLogger.warning("{0} missing progress_bar reference", [name])
|
||||
|
||||
# Configure Multiplayer Syncronizer
|
||||
# Base Station creates the config, here we append to it:
|
||||
var properties: Array[NodePath] = [ ".:current_work", ".:max_work", ".:result_id"]
|
||||
# The child station has to call super._enter_tree() for this to be called
|
||||
func _enter_tree() -> void:
|
||||
super._enter_tree()
|
||||
var properties: Array[NodePath] = [".:current_work", ".:max_work", ".:result_id"]
|
||||
for property_path in properties:
|
||||
sync_config.add_property(property_path)
|
||||
sync_config.property_set_spawn(property_path, false)
|
||||
sync_config.property_set_replication_mode(property_path, SceneReplicationConfig.REPLICATION_MODE_ON_CHANGE)
|
||||
|
||||
|
||||
# The child station has to call super.ready() for this to be called
|
||||
func ready() -> void:
|
||||
super.ready()
|
||||
if not progress_bar:
|
||||
SweetLogger.warning("{0} missing progress_bar reference", [name])
|
||||
|
||||
|
||||
# The child station has to call super.process(delta) for this to be called
|
||||
func process(delta: float) -> void:
|
||||
|
||||
@@ -98,7 +98,7 @@ const TIMESTAMP_BG_COLOR = "#1e3a5f"
|
||||
#===================================================================================#
|
||||
## Column widths for alignment (in characters)
|
||||
const PEER_ID_COLUMN_WIDTH = 6
|
||||
const LOG_TYPE_COLUMN_WIDTH = 13
|
||||
const LOG_TYPE_COLUMN_WIDTH = 10
|
||||
## Default mm:ss:ms; with SHOW_TIMESTAMP_HOURS, hh:mm:ss:ms
|
||||
const TIMESTAMP_COLUMN_WIDTH_MMSSMS = 9
|
||||
const TIMESTAMP_COLUMN_WIDTH_HHMMSSMS = 12
|
||||
@@ -223,13 +223,13 @@ func _print_rich_log(peer_id_str: String, log_type: String, message: String, scr
|
||||
|
||||
# Use peer ID directly without "Peer" prefix
|
||||
var peer_label = peer_id_str
|
||||
var log_type_label = str(frame_count) + " - " + log_type.to_upper()
|
||||
var log_type_label = str(frame_count) + "-" + log_type.to_upper()
|
||||
|
||||
# Pad peer ID column for alignment
|
||||
var peer_padded = _truncate_text(_pad_text(peer_label, PEER_ID_COLUMN_WIDTH), TRUNCATE_PEER_NAME)
|
||||
|
||||
# Format peer ID with its background color
|
||||
var peer_formatted = _format_rich_text(peer_padded + " ", peer_color.bg_color, peer_color.text_color)
|
||||
var peer_formatted = _format_rich_text(" " + peer_padded, peer_color.bg_color, peer_color.text_color)
|
||||
|
||||
# Format log type with its background color
|
||||
var log_type_padded = _pad_text(log_type_label, LOG_TYPE_COLUMN_WIDTH)
|
||||
|
||||
@@ -37,3 +37,5 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.8000001, 0, 0)
|
||||
|
||||
[node name="PotatoDispenser" parent="." unique_id=522124817 instance=ExtResource("9_ok0xr")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8000001, 0, 0.6)
|
||||
|
||||
[editable path="Table"]
|
||||
|
||||
@@ -1,68 +1,115 @@
|
||||
# RecipeManager
|
||||
# Recipes
|
||||
|
||||
## Overview
|
||||
## FoodItem
|
||||
|
||||
`RecipeManager` is a static Godot class that centralizes recipe data for food item combinations and allows runtime lookup of result scenes.
|
||||
Every ingredient, meal, and side in the game is a `FoodItem` node (`prefabs/food_item.gd`), a sibling component on the pickable item's scene. It's just three exported fields:
|
||||
|
||||
The manager loads a single YAML file at startup: `res://recipes.yaml`, using the installed `addons/yaml` addon.
|
||||
- `id: String` — the recipe key this item is looked up by in `recipes.yaml` (e.g. `"raw_burger"`).
|
||||
- `type: Type` — `MEAL`, `SIDE`, `INGREDIENT`, or `NONE`. Used for things like which plate slot an item lands in.
|
||||
- `sell_value: int` — money paid out when the item is sold/consumed.
|
||||
|
||||
## Responsibilities
|
||||
`id` is the link between a spawned scene and its entry in `recipes.yaml` — every process below keys off it.
|
||||
|
||||
- Load recipe definitions from `recipes.yaml`.
|
||||
- Provide symmetric lookups for two-item combining recipes so `A + B` and `B + A` map to the same result.
|
||||
- Resolve item IDs to their corresponding scene resources.
|
||||
- Print all loaded combining recipes when the application starts.
|
||||
## `recipes.yaml`
|
||||
|
||||
## Data structure in `recipes.yaml`
|
||||
|
||||
The recipe YAML file contains two top-level sections:
|
||||
|
||||
- `items`: maps item IDs to their scene path and optional metadata.
|
||||
- `combining`: maps result item IDs to one or more recipe pairs.
|
||||
|
||||
Example structure:
|
||||
`RecipeManager` loads a single file, `res://recipes.yaml`, with one top-level section per process plus an `items` section mapping each `id` to its scene:
|
||||
|
||||
```yaml
|
||||
items:
|
||||
hamburger:
|
||||
scene: res://Items/hamburger.tscn
|
||||
type: meal
|
||||
burger_buns:
|
||||
scene: res://Items/BurgerBuns.tscn
|
||||
type: ingredient
|
||||
raw_burger:
|
||||
scene: res://items/burger.tscn
|
||||
cooked_burger:
|
||||
scene: res://items/cooked_burger.tscn
|
||||
|
||||
combining:
|
||||
combining: # instant, two items -> one
|
||||
hamburger:
|
||||
- [cooked_burger, burger_buns]
|
||||
- [charcoal, charcoal]
|
||||
charcoal:
|
||||
- [cube, cube]
|
||||
|
||||
cooking: # timed, one item -> one (a station's work meter)
|
||||
cooked_burger:
|
||||
ingredient: raw_burger
|
||||
time: 4
|
||||
|
||||
chopping: # timed, one item -> one
|
||||
chopped_potato:
|
||||
ingredient: potato
|
||||
work: 30
|
||||
|
||||
rolling: # parsed, no station consumes it yet
|
||||
pie_base:
|
||||
ingredient: dough
|
||||
work: 80
|
||||
|
||||
augmenting: # parsed, no station consumes it yet
|
||||
cooked_burger:
|
||||
has_tomato: sliced_tomato
|
||||
```
|
||||
|
||||
### `items`
|
||||
`cooking` entries may also be a list, so multiple ingredients can produce the same result (see `charcoal` in the real file, which can be made by overcooking three different things).
|
||||
|
||||
Each entry under `items` uses the item ID as the key.
|
||||
The manager reads the `scene` field for each item and uses it to resolve the packed scene for recipe results.
|
||||
## RecipeManager
|
||||
|
||||
### `combining`
|
||||
`RecipeManager` (`global/RecipeManager.gd`) is a static class — no instance needed. On first use it lazily parses `recipes.yaml` (via the `YAML` addon) into five internal maps (`_combining_map`, `_cooking_map`, `_chopping_map`, `_rolling_map`, `_augmenting_map`) plus `_scene_paths`, and never re-parses after (`_loaded` guard).
|
||||
|
||||
Each entry under `combining` represents a result item ID, and its value is a list of recipe pairs.
|
||||
Each recipe pair is a two-item array of ingredient IDs.
|
||||
This allows a single result item to have multiple valid recipes.
|
||||
It doesn't hand out those raw maps. Instead it exposes narrow getters that the rest of the game calls:
|
||||
|
||||
The manager normalizes each ingredient pair using a sorted key string internally, so lookups are symmetric.
|
||||
| Function | Returns |
|
||||
|---|---|
|
||||
| `get_combination_result(a, b)` | `PackedScene` for combining two ids (order-independent) |
|
||||
| `get_cooking_result(ingredient)` | result id, or `""` if not cookable |
|
||||
| `get_cooking_time(ingredient)` | seconds of work needed |
|
||||
| `get_chopping_result(ingredient)` | result id, or `""` if not choppable |
|
||||
| `get_chopping_work(ingredient)` | work needed |
|
||||
| `get_item_scene(id)` | `PackedScene` for any item id |
|
||||
| `print_all_recipes()` | debug dump of every loaded recipe, called once from `main.gd` at startup |
|
||||
|
||||
## Runtime behavior
|
||||
`rolling` and `augmenting` are parsed and included in `print_all_recipes()`, but currently have no matching getters or station — they're not wired into a conversion process yet.
|
||||
|
||||
- `FoodItem.id` is used when combining items to look up the recipe result.
|
||||
- `RecipeManager.get_combination(first_id, second_id)` computes a canonical key for the pair and returns the resulting item's scene.
|
||||
- On startup, `main.gd` calls `RecipeManager.print_all_recipes()`, which logs each combining recipe in the form:
|
||||
## Conversion processes
|
||||
|
||||
`ingredient_a + ingredient_b -> result_id`
|
||||
Two different mechanisms turn one `FoodItem` into another today:
|
||||
|
||||
## Notes
|
||||
**Combining** — instant, no station involved. `CombinableItem` (`prefabs/combinable_item.gd`) sits on every pickable item; while the item is snapped into a zone, its trigger area watches for another `FoodItem` entering. On contact it asks `RecipeManager.get_combination_result(a, b)` — if a recipe exists, it despawns both ingredients and spawns the result in their place.
|
||||
|
||||
- `RecipeManager` prefers the installed YAML addon to parse `recipes.yaml` when it is available.
|
||||
- If the addon is unavailable in the current runtime, the manager falls back to a lightweight built-in parser for the simple `items`/`combining` file format.
|
||||
- Scene paths now live in the YAML data instead of being hardcoded in the manager.
|
||||
- Because `RecipeManager` is static, it can be used from any script without creating an instance.
|
||||
**Cooking / chopping** — timed, driven by `WorkStation`'s shared work meter (`current_work` / `max_work` / `result_id`, see [Stations.md](Stations.md)). When a `Hob` or `Counter` picks up a `FoodItem`, it asks `RecipeManager` whether that ingredient has a recipe for its process (`get_cooking_result`/`get_chopping_result`) and, if so, how much work it needs (`get_cooking_time`/`get_chopping_work`), then starts accumulating work — a `Hob` ticks it every frame, a `Counter` adds it per knife swipe. Once `current_work` reaches `max_work`, `WorkStation.convert_item()` calls `RecipeManager.get_item_scene(result_id)`, despawns the original item, and spawns the result in the same snap zone.
|
||||
|
||||
Separately, `ItemContainer` (`Containers/container.gd`) calls `get_item_scene(id)` too, but only to instantiate the correct visual mesh for each id in a plate's synced contents list — that's a display lookup, not a conversion.
|
||||
|
||||
## Diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
YAML["recipes.yaml"] -->|"YAML.load_file()"| RM["RecipeManager<br/>(global/RecipeManager.gd)"]
|
||||
RM --> Maps["_combining_map / _cooking_map / _chopping_map / _scene_paths<br/>(_rolling_map / _augmenting_map parsed, not yet consumed)"]
|
||||
|
||||
subgraph Instant["Instant conversion"]
|
||||
direction TB
|
||||
CI["CombinableItem<br/>(prefabs/combinable_item.gd)"]
|
||||
end
|
||||
|
||||
subgraph Timed["Timed conversion"]
|
||||
direction TB
|
||||
Hob["Hob<br/>(stations/hob.gd)"]
|
||||
Counter["Counter<br/>(stations/counter.gd)"]
|
||||
WS["WorkStation.convert_item()<br/>(abstract/work_station.gd)"]
|
||||
Hob -.->|"result_id, max_work"| WS
|
||||
Counter -.->|"result_id, max_work"| WS
|
||||
end
|
||||
|
||||
subgraph Display["Display only, not a conversion"]
|
||||
direction TB
|
||||
Container["ItemContainer<br/>(Containers/container.gd)"]
|
||||
end
|
||||
|
||||
CI -->|"get_combination_result(a, b)"| RM
|
||||
Hob -->|"get_cooking_result / get_cooking_time"| RM
|
||||
Counter -->|"get_chopping_result / get_chopping_work"| RM
|
||||
WS -->|"get_item_scene(result_id)"| RM
|
||||
Container -->|"get_item_scene(id)"| RM
|
||||
|
||||
Spawn(("NetworkManager.spawn_item"))
|
||||
CI ==>|"instantiates result"| Spawn
|
||||
WS ==>|"instantiates result"| Spawn
|
||||
```
|
||||
|
||||
Solid arrows are calls into `RecipeManager`; dotted arrows are `Hob`/`Counter` handing their recipe off to `WorkStation`'s shared work meter; thick arrows are the actual item conversion. `CombinableItem` and `WorkStation` are grouped in adjacent lanes so both paths to `NetworkManager.spawn_item` stay short; `ItemContainer` sits in its own lane since it never spawns anything.
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# Stations
|
||||
|
||||
## Overview
|
||||
|
||||
A "station" is any interactable kitchen fixture a player works at: `Counter`, `Hob`, `Sink`, `Table`, `DirtStation`, and the item dispensers. They share networking, sound, and snap-zone plumbing through a common script base, and share their node layout through a common base scene.
|
||||
|
||||
Two independent inheritance mechanisms combine to build a concrete station, e.g. `Table`:
|
||||
|
||||
- **Scene inheritance** — `table.tscn` is a Godot "inherited scene" of `abstract/station.tscn`, so it gets the same child nodes (snap zone, synchronizer, audio players, progress bar) for free.
|
||||
- **Script inheritance** — `table.gd` (attached as a script override on that inherited scene) extends `SharedInventoryStation`, which extends `Station`.
|
||||
|
||||
These two axes are independent: which base scene a `.tscn` inherits from is unrelated to which class its attached script extends.
|
||||
|
||||
## Scene inheritance
|
||||
|
||||
`abstract/station.tscn` is the base scene every station scene inherits from:
|
||||
|
||||
```
|
||||
Station (Node3D)
|
||||
├─ StationMovement (drag-to-move handle, res://stations/StationMovement.tscn)
|
||||
├─ MultiplayerSynchronizer
|
||||
├─ AudioStreamPlayer3DNotification
|
||||
├─ AudioStreamPlayer3DAmbient
|
||||
├─ ProgressBar3D
|
||||
└─ SnapZone
|
||||
```
|
||||
|
||||
Each concrete station scene inherits this scene and overrides the root node's `script` property to attach its own class:
|
||||
|
||||
| Scene | Script attached |
|
||||
|---|---|
|
||||
| `Counter.tscn` | `stations/counter.gd` (`Counter`) |
|
||||
| `Hob.tscn` | `stations/hob.gd` (`Hob`) |
|
||||
| `sink.tscn` | `stations/sink.gd` (`Sink`) |
|
||||
| `table.tscn` | `stations/table.gd` (`Table`) |
|
||||
| `dirt_station.tscn` | `stations/dirt_station.gd` (`DirtStation`) |
|
||||
| `BurgerBunsDispenser.tscn`, `cube_side_dispense.tscn`, `plate_dispenser.tscn`, `potato_dispenser.tscn`, `raw_burger_dispenser.tscn` | `stations/item_dispenser.gd` (`ItemDispenser`) |
|
||||
|
||||
`StationMovement` (`stations/station_movement.gd`) is a sibling node composed into the base scene, not a station subclass — it's what lets players pick the whole station up and move it in build mode.
|
||||
|
||||
## Script (class) inheritance
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
direction TB
|
||||
Node3D <|-- Station
|
||||
Station <|-- WorkStation
|
||||
Station <|-- SharedInventoryStation
|
||||
Station <|-- DirtStation
|
||||
Station <|-- ItemDispenser
|
||||
WorkStation <|-- Hob
|
||||
WorkStation <|-- Sink
|
||||
WorkStation <|-- Counter
|
||||
SharedInventoryStation <|-- Table
|
||||
|
||||
class Station {
|
||||
<<abstract>>
|
||||
+snap_zone : XRToolsSnapZone
|
||||
+synchronizer : MultiplayerSynchronizer
|
||||
+enabled : bool
|
||||
+on_object_picked_up(item)
|
||||
+on_object_dropped(item)
|
||||
+refresh_display()
|
||||
}
|
||||
class WorkStation {
|
||||
<<abstract>>
|
||||
+current_work : float
|
||||
+max_work : float
|
||||
+result_id : String
|
||||
+add_work(work)
|
||||
+convert_item()
|
||||
}
|
||||
class SharedInventoryStation {
|
||||
<<abstract>>
|
||||
+probes : Area3D[]
|
||||
+neighbors : Dictionary
|
||||
+get_connected_group()
|
||||
+is_group_leader()
|
||||
}
|
||||
class Hob {
|
||||
+cook_speed : float
|
||||
}
|
||||
class Sink {
|
||||
+wash_speed : float
|
||||
}
|
||||
class Counter {
|
||||
+chop_work_steps : float
|
||||
}
|
||||
class DirtStation
|
||||
class ItemDispenser {
|
||||
+item_scene : PackedScene
|
||||
}
|
||||
class Table {
|
||||
+_state : TableState
|
||||
+place_order()
|
||||
}
|
||||
```
|
||||
|
||||
| Class | File | Role |
|
||||
|---|---|---|
|
||||
| `Station` | `abstract/station.gd` | Base for every station. Owns the snap zone, sets up the `MultiplayerSynchronizer`'s replication config, propagates pickup/drop events to clients over RPC, and exposes virtual hooks (`on_object_picked_up`, `on_object_dropped`, `on_food_item_picked_up`, `on_food_item_dropped`, `refresh_display`). |
|
||||
| `WorkStation` | `abstract/work_station.gd` | Base for stations that convert a held `FoodItem` over time via a synced work meter (`current_work`/`max_work`/`result_id`). `add_work()` routes through the server via RPC, and `convert_item()` swaps the held item for the recipe result on completion. |
|
||||
| `Hob` | `stations/hob.gd` | Cooks food using `RecipeManager` cooking recipes; plays a cook animation while active. |
|
||||
| `Sink` | `stations/sink.gd` | Washes a dirty `PlateController` over time; drives water/bubble effect nodes. |
|
||||
| `Counter` | `stations/counter.gd` | Chops food when a knife gesture area detects a fast-enough swipe. |
|
||||
| `SharedInventoryStation` | `abstract/shared_inventory_station.gd` | Base for stations that need to know about same-kind neighbors. Resolves `probe_paths` into neighbor `Area3D`s, builds a connected-group graph (cycle-safe, for belt/table loops), and computes a deterministic group leader. |
|
||||
| `Table` | `stations/table.gd` | The only concrete `SharedInventoryStation`. Runs the customer-order finite state machine (`EMPTY → THINKING → ORDERING → WAITING_PRIMARY → WAITING_FRIEND → EATING`); the connected group's leader orchestrates state, followers mirror it. |
|
||||
| `DirtStation` | `stations/dirt_station.gd` | Marks a dropped plate as dirty. Extends `Station` directly (no work meter). |
|
||||
| `ItemDispenser` | `stations/item_dispenser.gd` | Server keeps a fresh instance of `item_scene` spawned into its snap zone whenever it's empty. Extends `Station` directly. |
|
||||
|
||||
## Networking pattern
|
||||
|
||||
Every station follows the same client/server split, established in `Station`:
|
||||
|
||||
- The server (`NetworkManager.owns_world()`) is authoritative for state changes.
|
||||
- A client calls a public method (e.g. `add_work`, `place_order`); if it isn't the server, that method forwards the call via `*_id.rpc_id(1)` and returns.
|
||||
- The server applies the change locally, then RPCs the result back out to clients (e.g. `_clients_on_object_picked_up_handler`, `_everyone_on_work_complete`).
|
||||
- Frequently-changing fields (`current_work`, `result_id`, table `_state`, etc.) are also registered on `sync_config` in `_enter_tree()` for straight property replication.
|
||||
|
||||
## Notes
|
||||
|
||||
- `stations/station.gd` (`extends "res://stations/hob.gd"`, no `class_name`) is not attached to any scene and isn't part of this hierarchy — it looks like leftover scaffolding.
|
||||
@@ -0,0 +1,40 @@
|
||||
class_name VrSpectatorCamera
|
||||
extends Node
|
||||
|
||||
# Debug tool: on a non-VR client, follows a connected VR client's head so you can spectate it.
|
||||
|
||||
@export var players_path: NodePath = ^"../Players"
|
||||
|
||||
@onready var _players: Node = get_node_or_null(players_path)
|
||||
var _camera: Camera3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
var xr_interface := XRServer.find_interface("OpenXR")
|
||||
if xr_interface and xr_interface.is_initialized():
|
||||
SweetLogger.warning("{0} disabled - this instance is the VR client", [name])
|
||||
return
|
||||
if not _players:
|
||||
SweetLogger.warning("{0} could not resolve players_path {1}", [name, players_path])
|
||||
return
|
||||
_camera = Camera3D.new()
|
||||
add_child(_camera)
|
||||
_camera.make_current()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not _camera:
|
||||
return
|
||||
var head := _find_remote_head()
|
||||
if head:
|
||||
_camera.global_transform = head.global_transform
|
||||
|
||||
|
||||
# First connected player that isn't this local peer
|
||||
func _find_remote_head() -> Node3D:
|
||||
var my_id := multiplayer.get_unique_id()
|
||||
for player in _players.get_children():
|
||||
if player.name == str(my_id):
|
||||
continue
|
||||
return player.get_node_or_null("Head")
|
||||
return null
|
||||
@@ -0,0 +1 @@
|
||||
uid://c4y0yigqv7355
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene format=3 uid="uid://3i1xb74cfsh5"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c4y0yigqv7355" path="res://test/vr_spectator_camera.gd" id="1_6xorv"]
|
||||
|
||||
[node name="VRSpectatorCamera" type="Node" unique_id=310823561]
|
||||
script = ExtResource("1_6xorv")
|
||||
Reference in New Issue
Block a user