Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b7e52cd82 | ||
|
|
980ee53579 | ||
|
|
793b214786 | ||
|
|
9b88a10609 | ||
|
|
68e6d814d9 | ||
|
|
db4a659236 | ||
|
|
f4df2bc99d | ||
|
|
5584f74a8b | ||
|
|
77517be778 | ||
|
|
40f5e25c41 | ||
|
|
eaeb841c1d | ||
|
|
5f40fa3f7b | ||
|
|
4b2e687188 |
19
README.md
19
README.md
@@ -16,7 +16,24 @@ addon SDK with guard rails.
|
|||||||
## Status
|
## Status
|
||||||
See `docs/ROADMAP.md` for milestones and `docs/ARCHITECTURE.md` for the design.
|
See `docs/ROADMAP.md` for milestones and `docs/ARCHITECTURE.md` for the design.
|
||||||
|
|
||||||
Current milestone: **M0 — playable single-player world skeleton** (scaffolding on disk; needs Godot 4 installed to open & run).
|
**Emberwild 0.4.0** is released (download page and player guide:
|
||||||
|
https://emberwild-95k.pages.dev, builds on the Gitea releases). Built so far:
|
||||||
|
- M0-M1: the island, survival, crafting, building, weather, wildlife, hunting, fishing.
|
||||||
|
- M2: self-hosted multiplayer (Host Game, dedicated servers, rules, chat, admin).
|
||||||
|
- P1-P5 (`docs/PROGRESSION_DESIGN.md`): iron, stone building, the wolf moon and
|
||||||
|
alpha, the bear, crows and foxes, the barrow, hazards, the Old Stag. P6: a use
|
||||||
|
for every material, lights to place, dropping items, bag rows on the hotbar.
|
||||||
|
- T1, T2, T4 (`docs/TUTORIAL_DESIGN.md`): the tutorial with Old Tam (spoken),
|
||||||
|
and his tips in normal worlds.
|
||||||
|
- M4 so far: packaged builds, the update check, music, ambience, sound effects
|
||||||
|
and creature voices (`assets/audio/CREDITS.md`), the trailer and help pages,
|
||||||
|
the map (N) and compass.
|
||||||
|
- W0-W3, J1 (`docs/WORLDLETS_DESIGN.md`): worldlets as data, the Cove (beach,
|
||||||
|
pier, sea fishing), and the cliff tunnel between them, with a save file per
|
||||||
|
worldlet (single-player).
|
||||||
|
Next: J3, the journey on a server ("spread out"), then the Highlands.
|
||||||
|
Running from the source, `godot --path . -- --dev-worldlets` lets New World
|
||||||
|
start in any worldlet, to try one directly.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
- **Godot 4.3+** (standard build; no C# needed — this project uses GDScript).
|
- **Godot 4.3+** (standard build; no C# needed — this project uses GDScript).
|
||||||
|
|||||||
BIN
assets/audio/voice/hint_cove.ogg
Normal file
BIN
assets/audio/voice/hint_cove.ogg
Normal file
Binary file not shown.
BIN
assets/audio/voice/hint_seam.ogg
Normal file
BIN
assets/audio/voice/hint_seam.ogg
Normal file
Binary file not shown.
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"hint_cold": "91c5088e453fb0e6",
|
"hint_cold": "91c5088e453fb0e6",
|
||||||
|
"hint_cove": "126f0b5e977fbef7",
|
||||||
"hint_died": "e865f8a63622b3c3",
|
"hint_died": "e865f8a63622b3c3",
|
||||||
"hint_diet": "3178b319c44e9fa0",
|
"hint_diet": "3178b319c44e9fa0",
|
||||||
"hint_freezing": "48b1e505119a0fc9",
|
"hint_freezing": "48b1e505119a0fc9",
|
||||||
@@ -8,6 +9,7 @@
|
|||||||
"hint_hungry": "ad76f22cb8e034a0",
|
"hint_hungry": "ad76f22cb8e034a0",
|
||||||
"hint_night": "2a07d9c0bfae4bf9",
|
"hint_night": "2a07d9c0bfae4bf9",
|
||||||
"hint_rotten": "98b69de03dbee089",
|
"hint_rotten": "98b69de03dbee089",
|
||||||
|
"hint_seam": "b353f46816d329b7",
|
||||||
"hint_sheltered": "ba6d6ff8bc4a1c80",
|
"hint_sheltered": "ba6d6ff8bc4a1c80",
|
||||||
"hint_sick": "ec07963c46638ffc",
|
"hint_sick": "ec07963c46638ffc",
|
||||||
"hint_snow": "8f0ac7d093f73840",
|
"hint_snow": "8f0ac7d093f73840",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"_comment": "Fishing by water type. world.water_at() says which type a spot is; a type with no entry has no fish. Weights are relative. 'dawn_dusk' replaces the weight at dawn (05:00-08:30) and dusk (17:00-20:30). Bite waits are seconds, scaled by the modifiers.",
|
"_comment": "Fishing by water type. world.water_at() says which type a spot is; a type with no entry has no fish. Weights are relative. 'dawn_dusk' replaces the weight at dawn (05:00-08:30) and dusk (17:00-20:30). Bite waits are seconds, scaled by the modifiers. A fish with its own 'min_depth' only bites in water at least that deep (cod: cast well out from the pier).",
|
||||||
"modifiers": {
|
"modifiers": {
|
||||||
"dawn_dusk_wait": 0.6,
|
"dawn_dusk_wait": 0.6,
|
||||||
"rain_wait": 0.8,
|
"rain_wait": 0.8,
|
||||||
@@ -24,6 +24,26 @@
|
|||||||
"dawn_dusk": 40
|
"dawn_dusk": 40
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"sea": {
|
||||||
|
"min_depth": 1.0,
|
||||||
|
"bite_wait": [
|
||||||
|
6.0,
|
||||||
|
16.0
|
||||||
|
],
|
||||||
|
"fish": [
|
||||||
|
{
|
||||||
|
"id": "mackerel",
|
||||||
|
"weight": 75,
|
||||||
|
"dawn_dusk": 65
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cod",
|
||||||
|
"weight": 25,
|
||||||
|
"dawn_dusk": 35,
|
||||||
|
"min_depth": 2.2
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"guide": "Old Tam",
|
"guide": "Old Tam",
|
||||||
"_doc": "T4 (docs/TUTORIAL_DESIGN.md §5): one-off tips in normal worlds, each shown once per player (user://hints.json), in list order when several are due. 'when' is kind:value, with | for alternatives: status (a survival status, by prefix), weather (a state), has (an item in your bag), event (howl, died), or a plain check (night, stamina_low, wolf_moon, hard_winter). 'hold' is how long it must stay true first (seconds, default 1).",
|
"_doc": "T4 (docs/TUTORIAL_DESIGN.md §5): one-off tips in normal worlds, each shown once per player (user://hints.json), in list order when several are due. 'when' is kind:value, with | for alternatives: status (a survival status, by prefix), weather (a state), has (an item in your bag), event (howl, died), worldlet (where you are on the journey), or a plain check (night, stamina_low, wolf_moon, hard_winter, seam: holding the antler pickaxe with the cliff tunnel's seam still to dig). 'hold' is how long it must stay true first (seconds, default 1).",
|
||||||
"hints": [
|
"hints": [
|
||||||
{"id": "died", "when": "event:died", "title": "Back on your feet",
|
{"id": "died", "when": "event:died", "title": "Back on your feet",
|
||||||
"text": "Dying's not the end out here. Whatever you carried is waiting where you fell, so go back for it before anything else. Sleep in a bed once and that's where you'll wake."},
|
"text": "Dying's not the end out here. Whatever you carried is waiting where you fell, so go back for it before anything else. Sleep in a bed once and that's where you'll wake."},
|
||||||
@@ -33,6 +33,10 @@
|
|||||||
{"id": "stamina", "when": "stamina_low", "title": "Out of breath",
|
{"id": "stamina", "when": "stamina_low", "title": "Out of breath",
|
||||||
"text": "You're spent. Stop swinging and running a moment and your stamina comes back. Rested by a fire, it comes back faster."},
|
"text": "You're spent. Stop swinging and running a moment and your stamina comes back. Rested by a fire, it comes back faster."},
|
||||||
{"id": "sheltered", "when": "status:Sheltered", "hold": 3, "title": "A proper shelter",
|
{"id": "sheltered", "when": "status:Sheltered", "hold": 3, "title": "A proper shelter",
|
||||||
"text": "Roof and walls: the weather can't touch you here. Sit by a fire in here a little while and you'll be Rested."}
|
"text": "Roof and walls: the weather can't touch you here. Sit by a fire in here a little while and you'll be Rested."},
|
||||||
|
{"id": "seam", "when": "seam", "hold": 3, "title": "The cracked seam",
|
||||||
|
"text": "That antler pick bites rock iron only scratches. Out on the clifftop, near the edge, there's a seam of cracked rock, and a way down to the sea behind it if you dig. Take wood to prop it as you go, and lights: it's black as pitch in there."},
|
||||||
|
{"id": "cove", "when": "worldlet:cove", "hold": 3, "title": "The Cove",
|
||||||
|
"text": "You made it down. Sand, salt and sea fish. Fish off the end of the pier: mackerel bite close in, cod further out where it's deep. The sea cave takes you back up the tunnel, and home's waiting at the top."}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
226
data/items.json
226
data/items.json
@@ -347,7 +347,7 @@
|
|||||||
"name": "Hide",
|
"name": "Hide",
|
||||||
"category": "resource",
|
"category": "resource",
|
||||||
"stack": 20,
|
"stack": 20,
|
||||||
"description": "Raw hide from a deer or boar. Makes a warm cloak.",
|
"description": "Rawhide from an animal. For cloaks and bedrolls.",
|
||||||
"mesh": "hide"
|
"mesh": "hide"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -355,7 +355,7 @@
|
|||||||
"name": "Feather",
|
"name": "Feather",
|
||||||
"category": "resource",
|
"category": "resource",
|
||||||
"stack": 50,
|
"stack": 50,
|
||||||
"description": "Found in meadows. Fletching for arrows.",
|
"description": "Fletching for arrows, and stuffing for a bedroll.",
|
||||||
"mesh": "feather"
|
"mesh": "feather"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -443,7 +443,7 @@
|
|||||||
"name": "Wolf Pelt",
|
"name": "Wolf Pelt",
|
||||||
"category": "resource",
|
"category": "resource",
|
||||||
"stack": 20,
|
"stack": 20,
|
||||||
"description": "Thick grey fur. The warmest thing on the island.",
|
"description": "A grey wolf pelt. For wolf and alpha cloaks, or a rug.",
|
||||||
"mesh": "wolf_pelt"
|
"mesh": "wolf_pelt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -543,6 +543,72 @@
|
|||||||
},
|
},
|
||||||
"keeps": 4
|
"keeps": 4
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "mackerel",
|
||||||
|
"name": "Mackerel",
|
||||||
|
"category": "food",
|
||||||
|
"stack": 20,
|
||||||
|
"description": "A quick, striped sea fish, caught off the pier. Grill it at a campfire. (Fish)",
|
||||||
|
"mesh": "mackerel",
|
||||||
|
"food": {
|
||||||
|
"hunger": 5,
|
||||||
|
"health": -2,
|
||||||
|
"diet": {
|
||||||
|
"fish": 10
|
||||||
|
},
|
||||||
|
"risk": 0.2
|
||||||
|
},
|
||||||
|
"keeps": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cod",
|
||||||
|
"name": "Cod",
|
||||||
|
"category": "food",
|
||||||
|
"stack": 20,
|
||||||
|
"description": "A big sea fish from deeper water: cast well out. (Fish)",
|
||||||
|
"mesh": "cod",
|
||||||
|
"food": {
|
||||||
|
"hunger": 8,
|
||||||
|
"health": -2,
|
||||||
|
"diet": {
|
||||||
|
"fish": 16
|
||||||
|
},
|
||||||
|
"risk": 0.2
|
||||||
|
},
|
||||||
|
"keeps": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "grilled_mackerel",
|
||||||
|
"name": "Grilled Mackerel",
|
||||||
|
"category": "food",
|
||||||
|
"stack": 20,
|
||||||
|
"description": "Oily and good. (Fish)",
|
||||||
|
"mesh": "grilled_mackerel",
|
||||||
|
"food": {
|
||||||
|
"hunger": 22,
|
||||||
|
"health": 8,
|
||||||
|
"diet": {
|
||||||
|
"fish": 36
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"keeps": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "grilled_cod",
|
||||||
|
"name": "Grilled Cod",
|
||||||
|
"category": "food",
|
||||||
|
"stack": 20,
|
||||||
|
"description": "Thick white flakes: a proper meal. (Fish)",
|
||||||
|
"mesh": "grilled_cod",
|
||||||
|
"food": {
|
||||||
|
"hunger": 32,
|
||||||
|
"health": 12,
|
||||||
|
"diet": {
|
||||||
|
"fish": 52
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"keeps": 4
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "iron_nugget",
|
"id": "iron_nugget",
|
||||||
"name": "Iron Nugget",
|
"name": "Iron Nugget",
|
||||||
@@ -564,7 +630,7 @@
|
|||||||
"name": "Clay",
|
"name": "Clay",
|
||||||
"category": "resource",
|
"category": "resource",
|
||||||
"stack": 50,
|
"stack": 50,
|
||||||
"description": "Grey clay from the lake shore. For kilns and smelters.",
|
"description": "Grey clay from the lake shore. For kilns, smelters and, fired at a campfire, clay jars that keep food fresh.",
|
||||||
"mesh": "clay"
|
"mesh": "clay"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -928,7 +994,7 @@
|
|||||||
"name": "Alpha Pelt",
|
"name": "Alpha Pelt",
|
||||||
"category": "resource",
|
"category": "resource",
|
||||||
"stack": 5,
|
"stack": 5,
|
||||||
"description": "The alpha's black fur, thick as a blanket.",
|
"description": "The alpha's dark pelt. For the alpha cloak or a fang necklace.",
|
||||||
"mesh": "alpha_pelt"
|
"mesh": "alpha_pelt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -936,7 +1002,7 @@
|
|||||||
"name": "Wolf Fang",
|
"name": "Wolf Fang",
|
||||||
"category": "resource",
|
"category": "resource",
|
||||||
"stack": 20,
|
"stack": 20,
|
||||||
"description": "A long yellowed fang from the alpha. A trophy, and a fine clasp.",
|
"description": "A long yellowed fang from the alpha. For the alpha cloak, and a fang necklace that blunts wolf bites.",
|
||||||
"mesh": "wolf_fang"
|
"mesh": "wolf_fang"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -956,7 +1022,7 @@
|
|||||||
"name": "Bear Hide",
|
"name": "Bear Hide",
|
||||||
"category": "resource",
|
"category": "resource",
|
||||||
"stack": 5,
|
"stack": 5,
|
||||||
"description": "A huge brown hide. Makes a fur rug.",
|
"description": "A huge brown hide. For a fur rug or a bear cloak, the warmest there is.",
|
||||||
"mesh": "bear_hide"
|
"mesh": "bear_hide"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -964,7 +1030,7 @@
|
|||||||
"name": "Bear Fat",
|
"name": "Bear Fat",
|
||||||
"category": "resource",
|
"category": "resource",
|
||||||
"stack": 20,
|
"stack": 20,
|
||||||
"description": "Rendered fat. Burns three times as long as wood in a fire.",
|
"description": "Thick white fat. Burns long on a fire, and makes pemmican and tallow candles.",
|
||||||
"mesh": "bear_fat",
|
"mesh": "bear_fat",
|
||||||
"fuel": 180
|
"fuel": 180
|
||||||
},
|
},
|
||||||
@@ -996,7 +1062,7 @@
|
|||||||
"name": "Rotten Food",
|
"name": "Rotten Food",
|
||||||
"category": "resource",
|
"category": "resource",
|
||||||
"stack": 50,
|
"stack": 50,
|
||||||
"description": "It's gone off. Good for nothing: throw it out.",
|
"description": "It's gone off. Q drops it. Carried while fishing it's bait: bites come sooner, and each bite takes one.",
|
||||||
"mesh": "rotten_food"
|
"mesh": "rotten_food"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1129,6 +1195,148 @@
|
|||||||
},
|
},
|
||||||
"station": "forge"
|
"station": "forge"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "clay_jar",
|
||||||
|
"name": "Clay Jar",
|
||||||
|
"category": "placeable",
|
||||||
|
"stack": 5,
|
||||||
|
"description": "Fired clay with a lid. Holds 6 stacks, and food kept in it lasts three times as long.",
|
||||||
|
"mesh": "clay_jar",
|
||||||
|
"place": {
|
||||||
|
"piece": "jar",
|
||||||
|
"footprint": [
|
||||||
|
0.7,
|
||||||
|
0.7
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bedroll",
|
||||||
|
"name": "Bedroll",
|
||||||
|
"category": "placeable",
|
||||||
|
"stack": 1,
|
||||||
|
"description": "Hide stuffed with feathers. Sets your spawn point like a bed, and you can sleep the night out under the open sky (but not in rain or snow).",
|
||||||
|
"mesh": "bedroll",
|
||||||
|
"place": {
|
||||||
|
"piece": "bed",
|
||||||
|
"footprint": [
|
||||||
|
0.9,
|
||||||
|
2.0
|
||||||
|
],
|
||||||
|
"open_air": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pelt_rug",
|
||||||
|
"name": "Wolf-pelt Rug",
|
||||||
|
"category": "placeable",
|
||||||
|
"stack": 1,
|
||||||
|
"description": "Wolf pelts stitched into a rug. In a sheltered room: a little warmer, and you get Rested twice as fast.",
|
||||||
|
"mesh": "pelt_rug",
|
||||||
|
"place": {
|
||||||
|
"piece": "rug",
|
||||||
|
"footprint": [
|
||||||
|
1.4,
|
||||||
|
1.8
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fang_necklace",
|
||||||
|
"name": "Fang Necklace",
|
||||||
|
"category": "tool",
|
||||||
|
"stack": 1,
|
||||||
|
"description": "The alpha's fangs on a strip of its hide. Carry it anywhere in your bag and wolves, the alpha too, bite 30% less hard.",
|
||||||
|
"mesh": "fang_necklace",
|
||||||
|
"charm": {
|
||||||
|
"against": "wolf",
|
||||||
|
"damage": 0.7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bear_cloak",
|
||||||
|
"name": "Bear Cloak",
|
||||||
|
"category": "tool",
|
||||||
|
"stack": 1,
|
||||||
|
"description": "Right-click to wear it (+40 warmth, rain barely touches you): the warmest cloak there is. Take it off from the inventory screen.",
|
||||||
|
"mesh": "bear_cloak",
|
||||||
|
"wear": {
|
||||||
|
"warmth": 40,
|
||||||
|
"wet_chill": 0.3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pemmican",
|
||||||
|
"name": "Pemmican",
|
||||||
|
"category": "food",
|
||||||
|
"stack": 20,
|
||||||
|
"description": "Cooked meat and berries pounded into bear fat. Filling, counts as meat and greens, and keeps 30 days.",
|
||||||
|
"mesh": "pemmican",
|
||||||
|
"keeps": 30,
|
||||||
|
"food": {
|
||||||
|
"hunger": 40,
|
||||||
|
"health": 10,
|
||||||
|
"diet": {
|
||||||
|
"meat": 28,
|
||||||
|
"plants": 28
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tallow_candle",
|
||||||
|
"name": "Tallow Candle",
|
||||||
|
"category": "placeable",
|
||||||
|
"stack": 20,
|
||||||
|
"description": "Bear fat and a fibre wick. Set it on a floor or a step and it lights a small room, steadily, for good.",
|
||||||
|
"mesh": "tallow_candle",
|
||||||
|
"place": {
|
||||||
|
"piece": "lamp",
|
||||||
|
"footprint": [
|
||||||
|
0.2,
|
||||||
|
0.2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lamp": {
|
||||||
|
"range": 7.0,
|
||||||
|
"energy": 1.2,
|
||||||
|
"height": 0.2,
|
||||||
|
"flicker": 0.08,
|
||||||
|
"flame": 0.022,
|
||||||
|
"color": [
|
||||||
|
1.0,
|
||||||
|
0.82,
|
||||||
|
0.6
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "torch_stand",
|
||||||
|
"name": "Torch Stand",
|
||||||
|
"category": "placeable",
|
||||||
|
"stack": 5,
|
||||||
|
"description": "A torch on a wooden stand. Lights a whole room or a doorway, and never goes out.",
|
||||||
|
"mesh": "torch_stand",
|
||||||
|
"place": {
|
||||||
|
"piece": "lamp",
|
||||||
|
"footprint": [
|
||||||
|
0.5,
|
||||||
|
0.5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lamp": {
|
||||||
|
"range": 11.0,
|
||||||
|
"energy": 1.6,
|
||||||
|
"height": 1.45,
|
||||||
|
"flicker": 0.22,
|
||||||
|
"flame": 0.06,
|
||||||
|
"color": [
|
||||||
|
1.0,
|
||||||
|
0.74,
|
||||||
|
0.5
|
||||||
|
],
|
||||||
|
"shadows": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,6 +249,26 @@
|
|||||||
"trout": 1
|
"trout": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "grilled_mackerel",
|
||||||
|
"output": "grilled_mackerel",
|
||||||
|
"count": 1,
|
||||||
|
"station": "campfire",
|
||||||
|
"category": "cooking",
|
||||||
|
"inputs": {
|
||||||
|
"mackerel": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "grilled_cod",
|
||||||
|
"output": "grilled_cod",
|
||||||
|
"count": 1,
|
||||||
|
"station": "campfire",
|
||||||
|
"category": "cooking",
|
||||||
|
"inputs": {
|
||||||
|
"cod": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "charcoal_kiln",
|
"id": "charcoal_kiln",
|
||||||
"output": "charcoal_kiln",
|
"output": "charcoal_kiln",
|
||||||
@@ -578,6 +598,28 @@
|
|||||||
"salt": 1
|
"salt": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "salted_mackerel",
|
||||||
|
"output": "salted_fish",
|
||||||
|
"count": 2,
|
||||||
|
"station": "",
|
||||||
|
"category": "cooking",
|
||||||
|
"inputs": {
|
||||||
|
"mackerel": 2,
|
||||||
|
"salt": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "salted_cod",
|
||||||
|
"output": "salted_fish",
|
||||||
|
"count": 2,
|
||||||
|
"station": "",
|
||||||
|
"category": "cooking",
|
||||||
|
"inputs": {
|
||||||
|
"cod": 1,
|
||||||
|
"salt": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "antler_pickaxe",
|
"id": "antler_pickaxe",
|
||||||
"output": "antler_pickaxe",
|
"output": "antler_pickaxe",
|
||||||
@@ -589,6 +631,98 @@
|
|||||||
"iron_bar": 1,
|
"iron_bar": 1,
|
||||||
"wood": 2
|
"wood": 2
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "clay_jar",
|
||||||
|
"output": "clay_jar",
|
||||||
|
"count": 1,
|
||||||
|
"station": "campfire",
|
||||||
|
"category": "furniture",
|
||||||
|
"inputs": {
|
||||||
|
"clay": 8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bedroll",
|
||||||
|
"output": "bedroll",
|
||||||
|
"count": 1,
|
||||||
|
"station": "workbench",
|
||||||
|
"category": "furniture",
|
||||||
|
"inputs": {
|
||||||
|
"hide": 3,
|
||||||
|
"feather": 6,
|
||||||
|
"fiber": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pelt_rug",
|
||||||
|
"output": "pelt_rug",
|
||||||
|
"count": 1,
|
||||||
|
"station": "workbench",
|
||||||
|
"category": "furniture",
|
||||||
|
"inputs": {
|
||||||
|
"wolf_pelt": 3,
|
||||||
|
"fiber": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fang_necklace",
|
||||||
|
"output": "fang_necklace",
|
||||||
|
"count": 1,
|
||||||
|
"station": "workbench",
|
||||||
|
"category": "clothing",
|
||||||
|
"inputs": {
|
||||||
|
"wolf_fang": 2,
|
||||||
|
"alpha_pelt": 1,
|
||||||
|
"fiber": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bear_cloak",
|
||||||
|
"output": "bear_cloak",
|
||||||
|
"count": 1,
|
||||||
|
"station": "workbench",
|
||||||
|
"category": "clothing",
|
||||||
|
"inputs": {
|
||||||
|
"bear_hide": 2,
|
||||||
|
"hide": 2,
|
||||||
|
"fiber": 4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "pemmican",
|
||||||
|
"output": "pemmican",
|
||||||
|
"count": 3,
|
||||||
|
"station": "campfire",
|
||||||
|
"category": "cooking",
|
||||||
|
"inputs": {
|
||||||
|
"cooked_meat": 2,
|
||||||
|
"berries": 3,
|
||||||
|
"bear_fat": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tallow_candle",
|
||||||
|
"output": "tallow_candle",
|
||||||
|
"count": 4,
|
||||||
|
"station": "",
|
||||||
|
"category": "furniture",
|
||||||
|
"inputs": {
|
||||||
|
"bear_fat": 1,
|
||||||
|
"fiber": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "torch_stand",
|
||||||
|
"output": "torch_stand",
|
||||||
|
"count": 1,
|
||||||
|
"station": "",
|
||||||
|
"category": "furniture",
|
||||||
|
"inputs": {
|
||||||
|
"wood": 2,
|
||||||
|
"stick": 2,
|
||||||
|
"fiber": 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
22
data/worldlets/cove.json
Normal file
22
data/worldlets/cove.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"_doc": "The Cove (docs/WORLDLETS_DESIGN.md §5, W3): the second worldlet, reached from the Valley down the cliff tunnel (J1). Low hills run down to a wide sandy bay with a pier (scripts/world/pier.gd) for sea fishing; a small pond inland; mountains behind. Not a start: you get here on the journey.",
|
||||||
|
"id": "cove",
|
||||||
|
"name": "Cove",
|
||||||
|
"description": "Low hills running down to a sheltered sandy bay, with a pier out over the water for sea fishing.",
|
||||||
|
"start": false,
|
||||||
|
"size": 320,
|
||||||
|
"resolution": 256,
|
||||||
|
"terrain": [
|
||||||
|
{"type": "hills", "height_scale": 5.5, "detail": 1.2},
|
||||||
|
{"type": "rim", "play_radius": 100.0, "rim_width": 30.0, "mountain_height": 52.0, "rim_warp": 14.0, "wall_frac": 0.45},
|
||||||
|
{"type": "beach", "angle_deg": 90.0, "half_width_deg": 55.0, "fade_deg": 22.0, "shore_offset": -16.0, "slope": 0.08, "current_out": 22.0, "wall_out": 36.0},
|
||||||
|
{"type": "lake", "center": [-30.0, -40.0], "radius": 11.0, "water_level": -2.6},
|
||||||
|
{"type": "ocean_edge"}
|
||||||
|
],
|
||||||
|
"climate": {"snowline": 42.0},
|
||||||
|
"places": [{"type": "pier", "at": "beach"}],
|
||||||
|
"next": [
|
||||||
|
{"to": "valley", "by": "tunnel", "where": "sea_cave"},
|
||||||
|
{"to": "highlands", "by": "sail", "where": "pier"}
|
||||||
|
]
|
||||||
|
}
|
||||||
18
data/worldlets/valley.json
Normal file
18
data/worldlets/valley.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"_doc": "A worldlet (docs/WORLDLETS_DESIGN.md): its name and description (the New World page), whether a new world can start here (\"start\"; the others are reached on the journey), its size, and its terrain as building blocks applied in order (scripts/world/terrain.gd: hills, rim, sea_cliff, lake, ocean_edge). The numbers here are the Valley exactly as it was hand-coded: tests/test_worldlets.gd checks it still makes the same height grid.",
|
||||||
|
"id": "valley",
|
||||||
|
"name": "Valley",
|
||||||
|
"description": "A sheltered valley ringed by mountains, with a lake in the middle and sea cliffs on one side.",
|
||||||
|
"start": true,
|
||||||
|
"size": 320,
|
||||||
|
"resolution": 256,
|
||||||
|
"terrain": [
|
||||||
|
{"type": "hills", "height_scale": 9.0, "detail": 1.4},
|
||||||
|
{"type": "rim", "play_radius": 100.0, "rim_width": 30.0, "mountain_height": 58.0, "rim_warp": 16.0, "wall_frac": 0.45},
|
||||||
|
{"type": "sea_cliff", "angle_deg": -75.0, "half_width_deg": 38.0, "fade_deg": 22.0, "sea_level": -24.0, "cliff_top": 0.5, "cliff_offset": 4.0, "drop_width": 6.0},
|
||||||
|
{"type": "lake", "center": [14.0, -46.0], "radius": 24.0, "water_level": -2.6},
|
||||||
|
{"type": "ocean_edge"}
|
||||||
|
],
|
||||||
|
"climate": {"snowline": 40.0},
|
||||||
|
"next": [{"to": "cove", "by": "tunnel", "where": "sea_cliff"}]
|
||||||
|
}
|
||||||
@@ -1,6 +1,26 @@
|
|||||||
# Progression and challenges design
|
# Progression and challenges design
|
||||||
|
|
||||||
Status: **P1-P5 built** (iron, stone, adversaries, hazards, the Old Stag).
|
Status: **P1-P6 built** (iron, stone, adversaries, hazards, the Old Stag,
|
||||||
|
and uses for every material).
|
||||||
|
|
||||||
|
*As built in P6 (0.3.0, every material earns its place):*
|
||||||
|
- Clay jar (8 clay, fired at a campfire): 6 slots, food inside ages at a
|
||||||
|
third of the rate (`Inventory.keep_mult`, which moves the "rot" stamps).
|
||||||
|
- Bedroll (3 hide, 6 feathers, 4 fibre): a bed (group "bed", like the bed
|
||||||
|
everywhere a spawn bed is looked for) that needs no shelter, only no rain
|
||||||
|
or snow on it.
|
||||||
|
- Wolf-pelt rug (3 wolf pelts): a rug, so Cozy like the fur rug.
|
||||||
|
- Fang necklace (2 fangs, 1 alpha pelt): a "charm" carried anywhere in the
|
||||||
|
bag; hits whose cause names a wolf do 70% (`Actions.charm_mult`).
|
||||||
|
- Bear cloak (2 bear hide, 2 hide): +40 warmth, the warmest.
|
||||||
|
- Pemmican (2 cooked meat, 3 berries, 1 bear fat at a campfire, makes 3):
|
||||||
|
meat and greens, keeps 30 days.
|
||||||
|
- Rotten food is fishing bait: carried, bite waits x0.6, a bite uses one.
|
||||||
|
- Lights to place (after 0.3.0): tallow candle (bear fat and fibre, makes 4,
|
||||||
|
7 m) and torch stand (11 m, shadows), always lit (`lamp.gd`).
|
||||||
|
- `Q` drops what you hold (shift: one), at your feet, online too; `K`/`M`
|
||||||
|
move the hotbar up and down the bag's five rows (any slot can be held).
|
||||||
|
- `tests/test_items_more.gd` checks every material has a use.
|
||||||
|
|
||||||
*As built in P1:*
|
*As built in P1:*
|
||||||
- Iron nuggets (70, in the scree under the rim), clay (60, on the lake
|
- Iron nuggets (70, in the scree under the rim), clay (60, on the lake
|
||||||
|
|||||||
@@ -85,8 +85,22 @@ phases M2.0-M2.7).
|
|||||||
## M4 — Content & polish · ongoing
|
## M4 — Content & polish · ongoing
|
||||||
- More biomes, creatures, items, building pieces
|
- More biomes, creatures, items, building pieces
|
||||||
- PvP / raiding rules, building damage
|
- PvP / raiding rules, building damage
|
||||||
- Better UI, settings, controls
|
- Better UI, settings, controls. *Done so far:* volume sliders, tips and
|
||||||
- Audio, music, weather
|
update settings in the pause menu; `Q` drops items; `K`/`M` put any bag row
|
||||||
|
on the 1-8 keys (0.3.0); the map on `N`, filling in as you explore, and a
|
||||||
|
compass along the top (0.4.0, Gitea issue #1).
|
||||||
|
- More places: the worldlets and the journey (`docs/WORLDLETS_DESIGN.md`).
|
||||||
|
*Done (0.4.0):* W0-W3 and J1, the Cove reached down the cliff tunnel
|
||||||
|
(single-player). Next J3 (multiplayer, "spread out"), then J2/J4.
|
||||||
|
- Audio, music, weather. *Done (0.2.0-0.2.1):* mood music (calm, night,
|
||||||
|
storm, danger, boss) that crossfades with what's happening, ambience (CC0
|
||||||
|
birdsong, crickets, owls, fire), sound effects for every action (Kenney
|
||||||
|
CC0 plus synthesised), creature voices, Old Tam's spoken lines (Kokoro).
|
||||||
|
Sources and licences: `assets/audio/CREDITS.md`; tools in `tools/audio/`
|
||||||
|
and `tools/voice/`.
|
||||||
|
- Help: the player guide on the download site (`tools/site/`), written
|
||||||
|
sections plus item, recipe, creature, fish and server-setting tables made
|
||||||
|
from the game's own data each build. The trailer (`tools/trailer/`).
|
||||||
- Packaged builds for Win/Mac/Linux (+ Mac signing/notarisation).
|
- Packaged builds for Win/Mac/Linux (+ Mac signing/notarisation).
|
||||||
*First cut done:* `tools/export.sh` makes a universal macOS .app (ad-hoc
|
*First cut done:* `tools/export.sh` makes a universal macOS .app (ad-hoc
|
||||||
signed, not notarised) and a Windows x86_64 .exe + .pck (unsigned), zipped
|
signed, not notarised) and a Windows x86_64 .exe + .pck (unsigned), zipped
|
||||||
|
|||||||
@@ -1,7 +1,98 @@
|
|||||||
# Worldlets design
|
# Worldlets design
|
||||||
|
|
||||||
Status: **proposal**. Nothing here is built yet, apart from the first piece
|
Status: **W0-W3 and J1 built** (the Cove without its stream yet; the
|
||||||
of groundwork (`world.water_at()` and `data/fish.json`, see §6).
|
tunnel single-player only); the rest is planned.
|
||||||
|
|
||||||
|
*As built in J1:*
|
||||||
|
- `scripts/world/tunnel.gd`: the seam (an outcrop on the clifftop strip,
|
||||||
|
7 m in from the edge, its angle from the seed) and the tunnel space, out
|
||||||
|
of sight 260 m under the island: a CSG rock block with the landings and
|
||||||
|
six sloping stretches cut out (6 m on, 4 m down each, switching back on
|
||||||
|
two lanes: 24 m down), then a sea cave. Faces (`tunnel_face.gd`) answer
|
||||||
|
like boulders, so the pick works on them: tier 3 only ("Too hard: you
|
||||||
|
need a better pick"), 10 blows for the seam and 15 per stretch. Each
|
||||||
|
stretch must be propped (2 wood, [E] at its mouth) before the next face
|
||||||
|
takes a blow ("It's loose..."): 10 wood all the way down.
|
||||||
|
- Underground: no sun, moon, sky reflections or sky-tinted fog
|
||||||
|
(`day_night.underground`), no weather, no lake water, sheltered. Candles
|
||||||
|
and torch stands place anywhere on its floor and save as ordinary pieces.
|
||||||
|
- Walking out of the sea cave's mouth travels (`world.travel()`): the
|
||||||
|
worldlet you leave is saved, your character and the clock go with you,
|
||||||
|
and the other loads, arriving at its sea cave (`sea_cave.gd`, on the
|
||||||
|
Cove's beach where the headland starts). [E] at the cave goes back up the
|
||||||
|
same tunnel to its bottom, once it's dug through.
|
||||||
|
- Saves: `world.json` is the home worldlet's state, as before;
|
||||||
|
others are `worldlets/<id>.json`; meta.json's `worldlet` is where you
|
||||||
|
are, `home` where you began, `arrive` where the next load puts you.
|
||||||
|
- `tests/test_tunnel.gd`: dig through, walk every stretch down and back
|
||||||
|
up, light it, reload, out to the Cove, back up, out at the top, and the
|
||||||
|
chest at home is still there.
|
||||||
|
- Not yet: Old Tam's tip about the seam, the Guide page, and travel on a
|
||||||
|
server (J3: the tunnel isn't built online). The journey between
|
||||||
|
worldlets is §10, and the two ways to play (the journey, or one land) are in
|
||||||
|
its decisions.
|
||||||
|
|
||||||
|
*As built in W3:*
|
||||||
|
- `data/worldlets/cove.json`: low hills, mountains behind, a wide sandy
|
||||||
|
bay (a `beach` block) and a small pond. Not a start; its `next` is back
|
||||||
|
to the Valley (tunnel, J1) and on to the Highlands (sail).
|
||||||
|
- The pier (`scripts/world/pier.gd`), a world "place" (`"places":
|
||||||
|
[{"type": "pier"}]`): planks on posts from where the sand is at deck
|
||||||
|
height to 24 m past the waterline. Worked out from the terrain, so it's
|
||||||
|
the same for everyone and never saved or dismantled; scenery keeps clear
|
||||||
|
of it. ~1.9 m of sea at its end, ~2.7 m a cast beyond.
|
||||||
|
- Sea fish in `data/fish.json`: mackerel (common) and cod, which only bites
|
||||||
|
in water 2.2 m or deeper (a per-fish `min_depth`). Grilled at a campfire,
|
||||||
|
salted, or smoked, like the lake's.
|
||||||
|
- New World has no "Where": the journey always begins in the Valley (the
|
||||||
|
Host page keeps it, for One land mode). Started with
|
||||||
|
`godot --path . -- --dev-worldlets`, New World and Host offer every
|
||||||
|
worldlet, the others marked "(testing)", to try one directly.
|
||||||
|
- No sea stacks without a sea cliff.
|
||||||
|
- Tests: `test_cove` (walk onto the pier, catch a mackerel off its end, cod
|
||||||
|
only in deep water), and the Cove in `test_terrain_edges` and
|
||||||
|
`test_boundary`.
|
||||||
|
- Still to do: the stream (it needs water that runs downhill, which the
|
||||||
|
one flat water surface can't do: its own ribbon of water, planned with
|
||||||
|
the Cove's sea cave when the tunnel arrives).
|
||||||
|
|
||||||
|
*As built in W2:*
|
||||||
|
- `lake` blocks can repeat: every lake and pond is its own body
|
||||||
|
(`terrain.lakes`, `world.water_bodies()`), all at one surface
|
||||||
|
(`water_level`, set by the first). The first is still "the lake" to the
|
||||||
|
older code (ice, its fish, the tutorial).
|
||||||
|
- A `beach` block (angle, width, `shore_offset`, `slope`, `current_out`,
|
||||||
|
`wall_out`): the land eases to a waterline and shelves gently into the
|
||||||
|
sea, which is then the same surface as the ponds (one ocean sheet, no
|
||||||
|
inland disc). The invisible wall stands well out in the water, and before
|
||||||
|
it a current (`terrain.current_at()`, `world._current()`) carries
|
||||||
|
swimmers back faster than they can swim, with a toast.
|
||||||
|
- `water_at()` tells the sea off a beach from a pond.
|
||||||
|
- Test-only worldlets live in `tests/worldlets/` (never listed or offered).
|
||||||
|
`testbay.json` (two ponds and a beach) runs through
|
||||||
|
`test_water_bodies`, `test_terrain_edges` (8 seeds) and `test_boundary`
|
||||||
|
(swimming out across the beach).
|
||||||
|
|
||||||
|
*As built in W1:* a "Where" picker on the New World and Host Game pages
|
||||||
|
(worldlets with `"start": true`; only the Valley, so it's greyed out), the
|
||||||
|
worldlet in the Load list, and a hosted island keeps its worldlet like its
|
||||||
|
seed.
|
||||||
|
|
||||||
|
*As built in W0:*
|
||||||
|
- `data/worldlets/valley.json`: the Valley as data, its terrain as five
|
||||||
|
building blocks in order (`hills`, `rim`, `sea_cliff`, `lake`,
|
||||||
|
`ocean_edge`) with every number that used to be hard-coded.
|
||||||
|
- `terrain.gd` runs a worldlet's blocks (`use_worldlet()`); each block is
|
||||||
|
the old code's arithmetic, unchanged. `tests/test_worldlets.gd` checks
|
||||||
|
four seeds give exactly the same height grid as before (SHA-256 of the
|
||||||
|
grid, taken from the old code).
|
||||||
|
- The worldlet is chosen like the seed: a server's (it sends its real one),
|
||||||
|
else the save's `meta.json` "worldlet", else the Valley (worlds from
|
||||||
|
before W0). New worlds and servers record it.
|
||||||
|
- The WorldInfo API on the world: `worldlet_id()`, `water_bodies()`,
|
||||||
|
`climate()`, `crossings()` (the planned ways on), beside `water_at()`.
|
||||||
|
Systems still read the terrain's numbers directly; they move over to
|
||||||
|
these as each new worldlet needs them to differ (W1-W3).
|
||||||
|
|
||||||
## 1. What a worldlet is
|
## 1. What a worldlet is
|
||||||
A **worldlet** is a small, complete world type: a ~320 m playable area with
|
A **worldlet** is a small, complete world type: a ~320 m playable area with
|
||||||
@@ -164,3 +255,197 @@ its edge or animals that spawn in the sea.
|
|||||||
wolves sooner, or keep the same grace period everywhere?
|
wolves sooner, or keep the same grace period everywhere?
|
||||||
4. **Which second worldlet first:** Cove (sea fishing, the beach you
|
4. **Which second worldlet first:** Cove (sea fishing, the beach you
|
||||||
mentioned) is my suggestion.
|
mentioned) is my suggestion.
|
||||||
|
|
||||||
|
## 10. The journey: tunnelling out (proposal, Nov 2026)
|
||||||
|
|
||||||
|
A goal for the game, from Paul: once you have a good enough pick, you tunnel
|
||||||
|
through the mountain rim into the next, slightly harder worldlet, and now and
|
||||||
|
then go back to earlier ones for what only they have. This is option B
|
||||||
|
(linked worldlets) from §2, reached by digging rather than a path.
|
||||||
|
|
||||||
|
### The shape of it
|
||||||
|
- **A chain of worldlets**, each a little harder: Valley (today's island) →
|
||||||
|
Cove (sea, salt, mackerel) → Highlands (cold, deep snow, bigger packs) →
|
||||||
|
Marsh (bog iron, sickness, mists) → the Ember, the volcanic heart the game
|
||||||
|
is named for: the end of the journey.
|
||||||
|
- **The goal**, something to tell players on day 1: Old Tam talks of the
|
||||||
|
land beyond the mountains, where the fire in the earth still burns. Reach
|
||||||
|
the Ember and light the old beacon there. (Whether there's an ending or
|
||||||
|
it's open-ended is a question below.)
|
||||||
|
- **Gates:** each tunnel needs the best tool from the worldlet you're in.
|
||||||
|
The antler pickaxe (from the Old Stag) opens the Valley's tunnel; the
|
||||||
|
Highlands need a pick made from something found in the Cove, and so on.
|
||||||
|
Each worldlet has its own boss or trial, like the Old Stag.
|
||||||
|
|
||||||
|
### A different crossing each time (Paul, Nov 2026)
|
||||||
|
Players have to work out how to get from each worldlet to the next, and
|
||||||
|
it's a different way every time: tunnelling, climbing, burrowing, sailing.
|
||||||
|
Each crossing is that worldlet's puzzle, and what it needs comes from what
|
||||||
|
you've learned and gathered there:
|
||||||
|
|
||||||
|
| From → to | Crossing | Needs (a first idea) |
|
||||||
|
|---|---|---|
|
||||||
|
| Valley → Cove | **Tunnel** through the rim at a seam | the antler pickaxe, timber props, light |
|
||||||
|
| Cove → Highlands | **Sail** out of the bay and round the headland | a boat (planks, nails, a hide sail, rope), calm weather, food that keeps |
|
||||||
|
| Highlands → Marsh | **Climb** a frozen cliff | climbing picks and rope, warmth to last the climb (bear cloak, pemmican) |
|
||||||
|
| Marsh → Ember | **Burrow** under the old lava crust | a digging tool from bog iron, and a way through the heat and gas |
|
||||||
|
|
||||||
|
- **The hint, not the answer:** each worldlet shows its way out without
|
||||||
|
explaining it: a seam in the rock with old pick marks, a wrecked boat on
|
||||||
|
the beach, rope frayed on a cliff ledge. Old Tam (and his tips) nudge,
|
||||||
|
and the guide can keep it vague.
|
||||||
|
- **Coming back** uses the same crossing in reverse (the tunnel stays dug,
|
||||||
|
the boat stays moored, the rope stays fixed), so supply runs get easier
|
||||||
|
once you've made the way.
|
||||||
|
- **New systems this needs, one per crossing:** digging into a rim seam
|
||||||
|
(tunnel and burrow share most of it), a boat you build and steer on the
|
||||||
|
water, and climbing a marked face. Each is a phase of its own (J1 tunnel;
|
||||||
|
later the sail, the climb, the burrow).
|
||||||
|
|
||||||
|
### Tunnelling (the first crossing)
|
||||||
|
- **Where:** each worldlet's rim has one or two **seams**: cracked, darker
|
||||||
|
rock faces with a few loose stones at the foot, findable by exploring
|
||||||
|
(and Old Tam's tips). The rim stays a wall everywhere else.
|
||||||
|
- **How:** hit a seam with a good enough pick, many times. Each stretch dug
|
||||||
|
needs **timber props** (wood) or it falls back in, and it's dark in there:
|
||||||
|
candles, torch stands or a lantern. The tunnel is a short, built-for-it
|
||||||
|
cave (a few bends, maybe a side chamber with ore or a hazard), so it
|
||||||
|
stays hand-made rather than a hole through the terrain mesh, which the
|
||||||
|
heightmap can't do.
|
||||||
|
- **Through:** the far end opens onto the next worldlet. Walking out loads
|
||||||
|
it (a short fade, "The Cove"), with your bag, cloak and stats as they are.
|
||||||
|
|
||||||
|
### The first tunnel: down the sea cliff to the Cove
|
||||||
|
The Valley's sea side ends at a sheer cliff, its top about level with the
|
||||||
|
valley and the sea some 25 m below. The Cove is down there, so the first
|
||||||
|
tunnel goes **down**, inside the cliff, from the clifftop to the shore.
|
||||||
|
- **The seam:** a cracked outcrop on the clifftop strip near the edge (the
|
||||||
|
sea_cliff block marks where), with old pick marks and loose stones at its
|
||||||
|
foot. It can't be dug until you have the antler pickaxe (tier 3); a lesser
|
||||||
|
pick just chips it ("Too hard: you need a better pick").
|
||||||
|
- **Digging down:** the tunnel is about six stretches, each a few metres on
|
||||||
|
and a few down: a sloping passage that turns back on itself like a
|
||||||
|
stair, so the 25 m drop fits inside the cliff. A stretch takes many pick
|
||||||
|
blows (stamina, and wear on the pick) to open.
|
||||||
|
- **Props:** each new stretch needs a **timber prop** (2 wood) set at its
|
||||||
|
mouth before you can dig the next. Leave one out and the face won't take
|
||||||
|
another blow ("It's loose: prop it first"); nothing caves in on you, so
|
||||||
|
it's a cost, not a trap. That's 10 wood all the way down.
|
||||||
|
- **Light:** the tunnel has no daylight at all. Every stretch is pitch dark
|
||||||
|
until you bring light: a torch in your hand works, but your hands are full
|
||||||
|
with the pick, so the way to do it is to set **candles and torch stands**
|
||||||
|
as you go (tallow candles from bear fat, torch stands from wood). Placed
|
||||||
|
lights stay, so the way is lit for the trip back and for friends.
|
||||||
|
- **The bottom:** the last stretch breaks out into a sea cave, its mouth
|
||||||
|
opening on the Cove's shingle beach. Walking out of the mouth is the
|
||||||
|
crossing: a fade, then the Cove, arriving at the cave's Cove end. The way
|
||||||
|
back starts there and climbs the same tunnel.
|
||||||
|
- **What's saved:** the tunnel belongs to the Valley's save: how far it's
|
||||||
|
dug, the props, the lights. On a server it's shared: anyone can dig on
|
||||||
|
from where someone stopped, and the lights are everyone's.
|
||||||
|
- **How it's built:** the tunnel is its own small space (a "between" scene
|
||||||
|
next to the worldlet), made from a kit of rock pieces (straight, turn,
|
||||||
|
slope down, the cave mouth) laid out from the seed, with box-shaped
|
||||||
|
colliders. The heightmap can't have holes or overhangs, so the passage is
|
||||||
|
never cut into the terrain itself: you walk into the seam and you're in
|
||||||
|
the tunnel scene.
|
||||||
|
|
||||||
|
### Why go back
|
||||||
|
- **Each worldlet has things the others lack.** Clay and salt only in the
|
||||||
|
Valley and Cove; little wood in the Highlands; no bears outside the
|
||||||
|
Valley (so bear fat for candles and pemmican is a supply run); a fish or
|
||||||
|
herb only one place has. Food that keeps (pemmican, salted and smoked
|
||||||
|
meat, clay jars) becomes what you carry between them.
|
||||||
|
- **Your bases stay** where you built them: each worldlet keeps its own
|
||||||
|
save file, so your Valley hut, chests and jars are waiting.
|
||||||
|
- **Time:** a worldlet you're not in stands still (as a server does with
|
||||||
|
nobody on), so a base you left doesn't starve or get raided.
|
||||||
|
|
||||||
|
### Building it (phases)
|
||||||
|
| Phase | Delivers | Done when |
|
||||||
|
|---|---|---|
|
||||||
|
| **W0-W2** | the worldlet groundwork above (terrain pipeline, worldlet in the save, several water bodies) | as §8 |
|
||||||
|
| **J1** | seams in the rim, digging with props and light, a tunnel scene, travel between two worldlets in single-player, per-worldlet save files | dig through from the Valley, walk to the Cove, walk back to your hut |
|
||||||
|
| **W3** | the Cove (§5) as the second worldlet | its suites pass, and its tunnel back works |
|
||||||
|
| **J2** | gates and supply needs: tool tiers per tunnel, resources only some worldlets have | a test bot can't open the second tunnel without the Cove's pick |
|
||||||
|
| **J3** | multiplayer travel (see below) | two players in different worldlets on one server, or the rule we choose |
|
||||||
|
| **J4** | Highlands, Marsh, the Ember and its ending | the journey can be finished |
|
||||||
|
|
||||||
|
### Multiplayer: the hard part
|
||||||
|
A server today runs one island. Options:
|
||||||
|
- **Everyone together:** travel needs every player at the tunnel (like the
|
||||||
|
sleep rule). Simplest; one worldlet runs at a time.
|
||||||
|
- **Spread out:** the server runs each worldlet that has someone in it.
|
||||||
|
The most freedom, and the most work and CPU.
|
||||||
|
- **Party leader:** whoever goes through brings the others with a vote.
|
||||||
|
|
||||||
|
### Decisions (Paul, Nov 2026)
|
||||||
|
- **Extendable, not finished:** the Ember has a last boss, and after it you
|
||||||
|
keep playing. The chain is built so more worldlets (and crossings) can be
|
||||||
|
added after the Ember later: each worldlet names the next, nothing assumes
|
||||||
|
the Ember is the end.
|
||||||
|
- **The crossings:** tunnel, sail, climb, burrow, in that order.
|
||||||
|
- **Harder by biome, not just by numbers:** each worldlet has its own,
|
||||||
|
stronger animals and its own hazards. The Marsh, for one: mosquitoes whose
|
||||||
|
bites can make you sick, and bog, mist and the like. Others in the same
|
||||||
|
spirit per biome (to design with each worldlet).
|
||||||
|
- **Multiplayer: spread out.** Each player goes where they like; the server
|
||||||
|
runs every worldlet someone is in, and the rest stand still. Built in J3,
|
||||||
|
after single-player travel works. What that means for the code:
|
||||||
|
- one server process holds several worldlet scenes, each with its own
|
||||||
|
clock, weather, creatures and save file;
|
||||||
|
- every player belongs to one worldlet at a time, and the server only
|
||||||
|
sends them that worldlet's world, creatures, sounds and chat-free events
|
||||||
|
(chat and the player list stay server-wide);
|
||||||
|
- a crossing moves a player from one worldlet to another (save their
|
||||||
|
place, load or wake the other worldlet, send them its world);
|
||||||
|
- sleep: "everyone in bed" counts the players in that worldlet only;
|
||||||
|
- the Old Stag and other bosses scale with the players in their worldlet.
|
||||||
|
- **Boats: one each, no passengers.** A boat belongs to whoever built it
|
||||||
|
(like a bed): only the owner, and their friends under the server's locks
|
||||||
|
rule, can sail it, and nobody else can dismantle it. It stays moored where
|
||||||
|
it was last left (the Cove end or the Highlands end), so nobody is
|
||||||
|
stranded by someone else taking the only boat. Lose or wreck it and you
|
||||||
|
build another, which is harder where wood is scarce (the Highlands), so
|
||||||
|
plan the trip back. Each player has to earn their own crossing by boat.
|
||||||
|
- **The other crossings are shared:** a dug tunnel, a fixed climbing rope
|
||||||
|
and a dug burrow are changes to the world, there for everyone once one
|
||||||
|
player has made them (like a door or a bridge).
|
||||||
|
- **Two ways to play (Paul, Nov 2026).**
|
||||||
|
- **The journey** (the standard game), single-player and multiplayer
|
||||||
|
alike: every new world starts in the Valley and the worldlets come in
|
||||||
|
their fixed order (Valley, Cove, Highlands, Marsh, Ember). Each
|
||||||
|
worldlet's shape is set by its definition; the seed only changes the
|
||||||
|
detail (hills, trees, where the den is), so the journey is the same
|
||||||
|
adventure for everyone.
|
||||||
|
- **One land** (a hosting choice): no journey, no crossings, the server
|
||||||
|
is locked to one worldlet, and the host picks which, from all of them.
|
||||||
|
Co-op or PvP as now (the rules preset). In code: a `mode` in
|
||||||
|
`server.json` ("journey" / "one_land"); in one-land mode the Host
|
||||||
|
page's "Where" lists every worldlet rather than only the starts, and
|
||||||
|
the crossings (`world.crossings()`) answer none, so a tunnel seam
|
||||||
|
can't be dug. Kept, like the seed and the worldlet, once the island
|
||||||
|
exists. Worth building once the Cove exists (until then there's only
|
||||||
|
the Valley to pick).
|
||||||
|
|
||||||
|
### Server cost, measured (0.3.1)
|
||||||
|
One worldlet, a dedicated server on the M5 Pro, never pausing: about
|
||||||
|
**12% of one CPU core and 270 MB of memory**, with nobody on. Players add
|
||||||
|
creatures, weather and pickups around them. So with *spread out*, the worst
|
||||||
|
case of 8 players in all 5 worldlets is about five of today's servers in
|
||||||
|
one process: roughly 1.4 GB and a little over half a core before players,
|
||||||
|
and worldlets with nobody in them stand still and cost next to nothing.
|
||||||
|
Comfortable on the home Mac; a small cloud server (2 GB) would be tight.
|
||||||
|
|
||||||
|
### (Answered) questions
|
||||||
|
1. **The ending:** a real ending at the Ember (light the beacon, a last
|
||||||
|
boss, the credits) or open-ended (the journey is the goal, keep living)?
|
||||||
|
2. **The crossings:** do the four above fit (tunnel, sail, climb, burrow),
|
||||||
|
and in that order? Should a worldlet have more than one way out (a hard
|
||||||
|
one and a long one)? And for the tunnel: only at marked seams, or
|
||||||
|
anywhere in the rim (more freedom, harder to make good tunnels)?
|
||||||
|
3. **Multiplayer:** everyone together, spread out, or a party vote?
|
||||||
|
4. **How much harder** each worldlet: stronger animals and weather only, or
|
||||||
|
new rules too (thirst in the Ember, sickness in the Marsh)?
|
||||||
|
5. **Order:** Cove second (sea fishing, gentle) as planned, or straight
|
||||||
|
into the Highlands for a harder step?
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ runnable=true
|
|||||||
dedicated_server=false
|
dedicated_server=false
|
||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter="data/*.json"
|
include_filter="data/*.json, data/worldlets/*.json"
|
||||||
exclude_filter="tests/*, docs/*, tools/*, server/*"
|
exclude_filter="tests/*, docs/*, tools/*, server/*"
|
||||||
export_path="build/macos/Emberwild.zip"
|
export_path="build/macos/Emberwild.zip"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
@@ -29,8 +29,8 @@ application/icon_interpolation=4
|
|||||||
application/bundle_identifier="com.pbailey.emberwild"
|
application/bundle_identifier="com.pbailey.emberwild"
|
||||||
application/signature=""
|
application/signature=""
|
||||||
application/app_category="Games"
|
application/app_category="Games"
|
||||||
application/short_version="0.2.1"
|
application/short_version="0.4.0"
|
||||||
application/version="0.2.1"
|
application/version="0.4.0"
|
||||||
application/copyright=""
|
application/copyright=""
|
||||||
display/high_res=true
|
display/high_res=true
|
||||||
codesign/codesign=1
|
codesign/codesign=1
|
||||||
@@ -64,7 +64,7 @@ runnable=true
|
|||||||
dedicated_server=false
|
dedicated_server=false
|
||||||
custom_features=""
|
custom_features=""
|
||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter="data/*.json"
|
include_filter="data/*.json, data/worldlets/*.json"
|
||||||
exclude_filter="tests/*, docs/*, tools/*, server/*"
|
exclude_filter="tests/*, docs/*, tools/*, server/*"
|
||||||
export_path="build/windows/Emberwild/Emberwild.exe"
|
export_path="build/windows/Emberwild/Emberwild.exe"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
@@ -89,8 +89,8 @@ application/modify_resources=false
|
|||||||
application/icon=""
|
application/icon=""
|
||||||
application/console_wrapper_icon=""
|
application/console_wrapper_icon=""
|
||||||
application/icon_interpolation=4
|
application/icon_interpolation=4
|
||||||
application/file_version="0.2.1"
|
application/file_version="0.4.0"
|
||||||
application/product_version="0.2.1"
|
application/product_version="0.4.0"
|
||||||
application/company_name=""
|
application/company_name=""
|
||||||
application/product_name="Emberwild"
|
application/product_name="Emberwild"
|
||||||
application/file_description="Emberwild"
|
application/file_description="Emberwild"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ config/description="Low-poly survival game — Valheim look, Rust depth, self-ho
|
|||||||
run/main_scene="res://scenes/title.tscn"
|
run/main_scene="res://scenes/title.tscn"
|
||||||
config/features=PackedStringArray("4.7", "Forward Plus")
|
config/features=PackedStringArray("4.7", "Forward Plus")
|
||||||
config/icon="res://icon.svg"
|
config/icon="res://icon.svg"
|
||||||
config/version="0.2.1"
|
config/version="0.4.0"
|
||||||
|
|
||||||
[autoload]
|
[autoload]
|
||||||
|
|
||||||
|
|||||||
@@ -43,5 +43,5 @@ offset_left = 16.0
|
|||||||
offset_top = 12.0
|
offset_top = 12.0
|
||||||
offset_right = 600.0
|
offset_right = 600.0
|
||||||
offset_bottom = 60.0
|
offset_bottom = 60.0
|
||||||
text = "E interact · LMB use/place · RMB eat · R rotate · X dismantle · Tab inventory & crafting · Esc menu & controls"
|
text = "E interact · LMB use/place · RMB eat · R rotate · X dismantle · Q drop · K/M bag row · N map · Tab inventory & crafting · Esc menu & controls"
|
||||||
label_settings = SubResource("Label_1")
|
label_settings = SubResource("Label_1")
|
||||||
|
|||||||
@@ -2,8 +2,15 @@ extends "res://scripts/building/piece.gd"
|
|||||||
## Bed: E sets your spawn point here. Once it's yours, E at night sleeps until
|
## Bed: E sets your spawn point here. Once it's yours, E at night sleeps until
|
||||||
## morning, if the bed is under a roof with walls around it.
|
## morning, if the bed is under a roof with walls around it.
|
||||||
|
|
||||||
|
func _build() -> void:
|
||||||
|
add_to_group("bed") # (a bed or a bedroll: either is a spawn point)
|
||||||
|
|
||||||
func display_name() -> String:
|
func display_name() -> String:
|
||||||
return "Bed"
|
return String(def.get("name", "Bed"))
|
||||||
|
|
||||||
|
## A bedroll: you can sleep in the open (world.sleep_block_reason).
|
||||||
|
func open_air() -> bool:
|
||||||
|
return bool(def.get("place", {}).get("open_air", false))
|
||||||
|
|
||||||
func is_spawn() -> bool:
|
func is_spawn() -> bool:
|
||||||
var w := get_parent().get_parent()
|
var w := get_parent().get_parent()
|
||||||
@@ -11,8 +18,8 @@ func is_spawn() -> bool:
|
|||||||
|
|
||||||
func interact_prompt(_player: Node) -> String:
|
func interact_prompt(_player: Node) -> String:
|
||||||
if not is_spawn():
|
if not is_spawn():
|
||||||
return "Bed [E] Set spawn point"
|
return "%s [E] Set spawn point" % display_name()
|
||||||
return "Bed (your spawn point) [E] Sleep"
|
return "%s (your spawn point) [E] Sleep" % display_name()
|
||||||
|
|
||||||
func interact(player: Node) -> void:
|
func interact(player: Node) -> void:
|
||||||
Actions.use_bed(player, self)
|
Actions.use_bed(player, self)
|
||||||
|
|||||||
1
scripts/building/bed.gd.uid
Normal file
1
scripts/building/bed.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://nx08dmty60ny
|
||||||
1
scripts/building/build_piece.gd.uid
Normal file
1
scripts/building/build_piece.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dsbvhwas6wsxf
|
||||||
1
scripts/building/build_system.gd.uid
Normal file
1
scripts/building/build_system.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://gj02g6p3xfip
|
||||||
1
scripts/building/door.gd.uid
Normal file
1
scripts/building/door.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dhsyx4mrm28oe
|
||||||
1
scripts/building/forge.gd.uid
Normal file
1
scripts/building/forge.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cl5ip4achfdwo
|
||||||
1
scripts/building/hearth.gd.uid
Normal file
1
scripts/building/hearth.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://mjs6qb5p3q1a
|
||||||
29
scripts/building/jar.gd
Normal file
29
scripts/building/jar.gd
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
extends "res://scripts/building/piece.gd"
|
||||||
|
## Clay jar: 6 slots of storage, opened with E like a chest, and food kept in
|
||||||
|
## it lasts three times as long (Inventory.keep_mult).
|
||||||
|
|
||||||
|
const SLOTS := 6
|
||||||
|
const KEEP := 3.0
|
||||||
|
var inventory := Inventory.new(SLOTS)
|
||||||
|
|
||||||
|
func _build() -> void:
|
||||||
|
inventory.keep_mult = KEEP
|
||||||
|
|
||||||
|
func display_name() -> String:
|
||||||
|
return "Clay Jar"
|
||||||
|
|
||||||
|
func dismantle_block_reason() -> String:
|
||||||
|
for s in inventory.slots:
|
||||||
|
if s != null:
|
||||||
|
return "Empty the jar first"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
func interact_prompt(_player: Node) -> String:
|
||||||
|
var used := 0
|
||||||
|
for s in inventory.slots:
|
||||||
|
if s != null:
|
||||||
|
used += 1
|
||||||
|
return "Clay jar (%d/%d, food keeps 3x as long) [E] Open" % [used, SLOTS]
|
||||||
|
|
||||||
|
func interact(player: Node) -> void:
|
||||||
|
player.open_inventory(self)
|
||||||
1
scripts/building/jar.gd.uid
Normal file
1
scripts/building/jar.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ciqsy8msggcke
|
||||||
64
scripts/building/lamp.gd
Normal file
64
scripts/building/lamp.gd
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
extends "res://scripts/building/piece.gd"
|
||||||
|
## A light to place indoors (or anywhere): a tallow candle or a torch stand.
|
||||||
|
## Always lit, no fuel. Its item's "lamp" says how: {"range", "energy",
|
||||||
|
## "height" (of the flame), "flicker" (0 = steady), "color"}.
|
||||||
|
## Flames are a small additive glow; the light itself is an OmniLight3D.
|
||||||
|
|
||||||
|
var _light: OmniLight3D
|
||||||
|
var _flame: MeshInstance3D
|
||||||
|
var _noise := FastNoiseLite.new()
|
||||||
|
var _t := 0.0
|
||||||
|
var _base := 1.0
|
||||||
|
var _flicker := 0.0
|
||||||
|
var _h := 0.5
|
||||||
|
|
||||||
|
func _build() -> void:
|
||||||
|
add_to_group("lamp")
|
||||||
|
var l: Dictionary = def.get("lamp", {})
|
||||||
|
_base = float(l.get("energy", 1.0))
|
||||||
|
_flicker = float(l.get("flicker", 0.2))
|
||||||
|
_h = float(l.get("height", 0.5))
|
||||||
|
_noise.seed = int(get_instance_id() % 1000)
|
||||||
|
_noise.frequency = 2.0
|
||||||
|
_t = randf() * 100.0
|
||||||
|
_light = OmniLight3D.new()
|
||||||
|
var c: Array = l.get("color", [1.0, 0.7, 0.4])
|
||||||
|
_light.light_color = Color(c[0], c[1], c[2])
|
||||||
|
_light.light_energy = _base
|
||||||
|
_light.omni_range = float(l.get("range", 6.0))
|
||||||
|
_light.omni_attenuation = 1.3
|
||||||
|
_light.shadow_enabled = bool(l.get("shadows", false))
|
||||||
|
_light.position = Vector3(0, _h + 0.05, 0)
|
||||||
|
add_child(_light)
|
||||||
|
# The flame: a small glowing teardrop at the wick.
|
||||||
|
var m := StandardMaterial3D.new()
|
||||||
|
m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
m.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
|
||||||
|
m.albedo_color = Color(1.0, 0.72, 0.32, 0.85)
|
||||||
|
var s := SphereMesh.new()
|
||||||
|
var fr := float(l.get("flame", 0.03))
|
||||||
|
s.radius = fr
|
||||||
|
s.height = fr * 3.2
|
||||||
|
s.radial_segments = 8
|
||||||
|
s.rings = 4
|
||||||
|
s.material = m
|
||||||
|
_flame = MeshInstance3D.new()
|
||||||
|
_flame.mesh = s
|
||||||
|
_flame.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
_flame.position = Vector3(0, _h, 0)
|
||||||
|
add_child(_flame)
|
||||||
|
|
||||||
|
func display_name() -> String:
|
||||||
|
return String(def.get("name", "Light"))
|
||||||
|
|
||||||
|
func interact_prompt(_player: Node) -> String:
|
||||||
|
return "%s (X to take it up)" % display_name()
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if _flicker <= 0.0 or _light == null:
|
||||||
|
return
|
||||||
|
_t += delta
|
||||||
|
var f := _noise.get_noise_1d(_t * 9.0)
|
||||||
|
_light.light_energy = _base * (1.0 + f * _flicker)
|
||||||
|
_flame.scale = Vector3.ONE * (1.0 + f * _flicker * 0.8)
|
||||||
1
scripts/building/lamp.gd.uid
Normal file
1
scripts/building/lamp.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cwvlv0fbxhlgj
|
||||||
@@ -23,6 +23,8 @@ const RECIPES := {
|
|||||||
{"inputs": {"raw_meat": 2, "wood": 1}, "output": "smoked_meat", "count": 2, "seconds": 60.0},
|
{"inputs": {"raw_meat": 2, "wood": 1}, "output": "smoked_meat", "count": 2, "seconds": 60.0},
|
||||||
{"inputs": {"perch": 2, "wood": 1}, "output": "smoked_fish", "count": 2, "seconds": 60.0},
|
{"inputs": {"perch": 2, "wood": 1}, "output": "smoked_fish", "count": 2, "seconds": 60.0},
|
||||||
{"inputs": {"trout": 1, "wood": 1}, "output": "smoked_fish", "count": 2, "seconds": 60.0},
|
{"inputs": {"trout": 1, "wood": 1}, "output": "smoked_fish", "count": 2, "seconds": 60.0},
|
||||||
|
{"inputs": {"mackerel": 2, "wood": 1}, "output": "smoked_fish", "count": 2, "seconds": 60.0},
|
||||||
|
{"inputs": {"cod": 1, "wood": 1}, "output": "smoked_fish", "count": 2, "seconds": 60.0},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
## What the prompt says it makes, and what it needs when idle.
|
## What the prompt says it makes, and what it needs when idle.
|
||||||
|
|||||||
1
scripts/building/processor.gd.uid
Normal file
1
scripts/building/processor.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://jvcfwahbmdsm
|
||||||
1
scripts/building/rug.gd.uid
Normal file
1
scripts/building/rug.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d21if6sepl888
|
||||||
1
scripts/building/stonecutter.gd.uid
Normal file
1
scripts/building/stonecutter.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dk1ge2xxgiev1
|
||||||
@@ -31,6 +31,10 @@ func _ready() -> void:
|
|||||||
_bind("inventory", [KEY_TAB, KEY_I])
|
_bind("inventory", [KEY_TAB, KEY_I])
|
||||||
_bind("rotate_piece", [KEY_R])
|
_bind("rotate_piece", [KEY_R])
|
||||||
_bind("dismantle", [KEY_X])
|
_bind("dismantle", [KEY_X])
|
||||||
|
_bind("drop", [KEY_Q]) # (shift+Q drops just one)
|
||||||
|
_bind("row_up", [KEY_K]) # the 1-8 keys pick from the bag row above...
|
||||||
|
_bind("row_down", [KEY_M]) # ...or below
|
||||||
|
_bind("map", [KEY_N]) # the map (Gitea issue #1)
|
||||||
for i in range(8):
|
for i in range(8):
|
||||||
_bind("hotbar_%d" % (i + 1), [KEY_1 + i])
|
_bind("hotbar_%d" % (i + 1), [KEY_1 + i])
|
||||||
_bind_mouse("attack", MOUSE_BUTTON_LEFT)
|
_bind_mouse("attack", MOUSE_BUTTON_LEFT)
|
||||||
@@ -254,6 +258,14 @@ func harvest_hit(player: Node3D, node: Node3D, hit_point: Vector3) -> bool:
|
|||||||
return false
|
return false
|
||||||
var held: Dictionary = ItemDB.get_item(player.held_item_id())
|
var held: Dictionary = ItemDB.get_item(player.held_item_id())
|
||||||
var tool: Dictionary = held.get("tool", {})
|
var tool: Dictionary = held.get("tool", {})
|
||||||
|
if node.has_method("refusal"): # (the cliff tunnel's faces: their own rules first)
|
||||||
|
var why: String = node.refusal()
|
||||||
|
if not why.is_empty():
|
||||||
|
notify.emit(why)
|
||||||
|
return false
|
||||||
|
if tool.get("kind", "") == node.tool_kind and int(tool.get("tier", 0)) < node.min_tier:
|
||||||
|
notify.emit("Too hard: you need a better pick")
|
||||||
|
return false
|
||||||
if tool.get("kind", "") != node.tool_kind or int(tool.get("tier", 0)) < node.min_tier:
|
if tool.get("kind", "") != node.tool_kind or int(tool.get("tier", 0)) < node.min_tier:
|
||||||
notify.emit("Needs a %s (Tab to craft)" % ItemDB.item_name("stone_" + node.tool_kind))
|
notify.emit("Needs a %s (Tab to craft)" % ItemDB.item_name("stone_" + node.tool_kind))
|
||||||
return false
|
return false
|
||||||
@@ -736,8 +748,53 @@ static func harvest_sound(tool_kind: String, felled: bool) -> String:
|
|||||||
|
|
||||||
## A piece going down: wood, stone, or something soft.
|
## A piece going down: wood, stone, or something soft.
|
||||||
static func place_sound(id: String) -> String:
|
static func place_sound(id: String) -> String:
|
||||||
if id.contains("stone") or id in ["campfire", "forge", "hearth", "stonecutter"]:
|
if id.contains("stone") or id.contains("jar") or id in ["campfire", "forge", "hearth", "stonecutter"]:
|
||||||
return "place_stone"
|
return "place_stone"
|
||||||
if id.contains("rug") or id.contains("bed") or id == "smoking_rack":
|
if id.contains("rug") or id.contains("bed") or id == "smoking_rack":
|
||||||
return "place_soft"
|
return "place_soft"
|
||||||
return "place_wood"
|
return "place_wood"
|
||||||
|
|
||||||
|
# --- charms and bait ------------------------------------------------------------------
|
||||||
|
|
||||||
|
## A charm carried anywhere in the bag (the fang necklace: {"against": "wolf",
|
||||||
|
## "damage": 0.7}) takes the edge off hits from what it's against. `by` is the
|
||||||
|
## hit's "a wolf" / "a alpha wolf" / "a spirit wolf" (survival.damage).
|
||||||
|
static func charm_mult(inv: Inventory, by: String) -> float:
|
||||||
|
var m := 1.0
|
||||||
|
if by.is_empty():
|
||||||
|
return m
|
||||||
|
for s in inv.slots:
|
||||||
|
if s == null:
|
||||||
|
continue
|
||||||
|
var c: Dictionary = ItemDB.get_item(String(s.id)).get("charm", {})
|
||||||
|
if not c.is_empty() and by.to_lower().contains(String(c.get("against", "?"))):
|
||||||
|
m = minf(m, float(c.get("damage", 1.0)))
|
||||||
|
return m
|
||||||
|
|
||||||
|
const BAIT := "rotten_food"
|
||||||
|
const BAIT_WAIT := 0.6 ## carried while fishing: bites come this much sooner
|
||||||
|
|
||||||
|
## Drops the stack in `slot` (or `count` of it) at your feet, a little in
|
||||||
|
## front (Q drops what's in your hand). Online the server does it, for everyone.
|
||||||
|
func drop(player: Node3D, slot: int, count: int = 0) -> bool:
|
||||||
|
if _online():
|
||||||
|
return _ask("drop", [slot, count])
|
||||||
|
var s = player.inventory.get_slot(slot)
|
||||||
|
if s == null:
|
||||||
|
return false
|
||||||
|
var n: int = int(s.count) if count <= 0 else mini(count, int(s.count))
|
||||||
|
# (on a server the player is a proxy under PlayerNet, not a child of the world)
|
||||||
|
var world: Node = WorldNet.world if Net.is_server() else player.get_parent()
|
||||||
|
if world == null or not world.has_method("drop_item"):
|
||||||
|
return false
|
||||||
|
var fwd: Vector3 = -player.global_basis.z
|
||||||
|
fwd.y = 0.0
|
||||||
|
var at: Vector3 = player.global_position + fwd.normalized() * 0.9
|
||||||
|
at.y = world.terrain.sample_height(at.x, at.z) + 0.05 if world.get("terrain") != null else player.global_position.y
|
||||||
|
var extra := Inventory.extra_of(s)
|
||||||
|
if world.drop_item(String(s.id), n, Transform3D(Basis(), at), extra) == null:
|
||||||
|
return false
|
||||||
|
player.inventory.take_from(slot, n)
|
||||||
|
Sfx.at("pickup", at)
|
||||||
|
notify.emit("Dropped %d %s" % [n, ItemDB.item_name(String(s.id))])
|
||||||
|
return true
|
||||||
|
|||||||
1
scripts/core/boot.gd.uid
Normal file
1
scripts/core/boot.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://frm15unnr4w5
|
||||||
1
scripts/core/guide.gd.uid
Normal file
1
scripts/core/guide.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://nva2x7o0ggw1
|
||||||
@@ -135,4 +135,9 @@ func _met(when: String) -> bool:
|
|||||||
return not dead and world.day_night.is_night() and world.day_night.wolf_moon_now()
|
return not dead and world.day_night.is_night() and world.day_night.wolf_moon_now()
|
||||||
"hard_winter":
|
"hard_winter":
|
||||||
return not dead and world.weather != null and world.weather.in_hard_winter()
|
return not dead and world.weather != null and world.weather.in_hard_winter()
|
||||||
|
"worldlet": # (where you are on the journey: "worldlet:cove")
|
||||||
|
return not dead and world.has_method("worldlet_id") and String(world.worldlet_id()) in vals
|
||||||
|
"seam": # (an antler pickaxe in your bag, and the cliff tunnel's seam still to dig)
|
||||||
|
return not dead and world.get("tunnel") != null and not world.tunnel.opened \
|
||||||
|
and player.inventory.count("antler_pickaxe") > 0
|
||||||
return false
|
return false
|
||||||
|
|||||||
1
scripts/core/hints.gd.uid
Normal file
1
scripts/core/hints.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cd4pyfn6igpx0
|
||||||
@@ -65,6 +65,10 @@ static func extra_of(stack: Dictionary) -> Dictionary:
|
|||||||
static var clock := 0.0
|
static var clock := 0.0
|
||||||
static var day_seconds := 1200.0
|
static var day_seconds := 1200.0
|
||||||
static var rot_speed := 1.0
|
static var rot_speed := 1.0
|
||||||
|
## How many times longer food keeps in here (a clay jar: 3), and the clock at
|
||||||
|
## the last spoil() (the time since is what's slowed).
|
||||||
|
var keep_mult := 1.0
|
||||||
|
var _last_clock := -1.0
|
||||||
const ROTTEN := "rotten_food"
|
const ROTTEN := "rotten_food"
|
||||||
|
|
||||||
static func _db() -> Node:
|
static func _db() -> Node:
|
||||||
@@ -110,6 +114,15 @@ func spoil() -> int:
|
|||||||
return 0
|
return 0
|
||||||
var rotted := 0
|
var rotted := 0
|
||||||
var touched := false
|
var touched := false
|
||||||
|
# A clay jar (keep_mult 3): food in it ages at a third of the rate, so its
|
||||||
|
# "rot" stamp moves back by two thirds of the time that's passed.
|
||||||
|
if keep_mult > 1.0:
|
||||||
|
if _last_clock >= 0.0 and clock > _last_clock:
|
||||||
|
var later := (clock - _last_clock) * (1.0 - 1.0 / keep_mult)
|
||||||
|
for s in slots:
|
||||||
|
if s != null and s.has("rot") and float(s.rot) > _last_clock:
|
||||||
|
s.rot = float(s.rot) + later
|
||||||
|
_last_clock = clock
|
||||||
for i in range(slots.size()):
|
for i in range(slots.size()):
|
||||||
var s = slots[i]
|
var s = slots[i]
|
||||||
if s == null or keeps_days(String(s.id)) <= 0.0:
|
if s == null or keeps_days(String(s.id)) <= 0.0:
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ func define_water(water: String, d: Dictionary) -> void:
|
|||||||
if not items.has(f.get("id", "")):
|
if not items.has(f.get("id", "")):
|
||||||
push_error("ItemDB: fish '%s' in water '%s' is not an item" % [f.get("id", "?"), water])
|
push_error("ItemDB: fish '%s' in water '%s' is not an item" % [f.get("id", "?"), water])
|
||||||
continue
|
continue
|
||||||
fish.append({"id": f.id, "weight": float(f.get("weight", 1)), "dawn_dusk": float(f.get("dawn_dusk", f.get("weight", 1)))})
|
fish.append({"id": f.id, "weight": float(f.get("weight", 1)), "dawn_dusk": float(f.get("dawn_dusk", f.get("weight", 1))),
|
||||||
|
"min_depth": float(f.get("min_depth", 0.0))}) # (a fish of deeper water: cod)
|
||||||
var w: Array = d.get("bite_wait", [5.0, 14.0])
|
var w: Array = d.get("bite_wait", [5.0, 14.0])
|
||||||
fishing.waters[water] = {"min_depth": float(d.get("min_depth", 0.5)), "bite_wait": [float(w[0]), float(w[1])], "fish": fish}
|
fishing.waters[water] = {"min_depth": float(d.get("min_depth", 0.5)), "bite_wait": [float(w[0]), float(w[1])], "fish": fish}
|
||||||
|
|
||||||
|
|||||||
1
scripts/core/music.gd.uid
Normal file
1
scripts/core/music.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://jojnurcejyff
|
||||||
1
scripts/core/narrator.gd.uid
Normal file
1
scripts/core/narrator.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ctvohlyg6yr5m
|
||||||
@@ -56,16 +56,17 @@ func create_world(world_name: String, seed: int, options: Dictionary = {}) -> St
|
|||||||
var now := Time.get_unix_time_from_system()
|
var now := Time.get_unix_time_from_system()
|
||||||
var meta := {
|
var meta := {
|
||||||
"name": world_name, "seed": seed, "created": now, "last_played": now,
|
"name": world_name, "seed": seed, "created": now, "last_played": now,
|
||||||
"play_time": 0.0, "version": VERSION,
|
"play_time": 0.0, "version": VERSION, "worldlet": "valley",
|
||||||
}
|
}
|
||||||
for k in options:
|
for k in options:
|
||||||
meta[k] = options[k]
|
meta[k] = options[k]
|
||||||
|
meta["home"] = String(meta.worldlet)
|
||||||
_write_json(_dir(id).path_join("meta.json"), meta)
|
_write_json(_dir(id).path_join("meta.json"), meta)
|
||||||
return id
|
return id
|
||||||
|
|
||||||
## The world slot called exactly `id` (a server's world), made with `seed` if
|
## The world slot called exactly `id` (a server's world), made with `seed` if
|
||||||
## it doesn't exist yet. An existing world keeps its own seed.
|
## it doesn't exist yet. An existing world keeps its own seed.
|
||||||
func open_or_create(id: String, seed: int) -> String:
|
func open_or_create(id: String, seed: int, worldlet: String = "valley") -> String:
|
||||||
if not read_meta(id).is_empty():
|
if not read_meta(id).is_empty():
|
||||||
var had := int(read_meta(id).get("seed", seed))
|
var had := int(read_meta(id).get("seed", seed))
|
||||||
if had != seed:
|
if had != seed:
|
||||||
@@ -75,7 +76,7 @@ func open_or_create(id: String, seed: int) -> String:
|
|||||||
var now := Time.get_unix_time_from_system()
|
var now := Time.get_unix_time_from_system()
|
||||||
_write_json(_dir(id).path_join("meta.json"), {
|
_write_json(_dir(id).path_join("meta.json"), {
|
||||||
"name": id, "seed": seed, "created": now, "last_played": now,
|
"name": id, "seed": seed, "created": now, "last_played": now,
|
||||||
"play_time": 0.0, "version": VERSION,
|
"play_time": 0.0, "version": VERSION, "worldlet": worldlet,
|
||||||
})
|
})
|
||||||
return id
|
return id
|
||||||
|
|
||||||
@@ -84,6 +85,8 @@ func delete_world(id: String) -> void:
|
|||||||
var d := DirAccess.open(dir)
|
var d := DirAccess.open(dir)
|
||||||
if d == null:
|
if d == null:
|
||||||
return
|
return
|
||||||
|
for sub in d.get_directories(): # (worldlets/, and a server's backups/)
|
||||||
|
_remove_tree(dir.path_join(sub))
|
||||||
for f in d.get_files():
|
for f in d.get_files():
|
||||||
d.remove(f)
|
d.remove(f)
|
||||||
DirAccess.remove_absolute(dir)
|
DirAccess.remove_absolute(dir)
|
||||||
@@ -93,16 +96,64 @@ func delete_world(id: String) -> void:
|
|||||||
func read_meta(id: String) -> Dictionary:
|
func read_meta(id: String) -> Dictionary:
|
||||||
return _read_json(_dir(id).path_join("meta.json"))
|
return _read_json(_dir(id).path_join("meta.json"))
|
||||||
|
|
||||||
func load_state(id: String) -> Dictionary:
|
# --- worldlets in one world (J1, docs/WORLDLETS_DESIGN.md §10) --------------------
|
||||||
var s := _read_json(_dir(id).path_join("world.json"))
|
# A world can span several worldlets (the journey). Each keeps its own state
|
||||||
if s.is_empty() and FileAccess.file_exists(_dir(id).path_join("world.json.bak")):
|
# file: the one it began in (its "home") is world.json, as before, so older
|
||||||
push_warning("Saves: world.json unreadable, using backup")
|
# saves are unchanged; the others are worldlets/<id>.json. meta.json's
|
||||||
s = _read_json(_dir(id).path_join("world.json.bak"))
|
# "worldlet" is where you are now, so that's the one loaded.
|
||||||
|
|
||||||
|
## The worldlet world `id` is being played in now.
|
||||||
|
func worldlet_of(id: String) -> String:
|
||||||
|
return String(read_meta(id).get("worldlet", "valley"))
|
||||||
|
|
||||||
|
## The worldlet world `id` began in (its world.json).
|
||||||
|
func home_of(id: String) -> String:
|
||||||
|
var m := read_meta(id)
|
||||||
|
return String(m.get("home", m.get("worldlet", "valley")))
|
||||||
|
|
||||||
|
## The state file for worldlet `w` of world `id`.
|
||||||
|
func state_path(id: String, w: String) -> String:
|
||||||
|
if w == home_of(id):
|
||||||
|
return _dir(id).path_join("world.json")
|
||||||
|
return _dir(id).path_join("worldlets/%s.json" % w)
|
||||||
|
|
||||||
|
## Whether world `id` has been to worldlet `w` (has a state for it).
|
||||||
|
func has_state(id: String, w: String) -> bool:
|
||||||
|
return FileAccess.file_exists(state_path(id, w))
|
||||||
|
|
||||||
|
## Moves world `id` to worldlet `to` (the next load is there), arriving at
|
||||||
|
## `arrive` (a place the arriving world puts you: "sea_cave", "tunnel_bottom").
|
||||||
|
func set_worldlet(id: String, to: String, arrive: String) -> void:
|
||||||
|
var m := read_meta(id)
|
||||||
|
if not m.has("home"):
|
||||||
|
m["home"] = String(m.get("worldlet", "valley")) # (pin it before "worldlet" moves)
|
||||||
|
m["worldlet"] = to
|
||||||
|
m["arrive"] = arrive
|
||||||
|
_write_json(_dir(id).path_join("meta.json"), m)
|
||||||
|
|
||||||
|
## Where the next load should put you, once (then forgotten).
|
||||||
|
func take_arrival(id: String) -> String:
|
||||||
|
var m := read_meta(id)
|
||||||
|
var a := String(m.get("arrive", ""))
|
||||||
|
if not a.is_empty():
|
||||||
|
m.erase("arrive")
|
||||||
|
_write_json(_dir(id).path_join("meta.json"), m)
|
||||||
|
return a
|
||||||
|
|
||||||
|
## The state of the worldlet being played (or of worldlet `w`).
|
||||||
|
func load_state(id: String, w: String = "") -> Dictionary:
|
||||||
|
var path := state_path(id, worldlet_of(id) if w.is_empty() else w)
|
||||||
|
var s := _read_json(path)
|
||||||
|
if s.is_empty() and FileAccess.file_exists(path + ".bak"):
|
||||||
|
push_warning("Saves: %s unreadable, using backup" % path.get_file())
|
||||||
|
s = _read_json(path + ".bak")
|
||||||
return s
|
return s
|
||||||
|
|
||||||
## Writes the state for `id` and updates its meta. Returns true on success.
|
## Writes the state for `id` (the worldlet being played, or `w`) and
|
||||||
func save_state(id: String, state: Dictionary, session_seconds: float) -> bool:
|
## updates its meta. Returns true on success.
|
||||||
var path := _dir(id).path_join("world.json")
|
func save_state(id: String, state: Dictionary, session_seconds: float, w: String = "") -> bool:
|
||||||
|
var path := state_path(id, worldlet_of(id) if w.is_empty() else w)
|
||||||
|
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
|
||||||
if FileAccess.file_exists(path):
|
if FileAccess.file_exists(path):
|
||||||
DirAccess.copy_absolute(path, path + ".bak")
|
DirAccess.copy_absolute(path, path + ".bak")
|
||||||
state["version"] = VERSION
|
state["version"] = VERSION
|
||||||
@@ -129,3 +180,10 @@ func _read_json(path: String) -> Dictionary:
|
|||||||
return {}
|
return {}
|
||||||
var data = JSON.parse_string(FileAccess.get_file_as_string(path))
|
var data = JSON.parse_string(FileAccess.get_file_as_string(path))
|
||||||
return data if typeof(data) == TYPE_DICTIONARY else {}
|
return data if typeof(data) == TYPE_DICTIONARY else {}
|
||||||
|
|
||||||
|
static func _remove_tree(d: String) -> void:
|
||||||
|
for sub in DirAccess.get_directories_at(d):
|
||||||
|
_remove_tree(d.path_join(sub))
|
||||||
|
for f in DirAccess.get_files_at(d):
|
||||||
|
DirAccess.remove_absolute(d.path_join(f))
|
||||||
|
DirAccess.remove_absolute(d)
|
||||||
|
|||||||
1
scripts/core/sfx.gd.uid
Normal file
1
scripts/core/sfx.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://br583euuk842q
|
||||||
1
scripts/core/updates.gd.uid
Normal file
1
scripts/core/updates.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://3lflq11qfg00
|
||||||
1
scripts/creatures/bear.gd.uid
Normal file
1
scripts/creatures/bear.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://co3yk3efstys2
|
||||||
1
scripts/creatures/bear_range.gd.uid
Normal file
1
scripts/creatures/bear_range.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ce34amx3oagyx
|
||||||
1
scripts/creatures/creature.gd.uid
Normal file
1
scripts/creatures/creature.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cdxggdwnq1ctn
|
||||||
1
scripts/creatures/creature_meshes.gd.uid
Normal file
1
scripts/creatures/creature_meshes.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dy6utmjegblry
|
||||||
1
scripts/creatures/crow.gd.uid
Normal file
1
scripts/creatures/crow.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bs230fcuvorft
|
||||||
1
scripts/creatures/den.gd.uid
Normal file
1
scripts/creatures/den.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://beng43qyj6rke
|
||||||
1
scripts/creatures/fox.gd.uid
Normal file
1
scripts/creatures/fox.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cfailgwvqv2fc
|
||||||
1
scripts/creatures/guard.gd.uid
Normal file
1
scripts/creatures/guard.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://x0xva7tw6ind
|
||||||
1
scripts/creatures/old_stag.gd.uid
Normal file
1
scripts/creatures/old_stag.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://f88v5i7s5e5c
|
||||||
1
scripts/creatures/projectile.gd.uid
Normal file
1
scripts/creatures/projectile.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ccpmq0ddkoc8g
|
||||||
1
scripts/creatures/spawner.gd.uid
Normal file
1
scripts/creatures/spawner.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dukt8gme03lsb
|
||||||
1
scripts/creatures/thieves.gd.uid
Normal file
1
scripts/creatures/thieves.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cpwa4sffdi3nt
|
||||||
1
scripts/creatures/tutorial_wolf.gd.uid
Normal file
1
scripts/creatures/tutorial_wolf.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b8wfmfnhg2edq
|
||||||
1
scripts/creatures/wolf.gd.uid
Normal file
1
scripts/creatures/wolf.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://024u453fdbml
|
||||||
1
scripts/creatures/wolf_pack.gd.uid
Normal file
1
scripts/creatures/wolf_pack.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cbb3g7cjprhy8
|
||||||
@@ -15,6 +15,7 @@ const BITE_WINDOW := 1.5
|
|||||||
const LINE_LENGTH := 26.0
|
const LINE_LENGTH := 26.0
|
||||||
|
|
||||||
var water_type := "" ## "lake", "sea"... (world.water_at)
|
var water_type := "" ## "lake", "sea"... (world.water_at)
|
||||||
|
var water_depth := 0.0 ## how deep it is where the float came down
|
||||||
var table: Dictionary = {} ## that water's fishing table (ItemDB.water_fishing)
|
var table: Dictionary = {} ## that water's fishing table (ItemDB.water_fishing)
|
||||||
|
|
||||||
var world: Node3D
|
var world: Node3D
|
||||||
@@ -74,6 +75,8 @@ func _dawn_dusk() -> bool:
|
|||||||
return (h > 5.0 and h < 8.5) or (h > 17.0 and h < 20.5)
|
return (h > 5.0 and h < 8.5) or (h > 17.0 and h < 20.5)
|
||||||
|
|
||||||
## How long until the next bite (seconds), from data/fish.json.
|
## How long until the next bite (seconds), from data/fish.json.
|
||||||
|
var baited := false ## this wait was shortened by bait (rotten food)
|
||||||
|
|
||||||
func _next_wait() -> float:
|
func _next_wait() -> float:
|
||||||
var mods: Dictionary = ItemDB.fishing.modifiers
|
var mods: Dictionary = ItemDB.fishing.modifiers
|
||||||
var w: Array = table.get("bite_wait", [5.0, 14.0])
|
var w: Array = table.get("bite_wait", [5.0, 14.0])
|
||||||
@@ -82,11 +85,16 @@ func _next_wait() -> float:
|
|||||||
k *= float(mods.get("dawn_dusk_wait", 0.6)) # fish feed at dawn and dusk
|
k *= float(mods.get("dawn_dusk_wait", 0.6)) # fish feed at dawn and dusk
|
||||||
if world.weather.precip > 0.3:
|
if world.weather.precip > 0.3:
|
||||||
k *= float(mods.get("rain_wait", 0.8))
|
k *= float(mods.get("rain_wait", 0.8))
|
||||||
|
# Rotten food in your bag is bait: the fish come sooner, and a bite takes one.
|
||||||
|
baited = player != null and player.get("inventory") != null and player.inventory.count(Actions.BAIT) > 0
|
||||||
|
if baited:
|
||||||
|
k *= Actions.BAIT_WAIT
|
||||||
return rng.randf_range(w[0], w[1]) * k
|
return rng.randf_range(w[0], w[1]) * k
|
||||||
|
|
||||||
## A weighted pick from this water's fish (dawn/dusk weights when they apply).
|
## A weighted pick from this water's fish (dawn/dusk weights when they apply).
|
||||||
|
## (A fish with its own "min_depth" only bites in water that deep: cod.)
|
||||||
func _pick_fish() -> String:
|
func _pick_fish() -> String:
|
||||||
var fish: Array = table.get("fish", [])
|
var fish: Array = table.get("fish", []).filter(func(f): return water_depth >= float(f.get("min_depth", 0.0)))
|
||||||
var key := "dawn_dusk" if _dawn_dusk() else "weight"
|
var key := "dawn_dusk" if _dawn_dusk() else "weight"
|
||||||
var total := 0.0
|
var total := 0.0
|
||||||
for f in fish:
|
for f in fish:
|
||||||
@@ -114,6 +122,7 @@ func step(dt: float) -> void:
|
|||||||
if not water.is_empty() and p.y <= float(water.surface):
|
if not water.is_empty() and p.y <= float(water.surface):
|
||||||
p.y = water.surface
|
p.y = water.surface
|
||||||
water_type = water.type
|
water_type = water.type
|
||||||
|
water_depth = float(water.depth)
|
||||||
table = ItemDB.water_fishing(water_type)
|
table = ItemDB.water_fishing(water_type)
|
||||||
if table.is_empty() or table.fish.is_empty():
|
if table.is_empty() or table.fish.is_empty():
|
||||||
state = "landed"
|
state = "landed"
|
||||||
@@ -139,6 +148,8 @@ func step(dt: float) -> void:
|
|||||||
bite_left = BITE_WINDOW
|
bite_left = BITE_WINDOW
|
||||||
Actions.tell(player, "Bite! Click to reel in")
|
Actions.tell(player, "Bite! Click to reel in")
|
||||||
Sfx.at("bite", global_position)
|
Sfx.at("bite", global_position)
|
||||||
|
if baited and player.inventory.remove(Actions.BAIT, 1):
|
||||||
|
baited = false
|
||||||
"biting":
|
"biting":
|
||||||
bite_left -= dt
|
bite_left -= dt
|
||||||
# The float bobs hard and goes under.
|
# The float bobs hard and goes under.
|
||||||
|
|||||||
1
scripts/fishing/bobber.gd.uid
Normal file
1
scripts/fishing/bobber.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://br7isyu6hxnsd
|
||||||
1
scripts/fishing/lake_fish.gd.uid
Normal file
1
scripts/fishing/lake_fish.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cs6ucd2nldqmj
|
||||||
@@ -75,6 +75,10 @@ static func build(key: String) -> ArrayMesh:
|
|||||||
"trout": m = _fish(TROUT, 0.24, false)
|
"trout": m = _fish(TROUT, 0.24, false)
|
||||||
"grilled_perch": m = _fish(PERCH, 0.16, true)
|
"grilled_perch": m = _fish(PERCH, 0.16, true)
|
||||||
"grilled_trout": m = _fish(TROUT, 0.24, true)
|
"grilled_trout": m = _fish(TROUT, 0.24, true)
|
||||||
|
"mackerel": m = _fish(MACKEREL, 0.2, false)
|
||||||
|
"cod": m = _fish(COD, 0.3, false)
|
||||||
|
"grilled_mackerel": m = _fish(MACKEREL, 0.2, true)
|
||||||
|
"grilled_cod": m = _fish(COD, 0.3, true)
|
||||||
"fiber_plant": m = _fiber_plant()
|
"fiber_plant": m = _fiber_plant()
|
||||||
"iron_nugget": m = _pebble(61, RUST, Vector3(0.07, 0.05, 0.06), 0.6)
|
"iron_nugget": m = _pebble(61, RUST, Vector3(0.07, 0.05, 0.06), 0.6)
|
||||||
"iron_ore": m = _ore()
|
"iron_ore": m = _ore()
|
||||||
@@ -115,6 +119,14 @@ static func build(key: String) -> ArrayMesh:
|
|||||||
"bear_hide": m = _pelt(BEAR_FUR)
|
"bear_hide": m = _pelt(BEAR_FUR)
|
||||||
"bear_fat": m = _pebble(271, Color(0.92, 0.86, 0.68), Vector3(0.1, 0.06, 0.08), 0.1)
|
"bear_fat": m = _pebble(271, Color(0.92, 0.86, 0.68), Vector3(0.1, 0.06, 0.08), 0.1)
|
||||||
"fur_rug": m = _rug()
|
"fur_rug": m = _rug()
|
||||||
|
"pelt_rug": m = _rug(WOLF_FUR, 0.85)
|
||||||
|
"bear_cloak": m = _cloak(BEAR_FUR)
|
||||||
|
"clay_jar": m = _jar()
|
||||||
|
"tallow_candle": m = _candle()
|
||||||
|
"torch_stand": m = _torch_stand()
|
||||||
|
"bedroll": m = _bedroll()
|
||||||
|
"fang_necklace": m = _necklace()
|
||||||
|
"pemmican": m = _pebble(331, Color(0.42, 0.24, 0.16), Vector3(0.1, 0.05, 0.07), 0.15)
|
||||||
"barrow_key": m = _barrow_key()
|
"barrow_key": m = _barrow_key()
|
||||||
"rotten_food": m = _pebble(321, Color(0.33, 0.3, 0.16), Vector3(0.11, 0.06, 0.1), 0.45)
|
"rotten_food": m = _pebble(321, Color(0.33, 0.3, 0.16), Vector3(0.11, 0.06, 0.1), 0.45)
|
||||||
"salt": m = _salt()
|
"salt": m = _salt()
|
||||||
@@ -686,6 +698,8 @@ static func _cloak(col: Color = HIDE) -> ArrayMesh:
|
|||||||
|
|
||||||
const PERCH := Color(0.45, 0.52, 0.30)
|
const PERCH := Color(0.45, 0.52, 0.30)
|
||||||
const TROUT := Color(0.50, 0.55, 0.58)
|
const TROUT := Color(0.50, 0.55, 0.58)
|
||||||
|
const MACKEREL := Color(0.22, 0.42, 0.48)
|
||||||
|
const COD := Color(0.56, 0.52, 0.40)
|
||||||
const GRILLED := Color(0.55, 0.36, 0.18)
|
const GRILLED := Color(0.55, 0.36, 0.18)
|
||||||
const FLOAT_RED := Color(0.85, 0.15, 0.12)
|
const FLOAT_RED := Color(0.85, 0.15, 0.12)
|
||||||
|
|
||||||
@@ -999,19 +1013,81 @@ static func _fang() -> ArrayMesh:
|
|||||||
const BEAR_FUR := Color(0.36, 0.24, 0.15)
|
const BEAR_FUR := Color(0.36, 0.24, 0.15)
|
||||||
|
|
||||||
## A bearskin laid flat: body, four splayed legs and the head toward -Z.
|
## A bearskin laid flat: body, four splayed legs and the head toward -Z.
|
||||||
static func _rug() -> ArrayMesh:
|
## A skin rug with its head at -Z: the bear's (fur_rug), or wolves' a bit smaller.
|
||||||
|
static func _rug(fur: Color = BEAR_FUR, k: float = 1.0) -> ArrayMesh:
|
||||||
var st := P._begin()
|
var st := P._begin()
|
||||||
var r := _rng(273)
|
var r := _rng(273)
|
||||||
_box(st, Vector3(0, 0.015, 0.1), Vector3(1.0, 0.03, 1.5), BEAR_FUR)
|
_box(st, Vector3(0, 0.015, 0.1) * k, Vector3(1.0, 0.03 / k, 1.5) * k, fur)
|
||||||
_box(st, Vector3(0, 0.02, 0.1), Vector3(0.8, 0.03, 1.3), P._mul(BEAR_FUR, 1.12))
|
_box(st, Vector3(0, 0.02, 0.1) * k, Vector3(0.8, 0.03 / k, 1.3) * k, P._mul(fur, 1.12))
|
||||||
for sx in [-1.0, 1.0]:
|
for sx in [-1.0, 1.0]:
|
||||||
for sz in [-0.45, 0.6]:
|
for sz in [-0.45, 0.6]:
|
||||||
_box(st, Vector3(sx * 0.62, 0.012, sz), Vector3(0.42, 0.024, 0.24), _board(r, BEAR_FUR), Basis(Vector3.UP, sx * (0.5 if sz < 0 else -0.5)))
|
_box(st, Vector3(sx * 0.62, 0.012, sz) * k, Vector3(0.42, 0.024 / k, 0.24) * k, _board(r, fur), Basis(Vector3.UP, sx * (0.5 if sz < 0 else -0.5)))
|
||||||
_box(st, Vector3(0, 0.05, -0.85), Vector3(0.38, 0.1, 0.34), P._mul(BEAR_FUR, 0.95))
|
_box(st, Vector3(0, 0.05, -0.85) * k, Vector3(0.38, 0.1, 0.34) * k, P._mul(fur, 0.95))
|
||||||
_box(st, Vector3(0, 0.045, -1.06), Vector3(0.18, 0.08, 0.14), Color(0.58, 0.45, 0.32))
|
_box(st, Vector3(0, 0.045, -1.06) * k, Vector3(0.18, 0.08, 0.14) * k, Color(0.58, 0.45, 0.32))
|
||||||
for sx in [-0.14, 0.14]:
|
for sx in [-0.14, 0.14]:
|
||||||
_box(st, Vector3(sx, 0.11, -0.8), Vector3(0.08, 0.05, 0.05), P._mul(BEAR_FUR, 0.8))
|
_box(st, Vector3(sx, 0.11, -0.8) * k, Vector3(0.08, 0.05, 0.05) * k, P._mul(fur, 0.8))
|
||||||
_box(st, Vector3(0, 0.02, 0.92), Vector3(0.12, 0.03, 0.12), BEAR_FUR)
|
_box(st, Vector3(0, 0.02, 0.92) * k, Vector3(0.12, 0.03, 0.12) * k, fur)
|
||||||
|
return P._commit(st)
|
||||||
|
|
||||||
|
## A fired clay jar with a wooden lid, about knee high.
|
||||||
|
static func _jar() -> ArrayMesh:
|
||||||
|
var st := P._begin()
|
||||||
|
var r := _rng(337)
|
||||||
|
P._cylinder(st, Vector3(0, 0.0, 0), Vector3(0, 0.12, 0), 0.16, 0.26, 10, P._mul(CLAY_FIRED, 0.92), r, -1.0, CLAY_FIRED)
|
||||||
|
P._cylinder(st, Vector3(0, 0.12, 0), Vector3(0, 0.42, 0), 0.26, 0.24, 10, CLAY_FIRED, r)
|
||||||
|
P._cylinder(st, Vector3(0, 0.42, 0), Vector3(0, 0.52, 0), 0.24, 0.16, 10, P._mul(CLAY_FIRED, 1.06), r)
|
||||||
|
P._cylinder(st, Vector3(0, 0.52, 0), Vector3(0, 0.56, 0), 0.17, 0.17, 10, P._mul(CLAY_FIRED, 0.85), r)
|
||||||
|
P._cylinder(st, Vector3(0, 0.56, 0), Vector3(0, 0.6, 0), 0.19, 0.17, 10, PLANK_DARK, r, -1.0, PLANK) # lid
|
||||||
|
_box(st, Vector3(0, 0.63, 0), Vector3(0.08, 0.04, 0.05), PLANK_DARK)
|
||||||
|
return P._commit(st)
|
||||||
|
|
||||||
|
## A squat tallow candle on a clay dish; the wick's top is at y = 0.2
|
||||||
|
## (lamp.gd puts the flame there).
|
||||||
|
static func _candle() -> ArrayMesh:
|
||||||
|
var st := P._begin()
|
||||||
|
var r := _rng(359)
|
||||||
|
P._cylinder(st, Vector3(0, 0.0, 0), Vector3(0, 0.02, 0), 0.08, 0.09, 8, CLAY_FIRED, r, -1.0, P._mul(CLAY_FIRED, 1.1))
|
||||||
|
P._cylinder(st, Vector3(0, 0.02, 0), Vector3(0, 0.16, 0), 0.04, 0.036, 8, Color(0.93, 0.89, 0.74), r, -1.0, Color(0.96, 0.93, 0.8))
|
||||||
|
P._cylinder(st, Vector3(0, 0.16, 0), Vector3(0, 0.19, 0), 0.004, 0.003, 4, ASH, r)
|
||||||
|
return P._commit(st)
|
||||||
|
|
||||||
|
## A torch in a wooden stand: three splayed feet, a post, the head at 1.4 m
|
||||||
|
## (lamp.gd's flame sits just above it).
|
||||||
|
static func _torch_stand() -> ArrayMesh:
|
||||||
|
var st := P._begin()
|
||||||
|
var r := _rng(367)
|
||||||
|
for i in range(3):
|
||||||
|
var a := TAU * i / 3.0
|
||||||
|
P._cylinder(st, Vector3(cos(a) * 0.28, 0.0, sin(a) * 0.28), Vector3(0, 0.45, 0), 0.03, 0.035, 5, _board(r, PLANK_DARK), r)
|
||||||
|
P._cylinder(st, Vector3(0, 0.0, 0), Vector3(0, 1.3, 0), 0.04, 0.035, 6, _board(r), r)
|
||||||
|
_box(st, Vector3(0, 0.5, 0), Vector3(0.12, 0.05, 0.12), BINDING)
|
||||||
|
P._cylinder(st, Vector3(0, 1.28, 0), Vector3(0, 1.42, 0), 0.05, 0.06, 6, CLOTH, r, -1.0, ASH)
|
||||||
|
return P._commit(st)
|
||||||
|
|
||||||
|
## A hide bedroll: a flat mat with its rolled-up end and a feather pillow.
|
||||||
|
static func _bedroll() -> ArrayMesh:
|
||||||
|
var st := P._begin()
|
||||||
|
var r := _rng(347)
|
||||||
|
_box(st, Vector3(0, 0.03, 0.15), Vector3(0.8, 0.06, 1.6), HIDE)
|
||||||
|
_box(st, Vector3(0, 0.065, 0.2), Vector3(0.66, 0.02, 1.35), P._mul(HIDE, 1.12))
|
||||||
|
P._cylinder(st, Vector3(-0.4, 0.13, 0.95), Vector3(0.4, 0.13, 0.95), 0.12, 0.12, 8, _board(r, HIDE), r, -1.0, P._mul(HIDE, 0.8))
|
||||||
|
_box(st, Vector3(0, 0.1, -0.5), Vector3(0.5, 0.09, 0.28), FEATHER)
|
||||||
|
for sx in [-0.25, 0.25]:
|
||||||
|
_box(st, Vector3(sx, 0.18, 0.95), Vector3(0.04, 0.26, 0.04), BINDING)
|
||||||
|
return P._commit(st)
|
||||||
|
|
||||||
|
## A cord with five fangs hanging from a dark scrap of pelt.
|
||||||
|
static func _necklace() -> ArrayMesh:
|
||||||
|
var st := P._begin()
|
||||||
|
var r := _rng(353)
|
||||||
|
for i in range(12):
|
||||||
|
var a := TAU * i / 12.0
|
||||||
|
var b := TAU * (i + 1) / 12.0
|
||||||
|
P._cylinder(st, Vector3(cos(a) * 0.12, 0.01, sin(a) * 0.1), Vector3(cos(b) * 0.12, 0.01, sin(b) * 0.1), 0.006, 0.006, 4, BINDING, r)
|
||||||
|
_box(st, Vector3(0, 0.015, 0.1), Vector3(0.09, 0.02, 0.05), ALPHA_FUR)
|
||||||
|
for i in range(5):
|
||||||
|
var x := (i - 2) * 0.035
|
||||||
|
P._cylinder(st, Vector3(x, 0.012, 0.12), Vector3(x * 1.2, 0.012, 0.19 - absf(i - 2) * 0.012), 0.012, 0.002, 5, Color(0.88, 0.82, 0.62), r)
|
||||||
return P._commit(st)
|
return P._commit(st)
|
||||||
|
|
||||||
# --- P3 the barrow --------------------------------------------------------------------
|
# --- P3 the barrow --------------------------------------------------------------------
|
||||||
|
|||||||
1
scripts/net/avatar.gd.uid
Normal file
1
scripts/net/avatar.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bccgr18dkfhad
|
||||||
1
scripts/net/character_mesh.gd.uid
Normal file
1
scripts/net/character_mesh.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://w2qc4a4ai4vt
|
||||||
1
scripts/net/chat.gd.uid
Normal file
1
scripts/net/chat.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cj8ew4d4huen5
|
||||||
1
scripts/net/creature_net.gd.uid
Normal file
1
scripts/net/creature_net.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c3amf3mwhetmf
|
||||||
1
scripts/net/identity.gd.uid
Normal file
1
scripts/net/identity.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://x0plvp658wne
|
||||||
@@ -32,7 +32,7 @@ signal clock(day: int, time: float) ## the server's time of day
|
|||||||
const Identity = preload("res://scripts/net/identity.gd")
|
const Identity = preload("res://scripts/net/identity.gd")
|
||||||
const ServerConfig = preload("res://scripts/net/server_config.gd")
|
const ServerConfig = preload("res://scripts/net/server_config.gd")
|
||||||
|
|
||||||
const PROTOCOL := 13 ## 3: shared world; 4: the server decides; 5: wildlife; 6: rules and chat (M2.6); 7: the alpha, broken doors; 8: the bear; 9: crows and foxes; 10: the barrow (P3); 11: food spoilage, lightning (P4); 12: the standing stones and the Old Stag (P5); 13: sounds sent to players nearby (sfx.gd)
|
const PROTOCOL := 16 ## 16: worldlets (the Cove), sea fish (mackerel, cod); 3: shared world; 4: the server decides; 5: wildlife; 6: rules and chat (M2.6); 7: the alpha, broken doors; 8: the bear; 9: crows and foxes; 10: the barrow (P3); 11: food spoilage, lightning (P4); 12: the standing stones and the Old Stag (P5); 13: sounds sent to players nearby (sfx.gd); 14: dropping items, any bag row in hand, new items (jar, bedroll, necklace, bear cloak, rug, pemmican); 15: placed lights (candle, torch stand)
|
||||||
const DEFAULT_PORT := 24242
|
const DEFAULT_PORT := 24242
|
||||||
const HANDSHAKE_TIMEOUT := 10.0
|
const HANDSHAKE_TIMEOUT := 10.0
|
||||||
const JOIN_TIMEOUT := 20.0 ## includes the server building its terrain on first join
|
const JOIN_TIMEOUT := 20.0 ## includes the server building its terrain on first join
|
||||||
@@ -304,14 +304,14 @@ var _host_out := ""
|
|||||||
var _host_port := DEFAULT_PORT
|
var _host_port := DEFAULT_PORT
|
||||||
|
|
||||||
## Settings for a hosted game: name, password, port, a rules preset
|
## Settings for a hosted game: name, password, port, a rules preset
|
||||||
## (co-op / rivals / adversarial) and the seed for a new island.
|
## (co-op / rivals / adversarial) and the seed and worldlet for a new island.
|
||||||
const PRESETS := {
|
const PRESETS := {
|
||||||
"co-op": {"pvp": "false", "raiding": "false", "locks": "owner_and_friends", "death_penalty": "grave"},
|
"co-op": {"pvp": "false", "raiding": "false", "locks": "owner_and_friends", "death_penalty": "grave"},
|
||||||
"rivals": {"pvp": "true", "raiding": "false", "locks": "owner_and_friends", "death_penalty": "grave"},
|
"rivals": {"pvp": "true", "raiding": "false", "locks": "owner_and_friends", "death_penalty": "grave"},
|
||||||
"adversarial": {"pvp": "true", "raiding": "true", "locks": "owner", "death_penalty": "drop_all"},
|
"adversarial": {"pvp": "true", "raiding": "true", "locks": "owner", "death_penalty": "drop_all"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func host(server_name: String, password: String, port: int, preset: String, seed: int) -> void:
|
func host(server_name: String, password: String, port: int, preset: String, seed: int, worldlet: String = "valley") -> void:
|
||||||
if hosting or state != "offline":
|
if hosting or state != "offline":
|
||||||
return
|
return
|
||||||
if _host_pid != 0 and OS.is_process_running(_host_pid):
|
if _host_pid != 0 and OS.is_process_running(_host_pid):
|
||||||
@@ -324,8 +324,9 @@ func host(server_name: String, password: String, port: int, preset: String, seed
|
|||||||
c.data.password = password
|
c.data.password = password
|
||||||
c.data.port = port
|
c.data.port = port
|
||||||
var fresh := not FileAccess.file_exists(host_config.get_base_dir().path_join("worlds/server-world/meta.json"))
|
var fresh := not FileAccess.file_exists(host_config.get_base_dir().path_join("worlds/server-world/meta.json"))
|
||||||
if fresh:
|
if fresh: # (an island keeps its seed and worldlet once it's made)
|
||||||
c.data.seed = seed
|
c.data.seed = seed
|
||||||
|
c.data.worldlet = worldlet
|
||||||
c.save()
|
c.save()
|
||||||
_host_port = port
|
_host_port = port
|
||||||
var args := PackedStringArray(["--headless"])
|
var args := PackedStringArray(["--headless"])
|
||||||
@@ -638,7 +639,7 @@ func _c2s_proof(sig: PackedByteArray) -> void:
|
|||||||
print("[server] %s joined (id %s%s, %d/%d online)" % [name, id, ", admin" if p.admin else "",
|
print("[server] %s joined (id %s%s, %d/%d online)" % [name, id, ", admin" if p.admin else "",
|
||||||
players.size(), int(config.v("max_players"))])
|
players.size(), int(config.v("max_players"))])
|
||||||
# (The world's own seed: an existing world keeps the seed it was made with.)
|
# (The world's own seed: an existing world keeps the seed it was made with.)
|
||||||
var world := {"seed": int(_terrain.noise_seed), "worldlet": String(config.v("worldlet")), "spawn": start, "yaw": yaw,
|
var world := {"seed": int(_terrain.noise_seed), "worldlet": String(_terrain.worldlet_id), "spawn": start, "yaw": yaw,
|
||||||
"island_spawn": _spawn, "day": _world.day_night.day, "time": _world.day_night.time, "server_time": server_time,
|
"island_spawn": _spawn, "day": _world.day_night.day, "time": _world.day_night.time, "server_time": server_time,
|
||||||
"weather": _world.weather.override}
|
"weather": _world.weather.override}
|
||||||
_s2c_welcome.rpc_id(peer, {"you": peer, "server": config.public_info(), "players": players, "world": world})
|
_s2c_welcome.rpc_id(peer, {"you": peer, "server": config.public_info(), "players": players, "world": world})
|
||||||
|
|||||||
1
scripts/net/net.gd.uid
Normal file
1
scripts/net/net.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d1153ihwyw01t
|
||||||
1
scripts/net/net_sync.gd.uid
Normal file
1
scripts/net/net_sync.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bgyhqtmwppt4g
|
||||||
@@ -341,7 +341,7 @@ func _run(p: Node3D, what: String, a: Array) -> void:
|
|||||||
WorldNet.piece_state(f)
|
WorldNet.piece_state(f)
|
||||||
"bed":
|
"bed":
|
||||||
var b := WorldNet.piece(_i(a, 0))
|
var b := WorldNet.piece(_i(a, 0))
|
||||||
if b != null and b.item_id == "bed" and _allowed(Rules.use_problem(p.player_id, b)):
|
if b != null and b.is_in_group("bed") and _allowed(Rules.use_problem(p.player_id, b)):
|
||||||
Actions.use_bed(p, b)
|
Actions.use_bed(p, b)
|
||||||
"inv_click":
|
"inv_click":
|
||||||
# Slot `a[1]` of your own inventory (a[0] = 0) or a chest's.
|
# Slot `a[1]` of your own inventory (a[0] = 0) or a chest's.
|
||||||
@@ -365,6 +365,10 @@ func _run(p: Node3D, what: String, a: Array) -> void:
|
|||||||
var slot := _i(a, 1)
|
var slot := _i(a, 1)
|
||||||
if slot >= 0 and slot < from.size():
|
if slot >= 0 and slot < from.size():
|
||||||
Actions.transfer(p, from, slot, to, c)
|
Actions.transfer(p, from, slot, to, c)
|
||||||
|
"drop":
|
||||||
|
var ds := _i(a, 0)
|
||||||
|
if ds >= 0 and ds < p.inventory.size():
|
||||||
|
Actions.drop(p, ds, _i(a, 1))
|
||||||
"close":
|
"close":
|
||||||
# The inventory screen closed: what's on the cursor goes back in
|
# The inventory screen closed: what's on the cursor goes back in
|
||||||
# your bag, or at your feet if it's full.
|
# your bag, or at your feet if it's full.
|
||||||
|
|||||||
1
scripts/net/player_net.gd.uid
Normal file
1
scripts/net/player_net.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dim4cfgbtabvf
|
||||||
1
scripts/net/rules.gd.uid
Normal file
1
scripts/net/rules.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d2fotty6gceeh
|
||||||
1
scripts/net/server_config.gd.uid
Normal file
1
scripts/net/server_config.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cykv4lse4g62a
|
||||||
@@ -63,7 +63,7 @@ func held_item_id() -> String:
|
|||||||
return s.id if s != null else ""
|
return s.id if s != null else ""
|
||||||
|
|
||||||
func select(i: int) -> void:
|
func select(i: int) -> void:
|
||||||
selected = wrapi(i, 0, HOTBAR)
|
selected = wrapi(i, 0, inventory.size()) # (any bag slot: player.gd's rows)
|
||||||
selected_changed.emit()
|
selected_changed.emit()
|
||||||
|
|
||||||
func held_heat() -> float:
|
func held_heat() -> float:
|
||||||
@@ -85,7 +85,7 @@ func bed_piece(world: Node3D) -> Node3D:
|
|||||||
if not has_bed_spawn:
|
if not has_bed_spawn:
|
||||||
return null
|
return null
|
||||||
for p in world.all_pieces():
|
for p in world.all_pieces():
|
||||||
if p.item_id == "bed" and claims_bed(p):
|
if p.is_in_group("bed") and claims_bed(p):
|
||||||
return p
|
return p
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
|||||||
1
scripts/net/server_player.gd.uid
Normal file
1
scripts/net/server_player.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dkdnvh8f18o2x
|
||||||
@@ -49,7 +49,7 @@ func open_server_world(config: RefCounted) -> Node3D:
|
|||||||
var id := String(config.v("world")).validate_filename()
|
var id := String(config.v("world")).validate_filename()
|
||||||
if id.is_empty():
|
if id.is_empty():
|
||||||
id = "server-world"
|
id = "server-world"
|
||||||
Saves.current_id = Saves.open_or_create(id, int(config.v("seed")))
|
Saves.current_id = Saves.open_or_create(id, int(config.v("seed")), String(config.v("worldlet")))
|
||||||
backup("start-up") # (the state it's about to load, before anyone changes it)
|
backup("start-up") # (the state it's about to load, before anyone changes it)
|
||||||
_backup_left = maxf(float(config.v("backup_hours")), 0.25) * 3600.0
|
_backup_left = maxf(float(config.v("backup_hours")), 0.25) * 3600.0
|
||||||
var w: Node3D = load("res://scenes/world.tscn").instantiate()
|
var w: Node3D = load("res://scenes/world.tscn").instantiate()
|
||||||
|
|||||||
1
scripts/net/world_net.gd.uid
Normal file
1
scripts/net/world_net.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cmk7ho2ij6esq
|
||||||
@@ -237,13 +237,22 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||||||
return
|
return
|
||||||
for i in range(8):
|
for i in range(8):
|
||||||
if event.is_action_pressed("hotbar_%d" % (i + 1)):
|
if event.is_action_pressed("hotbar_%d" % (i + 1)):
|
||||||
player.select(i)
|
player.select_in_row(i)
|
||||||
|
return
|
||||||
|
if event.is_action_pressed("row_up"):
|
||||||
|
player.change_row(-1)
|
||||||
|
return
|
||||||
|
if event.is_action_pressed("row_down"):
|
||||||
|
player.change_row(1)
|
||||||
|
return
|
||||||
|
if event.is_action_pressed("drop"):
|
||||||
|
Actions.drop(player, player.selected, 1 if Input.is_key_pressed(KEY_SHIFT) else 0)
|
||||||
return
|
return
|
||||||
if event is InputEventMouseButton and event.pressed and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
if event is InputEventMouseButton and event.pressed and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
|
||||||
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
||||||
player.select(player.selected - 1)
|
player.select_in_row(player.selected % player.HOTBAR - 1)
|
||||||
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||||
player.select(player.selected + 1)
|
player.select_in_row(player.selected % player.HOTBAR + 1)
|
||||||
if event.is_action_released("attack") and _drawing:
|
if event.is_action_released("attack") and _drawing:
|
||||||
_release_bow()
|
_release_bow()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -49,10 +49,26 @@ func held_item_id() -> String:
|
|||||||
var s = inventory.get_slot(selected)
|
var s = inventory.get_slot(selected)
|
||||||
return s.id if s != null else ""
|
return s.id if s != null else ""
|
||||||
|
|
||||||
|
## Holds the item in bag slot `i` (any slot: 1-8 pick from the current row,
|
||||||
|
## K and M move between rows of 8).
|
||||||
func select(i: int) -> void:
|
func select(i: int) -> void:
|
||||||
selected = wrapi(i, 0, HOTBAR)
|
selected = wrapi(i, 0, inventory.size())
|
||||||
selected_changed.emit()
|
selected_changed.emit()
|
||||||
|
|
||||||
|
## The bag row (0 = the top) the 1-8 keys and the hotbar show.
|
||||||
|
func row() -> int:
|
||||||
|
return selected / HOTBAR
|
||||||
|
|
||||||
|
func rows() -> int:
|
||||||
|
return inventory.size() / HOTBAR
|
||||||
|
|
||||||
|
## Column `col` of the current row; and the same column a row up or down.
|
||||||
|
func select_in_row(col: int) -> void:
|
||||||
|
select(row() * HOTBAR + wrapi(col, 0, HOTBAR))
|
||||||
|
|
||||||
|
func change_row(by: int) -> void:
|
||||||
|
select(wrapi(row() + by, 0, rows()) * HOTBAR + selected % HOTBAR)
|
||||||
|
|
||||||
## Called by workbenches and chests (container = null for plain crafting).
|
## Called by workbenches and chests (container = null for plain crafting).
|
||||||
func open_inventory(container: Node) -> void:
|
func open_inventory(container: Node) -> void:
|
||||||
inventory_requested.emit(container)
|
inventory_requested.emit(container)
|
||||||
|
|||||||
@@ -103,8 +103,9 @@ func step(dt: float) -> void:
|
|||||||
_shelter_left -= dt
|
_shelter_left -= dt
|
||||||
if _shelter_left <= 0.0:
|
if _shelter_left <= 0.0:
|
||||||
_shelter_left = 0.25
|
_shelter_left = 0.25
|
||||||
roofed = world.build.roofed_at(p)
|
var cave: bool = world.has_method("underground_at") and world.underground_at(p) # (the tunnel: rock all round)
|
||||||
sheltered = roofed and world.build.enclosure_at(p) >= 0.74
|
roofed = cave or world.build.roofed_at(p)
|
||||||
|
sheltered = roofed and (cave or world.build.enclosure_at(p) >= 0.74)
|
||||||
stone_room = sheltered and world.build.stone_walls_at(p) >= 0.5
|
stone_room = sheltered and world.build.stone_walls_at(p) >= 0.5
|
||||||
cozy = sheltered and _rug_near(p)
|
cozy = sheltered and _rug_near(p)
|
||||||
# Rain soaks you unless there's a roof over your head.
|
# Rain soaks you unless there's a roof over your head.
|
||||||
@@ -274,6 +275,8 @@ func reset_after_death() -> void:
|
|||||||
func damage(amount: float, from: Vector3, by: String = "") -> void:
|
func damage(amount: float, from: Vector3, by: String = "") -> void:
|
||||||
if dead:
|
if dead:
|
||||||
return
|
return
|
||||||
|
if player != null and player.get("inventory") != null:
|
||||||
|
amount *= Actions.charm_mult(player.inventory, by) # (a fang necklace against wolves)
|
||||||
cause = "Killed by %s" % by if not by.is_empty() else "You were killed"
|
cause = "Killed by %s" % by if not by.is_empty() else "You were killed"
|
||||||
health = maxf(health - amount, 0.0)
|
health = maxf(health - amount, 0.0)
|
||||||
_hurt_cd = HURT_NO_REGEN
|
_hurt_cd = HURT_NO_REGEN
|
||||||
|
|||||||
53
scripts/ui/compass.gd
Normal file
53
scripts/ui/compass.gd
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
extends Control
|
||||||
|
## The compass (Gitea issue #1): a strip across the top of the screen that
|
||||||
|
## shows which way you're facing, N/E/S/W and the points between, with a tick
|
||||||
|
## at the centre. North is the map's top (-Z).
|
||||||
|
|
||||||
|
const UITheme = preload("res://scripts/ui/theme.gd")
|
||||||
|
const WIDTH := 420.0
|
||||||
|
const SPAN := 180.0 ## degrees shown across the strip
|
||||||
|
const POINTS := [[0, "N"], [45, "NE"], [90, "E"], [135, "SE"], [180, "S"], [225, "SW"], [270, "W"], [315, "NW"]]
|
||||||
|
|
||||||
|
var player: Node3D
|
||||||
|
|
||||||
|
func setup(p: Node3D) -> void:
|
||||||
|
player = p
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
set_anchors_preset(Control.PRESET_CENTER_TOP)
|
||||||
|
offset_left = -WIDTH * 0.5
|
||||||
|
offset_right = WIDTH * 0.5
|
||||||
|
offset_top = 44.0
|
||||||
|
offset_bottom = 74.0
|
||||||
|
|
||||||
|
## Which way `p` faces, in degrees clockwise from north (0 = N, 90 = E).
|
||||||
|
static func heading(p: Node3D) -> float:
|
||||||
|
var f: Vector3 = -p.global_basis.z
|
||||||
|
return fposmod(rad_to_deg(atan2(f.x, -f.z)), 360.0)
|
||||||
|
|
||||||
|
func _process(_d: float) -> void:
|
||||||
|
visible = player != null and is_instance_valid(player) and not bool(player.get("ui_open"))
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
func _draw() -> void:
|
||||||
|
if player == null or not is_instance_valid(player):
|
||||||
|
return
|
||||||
|
var h := heading(player)
|
||||||
|
var font := get_theme_default_font()
|
||||||
|
draw_rect(Rect2(Vector2.ZERO, size), Color(0, 0, 0, 0.35))
|
||||||
|
for deg in range(0, 360, 15):
|
||||||
|
var off := wrapf(deg - h, -180.0, 180.0)
|
||||||
|
if absf(off) > SPAN * 0.5:
|
||||||
|
continue
|
||||||
|
var x := WIDTH * 0.5 + off / SPAN * WIDTH
|
||||||
|
var big := deg % 45 == 0
|
||||||
|
draw_line(Vector2(x, 0), Vector2(x, 6 if not big else 9), Color(1, 1, 1, 0.5 if not big else 0.8), 1.0)
|
||||||
|
for pt in POINTS:
|
||||||
|
var off := wrapf(float(pt[0]) - h, -180.0, 180.0)
|
||||||
|
if absf(off) > SPAN * 0.5 - 6.0:
|
||||||
|
continue
|
||||||
|
var x := WIDTH * 0.5 + off / SPAN * WIDTH
|
||||||
|
var s := 17 if String(pt[1]).length() == 1 else 13
|
||||||
|
var col := Color(1.0, 0.55, 0.35) if pt[1] == "N" else Color(1, 1, 1, 0.9)
|
||||||
|
var w := font.get_string_size(pt[1], HORIZONTAL_ALIGNMENT_CENTER, -1, s).x
|
||||||
|
draw_string(font, Vector2(x - w * 0.5, 25), pt[1], HORIZONTAL_ALIGNMENT_LEFT, -1, s, col)
|
||||||
|
draw_line(Vector2(WIDTH * 0.5, 0), Vector2(WIDTH * 0.5, 30), Color(1.0, 0.85, 0.4, 0.9), 2.0)
|
||||||
1
scripts/ui/compass.gd.uid
Normal file
1
scripts/ui/compass.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b86dwl7mmrk2y
|
||||||
1
scripts/ui/guide.gd.uid
Normal file
1
scripts/ui/guide.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dbijp4r843nyk
|
||||||
1
scripts/ui/hint_card.gd.uid
Normal file
1
scripts/ui/hint_card.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://0npv7fub184o
|
||||||
@@ -12,6 +12,7 @@ var player: CharacterBody3D
|
|||||||
var _prompt: Label
|
var _prompt: Label
|
||||||
var _held_name: Label
|
var _held_name: Label
|
||||||
var _hotbar: Array = []
|
var _hotbar: Array = []
|
||||||
|
var _row_label: Label
|
||||||
var _toasts: VBoxContainer
|
var _toasts: VBoxContainer
|
||||||
var _screen: Control
|
var _screen: Control
|
||||||
var _menu: Control
|
var _menu: Control
|
||||||
@@ -20,6 +21,8 @@ var _diet_bars: Dictionary = {}
|
|||||||
var _status: Label
|
var _status: Label
|
||||||
var _clock: Label
|
var _clock: Label
|
||||||
var _hurt: ColorRect
|
var _hurt: ColorRect
|
||||||
|
var compass: Control
|
||||||
|
var map: Control
|
||||||
|
|
||||||
## A red flash at the screen edge when something hits you.
|
## A red flash at the screen edge when something hits you.
|
||||||
func _on_hurt(amount: float, _from: Vector3) -> void:
|
func _on_hurt(amount: float, _from: Vector3) -> void:
|
||||||
@@ -66,6 +69,16 @@ func bind(p: CharacterBody3D) -> void:
|
|||||||
bar.add_child(s)
|
bar.add_child(s)
|
||||||
_hotbar.append(s)
|
_hotbar.append(s)
|
||||||
root.add_child(bar)
|
root.add_child(bar)
|
||||||
|
# Which bag row the hotbar is showing (K / M change it), beside the bar.
|
||||||
|
_row_label = UITheme.label("", 14, UITheme.TEXT_DIM)
|
||||||
|
_row_label.set_anchors_and_offsets_preset(Control.PRESET_CENTER_BOTTOM)
|
||||||
|
_row_label.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||||
|
_row_label.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
||||||
|
_row_label.offset_left = 300
|
||||||
|
_row_label.offset_right = 420
|
||||||
|
_row_label.offset_bottom = -40
|
||||||
|
_row_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
root.add_child(_row_label)
|
||||||
|
|
||||||
_held_name = UITheme.label("", 15, UITheme.TEXT_DIM)
|
_held_name = UITheme.label("", 15, UITheme.TEXT_DIM)
|
||||||
_held_name.set_anchors_and_offsets_preset(Control.PRESET_CENTER_BOTTOM)
|
_held_name.set_anchors_and_offsets_preset(Control.PRESET_CENTER_BOTTOM)
|
||||||
@@ -185,6 +198,15 @@ func _build_stats(root: Control) -> void:
|
|||||||
tips.name = "HintCard"
|
tips.name = "HintCard"
|
||||||
root.add_child(tips)
|
root.add_child(tips)
|
||||||
tips.setup()
|
tips.setup()
|
||||||
|
# The compass along the top, and the map on N (Gitea issue #1).
|
||||||
|
compass = preload("res://scripts/ui/compass.gd").new()
|
||||||
|
compass.name = "Compass"
|
||||||
|
root.add_child(compass)
|
||||||
|
compass.setup(player)
|
||||||
|
map = preload("res://scripts/ui/map_screen.gd").new()
|
||||||
|
map.name = "Map"
|
||||||
|
root.add_child(map)
|
||||||
|
map.setup(player.get_parent(), player)
|
||||||
|
|
||||||
func _process(_d: float) -> void:
|
func _process(_d: float) -> void:
|
||||||
_fade_chat()
|
_fade_chat()
|
||||||
@@ -376,7 +398,9 @@ func _input(event: InputEvent) -> void:
|
|||||||
if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_ESCAPE:
|
if event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_ESCAPE:
|
||||||
if is_dead_screen():
|
if is_dead_screen():
|
||||||
return # (respawn first)
|
return # (respawn first)
|
||||||
if _screen.is_open():
|
if map != null and map.is_open():
|
||||||
|
map.close()
|
||||||
|
elif _screen.is_open():
|
||||||
_screen.close()
|
_screen.close()
|
||||||
elif _menu.is_open():
|
elif _menu.is_open():
|
||||||
_menu.back()
|
_menu.back()
|
||||||
@@ -387,14 +411,23 @@ func _input(event: InputEvent) -> void:
|
|||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if _menu.is_open():
|
if _menu.is_open():
|
||||||
return
|
return
|
||||||
|
if event.is_action_pressed("map") and not _screen.is_open() and not is_dead_screen() \
|
||||||
|
and (map.is_open() or not player.ui_open):
|
||||||
|
map.toggle()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
|
if map.is_open():
|
||||||
|
return
|
||||||
if event.is_action_pressed("inventory") and not _screen.is_open():
|
if event.is_action_pressed("inventory") and not _screen.is_open():
|
||||||
_screen.open(null)
|
_screen.open(null)
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
func _refresh() -> void:
|
func _refresh() -> void:
|
||||||
|
var first: int = player.row() * _hotbar.size()
|
||||||
for i in range(_hotbar.size()):
|
for i in range(_hotbar.size()):
|
||||||
_hotbar[i].set_stack(player.inventory.get_slot(i))
|
_hotbar[i].set_stack(player.inventory.get_slot(first + i))
|
||||||
_hotbar[i].set_selected(i == player.selected)
|
_hotbar[i].set_selected(first + i == player.selected)
|
||||||
|
_row_label.text = "Row %d of %d\nK / M" % [player.row() + 1, player.rows()]
|
||||||
var id: String = player.held_item_id()
|
var id: String = player.held_item_id()
|
||||||
_held_name.text = ItemDB.item_name(id) if not id.is_empty() else ""
|
_held_name.text = ItemDB.item_name(id) if not id.is_empty() else ""
|
||||||
|
|
||||||
|
|||||||
1
scripts/ui/hut_outline.gd.uid
Normal file
1
scripts/ui/hut_outline.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://87b73wsdjp7t
|
||||||
@@ -74,7 +74,7 @@ func bind(p: CharacterBody3D) -> void:
|
|||||||
if i == 7:
|
if i == 7:
|
||||||
pass
|
pass
|
||||||
pv.add_child(grid)
|
pv.add_child(grid)
|
||||||
pv.add_child(UITheme.label("Row 1 is your hotbar. Shift-click moves stacks to/from a chest.", 13, UITheme.TEXT_DIM))
|
pv.add_child(UITheme.label("1-8 pick from the row you're on; K / M change row. Shift-click moves stacks to/from a chest. Q drops what you hold.", 13, UITheme.TEXT_DIM))
|
||||||
var wr := HBoxContainer.new()
|
var wr := HBoxContainer.new()
|
||||||
wr.add_theme_constant_override("separation", 10)
|
wr.add_theme_constant_override("separation", 10)
|
||||||
_worn_label = UITheme.label("", 15)
|
_worn_label = UITheme.label("", 15)
|
||||||
|
|||||||
182
scripts/ui/map_screen.gd
Normal file
182
scripts/ui/map_screen.gd
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
extends Control
|
||||||
|
## The map (Gitea issue #1), on N. The worldlet you're in, drawn from its
|
||||||
|
## terrain (water, sand, grass, rock, snow, shaded by slope), but only where
|
||||||
|
## you've been: the rest is blank until you explore it (world.explored).
|
||||||
|
## Marked where you've seen them: the den, the barrow, the standing stones,
|
||||||
|
## the tunnel's seam, the pier and sea cave, your beds and buildings, your
|
||||||
|
## camp (fires, chests, benches) and your grave. You're a dot; which way
|
||||||
|
## you're facing is on the compass (and under the map). North is up.
|
||||||
|
|
||||||
|
const UITheme = preload("res://scripts/ui/theme.gd")
|
||||||
|
const Compass = preload("res://scripts/ui/compass.gd")
|
||||||
|
const RES := 200 ## map image pixels per side
|
||||||
|
const SIDE := 500.0 ## map size on screen
|
||||||
|
|
||||||
|
const MARKS := {
|
||||||
|
"den": [Color(0.85, 0.30, 0.25), "Wolf den"],
|
||||||
|
"barrow": [Color(0.75, 0.70, 0.55), "Barrow"],
|
||||||
|
"stones": [Color(0.80, 0.80, 0.85), "Standing stones"],
|
||||||
|
"seam": [Color(0.95, 0.60, 0.25), "Cliff tunnel"],
|
||||||
|
"pier": [Color(0.70, 0.50, 0.30), "Pier"],
|
||||||
|
"cave": [Color(0.95, 0.60, 0.25), "Sea cave"],
|
||||||
|
"bed": [Color(0.40, 0.75, 1.00), "Bed"],
|
||||||
|
"build": [Color(0.62, 0.45, 0.28), "Your buildings"],
|
||||||
|
"camp": [Color(1.00, 0.80, 0.35), "Camp"],
|
||||||
|
"grave": [Color(0.85, 0.85, 0.85), "Your things"],
|
||||||
|
}
|
||||||
|
|
||||||
|
var world: Node3D
|
||||||
|
var player: Node3D
|
||||||
|
var _tex: TextureRect
|
||||||
|
var _marks: Control
|
||||||
|
var _title: Label
|
||||||
|
var _facing: Label
|
||||||
|
|
||||||
|
func setup(w: Node3D, p: Node3D) -> void:
|
||||||
|
world = w
|
||||||
|
player = p
|
||||||
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
visible = false
|
||||||
|
var dim := ColorRect.new()
|
||||||
|
dim.color = Color(0, 0, 0, 0.6)
|
||||||
|
dim.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
add_child(dim)
|
||||||
|
var centre := CenterContainer.new()
|
||||||
|
centre.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
centre.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(centre)
|
||||||
|
var box := VBoxContainer.new()
|
||||||
|
box.add_theme_constant_override("separation", 6)
|
||||||
|
centre.add_child(box)
|
||||||
|
_title = UITheme.label("", 22)
|
||||||
|
_title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
box.add_child(_title)
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.add_theme_constant_override("separation", 14)
|
||||||
|
box.add_child(row)
|
||||||
|
var frame := Control.new()
|
||||||
|
frame.custom_minimum_size = Vector2(SIDE, SIDE)
|
||||||
|
row.add_child(frame)
|
||||||
|
_tex = TextureRect.new()
|
||||||
|
_tex.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_tex.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
|
_tex.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
|
||||||
|
frame.add_child(_tex)
|
||||||
|
_marks = Control.new()
|
||||||
|
_marks.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_marks.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_marks.draw.connect(_draw_marks)
|
||||||
|
frame.add_child(_marks)
|
||||||
|
var legend := VBoxContainer.new()
|
||||||
|
legend.add_theme_constant_override("separation", 4)
|
||||||
|
row.add_child(legend)
|
||||||
|
legend.add_child(UITheme.label("Key", 16, UITheme.TEXT_DIM))
|
||||||
|
var you := UITheme.label("● You", 14)
|
||||||
|
you.add_theme_color_override("font_color", Color(1, 1, 1))
|
||||||
|
legend.add_child(you)
|
||||||
|
for k in MARKS:
|
||||||
|
var l := UITheme.label("■ %s" % MARKS[k][1], 14)
|
||||||
|
l.add_theme_color_override("font_color", MARKS[k][0])
|
||||||
|
legend.add_child(l)
|
||||||
|
_facing = UITheme.label("", 15, UITheme.TEXT_DIM)
|
||||||
|
_facing.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
box.add_child(_facing)
|
||||||
|
var help := UITheme.label("N or Esc to close · north is up · the map fills in as you explore", 13, UITheme.TEXT_DIM)
|
||||||
|
help.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
box.add_child(help)
|
||||||
|
|
||||||
|
func is_open() -> bool:
|
||||||
|
return visible
|
||||||
|
|
||||||
|
func open() -> void:
|
||||||
|
_tex.texture = ImageTexture.create_from_image(render(world))
|
||||||
|
_title.text = "The %s" % preload("res://scripts/world/terrain.gd").worldlet_name(world.worldlet_id())
|
||||||
|
visible = true
|
||||||
|
player.ui_open = true
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
|
||||||
|
func close() -> void:
|
||||||
|
visible = false
|
||||||
|
player.ui_open = false
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||||
|
|
||||||
|
func toggle() -> void:
|
||||||
|
if visible:
|
||||||
|
close()
|
||||||
|
else:
|
||||||
|
open()
|
||||||
|
|
||||||
|
func _process(_d: float) -> void:
|
||||||
|
if not visible:
|
||||||
|
return
|
||||||
|
var h: float = Compass.heading(player)
|
||||||
|
var names := ["north", "north-east", "east", "south-east", "south", "south-west", "west", "north-west"]
|
||||||
|
_facing.text = "Facing %s" % names[int(round(h / 45.0)) % 8]
|
||||||
|
if world.underground_at(player.global_position):
|
||||||
|
_facing.text += " · in the tunnel, under the cliff"
|
||||||
|
_marks.queue_redraw()
|
||||||
|
|
||||||
|
## Where (x, z) in the world falls on the map, in its frame's pixels.
|
||||||
|
func _to_map(p: Vector3) -> Vector2:
|
||||||
|
var s: float = world.terrain.size
|
||||||
|
return Vector2((p.x + s * 0.5) / s, (p.z + s * 0.5) / s) * SIDE
|
||||||
|
|
||||||
|
func _draw_marks() -> void:
|
||||||
|
var font := get_theme_default_font()
|
||||||
|
for m in world.map_marks():
|
||||||
|
var at := _to_map(m.pos)
|
||||||
|
var col: Color = MARKS.get(m.kind, [Color.WHITE])[0]
|
||||||
|
var r := 3.0 if m.kind == "build" else 6.0
|
||||||
|
_marks.draw_rect(Rect2(at - Vector2(r, r), Vector2(r, r) * 2.0), col)
|
||||||
|
_marks.draw_rect(Rect2(at - Vector2(r, r), Vector2(r, r) * 2.0), Color(0, 0, 0, 0.7), false, 1.0)
|
||||||
|
if m.kind in ["den", "barrow", "stones", "seam", "pier", "cave"]:
|
||||||
|
_marks.draw_string(font, at + Vector2(9, 5), String(m.label), HORIZONTAL_ALIGNMENT_LEFT, -1, 13, col)
|
||||||
|
# You: a dot (the way you're facing is on the compass).
|
||||||
|
var p := player.global_position
|
||||||
|
if world.underground_at(p) and world.tunnel != null:
|
||||||
|
p = world.tunnel.seam_at # (down the tunnel: shown at its top)
|
||||||
|
var me := _to_map(p)
|
||||||
|
_marks.draw_circle(me, 6.5, Color(0, 0, 0, 0.8))
|
||||||
|
_marks.draw_circle(me, 5.0, Color(1, 1, 1))
|
||||||
|
# North.
|
||||||
|
_marks.draw_string(font, Vector2(SIDE - 26, 24), "N", HORIZONTAL_ALIGNMENT_LEFT, -1, 18, Color(1.0, 0.55, 0.35))
|
||||||
|
_marks.draw_line(Vector2(SIDE - 20, 30), Vector2(SIDE - 20, 48), Color(1.0, 0.55, 0.35), 2.0)
|
||||||
|
|
||||||
|
## The map image: the terrain, coloured and shaded, blank where unexplored.
|
||||||
|
static func render(w: Node3D) -> Image:
|
||||||
|
var t: Node3D = w.terrain
|
||||||
|
var img := Image.create(RES, RES, false, Image.FORMAT_RGB8)
|
||||||
|
var s: float = t.size
|
||||||
|
var step := s / RES
|
||||||
|
var blank := Color(0.16, 0.14, 0.12)
|
||||||
|
for iy in range(RES):
|
||||||
|
for ix in range(RES):
|
||||||
|
var x := -s * 0.5 + (ix + 0.5) * step
|
||||||
|
var z := -s * 0.5 + (iy + 0.5) * step
|
||||||
|
if not w.seen(x, z):
|
||||||
|
# (a faint paper grain, so blank reads as "not yet", not "nothing")
|
||||||
|
img.set_pixel(ix, iy, blank.lightened(0.04 * float((ix * 7 + iy * 13) % 3) / 2.0))
|
||||||
|
continue
|
||||||
|
img.set_pixel(ix, iy, ground_colour(t, x, z))
|
||||||
|
return img
|
||||||
|
|
||||||
|
static func ground_colour(t: Node3D, x: float, z: float) -> Color:
|
||||||
|
var h: float = t.sample_height(x, z)
|
||||||
|
var surface: float = t.water_level if t.inside_boundary(x, z, -2.0) else t.sea_level
|
||||||
|
if h < surface:
|
||||||
|
var deep := clampf((surface - h) / 6.0, 0.0, 1.0)
|
||||||
|
return Color(0.30, 0.52, 0.62).lerp(Color(0.10, 0.22, 0.36), deep)
|
||||||
|
var n: Vector3 = t.sample_normal(x, z)
|
||||||
|
var c: Color
|
||||||
|
if h > t.snow_height:
|
||||||
|
c = Color(0.92, 0.93, 0.95)
|
||||||
|
elif n.y < 0.75:
|
||||||
|
c = Color(0.48, 0.46, 0.43) # rock
|
||||||
|
elif h < surface + 0.9:
|
||||||
|
c = Color(0.80, 0.74, 0.55) # shore
|
||||||
|
else:
|
||||||
|
c = Color(0.36, 0.52, 0.27).lerp(Color(0.46, 0.50, 0.32), clampf(h / 20.0, 0.0, 1.0))
|
||||||
|
# Shading from the north-west, so hills and cliffs stand out.
|
||||||
|
var light := clampf(0.75 + (-n.x - n.z) * 0.9, 0.45, 1.25)
|
||||||
|
return Color(c.r * light, c.g * light, c.b * light)
|
||||||
1
scripts/ui/map_screen.gd.uid
Normal file
1
scripts/ui/map_screen.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b80va7jn1pyxo
|
||||||
@@ -13,7 +13,9 @@ const CONTROLS := [
|
|||||||
["Left click (fishing rod)", "Cast / reel in when the float dips"],
|
["Left click (fishing rod)", "Cast / reel in when the float dips"],
|
||||||
["R", "Rotate piece (building pieces: quarter turns)"], ["X", "Dismantle the piece you're looking at"],
|
["R", "Rotate piece (building pieces: quarter turns)"], ["X", "Dismantle the piece you're looking at"],
|
||||||
["E on a bed", "Set spawn point / sleep (night, under a roof)"],
|
["E on a bed", "Set spawn point / sleep (night, under a roof)"],
|
||||||
["1 - 8, wheel", "Hotbar"], ["Tab", "Inventory & crafting (shift-click Craft for 5)"],
|
["1 - 8, wheel", "Hotbar (the bag row you're on)"], ["K / M", "Hotbar shows the bag row above / below"],
|
||||||
|
["Q (shift+Q)", "Drop what you're holding (just one)"], ["Tab", "Inventory & crafting (shift-click Craft for 5)"],
|
||||||
|
["N", "Map (fills in as you explore; the compass is along the top)"],
|
||||||
["Shift + click", "Move stack to/from a chest"], ["Right click", "Split stack"],
|
["Shift + click", "Move stack to/from a chest"], ["Right click", "Split stack"],
|
||||||
["Esc", "This menu (game autosaves every 2 min)"],
|
["Esc", "This menu (game autosaves every 2 min)"],
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -370,6 +370,7 @@ func _host_page() -> Control:
|
|||||||
_host_preset.add_item(n)
|
_host_preset.add_item(n)
|
||||||
_host_preset.custom_minimum_size = Vector2(420, 36)
|
_host_preset.custom_minimum_size = Vector2(420, 36)
|
||||||
v.add_child(_host_preset)
|
v.add_child(_host_preset)
|
||||||
|
_host_worldlet = _worldlet_picker(v)
|
||||||
var row := HBoxContainer.new()
|
var row := HBoxContainer.new()
|
||||||
row.add_theme_constant_override("separation", 10)
|
row.add_theme_constant_override("separation", 10)
|
||||||
var c1 := VBoxContainer.new()
|
var c1 := VBoxContainer.new()
|
||||||
@@ -424,6 +425,7 @@ func _fill_host() -> void:
|
|||||||
_host_seed.editable = not existing
|
_host_seed.editable = not existing
|
||||||
_host_seed_label.text = "Seed" if not existing else "Seed (kept)"
|
_host_seed_label.text = "Seed" if not existing else "Seed (kept)"
|
||||||
_host_seed.text = "" if not existing else str(int(c.data.seed))
|
_host_seed.text = "" if not existing else str(int(c.data.seed))
|
||||||
|
_show_host_worldlet(existing, meta_path)
|
||||||
_host_msg.text = "Your hosted island carries on where you left it." if existing else "A new island will be made."
|
_host_msg.text = "Your hosted island carries on where you left it." if existing else "A new island will be made."
|
||||||
_host_msg.add_theme_color_override("font_color", UITheme.TEXT_DIM)
|
_host_msg.add_theme_color_override("font_color", UITheme.TEXT_DIM)
|
||||||
_host_btn.disabled = false
|
_host_btn.disabled = false
|
||||||
@@ -439,7 +441,8 @@ func _start_host() -> void:
|
|||||||
if name.is_empty():
|
if name.is_empty():
|
||||||
name = "%s's island" % Net.get_identity().name
|
name = "%s's island" % Net.get_identity().name
|
||||||
_host_btn.disabled = true
|
_host_btn.disabled = true
|
||||||
Net.host(name, _host_pw.text, int(port_t), PRESET_IDS[maxi(_host_preset.selected, 0)], seed)
|
Net.host(name, _host_pw.text, int(port_t), PRESET_IDS[maxi(_host_preset.selected, 0)], seed,
|
||||||
|
picked_worldlet(_host_worldlet))
|
||||||
|
|
||||||
func _on_host_status(text: String, failed: bool) -> void:
|
func _on_host_status(text: String, failed: bool) -> void:
|
||||||
if not _pages["host"].visible:
|
if not _pages["host"].visible:
|
||||||
@@ -591,6 +594,10 @@ func _new_page() -> Control:
|
|||||||
_rot_pick.add_item(t[1])
|
_rot_pick.add_item(t[1])
|
||||||
_rot_pick.custom_minimum_size = Vector2(360, 36)
|
_rot_pick.custom_minimum_size = Vector2(360, 36)
|
||||||
v.add_child(_rot_pick)
|
v.add_child(_rot_pick)
|
||||||
|
# The journey always begins in the Valley, so a new world has no "Where"
|
||||||
|
# (only with --dev-worldlets, to try the others).
|
||||||
|
if dev_worldlets():
|
||||||
|
_worldlet_pick = _worldlet_picker(v)
|
||||||
var h := HBoxContainer.new()
|
var h := HBoxContainer.new()
|
||||||
h.add_theme_constant_override("separation", 10)
|
h.add_theme_constant_override("separation", 10)
|
||||||
h.add_child(_button("Create & Play", _create, 170))
|
h.add_child(_button("Create & Play", _create, 170))
|
||||||
@@ -633,7 +640,8 @@ func _fill_list() -> void:
|
|||||||
info.add_child(UITheme.label(w.name, 18))
|
info.add_child(UITheme.label(w.name, 18))
|
||||||
var played := int(w.get("play_time", 0.0))
|
var played := int(w.get("play_time", 0.0))
|
||||||
var when := Time.get_datetime_string_from_unix_time(int(w.get("last_played", 0)), true)
|
var when := Time.get_datetime_string_from_unix_time(int(w.get("last_played", 0)), true)
|
||||||
info.add_child(UITheme.label("seed %d · played %dh %02dm · %s" % [int(w.seed), played / 3600, (played / 60) % 60, when], 13, UITheme.TEXT_DIM))
|
info.add_child(UITheme.label("%s · seed %d · played %dh %02dm · %s" % [Terrain.worldlet_name(String(w.get("worldlet", "valley"))),
|
||||||
|
int(w.seed), played / 3600, (played / 60) % 60, when], 13, UITheme.TEXT_DIM))
|
||||||
h.add_child(info)
|
h.add_child(info)
|
||||||
h.add_child(_button("Play", _play.bind(w.id), 80))
|
h.add_child(_button("Play", _play.bind(w.id), 80))
|
||||||
var del := Button.new()
|
var del := Button.new()
|
||||||
@@ -681,7 +689,85 @@ func _create() -> void:
|
|||||||
var seed_text := _seed_edit.text.strip_edges()
|
var seed_text := _seed_edit.text.strip_edges()
|
||||||
var seed := int(seed_text) if seed_text.is_valid_int() else (seed_text.hash() if not seed_text.is_empty() else randi())
|
var seed := int(seed_text) if seed_text.is_valid_int() else (seed_text.hash() if not seed_text.is_empty() else randi())
|
||||||
var rot: String = ROT_CHOICES[maxi(_rot_pick.selected, 0)][0]
|
var rot: String = ROT_CHOICES[maxi(_rot_pick.selected, 0)][0]
|
||||||
_play(Saves.create_world(world_name, seed, {"rot": rot}))
|
_play(Saves.create_world(world_name, seed, {"rot": rot, "worldlet": picked_worldlet(_worldlet_pick)}))
|
||||||
|
|
||||||
|
# --- worldlets (docs/WORLDLETS_DESIGN.md, W1) -----------------------------------------
|
||||||
|
|
||||||
|
const Terrain = preload("res://scripts/world/terrain.gd")
|
||||||
|
var _worldlet_pick: OptionButton
|
||||||
|
var _host_worldlet: OptionButton
|
||||||
|
|
||||||
|
## "Where": the worldlets a new world can start in, with the chosen one's
|
||||||
|
## description under it. (Only the Valley today: the others are reached on
|
||||||
|
## the journey, so the list is short and the picker sits still.)
|
||||||
|
func _worldlet_picker(v: VBoxContainer) -> OptionButton:
|
||||||
|
var starts: Array = offered_worldlets(dev_worldlets())
|
||||||
|
v.add_child(UITheme.label("Where", 15, UITheme.TEXT_DIM))
|
||||||
|
var pick := OptionButton.new()
|
||||||
|
pick.custom_minimum_size = Vector2(360, 36)
|
||||||
|
pick.focus_mode = Control.FOCUS_NONE
|
||||||
|
for w in starts:
|
||||||
|
pick.add_item(String(w.name))
|
||||||
|
pick.set_item_metadata(pick.item_count - 1, String(w.id))
|
||||||
|
pick.disabled = starts.size() < 2
|
||||||
|
v.add_child(pick)
|
||||||
|
var about := UITheme.label(String(starts[0].description) if not starts.is_empty() else "", 13, UITheme.TEXT_DIM)
|
||||||
|
about.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
about.custom_minimum_size = Vector2(360, 0)
|
||||||
|
v.add_child(about)
|
||||||
|
pick.item_selected.connect(func(i: int):
|
||||||
|
about.text = String(Terrain.load_worldlet(String(pick.get_item_metadata(i))).get("description", "")))
|
||||||
|
return pick
|
||||||
|
|
||||||
|
## Started with `-- --dev-worldlets`: every worldlet can be picked, for
|
||||||
|
## trying one before the journey reaches it.
|
||||||
|
static func dev_worldlets() -> bool:
|
||||||
|
return "--dev-worldlets" in OS.get_cmdline_user_args()
|
||||||
|
|
||||||
|
## The worldlets offered for a new world or island: the starts, and (`dev`)
|
||||||
|
## the rest too, marked for testing.
|
||||||
|
static func offered_worldlets(dev: bool) -> Array:
|
||||||
|
var out: Array = Terrain.starting_worldlets()
|
||||||
|
if dev:
|
||||||
|
for w in Terrain.worldlets():
|
||||||
|
if not w.start:
|
||||||
|
var t: Dictionary = w.duplicate()
|
||||||
|
t.name = "%s (testing)" % w.name
|
||||||
|
out.append(t)
|
||||||
|
return out
|
||||||
|
|
||||||
|
## The Host page's "Where": a hosted island keeps its worldlet, like its
|
||||||
|
## seed, so once it exists the picker shows that one and stays put.
|
||||||
|
func _show_host_worldlet(existing: bool, meta_path: String) -> void:
|
||||||
|
var id := "valley"
|
||||||
|
if existing:
|
||||||
|
var meta: Variant = JSON.parse_string(FileAccess.get_file_as_string(meta_path))
|
||||||
|
if meta is Dictionary:
|
||||||
|
id = String(meta.get("worldlet", "valley"))
|
||||||
|
var i := select_worldlet(_host_worldlet, id)
|
||||||
|
if i >= 0:
|
||||||
|
_host_worldlet.item_selected.emit(i)
|
||||||
|
_host_worldlet.disabled = existing or _host_worldlet.item_count < 2
|
||||||
|
|
||||||
|
## Selects worldlet `id` in `pick`, adding it if it isn't a starting one
|
||||||
|
## (an island made somewhere else). Returns its index, or -1.
|
||||||
|
static func select_worldlet(pick: OptionButton, id: String) -> int:
|
||||||
|
for i in pick.item_count:
|
||||||
|
if String(pick.get_item_metadata(i)) == id:
|
||||||
|
pick.select(i)
|
||||||
|
return i
|
||||||
|
if Terrain.load_worldlet(id).is_empty():
|
||||||
|
return -1
|
||||||
|
pick.add_item(Terrain.worldlet_name(id))
|
||||||
|
pick.set_item_metadata(pick.item_count - 1, id)
|
||||||
|
pick.select(pick.item_count - 1)
|
||||||
|
return pick.item_count - 1
|
||||||
|
|
||||||
|
## The id of the worldlet chosen in `pick` (the Valley if nothing is).
|
||||||
|
static func picked_worldlet(pick: OptionButton) -> String:
|
||||||
|
if pick == null or pick.item_count == 0 or pick.selected < 0:
|
||||||
|
return "valley"
|
||||||
|
return String(pick.get_item_metadata(pick.selected))
|
||||||
|
|
||||||
## The tutorial (docs/TUTORIAL_DESIGN.md): its own world on the original
|
## The tutorial (docs/TUTORIAL_DESIGN.md): its own world on the original
|
||||||
## island (seed 1337), made the first time and carried on after that.
|
## island (seed 1337), made the first time and carried on after that.
|
||||||
|
|||||||
1
scripts/world/barrow.gd.uid
Normal file
1
scripts/world/barrow.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://5vs0iggnxpuc
|
||||||
@@ -15,6 +15,8 @@ const DAY_BIAS := 0.2
|
|||||||
var day := 1
|
var day := 1
|
||||||
var time := 8.0 / 24.0
|
var time := 8.0 / 24.0
|
||||||
var frozen := false ## title-screen backdrop: hold a fixed time
|
var frozen := false ## title-screen backdrop: hold a fixed time
|
||||||
|
var underground := 0.0 ## 0..1: how far into the tunnel you are (no daylight; world.gd sets it)
|
||||||
|
const AERIAL := 0.55 ## the fog's sky tint with distance (scenes/default_env.tres)
|
||||||
|
|
||||||
var sun: DirectionalLight3D
|
var sun: DirectionalLight3D
|
||||||
var moon: DirectionalLight3D
|
var moon: DirectionalLight3D
|
||||||
@@ -123,6 +125,26 @@ func apply(force: bool) -> void:
|
|||||||
env.ambient_light_energy = lerpf(0.3, 1.0, daylight) * lerpf(1.0, 0.85, overcast) + w_flash * 3.0
|
env.ambient_light_energy = lerpf(0.3, 1.0, daylight) * lerpf(1.0, 0.85, overcast) + w_flash * 3.0
|
||||||
env.fog_light_energy = lerpf(0.25, 1.0, daylight) * lerpf(1.0, 0.7, overcast) + w_flash * 2.0
|
env.fog_light_energy = lerpf(0.25, 1.0, daylight) * lerpf(1.0, 0.7, overcast) + w_flash * 2.0
|
||||||
env.volumetric_fog_density = lerpf(0.0025, 0.0045, daylight) * lerpf(1.0, 2.0, overcast)
|
env.volumetric_fog_density = lerpf(0.0025, 0.0045, daylight) * lerpf(1.0, 2.0, overcast)
|
||||||
|
if underground > 0.0:
|
||||||
|
# In the tunnel (J1): no sun, moon or sky, only the lights you bring.
|
||||||
|
var u := underground
|
||||||
|
sun.light_energy *= 1.0 - u
|
||||||
|
sun.visible = sun.light_energy > 0.01
|
||||||
|
sun.shadow_enabled = sun.visible
|
||||||
|
moon.light_energy *= 1.0 - u
|
||||||
|
moon.visible = moon.light_energy > 0.01
|
||||||
|
env.ambient_light_energy = lerpf(env.ambient_light_energy, 0.015, u)
|
||||||
|
env.ambient_light_sky_contribution = 1.0 - u
|
||||||
|
env.fog_light_energy = lerpf(env.fog_light_energy, 0.0, u)
|
||||||
|
env.volumetric_fog_density *= 1.0 - u
|
||||||
|
# (Sky reflections and the fog's sky-coloured distance would light the
|
||||||
|
# rock blue-grey all the way down: off.)
|
||||||
|
env.fog_aerial_perspective = lerpf(AERIAL, 0.0, u)
|
||||||
|
env.reflected_light_source = Environment.REFLECTION_SOURCE_DISABLED if u > 0.5 else Environment.REFLECTION_SOURCE_SKY
|
||||||
|
else:
|
||||||
|
env.ambient_light_sky_contribution = 1.0
|
||||||
|
env.fog_aerial_perspective = AERIAL
|
||||||
|
env.reflected_light_source = Environment.REFLECTION_SOURCE_SKY
|
||||||
env.fog_density = BASE_FOG * w_fog
|
env.fog_density = BASE_FOG * w_fog
|
||||||
|
|
||||||
_sky_timer -= get_process_delta_time()
|
_sky_timer -= get_process_delta_time()
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ func _on_new_day(day: int) -> void:
|
|||||||
if Weather.hard_winter(day + 1) and not Weather.hard_winter(day):
|
if Weather.hard_winter(day + 1) and not Weather.hard_winter(day):
|
||||||
Actions.tell_all("The air has a bite to it. A hard winter is coming tomorrow: three bitter days. Stock up on food and firewood.")
|
Actions.tell_all("The air has a bite to it. A hard winter is coming tomorrow: three bitter days. Stock up on food and firewood.")
|
||||||
elif Weather.hard_winter(day) and not Weather.hard_winter(day - 1):
|
elif Weather.hard_winter(day) and not Weather.hard_winter(day - 1):
|
||||||
Actions.tell_all("A hard winter has set in. The lake's edge has frozen, and the animals are holed up.")
|
Actions.tell_all("A hard winter has set in. The %s's edge has frozen, and the animals are holed up." % ("lake" if world.worldlet_id() == "valley" else "pond"))
|
||||||
elif Weather.hard_winter(day - 1) and not Weather.hard_winter(day):
|
elif Weather.hard_winter(day - 1) and not Weather.hard_winter(day):
|
||||||
Actions.tell_all("The hard winter is over.")
|
Actions.tell_all("The hard winter is over.")
|
||||||
|
|
||||||
|
|||||||
1
scripts/world/hazards.gd.uid
Normal file
1
scripts/world/hazards.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bmcefncielej3
|
||||||
114
scripts/world/pier.gd
Normal file
114
scripts/world/pier.gd
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
extends Node3D
|
||||||
|
## The Cove's pier (W3, docs/WORLDLETS_DESIGN.md §5): a wooden walkway from
|
||||||
|
## the sand straight out over the bay, so a cast from its end reaches deep
|
||||||
|
## sea water (mackerel, and cod further out). Part of the world, like the
|
||||||
|
## barrow: worked out from the terrain alone, so every player and the server
|
||||||
|
## get the same one, and it's never saved, built or dismantled.
|
||||||
|
##
|
||||||
|
## It starts where the beach has risen to deck height (so you walk straight
|
||||||
|
## on) and runs `LENGTH_OUT` metres past the waterline.
|
||||||
|
|
||||||
|
const WIDTH := 2.4
|
||||||
|
const DECK_ABOVE := 1.0 ## deck height over the water
|
||||||
|
const LENGTH_OUT := 24.0 ## how far past the waterline it reaches
|
||||||
|
const POST_EVERY := 3.0
|
||||||
|
const WOOD := Color(0.42, 0.30, 0.19)
|
||||||
|
const POST := Color(0.30, 0.22, 0.15)
|
||||||
|
|
||||||
|
var start := Vector3.ZERO ## the landward end, on the sand
|
||||||
|
var end := Vector3.ZERO ## the seaward end (fish from here)
|
||||||
|
var dir := Vector3.ZERO ## out to sea along the deck
|
||||||
|
|
||||||
|
## Lays the pier off the terrain's beach. Returns false (and adds nothing)
|
||||||
|
## when the worldlet has no beach.
|
||||||
|
func place(terrain: Node3D) -> bool:
|
||||||
|
if not terrain._has_beach or not terrain.worldlet.get("places", []).any(func(p): return p is Dictionary and p.get("type", "") == "pier"):
|
||||||
|
return false
|
||||||
|
var deck_y: float = terrain.water_level + DECK_ABOVE
|
||||||
|
var out2: Vector2 = terrain.shore_point(LENGTH_OUT)
|
||||||
|
var root2: Vector2 = terrain.shore_point(-2.0)
|
||||||
|
var d2 := (out2 - root2).normalized()
|
||||||
|
# Walk inland from the waterline to where the sand is at deck height.
|
||||||
|
var s2 := root2
|
||||||
|
for _i in range(40):
|
||||||
|
if terrain.sample_height(s2.x, s2.y) >= deck_y - 0.15:
|
||||||
|
break
|
||||||
|
s2 -= d2 * 0.5
|
||||||
|
start = Vector3(s2.x, deck_y, s2.y)
|
||||||
|
end = Vector3(out2.x, deck_y, out2.y)
|
||||||
|
dir = (end - start).normalized()
|
||||||
|
_build(terrain)
|
||||||
|
return true
|
||||||
|
|
||||||
|
## Whether (x, z) is under or right beside the pier (scenery keeps clear).
|
||||||
|
func covers(x: float, z: float, margin: float = 1.0) -> bool:
|
||||||
|
var a := Vector2(start.x, start.z)
|
||||||
|
var b := Vector2(end.x, end.z)
|
||||||
|
var p := Vector2(x, z)
|
||||||
|
var t := clampf((p - a).dot(b - a) / maxf((b - a).length_squared(), 0.001), 0.0, 1.0)
|
||||||
|
return p.distance_to(a.lerp(b, t)) < WIDTH * 0.5 + margin
|
||||||
|
|
||||||
|
func _build(terrain: Node3D) -> void:
|
||||||
|
var length := start.distance_to(end)
|
||||||
|
var yaw := atan2(dir.x, dir.z)
|
||||||
|
var basis := Basis(Vector3.UP, yaw)
|
||||||
|
var mid := (start + end) * 0.5
|
||||||
|
# The deck: planks across it, with a little gap and wobble between them.
|
||||||
|
var st := SurfaceTool.new()
|
||||||
|
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||||
|
var n := int(length / 0.32)
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = 7171
|
||||||
|
for i in range(n):
|
||||||
|
var z := -length * 0.5 + (i + 0.5) * length / n
|
||||||
|
var h := 0.08 + rng.randf_range(-0.01, 0.01)
|
||||||
|
_box(st, Vector3(rng.randf_range(-0.04, 0.04), -h * 0.5, z), Vector3(WIDTH, h, length / n - 0.03),
|
||||||
|
WOOD.darkened(rng.randf_range(0.0, 0.18)))
|
||||||
|
# Posts down to the bed, in pairs, and a rail beam along each side.
|
||||||
|
var k := 0.0
|
||||||
|
while k <= length + 0.01:
|
||||||
|
var along := -length * 0.5 + k
|
||||||
|
for side in [-1.0, 1.0]:
|
||||||
|
var local := Vector3(side * (WIDTH * 0.5 - 0.1), 0.0, along)
|
||||||
|
var world_p: Vector3 = mid + basis * local
|
||||||
|
var bed: float = terrain.sample_height(world_p.x, world_p.z)
|
||||||
|
var tall := start.y - bed + 0.3
|
||||||
|
if tall > 0.2:
|
||||||
|
_box(st, local + Vector3(0, -tall * 0.5 - 0.05, 0), Vector3(0.22, tall, 0.22), POST)
|
||||||
|
k += POST_EVERY
|
||||||
|
for side in [-1.0, 1.0]:
|
||||||
|
_box(st, Vector3(side * (WIDTH * 0.5 - 0.06), -0.16, 0.0), Vector3(0.12, 0.14, length), POST)
|
||||||
|
st.generate_normals()
|
||||||
|
var mi := MeshInstance3D.new()
|
||||||
|
mi.name = "Deck"
|
||||||
|
mi.mesh = st.commit()
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.vertex_color_use_as_albedo = true
|
||||||
|
mat.roughness = 0.9
|
||||||
|
mi.material_override = mat
|
||||||
|
add_child(mi)
|
||||||
|
# Walkable deck (on the ground layer, like the terrain).
|
||||||
|
var body := StaticBody3D.new()
|
||||||
|
body.name = "DeckBody"
|
||||||
|
var cs := CollisionShape3D.new()
|
||||||
|
var box := BoxShape3D.new()
|
||||||
|
box.size = Vector3(WIDTH, 0.12, length)
|
||||||
|
cs.shape = box
|
||||||
|
cs.position = Vector3(0, -0.06, 0)
|
||||||
|
body.add_child(cs)
|
||||||
|
add_child(body)
|
||||||
|
transform = Transform3D(basis, mid)
|
||||||
|
add_to_group("pier")
|
||||||
|
|
||||||
|
static func _box(st: SurfaceTool, c: Vector3, s: Vector3, col: Color) -> void:
|
||||||
|
var h := s * 0.5
|
||||||
|
var v := [
|
||||||
|
c + Vector3(-h.x, -h.y, -h.z), c + Vector3(h.x, -h.y, -h.z), c + Vector3(h.x, h.y, -h.z), c + Vector3(-h.x, h.y, -h.z),
|
||||||
|
c + Vector3(-h.x, -h.y, h.z), c + Vector3(h.x, -h.y, h.z), c + Vector3(h.x, h.y, h.z), c + Vector3(-h.x, h.y, h.z),
|
||||||
|
]
|
||||||
|
var faces := [[0, 3, 2, 1], [4, 5, 6, 7], [0, 4, 7, 3], [1, 2, 6, 5], [3, 7, 6, 2], [0, 1, 5, 4]]
|
||||||
|
st.set_color(col)
|
||||||
|
for f in faces:
|
||||||
|
# (listed anticlockwise from outside; Godot's front faces are clockwise)
|
||||||
|
st.add_vertex(v[f[0]]); st.add_vertex(v[f[2]]); st.add_vertex(v[f[1]])
|
||||||
|
st.add_vertex(v[f[0]]); st.add_vertex(v[f[3]]); st.add_vertex(v[f[2]])
|
||||||
1
scripts/world/pier.gd.uid
Normal file
1
scripts/world/pier.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ps0h2qmbn5ms
|
||||||
@@ -31,6 +31,8 @@ const FLOWER_COLORS := [
|
|||||||
@export var grass_tiles_per_update: int = 6
|
@export var grass_tiles_per_update: int = 6
|
||||||
|
|
||||||
var terrain: Node3D
|
var terrain: Node3D
|
||||||
|
var pier: Node3D ## the Cove's pier, kept clear of scenery (set by the world)
|
||||||
|
var keep_clear: Array = [] ## [[Vector3, radius]]: the tunnel's seam, the sea cave
|
||||||
var _player: Node3D
|
var _player: Node3D
|
||||||
var _spawn := Vector3.ZERO
|
var _spawn := Vector3.ZERO
|
||||||
var _forest_noise := FastNoiseLite.new()
|
var _forest_noise := FastNoiseLite.new()
|
||||||
@@ -417,7 +419,13 @@ func _scatter_trees() -> void:
|
|||||||
|
|
||||||
## Open grassy strip along the sea-cliff edge: no trees, bushes or boulders, so
|
## Open grassy strip along the sea-cliff edge: no trees, bushes or boulders, so
|
||||||
## the view out to sea is clear and you can walk right up to the edge.
|
## the view out to sea is clear and you can walk right up to the edge.
|
||||||
|
## (Also kept clear: the Cove's pier and the sand by its foot.)
|
||||||
func _on_clifftop(x: float, z: float) -> bool:
|
func _on_clifftop(x: float, z: float) -> bool:
|
||||||
|
if pier != null and pier.covers(x, z, 2.0):
|
||||||
|
return true
|
||||||
|
for c in keep_clear:
|
||||||
|
if Vector2(x - c[0].x, z - c[0].z).length() < float(c[1]):
|
||||||
|
return true
|
||||||
return terrain.sea_weight(x, z) > 0.5 and not terrain.inside_boundary(x, z, 10.0) \
|
return terrain.sea_weight(x, z) > 0.5 and not terrain.inside_boundary(x, z, 10.0) \
|
||||||
and terrain.inside_boundary(x, z, -8.0)
|
and terrain.inside_boundary(x, z, -8.0)
|
||||||
|
|
||||||
@@ -486,7 +494,7 @@ func _scatter_rocks() -> void:
|
|||||||
# Sea stacks: tall pillars standing off the cliff (no collision, unreachable).
|
# Sea stacks: tall pillars standing off the cliff (no collision, unreachable).
|
||||||
var placed := 0
|
var placed := 0
|
||||||
var tries := 0
|
var tries := 0
|
||||||
while placed < 9 and tries < 200:
|
while placed < 9 and tries < 200 and terrain._has_sea: # (only off a sea cliff)
|
||||||
tries += 1
|
tries += 1
|
||||||
var ang: float = terrain.sea_angle_deg + rng.randf_range(-1.0, 1.0) * (terrain.sea_half_width_deg + 8.0)
|
var ang: float = terrain.sea_angle_deg + rng.randf_range(-1.0, 1.0) * (terrain.sea_half_width_deg + 8.0)
|
||||||
var p: Vector2 = terrain.coast_point(ang, rng.randf_range(10.0, 48.0))
|
var p: Vector2 = terrain.coast_point(ang, rng.randf_range(10.0, 48.0))
|
||||||
@@ -672,7 +680,7 @@ func _scatter_pickups() -> void:
|
|||||||
continue
|
continue
|
||||||
var h: float = terrain.sample_height(x, z)
|
var h: float = terrain.sample_height(x, z)
|
||||||
var n: Vector3 = terrain.sample_normal(x, z)
|
var n: Vector3 = terrain.sample_normal(x, z)
|
||||||
if h < terrain.water_level + 0.3 or n.y < 0.8:
|
if h < terrain.water_level + 0.3 or n.y < 0.8 or (pier != null and pier.covers(x, z, 1.0)):
|
||||||
continue
|
continue
|
||||||
var chance := 1.0
|
var chance := 1.0
|
||||||
match id:
|
match id:
|
||||||
|
|||||||
120
scripts/world/sea_cave.gd
Normal file
120
scripts/world/sea_cave.gd
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
extends Node3D
|
||||||
|
## The Cove's sea cave (J1): the bottom of the Valley's cliff tunnel, seen
|
||||||
|
## from the shore. A dark opening in the rocks at the end of the beach,
|
||||||
|
## where the headland meets the sand. [E] goes in and up the tunnel, back to
|
||||||
|
## the Valley, once that tunnel has been dug through from the top. (Until
|
||||||
|
## then the cave runs back into solid rock.)
|
||||||
|
##
|
||||||
|
## Placed from the terrain alone, like the pier, so it's the same for
|
||||||
|
## everyone; nothing about it is saved here (the tunnel is the Valley's).
|
||||||
|
|
||||||
|
var world: Node3D
|
||||||
|
var to := "valley"
|
||||||
|
var front := Vector3.ZERO ## where you stand in front of it (arriving from the tunnel)
|
||||||
|
|
||||||
|
## Places the cave if this worldlet has a way back by tunnel from a sea cave.
|
||||||
|
func place(w: Node3D) -> bool:
|
||||||
|
world = w
|
||||||
|
var t: Node3D = w.terrain
|
||||||
|
var way: Dictionary = {}
|
||||||
|
for c in w.crossings():
|
||||||
|
if c is Dictionary and c.get("by", "") == "tunnel" and c.get("where", "") == "sea_cave":
|
||||||
|
way = c
|
||||||
|
if way.is_empty() or not t._has_beach:
|
||||||
|
return false
|
||||||
|
to = String(way.to)
|
||||||
|
# At the end of the beach, where the headland starts: the first dry,
|
||||||
|
# gentle spot coming in from the waterline.
|
||||||
|
var a := deg_to_rad(t.beach_angle_deg + t.beach_half_width_deg + t.beach_fade_deg * 0.35)
|
||||||
|
var dir := Vector2(cos(a), sin(a))
|
||||||
|
var at := Vector3.INF
|
||||||
|
for i in range(60):
|
||||||
|
var r: float = t._shore_d() - 2.0 - i * 0.5 - t._rim_offset(dir.x, dir.y)
|
||||||
|
var p := dir * r
|
||||||
|
var h: float = t.sample_height(p.x, p.y)
|
||||||
|
if h > t.water_level + 0.5 and t.sample_normal(p.x, p.y).y > 0.86 and t.inside_boundary(p.x, p.y, 3.0):
|
||||||
|
at = Vector3(p.x, h, p.y)
|
||||||
|
break
|
||||||
|
if at == Vector3.INF:
|
||||||
|
return false
|
||||||
|
# Facing back along the beach (towards its middle), so you see it from the sand.
|
||||||
|
var mid: Vector2 = t.shore_point(-6.0)
|
||||||
|
var face := Vector3(mid.x - at.x, 0.0, mid.y - at.z).normalized()
|
||||||
|
global_transform = Transform3D(Basis.looking_at(face, Vector3.UP), at)
|
||||||
|
front = at + face * 2.4
|
||||||
|
front.y = t.sample_height(front.x, front.z)
|
||||||
|
_build(w)
|
||||||
|
add_to_group("sea_cave")
|
||||||
|
return true
|
||||||
|
|
||||||
|
## Whether the Valley's tunnel has been dug right down to here.
|
||||||
|
func open() -> bool:
|
||||||
|
if not Saves.has_world():
|
||||||
|
return false
|
||||||
|
var s: Dictionary = Saves.load_state(Saves.current_id, to)
|
||||||
|
return int(s.get("tunnel", {}).get("dug", 0)) >= preload("res://scripts/world/tunnel.gd").STRETCHES
|
||||||
|
|
||||||
|
func interact_prompt(_player: Node) -> String:
|
||||||
|
if open():
|
||||||
|
return "[E] Go into the sea cave and up the tunnel to the %s" % preload("res://scripts/world/terrain.gd").worldlet_name(to)
|
||||||
|
return "A sea cave. It runs back into solid rock: the way up hasn't been dug"
|
||||||
|
|
||||||
|
func interact(_player: Node) -> void:
|
||||||
|
if open():
|
||||||
|
world.travel(to, "tunnel_bottom")
|
||||||
|
else:
|
||||||
|
Actions.notify.emit(interact_prompt(null))
|
||||||
|
|
||||||
|
func _build(w: Node3D) -> void:
|
||||||
|
var rock := StandardMaterial3D.new()
|
||||||
|
rock.albedo_color = Color(0.42, 0.40, 0.38)
|
||||||
|
rock.roughness = 1.0
|
||||||
|
if w.terrain.noise_tex != null:
|
||||||
|
rock.albedo_texture = w.terrain.noise_tex
|
||||||
|
rock.uv1_triplanar = true
|
||||||
|
rock.uv1_scale = Vector3(0.18, 0.18, 0.18)
|
||||||
|
var body := StaticBody3D.new()
|
||||||
|
add_child(body)
|
||||||
|
# (local -Z faces the beach; the rocks are behind, +Z)
|
||||||
|
for part in [[Vector3(-2.0, 1.6, 1.0), Vector3(1.8, 3.6, 3.0)], [Vector3(2.0, 1.4, 1.0), Vector3(1.8, 3.2, 3.0)],
|
||||||
|
[Vector3(0, 3.5, 1.0), Vector3(5.8, 1.4, 3.2)], [Vector3(0, 1.4, 2.4), Vector3(2.4, 3.0, 0.6)],
|
||||||
|
[Vector3(-3.4, 1.0, 2.0), Vector3(2.0, 2.4, 3.0)], [Vector3(3.2, 0.8, 2.2), Vector3(1.8, 2.0, 2.6)]]:
|
||||||
|
var mi := MeshInstance3D.new()
|
||||||
|
var bm := BoxMesh.new()
|
||||||
|
bm.size = part[1]
|
||||||
|
mi.mesh = bm
|
||||||
|
mi.material_override = rock
|
||||||
|
mi.position = part[0]
|
||||||
|
mi.rotation.y = part[0].x * 0.06
|
||||||
|
add_child(mi)
|
||||||
|
var cs := CollisionShape3D.new()
|
||||||
|
var box := BoxShape3D.new()
|
||||||
|
box.size = part[1]
|
||||||
|
cs.shape = box
|
||||||
|
cs.position = part[0]
|
||||||
|
cs.rotation.y = part[0].x * 0.06
|
||||||
|
body.add_child(cs)
|
||||||
|
var dark := MeshInstance3D.new()
|
||||||
|
var q := QuadMesh.new()
|
||||||
|
q.size = Vector2(2.2, 2.8)
|
||||||
|
dark.mesh = q
|
||||||
|
var m := StandardMaterial3D.new()
|
||||||
|
m.albedo_color = Color(0.02, 0.02, 0.025)
|
||||||
|
m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
dark.material_override = m
|
||||||
|
dark.position = Vector3(0, 1.4, 2.08)
|
||||||
|
dark.rotation.y = PI
|
||||||
|
add_child(dark)
|
||||||
|
# What you press E on: the opening (interactables layer: you walk into it).
|
||||||
|
var spot := StaticBody3D.new()
|
||||||
|
spot.collision_layer = 2
|
||||||
|
spot.collision_mask = 0
|
||||||
|
var cs2 := CollisionShape3D.new()
|
||||||
|
var b2 := BoxShape3D.new()
|
||||||
|
b2.size = Vector3(2.2, 2.8, 1.6)
|
||||||
|
cs2.shape = b2
|
||||||
|
spot.add_child(cs2)
|
||||||
|
spot.position = Vector3(0, 1.4, 1.2)
|
||||||
|
spot.set_script(preload("res://scripts/world/sea_cave_spot.gd"))
|
||||||
|
spot.cave = self
|
||||||
|
add_child(spot)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user