21 Commits

Author SHA1 Message Date
algodoogle 8c2227432c some chaga 2026-07-25 17:29:27 +01:00
JonShard 84433160fe Kanban 2026-07-17 00:05:26 +02:00
JonShard 588d11a9ff Update main scene 2026-07-16 23:37:16 +02:00
JonShard 3df2a0af58 Merge branch 'multy' of https://git.offcoursegames.com/JonShard/VRyHungry1 into multy 2026-07-16 23:23:59 +02:00
JonShard 832e539a96 Update main scene 2026-07-16 23:23:46 +02:00
algodoogle 4e6c645938 fix bug 2026-07-16 22:23:33 +01:00
algodoogle 1869dbf054 multyplayer 2026-07-16 18:36:49 +01:00
algodoogle 5e0b58a984 add kanban 2026-07-16 15:52:34 +01:00
JonShard 329bb1f984 Add plates accept MEALS and SIDES 2026-07-16 16:28:57 +02:00
JonShard e27ddfe3f1 Reorganize project into folder structure 2026-07-16 15:09:12 +02:00
JonShard 4b8e017d64 Fix circular dependency in combinable_item scene 2026-07-16 14:31:51 +02:00
JonShard 5e6b403a43 Add Container detects platable items 2026-07-16 12:07:32 +02:00
JonShard 3f1b9aea38 Add notes in TODO.md 2026-07-15 12:45:10 +02:00
JonShard 885c6f49f0 Add hamburger and refactor CookableItem and CombinableItem into scenes 2026-07-15 11:14:23 +02:00
JonShard 485ca899ab Add cooked_burger and change recipe chain 2026-07-15 10:14:45 +02:00
algodoogle 57ddfb0355 Merge branch 'master' of https://git.offcoursegames.com/JonShard/VRyHungry1 2026-07-14 19:20:16 +01:00
algodoogle f162578c3b plate 2026-07-14 19:19:48 +01:00
JonShard 9c55853930 Add Item dispenser that will dispense burger buns 2026-07-14 19:57:29 +02:00
JonShard f106d1a6ff Fix positioning of BurgerBuns 2026-07-14 19:25:54 +02:00
JonShard d770195b78 Merge branch 'master' of https://git.offcoursegames.com/JonShard/VRyHungry1 2026-07-14 19:20:21 +02:00
JonShard 7ee267e6ac Add burger buns 2026-07-14 19:20:17 +02:00
146 changed files with 7563 additions and 334 deletions
+103
View File
@@ -0,0 +1,103 @@
extends Node3D
@export var target_group : String # Items with this tag can be added to the container
@export var meal_positions: Array[Node3D] = []
@export var side_positions: Array[Node3D] = []
@onready var area_3d: Area3D = $Area3D
@onready var xr_pickable: XRToolsPickable = get_parent() as XRToolsPickable
@onready var _meal_container: Node3D = $MealContainer
@onready var _side_container: Node3D = $SidesContainer
var contained_items: Array[FoodItem] # Expose nice list of others to read
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
area_3d.body_entered.connect(_on_body_entered)
if not area_3d:
push_error("Area3D node not found in container.gd")
if not xr_pickable:
push_error("XRPickable node not found in container.gd")
func _on_body_entered(body: Node3D) -> void:
# Container fill mutates world state (reparent/freeze); run only on the
# world owner. Item transforms replicate to clients via each item's
# MultiplayerSynchronizer (global_transform), so contents follow the plate.
if not NetworkManager.owns_world():
return
if not body.is_in_group(target_group):
return
var picked_by = xr_pickable.get_picked_up_by()
if picked_by and picked_by.is_in_group("station"):
# If compatible and enough space, add item
var food_item = body.get_node("FoodItem")
print("Container in station found %s of type %s: %s" % [target_group, FoodItem.Type.keys()[food_item.type], body.name])
if food_item.type == FoodItem.Type.MEAL and _meal_container.get_child_count() < meal_positions.size():
print("Container adding meal")
_add_item(body, meal_positions, _meal_container)
if food_item.type == FoodItem.Type.SIDE and _side_container.get_child_count() < side_positions.size():
print("Container adding side")
_add_item(body, side_positions, _side_container)
func _add_item(item: Node3D, positions: Array[Node3D], container_root: Node3D) -> void:
var pickable = item as XRToolsPickable
var rigidbody = item as RigidBody3D
# Drop item
if pickable:
if pickable.is_picked_up():
pickable.drop()
pickable.enabled = false
# Freeze the rigid body and disable its collisions so it doesn't fight the container
if rigidbody:
rigidbody.freeze = true
rigidbody.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
rigidbody.process_mode = PROCESS_MODE_DISABLED
# Optional: Disable collision layer/mask so it doesn't bump into other food
rigidbody.collision_layer = 0
rigidbody.collision_mask = 0
item.reparent(container_root, false) # false = discard global transform
# Get target position index
var target_slot_index = container_root.get_child_count() - 1
if target_slot_index < positions.size():
item.position = positions[target_slot_index].position
item.rotation = positions[target_slot_index].rotation
# Add to list
var food_node = item.get_node_or_null("FoodItem")
if food_node:
contained_items.append(food_node as FoodItem)
func clear() -> void:
# delete all nodes in containers and contained_items
pass
#plate (Pickalbe)
#XRGrapPoints
#container (script) (meal positions[1], side positions[4])
#area
#meals
#meal - burger
#sides
#side - chips
#side - onion rings
#tray (Pickalbe)
#XRGrapPoints
#container (script) (meal positions[4], side positions[0])
#food items
#meals
#meal - cookie
#meal - cookie
#meal - cookie
#meal - cookie
+1
View File
@@ -0,0 +1 @@
uid://6lyhyial1fh5
+21
View File
@@ -0,0 +1,21 @@
[gd_scene format=3 uid="uid://du31pqeytu8as"]
[ext_resource type="Script" uid="uid://6lyhyial1fh5" path="res://Containers/container.gd" id="1_r4cle"]
[sub_resource type="SphereShape3D" id="SphereShape3D_vlqg6"]
radius = 0.2
[node name="Container" type="Node3D" unique_id=377793422 node_paths=PackedStringArray("meal_positions")]
script = ExtResource("1_r4cle")
meal_positions = [null]
[node name="Area3D" type="Area3D" parent="." unique_id=126983781]
collision_layer = 2
collision_mask = 65540
[node name="CollisionShape3D" type="CollisionShape3D" parent="Area3D" unique_id=286964882]
shape = SubResource("SphereShape3D_vlqg6")
[node name="MealContainer" type="Node3D" parent="." unique_id=258551044]
[node name="SidesContainer" type="Node3D" parent="." unique_id=1464979199]
+97
View File
@@ -0,0 +1,97 @@
[gd_scene format=3 uid="uid://bp3v1jl8pctro"]
[ext_resource type="Script" uid="uid://bfrlpbsqg5lqr" path="res://addons/godot-xr-tools/objects/pickable.gd" id="1_nq5sa"]
[ext_resource type="PackedScene" uid="uid://du31pqeytu8as" path="res://Containers/container.tscn" id="1_s5yhh"]
[ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="2_3bkoa"]
[ext_resource type="Animation" uid="uid://db62hs5s4n2b3" path="res://addons/godot-xr-tools/hands/animations/left/Grip 4.res" id="3_6v47p"]
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="4_dhtvl"]
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_vlcpl"]
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_el51w"]
[ext_resource type="Script" path="res://Prefabs/net_pickable.gd" id="7_net"]
[sub_resource type="SceneReplicationConfig" id="Repl_item"]
properties/0/path = NodePath(".:global_transform")
properties/0/spawn = true
properties/0/replication_mode = 1
[sub_resource type="CylinderShape3D" id="CylinderShape3D_kek77"]
height = 0.0635376
radius = 0.20996094
[sub_resource type="Resource" id="Resource_2hcq0"]
script = ExtResource("4_dhtvl")
closed_pose = ExtResource("3_6v47p")
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
[sub_resource type="Resource" id="Resource_dlngj"]
script = ExtResource("4_dhtvl")
closed_pose = ExtResource("6_el51w")
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
[node name="Plate" type="RigidBody3D" unique_id=190487773]
collision_layer = 4
collision_mask = 196615
freeze_mode = 1
script = ExtResource("1_nq5sa")
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1365637864]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.007171631, 0)
shape = SubResource("CylinderShape3D_kek77")
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=1767596918]
autosmooth = true
smoothing_angle = 103.9
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=410722205]
radius = 0.2
height = 0.03
sides = 16
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="CSGCombiner3D/CSGCylinder3D" unique_id=1041060379]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.010312311, 0)
operation = 2
radius = 0.15
height = 0.03
sides = 16
[node name="CSGCylinder3D2" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=1827566539]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.027119078, 0)
radius = 0.15
height = 0.02
[node name="GrabPointHandLeft" parent="." unique_id=317886291 instance=ExtResource("2_3bkoa")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.1699356, 0.04601878, -0.08606844)
hand_pose = SubResource("Resource_2hcq0")
[node name="GrabPointHandRight" parent="." unique_id=248026585 instance=ExtResource("5_vlcpl")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.16517048, 0.042291984, -0.08604182)
hand_pose = SubResource("Resource_dlngj")
[node name="NetPickable" type="Node" parent="."]
script = ExtResource("7_net")
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("Repl_item")
[node name="Container" parent="." unique_id=377793422 node_paths=PackedStringArray("meal_positions", "side_positions") instance=ExtResource("1_s5yhh")]
target_group = "platalbe_item"
meal_positions = [NodePath("MealPositions/Slot1")]
side_positions = [NodePath("SidePositions/Slot1"), NodePath("SidePositions/Slot2"), NodePath("SidePositions/Slot3"), NodePath("SidePositions/Slot4")]
[node name="MealPositions" type="Node3D" parent="Container" unique_id=1682052877]
[node name="Slot1" type="Node3D" parent="Container/MealPositions" unique_id=306927548]
[node name="SidePositions" type="Node3D" parent="Container" unique_id=1112098031]
[node name="Slot1" type="Node3D" parent="Container/SidePositions" unique_id=1314895915]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.05862564, 0, -0.098608926)
[node name="Slot2" type="Node3D" parent="Container/SidePositions" unique_id=46117678]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.090641126, 0, -0.08815916)
[node name="Slot3" type="Node3D" parent="Container/SidePositions" unique_id=1994697744]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.08457637, 0, 0.08950478)
[node name="Slot4" type="Node3D" parent="Container/SidePositions" unique_id=379534402]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.06938871, 0, 0.072598234)
+79
View File
@@ -0,0 +1,79 @@
[gd_scene format=3 uid="uid://c0lknik4noobs"]
[ext_resource type="PackedScene" uid="uid://c8l60rnugru40" path="res://addons/godot-xr-tools/objects/pickable.tscn" id="1_g1t48"]
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="2_rwqhn"]
[ext_resource type="Animation" uid="uid://s1vqcxyqcvea" path="res://addons/godot-xr-tools/hands/animations/right/Grip 5.res" id="3_qaxva"]
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="4_wb51u"]
[ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="5_wqyr2"]
[ext_resource type="Animation" uid="uid://bediglpx0rj7i" path="res://addons/godot-xr-tools/hands/animations/left/Grip 5.res" id="6_ftab3"]
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="7_wb51u"]
[ext_resource type="Script" uid="uid://coidnv8b2yvxr" path="res://Prefabs/combine_recipe.gd" id="8_wqyr2"]
[ext_resource type="PackedScene" uid="uid://b3m2ag8g5rj4r" path="res://Items/hamburger.tscn" id="9_wb51u"]
[sub_resource type="CylinderShape3D" id="CylinderShape3D_vlqg6"]
height = 0.10708985
radius = 0.107910156
[sub_resource type="Resource" id="Resource_wkb0k"]
script = ExtResource("4_wb51u")
closed_pose = ExtResource("3_qaxva")
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
[sub_resource type="Resource" id="Resource_j68hc"]
script = ExtResource("4_wb51u")
closed_pose = ExtResource("6_ftab3")
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_hoqox"]
albedo_color = Color(0.6784191, 0.54546416, 0.016760282, 1)
[sub_resource type="Resource" id="Resource_hwyyd"]
script = ExtResource("8_wqyr2")
ingredient_id = &"cooked_burger"
result = ExtResource("9_wb51u")
metadata/_custom_type_script = "uid://coidnv8b2yvxr"
[node name="BurgerBuns" unique_id=1088240294 instance=ExtResource("1_g1t48")]
[node name="CollisionShape3D" parent="." index="0"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.05776599, 0)
shape = SubResource("CylinderShape3D_vlqg6")
[node name="GrabPointHandRight" parent="." index="1" unique_id=141610509 instance=ExtResource("2_rwqhn")]
transform = Transform3D(0.9978254, -0.06591213, 0, 0.06591213, 0.9978254, 0, 0, 0, 1, 0.08952884, 0.07439406, -0.056857675)
hand_pose = SubResource("Resource_wkb0k")
[node name="GrabPointHandLeft" parent="." index="2" unique_id=826213104 instance=ExtResource("5_wqyr2")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.08001605, 0.07612594, -0.05340144)
hand_pose = SubResource("Resource_j68hc")
[node name="CSGCombiner3DBuns" type="CSGCombiner3D" parent="." index="3" unique_id=178629624]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.021904588, 0.025076538, -0.0044593215)
autosmooth = true
smoothing_angle = 138.9
[node name="CSGCylinder3D2" type="CSGCylinder3D" parent="CSGCombiner3DBuns" index="0" unique_id=110174152]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.004122257, 0)
radius = 0.085
height = 0.04
sides = 16
material = SubResource("StandardMaterial3D_hoqox")
[node name="CSGCylinder3D3" type="CSGCylinder3D" parent="CSGCombiner3DBuns" index="1" unique_id=2003491826]
transform = Transform3D(0.82770383, -0.5611652, 0, 0.5611652, 0.82770383, 0, 0, 0, 1, -0.048392475, 0.029736161, 0)
radius = 0.085
height = 0.04
sides = 16
material = SubResource("StandardMaterial3D_hoqox")
[node name="CSGCylinder3D4" type="CSGCylinder3D" parent="CSGCombiner3DBuns/CSGCylinder3D3" index="0" unique_id=391028348]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.9604645e-08, 0.040259957, 0)
radius = 0.085
height = 0.04
sides = 16
cone = true
material = SubResource("StandardMaterial3D_hoqox")
[node name="CombinableItem" parent="." index="4" unique_id=996936271 instance=ExtResource("7_wb51u")]
id = &"burger_buns"
recipes = Array[ExtResource("8_wqyr2")]([SubResource("Resource_hwyyd")])
+10 -13
View File
@@ -6,17 +6,18 @@
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="4_mgacb"] [ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="4_mgacb"]
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_vw7i6"] [ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_vw7i6"]
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_cp3eg"] [ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_cp3eg"]
[ext_resource type="Script" path="res://combinable_item.gd" id="7_comb"] [ext_resource type="Script" path="res://Prefabs/net_pickable.gd" id="7_net"]
[ext_resource type="Script" path="res://combine_zone.gd" id="8_zone"]
[sub_resource type="SceneReplicationConfig" id="Repl_item"]
properties/0/path = NodePath(".:global_transform")
properties/0/spawn = true
properties/0/replication_mode = 1
[sub_resource type="CylinderShape3D" id="CylinderShape3D_gkni8"] [sub_resource type="CylinderShape3D" id="CylinderShape3D_gkni8"]
custom_solver_bias = 0.1 custom_solver_bias = 0.1
height = 0.1 height = 0.1
radius = 0.1 radius = 0.1
[sub_resource type="BoxShape3D" id="BoxShape3D_combine"]
size = Vector3(0.16, 0.16, 0.16)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_e28iu"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_e28iu"]
albedo_color = Color(0.06879827, 0.06879828, 0.06879828, 1) albedo_color = Color(0.06879827, 0.06879828, 0.06879828, 1)
@@ -52,12 +53,8 @@ hand_pose = SubResource("Resource_lc22d")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.06531982, 0.042291984, -0.08604182) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.06531982, 0.042291984, -0.08604182)
hand_pose = SubResource("Resource_qyiot") hand_pose = SubResource("Resource_qyiot")
[node name="CombinableItem" type="Node" parent="." index="4"] [node name="NetPickable" type="Node" parent="." index="4"]
script = ExtResource("7_comb") script = ExtResource("7_net")
id = &"charcoal"
[node name="CombineZone" type="Area3D" parent="." index="5"] [node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." index="5"]
script = ExtResource("8_zone") replication_config = SubResource("Repl_item")
[node name="CollisionShape3D" type="CollisionShape3D" parent="CombineZone"]
shape = SubResource("BoxShape3D_combine")
+26 -22
View File
@@ -6,11 +6,17 @@
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="4_hc3f7"] [ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="4_hc3f7"]
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_a0a5b"] [ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_a0a5b"]
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_bdp75"] [ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_bdp75"]
[ext_resource type="Script" uid="uid://bs7cxydoxk1cx" path="res://cookable_item.gd" id="7_hc3f7"] [ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="7_bdp75"]
[ext_resource type="PackedScene" uid="uid://cucc3gaqu2nab" path="res://Charcoal.tscn" id="8_a0a5b"] [ext_resource type="PackedScene" uid="uid://cucc3gaqu2nab" path="res://Items/Charcoal.tscn" id="8_a0a5b"]
[ext_resource type="Script" uid="uid://dounic663dn5q" path="res://combinable_item.gd" id="9_comb"] [ext_resource type="PackedScene" uid="uid://7earnjgdmwgx" path="res://Prefabs/cookable_item.tscn" id="10_65ccv"]
[ext_resource type="Script" uid="uid://e3im02nq5cye" path="res://combine_zone.gd" id="10_zone"] [ext_resource type="Script" uid="uid://coidnv8b2yvxr" path="res://Prefabs/combine_recipe.gd" id="11_recipe"]
[ext_resource type="Script" uid="uid://coidnv8b2yvxr" path="res://combine_recipe.gd" id="11_recipe"] [ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="11_vjw41"]
[ext_resource type="Script" path="res://Prefabs/net_pickable.gd" id="12_net"]
[sub_resource type="SceneReplicationConfig" id="Repl_item"]
properties/0/path = NodePath(".:global_transform")
properties/0/spawn = true
properties/0/replication_mode = 1
[sub_resource type="BoxShape3D" id="BoxShape3D_m1xbq"] [sub_resource type="BoxShape3D" id="BoxShape3D_m1xbq"]
size = Vector3(0.1, 0.1, 0.1) size = Vector3(0.1, 0.1, 0.1)
@@ -32,15 +38,13 @@ script = ExtResource("4_hc3f7")
closed_pose = ExtResource("6_bdp75") closed_pose = ExtResource("6_bdp75")
metadata/_custom_type_script = "uid://dvobm6vcfnqe8" metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
[sub_resource type="Resource" id="Resource_recipe_cube"] [sub_resource type="Resource" id="Resource_i8ixk"]
script = ExtResource("11_recipe") script = ExtResource("11_recipe")
ingredient_id = &"cube" ingredient_id = &"cube"
result = ExtResource("8_a0a5b") result = ExtResource("8_a0a5b")
metadata/_custom_type_script = "uid://coidnv8b2yvxr"
[sub_resource type="BoxShape3D" id="BoxShape3D_combine"] [node name="PickableObject" unique_id=1675596942 groups=["platalbe_item"] instance=ExtResource("1_dbtw8")]
size = Vector3(0.16, 0.16, 0.16)
[node name="PickableObject" unique_id=1675596942 instance=ExtResource("1_dbtw8")]
[node name="CollisionShape3D" parent="." index="0"] [node name="CollisionShape3D" parent="." index="0"]
shape = SubResource("BoxShape3D_m1xbq") shape = SubResource("BoxShape3D_m1xbq")
@@ -56,18 +60,18 @@ hand_pose = SubResource("Resource_lc22d")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.06531982, 0.042291984, -0.08604182) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.06531982, 0.042291984, -0.08604182)
hand_pose = SubResource("Resource_qyiot") hand_pose = SubResource("Resource_qyiot")
[node name="CookableItem" type="Node" parent="." index="4" unique_id=1139889801] [node name="CombinableItem" parent="." index="4" unique_id=996936271 instance=ExtResource("7_bdp75")]
script = ExtResource("7_hc3f7")
cooking_time = 2.0
turns_into = ExtResource("8_a0a5b")
[node name="CombinableItem" type="Node" parent="." index="5" unique_id=1265358312]
script = ExtResource("9_comb")
id = &"cube" id = &"cube"
recipes = Array[ExtResource("11_recipe")]([SubResource("Resource_recipe_cube")]) recipes = Array[ExtResource("11_recipe")]([SubResource("Resource_i8ixk")])
[node name="CombineZone" type="Area3D" parent="." index="6" unique_id=1412308162] [node name="CookableItem" parent="." index="5" unique_id=280820828 instance=ExtResource("10_65ccv")]
script = ExtResource("10_zone")
[node name="CollisionShape3D" type="CollisionShape3D" parent="CombineZone" index="0" unique_id=1926800015] [node name="FoodItem" parent="." index="6" unique_id=63948206 instance=ExtResource("11_vjw41")]
shape = SubResource("BoxShape3D_combine") id = "cube"
type = 1
[node name="NetPickable" type="Node" parent="." index="7"]
script = ExtResource("12_net")
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." index="8"]
replication_config = SubResource("Repl_item")
+19 -30
View File
@@ -7,15 +7,18 @@
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="5_w8sii"] [ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="5_w8sii"]
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="6_mvnl5"] [ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="6_mvnl5"]
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="7_wqxjj"] [ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="7_wqxjj"]
[ext_resource type="Script" uid="uid://bs7cxydoxk1cx" path="res://cookable_item.gd" id="8_t4l03"] [ext_resource type="PackedScene" uid="uid://7earnjgdmwgx" path="res://Prefabs/cookable_item.tscn" id="9_hwyyd"]
[ext_resource type="PackedScene" uid="uid://cucc3gaqu2nab" path="res://Charcoal.tscn" id="9_x1g8b"] [ext_resource type="PackedScene" uid="uid://cfc4ho67u4r5e" path="res://Items/cooked_burger.tscn" id="9_r8jvx"]
[ext_resource type="Script" uid="uid://dounic663dn5q" path="res://combinable_item.gd" id="10_qe0ug"] [ext_resource type="Script" path="res://Prefabs/net_pickable.gd" id="12_net"]
[ext_resource type="Script" uid="uid://coidnv8b2yvxr" path="res://combine_recipe.gd" id="11_dq5p0"]
[ext_resource type="Script" uid="uid://e3im02nq5cye" path="res://combine_zone.gd" id="12_7llnd"] [sub_resource type="SceneReplicationConfig" id="Repl_item"]
properties/0/path = NodePath(".:global_transform")
properties/0/spawn = true
properties/0/replication_mode = 1
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"] [sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
height = 0.063323975 height = 0.0338974
radius = 0.12011719 radius = 0.08544922
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_bvwq1"] [sub_resource type="StandardMaterial3D" id="StandardMaterial3D_bvwq1"]
albedo_texture = ExtResource("2_r8jvx") albedo_texture = ExtResource("2_r8jvx")
@@ -30,23 +33,16 @@ script = ExtResource("5_w8sii")
closed_pose = ExtResource("7_wqxjj") closed_pose = ExtResource("7_wqxjj")
metadata/_custom_type_script = "uid://dvobm6vcfnqe8" metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
[sub_resource type="Resource" id="Resource_recipe_cube"]
script = ExtResource("11_dq5p0")
ingredient_id = &"cube"
result = ExtResource("9_x1g8b")
[sub_resource type="BoxShape3D" id="BoxShape3D_combine"]
size = Vector3(0.16, 0.16, 0.16)
[node name="burger" unique_id=1675596942 instance=ExtResource("1_fco8w")] [node name="burger" unique_id=1675596942 instance=ExtResource("1_fco8w")]
[node name="CollisionShape3D" parent="." index="0"] [node name="CollisionShape3D" parent="." index="0"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.0005453825, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.00077831745, 0)
shape = SubResource("CylinderShape3D_fco8w") shape = SubResource("CylinderShape3D_fco8w")
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="." index="1" unique_id=59319982] [node name="CSGCylinder3D" type="CSGCylinder3D" parent="." index="1" unique_id=59319982]
radius = 0.1 radius = 0.08300781
height = 0.03 height = 0.03
sides = 16
material = SubResource("StandardMaterial3D_bvwq1") material = SubResource("StandardMaterial3D_bvwq1")
[node name="GrabPointHandLeft" parent="." index="2" unique_id=1571481674 instance=ExtResource("3_qixic")] [node name="GrabPointHandLeft" parent="." index="2" unique_id=1571481674 instance=ExtResource("3_qixic")]
@@ -57,18 +53,11 @@ hand_pose = SubResource("Resource_lc22d")
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.06531982, 0.042291984, -0.08604182) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.06531982, 0.042291984, -0.08604182)
hand_pose = SubResource("Resource_qyiot") hand_pose = SubResource("Resource_qyiot")
[node name="CookableItem" type="Node" parent="." index="4" unique_id=1139889801] [node name="CookableItem" parent="." index="4" unique_id=280820828 instance=ExtResource("9_hwyyd")]
script = ExtResource("8_t4l03") turns_into = ExtResource("9_r8jvx")
cooking_time = 2.0
turns_into = ExtResource("9_x1g8b")
[node name="CombinableItem" type="Node" parent="." index="5" unique_id=1265358312] [node name="NetPickable" type="Node" parent="." index="5"]
script = ExtResource("10_qe0ug") script = ExtResource("12_net")
id = &"burger"
recipes = Array[ExtResource("11_dq5p0")]([SubResource("Resource_recipe_cube")])
[node name="CombineZone" type="Area3D" parent="." index="6" unique_id=1412308162] [node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="." index="6"]
script = ExtResource("12_7llnd") replication_config = SubResource("Repl_item")
[node name="CollisionShape3D" type="CollisionShape3D" parent="CombineZone" index="0" unique_id=1926800015]
shape = SubResource("BoxShape3D_combine")
+60
View File
@@ -0,0 +1,60 @@
[gd_scene format=3 uid="uid://cfc4ho67u4r5e"]
[ext_resource type="PackedScene" uid="uid://c8l60rnugru40" path="res://addons/godot-xr-tools/objects/pickable.tscn" id="1_ut7mg"]
[ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="3_pkevi"]
[ext_resource type="Animation" uid="uid://db62hs5s4n2b3" path="res://addons/godot-xr-tools/hands/animations/left/Grip 4.res" id="4_dq7qw"]
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="5_ychvb"]
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="6_etdv2"]
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="7_rl64h"]
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="10_ut7mg"]
[ext_resource type="PackedScene" uid="uid://7earnjgdmwgx" path="res://Prefabs/cookable_item.tscn" id="11_5ch2b"]
[ext_resource type="Script" uid="uid://coidnv8b2yvxr" path="res://Prefabs/combine_recipe.gd" id="11_rjayc"]
[ext_resource type="PackedScene" uid="uid://b3m2ag8g5rj4r" path="res://Items/hamburger.tscn" id="11_ut7mg"]
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
height = 0.04777527
radius = 0.08300781
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_6l01i"]
albedo_color = Color(0.29, 0.101500005, 0, 1)
[sub_resource type="Resource" id="Resource_lc22d"]
script = ExtResource("5_ychvb")
closed_pose = ExtResource("4_dq7qw")
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
[sub_resource type="Resource" id="Resource_qyiot"]
script = ExtResource("5_ychvb")
closed_pose = ExtResource("7_rl64h")
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
[sub_resource type="Resource" id="Resource_wi1yl"]
script = ExtResource("11_rjayc")
ingredient_id = &"burger_buns"
result = ExtResource("11_ut7mg")
metadata/_custom_type_script = "uid://coidnv8b2yvxr"
[node name="CookedBurger" unique_id=1675596942 instance=ExtResource("1_ut7mg")]
[node name="CollisionShape3D" parent="." index="0"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.0072289705, 0)
shape = SubResource("CylinderShape3D_fco8w")
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="." index="1" unique_id=59319982]
radius = 0.08300781
height = 0.03
sides = 16
material = SubResource("StandardMaterial3D_6l01i")
[node name="GrabPointHandLeft" parent="." index="2" unique_id=1571481674 instance=ExtResource("3_pkevi")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.067650706, 0.04601878, -0.08606844)
hand_pose = SubResource("Resource_lc22d")
[node name="GrabPointHandRight" parent="." index="3" unique_id=514404634 instance=ExtResource("6_etdv2")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.06531982, 0.042291984, -0.08604182)
hand_pose = SubResource("Resource_qyiot")
[node name="CombinableItem" parent="." index="4" unique_id=434304805 instance=ExtResource("10_ut7mg")]
recipes = Array[ExtResource("11_rjayc")]([SubResource("Resource_wi1yl")])
[node name="CookableItem" parent="." index="5" unique_id=280820828 instance=ExtResource("11_5ch2b")]
+81
View File
@@ -0,0 +1,81 @@
[gd_scene format=3 uid="uid://b3m2ag8g5rj4r"]
[ext_resource type="PackedScene" uid="uid://c8l60rnugru40" path="res://addons/godot-xr-tools/objects/pickable.tscn" id="1_3gf3l"]
[ext_resource type="PackedScene" uid="uid://c25yxb0vt53vc" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_left.tscn" id="2_0mkco"]
[ext_resource type="Animation" uid="uid://db62hs5s4n2b3" path="res://addons/godot-xr-tools/hands/animations/left/Grip 4.res" id="3_6tnvi"]
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="4_cljtj"]
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_dunns"]
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_fh0f6"]
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="7_yy3y8"]
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
height = 0.10392761
radius = 0.096191406
[sub_resource type="Resource" id="Resource_lc22d"]
script = ExtResource("4_cljtj")
closed_pose = ExtResource("3_6tnvi")
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
[sub_resource type="Resource" id="Resource_qyiot"]
script = ExtResource("4_cljtj")
closed_pose = ExtResource("6_fh0f6")
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_3gf3l"]
albedo_color = Color(0.6784191, 0.54546416, 0.016760282, 1)
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_6l01i"]
albedo_color = Color(0.29, 0.101500005, 0, 1)
[node name="Hamburger" unique_id=1675596942 groups=["platalbe_item"] instance=ExtResource("1_3gf3l")]
gravity_scale = 0.4
[node name="CollisionShape3D" parent="." index="0"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.02, 0.02, 0)
shape = SubResource("CylinderShape3D_fco8w")
[node name="GrabPointHandLeft" parent="." index="1" unique_id=1571481674 instance=ExtResource("2_0mkco")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.067650706, 0.04601878, -0.08606844)
hand_pose = SubResource("Resource_lc22d")
[node name="GrabPointHandRight" parent="." index="2" unique_id=514404634 instance=ExtResource("5_dunns")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.06531982, 0.042291984, -0.08604182)
hand_pose = SubResource("Resource_qyiot")
[node name="CSGCombiner3DBuns" type="CSGCombiner3D" parent="." index="3" unique_id=678881053]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.021904588, 0.025076538, -0.0044593215)
autosmooth = true
smoothing_angle = 138.9
[node name="CSGCylinder3D2" type="CSGCylinder3D" parent="CSGCombiner3DBuns" index="0" unique_id=776920274]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.022625268, -0.03479519, 0.0027926378)
radius = 0.085
height = 0.04
sides = 16
material = SubResource("StandardMaterial3D_3gf3l")
[node name="CSGCylinder3D3" type="CSGCylinder3D" parent="CSGCombiner3DBuns" index="1" unique_id=448728548]
transform = Transform3D(0.82770383, -0.5611652, 0, 0.5611652, 0.82770383, 0, 0, 0, 1, -0.07384333, 0.015895892, 0)
radius = 0.085
height = 0.04
sides = 16
material = SubResource("StandardMaterial3D_3gf3l")
[node name="CSGCylinder3D4" type="CSGCylinder3D" parent="CSGCombiner3DBuns/CSGCylinder3D3" index="0" unique_id=837229235]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.9604645e-08, 0.040259957, 0)
radius = 0.085
height = 0.04
sides = 16
cone = true
material = SubResource("StandardMaterial3D_3gf3l")
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="." index="4" unique_id=59319982]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.02356495, 0)
radius = 0.08300781
height = 0.03
sides = 16
material = SubResource("StandardMaterial3D_6l01i")
[node name="FoodItem" parent="." index="5" unique_id=63948206 instance=ExtResource("7_yy3y8")]
type = 0
+49
View File
@@ -0,0 +1,49 @@
extends Control
## The 2D UI rendered inside the world-space menu (XRToolsViewport2DIn3D).
## Lets the player type an IP on a numeric keypad and pick Host / Join / Solo.
## Pure UI: it emits intents; main.gd performs the networking.
signal host_pressed()
signal join_pressed(ip: String)
signal solo_pressed()
#var _ip := "127.0.0.1"
var _ip := "51.175.10.161"
@onready var _ip_label: Label = %IPLabel
@onready var _status: Label = %Status
@onready var _keypad: GridContainer = %Keypad
@onready var _host_btn: Button = %HostButton
@onready var _join_btn: Button = %JoinButton
@onready var _solo_btn: Button = %SoloButton
func _ready() -> void:
for child in _keypad.get_children():
if child is Button:
child.pressed.connect(_on_key.bind(child.text))
_host_btn.pressed.connect(func(): host_pressed.emit())
_join_btn.pressed.connect(func(): join_pressed.emit(_ip))
_solo_btn.pressed.connect(func(): solo_pressed.emit())
_refresh()
func _on_key(key: String) -> void:
match key:
"DEL":
_ip = _ip.substr(0, max(0, _ip.length() - 1))
_:
if _ip.length() < 21:
_ip += key
_refresh()
func _refresh() -> void:
_ip_label.text = _ip if not _ip.is_empty() else "_"
## Called by main.gd (via network_menu) to show connection feedback.
func set_status(text: String) -> void:
if _status:
_status.text = text
+1
View File
@@ -0,0 +1 @@
uid://dvivyrspjcjgm
+172
View File
@@ -0,0 +1,172 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://Net/join_panel.gd" id="1_panel"]
[node name="JoinPanel" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1_panel")
[node name="Background" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.09, 0.1, 0.13, 1)
[node name="Margin" type="MarginContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
theme_override_constants/margin_left = 24
theme_override_constants/margin_top = 16
theme_override_constants/margin_right = 24
theme_override_constants/margin_bottom = 16
[node name="VBox" type="VBoxContainer" parent="Margin"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="Title" type="Label" parent="Margin/VBox"]
layout_mode = 2
theme_override_font_sizes/font_size = 26
text = "VRyHungry — Multiplayer"
horizontal_alignment = 1
[node name="IPLabel" type="Label" parent="Margin/VBox"]
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 34
text = "127.0.0.1"
horizontal_alignment = 1
[node name="Status" type="Label" parent="Margin/VBox"]
unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_color = Color(0.8, 0.8, 0.5, 1)
theme_override_font_sizes/font_size = 18
text = "Enter host IP, then Join"
horizontal_alignment = 1
[node name="Keypad" type="GridContainer" parent="Margin/VBox"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/h_separation = 8
theme_override_constants/v_separation = 8
columns = 3
[node name="B1" type="Button" parent="Margin/VBox/Keypad"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_font_sizes/font_size = 26
text = "1"
[node name="B2" type="Button" parent="Margin/VBox/Keypad"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_font_sizes/font_size = 26
text = "2"
[node name="B3" type="Button" parent="Margin/VBox/Keypad"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_font_sizes/font_size = 26
text = "3"
[node name="B4" type="Button" parent="Margin/VBox/Keypad"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_font_sizes/font_size = 26
text = "4"
[node name="B5" type="Button" parent="Margin/VBox/Keypad"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_font_sizes/font_size = 26
text = "5"
[node name="B6" type="Button" parent="Margin/VBox/Keypad"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_font_sizes/font_size = 26
text = "6"
[node name="B7" type="Button" parent="Margin/VBox/Keypad"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_font_sizes/font_size = 26
text = "7"
[node name="B8" type="Button" parent="Margin/VBox/Keypad"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_font_sizes/font_size = 26
text = "8"
[node name="B9" type="Button" parent="Margin/VBox/Keypad"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_font_sizes/font_size = 26
text = "9"
[node name="BDot" type="Button" parent="Margin/VBox/Keypad"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_font_sizes/font_size = 26
text = "."
[node name="B0" type="Button" parent="Margin/VBox/Keypad"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_font_sizes/font_size = 26
text = "0"
[node name="BDel" type="Button" parent="Margin/VBox/Keypad"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_font_sizes/font_size = 22
text = "DEL"
[node name="Buttons" type="HBoxContainer" parent="Margin/VBox"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="HostButton" type="Button" parent="Margin/VBox/Buttons"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
custom_minimum_size = Vector2(0, 56)
theme_override_font_sizes/font_size = 24
text = "HOST"
[node name="JoinButton" type="Button" parent="Margin/VBox/Buttons"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
custom_minimum_size = Vector2(0, 56)
theme_override_font_sizes/font_size = 24
text = "JOIN"
[node name="SoloButton" type="Button" parent="Margin/VBox/Buttons"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
custom_minimum_size = Vector2(0, 56)
theme_override_font_sizes/font_size = 24
text = "SOLO"
+319
View File
@@ -0,0 +1,319 @@
extends Node
## Client-server session manager for VRyHungry (listen-server model).
##
## Registered as the "NetworkManager" autoload. Owns transport (ENet), tracks
## the session, and is the single place that reassigns multiplayer authority
## (only the server does so). The world scene (main.gd) registers its spawners
## here via [method register_world]; higher layers (players, items, stations)
## build on top of this in later phases.
const DEFAULT_PORT := 24565
const MAX_CLIENTS := 7
## Emitted on every peer (including the server for its own local player) when a
## player peer joins. On the server this fires for each remote peer; the server
## uses it to spawn that peer's player.
signal player_joined(peer_id: int)
signal player_left(peer_id: int)
signal session_started(is_server: bool)
signal session_ended()
signal connection_failed()
# World hooks, registered by main.gd once the scene tree exists.
var _world: Node = null
var _players_spawner: MultiplayerSpawner = null
var _items_spawner: MultiplayerSpawner = null
var _log_file: FileAccess
func _ready() -> void:
_open_log()
multiplayer.peer_connected.connect(_on_peer_connected)
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
multiplayer.connected_to_server.connect(_on_connected_to_server)
multiplayer.connection_failed.connect(_on_connection_failed)
multiplayer.server_disconnected.connect(_on_server_disconnected)
# --- Public API ------------------------------------------------------------
## Start hosting. The host is peer 1 and also plays (listen server).
func host(port: int = DEFAULT_PORT) -> Error:
var peer := ENetMultiplayerPeer.new()
var err := peer.create_server(port, MAX_CLIENTS)
if err != OK:
log_line("HOST failed to create_server on port %d: %s" % [port, error_string(err)])
return err
multiplayer.multiplayer_peer = peer
log_line("HOST started on port %d (peer id %d)" % [port, multiplayer.get_unique_id()])
session_started.emit(true)
# The host's own local player joins immediately.
_on_player_present(multiplayer.get_unique_id())
return OK
## Join an existing host.
func join(address: String = "127.0.0.1", port: int = DEFAULT_PORT) -> Error:
var peer := ENetMultiplayerPeer.new()
var err := peer.create_client(address, port)
if err != OK:
log_line("JOIN failed to create_client %s:%d: %s" % [address, port, error_string(err)])
return err
multiplayer.multiplayer_peer = peer
log_line("JOIN connecting to %s:%d ..." % [address, port])
return OK
## Leave the session and tear down transport.
func leave() -> void:
_go_offline()
log_line("Session ended")
session_ended.emit()
# Restore Godot's default OfflineMultiplayerPeer (rather than leaving the peer
# null), so is_multiplayer_authority()/get_unique_id() keep working while we are
# back in single-player / menu state.
func _go_offline() -> void:
if multiplayer.multiplayer_peer:
multiplayer.multiplayer_peer.close()
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
# --- Item spawning ---------------------------------------------------------
## Spawn a networked item. Server-only when online (replicates to all peers via
## the ItemsSpawner); works directly when offline. Returns the new node on the
## machine that owns spawning, else null.
func spawn_item(scene_path: String, xform: Transform3D) -> Node:
if is_online() and not is_server():
return null
var data := {"scene": scene_path, "xform": xform}
if is_online() and _items_spawner:
return _items_spawner.spawn(data)
# Offline: instantiate directly under the world.
var inst := _spawn_item_from_data(data)
if _world and inst:
_world.add_child(inst)
return inst
# MultiplayerSpawner custom spawn function: runs on every peer to build the node
# from the replicated payload.
func _spawn_item_from_data(data: Variant) -> Node:
var scene: PackedScene = load(data["scene"])
if not scene:
push_error("spawn_item: could not load scene %s" % str(data.get("scene")))
return null
var inst := scene.instantiate()
if inst is Node3D:
inst.transform = data["xform"]
return inst
# --- Item grab-authority transfer -----------------------------------------
## A client (or host) requests authority over an item it just grabbed. Runs on
## the server. If the item was snapped into a station, the station releases it
## so the grabber cleanly takes ownership.
@rpc("any_peer", "reliable")
func request_item_authority(item_path: NodePath) -> void:
if not is_server():
return
var sender := multiplayer.get_remote_sender_id()
# Assign authority + held state first (disables the item on the server so its
# snap zone won't re-grab it), then release it from any station.
_set_item_authority.rpc(item_path, sender)
var item := get_node_or_null(item_path)
if item:
_release_from_snap_zones(item)
## A player releases an item, forwarding its throw velocity so the server can
## resume simulating it. Runs on the server. If released next to a station, the
## server snaps it in (server-authoritative placement).
@rpc("any_peer", "reliable")
func release_item_authority(item_path: NodePath, lin: Vector3, ang: Vector3) -> void:
if not is_server():
return
_set_item_authority.rpc(item_path, 1)
var item := get_node_or_null(item_path)
if item is RigidBody3D:
item.freeze = false
item.linear_velocity = lin
item.angular_velocity = ang
_try_snap_into_station(item)
# All station snap zones in the world (nodes in the "station" group).
func _station_snap_zones() -> Array:
var zones := []
for station in get_tree().get_nodes_in_group("station"):
var zone = station.get_node_or_null("XRToolsSnapZone")
if zone:
zones.append(zone)
return zones
# If the item is snapped into any station, drop it from that station.
func _release_from_snap_zones(item: Node) -> void:
for zone in _station_snap_zones():
if zone.picked_up_object == item:
zone.drop_object()
# Snap the item into the nearest empty station snap zone within grab range.
func _try_snap_into_station(item: Node) -> void:
if not (item is Node3D):
return
for zone in _station_snap_zones():
if is_instance_valid(zone.picked_up_object):
continue
if zone.global_position.distance_to(item.global_position) <= zone.grab_distance:
zone.pick_up_object(item)
return
# Server broadcasts an authority assignment so every peer agrees on who owns the
# item (set_multiplayer_authority is a local call and must run everywhere).
@rpc("authority", "call_local", "reliable")
func _set_item_authority(item_path: NodePath, peer: int) -> void:
var item := get_node_or_null(item_path)
if not item:
return
item.set_multiplayer_authority(peer) # recursive: item + synchronizer + NetPickable
var np := item.get_node_or_null("NetPickable")
if np:
np.net_held_by = 0 if peer == 1 else peer
np.apply_held_state()
func is_server() -> bool:
return is_online() and multiplayer.is_server()
## True only when a real ENet session is active. Godot installs a default
## OfflineMultiplayerPeer, so a non-null peer alone does not mean "online".
func is_online() -> bool:
var p := multiplayer.multiplayer_peer
return p != null and not (p is OfflineMultiplayerPeer)
## True on the machine that owns authoritative world logic: the server when
## online, or the single player when offline. Station logic and spawning should
## only run where this is true, so state has one source of truth.
func owns_world() -> bool:
return not is_online() or is_server()
# --- Station work-progress seam -------------------------------------------
## Reusable entry point for a client to contribute work to a station (e.g. a
## future chopping/gesture station). The client detects the gesture locally and
## calls this; the server validates and accumulates. Timer-driven stations like
## the Hob don't need it, but it is the drop-in seam for input-driven ones.
@rpc("any_peer", "reliable")
func submit_work(station_path: NodePath, amount: float) -> void:
if not is_server():
return
var station := get_node_or_null(station_path)
if station and station.has_method("add_work"):
station.add_work(multiplayer.get_remote_sender_id(), amount)
## Called by main.gd once the world scene is ready, passing its spawners.
func register_world(world: Node, players_spawner: MultiplayerSpawner, items_spawner: MultiplayerSpawner) -> void:
_world = world
_players_spawner = players_spawner
_items_spawner = items_spawner
if _items_spawner:
_items_spawner.spawn_function = _spawn_item_from_data
log_line("World registered (players_spawner=%s items_spawner=%s)" % [str(players_spawner != null), str(items_spawner != null)])
## Called by main.gd after it has registered the world and connected its
## player_joined/left listeners. Kicks off any command-line driven session so
## that session signals never fire before the world is listening.
func world_ready() -> void:
_handle_cmdline()
# --- Session signal handlers ----------------------------------------------
func _on_peer_connected(peer_id: int) -> void:
log_line("peer_connected: %d" % peer_id)
# Only the server reacts by materialising that peer's player.
if is_server():
_on_player_present(peer_id)
func _on_peer_disconnected(peer_id: int) -> void:
log_line("peer_disconnected: %d" % peer_id)
if is_server():
_on_player_absent(peer_id)
func _on_connected_to_server() -> void:
log_line("connected_to_server (my id=%d)" % multiplayer.get_unique_id())
session_started.emit(false)
func _on_connection_failed() -> void:
log_line("connection_failed")
_go_offline()
connection_failed.emit()
func _on_server_disconnected() -> void:
log_line("server_disconnected")
_go_offline()
session_ended.emit()
# Player materialise/dematerialise. Phase 2 wires these to the PlayersSpawner;
# for now they announce presence so the transport layer is independently testable.
func _on_player_present(peer_id: int) -> void:
log_line("player_present: %d" % peer_id)
player_joined.emit(peer_id)
func _on_player_absent(peer_id: int) -> void:
log_line("player_absent: %d" % peer_id)
player_left.emit(peer_id)
# --- Command-line driven test bootstrap -----------------------------------
func _handle_cmdline() -> void:
var args := OS.get_cmdline_user_args()
if args.has("--server"):
log_line("cmdline: --server")
host()
elif args.has("--join"):
var idx := args.find("--join")
var addr := "127.0.0.1"
if idx + 1 < args.size():
addr = args[idx + 1]
log_line("cmdline: --join %s" % addr)
join(addr)
# --- Logging ---------------------------------------------------------------
func _open_log() -> void:
var dir := OS.get_environment("TEMP")
if dir.is_empty():
dir = OS.get_environment("TMPDIR")
if dir.is_empty():
dir = "user://"
var path := dir.path_join("vryhungry_net_%d.log" % OS.get_process_id())
_log_file = FileAccess.open(path, FileAccess.WRITE)
log_line("=== NetworkManager log (pid %d) ===" % OS.get_process_id())
func log_line(s: String) -> void:
var id := 0
var p := multiplayer.multiplayer_peer
if p != null and p.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
id = multiplayer.get_unique_id()
var line := "[NET %d] %s" % [id, s]
print(line)
if _log_file:
_log_file.store_line(line)
_log_file.flush()
+1
View File
@@ -0,0 +1 @@
uid://deefoory0vqmm
+37
View File
@@ -0,0 +1,37 @@
extends Node3D
## World-space main menu: hosts the 2D join_panel on an XRToolsViewport2DIn3D so
## the player can point at it with a controller laser. Re-emits the panel's
## intents; main.gd drives the actual networking.
signal host_pressed()
signal join_pressed(ip: String)
signal solo_pressed()
@onready var _vp: XRToolsViewport2DIn3D = $Viewport2Din3D
func _ready() -> void:
# The viewport instantiates its 2D scene during its own _ready; connect once
# that has happened.
_connect_scene.call_deferred()
func _connect_scene() -> void:
_vp.connect_scene_signal("host_pressed", func(): host_pressed.emit())
_vp.connect_scene_signal("join_pressed", func(ip): join_pressed.emit(ip))
_vp.connect_scene_signal("solo_pressed", func(): solo_pressed.emit())
func show_menu() -> void:
visible = true
func hide_menu() -> void:
visible = false
func set_status(text: String) -> void:
var scene := _vp.get_scene_instance()
if scene and scene.has_method("set_status"):
scene.set_status(text)
+1
View File
@@ -0,0 +1 @@
uid://1o2nca75po1f
+13
View File
@@ -0,0 +1,13 @@
[gd_scene load_steps=4 format=3]
[ext_resource type="Script" path="res://Net/network_menu.gd" id="1_menu"]
[ext_resource type="PackedScene" uid="uid://clujaf3u776a3" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d.tscn" id="2_vp"]
[ext_resource type="PackedScene" path="res://Net/join_panel.tscn" id="3_panel"]
[node name="NetworkMenu" type="Node3D"]
script = ExtResource("1_menu")
[node name="Viewport2Din3D" parent="." instance=ExtResource("2_vp")]
screen_size = Vector2(0.6, 0.4)
scene = ExtResource("3_panel")
viewport_size = Vector2(600, 400)
+48
View File
@@ -0,0 +1,48 @@
[gd_scene load_steps=7 format=3 uid="uid://cnetplayer0001"]
[ext_resource type="Script" path="res://Player/player.gd" id="1_player"]
[sub_resource type="SceneReplicationConfig" id="Repl_player"]
properties/0/path = NodePath(".:head_xform")
properties/0/spawn = true
properties/0/replication_mode = 1
properties/1/path = NodePath(".:left_xform")
properties/1/spawn = true
properties/1/replication_mode = 1
properties/2/path = NodePath(".:right_xform")
properties/2/spawn = true
properties/2/replication_mode = 1
properties/3/path = NodePath(".:net_heartbeat")
properties/3/spawn = true
properties/3/replication_mode = 1
[sub_resource type="StandardMaterial3D" id="Mat_head"]
albedo_color = Color(0.85, 0.7, 0.55, 1)
[sub_resource type="BoxMesh" id="Mesh_head"]
material = SubResource("Mat_head")
size = Vector3(0.18, 0.24, 0.16)
[sub_resource type="StandardMaterial3D" id="Mat_hand"]
albedo_color = Color(0.2, 0.5, 0.9, 1)
[sub_resource type="BoxMesh" id="Mesh_hand"]
material = SubResource("Mat_hand")
size = Vector3(0.09, 0.05, 0.12)
[node name="Player" type="Node3D"]
script = ExtResource("1_player")
[node name="Avatar" type="Node3D" parent="."]
[node name="Head" type="MeshInstance3D" parent="Avatar"]
mesh = SubResource("Mesh_head")
[node name="LeftHand" type="MeshInstance3D" parent="Avatar"]
mesh = SubResource("Mesh_hand")
[node name="RightHand" type="MeshInstance3D" parent="Avatar"]
mesh = SubResource("Mesh_hand")
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("Repl_player")
+80
View File
@@ -0,0 +1,80 @@
extends Node3D
## One instance per connected peer. The owning peer is the multiplayer
## authority. The local (authority) player builds the full XR rig from
## XROrigin.tscn and feeds its head/hand poses into synced properties; remote
## players show a lightweight avatar driven by those synced poses.
const RIG_SCENE := preload("res://XROrigin.tscn")
# Synced avatar pose. Authority writes these from the XR trackers each frame;
# remotes read them onto the Avatar. net_heartbeat is a monotonic counter used
# to confirm replication is live (also handy for debugging).
@export var head_xform: Transform3D = Transform3D.IDENTITY
@export var left_xform: Transform3D = Transform3D.IDENTITY
@export var right_xform: Transform3D = Transform3D.IDENTITY
@export var net_heartbeat: int = 0
var _rig: Node3D
var _camera: Node3D
var _left: Node3D
var _right: Node3D
var _is_local := false
@onready var _avatar: Node3D = $Avatar
@onready var _avatar_head: Node3D = $Avatar/Head
@onready var _avatar_left: Node3D = $Avatar/LeftHand
@onready var _avatar_right: Node3D = $Avatar/RightHand
func _enter_tree() -> void:
# The spawner names each player by its owning peer id.
set_multiplayer_authority(str(name).to_int())
# (replication config is authored in Player.tscn's MultiplayerSynchronizer)
func _ready() -> void:
_is_local = is_multiplayer_authority()
if _is_local:
_setup_local()
else:
_setup_remote()
if Engine.has_singleton("NetworkManager") or get_node_or_null("/root/NetworkManager"):
NetworkManager.log_line("Player %s ready (authority=%d local=%s)" % [name, get_multiplayer_authority(), str(_is_local)])
func _setup_local() -> void:
# We see through the headset; hide our own avatar mesh.
_avatar.visible = false
_rig = RIG_SCENE.instantiate()
add_child(_rig)
_camera = _rig.get_node_or_null("XRCamera3D")
_left = _rig.get_node_or_null("XRControllerLeftHand")
_right = _rig.get_node_or_null("XRControllerRightHand")
_enable_xr()
func _enable_xr() -> void:
var xr := XRServer.find_interface("OpenXR")
if xr and xr.is_initialized():
get_viewport().use_xr = true
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
func _setup_remote() -> void:
_avatar.visible = true
func _process(_delta: float) -> void:
if _is_local:
if _camera:
head_xform = _camera.global_transform
left_xform = _left.global_transform
right_xform = _right.global_transform
net_heartbeat += 1
else:
_avatar_head.global_transform = head_xform
_avatar_left.global_transform = left_xform
_avatar_right.global_transform = right_xform
+1
View File
@@ -0,0 +1 @@
uid://ndrfa2xle033
+19
View File
@@ -0,0 +1,19 @@
[gd_scene format=3 uid="uid://3lr2dhy62rhk"]
[ext_resource type="Script" uid="uid://dounic663dn5q" path="res://Prefabs/combinable_item.gd" id="1_q21ti"]
[ext_resource type="Script" uid="uid://coidnv8b2yvxr" path="res://Prefabs/combine_recipe.gd" id="2_anbu7"]
[ext_resource type="Script" uid="uid://e3im02nq5cye" path="res://Prefabs/combine_zone.gd" id="4_0om48"]
[sub_resource type="BoxShape3D" id="BoxShape3D_combine"]
size = Vector3(0.16, 0.16, 0.16)
[node name="CombinableItem" type="Node3D" unique_id=996936271]
script = ExtResource("1_q21ti")
id = &"some_food_item"
recipes = Array[ExtResource("2_anbu7")]([null])
[node name="CombineZone" type="Area3D" parent="." unique_id=1514178016]
script = ExtResource("4_0om48")
[node name="CollisionShape3D" type="CollisionShape3D" parent="CombineZone" unique_id=706892516]
shape = SubResource("BoxShape3D_combine")
+12 -7
View File
@@ -8,7 +8,7 @@ extends Area3D
## incoming item is consumed. ## incoming item is consumed.
# The pickable this zone belongs to (its parent). # The pickable this zone belongs to (its parent).
@onready var _item: XRToolsPickable = get_parent() as XRToolsPickable @onready var _item: XRToolsPickable = get_parent().get_parent() as XRToolsPickable
# The base item's recipe data. # The base item's recipe data.
@onready var _combinable: CombinableItem = _find_combinable(_item) @onready var _combinable: CombinableItem = _find_combinable(_item)
@@ -28,7 +28,7 @@ func _ready() -> void:
set_deferred("monitorable", false) set_deferred("monitorable", false)
if not _item: if not _item:
push_error("CombineZone must be a child of an XRToolsPickable.") push_error("CombineZone must be a grand child of an XRToolsPickable.")
return return
if not _combinable: if not _combinable:
push_error("CombineZone requires a CombinableItem sibling on ", _item.name, ".") push_error("CombineZone requires a CombinableItem sibling on ", _item.name, ".")
@@ -54,6 +54,12 @@ func _on_body_entered(body: Node3D) -> void:
if _combining or body == _item: if _combining or body == _item:
return return
# Combining is a world-state change: only the server/offline host runs it.
# (Clients never have the base item snapped, so their zone won't monitor, but
# guard explicitly in case of overlap during authority handoff.)
if not NetworkManager.owns_world():
return
var other := _find_combinable(body) var other := _find_combinable(body)
if not other: if not other:
return return
@@ -75,11 +81,9 @@ func _combine(other_body: Node3D, result: PackedScene) -> void:
_combining = false _combining = false
return return
# Spawn the result at the base item's location, in world space. # Spawn the result (networked) at the base item's location, in world space.
var base_transform := _item.global_transform var base_transform := _item.global_transform
var result_instance: Node3D = result.instantiate() var result_instance := NetworkManager.spawn_item(result.resource_path, base_transform)
_item.get_tree().current_scene.add_child(result_instance)
result_instance.global_transform = base_transform
# Free the base item and consume the incoming item. # Free the base item and consume the incoming item.
snap_zone.drop_object() snap_zone.drop_object()
@@ -90,7 +94,8 @@ func _combine(other_body: Node3D, result: PackedScene) -> void:
other_body.queue_free() other_body.queue_free()
# Snap the result into the now-empty zone. # Snap the result into the now-empty zone.
snap_zone.pick_up_object(result_instance) if result_instance:
snap_zone.pick_up_object(result_instance)
# Find a CombinableItem among the direct children of [param node]. # Find a CombinableItem among the direct children of [param node].
+9
View File
@@ -0,0 +1,9 @@
[gd_scene format=3 uid="uid://7earnjgdmwgx"]
[ext_resource type="Script" uid="uid://bs7cxydoxk1cx" path="res://Prefabs/cookable_item.gd" id="1_6hwx6"]
[ext_resource type="PackedScene" uid="uid://cucc3gaqu2nab" path="res://Items/Charcoal.tscn" id="2_rpt7j"]
[node name="CookableItem" type="Node" unique_id=280820828]
script = ExtResource("1_6hwx6")
cooking_time = 2.0
turns_into = ExtResource("2_rpt7j")
+8
View File
@@ -0,0 +1,8 @@
class_name FoodItem
extends Node
# Define the Enum at the top of your script
enum Type { MEAL, SIDE, INGREDIENT, NONE }
@export var id: String
@export var type: Type = Type.NONE
+1
View File
@@ -0,0 +1 @@
uid://5tpoohkopryv
+8
View File
@@ -0,0 +1,8 @@
[gd_scene format=3 uid="uid://bfj80jh13t6e5"]
[ext_resource type="Script" uid="uid://5tpoohkopryv" path="res://Prefabs/food_item.gd" id="1_0na5k"]
[node name="FoodItem" type="Node" unique_id=63948206]
script = ExtResource("1_0na5k")
id = "hamburger"
item_type = "Meal"
+75
View File
@@ -0,0 +1,75 @@
class_name NetPickable
extends Node
## Networks an XRToolsPickable (RigidBody3D). Default authority is the server,
## which simulates free items and syncs their transform. Grabbing an item with a
## hand transfers authority to the grabbing client (its local kinematic grab
## driver then drives the synced transform); releasing returns authority and the
## throw velocity to the server. Non-authority peers keep the body frozen and
## follow the synced transform, so only one machine ever simulates an item.
@onready var _item: XRToolsPickable = get_parent() as XRToolsPickable
var net_held_by := 0 # peer id currently holding (0 = free); kept consistent via NetworkManager
var _was_authority := true
func _ready() -> void:
if not _item:
push_error("NetPickable must be a child of an XRToolsPickable")
return
_item.grabbed.connect(_on_grabbed)
_item.released.connect(_on_released)
_was_authority = _item.is_multiplayer_authority()
_apply_authority_state()
## Enable the item only where it is free or where we are its current holder, so
## a peer's snap zone / hand never grabs an item another peer is holding. Called
## by NetworkManager whenever net_held_by changes. Note: net_held_by is set per
## peer by RPC (not by a synchronizer), so the holder keeps its item enabled
## while everyone else disables their copy.
func apply_held_state() -> void:
if not _item:
return
_item.enabled = net_held_by == 0 or net_held_by == multiplayer.get_unique_id()
func _physics_process(_delta: float) -> void:
if not NetworkManager.is_online():
return
var mine := _item.is_multiplayer_authority()
if mine != _was_authority:
_was_authority = mine
_apply_authority_state()
# Configure physics for whether we own this item or merely mirror it.
func _apply_authority_state() -> void:
if not _item:
return
if _item.is_multiplayer_authority():
# We own physics: simulate the item whenever it is not held.
if not _item.is_picked_up():
_item.freeze = false
else:
# Someone else owns it: follow the synced transform kinematically.
_item.freeze = true
_item.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
func _on_grabbed(_pickable: Node3D, by: Node3D) -> void:
if not NetworkManager.is_online():
return
# Only hand grabs transfer authority; snap-zone grabs are server-driven.
if not (by is XRToolsFunctionPickup):
return
NetworkManager.request_item_authority.rpc_id(1, _item.get_path())
func _on_released(_pickable: Node3D, by: Node3D) -> void:
if not NetworkManager.is_online():
return
if not (by is XRToolsFunctionPickup):
return
NetworkManager.release_item_authority.rpc_id(1, _item.get_path(), _item.linear_velocity, _item.angular_velocity)
+1
View File
@@ -0,0 +1 @@
uid://6n784li8n5ck
+132
View File
@@ -0,0 +1,132 @@
[gd_scene format=3 uid="uid://do6mrslyqe5qe"]
[ext_resource type="Script" uid="uid://b4ldkd3ngs0vp" path="res://main.gd" id="1_57ppd"]
[ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://XROrigin.tscn" id="2_o1b3t"]
[ext_resource type="Material" uid="uid://cmia50cfqxxo4" path="res://Textures/dev_material_3d.tres" id="3_irf4n"]
[ext_resource type="PackedScene" uid="uid://j7caslh27nor" path="res://Stations/Hob.tscn" id="4_ctden"]
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="5_57ppd"]
[ext_resource type="PackedScene" uid="uid://e4i6o5oriecx" path="res://Items/PickupCube.tscn" id="6_eoxxy"]
[ext_resource type="Sky" uid="uid://c1abtpwhdm2d5" path="res://Textures/sky/NightSkyHDRI009_16K.tres" id="7_n8fyw"]
[ext_resource type="PackedScene" uid="uid://c0lknik4noobs" path="res://Items/BurgerBuns.tscn" id="8_rraok"]
[ext_resource type="PackedScene" uid="uid://c6rift56ql3f8" path="res://Stations/BurgerBunsDispenser.tscn" id="9_sq0s2"]
[ext_resource type="PackedScene" uid="uid://bp3v1jl8pctro" path="res://Containers/plate.tscn" id="10_ay2w6"]
[ext_resource type="PackedScene" uid="uid://dpot5qie20vf6" path="res://Items/burger.tscn" id="11_qn1ku"]
[ext_resource type="PackedScene" uid="uid://b3m2ag8g5rj4r" path="res://Items/hamburger.tscn" id="12_hfwnn"]
[sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"]
size = Vector3(5, 0.1, 5)
[sub_resource type="BoxMesh" id="BoxMesh_24d3s"]
material = ExtResource("3_irf4n")
size = Vector3(5, 0.1, 5)
[sub_resource type="BoxShape3D" id="BoxShape3D_24d3s"]
[sub_resource type="BoxMesh" id="BoxMesh_i5j2u"]
[sub_resource type="SphereShape3D" id="SphereShape3D_dlkho"]
radius = 0.3
[sub_resource type="Environment" id="Environment_bvwq1"]
background_mode = 2
sky = ExtResource("7_n8fyw")
reflected_light_source = 2
ssr_enabled = true
ssao_enabled = true
ssil_enabled = true
sdfgi_enabled = true
[node name="Main" type="Node3D" unique_id=1312265607]
script = ExtResource("1_57ppd")
[node name="XROrigin3D" parent="." unique_id=2055526621 instance=ExtResource("2_o1b3t")]
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1376644384]
transform = Transform3D(-0.8660254, -0.43301278, 0.24999999, 0.3022632, -0.05510214, 0.9516306, -0.39829266, 0.8997021, 0.17860365, 0, 0, 0)
[node name="Floor" type="StaticBody3D" parent="." unique_id=122279514]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0)
[node name="CollisionShape3D" type="CollisionShape3D" parent="Floor" unique_id=958841648]
shape = SubResource("BoxShape3D_vlqg6")
[node name="MeshInstance3D" type="MeshInstance3D" parent="Floor/CollisionShape3D" unique_id=1939997056]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.00018164515, 0.0006352663, -0.002532661)
mesh = SubResource("BoxMesh_24d3s")
[node name="Hob" parent="." unique_id=1687971542 instance=ExtResource("4_ctden")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.29336345, 0.8981018, -1.484)
[node name="Counter" type="StaticBody3D" parent="." unique_id=1487893288 groups=["station"]]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.73186195, 0.8981018, -1.484)
[node name="CollisionShape3D" type="CollisionShape3D" parent="Counter" unique_id=692568688]
shape = SubResource("BoxShape3D_24d3s")
[node name="MeshInstance3D" type="MeshInstance3D" parent="Counter/CollisionShape3D" unique_id=127773521]
mesh = SubResource("BoxMesh_i5j2u")
[node name="Label3D" type="Label3D" parent="Counter/CollisionShape3D" unique_id=149978729]
transform = Transform3D(0.9999992, 0, 0, 0, 0.99999946, 0, 0, 0, 0.9999992, 0.002797456, 0.6309581, -0.2812457)
text = "Counter"
[node name="XRToolsSnapZone" type="Area3D" parent="Counter" unique_id=1647380272 groups=["station"]]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5270121, 0)
collision_layer = 65536
collision_mask = 65536
script = ExtResource("5_57ppd")
snap_mode = 1
initial_object = NodePath("../../Plate")
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
[node name="CollisionShape3D2" type="CollisionShape3D" parent="Counter/XRToolsSnapZone" unique_id=969340130]
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
shape = SubResource("SphereShape3D_dlkho")
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
environment = SubResource("Environment_bvwq1")
[node name="BurgerBuns" parent="." unique_id=1088240294 instance=ExtResource("8_rraok")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8162017, 1.6081157, -1.4714175)
[node name="BurgerBunsDispenser" parent="." unique_id=1720683779 instance=ExtResource("9_sq0s2")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.832131, 0.40028095, -1.4813508)
[node name="Plate" parent="." unique_id=190487773 instance=ExtResource("10_ay2w6")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.72025335, 1.4458435, -1.5107731)
[node name="burger" parent="." unique_id=1417604760 instance=ExtResource("11_qn1ku")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.1439226, 1.4195822, -1.2004558)
[node name="burger2" parent="." unique_id=1584460510 instance=ExtResource("11_qn1ku")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.1396115, 1.5300478, -1.2016745)
[node name="burger3" parent="." unique_id=1884095916 instance=ExtResource("11_qn1ku")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.1424646, 1.4969791, -1.210453)
[node name="burger4" parent="." unique_id=1844818864 instance=ExtResource("11_qn1ku")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.1414003, 1.4543622, -1.210453)
[node name="Hamburger" parent="." unique_id=761091445 instance=ExtResource("12_hfwnn")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.18456237, 1.5438088, -1.3716147)
[node name="PickableObject" parent="." unique_id=1675596942 instance=ExtResource("6_eoxxy")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -0.042440414, 1.4792972, -1.0473135)
[node name="PickableObject2" parent="." unique_id=713087634 instance=ExtResource("6_eoxxy")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, -0.029038578, 1.4639391, -1.1673055)
[node name="PickableObject3" parent="." unique_id=879935619 instance=ExtResource("6_eoxxy")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -0.15074077, 1.4792972, -1.0473135)
[node name="PickableObject4" parent="." unique_id=198541902 instance=ExtResource("6_eoxxy")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, -0.13733894, 1.4639391, -1.1673055)
[node name="Hamburger2" parent="." unique_id=1349934579 instance=ExtResource("12_hfwnn")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.15603885, 1.5329368, -1.6386203)
[node name="PickableObject5" parent="." unique_id=1021333509 instance=ExtResource("6_eoxxy")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.06786254, 1.4792972, -1.0473135)
[node name="PickableObject6" parent="." unique_id=1268843669 instance=ExtResource("6_eoxxy")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, 0.08126438, 1.4639391, -1.1673055)
+90
View File
@@ -0,0 +1,90 @@
[gd_scene format=3 uid="uid://do6mrslyqe5qe"]
[ext_resource type="Script" uid="uid://b4ldkd3ngs0vp" path="res://main.gd" id="1_57ppd"]
[ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://XROrigin.tscn" id="2_o1b3t"]
[ext_resource type="Material" uid="uid://cmia50cfqxxo4" path="res://Textures/dev_material_3d.tres" id="3_irf4n"]
[ext_resource type="PackedScene" uid="uid://j7caslh27nor" path="res://Stations/Hob.tscn" id="4_ctden"]
[ext_resource type="PackedScene" uid="uid://efaec6ymgabo" path="res://Stations/Counter.tscn" id="5_di04w"]
[ext_resource type="PackedScene" uid="uid://e4i6o5oriecx" path="res://Items/PickupCube.tscn" id="6_eoxxy"]
[ext_resource type="Sky" uid="uid://c1abtpwhdm2d5" path="res://Textures/sky/NightSkyHDRI009_16K.tres" id="7_n8fyw"]
[ext_resource type="PackedScene" uid="uid://c0lknik4noobs" path="res://Items/BurgerBuns.tscn" id="8_rraok"]
[ext_resource type="PackedScene" uid="uid://c6rift56ql3f8" path="res://Stations/BurgerBunsDispenser.tscn" id="9_sq0s2"]
[ext_resource type="PackedScene" uid="uid://bp3v1jl8pctro" path="res://Containers/plate.tscn" id="10_ay2w6"]
[ext_resource type="PackedScene" uid="uid://dpot5qie20vf6" path="res://Items/burger.tscn" id="11_qn1ku"]
[ext_resource type="PackedScene" uid="uid://b3m2ag8g5rj4r" path="res://Items/hamburger.tscn" id="12_hfwnn"]
[sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"]
size = Vector3(5, 0.1, 5)
[sub_resource type="BoxMesh" id="BoxMesh_24d3s"]
material = ExtResource("3_irf4n")
size = Vector3(5, 0.1, 5)
[sub_resource type="Environment" id="Environment_bvwq1"]
background_mode = 2
sky = ExtResource("7_n8fyw")
reflected_light_source = 2
ssr_enabled = true
ssao_enabled = true
ssil_enabled = true
sdfgi_enabled = true
[node name="Main" type="Node3D" unique_id=1312265607]
script = ExtResource("1_57ppd")
[node name="XROrigin3D" parent="." unique_id=2055526621 instance=ExtResource("2_o1b3t")]
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1376644384]
transform = Transform3D(-0.8660254, -0.43301278, 0.24999999, 0.3022632, -0.05510214, 0.9516306, -0.39829266, 0.8997021, 0.17860365, 0, 0, 0)
[node name="Floor" type="StaticBody3D" parent="." unique_id=122279514]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0)
[node name="CollisionShape3D" type="CollisionShape3D" parent="Floor" unique_id=958841648]
shape = SubResource("BoxShape3D_vlqg6")
[node name="MeshInstance3D" type="MeshInstance3D" parent="Floor/CollisionShape3D" unique_id=1939997056]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.00018164515, 0.0006352663, -0.002532661)
mesh = SubResource("BoxMesh_24d3s")
[node name="Hob" parent="." unique_id=1687971542 instance=ExtResource("4_ctden")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.29336345, 0.8981018, -1.484)
[node name="Counter" parent="." unique_id=1487893288 instance=ExtResource("5_di04w")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.73186195, 0.8981018, -1.484)
[node name="PickableObject" parent="." unique_id=1675596942 instance=ExtResource("6_eoxxy")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.5842906, 1.5761534, -1.8163961)
[node name="PickableObject2" parent="." unique_id=713087634 instance=ExtResource("6_eoxxy")]
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -0.14558735, 1.5607953, -1.8242048)
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
environment = SubResource("Environment_bvwq1")
[node name="BurgerBuns" parent="." unique_id=1088240294 instance=ExtResource("8_rraok")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8162017, 1.6081157, -1.4714175)
[node name="BurgerBunsDispenser" parent="." unique_id=1720683779 instance=ExtResource("9_sq0s2")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.832131, 0.40028095, -1.4813508)
[node name="BurgerBuns2" parent="." unique_id=1909843084 instance=ExtResource("8_rraok")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.1282921, 1.4976386, -1.0923254)
[node name="Plate" parent="." unique_id=190487773 instance=ExtResource("10_ay2w6")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.53572595, 1.4458435, -1.135189)
[node name="burger" parent="." unique_id=1417604760 instance=ExtResource("11_qn1ku")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.91607785, 1.4235804, -1.0899508)
[node name="burger2" parent="." unique_id=1584460510 instance=ExtResource("11_qn1ku")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.23121053, 1.4235804, -1.3225337)
[node name="burger3" parent="." unique_id=1884095916 instance=ExtResource("11_qn1ku")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.22395861, 1.4235804, -1.1413177)
[node name="burger4" parent="." unique_id=1844818864 instance=ExtResource("11_qn1ku")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.40808713, 1.4235804, -1.1413177)
[node name="Hamburger" parent="." unique_id=761091445 instance=ExtResource("12_hfwnn")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.5295425, 1.994761, -1.1201752)
+173
View File
@@ -0,0 +1,173 @@
[gd_scene format=3 uid="uid://c6rift56ql3f8"]
[ext_resource type="Script" uid="uid://0y6wnjcsum6" path="res://Stations/item_dispenser.gd" id="1_myl3s"]
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="1_p30r1"]
[ext_resource type="PackedScene" uid="uid://c0lknik4noobs" path="res://Items/BurgerBuns.tscn" id="2_1q74o"]
[sub_resource type="SphereShape3D" id="SphereShape3D_xmbo2"]
radius = 0.3
[sub_resource type="CylinderShape3D" id="CylinderShape3D_24d3s"]
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_vlqg6"]
albedo_color = Color(0.6784191, 0.54546416, 0.016760282, 1)
[node name="BurgerBunsDispenser" type="StaticBody3D" unique_id=1720683779]
script = ExtResource("1_myl3s")
item_scene = ExtResource("2_1q74o")
[node name="XRToolsSnapZone" type="Area3D" parent="." unique_id=805334513]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.00070536137, 1.0162505, -0.002165556)
collision_layer = 65536
collision_mask = 65536
script = ExtResource("1_p30r1")
snap_mode = 1
initial_object = NodePath("../../BurgerBuns")
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
[node name="CollisionShape3D" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=1104626493]
shape = SubResource("SphereShape3D_xmbo2")
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1470079357]
shape = SubResource("CylinderShape3D_24d3s")
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=1746426494]
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=858311379]
radius = 0.4
sides = 32
[node name="CSGCombiner3DBuns" type="CSGCombiner3D" parent="CSGCombiner3D" unique_id=1459017790]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.108142376, 1.0341886, -0.19146484)
autosmooth = true
smoothing_angle = 138.9
[node name="CSGCylinder3D2" type="CSGCylinder3D" parent="CSGCombiner3D/CSGCombiner3DBuns" unique_id=1328221793]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.004122257, 0)
radius = 0.085
height = 0.04
sides = 16
material = SubResource("StandardMaterial3D_vlqg6")
[node name="CSGCylinder3D3" type="CSGCylinder3D" parent="CSGCombiner3D/CSGCombiner3DBuns" unique_id=686624728]
transform = Transform3D(0.82770383, -0.5611652, 0, 0.5611652, 0.82770383, 0, 0, 0, 1, -0.048392475, 0.029736161, 0)
radius = 0.085
height = 0.04
sides = 16
material = SubResource("StandardMaterial3D_vlqg6")
[node name="CSGCylinder3D4" type="CSGCylinder3D" parent="CSGCombiner3D/CSGCombiner3DBuns/CSGCylinder3D3" unique_id=1113894081]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.9604645e-08, 0.040259957, 0)
radius = 0.085
height = 0.04
sides = 16
cone = true
material = SubResource("StandardMaterial3D_vlqg6")
[node name="CSGCombiner3DBuns2" type="CSGCombiner3D" parent="CSGCombiner3D" unique_id=1636439623]
transform = Transform3D(0.42896286, 0, -0.90332216, 0, 1, 0, 0.90332216, 0, 0.42896286, 0.13564643, 1.0341886, 0.20497215)
autosmooth = true
smoothing_angle = 138.9
[node name="CSGCylinder3D2" type="CSGCylinder3D" parent="CSGCombiner3D/CSGCombiner3DBuns2" unique_id=190780737]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.004122257, 0)
radius = 0.085
height = 0.04
sides = 16
material = SubResource("StandardMaterial3D_vlqg6")
[node name="CSGCylinder3D3" type="CSGCylinder3D" parent="CSGCombiner3D/CSGCombiner3DBuns2" unique_id=557804311]
transform = Transform3D(0.82770383, -0.5611652, 0, 0.5611652, 0.82770383, 0, 0, 0, 1, -0.048392475, 0.029736161, 0)
radius = 0.085
height = 0.04
sides = 16
material = SubResource("StandardMaterial3D_vlqg6")
[node name="CSGCylinder3D4" type="CSGCylinder3D" parent="CSGCombiner3D/CSGCombiner3DBuns2/CSGCylinder3D3" unique_id=605379447]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.9604645e-08, 0.040259957, 0)
radius = 0.085
height = 0.04
sides = 16
cone = true
material = SubResource("StandardMaterial3D_vlqg6")
[node name="CSGCombiner3DBuns3" type="CSGCombiner3D" parent="CSGCombiner3D" unique_id=1594575583]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.17070979, 1.0341885, 0.07147646)
autosmooth = true
smoothing_angle = 138.9
[node name="CSGCylinder3D2" type="CSGCylinder3D" parent="CSGCombiner3D/CSGCombiner3DBuns3" unique_id=1686142490]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.004122257, 0)
radius = 0.085
height = 0.04
sides = 16
material = SubResource("StandardMaterial3D_vlqg6")
[node name="CSGCylinder3D3" type="CSGCylinder3D" parent="CSGCombiner3D/CSGCombiner3DBuns3" unique_id=1773290691]
transform = Transform3D(0.82770383, -0.5611652, 0, 0.5611652, 0.82770383, 0, 0, 0, 1, -0.048392475, 0.029736161, 0)
radius = 0.085
height = 0.04
sides = 16
material = SubResource("StandardMaterial3D_vlqg6")
[node name="CSGCylinder3D4" type="CSGCylinder3D" parent="CSGCombiner3D/CSGCombiner3DBuns3/CSGCylinder3D3" unique_id=801922493]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.9604645e-08, 0.040259957, 0)
radius = 0.085
height = 0.04
sides = 16
cone = true
material = SubResource("StandardMaterial3D_vlqg6")
[node name="CSGCombiner3DBuns4" type="CSGCombiner3D" parent="CSGCombiner3D" unique_id=2055085730]
transform = Transform3D(0.95670986, 0, 0.29104328, 0, 1, 0, -0.29104328, 0, 0.95670986, -0.14758292, 1.0341886, -0.1304872)
autosmooth = true
smoothing_angle = 138.9
[node name="CSGCylinder3D2" type="CSGCylinder3D" parent="CSGCombiner3D/CSGCombiner3DBuns4" unique_id=547208858]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.004122257, 0)
radius = 0.085
height = 0.04
sides = 16
material = SubResource("StandardMaterial3D_vlqg6")
[node name="CSGCylinder3D3" type="CSGCylinder3D" parent="CSGCombiner3D/CSGCombiner3DBuns4" unique_id=185898007]
transform = Transform3D(0.82770383, -0.5611652, 0, 0.5611652, 0.82770383, 0, 0, 0, 1, -0.048392475, 0.029736161, 0)
radius = 0.085
height = 0.04
sides = 16
material = SubResource("StandardMaterial3D_vlqg6")
[node name="CSGCylinder3D4" type="CSGCylinder3D" parent="CSGCombiner3D/CSGCombiner3DBuns4/CSGCylinder3D3" unique_id=2117029178]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.9604645e-08, 0.040259957, 0)
radius = 0.085
height = 0.04
sides = 16
cone = true
material = SubResource("StandardMaterial3D_vlqg6")
[node name="CSGCombiner3DBuns5" type="CSGCombiner3D" parent="CSGCombiner3D" unique_id=546748728]
transform = Transform3D(0.20608562, 0, -0.978534, 0, 1, 0, 0.978534, 0, 0.20608562, -0.082762055, 1.0341886, 0.2722176)
autosmooth = true
smoothing_angle = 138.9
[node name="CSGCylinder3D2" type="CSGCylinder3D" parent="CSGCombiner3D/CSGCombiner3DBuns5" unique_id=102374914]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.004122257, 0)
radius = 0.085
height = 0.04
sides = 16
material = SubResource("StandardMaterial3D_vlqg6")
[node name="CSGCylinder3D3" type="CSGCylinder3D" parent="CSGCombiner3D/CSGCombiner3DBuns5" unique_id=189923990]
transform = Transform3D(0.82770383, -0.5611652, 0, 0.5611652, 0.82770383, 0, 0, 0, 1, -0.048392475, 0.029736161, 0)
radius = 0.085
height = 0.04
sides = 16
material = SubResource("StandardMaterial3D_vlqg6")
[node name="CSGCylinder3D4" type="CSGCylinder3D" parent="CSGCombiner3D/CSGCombiner3DBuns5/CSGCylinder3D3" unique_id=1980594001]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 5.9604645e-08, 0.040259957, 0)
radius = 0.085
height = 0.04
sides = 16
cone = true
material = SubResource("StandardMaterial3D_vlqg6")
+4 -3
View File
@@ -7,8 +7,9 @@
[sub_resource type="BoxMesh" id="BoxMesh_i5j2u"] [sub_resource type="BoxMesh" id="BoxMesh_i5j2u"]
[sub_resource type="SphereShape3D" id="SphereShape3D_dlkho"] [sub_resource type="SphereShape3D" id="SphereShape3D_dlkho"]
radius = 0.3
[node name="Counter" type="StaticBody3D" unique_id=1487893288] [node name="Counter" type="StaticBody3D" unique_id=1487893288 groups=["station"]]
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=692568688] [node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=692568688]
shape = SubResource("BoxShape3D_24d3s") shape = SubResource("BoxShape3D_24d3s")
@@ -20,12 +21,12 @@ mesh = SubResource("BoxMesh_i5j2u")
transform = Transform3D(0.9999992, 0, 0, 0, 0.99999946, 0, 0, 0, 0.9999992, 0.002797456, 0.6309581, -0.2812457) transform = Transform3D(0.9999992, 0, 0, 0, 0.99999946, 0, 0, 0, 0.9999992, 0.002797456, 0.6309581, -0.2812457)
text = "Counter" text = "Counter"
[node name="XRToolsSnapZone" type="Area3D" parent="." unique_id=1647380272] [node name="XRToolsSnapZone" type="Area3D" parent="." unique_id=1647380272 groups=["station"]]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5270121, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5270121, 0)
collision_layer = 65536 collision_layer = 65536
collision_mask = 65536 collision_mask = 65536
script = ExtResource("1_dlkho") script = ExtResource("1_dlkho")
snap_mode = 1 snap_mode = 0
metadata/_custom_type_script = "uid://cquqe4f1m1sw6" metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
[node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=969340130] [node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=969340130]
+12 -3
View File
@@ -1,6 +1,6 @@
[gd_scene format=3 uid="uid://j7caslh27nor"] [gd_scene format=3 uid="uid://j7caslh27nor"]
[ext_resource type="Script" uid="uid://bsn8vhv5adxdo" path="res://hob.gd" id="1_7jc4g"] [ext_resource type="Script" uid="uid://bsn8vhv5adxdo" path="res://Stations/hob.gd" id="1_7jc4g"]
[ext_resource type="Texture2D" uid="uid://b7n8d4krwbcvl" path="res://Textures/5.png" id="2_0sasq"] [ext_resource type="Texture2D" uid="uid://b7n8d4krwbcvl" path="res://Textures/5.png" id="2_0sasq"]
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="2_er1dp"] [ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="2_er1dp"]
@@ -14,8 +14,14 @@ uv1_triplanar = true
material = SubResource("StandardMaterial3D_m5xe2") material = SubResource("StandardMaterial3D_m5xe2")
[sub_resource type="SphereShape3D" id="SphereShape3D_vlqg6"] [sub_resource type="SphereShape3D" id="SphereShape3D_vlqg6"]
radius = 0.3
[node name="Hob" type="StaticBody3D" unique_id=1687971542] [sub_resource type="SceneReplicationConfig" id="Repl_hob"]
properties/0/path = NodePath(".:progress")
properties/0/spawn = true
properties/0/replication_mode = 1
[node name="Hob" type="StaticBody3D" unique_id=1687971542 groups=["station"]]
script = ExtResource("1_7jc4g") script = ExtResource("1_7jc4g")
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1283846023] [node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1283846023]
@@ -33,9 +39,12 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5270121, 0)
collision_layer = 65536 collision_layer = 65536
collision_mask = 65536 collision_mask = 65536
script = ExtResource("2_er1dp") script = ExtResource("2_er1dp")
snap_mode = 1 snap_mode = 0
metadata/_custom_type_script = "uid://cquqe4f1m1sw6" metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
[node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=370920392] [node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=370920392]
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0) transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
shape = SubResource("SphereShape3D_vlqg6") shape = SubResource("SphereShape3D_vlqg6")
[node name="ProgressSync" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("Repl_hob")
+62
View File
@@ -0,0 +1,62 @@
extends StaticBody3D
## Cooking station. The cook timer and item conversion run only on the machine
## that owns world logic (server or offline host); `progress` is replicated to
## clients for UI via a MultiplayerSynchronizer. This is the reference
## implementation of the reusable "station work-progress" pattern: a server-owned
## progress value advanced by an input source (here, a timer), which converts the
## held item once it reaches the threshold and spawns the result over the network.
@export var cook_speed := 1.0
@onready var snap_zone: XRToolsSnapZone = $XRToolsSnapZone
var item = null
var progress: float = 0.0
func _ready() -> void:
if snap_zone.initial_object:
item = snap_zone.initial_object
func convert_item(cookable: CookableItem) -> void:
var old_pickable = snap_zone.picked_up_object
if not old_pickable:
push_warning("Hob finished cooking item, but snap zone is missing its reference")
return
var original_transform: Transform3D = old_pickable.global_transform
# Spawn the result over the network (server) or locally (offline).
var result := NetworkManager.spawn_item(cookable.turns_into.resource_path, original_transform)
# Remove the cooked item (despawn replicates for spawner-managed items).
snap_zone.drop_object()
old_pickable.queue_free()
# Snap the result into the now-empty zone.
if result:
snap_zone.pick_up_object(result)
func _process(delta: float) -> void:
# Only the world owner runs cooking logic; clients receive `progress` + the
# converted item via replication.
if not NetworkManager.owns_world():
return
# Find the CookableItem in the held object.
var cookable = null
if snap_zone.picked_up_object:
var matches = snap_zone.picked_up_object.get_children().filter(func(c): return c is CookableItem)
if matches.size() > 0:
cookable = matches[0]
# Advance progress and convert once cooked.
if cookable:
progress += cook_speed * delta
if progress >= cookable.cooking_time:
progress = 0.0
convert_item(cookable)
else:
progress = 0.0
View File
+22
View File
@@ -0,0 +1,22 @@
extends StaticBody3D
@export var item_scene : PackedScene
@onready var snap_zone: XRToolsSnapZone = $XRToolsSnapZone
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
if not item_scene:
push_error("Item dispenser is missing a reference to it's item to dispense")
if not snap_zone:
push_error("Item dispenser is missing a reference to its snap zone child")
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
# Only the world owner dispenses, so every client doesn't spawn its own copy.
if not NetworkManager.owns_world():
return
if not snap_zone.picked_up_object:
var new_item := NetworkManager.spawn_item(item_scene.resource_path, snap_zone.global_transform)
if new_item:
snap_zone.pick_up_object(new_item)
+1
View File
@@ -0,0 +1 @@
uid://0y6wnjcsum6
+56
View File
@@ -0,0 +1,56 @@
- Move recipes into a Yaml file that is read, and handles symetry through a Static manager class?
A `FoodItem` has an ID.
Example:
```yaml
combining: # The combinging of two items, entries always have a list of exactly two items
cooked_burger:
- cooked_burger
- burger_buns
dough:
- flour
- water
cooking:
cooked_burger:
ingredient: raw_burger
time: 3
charcoal:
ingredient: cooked_burger
time: 4
charcoal:
ingredient: toast
time: 2
augmenting: # Process of adding an item onto another without changing it's type, but toggling attributes.
cooked_burger:
has_tomato: sliced_tomato
has_cheese: cheese
salad:
has_tomato: sliced_tomato
has_olives: olives
chopping:
salad:
ingredient: lettuce
work: 50 # Wave arms faster makes work go faster
rolling:
pie_base:
ingredient: dough
work: 80
```
```py
on_area_enter(other):
var result = RecipeManager.getCombination(my_id, other_id)
if result:
convertInto(result)
```
- Counters are stations like hobs that convert items by chopping, rolling ect depending on `RecipeManager`,
and only converts items after hand gestures are done enough?
- Plates are lists of `FoodItems`?
Plate[`Hamburger`] accepts `Chips` => Plate[`Hamburger`, `Chips`]
Plate[`Hamburger`, `Chips`] accepts `Cheese` => Plate[`Hamburger(Cheese=true)`, `Chips`]
The logic for combining has to be expanded for plates.
- Plates have a property `is_dirty` they get after a customer clears the plate, and is disabled after being processed in `Sink`
+1 -3
View File
@@ -10,6 +10,4 @@ uv1_triplanar = true
[resource] [resource]
next_pass = SubResource("StandardMaterial3D_0gbf8") next_pass = SubResource("StandardMaterial3D_0gbf8")
metallic = 1.0 albedo_color = Color(0.35156325, 0.3515627, 0.3515629, 1)
metallic_specular = 0.0
roughness = 0.0
+9
View File
@@ -7,6 +7,7 @@
[ext_resource type="PackedScene" uid="uid://diyu06cw06syv" path="res://addons/godot-xr-tools/player/player_body.tscn" id="4_6xr3x"] [ext_resource type="PackedScene" uid="uid://diyu06cw06syv" path="res://addons/godot-xr-tools/player/player_body.tscn" id="4_6xr3x"]
[ext_resource type="PackedScene" uid="uid://b6bk2pj8vbj28" path="res://addons/godot-xr-tools/functions/movement_turn.tscn" id="4_rd8py"] [ext_resource type="PackedScene" uid="uid://b6bk2pj8vbj28" path="res://addons/godot-xr-tools/functions/movement_turn.tscn" id="4_rd8py"]
[ext_resource type="Script" uid="uid://ck4yn3hxuobj7" path="res://addons/godot-xr-tools/player/player_body.gd" id="5_rd8py"] [ext_resource type="Script" uid="uid://ck4yn3hxuobj7" path="res://addons/godot-xr-tools/player/player_body.gd" id="5_rd8py"]
[ext_resource type="PackedScene" uid="uid://cqhw276realc" path="res://addons/godot-xr-tools/functions/function_pointer.tscn" id="6_ptr"]
[node name="XROrigin3D" type="XROrigin3D" unique_id=2055526621] [node name="XROrigin3D" type="XROrigin3D" unique_id=2055526621]
@@ -25,6 +26,10 @@ strafe = true
[node name="FunctionPickup" parent="XRControllerLeftHand" unique_id=2133415351 instance=ExtResource("3_rd8py")] [node name="FunctionPickup" parent="XRControllerLeftHand" unique_id=2133415351 instance=ExtResource("3_rd8py")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.009340696, -0.0074635968, 0.024033919) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.009340696, -0.0074635968, 0.024033919)
[node name="FunctionPointer" parent="XRControllerLeftHand" instance=ExtResource("6_ptr")]
show_laser = 2
show_target = true
[node name="XRControllerRightHand" type="XRController3D" parent="." unique_id=202756852] [node name="XRControllerRightHand" type="XRController3D" parent="." unique_id=202756852]
tracker = &"right_hand" tracker = &"right_hand"
@@ -39,6 +44,10 @@ smooth_turn_speed = 3.5
[node name="FunctionPickup" parent="XRControllerRightHand" unique_id=1876210450 instance=ExtResource("3_rd8py")] [node name="FunctionPickup" parent="XRControllerRightHand" unique_id=1876210450 instance=ExtResource("3_rd8py")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.0074635968, 0.024033919) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.0074635968, 0.024033919)
[node name="FunctionPointer" parent="XRControllerRightHand" instance=ExtResource("6_ptr")]
show_laser = 2
show_target = true
[node name="PlayerBody" type="CharacterBody3D" parent="." unique_id=1444632058 groups=["player_body"] instance=ExtResource("4_6xr3x")] [node name="PlayerBody" type="CharacterBody3D" parent="." unique_id=1444632058 groups=["player_body"] instance=ExtResource("4_6xr3x")]
process_priority = -100 process_priority = -100
process_physics_priority = -100 process_physics_priority = -100
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2022-2026 HolonProduction
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+305
View File
@@ -0,0 +1,305 @@
@tool
extends "kanban_resource.gd"
## Manages the loading and saving of other data.
const __UUID := preload("../uuid/uuid.gd")
const __Category := preload("category.gd")
const __Layout := preload("layout.gd")
const __Stage := preload("stage.gd")
const __Task := preload("task.gd")
const __KanbanResource := preload("kanban_resource.gd")
var layout: __Layout:
set(value):
if layout:
layout.changed.disconnect(__notify_changed)
layout = value
layout.changed.connect(__notify_changed)
var __categories: Dictionary
var __stages: Dictionary
var __tasks: Dictionary
## Generates a json representation of the board.
func to_json() -> Dictionary:
var dict := {}
var category_data := __propagate_uuid_dict(__categories)
dict["categories"] = category_data
var stage_data := __propagate_uuid_dict(__stages)
dict["stages"] = stage_data
var task_data := __propagate_uuid_dict(__tasks)
dict["tasks"] = task_data
dict["layout"] = layout.to_json()
return dict
## Save the board at `path`.
func save(path: String) -> void:
var file = FileAccess.open(path, FileAccess.WRITE)
if not file:
push_error("Error " + str(FileAccess.get_open_error()) + " while opening file for saving board data at " + path)
file.close()
return
var string := JSON.stringify(to_json(), "\t", false)
file.store_string(string)
file.close()
## Initializes the board state from json data.
func from_json(json: Dictionary) -> void:
__instantiate_uuid_array(json.get("categories", null), __Category, __add_category)
__instantiate_uuid_array(json.get("stages", null), __Stage, __add_stage)
__instantiate_uuid_array(json.get("tasks", null), __Task, __add_task)
layout = __Layout.new([])
if json.get("layout", null) is Dictionary:
layout.from_json(json["layout"])
else:
push_warning("Loading incomplete board data which is missing layout data.")
## Loads the data from `path` into the current instance.
func load(path: String) -> void:
var file = FileAccess.open(path, FileAccess.READ)
if not file:
push_error("Error " + str(FileAccess.get_open_error()) + " while opening file for loading board data at " + path)
file.close()
return
var json = JSON.new()
var err = json.parse(file.get_as_text())
file.close()
if err != OK:
push_error("Error " + str(err) + " while parsing board at " + path + " to json. At line " + str(json.get_error_line()) + " the following problem occured:\n" + json.get_error_message())
return
if json.data.has("columns"):
__from_legacy_file(json.data)
else:
from_json(json.data)
## Adds a category and returns the uuid which is associated with it.
func add_category(category: __Category, silent: bool = false) -> String:
var res := __add_category(category)
if not silent:
__notify_changed()
return res
## Returns the category associated with the given uuid or `null` if there is none.
func get_category(uuid: String) -> __Category:
if not __categories.has(uuid) and uuid != "":
push_warning('There is no category with the uuid "' + uuid + '".')
return __categories.get(uuid, null)
## Returns the count of categories.
func get_category_count() -> int:
return len(__categories)
## Returns the uuid's of all categories.
func get_categories() -> Array[String]:
var temp: Array[String] = []
temp.assign(__categories.keys())
return temp
## Removes a category by uuid.
func remove_category(uuid: String, silent: bool = false) -> void:
if __categories.has(uuid):
__categories[uuid].changed.disconnect(__notify_changed)
__categories.erase(uuid)
if not silent:
__notify_changed()
else:
push_warning("Trying to remove uuid wich is not associated with a category.")
## Adds a stage and returns the uuid which is associated with it.
func add_stage(stage: __Stage, silent: bool = false) -> String:
var res := __add_stage(stage)
if not silent:
__notify_changed()
return res
## Returns the stage associated with the given uuid or `null` if there is none.
func get_stage(uuid: String) -> __Stage:
if not __stages.has(uuid) and uuid != "":
push_warning('There is no stage with the uuid "' + uuid + '".')
return __stages.get(uuid, null)
## Returns the count of stages.
func get_stage_count() -> int:
return len(__stages)
## Returns the uuid's of all stages.
func get_stages() -> Array[String]:
var temp: Array[String] = []
temp.assign(__stages.keys())
return temp
## Removes a stage by uuid.
func remove_stage(uuid: String, silent: bool = false) -> void:
if __stages.has(uuid):
__stages[uuid].changed.disconnect(__notify_changed)
__stages.erase(uuid)
if not silent:
__notify_changed()
else:
push_warning("Trying to remove uuid wich is not associated with a stage.")
## Adds a task and returns the uuid which is associated with it.
func add_task(task: __Task, silent: bool = false) -> String:
var res := __add_task(task)
if not silent:
__notify_changed()
return res
## Returns the task associated with the given uuid or `null` if there is none.
func get_task(uuid: String) -> __Task:
if not __tasks.has(uuid) and uuid != "":
push_warning('There is no task with the uuid "' + uuid + '".')
return __tasks.get(uuid, null)
## Returns the count of tasks.
func get_task_count() -> int:
return len(__tasks)
## Returns the uuid's of all tasks.
func get_tasks() -> Array[String]:
var temp: Array[String] = []
temp.assign(__tasks.keys())
return temp
## Removes a task by uuid.
func remove_task(uuid: String, silent: bool = false) -> void:
if __tasks.has(uuid):
if __tasks[uuid].changed.is_connected(__notify_changed):
__tasks[uuid].changed.disconnect(__notify_changed)
__tasks.erase(uuid)
if not silent:
__notify_changed()
else:
push_warning("Trying to remove uuid wich is not associated with a task.")
# Internal version of `add_category` which can be provided with an uuid suggestion.
# The uuid that is passed can be altered by the board if it is already used by
# an other category. Therefore always use the returned uuid.
func __add_category(category: __Category, uuid: String = "") -> String:
category.changed.connect(__notify_changed)
if __categories.has(uuid):
push_warning("The uuid " + uuid + ' is already used. A new one will be generated for the category "' + category.title + '".')
if uuid == "":
uuid = __UUID.v4()
while uuid in __categories.keys():
uuid = __UUID.v4()
__categories[uuid] = category
return uuid
# Internal version of `add_stage` which can be provided with an uuid suggestion.
func __add_stage(stage: __Stage, uuid: String = "") -> String:
stage.changed.connect(__notify_changed)
if __stages.has(uuid):
push_warning("The uuid " + uuid + ' is already used. A new one will be generated for the stage "' + stage.title + '".')
if uuid == "":
uuid = __UUID.v4()
while uuid in __stages.keys():
uuid = __UUID.v4()
__stages[uuid] = stage
return uuid
# Internal version of `add_task` which can be provided with an uuid suggestion.
func __add_task(task: __Task, uuid: String = "") -> String:
task.changed.connect(__notify_changed)
if __tasks.has(uuid):
push_warning("The uuid " + uuid + ' is already used. A new one will be generated for the task "' + task.title + '".')
if uuid == "":
uuid = __UUID.v4()
while uuid in __tasks.keys():
uuid = __UUID.v4()
__tasks[uuid] = task
return uuid
# HACK: `array` should have the type `Array` but then `null` could not be passed.
func __instantiate_uuid_array(array, type: Script, add_callback: Callable) -> void:
if array == null:
push_warning("Loading incomplete board data which is missing data for '" + type.resource_path + "'.")
return
for data in array:
var instance: __KanbanResource = type.new()
instance.from_json(data)
add_callback.call(instance, data.get("uuid", ""))
# Converts a dictionary with (uuid, kanban_resource) pairs into a list
# json representations with the uuid added.
func __propagate_uuid_dict(dict: Dictionary) -> Array:
var res := []
for key in dict.keys():
var json: Dictionary = {"uuid": key}
json.merge(dict[key].to_json())
res.append(json)
return res
# TODO: Remove this sometime in the future.
## Loads a board from the old file format.
func __from_legacy_file(data: Dictionary) -> void:
var categories: Array[String] = []
var tasks: Array[String] = []
var stages: Array[String] = []
for c in data["categories"]:
categories.append(
__add_category(__Category.new(c["title"], c["color"])),
)
for t in data["tasks"]:
tasks.append(
__add_task(
__Task.new(t["title"], t["details"], categories[t["category"]]),
),
)
for s in data["stages"]:
var contained_tasks: Array[String] = []
for t in s["tasks"]:
contained_tasks.append(tasks[t])
stages.append(
__add_stage(
__Stage.new(s["title"], contained_tasks),
),
)
var columns: Array[PackedStringArray] = []
for c in data["columns"]:
var column = PackedStringArray([])
for s in c["stages"]:
column.append(stages[s])
columns.append(column)
layout = __Layout.new(columns)
+1
View File
@@ -0,0 +1 @@
uid://csd0qg7nc4p7m
+43
View File
@@ -0,0 +1,43 @@
@tool
extends "kanban_resource.gd"
## Data of a category.
var title: String:
set(value):
title = value
__notify_changed()
var color: Color:
set(value):
color = value
__notify_changed()
func _init(p_title: String = "", p_color: Color = Color()) -> void:
title = p_title
color = p_color
super._init()
func to_json() -> Dictionary:
return {
"title": title,
"color": color.to_html(false),
}
func from_json(json: Dictionary) -> void:
title = "Missing data."
color = Color.CORNFLOWER_BLUE
if json.has("title"):
title = json["title"]
else:
push_warning("Loading incomplete json data which is missing a title.")
if json.has("color"):
color = Color.html(json["color"])
else:
push_warning("Loading incomplete json data which is missing a color.")
+1
View File
@@ -0,0 +1 @@
uid://bymg657d5rk55
@@ -0,0 +1,30 @@
@tool
extends RefCounted
## Base class for kanban tasks data structures.
## Emitted when the resource changed. The properties are updated before emitting.
signal changed()
var __emit_changed := true
func _init() -> void:
pass
## Serializes the object as json.
func to_json() -> Dictionary:
push_error("Method to_json not implemented.")
return {}
## Deserializes the object from json.
func from_json(json: Dictionary) -> void:
push_error("Method from_json not implemented.")
func __notify_changed() -> void:
if __emit_changed:
changed.emit()
@@ -0,0 +1 @@
uid://dwbcvxus65wau
+49
View File
@@ -0,0 +1,49 @@
@tool
extends "kanban_resource.gd"
## Layout data.
# Use `PackedStringArray` because nested typed collections are not supported.
var columns: Array[PackedStringArray] = []:
get:
return columns.duplicate()
set(value):
columns = value
__notify_changed()
func _init(p_columns: Array[PackedStringArray] = []) -> void:
columns = p_columns
super._init()
func to_json() -> Dictionary:
var cols := []
for c in columns:
var col = []
for uuid in c:
col.append(uuid)
cols.append(col)
return {
"columns": cols,
}
func from_json(json: Dictionary) -> void:
if json.has("columns"):
if json["columns"] is Array:
var cols: Array[PackedStringArray] = []
for c in json["columns"]:
var arr := PackedStringArray()
if c is Array:
for id in c:
arr.append(id)
else:
push_warning("Layout data is corrupted.")
cols.append(arr)
columns = cols
else:
push_warning("Layout data is corrupted.")
else:
push_warning("Loading incomplete json data which is missing a list of columns.")
+1
View File
@@ -0,0 +1 @@
uid://rt5utu17k4q4
+159
View File
@@ -0,0 +1,159 @@
@tool
extends "kanban_resource.gd"
## Contains settings that are not bound to a board.
const DEFAULT_EDITOR_DATA_PATH: String = "res://kanban_tasks_data.kanban"
enum DescriptionOnBoard {
FULL,
FIRST_LINE,
UNTIL_FIRST_BLANK_LINE,
}
enum StepsOnBoard {
ONLY_OPEN,
ALL_OPEN_FIRST,
ALL_IN_ORDER
}
## Whether the first line of the description is shown on the board.
var show_description_preview: bool = true:
set(value):
show_description_preview = value
__notify_changed()
var show_steps_preview: bool = true:
set(value):
show_steps_preview = value
__notify_changed()
var show_category_on_board: bool = false:
set(value):
show_category_on_board = value
__notify_changed()
var edit_step_details_exclusively: bool = false:
set(value):
edit_step_details_exclusively = value
__notify_changed()
var max_displayed_lines_in_description: int = 0:
set(value):
max_displayed_lines_in_description = value
__notify_changed()
var description_on_board := DescriptionOnBoard.FIRST_LINE:
set(value):
description_on_board = value
__notify_changed()
var steps_on_board := StepsOnBoard.ONLY_OPEN:
set(value):
steps_on_board = value
__notify_changed()
var max_steps_on_board: int = 2:
set(value):
max_steps_on_board = value
__notify_changed()
var stages_width: int = 200:
set(value):
stages_width = value
__notify_changed()
var editor_data_file_path: String = DEFAULT_EDITOR_DATA_PATH:
set(value):
editor_data_file_path = value
__notify_changed()
var warn_about_empty_deletion: bool = false:
set(value):
warn_about_empty_deletion = value
__notify_changed()
var recent_file_count: int = 5:
set(value):
recent_file_count = value
recent_files.resize(value)
__notify_changed()
var recent_files: PackedStringArray = []:
get:
return recent_files.duplicate()
set(value):
recent_files = value
__notify_changed()
# Here such settings can come, which is own responsibiity of a user control.
# When it just want to persist its own state, but the setting is not used by anything else.
# In this case there is no need to mess up this class with bolerplate code
# E.g. the splitter position in the details editor window
# Set via set_internal_state to trigger notification
# (As no clean-up, during develolpment some mess can remain in it.
# Use clear or erase in your code in such cases, just don't forget there)
var internal_states: Dictionary = { }
func set_internal_state(property: String, value: Variant) -> void:
internal_states[property] = value
__notify_changed()
func to_json() -> Dictionary:
var res := {
"show_description_preview": show_description_preview,
"warn_about_empty_deletion": warn_about_empty_deletion,
"edit_step_details_exclusively": edit_step_details_exclusively,
"max_displayed_lines_in_description": max_displayed_lines_in_description,
"description_on_board": description_on_board,
"show_steps_preview": show_steps_preview,
"show_category_on_board": show_category_on_board,
"steps_on_board": steps_on_board,
"max_steps_on_board": max_steps_on_board,
"stages_width": stages_width,
}
if not Engine.is_editor_hint():
res["recent_file_count"] = recent_file_count
res["recent_files"] = recent_files
else:
res["editor_data_file_path"] = editor_data_file_path
res["internal_states"] = internal_states
return res
func from_json(json: Dictionary) -> void:
if json.has("show_description_preview"):
show_description_preview = json["show_description_preview"]
if json.has("warn_about_empty_deletion"):
warn_about_empty_deletion = json["warn_about_empty_deletion"]
if json.has("edit_step_details_exclusively"):
edit_step_details_exclusively = json["edit_step_details_exclusively"]
if json.has("max_displayed_lines_in_description"):
max_displayed_lines_in_description = json["max_displayed_lines_in_description"]
if json.has("description_on_board"):
description_on_board = json["description_on_board"]
if json.has("editor_data_file_path"):
editor_data_file_path = json["editor_data_file_path"]
if json.has("recent_file_count"):
recent_file_count = json["recent_file_count"]
if json.has("recent_files"):
recent_files = PackedStringArray(json["recent_files"])
if json.has("show_steps_preview"):
show_steps_preview = json["show_steps_preview"]
if json.has("steps_on_board"):
steps_on_board = json["steps_on_board"]
if json.has("max_steps_on_board"):
max_steps_on_board = json["max_steps_on_board"]
if json.has("stages_width"):
stages_width = json["stages_width"]
if json.has("show_category_on_board"):
show_category_on_board = json["show_category_on_board"]
if json.has("internal_states"):
internal_states = json["internal_states"]
__notify_changed()
+1
View File
@@ -0,0 +1 @@
uid://l6lfl3k3qwxw
+48
View File
@@ -0,0 +1,48 @@
@tool
extends "kanban_resource.gd"
## Data of a stage.
var title: String:
set(value):
title = value
__notify_changed()
var tasks: Array[String] = []:
get:
# Pass by value to avoid appending without emitting `changed`.
return tasks.duplicate()
set(value):
tasks = value
__notify_changed()
func _init(p_title: String = "", p_tasks: Array[String] = []) -> void:
title = p_title
tasks = p_tasks
super._init()
func to_json() -> Dictionary:
return {
"title": title,
"tasks": tasks,
}
func from_json(json: Dictionary) -> void:
if json.has("title"):
title = json["title"]
else:
push_warning("Loading incomplete json data which is missing a title.")
if json.has("tasks"):
# HACK: Workaround for casting to typed array.
var s: Array[String] = []
for i in json["tasks"]:
s.append(i)
tasks = s
else:
push_warning("Loading incomplete json data which is missing a list of tasks.")
+1
View File
@@ -0,0 +1 @@
uid://ciuqm01ng1wfq
+40
View File
@@ -0,0 +1,40 @@
@tool
extends "kanban_resource.gd"
## Data of a step.
var details: String:
set(value):
details = value
__notify_changed()
var done: bool:
set(value):
done = value
__notify_changed()
func _init(p_details: String = "", p_done: bool = false) -> void:
details = p_details
done = p_done
super._init()
func to_json() -> Dictionary:
return {
"details": details,
"done": done,
}
func from_json(json: Dictionary) -> void:
if json.has("details"):
details = json["details"]
else:
push_warning("Loading incomplete json data which is missing details.")
if json.has("done"):
done = json["done"]
else:
push_warning("Loading incomplete json data which is missing 'done'.")
+1
View File
@@ -0,0 +1 @@
uid://cvg6hllieqvxj
+86
View File
@@ -0,0 +1,86 @@
@tool
extends "kanban_resource.gd"
## Data of a task.
const __Step := preload("step.gd")
var title: String:
set(value):
title = value
__notify_changed()
var description: String:
set(value):
description = value
__notify_changed()
var category: String:
set(value):
category = value
__notify_changed()
var steps: Array[__Step]:
get:
return steps.duplicate()
set(value):
steps = value
__notify_changed()
func _init(p_title: String = "", p_description: String = "", p_category: String = "", p_steps: Array[__Step] = []) -> void:
title = p_title
description = p_description
category = p_category
steps = p_steps
super._init()
func add_step(step: __Step, silent: bool = false) -> void:
var new_steps = steps
new_steps.append(step)
steps = new_steps
step.changed.connect(__notify_changed)
if not silent:
__notify_changed()
func to_json() -> Dictionary:
var s: Array[Dictionary] = []
for step in steps:
s.append(step.to_json())
return {
"title": title,
"description": description,
"category": category,
"steps": s,
}
func from_json(json: Dictionary) -> void:
if json.has("title"):
title = json["title"]
else:
push_warning("Loading incomplete json data which is missing a title.")
if json.has("description"):
description = json["description"]
else:
push_warning("Loading incomplete json data which is missing a description.")
if json.has("category"):
category = json["category"]
else:
push_warning("Loading incomplete json data which is missing a category.")
if json.has("steps"):
var s: Array[__Step] = []
for step in json["steps"]:
s.append(__Step.new())
s[-1].from_json(step)
s[-1].changed.connect(__notify_changed)
steps = s
else:
push_warning("Loading incomplete json data which is missing steps.")
+1
View File
@@ -0,0 +1 @@
uid://b1j5opcoegc6y
@@ -0,0 +1,192 @@
@tool
extends VBoxContainer
## A label with editable content.
##
## While not editing the text is displayed in a label. So it does not stick out
## of some layout to much. Only while editing the label it is replaced with an
## line edit.
## Click onto the label to start editing it or start the editing mode via code
## by using [member show_edit]. When editing press [kbd]Enter[/kbd] to finish
## editing or press [kbd]Esc[/kbd] to discard your changes.
## Emitted when the text changed.
##
## [b]Note:[/b] This is only emitted when you confirm your editing by pressing
## [kbd]Enter[/kbd]. If you need access to all changes while editing use the
## line edit directly. You can get it by calling [method get_edit].
signal text_changed(new_text: String)
## The intentions with which the label can be edited.
enum INTENTION {
REPLACE, ## The text will be marked completly when editing.
ADDITION, ## The cursor is placed at the end when editing.
}
## The text to display and edit.
@export var text: String = "":
set(value):
text = value
__update_content()
text_changed.emit(text)
## The default intention when editing the [member text].
@export var default_intention := INTENTION.ADDITION
## Whether a double click is needed for editing. If [code]false[/code] a single
## click is enough.
@export var double_click: bool = true
# The line edit which is used to edit the text.
var __edit: LineEdit
# The label which is used to display the text.
var __label: Label
# The node which had focus before editing started. Used
# to give focus back to it, when [kbd]Enter[/kbd] is used.
var __old_focus: Control = null
func _ready() -> void:
alignment = BoxContainer.ALIGNMENT_CENTER
mouse_filter = Control.MOUSE_FILTER_PASS
# Setup the internal label.
__label = Label.new()
__label.size_flags_horizontal = SIZE_EXPAND_FILL
__label.size_flags_vertical = SIZE_SHRINK_CENTER
__label.mouse_filter = Control.MOUSE_FILTER_PASS
__label.clip_text = true
__label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
__label.gui_input.connect(__on_label_gui_input)
add_child(__label)
# Setup the internal line edit.
__edit = LineEdit.new()
__edit.visible = false
__edit.size_flags_horizontal = SIZE_EXPAND_FILL
__edit.size_flags_vertical = SIZE_FILL
__edit.text_submitted.connect(__on_edit_text_submitted)
__edit.gui_input.connect(__on_edit_gui_input)
__edit.focus_exited.connect(__on_edit_focus_exited, CONNECT_DEFERRED)
add_child(__edit)
__update_content()
# Wait for the label to get its true size.
await get_tree().create_timer(0.0).timeout
# Keep the same size when changing the edit mode.
custom_minimum_size.y = max(__label.size.y, __edit.size.y) * 1.1
func _input(event: InputEvent) -> void:
# End the editing when somewhere else was clicked.
if (event is InputEventMouseButton) and event.pressed and __edit.visible:
var local = __edit.make_input_local(event)
if not Rect2(Vector2.ZERO, __edit.size).has_point(local.position):
show_label()
## Start editing the text and pass an optional intention.
## This can be used to open the edit interface via code.
func show_edit(intention: INTENTION = default_intention) -> void:
if __edit.visible:
return
# When this node can grab focus the focus should not be given back to the
# old focus owner.
__old_focus = get_viewport().gui_get_focus_owner() if focus_mode == FOCUS_NONE else null
__update_content()
__label.visible = false
__edit.visible = true
__edit.grab_focus()
match intention:
INTENTION.ADDITION:
__edit.caret_column = len(__edit.text)
INTENTION.REPLACE:
__edit.select_all()
## Ends editing. If [code]apply_changes[/code] is [code]true[/code] the changed
## text will be applied to the own [member text]. Otherwise the changes will
## be discarded.
func show_label(apply_changes: bool = true) -> void:
if __label.visible:
return
if apply_changes:
text = __edit.text
if is_instance_valid(__old_focus):
__old_focus.grab_focus()
else:
if focus_mode == FOCUS_NONE:
__edit.release_focus()
else:
grab_focus()
__edit.visible = false
__label.visible = true
## Returns the [LineEdit] used to edit the text.
##
## [b]Warning:[/b] This is a required internal node, romoving and freeing it
## may cause a crash. Feel free to edit its parameters to change, how the
## [member text] is displayed.
func get_edit() -> LineEdit:
return __edit
## Returns the [Label] used display the text.
##
## [b]Warning:[/b] This is a required internal node, romoving and freeing it
## may cause a crash. Feel free to edit its parameters to change, how the
## [member text] is displayed.
func get_label() -> Label:
return __label
# Updates the diplayed text of [member __edit] and
# [member __label] based on [member text].
func __update_content() -> void:
if __label:
__label.text = text
if __edit:
__edit.text = text
func __on_label_gui_input(event: InputEvent) -> void:
# Edit when the label is clicked.
if event is InputEventMouseButton:
if event.is_pressed() and event.button_index == MOUSE_BUTTON_LEFT:
if double_click == event.is_double_click():
# Mark event as handled.
__label.accept_event()
show_edit()
func __on_edit_gui_input(event: InputEvent) -> void:
# Discard changes if ui_cancel action is pressed.
if event is InputEventKey and event.is_pressed():
if event.is_action(&"ui_cancel"):
show_label(false)
func __on_edit_text_submitted(_new_text: String) -> void:
# For some reason line edit does not accept the event on its own in GD4.
__edit.accept_event()
show_label()
func __on_edit_focus_exited() -> void:
if __edit.visible:
__old_focus = null
show_label()
@@ -0,0 +1 @@
uid://cmnh7scftf1d
@@ -0,0 +1,38 @@
@tool
extends Button
signal state_changed(expanded: bool)
@export var expanded: bool = true:
set(value):
if value != expanded:
expanded = value
__update_icon()
state_changed.emit(expanded)
var __texture_rect := TextureRect.new()
func _init() -> void:
focus_mode = Control.FOCUS_NONE
flat = true
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(center)
center.add_child(__texture_rect)
pressed.connect(__on_pressed)
__update_icon()
func _notification(what) -> void:
if what == NOTIFICATION_THEME_CHANGED and not is_part_of_edited_scene():
__texture_rect.texture = get_theme_icon(&"Collapse", &"EditorIcons")
func __update_icon() -> void:
__texture_rect.flip_v = expanded
func __on_pressed() -> void:
expanded = !expanded
@@ -0,0 +1 @@
uid://guc56bqr2khq
+26
View File
@@ -0,0 +1,26 @@
extends EditorExportPlugin
## Excludes Kanban Tasks from the exported project, since it's an editor-only
## plugin.
##
## To bypass this for the standalone export configure "kanban_tasks_standalone"
## as export feature.
func _get_name() -> String:
# Plugins are processed in the order of their alphabetically sorted names.
# We need this plugin to run before the GDScript export plugin runs.
return "0_exclude_kanban_tasks"
var is_standalone_export: bool = false
var base_path: String = ""
func _export_begin(features: PackedStringArray, is_debug: bool, path: String, flags: int) -> void:
is_standalone_export = features.has("kanban_tasks_standalone")
# Assumption: Located in base folder of plugin.
base_path = (get_script() as Script).resource_path.get_base_dir()
func _export_file(path: String, type: String, features: PackedStringArray) -> void:
if is_standalone_export:
return
if path.begins_with(base_path):
skip()
+1
View File
@@ -0,0 +1 @@
uid://cf55kdqqoumwb
+1
View File
@@ -0,0 +1 @@
<svg width="128" height="128" version="1.1" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><path d="m124.43 124.43c1.0492-1.0492.52463-3.6723-.52461-8.9185l-5.2462-26.231c-1.0492-5.2462-1.1213-5.449-4.1969-8.3938l-72.397-72.397c-4.6502-4.6502-12.138-4.6502-16.788-.000004l-16.788 16.788c-4.6502 4.6502-4.6502 12.138.000005 16.788l72.397 72.397c3.1477 3.1477 3.1477 3.1477 8.3939 4.1969l26.231 5.2462c5.2462 1.0492 7.8693 1.5738 8.9185.52461zm-36.199-27.805c-2.3179 2.3179-6.076 2.3179-8.3939-.000008l-33.576-33.575c-2.3179-2.3179-2.3179-6.076-.000007-8.3939 2.3179-2.3179 6.076-2.3179 8.3939.000009l33.576 33.575c2.3179 2.3179 2.3179 6.076-.000011 8.3939z" fill="#e0e0e0"/></svg>

After

Width:  |  Height:  |  Size: 831 B

+44
View File
@@ -0,0 +1,44 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bc22jf62qsikb"
path="res://.godot/imported/icon.svg-055e2d1e864a196c183290847b905009.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/kanban_tasks/icon.svg"
dest_files=["res://.godot/imported/icon.svg-055e2d1e864a196c183290847b905009.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=0
svg/scale=0.125
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true
+7
View File
@@ -0,0 +1,7 @@
[plugin]
name="Kanban Tasks - Todo Manager"
description="Another kanban board plugin for the godot engine."
author="HolonProduction"
version="v2.2.0"
script="plugin.gd"
+450
View File
@@ -0,0 +1,450 @@
@tool
extends "standalone_plugin.gd"
const __Singletons := preload("./plugin_singleton/singletons.gd")
const __Shortcuts := preload("./view/shortcuts.gd")
const __EditContext := preload("./view/edit_context.gd")
const __Settings := preload("./data/settings.gd")
const __BoardData := preload("./data/board.gd")
const __LayoutData := preload("./data/layout.gd")
const __TaskData := preload("./data/task.gd")
const __CategoryData := preload("./data/category.gd")
const __StageData := preload("./data/stage.gd")
const __BoardView := preload("./view/board/board.tscn")
const __BoardViewType := preload("./view/board/board.gd")
const __StartView := preload("./view/start/start.tscn")
const __StartViewType := preload("./view/start/start.gd")
const __DocumentationView := preload("./view/documentation/documentation.tscn")
const SETTINGS_KEY: String = "kanban_tasks/general/settings"
enum {
ACTION_SAVE,
ACTION_SAVE_AS,
ACTION_OPEN,
ACTION_CREATE,
ACTION_CLOSE,
ACTION_DOCUMENTATION,
ACTION_QUIT,
}
var main_panel_frame: MarginContainer
var start_view: __StartViewType
var file_dialog_save: FileDialog
var file_dialog_open: FileDialog
var discard_changes_dialog: ConfirmationDialog
var documentation_dialog: AcceptDialog
var file_menu: PopupMenu
var help_menu: PopupMenu
var board_view: __BoardViewType
var board_label: Label
var board_path: String = "":
set(value):
board_path = value
__update_board_label()
__update_menus()
var board_changed: bool = false:
set(value):
board_changed = value
__update_board_label()
var export_plugin: Variant = null
func _enter_tree() -> void:
board_label = Label.new()
if not Engine.is_editor_hint():
add_control_to_container(CONTAINER_TOOLBAR, board_label)
file_menu = PopupMenu.new()
file_menu.name = "File"
file_menu.add_item("Save board", ACTION_SAVE)
file_menu.add_item("Save board as...", ACTION_SAVE_AS)
file_menu.add_item("Close board", ACTION_CLOSE)
file_menu.add_item("Open board...", ACTION_OPEN)
file_menu.add_item("Create board", ACTION_CREATE)
file_menu.add_separator()
file_menu.add_item("Quit", ACTION_QUIT)
file_menu.id_pressed.connect(__action)
add_menu(file_menu)
help_menu = PopupMenu.new()
help_menu.name = "Help"
help_menu.add_item("Documentation", ACTION_DOCUMENTATION)
help_menu.id_pressed.connect(__action)
add_menu(help_menu)
file_dialog_save = FileDialog.new()
file_dialog_save.access = FileDialog.ACCESS_FILESYSTEM
file_dialog_save.file_mode = FileDialog.FILE_MODE_SAVE_FILE
file_dialog_save.add_filter("*.kanban, *.json", "Kanban Board")
file_dialog_save.min_size = Vector2(800, 500)
file_dialog_save.file_selected.connect(__save_board)
get_editor_interface().get_base_control().add_child(file_dialog_save)
file_dialog_open = FileDialog.new()
file_dialog_open.access = FileDialog.ACCESS_FILESYSTEM
file_dialog_open.file_mode = FileDialog.FILE_MODE_OPEN_FILE
file_dialog_open.add_filter("*.kanban, *.json", "Kanban Board")
file_dialog_open.min_size = Vector2(800, 500)
file_dialog_open.file_selected.connect(__open_board)
get_editor_interface().get_base_control().add_child(file_dialog_open)
discard_changes_dialog = ConfirmationDialog.new()
discard_changes_dialog.dialog_text = "All unsaved changes will be discarded."
discard_changes_dialog.unresizable = true
get_editor_interface().get_base_control().add_child(discard_changes_dialog)
documentation_dialog = __DocumentationView.instantiate()
get_editor_interface().get_base_control().add_child(documentation_dialog)
main_panel_frame = MarginContainer.new()
main_panel_frame.add_theme_constant_override(&"margin_top", 5)
main_panel_frame.add_theme_constant_override(&"margin_left", 5)
main_panel_frame.add_theme_constant_override(&"margin_bottom", 5)
main_panel_frame.add_theme_constant_override(&"margin_right", 5)
main_panel_frame.size_flags_vertical = Control.SIZE_EXPAND_FILL
get_editor_interface().get_editor_main_screen().add_child(main_panel_frame)
start_view = __StartView.instantiate()
start_view.create_board.connect(__action.bind(ACTION_CREATE))
start_view.open_board.connect(__on_start_view_open_board)
main_panel_frame.add_child(start_view)
_make_visible(false)
await get_tree().create_timer(0.0).timeout
__load_settings()
if Engine.is_editor_hint():
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
var editor_data_file_path = ctx.settings.editor_data_file_path
if FileAccess.file_exists(editor_data_file_path):
__open_board(editor_data_file_path)
elif FileAccess.file_exists("res://addons/kanban_tasks/data.json"):
# TODO: Remove sometime in the future.
# Migrate from old version.
__open_board("res://addons/kanban_tasks/data.json")
__save_board(editor_data_file_path)
else:
__create_board()
__save_board(editor_data_file_path)
if Engine.is_editor_hint():
export_plugin = load((get_script() as Script).resource_path.get_base_dir().path_join("export_plugin.gd")).new()
add_export_plugin(export_plugin)
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.save_board.connect(__editor_save_board)
ctx.reload_board.connect(__editor_reload_board)
ctx.create_board.connect(__editor_create_board)
__update_menus()
func _exit_tree() -> void:
if is_instance_valid(export_plugin):
remove_export_plugin(export_plugin)
if not Engine.is_editor_hint():
remove_control_from_container(CONTAINER_TOOLBAR, board_label)
board_label.queue_free()
remove_menu(file_menu)
file_menu.queue_free()
remove_menu(help_menu)
file_menu.queue_free()
file_dialog_save.queue_free()
file_dialog_open.queue_free()
discard_changes_dialog.queue_free()
documentation_dialog.queue_free()
main_panel_frame.queue_free()
start_view.queue_free()
if is_instance_valid(board_view):
board_view.queue_free()
func _shortcut_input(event: InputEvent) -> void:
var shortcuts: __Shortcuts = __Singletons.instance_of(__Shortcuts, self)
if not Engine.is_editor_hint() and shortcuts.save.matches_event(event):
get_viewport().set_input_as_handled()
__action(ACTION_SAVE)
if not Engine.is_editor_hint() and shortcuts.save_as.matches_event(event):
get_viewport().set_input_as_handled()
__action(ACTION_SAVE_AS)
func _has_main_screen() -> bool:
return true
func _make_visible(visible) -> void:
if main_panel_frame:
main_panel_frame.visible = visible
func _get_plugin_name() -> String:
return "Tasks"
func _get_plugin_icon() -> Texture2D:
return preload("./icon.svg")
func _notification(what: int) -> void:
match what:
NOTIFICATION_WM_CLOSE_REQUEST:
if not Engine.is_editor_hint():
if not board_changed:
get_tree().quit()
else:
__request_discard_changes(get_tree().quit)
func __update_menus() -> void:
if not is_instance_valid(file_menu):
return
var shortcuts: __Shortcuts = __Singletons.instance_of(__Shortcuts, self)
file_menu.set_item_disabled(
file_menu.get_item_index(ACTION_SAVE),
not is_instance_valid(board_view),
)
file_menu.set_item_shortcut(
file_menu.get_item_index(ACTION_SAVE),
shortcuts.save,
)
file_menu.set_item_disabled(
file_menu.get_item_index(ACTION_SAVE_AS),
not is_instance_valid(board_view),
)
file_menu.set_item_shortcut(
file_menu.get_item_index(ACTION_SAVE_AS),
shortcuts.save_as,
)
file_menu.set_item_disabled(
file_menu.get_item_index(ACTION_CLOSE),
not is_instance_valid(board_view),
)
func __update_board_label() -> void:
if not is_instance_valid(board_label):
return
if is_instance_valid(board_view):
if board_path.is_empty():
board_label.text = "unsaved"
else:
board_label.text = board_path
if board_changed:
board_label.text += "*"
else:
board_label.text = ""
func __action(id: int) -> void:
match id:
ACTION_SAVE:
__request_save()
ACTION_SAVE_AS:
__request_save(true)
ACTION_CREATE:
if not board_changed:
__create_board()
else:
__request_discard_changes(__create_board)
ACTION_OPEN:
if not board_changed:
file_dialog_open.popup_centered()
else:
__request_discard_changes(file_dialog_open.popup_centered)
ACTION_CLOSE:
if not board_changed:
__close_board()
else:
__request_discard_changes(__close_board)
ACTION_DOCUMENTATION:
documentation_dialog.popup_centered()
ACTION_QUIT:
get_tree().get_root().propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST)
func __editor_save_board() -> void:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
__save_board(ctx.settings.editor_data_file_path)
func __editor_reload_board(discard_changes: bool) -> void:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
if not discard_changes:
__action(ACTION_SAVE)
__open_board(ctx.settings.editor_data_file_path)
func __editor_create_board() -> void:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
__action(ACTION_SAVE)
__create_board()
__save_board(ctx.settings.editor_data_file_path)
func __request_discard_changes(callback: Callable) -> void:
for connection in discard_changes_dialog.confirmed.get_connections():
discard_changes_dialog.confirmed.disconnect(connection["callable"])
discard_changes_dialog.confirmed.connect(callback)
discard_changes_dialog.popup_centered()
func __request_save(force_new_location: bool = false) -> void:
if not is_instance_valid(board_view):
return
if not force_new_location and not board_path.is_empty():
__save_board(board_path)
else:
file_dialog_save.popup_centered()
func __create_board() -> void:
var data := __BoardData.new()
data.layout = __LayoutData.new([
PackedStringArray([data.add_stage(__StageData.new("Todo"))]),
PackedStringArray([data.add_stage(__StageData.new("Doing"))]),
PackedStringArray([data.add_stage(__StageData.new("Done"))]),
])
data.add_category(
__CategoryData.new(
"Task",
get_editor_interface().get_base_control().
get_theme_color(&"accent_color", &"Editor")
)
)
data.changed.connect(__on_board_changed)
__make_board_view_visible(data)
board_path = ""
board_changed = false
func __save_board(path: String) -> void:
if is_instance_valid(board_view):
__add_to_recent_files(path)
board_path = path
board_view.board_data.save(path)
board_changed = false
func __open_board(path: String) -> void:
var data := __BoardData.new()
data.load(path)
data.changed.connect(__on_board_changed)
__make_board_view_visible(data)
__add_to_recent_files(path)
board_path = path
board_changed = false
func __close_board() -> void:
board_view.queue_free()
board_view = null
board_path = ""
board_changed = false
start_view.show()
func __add_to_recent_files(path: String) -> void:
if Engine.is_editor_hint():
return
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
var files = ctx.settings.recent_files
if path in files:
files.remove_at(files.find(path))
files.insert(0, path)
else:
files.insert(0, path)
files.resize(ctx.settings.recent_file_count)
ctx.settings.recent_files = files
func __on_board_changed() -> void:
board_changed = true
if Engine.is_editor_hint():
__request_save()
func __on_start_view_open_board(path: String) -> void:
if path.is_empty():
__action(ACTION_OPEN)
else:
__open_board(path)
func __make_board_view_visible(data: __BoardData) -> void:
if is_instance_valid(board_view):
board_view.queue_free()
board_view = __BoardView.instantiate()
board_view.show_documentation.connect(__action.bind(ACTION_DOCUMENTATION))
board_view.board_data = data
main_panel_frame.add_child(board_view)
start_view.hide()
func __save_settings() -> void:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
var data := JSON.stringify(ctx.settings.to_json())
if Engine.is_editor_hint():
get_editor_interface().get_editor_settings().set_setting(
SETTINGS_KEY,
data,
)
else:
ProjectSettings.set_setting(
SETTINGS_KEY,
data,
)
save_project_settings()
func __load_settings() -> void:
var data: String = "{}"
if Engine.is_editor_hint():
var editor_settings = get_editor_interface().get_editor_settings()
if editor_settings.has_setting(SETTINGS_KEY):
data = editor_settings.get_setting(SETTINGS_KEY)
else:
if ProjectSettings.has_setting(SETTINGS_KEY):
data = ProjectSettings.get_setting(SETTINGS_KEY)
var json = JSON.new()
var err = json.parse(data)
if err != OK:
push_error(
"Error "
+ str(err)
+ " while parsing settings. At line "
+ str(json.get_error_line())
+ " the following problem occured:\n"
+ json.get_error_message()
)
return
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.settings.from_json(json.data)
ctx.settings.changed.connect(__save_settings)
+1
View File
@@ -0,0 +1 @@
uid://bxywkes3ex12p
@@ -0,0 +1,23 @@
extends Object
## Allows the registration of anonymous singletons into the scene tree.
const HOLDER_NAME: String = "PluginSingletons"
static func instance_of(p_script: Script, requester: Node) -> Variant:
var holder: Node = requester.get_tree().get_root().get_node_or_null(HOLDER_NAME)
if not is_instance_valid(holder):
holder = Node.new()
holder.name = HOLDER_NAME
requester.get_tree().get_root().add_child(holder)
for child in holder.get_children():
if child.get_script() == p_script:
return child
var instance: Node = p_script.new()
holder.add_child(instance)
return instance
@@ -0,0 +1 @@
uid://b6nperxjqdq6m
+37
View File
@@ -0,0 +1,37 @@
#@standalone
# The line above is needed for standalone detection. Do not modify it.
@tool
extends EditorPlugin
## The StandalonePlugin implementation for in editor use.
##
## This file violates DRY. It should not contain references to other files
## of standalone plugin. This allows the independent use as editor plugin.
## Additional containers for add_control_to_container
# CONTAINER_TOOLBAR = 0,
# CONTAINER_SPATIAL_EDITOR_MENU = 1,
# CONTAINER_SPATIAL_EDITOR_SIDE_LEFT = 2,
# CONTAINER_SPATIAL_EDITOR_SIDE_RIGHT = 3,
# CONTAINER_SPATIAL_EDITOR_BOTTOM = 4,
# CONTAINER_CANVAS_EDITOR_MENU = 5,
# CONTAINER_CANVAS_EDITOR_SIDE_LEFT = 6,
# CONTAINER_CANVAS_EDITOR_SIDE_RIGHT = 7,
# CONTAINER_CANVAS_EDITOR_BOTTOM = 8,
# CONTAINER_INSPECTOR_BOTTOM = 9,
# CONTAINER_PROJECT_SETTING_TAB_LEFT = 10,
# CONTAINER_PROJECT_SETTING_TAB_RIGHT = 11,
const CONTAINER_LAUNCH_PAD := 12
func save_project_settings() -> int:
return ProjectSettings.save()
func add_menu(menu: PopupMenu) -> void:
pass
func remove_menu(menu: PopupMenu) -> void:
pass
@@ -0,0 +1 @@
uid://b3jwfydpmke0f
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Xavier Sellier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+41
View File
@@ -0,0 +1,41 @@
# Note: The code might not be as pretty it could be, since it's written
# in a way that maximizes performance. Methods are inlined and loops are avoided.
extends Node
const MODULO_8_BIT = 256
static func getRandomInt():
# Randomize every time to minimize the risk of collisions
randomize()
return randi() % MODULO_8_BIT
static func uuidbin():
# 16 random bytes with the bytes on index 6 and 8 modified
return [
getRandomInt(), getRandomInt(), getRandomInt(), getRandomInt(),
getRandomInt(), getRandomInt(), ((getRandomInt()) & 0x0f) | 0x40, getRandomInt(),
((getRandomInt()) & 0x3f) | 0x80, getRandomInt(), getRandomInt(), getRandomInt(),
getRandomInt(), getRandomInt(), getRandomInt(), getRandomInt(),
]
static func v4():
# 16 random bytes with the bytes on index 6 and 8 modified
var b = uuidbin()
return '%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x' % [
# low
b[0], b[1], b[2], b[3],
# mid
b[4], b[5],
# hi
b[6], b[7],
# clock
b[8], b[9],
# clock
b[10], b[11], b[12], b[13], b[14], b[15]
]
+1
View File
@@ -0,0 +1 @@
uid://dsdb27xoyjldv
+157
View File
@@ -0,0 +1,157 @@
@tool
extends VBoxContainer
## The visual representation of a kanban board.
const __Singletons := preload("../../plugin_singleton/singletons.gd")
const __Shortcuts := preload("../shortcuts.gd")
const __EditContext := preload("../edit_context.gd")
const __BoardData := preload("../../data/board.gd")
const __StageScript := preload("../stage/stage.gd")
const __StageScene := preload("../stage/stage.tscn")
const __Filter := preload("../filter.gd")
const __SettingsScript := preload("../settings/settings.gd")
signal show_documentation()
var board_data: __BoardData
@onready var search_bar: LineEdit = %SearchBar
@onready var button_advanced_search: Button = %AdvancedSearch
@onready var button_show_categories: Button = %ShowCategories
@onready var button_show_descriptions: Button = %ShowDescriptions
@onready var button_show_steps: Button = %ShowSteps
@onready var button_documentation: Button = %Documentation
@onready var button_settings: Button = %Settings
@onready var column_holder: HBoxContainer = %ColumnHolder
@onready var settings: __SettingsScript = %SettingsView
func _ready():
update()
board_data.layout.changed.connect(update)
settings.board_data = board_data
search_bar.text_changed.connect(__on_filter_changed)
search_bar.text_submitted.connect(__on_search_bar_entered)
button_advanced_search.toggled.connect(__on_filter_changed)
button_show_categories.toggled.connect(__on_show_categories_toggled)
button_show_descriptions.toggled.connect(__on_show_descriptions_toggled)
button_show_steps.toggled.connect(__on_show_steps_toggled)
notification(NOTIFICATION_THEME_CHANGED)
await get_tree().create_timer(0.0).timeout
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.settings.changed.connect(update)
ctx.filter_changed.connect(__on_filter_changed_external)
button_documentation.pressed.connect(func(): show_documentation.emit())
button_documentation.visible = Engine.is_editor_hint()
button_settings.pressed.connect(settings.popup_centered_ratio_no_fullscreen)
func _shortcut_input(event: InputEvent) -> void:
if not __Shortcuts.should_handle_shortcut(self):
return
var shortcuts: __Shortcuts = __Singletons.instance_of(__Shortcuts, self)
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
if not event.is_echo() and event.is_pressed():
if shortcuts.search.matches_event(event):
search_bar.grab_focus()
get_viewport().set_input_as_handled()
elif shortcuts.undo.matches_event(event):
ctx.undo_redo.undo()
get_viewport().set_input_as_handled()
elif shortcuts.redo.matches_event(event):
ctx.undo_redo.redo()
get_viewport().set_input_as_handled()
func _notification(what):
if what == NOTIFICATION_THEME_CHANGED and not is_part_of_edited_scene():
if is_instance_valid(search_bar):
search_bar.right_icon = get_theme_icon(&"Search", &"EditorIcons")
if is_instance_valid(button_settings):
button_settings.icon = get_theme_icon(&"Tools", &"EditorIcons")
if is_instance_valid(button_documentation):
button_documentation.icon = get_theme_icon(&"Help", &"EditorIcons")
if is_instance_valid(button_advanced_search):
button_advanced_search.icon = get_theme_icon(&"Zoom", &"EditorIcons")
if is_instance_valid(button_show_categories):
button_show_categories.icon = get_theme_icon(&"Rectangle", &"EditorIcons")
if is_instance_valid(button_show_descriptions):
button_show_descriptions.icon = get_theme_icon(&"Script", &"EditorIcons")
if is_instance_valid(button_show_steps):
button_show_steps.icon = get_theme_icon(&"FileList", &"EditorIcons")
if is_instance_valid(settings):
settings.on_theme_changed()
func update() -> void:
for column in column_holder.get_children():
column.queue_free()
for column_data in board_data.layout.columns:
var column_scroll = ScrollContainer.new()
column_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
column_scroll.set_v_size_flags(Control.SIZE_EXPAND_FILL)
column_scroll.set_h_size_flags(Control.SIZE_EXPAND_FILL)
var column = VBoxContainer.new()
column.set_v_size_flags(Control.SIZE_EXPAND_FILL)
column.set_h_size_flags(Control.SIZE_EXPAND_FILL)
column_scroll.add_child(column)
column_holder.add_child(column_scroll)
for uuid in column_data:
var stage := __StageScene.instantiate()
stage.board_data = board_data
stage.data_uuid = uuid
column.add_child(stage)
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
button_show_categories.set_pressed_no_signal(ctx.settings.show_category_on_board)
button_show_descriptions.set_pressed_no_signal(ctx.settings.show_description_preview)
button_show_steps.set_pressed_no_signal(ctx.settings.show_steps_preview)
# Do not use parameters the method is bound to diffrent signals.
func __on_filter_changed(param1: Variant = null):
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
if ctx.filter_changed.is_connected(__on_filter_changed_external):
ctx.filter_changed.disconnect(__on_filter_changed_external)
ctx.filter = __Filter.new(search_bar.text, button_advanced_search.button_pressed)
ctx.filter_changed.connect(__on_filter_changed_external)
func __on_search_bar_entered(filter: String):
button_advanced_search.grab_focus()
func __on_filter_changed_external():
search_bar.text = ""
func __on_show_categories_toggled(button_pressed: bool):
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.settings.show_category_on_board = button_pressed
func __on_show_descriptions_toggled(button_pressed: bool):
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.settings.show_description_preview = button_pressed
func __on_show_steps_toggled(button_pressed: bool):
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.settings.show_steps_preview = button_pressed
@@ -0,0 +1 @@
uid://5hks8mhjyp62
+89
View File
@@ -0,0 +1,89 @@
[gd_scene load_steps=3 format=3 uid="uid://c5dk4lnyiag3w"]
[ext_resource type="Script" uid="uid://5hks8mhjyp62" path="res://addons/kanban_tasks/view/board/board.gd" id="1_p7lf4"]
[ext_resource type="PackedScene" uid="uid://dh1yunmhipirg" path="res://addons/kanban_tasks/view/settings/settings.tscn" id="2_by8mq"]
[node name="BoardView" type="VBoxContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
mouse_filter = 0
theme_override_constants/separation = 5
script = ExtResource("1_p7lf4")
[node name="Header" type="HBoxContainer" parent="."]
layout_mode = 2
theme_override_constants/separation = 5
[node name="SearchBar" type="LineEdit" parent="Header"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Search"
clear_button_enabled = true
[node name="AdvancedSearch" type="Button" parent="Header"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Search in details."
toggle_mode = true
[node name="VSeparator" type="VSeparator" parent="Header"]
layout_mode = 2
tooltip_text = "Show categories"
theme_override_constants/separation = 0
[node name="ShowCategories" type="Button" parent="Header"]
unique_name_in_owner = true
visible = false
layout_mode = 2
toggle_mode = true
[node name="ShowDescriptions" type="Button" parent="Header"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Show descriptions."
toggle_mode = true
[node name="ShowSteps" type="Button" parent="Header"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Show steps."
toggle_mode = true
[node name="VSeparator2" type="VSeparator" parent="Header"]
layout_mode = 2
theme_override_constants/separation = 0
[node name="Documentation" type="Button" parent="Header"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Open documentation."
flat = true
[node name="Settings" type="Button" parent="Header"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Manage board settings."
[node name="ScrollContainer" type="ScrollContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
mouse_filter = 0
vertical_scroll_mode = 0
[node name="ColumnHolder" type="HBoxContainer" parent="ScrollContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_constants/separation = 5
alignment = 1
[node name="SettingsView" parent="." instance=ExtResource("2_by8mq")]
unique_name_in_owner = true
visible = false
@@ -0,0 +1,44 @@
@tool
extends PopupMenu
const __BoardData := preload("../../data/board.gd")
var board_data: __BoardData
signal uuid_selected(uuid)
func _init() -> void:
about_to_popup.connect(__update_items_from_board)
id_pressed.connect(__on_id_pressed)
func popup_at_local_position(source: CanvasItem, local_position: Vector2) -> void:
popup_at_global_position(source, source.get_global_transform() * local_position)
func popup_at_global_position(source: CanvasItem, global_position: Vector2) -> void:
position = global_position
if not source.get_window().gui_embed_subwindows:
position += source.get_window().position
popup()
func popup_at_mouse_position(source: CanvasItem) -> void:
popup_at_global_position(source, source.get_global_mouse_position())
func __update_items_from_board() -> void:
clear()
size = Vector2i.ZERO
for uuid in board_data.get_categories():
var i = Image.create(16, 16, false, Image.FORMAT_RGB8)
i.fill(board_data.get_category(uuid).color)
var t = ImageTexture.create_from_image(i)
add_icon_item(t, board_data.get_category(uuid).title)
set_item_metadata(-1, uuid)
func __on_id_pressed(id) -> void:
uuid_selected.emit(get_item_metadata(id))
@@ -0,0 +1 @@
uid://pv2ghsb6w6fn
+225
View File
@@ -0,0 +1,225 @@
@tool
extends AcceptDialog
const __BoardData := preload("../../data/board.gd")
const __StepData := preload("../../data/step.gd")
const __StepEntry := preload("../details/step_entry.gd")
const __Singletons := preload("../../plugin_singleton/singletons.gd")
const __EditContext := preload("../edit_context.gd")
var board_data: __BoardData
var data_uuid: String
var __step_data: __StepData
@onready var category_select: OptionButton = %Category
@onready var h_split_container: HSplitContainer = %HSplitContainer
@onready var description_edit: TextEdit = %Description
@onready var step_holder: VBoxContainer = %StepHolder
@onready var steps_panel_container: PanelContainer = %PanelContainer
@onready var create_step_edit: LineEdit = %CreateStepEdit
@onready var step_details: VBoxContainer = %StepDetails
@onready var close_step_details_button: Button = %CloseStepDetails
@onready var step_edit: TextEdit = %StepEdit
func _ready() -> void:
about_to_popup.connect(__on_about_to_popup)
create_step_edit.text_submitted.connect(__create_step)
close_step_details_button.pressed.connect(__close_step_details)
notification(NOTIFICATION_THEME_CHANGED)
step_holder.entry_action_triggered.connect(__on_step_action_triggered)
step_holder.entry_move_requesed.connect(__step_move_requesed)
visibility_changed.connect(__save_internal_state)
func _notification(what: int) -> void:
if what == NOTIFICATION_THEME_CHANGED and not is_part_of_edited_scene():
if is_instance_valid(steps_panel_container):
steps_panel_container.add_theme_stylebox_override(&"panel", get_theme_stylebox(&"panel", &"Panel"))
if is_instance_valid(create_step_edit):
create_step_edit.right_icon = get_theme_icon(&"Add", &"EditorIcons")
if is_instance_valid(close_step_details_button):
close_step_details_button.icon = get_theme_icon(&"Close", &"EditorIcons")
func update() -> void:
if description_edit.text_changed.is_connected(__on_description_changed):
description_edit.text_changed.disconnect(__on_description_changed)
if description_edit.text != board_data.get_task(data_uuid).description:
description_edit.text = board_data.get_task(data_uuid).description
description_edit.text_changed.connect(__on_description_changed)
title = "Task Details: " + board_data.get_task(data_uuid).title
if category_select.item_selected.is_connected(__on_category_selected):
category_select.item_selected.disconnect(__on_category_selected)
category_select.clear()
for uuid in board_data.get_categories():
var i = Image.create(16, 16, false, Image.FORMAT_RGB8)
i.fill(board_data.get_category(uuid).color)
var t = ImageTexture.create_from_image(i)
category_select.add_icon_item(t, board_data.get_category(uuid).title)
category_select.set_item_metadata(-1, uuid)
if uuid == board_data.get_task(data_uuid).category:
category_select.select(category_select.item_count - 1)
category_select.item_selected.connect(__on_category_selected)
step_holder.clear_steps()
for step in board_data.get_task(data_uuid).steps:
step_holder.add_step(step)
for entry in step_holder.get_step_entries():
entry.being_edited = (entry.step_data == __step_data)
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
step_details.visible = is_instance_valid(__step_data)
description_edit.visible = not (ctx.settings.edit_step_details_exclusively and is_instance_valid(__step_data))
if is_instance_valid(__step_data):
if step_edit.text_changed.is_connected(__on_step_details_changed):
step_edit.text_changed.disconnect(__on_step_details_changed)
step_edit.text = __step_data.details
step_edit.text_changed.connect(__on_step_details_changed)
# Workaround for godotengine/godot#70451
func popup_centered_ratio_no_fullscreen(ratio: float = 0.8) -> void:
var viewport: Viewport = get_parent().get_viewport()
popup(Rect2i(Vector2(viewport.position) + viewport.size / 2.0 - viewport.size * ratio / 2.0, viewport.size * ratio))
func edit_step_details(step: __StepData) -> void:
if is_instance_valid(__step_data):
__step_data.changed.disconnect(update)
__step_data = step
__step_data.changed.connect(update)
update()
step_edit.set_caret_line(step_edit.get_line_count())
step_edit.set_caret_column(len(step_edit.get_line(step_edit.get_line_count() - 1)))
step_edit.grab_focus.call_deferred()
func move_step_up(step: __StepData) -> void:
var steps = board_data.get_task(data_uuid).steps
if step in steps and steps[0] != step:
var index = steps.find(step)
steps.erase(step)
steps.insert(index - 1, step)
board_data.get_task(data_uuid).steps = steps
update()
func move_step_down(step: __StepData) -> void:
var steps = board_data.get_task(data_uuid).steps
if step in steps and steps[-1] != step:
var index = steps.find(step)
steps.erase(step)
steps.insert(index + 1, step)
board_data.get_task(data_uuid).steps = steps
update()
func delete_step(step: __StepData) -> void:
close_step_details(step)
var steps = board_data.get_task(data_uuid).steps
if step in steps:
steps.erase(step)
board_data.get_task(data_uuid).steps = steps
update()
func close_step_details(step: __StepData) -> void:
if __step_data == step:
__close_step_details()
func __on_step_action_triggered(entry: __StepEntry, action: __StepEntry.Actions) -> void:
match action:
__StepEntry.Actions.EDIT_HARD:
edit_step_details(entry.step_data)
__StepEntry.Actions.EDIT_SOFT:
if is_instance_valid(__step_data):
edit_step_details(entry.step_data)
__StepEntry.Actions.CLOSE:
close_step_details(entry.step_data)
__StepEntry.Actions.DELETE:
delete_step(entry.step_data)
__StepEntry.Actions.MOVE_UP:
move_step_up(entry.step_data)
__StepEntry.Actions.MOVE_DOWN:
move_step_down(entry.step_data)
func __step_move_requesed(moved_entry: __StepEntry, target_entry: __StepEntry, move_after_target: bool) -> void:
var steps = board_data.get_task(data_uuid).steps
var moved_idx = steps.find(moved_entry.step_data)
var target_idx = steps.find(target_entry.step_data)
if moved_idx < 0 or target_idx < 0 or moved_idx == target_idx:
return
steps.erase(moved_entry.step_data)
if moved_idx < target_idx:
target_idx -= 1
if move_after_target:
steps.insert(target_idx + 1, moved_entry.step_data)
else:
steps.insert(target_idx, moved_entry.step_data)
board_data.get_task(data_uuid).steps = steps
update()
func __load_internal_state() -> void:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
if ctx.settings.internal_states.has("details_editor_step_holder_width"):
h_split_container.split_offset = ctx.settings.internal_states["details_editor_step_holder_width"]
func __save_internal_state() -> void:
if not visible:
var ctx: __EditContext = __Singletons.instance_of(__EditContext, self)
ctx.settings.set_internal_state("details_editor_step_holder_width", h_split_container.split_offset)
func __close_step_details() -> void:
__step_data.changed.disconnect(update)
__step_data = null
update()
func __on_step_details_changed() -> void:
if __step_data.changed.is_connected(update):
__step_data.changed.disconnect(update)
__step_data.details = step_edit.text
__step_data.changed.connect(update)
func __on_about_to_popup() -> void:
if is_instance_valid(__step_data):
__close_step_details()
update()
__load_internal_state()
if board_data.get_task(data_uuid).description.is_empty():
description_edit.grab_focus.call_deferred()
func __on_description_changed() -> void:
board_data.get_task(data_uuid).description = description_edit.text
func __on_category_selected(index: int) -> void:
board_data.get_task(data_uuid).category = category_select.get_item_metadata(index)
func __create_step(text: String) -> void:
if text.is_empty():
return
var task = board_data.get_task(data_uuid)
var data = __StepData.new(text)
task.add_step(data)
create_step_edit.text = ""
update()
for step in step_holder.get_step_entries():
if step.step_data == data:
step.grab_focus.call_deferred()
@@ -0,0 +1 @@
uid://cfuka8bfmcwog
@@ -0,0 +1,100 @@
[gd_scene format=3 uid="uid://bwi22eyrmeeet"]
[ext_resource type="Script" uid="uid://cfuka8bfmcwog" path="res://addons/kanban_tasks/view/details/details.gd" id="1_gh7s6"]
[ext_resource type="PackedScene" uid="uid://dwjg5vyxx4g48" path="res://addons/kanban_tasks/view/details/step_holder.tscn" id="2_0ptaf"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g2k57"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 5.0
bg_color = Color(0.1, 0.1, 0.1, 0.6)
corner_radius_top_left = 3
corner_radius_top_right = 3
corner_radius_bottom_right = 3
corner_radius_bottom_left = 3
corner_detail = 5
[node name="Details" type="AcceptDialog" unique_id=537054767]
oversampling_override = 1.0
title = "Task Details"
position = Vector2i(0, 36)
size = Vector2i(916, 557)
ok_button_text = "Close"
script = ExtResource("1_gh7s6")
[node name="VBoxContainer" type="VBoxContainer" parent="." unique_id=1730906135]
custom_minimum_size = Vector2(900, 500)
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
grow_horizontal = 2
grow_vertical = 2
[node name="Category" type="OptionButton" parent="VBoxContainer" unique_id=879562296]
unique_name_in_owner = true
layout_mode = 2
[node name="HSplitContainer" type="HSplitContainer" parent="VBoxContainer" unique_id=1738893748]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
[node name="VSplitContainer" type="VSplitContainer" parent="VBoxContainer/HSplitContainer" unique_id=1951989716]
layout_mode = 2
size_flags_horizontal = 3
[node name="Description" type="TextEdit" parent="VBoxContainer/HSplitContainer/VSplitContainer" unique_id=2088816932]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
wrap_mode = 1
[node name="StepDetails" type="VBoxContainer" parent="VBoxContainer/HSplitContainer/VSplitContainer" unique_id=1125161346]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HSplitContainer/VSplitContainer/StepDetails" unique_id=2007120957]
layout_mode = 2
[node name="Label" type="Label" parent="VBoxContainer/HSplitContainer/VSplitContainer/StepDetails/HBoxContainer" unique_id=609616893]
layout_mode = 2
size_flags_horizontal = 3
text = "Step Details:"
[node name="CloseStepDetails" type="Button" parent="VBoxContainer/HSplitContainer/VSplitContainer/StepDetails/HBoxContainer" unique_id=1389055250]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Close"
flat = true
[node name="StepEdit" type="TextEdit" parent="VBoxContainer/HSplitContainer/VSplitContainer/StepDetails" unique_id=2005028969]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
wrap_mode = 1
[node name="StepList" type="VBoxContainer" parent="VBoxContainer/HSplitContainer" unique_id=1447925866]
custom_minimum_size = Vector2(200, 0)
layout_mode = 2
[node name="CreateStepEdit" type="LineEdit" parent="VBoxContainer/HSplitContainer/StepList" unique_id=479211446]
unique_name_in_owner = true
layout_mode = 2
placeholder_text = "Create Step"
[node name="PanelContainer" type="PanelContainer" parent="VBoxContainer/HSplitContainer/StepList" unique_id=1566602760]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_g2k57")
[node name="StepHolder" parent="VBoxContainer/HSplitContainer/StepList/PanelContainer" unique_id=128405745 instance=ExtResource("2_0ptaf")]
unique_name_in_owner = true
layout_mode = 2
steps_focus_mode = 2
@@ -0,0 +1,135 @@
@tool
extends HBoxContainer
## Visual representation of a step.
signal action_triggered(entry: __StepEntry, action: Actions)
const __EditLabel := preload("../../edit_label/edit_label.gd")
const __StepData := preload("../../data/step.gd")
const __Singletons := preload("../../plugin_singleton/singletons.gd")
const __Shortcuts := preload("../shortcuts.gd")
const __StepEntry := preload("step_entry.gd")
enum Actions {
DELETE,
MOVE_UP,
MOVE_DOWN,
EDIT_HARD, ## Forces the step details to open.
EDIT_SOFT, ## Only switches to this step if the details are opened.
CLOSE,
}
@export var context_menu_enabled: bool = true
var done: CheckBox
var title_label: Label
var focus_box: StyleBoxFlat
var context_menu: PopupMenu
var step_data: __StepData
var being_edited := false
func _ready() -> void:
set_h_size_flags(SIZE_EXPAND_FILL)
context_menu = PopupMenu.new()
context_menu.id_pressed.connect(__action)
add_child(context_menu)
done = CheckBox.new()
done.focus_mode = Control.FOCUS_NONE
done.toggled.connect(__set_done)
add_child(done)
title_label = Label.new()
title_label.set_h_size_flags(SIZE_EXPAND_FILL)
title_label.text = step_data.details
title_label.max_lines_visible = 1
title_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
add_child(title_label)
focus_box = StyleBoxFlat.new()
focus_box.bg_color = Color(1, 1, 1, 0.1)
notification(NOTIFICATION_THEME_CHANGED)
step_data.changed.connect(update)
update()
func _shortcut_input(event: InputEvent) -> void:
if not __Shortcuts.should_handle_shortcut(self):
return
var shortcuts: __Shortcuts = __Singletons.instance_of(__Shortcuts, self)
if not event.is_echo() and event.is_pressed():
if shortcuts.rename.matches_event(event):
get_viewport().set_input_as_handled()
__action(Actions.EDIT_HARD)
elif shortcuts.confirm.matches_event(event):
get_viewport().set_input_as_handled()
done.button_pressed = not done.button_pressed
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT:
accept_event()
if context_menu_enabled:
__update_context_menu()
context_menu.position = get_global_mouse_position()
if not get_window().gui_embed_subwindows:
context_menu.position += get_window().position
context_menu.popup()
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed() and event.is_double_click():
__action(Actions.EDIT_HARD)
func _notification(what) -> void:
match(what):
NOTIFICATION_DRAW:
if has_focus() or being_edited:
focus_box.draw(get_canvas_item(), Rect2(Vector2.ZERO, get_rect().size))
NOTIFICATION_FOCUS_ENTER:
__action(Actions.EDIT_SOFT)
func update() -> void:
tooltip_text = step_data.details
done.set_pressed_no_signal(step_data.done)
title_label.text = step_data.details
func __action(what: Actions) -> void:
action_triggered.emit(self, what)
func __update_context_menu() -> void:
var shortcuts: __Shortcuts = __Singletons.instance_of(__Shortcuts, self)
context_menu.clear()
context_menu.size = Vector2.ZERO
if being_edited:
context_menu.add_icon_item(get_theme_icon(&"Close", &"EditorIcons"), "Close", Actions.CLOSE)
else:
context_menu.add_icon_item(get_theme_icon(&"Rename", &"EditorIcons"), "Edit", Actions.EDIT_HARD)
context_menu.set_item_shortcut(context_menu.get_item_index(Actions.EDIT_HARD), shortcuts.rename)
context_menu.add_icon_item(get_theme_icon(&"MoveUp", &"EditorIcons"), "Move Up", Actions.MOVE_UP)
context_menu.set_item_disabled(context_menu.get_item_index(Actions.MOVE_UP), get_index() == 0)
context_menu.add_icon_item(get_theme_icon(&"MoveDown", &"EditorIcons"), "Move Down", Actions.MOVE_DOWN)
context_menu.set_item_disabled(context_menu.get_item_index(Actions.MOVE_DOWN), get_index() == get_parent().get_child_count() - 1)
context_menu.add_separator()
context_menu.add_icon_item(get_theme_icon(&"Remove", &"EditorIcons"), "Delete", Actions.DELETE)
func __set_done(done: bool) -> void:
__action(Actions.CLOSE)
step_data.done = done
@@ -0,0 +1 @@
uid://cdyn8h04v5qqh
@@ -0,0 +1,190 @@
@tool
extends VBoxContainer
signal entry_action_triggered(entry: __StepEntry, action: __StepEntry.Actions)
signal entry_move_requesed(moved_entry: __StepEntry, target_entry: __StepEntry, move_after_target: bool)
const __StepData := preload("../../data/step.gd")
const __StepEntry := preload("../details/step_entry.gd")
@export var scrollable: bool = true:
set(value):
if value != scrollable:
scrollable = value
__update_children_settings()
@export var steps_can_be_removed: bool = true:
set(value):
if value != steps_can_be_removed:
steps_can_be_removed = value
__update_children_settings()
@export var steps_can_be_reordered: bool = true:
set(value):
if value != steps_can_be_reordered:
steps_can_be_reordered = value
__update_children_settings()
@export var steps_have_context_menu: bool = true:
set(value):
if value != steps_have_context_menu:
steps_have_context_menu = value
__update_children_settings()
@export var steps_focus_mode := FocusMode.FOCUS_NONE:
set(value):
if value != steps_focus_mode:
steps_focus_mode = value
__update_children_settings()
var __mouse_entered_step_list: bool = false
var __move_target_entry: __StepEntry = null
var __move_after_target: bool = false
@onready var __scroll_container: ScrollContainer = %ScrollContainer
@onready var __remove_separator: HSeparator = %RemoveSeparator
@onready var __step_list: VBoxContainer = %StepList
@onready var __remove_area: Button = %RemoveArea
func _ready() -> void:
__step_list.draw.connect(__on_step_list_draw)
__step_list.mouse_exited.connect(__on_step_list_mouse_exited)
__step_list.mouse_entered.connect(__on_step_list_mouse_entered)
notification(NOTIFICATION_THEME_CHANGED)
__update_children_settings()
func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
if not steps_can_be_removed and not steps_can_be_reordered:
return false
if data is __StepEntry:
if __remove_area.get_global_rect().has_point(get_global_transform() * at_position):
return true
__update_move_target(at_position)
return (__move_target_entry != null)
return false
func _get_drag_data(at_position: Vector2) -> Variant:
if not steps_can_be_removed and not steps_can_be_reordered:
return null
for entry in get_step_entries():
if entry.get_global_rect().has_point(get_global_transform() * at_position):
var preview := Label.new()
preview.text = entry.step_data.details
set_drag_preview(preview)
return entry
return null
func _drop_data(at_position: Vector2, data: Variant) -> void:
if __move_target_entry != null:
entry_move_requesed.emit(data, __move_target_entry, __move_after_target)
__move_target_entry = null
if data is __StepEntry:
if __remove_area.get_global_rect().has_point(get_global_transform() * at_position):
data.__action(__StepEntry.Actions.DELETE)
func _notification(what: int) -> void:
if what == NOTIFICATION_THEME_CHANGED and not is_part_of_edited_scene():
if is_instance_valid(__remove_area):
__remove_area.icon = get_theme_icon(&"Remove", &"EditorIcons")
func add_step(step: __StepData) -> void:
var entry = __StepEntry.new()
entry.step_data = step
entry.show_behind_parent = true
entry.size_flags_horizontal = Control.SIZE_EXPAND_FILL
__step_list.add_child(entry)
entry.size_flags_horizontal = Control.SIZE_EXPAND_FILL
entry.action_triggered.connect(__on_entry_action_triggered)
entry.context_menu_enabled = steps_have_context_menu
entry.focus_mode = steps_focus_mode
func clear_steps() -> void:
for step in get_step_entries():
__step_list.remove_child(step)
step.queue_free()
func get_step_entries() -> Array[__StepEntry]:
var step_entries: Array[__StepEntry] = []
if is_instance_valid(__step_list):
for child in __step_list.get_children():
if child is __StepEntry:
step_entries.append(child)
return step_entries
func __update_children_settings() -> void:
if is_instance_valid(__scroll_container):
__scroll_container.vertical_scroll_mode = ScrollContainer.SCROLL_MODE_AUTO if scrollable else ScrollContainer.SCROLL_MODE_DISABLED
if is_instance_valid(__remove_separator):
__remove_separator.visible = steps_can_be_removed
if is_instance_valid(__remove_area):
__remove_area.visible = steps_can_be_removed
for entry in get_step_entries():
entry.context_menu_enabled = steps_have_context_menu
entry.focus_mode = steps_focus_mode
func __update_move_target(at_position: Vector2) -> void:
var at_global_position := get_global_transform() * at_position
# This __mouse_entered_step_list is needed here, as this seemed to be the only reliable solution, as:
# 1) something is NOK with transforming at_position to global and compare with step_list.global_rect
# 2) cannot decide what is the visible rect of the step_list
# 3) _can_drop_data was called even after mouse is outside the list (to the bottom direction)
if __mouse_entered_step_list:
var closes_entry: __StepEntry = null
var smallest_distance: float
var position_is_after_closes_entry: bool
for e in get_step_entries():
var entry_global_rect = e.get_global_rect()
var distance := abs(at_global_position.y - entry_global_rect.position.y)
if closes_entry == null or distance < smallest_distance:
closes_entry = e
smallest_distance = distance
position_is_after_closes_entry = false
distance = abs(at_global_position.y - entry_global_rect.end.y)
if closes_entry == null or distance < smallest_distance:
closes_entry = e
smallest_distance = distance
position_is_after_closes_entry = true
__move_target_entry = closes_entry
__move_after_target = position_is_after_closes_entry
else:
__move_target_entry = null
__step_list.queue_redraw()
func __on_step_list_mouse_entered() -> void:
__mouse_entered_step_list = true
func __on_step_list_mouse_exited() -> void:
__mouse_entered_step_list = false
__update_move_target(get_local_mouse_position())
func __on_step_list_draw() -> void:
if __move_target_entry != null:
var target_rect := __step_list.get_global_transform().inverse() * __move_target_entry.get_global_rect()
var separation = __step_list.get_theme_constant(&"separation")
var preview_rect := Rect2(
Vector2(0, target_rect.end.y if __move_after_target else target_rect.position.y - separation),
Vector2(target_rect.size.x, separation)
)
if preview_rect.position.y < 0:
preview_rect.position.y = 0
if preview_rect.end.y > __step_list.size.y:
preview_rect.position.y -= (preview_rect.end.y - __step_list.size.y)
__step_list.draw_rect(preview_rect, get_theme_color(&"step_move_review_color"))
func __on_entry_action_triggered(entry, action) -> void:
entry_action_triggered.emit(entry, action)
@@ -0,0 +1 @@
uid://dw1afc1ntflyw
@@ -0,0 +1,45 @@
[gd_scene format=3 uid="uid://dwjg5vyxx4g48"]
[ext_resource type="Script" uid="uid://dw1afc1ntflyw" path="res://addons/kanban_tasks/view/details/step_holder.gd" id="1_exd17"]
[sub_resource type="Theme" id="Theme_1hs0w"]
StepHolder/base_type = &"VBoxContainer"
StepHolder/colors/step_move_review_color = Color(0.439216, 0.729412, 0.980392, 0.501961)
[node name="StepHolder" type="VBoxContainer" unique_id=1736962155]
offset_right = 326.0
offset_bottom = 500.0
size_flags_horizontal = 3
size_flags_vertical = 3
theme = SubResource("Theme_1hs0w")
theme_type_variation = &"StepHolder"
script = ExtResource("1_exd17")
[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=1852819234]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
metadata/_edit_use_anchors_ = true
[node name="StepList" type="VBoxContainer" parent="ScrollContainer" unique_id=643748775]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="RemoveSeparator" type="HSeparator" parent="." unique_id=106663436]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 0
[node name="RemoveArea" type="Button" parent="." unique_id=1184154065]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 40)
layout_mode = 2
size_flags_vertical = 8
focus_mode = 0
mouse_filter = 2
button_mask = 0
flat = true
icon_alignment = 1
@@ -0,0 +1 @@
<svg width="100" height="100" version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg"><g stroke-linecap="round" stroke-linejoin="round"><path d="m1.3229 1.3229h10.583v23.812h-10.583z" fill="#575b64" stroke="#575b64" stroke-width="1.0583"/><path d="m14.552 1.3229h10.583v23.812h-10.583z" fill="#626771" stroke="#626771" stroke-width="1.0583"/><path d="m2.3476 10.695h15.217v3.9785h-15.217z" transform="matrix(.99153-.12989.11858.99294 0 0)" fill="#179ceb" stroke="#179ceb" stroke-width="1.0584"/></g><path d="m16.933 10.848c-.9234.09187-1.7773.46826-2.9104 1.1617 1.1037-.18609 2.0649-.1914 3.4396.16123l-.52917-1.3229" fill="#a4c1d3" fill-opacity=".60851"/><path d="m16.931 10.715c-.016153.000299-.032539.003438-.048059.009819-.04982.02053-.082375.069113-.082165.12299v1.0588c.000615.08981.088821.15306.17415.12506l.22738-.075448.050642.15193a.1825.1825 0 00.23048.11524.1825.1825 0 00.11524-.23048l-.050126-.15141.22118-.073381c.09245-.03185.11961-.14979.050643-.21911l-.79272-.79478c-.02598-.026228-.061098-.03993-.096635-.039274z" fill="#d8d8d8" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://3edv1ymvukp0"
path="res://.godot/imported/1.svg-15461a63252c61d47c72a31629894a26.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/kanban_tasks/view/documentation/1.svg"
dest_files=["res://.godot/imported/1.svg-15461a63252c61d47c72a31629894a26.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=0
svg/scale=4.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
@@ -0,0 +1 @@
<svg width="100" height="100" version="1.1" viewBox="0 0 26.458 26.458" xmlns="http://www.w3.org/2000/svg"><g fill="#575b64" stroke="#575b64" stroke-linejoin="round"><path d="m1.3229 1.3229h10.583v23.812h-10.583z" stroke-width="1.0583"/><path d="m14.552 14.552h10.583v10.583h-10.583z" stroke-width="1.058"/><path d="m14.552 1.3229h10.583v10.583h-10.583z" stroke-width="1.0581"/></g><path d="m1.852 2.9104h9.5251v1.3229h-9.5251z" fill="#179ceb" stroke="#179ceb" stroke-width="1.0581" stroke-linejoin="round"/><path d="m2.1167 3.6049h7.9375" fill="none" stroke="#106da5" stroke-width=".79375" stroke-linecap="round"/><path d="m8.4073 3.5781-.23386-.80682.60804.65481.40926-.42095.10524.63142.42095.22217-.52619.070158-.22217.38587-.26894-.39756-.57296.15201z" fill="#afb7c9" fill-opacity=".84681"/><path d="m8.2585 3.454-.23386-.80682.60804.65481.40926-.42095.10524.63142.42095.22217-.52619.070158-.22217.38587-.26894-.39756-.57296.15201z" fill="#afb7c9" fill-opacity=".65957"/><path d="m8.8615 3.5711c-.016153.000299-.032539.0034-.048059.0098-.04982.02053-.082375.06911-.082165.12299v1.0588c.000615.08981.088821.15306.17415.12506l.22738-.07545.050642.15193a.1825.1825 0 00.23048.11524.1825.1825 0 00.11524-.23048l-.050126-.15141.22118-.07338c.09245-.03185.11961-.14979.050643-.21911l-.79272-.79478c-.02598-.02623-.061098-.03993-.096635-.03927z" fill="#d8d8d8"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bbo1gfac2wymg"
path="res://.godot/imported/2.svg-053c5d997067871191f4000b81461e5d.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/kanban_tasks/view/documentation/2.svg"
dest_files=["res://.godot/imported/2.svg-053c5d997067871191f4000b81461e5d.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=0
svg/scale=4.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

Some files were not shown because too many files have changed in this diff Show More