Split Station into WorkStation abstract script

This commit is contained in:
JonShard
2026-08-13 20:00:37 +02:00
parent 5ced240c43
commit 9e0c3ed174
27 changed files with 441 additions and 429 deletions
+115
View File
@@ -0,0 +1,115 @@
class_name Station
extends Node3D
### Abstract 'class', should never be instantiated ###
# - Keep all logic all stations share.
# - Contain as much of the networking code syncing stations as possible.
## Sounds
@export var pickup_sound: AudioStream
@export var drop_sound: AudioStream
## Node references. These are all required snd should be set in the inspector
@export var snap_zone: XRToolsSnapZone
@export var synchronizer: MultiplayerSynchronizer
var sync_config: SceneReplicationConfig
var enabled: bool: # When disabled the station only updated display and sounds.
get: return snap_zone.enabled
set(p_enabled):
SweetLogger.info("Set enabled {0} on station {1} on client {2}", [p_enabled, name, multiplayer.get_unique_id()])
snap_zone.enabled = p_enabled
snap_zone.set_process(p_enabled)
# The child station has to call super.ready() for this to be called
func ready() -> void:
SweetLogger.debug("Station {0} ready", [name])
if not snap_zone:
SweetLogger.warning("{0} missing snap_zone reference", [name])
if not synchronizer:
SweetLogger.warning("{0} missing synchronizer reference", [name])
# Disable if we're not the server.
if not NetworkManager.owns_world():
enabled = false
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()
## Signal handles ##
func _on_object_picked_up_handler(item: Node3D) -> void:
if not NetworkManager.owns_world():
return
_clients_on_object_picked_up_handler.rpc(item.get_path())
_apply_object_picked_up(item)
func _on_object_dropped_handler(item: Node3D) -> void:
if not NetworkManager.owns_world():
return
_clients_on_object_dropped_handler.rpc(item.get_path())
_apply_object_dropped(item)
func _apply_object_picked_up(item: Node3D) -> void:
SweetLogger.debug("Pickup: {0}", [item.name])
on_object_picked_up(item)
var food_item = Helper.find_food_item(item)
if food_item:
on_food_item_picked_up(food_item)
func _apply_object_dropped(item: Node3D) -> void:
SweetLogger.debug("Drop: {0}", [item.name])
on_object_dropped(item)
var food_item = Helper.find_food_item(item)
if food_item:
on_food_item_dropped(food_item)
# Propagate events to clients
@rpc("authority", "call_remote", "reliable")
func _clients_on_object_picked_up_handler(item_path: NodePath):
var item := Helper.get_node_from_path(self, item_path) as Node3D
if not item:
return
_apply_object_picked_up(item)
@rpc("authority", "call_remote", "reliable")
func _clients_on_object_dropped_handler(item_path: NodePath):
var item := Helper.get_node_from_path(self, item_path) as Node3D
if not item:
return
_apply_object_dropped(item)
## Virutal methods
# The child station implements these if it cares about them.
func on_object_picked_up(_item: Node3D) -> void:
SweetLogger.debug("Not implemented in {0}", [name])
func on_object_dropped(_item: Node3D) -> void:
SweetLogger.debug("Not implemented in {0}", [name])
func on_food_item_picked_up(_food_item: FoodItem) -> void:
SweetLogger.debug("Not implemented in {0}", [name])
func on_food_item_dropped(_food_item: FoodItem) -> void:
SweetLogger.debug("Not implemented in {0}", [name])
func refresh_display() -> void:
pass
+1
View File
@@ -0,0 +1 @@
uid://1ii5ov17p2p6
+23
View File
@@ -0,0 +1,23 @@
[gd_scene format=3 uid="uid://cbs8jiqe8rcmn"]
[ext_resource type="PackedScene" uid="uid://cvucwxibtol5f" path="res://stations/StationMovement.tscn" id="1_yuo1j"]
[ext_resource type="PackedScene" uid="uid://bxbocxdaayvwx" path="res://UI/progress_bar.tscn" id="2_be8yi"]
[ext_resource type="PackedScene" uid="uid://ce7vysyvondf8" path="res://addons/godot-xr-tools/objects/snap_zone.tscn" id="3_be8yi"]
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_0o2ds"]
[node name="Station" type="Node3D" unique_id=707500902]
[node name="StationMovement" parent="." unique_id=946873975 instance=ExtResource("1_yuo1j")]
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." unique_id=1536666704]
replication_config = SubResource("SceneReplicationConfig_0o2ds")
[node name="AudioStreamPlayer3DPulse" type="AudioStreamPlayer3D" parent="." unique_id=1976975302]
[node name="AudioStreamPlayer3DContinous" type="AudioStreamPlayer3D" parent="." unique_id=925832848]
[node name="ProgressBar3D" parent="." unique_id=654673176 instance=ExtResource("2_be8yi")]
[node name="SnapZone" parent="." unique_id=1315859105 groups=["station_zone"] instance=ExtResource("3_be8yi")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
+128
View File
@@ -0,0 +1,128 @@
class_name WorkStation
extends Station
### Abstract 'class', should never be instantiated ###
# This script serves two main jobs:
# - Keep all logic all workstations (sink, hob...) share.
# - Contain as much of the networking code syncing stations as possible.
## Sounds
@export var process_sound: AudioStream
@export var complete_sound: AudioStream
## Node references. These are all required snd should be set in the inspector
@export var notification_audio: AudioStreamPlayer3D # Short sounds like pickup or complete
@export var ambient_audio: AudioStreamPlayer3D # Releating sounds like a cooking noise
@export var progress_bar: ProgressBar3D
## Synced by MultiplayerSyncronizer ##
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 notification_audio:
SweetLogger.warning("{0} missing notification_audio reference", [name])
if not ambient_audio:
SweetLogger.warning("{0} missing ambient_audio reference", [name])
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"]
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.process(delta) for this to be called
func process(delta: float) -> void:
super.process(delta)
#SweetLogger.debug("station process result_id: {0}, work: {1}, max_work: {2}", [result_id, current_work, max_work])
refresh_display()
# Propagate events to clients
@rpc("authority", "call_local", "reliable")
func _everyone_on_work_complete():
on_work_complete()
## Virutal methods ##
# The child station implements these if it cares about them.
func on_work_complete() -> void:
SweetLogger.debug("Not implemented in {0}", [name])
# The child station has to call super.refresh_display for this to be called
func refresh_display() -> void:
super.refresh_display()
var is_active: bool = not result_id.is_empty()
progress_bar.set_bar_visible(is_active)
progress_bar.set_progress((current_work / max_work) if is_active else 0.0)
## Public methods ##
# Will be called by clients to the server, and server to server
func add_work(work: float) -> void:
SweetLogger.debug("Add work: {0} current: {1} max_work: {2}", [snappedf(work, 0.01), snappedf(current_work, 0.01), snappedf(max_work, 0.01)])
if work <= 0:
SweetLogger.warning("Work to add must be positive, work: {0}", [work])
return
if not NetworkManager.owns_world:
server_add_work(work)
return
if not result_id or result_id.is_empty():
SweetLogger.warning("Work can not be added when result_id is empty")
return
current_work += work
if current_work >= max_work:
_everyone_on_work_complete()
@rpc("any_peer", "call_remote", "reliable")
func server_add_work(work: float) -> void:
add_work(work)
# Converts the currenly held item into result_id, by despawning it and instantiating a new item
func convert_item() -> Node3D:
if not NetworkManager.owns_world():
return
SweetLogger.info("Convert item, result_id: {0}", [result_id])
if not NetworkManager.owns_world:
SweetLogger.warning("Only the server should ever call this!")
return null
if not result_id or result_id.is_empty():
SweetLogger.warning("Station tried to convert item, but result_id is null or empty!")
return
var old_pickable = snap_zone.picked_up_object
if not old_pickable:
SweetLogger.warning("Station tried to convert item, but item is missing!")
return null
var target_id := result_id
var original_transform = old_pickable.global_transform
var new_scene_instance = NetworkManager.spawn_item(RecipeManager.get_item_scene(target_id).resource_path, original_transform)
reset()
# Drop and free the old item and pick up the new one
SweetLogger.debug("Freeing old_pickable {0}", [old_pickable])
snap_zone.drop_object()
NetworkManager.despawn_item(old_pickable)
snap_zone.pick_up_object(new_scene_instance)
SweetLogger.info("Convert item into id: {0}, node name: {1}", [target_id, new_scene_instance.name])
return new_scene_instance
func reset() -> void:
SweetLogger.debug("->[]")
current_work = 0
max_work = 0
result_id = ""
+1
View File
@@ -0,0 +1 @@
uid://b25s3bhslrlxc