diff --git a/packs/base/data/abilities/abilities.json b/packs/base/data/abilities/abilities.json new file mode 100644 index 0000000..c244ba0 --- /dev/null +++ b/packs/base/data/abilities/abilities.json @@ -0,0 +1,7 @@ +{ + "schema": 1, + "abilities": [ + { "id": "ability:dash", "name": "Dash", "description": "Allows you to break through weak barriers." } + ] +} + diff --git a/packs/base/data/creatures/creatures.json b/packs/base/data/creatures/creatures.json new file mode 100644 index 0000000..ecc41fb --- /dev/null +++ b/packs/base/data/creatures/creatures.json @@ -0,0 +1,28 @@ +{ + "schema": 1, + "creatures": [ + { + "id": "creature:ember_fox", + "name": "Ember Fox", + "types": ["type:ember"], + "biomes": ["biome:forest"], + "time_windows": ["day", "night"], + "base_hp": 20, + "base_atk": 8, + "base_def": 6, + "capture_difficulty": 1 + }, + { + "id": "creature:dew_sprout", + "name": "Dew Sprout", + "types": ["type:flora"], + "biomes": ["biome:plains"], + "time_windows": ["morning", "day"], + "base_hp": 18, + "base_atk": 6, + "base_def": 7, + "capture_difficulty": 1 + } + ] +} + diff --git a/packs/base/data/items/items.json b/packs/base/data/items/items.json new file mode 100644 index 0000000..61d2a1c --- /dev/null +++ b/packs/base/data/items/items.json @@ -0,0 +1,8 @@ +{ + "schema": 1, + "items": [ + { "id": "item:forest_herb", "name": "Forest Herb", "stack_max": 99, "tags": ["natural"] }, + { "id": "item:rusty_coin", "name": "Rusty Coin", "stack_max": 99, "tags": ["unnatural"] } + ] +} + diff --git a/packs/base/data/npcs/npcdefs.json b/packs/base/data/npcs/npcdefs.json new file mode 100644 index 0000000..14d8d2a --- /dev/null +++ b/packs/base/data/npcs/npcdefs.json @@ -0,0 +1,24 @@ +{ + "schema": 1, + "npcdefs": [ + { + "id": "npcdef:shopkeeper_basic", + "name": "Mara", + "dialogue": ["Welcome!", "Take a look at my goods."], + "shop_inventory": [ + { "item": "item:forest_herb", "price": 10 } + ] + }, + { + "id": "npcdef:trainer_story_01", + "name": "Rook", + "dialogue": ["If you want to pass, prove yourself."], + "battle": { + "team": ["creature:ember_fox"], + "reward_money": 25, + "on_win_unlocks": ["ability:dash"] + } + } + ] +} + diff --git a/packs/base/data/world/chunks/0_0.json b/packs/base/data/world/chunks/0_0.json new file mode 100644 index 0000000..c752617 --- /dev/null +++ b/packs/base/data/world/chunks/0_0.json @@ -0,0 +1,34 @@ +{ + "schema": 1, + "id": "worldchunk:0_0", + "x": 0, + "y": 0, + "biome": "biome:forest", + "collision_rects": [ + { "id": "col:rockwall_01", "rect": [0, 0, 512, 32] } + ], + "spawn_zones": [ + { + "id": "spawn:forest_a", + "biome": "biome:forest", + "rect": [64, 64, 320, 320], + "time_windows": ["day", "night"], + "weather": ["any"] + } + ], + "pickups": [ + { + "id": "pickup:forest_herb_01", + "item": "item:forest_herb", + "pos": [120, 140], + "respawn_seconds": 600 + }, + { + "id": "pickup:rusty_coin_01", + "item": "item:rusty_coin", + "pos": [220, 180], + "respawn_seconds": 0 + } + ] +} + diff --git a/packs/base/data/world/chunks/0_1.json b/packs/base/data/world/chunks/0_1.json new file mode 100644 index 0000000..1121426 --- /dev/null +++ b/packs/base/data/world/chunks/0_1.json @@ -0,0 +1,17 @@ +{ + "schema": 1, + "id": "worldchunk:0_1", + "x": 0, + "y": 1, + "biome": "biome:plains", + "spawn_zones": [ + { + "id": "spawn:plains_a", + "biome": "biome:plains", + "rect": [80, 80, 360, 300], + "time_windows": ["morning", "day"], + "weather": ["any"] + } + ] +} + diff --git a/packs/base/data/world/chunks/1_0.json b/packs/base/data/world/chunks/1_0.json new file mode 100644 index 0000000..06c0750 --- /dev/null +++ b/packs/base/data/world/chunks/1_0.json @@ -0,0 +1,24 @@ +{ + "schema": 1, + "id": "worldchunk:1_0", + "x": 1, + "y": 0, + "biome": "biome:forest", + "gates": [ + { + "id": "gate:fallen_tree_01", + "rect": [200, 220, 96, 32], + "requires": "ability:dash", + "message": "A fallen tree blocks the path." + } + ], + "npcs": [ + { + "id": "npc:shopkeeper_01", + "kind": "shop", + "pos": [320, 300], + "ref": "npcdef:shopkeeper_basic" + } + ] +} + diff --git a/packs/base/data/world/chunks/1_1.json b/packs/base/data/world/chunks/1_1.json new file mode 100644 index 0000000..8feddf0 --- /dev/null +++ b/packs/base/data/world/chunks/1_1.json @@ -0,0 +1,16 @@ +{ + "schema": 1, + "id": "worldchunk:1_1", + "x": 1, + "y": 1, + "biome": "biome:town", + "npcs": [ + { + "id": "npc:trainer_01", + "kind": "trainer", + "pos": [260, 260], + "ref": "npcdef:trainer_story_01" + } + ] +} + diff --git a/packs/base/data/world/world_main.json b/packs/base/data/world/world_main.json new file mode 100644 index 0000000..96d3b57 --- /dev/null +++ b/packs/base/data/world/world_main.json @@ -0,0 +1,15 @@ +{ + "schema": 1, + "world_id": "world:main", + "display_name": "Main World", + "chunk_size_px": 512, + "tile_size_px": 16, + "start_pos_px": [256, 256], + "chunks": [ + { "x": 0, "y": 0, "path": "data/world/chunks/0_0.json" }, + { "x": 1, "y": 0, "path": "data/world/chunks/1_0.json" }, + { "x": 0, "y": 1, "path": "data/world/chunks/0_1.json" }, + { "x": 1, "y": 1, "path": "data/world/chunks/1_1.json" } + ] +} + diff --git a/packs/base/pack.json b/packs/base/pack.json new file mode 100644 index 0000000..c48a516 --- /dev/null +++ b/packs/base/pack.json @@ -0,0 +1,9 @@ +{ + "schema": 1, + "id": "base", + "name": "Base Featurepack", + "version": "0.0.1", + "priority": 0, + "worlds": ["world:main"] +} + diff --git a/project.godot b/project.godot index 22396a2..e5e0ff7 100644 --- a/project.godot +++ b/project.godot @@ -19,6 +19,41 @@ config/icon="res://icon.svg" project/assembly_name="ocker" +[input] + +ui_left={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":13,"pressure":0.0,"pressed":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":-1.0,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +] +} +ui_right={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":14,"pressure":0.0,"pressed":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":0,"axis_value":1.0,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) +] +} +ui_up={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":11,"pressure":0.0,"pressed":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":-1.0,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) +] +} +ui_down={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194322,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"button_index":12,"pressure":0.0,"pressed":false,"script":null) +, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":-1,"axis":1,"axis_value":1.0,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) +] +} + [physics] 3d/physics_engine="Jolt Physics" diff --git a/scenes/Main.tscn b/scenes/Main.tscn index 8d84c04..5469026 100644 --- a/scenes/Main.tscn +++ b/scenes/Main.tscn @@ -1,10 +1,21 @@ [gd_scene format=3 uid="uid://dackb8ekt4sk6"] [ext_resource type="PackedScene" uid="uid://bjt15rm720w5g" path="res://scenes/Player.tscn" id="1_elqb8"] +[ext_resource type="Script" uid="uid://cqdq8fslu7cyp" path="res://scripts/world/ChunkManager.cs" id="2_0bbpv"] +[ext_resource type="Script" uid="uid://db2vbbh5737ke" path="res://scripts/core/PackManager.cs" id="3_rarhs"] [node name="Main" type="Node2D" unique_id=1194367579] [node name="Overworld" type="Node2D" parent="." unique_id=1249792834] -[node name="Player" parent="." unique_id=1675096620 instance=ExtResource("1_elqb8")] -position = Vector2(-34, -36) +[node name="Player" parent="Overworld" unique_id=1675096620 instance=ExtResource("1_elqb8")] + +[node name="ChunkManager" type="Node2D" parent="Overworld" unique_id=1407815239] +script = ExtResource("2_0bbpv") + +[node name="ColorRect" type="ColorRect" parent="." unique_id=34197065] +offset_right = 40.0 +offset_bottom = 40.0 + +[node name="PackManager" type="Node" parent="." unique_id=1706387276] +script = ExtResource("3_rarhs") diff --git a/scenes/Player.tscn b/scenes/Player.tscn index 8e80370..e700709 100644 --- a/scenes/Player.tscn +++ b/scenes/Player.tscn @@ -1,20 +1,28 @@ [gd_scene format=3 uid="uid://bjt15rm720w5g"] -[ext_resource type="Script" uid="uid://duf0vlr2yin8l" path="res://scripts/Player.cs" id="1_p0vlq"] -[ext_resource type="Script" uid="uid://b25o2r2lf6nst" path="res://scripts/Visual.cs" id="2_v6fml"] +[ext_resource type="Script" uid="uid://duf0vlr2yin8l" path="res://scripts/gameplay/Player.cs" id="1_p0vlq"] [sub_resource type="CircleShape2D" id="CircleShape2D_v6fml"] +[sub_resource type="Gradient" id="Gradient_p0vlq"] + +[sub_resource type="GradientTexture2D" id="GradientTexture2D_v6fml"] +gradient = SubResource("Gradient_p0vlq") +width = 16 +height = 16 + [node name="Player" type="CharacterBody2D" unique_id=1675096620] script = ExtResource("1_p0vlq") -[node name="Sprite2D" type="Sprite2D" parent="." unique_id=554119030] - [node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=1337429488] position = Vector2(-15, -16) shape = SubResource("CircleShape2D_v6fml") -[node name="ColorRect" type="ColorRect" parent="CollisionShape2D" unique_id=2088988808] +[node name="Sprite2D" type="Sprite2D" parent="CollisionShape2D" unique_id=554119030] +position = Vector2(15, 16) +texture = SubResource("GradientTexture2D_v6fml") + +[node name="ColorRect" type="ColorRect" parent="CollisionShape2D" unique_id=1178008943] anchors_preset = 8 anchor_left = 0.5 anchor_top = 0.5 @@ -26,7 +34,5 @@ offset_right = 8.0 offset_bottom = 8.0 grow_horizontal = 2 grow_vertical = 2 -color = Color(0.13958341, 0.81406105, 0, 1) -[node name="Visual" type="Node2D" parent="." unique_id=630351727] -script = ExtResource("2_v6fml") +[node name="Camera2D" type="Camera2D" parent="." unique_id=483897590] diff --git a/scripts/Visual.cs b/scripts/Visual.cs deleted file mode 100644 index 7e4a134..0000000 --- a/scripts/Visual.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Godot; - -public partial class PlayerVisual : Node2D -{ - [Export] public float Radius = 8f; - - public override void _Draw() - { - DrawCircle(Vector2.Zero, Radius, new Color(0.2f, 0.8f, 1.0f)); - } - - public override void _Ready() - { - QueueRedraw(); - } -} diff --git a/scripts/Visual.cs.uid b/scripts/Visual.cs.uid deleted file mode 100644 index 91aefe6..0000000 --- a/scripts/Visual.cs.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b25o2r2lf6nst diff --git a/scripts/core/PackManager.cs b/scripts/core/PackManager.cs new file mode 100644 index 0000000..d34d76a --- /dev/null +++ b/scripts/core/PackManager.cs @@ -0,0 +1,74 @@ +using Godot; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; + +public partial class PackManager : Node +{ + public record PackManifest(int schema, string id, string name, string version, int priority); + + private readonly List<(string Root, PackManifest Manifest)> _packs = new(); + + public override void _Ready() + { + LoadPacks("res://packs"); + GD.Print($"Loaded packs: {string.Join(", ", _packs.Select(p => $"{p.Manifest.id}(prio={p.Manifest.priority})"))}"); + } + + public void LoadPacks(string packsRoot) + { + _packs.Clear(); + + if (!DirAccess.DirExistsAbsolute(packsRoot)) + { + GD.PrintErr($"Packs root missing: {packsRoot}"); + return; + } + + using var dir = DirAccess.Open(packsRoot); + dir.ListDirBegin(); + while (true) + { + var entry = dir.GetNext(); + if (entry == "") break; + if (entry.StartsWith(".")) continue; + + var packPath = $"{packsRoot}/{entry}"; + if (!dir.CurrentIsDir()) continue; + + var manifestPath = $"{packPath}/pack.json"; + if (!FileAccess.FileExists(manifestPath)) + continue; + + var json = FileAccess.GetFileAsString(manifestPath); + var manifest = JsonSerializer.Deserialize(json); + if (manifest == null) + { + GD.PrintErr($"Invalid pack.json: {manifestPath}"); + continue; + } + + _packs.Add((packPath, manifest)); + } + dir.ListDirEnd(); + + // Sort bottom->top by priority (low first, high last) + _packs.Sort((a, b) => a.Manifest.priority.CompareTo(b.Manifest.priority)); + } + + /// Returns the best (topmost) existing file path for a relative path inside a pack. + public string? Resolve(string relativePath) + { + relativePath = relativePath.TrimStart('/'); + + // Highest priority pack wins -> iterate from end + for (int i = _packs.Count - 1; i >= 0; i--) + { + var candidate = $"{_packs[i].Root}/{relativePath}"; + if (FileAccess.FileExists(candidate)) + return candidate; + } + return null; + } +} diff --git a/scripts/core/PackManager.cs.uid b/scripts/core/PackManager.cs.uid new file mode 100644 index 0000000..3ec38fd --- /dev/null +++ b/scripts/core/PackManager.cs.uid @@ -0,0 +1 @@ +uid://db2vbbh5737ke diff --git a/scripts/Player.cs b/scripts/gameplay/Player.cs similarity index 100% rename from scripts/Player.cs rename to scripts/gameplay/Player.cs diff --git a/scripts/Player.cs.uid b/scripts/gameplay/Player.cs.uid similarity index 100% rename from scripts/Player.cs.uid rename to scripts/gameplay/Player.cs.uid diff --git a/scripts/world/ChunkManager.cs b/scripts/world/ChunkManager.cs new file mode 100644 index 0000000..af46128 --- /dev/null +++ b/scripts/world/ChunkManager.cs @@ -0,0 +1,146 @@ +using Godot; +using System; +using System.Collections.Generic; +using System.Text.Json; + +public partial class ChunkManager : Node2D +{ + [Export] public NodePath PlayerPath; + [Export] public int LoadRadius = 1; // 1 -> 3x3 + + private CharacterBody2D? _player; + private PackManager? _packs; + + private WorldIndex? _world; + private Dictionary<(int x, int y), string> _chunkMap = new(); + private readonly Dictionary<(int x, int y), Node2D> _loaded = new(); + + public override void _Ready() + { + _player = GetNodeOrNull(PlayerPath); + if (_player == null) + { + GD.PrintErr("ChunkManager: PlayerPath not set or invalid."); + return; + } + + _packs = GetNodeOrNull("/root/Main/PackManager"); + if (_packs == null) + { + // fallback: search up the tree for PackManager + _packs = GetParent()?.GetNodeOrNull("PackManager"); + } + + if (_packs == null) + { + GD.PrintErr("ChunkManager: PackManager not found. Add it to your Main scene."); + return; + } + + LoadWorldIndex(); + } + + private void LoadWorldIndex() + { + var worldPath = _packs.Resolve("data/world/world_main.json"); + if (worldPath == null) + { + GD.PrintErr("ChunkManager: Could not resolve data/world/world_main.json from packs."); + return; + } + + var json = FileAccess.GetFileAsString(worldPath); + _world = JsonSerializer.Deserialize(json); + if (_world == null) + { + GD.PrintErr("ChunkManager: Failed to parse world index."); + return; + } + + _chunkMap = _world.BuildChunkMap(); + GD.Print($"World loaded: {_world.world_id}, chunks: {_chunkMap.Count}, chunk_size_px={_world.chunk_size_px}"); + } + + public override void _Process(double delta) + { + if (_player == null || _world == null) return; + + var (cx, cy) = WorldPosToChunk(_player.GlobalPosition, _world.chunk_size_px); + + for (int dy = -LoadRadius; dy <= LoadRadius; dy++) + for (int dx = -LoadRadius; dx <= LoadRadius; dx++) + { + var key = (cx + dx, cy + dy); + EnsureLoaded(key.x, key.y); + } + + // Unload chunks that are too far + var keysToRemove = new List<(int x, int y)>(); + foreach (var kv in _loaded) + { + var (x, y) = kv.Key; + if (Math.Abs(x - cx) > LoadRadius || Math.Abs(y - cy) > LoadRadius) + keysToRemove.Add((x, y)); + } + foreach (var k in keysToRemove) + { + _loaded[k].QueueFree(); + _loaded.Remove(k); + } + } + + private void EnsureLoaded(int x, int y) + { + var key = (x, y); + if (_loaded.ContainsKey(key)) return; + if (!_chunkMap.TryGetValue(key, out var relPath)) return; + + // Use pack resolve for the chunk file itself + var chunkPath = _packs!.Resolve(relPath); + if (chunkPath == null) + { + GD.PrintErr($"Chunk missing in packs: {relPath}"); + return; + } + + // For now, we don't parse chunk contents yet; we just visualize existence + biome by file naming/placeholder. + // Next step: parse biome from JSON and use it to color. + var chunkJson = FileAccess.GetFileAsString(chunkPath); + using var doc = JsonDocument.Parse(chunkJson); + var biome = doc.RootElement.TryGetProperty("biome", out var b) ? b.GetString() : "biome:unknown"; + + var node = CreateDebugChunkNode(x, y, _world!.chunk_size_px, biome ?? "biome:unknown"); + AddChild(node); + _loaded[key] = node; + } + + private static (int cx, int cy) WorldPosToChunk(Vector2 pos, int chunkSizePx) + { + int cx = Mathf.FloorToInt(pos.X / chunkSizePx); + int cy = Mathf.FloorToInt(pos.Y / chunkSizePx); + return (cx, cy); + } + + private static Node2D CreateDebugChunkNode(int cx, int cy, int chunkSizePx, string biome) + { + var n = new Node2D(); + n.Name = $"Chunk_{cx}_{cy}"; + n.Position = new Vector2(cx * chunkSizePx, cy * chunkSizePx); + + var rect = new ColorRect(); + rect.Size = new Vector2(chunkSizePx, chunkSizePx); + rect.MouseFilter = Control.MouseFilterEnum.Ignore; + + rect.Color = biome switch + { + "biome:forest" => new Color(0.1f, 0.35f, 0.1f, 0.25f), + "biome:plains" => new Color(0.35f, 0.35f, 0.1f, 0.25f), + "biome:town" => new Color(0.25f, 0.25f, 0.25f, 0.25f), + _ => new Color(0.2f, 0.2f, 0.2f, 0.25f) + }; + + // Add an outline using a Panel (cheap) or just rely on transparency for now + n.AddChild(rect); + return n; + } +} diff --git a/scripts/world/ChunkManager.cs.uid b/scripts/world/ChunkManager.cs.uid new file mode 100644 index 0000000..6679e70 --- /dev/null +++ b/scripts/world/ChunkManager.cs.uid @@ -0,0 +1 @@ +uid://cqdq8fslu7cyp diff --git a/scripts/world/WorldIndex.cs b/scripts/world/WorldIndex.cs new file mode 100644 index 0000000..18428cb --- /dev/null +++ b/scripts/world/WorldIndex.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Text.Json; + +public record WorldIndex(int schema, string world_id, string display_name, int chunk_size_px, int tile_size_px, int[] start_pos_px, List chunks) +{ + public Dictionary<(int x, int y), string> BuildChunkMap() + { + var map = new Dictionary<(int, int), string>(); + foreach (var c in chunks) + map[(c.x, c.y)] = c.path; + return map; + } +} + +public record WorldIndexChunk(int x, int y, string path); diff --git a/scripts/world/WorldIndex.cs.uid b/scripts/world/WorldIndex.cs.uid new file mode 100644 index 0000000..a892c41 --- /dev/null +++ b/scripts/world/WorldIndex.cs.uid @@ -0,0 +1 @@ +uid://bgtmfbm4qg0l5