W3: the Cove (bay, pond, pier for sea fishing), mackerel and cod (cod only in deep water), non-start worldlets offered for testing when run from source
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
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"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,9 +1,33 @@
|
|||||||
# Worldlets design
|
# Worldlets design
|
||||||
|
|
||||||
Status: **W0, W1 and W2 built**; the rest is planned. The journey between
|
Status: **W0-W3 built** (the Cove, without its stream yet); the rest is
|
||||||
|
planned. The journey between
|
||||||
worldlets is §10, and the two ways to play (the journey, or one land) are in
|
worldlets is §10, and the two ways to play (the journey, or one land) are in
|
||||||
its decisions.
|
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.
|
||||||
|
- Running from the source, the New World and Host pages also offer
|
||||||
|
worldlets that aren't starts, marked "(testing)", so the Cove can be
|
||||||
|
played before the tunnel exists. Release builds offer only the Valley.
|
||||||
|
- 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:*
|
*As built in W2:*
|
||||||
- `lake` blocks can repeat: every lake and pond is its own body
|
- `lake` blocks can repeat: every lake and pond is its own body
|
||||||
(`terrain.lakes`, `world.water_bodies()`), all at one surface
|
(`terrain.lakes`, `world.water_bodies()`), all at one surface
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -91,8 +92,9 @@ func _next_wait() -> float:
|
|||||||
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:
|
||||||
@@ -120,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"
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -694,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)
|
||||||
|
|
||||||
|
|||||||
@@ -698,7 +698,7 @@ var _host_worldlet: OptionButton
|
|||||||
## description under it. (Only the Valley today: the others are reached on
|
## description under it. (Only the Valley today: the others are reached on
|
||||||
## the journey, so the list is short and the picker sits still.)
|
## the journey, so the list is short and the picker sits still.)
|
||||||
func _worldlet_picker(v: VBoxContainer) -> OptionButton:
|
func _worldlet_picker(v: VBoxContainer) -> OptionButton:
|
||||||
var starts: Array = Terrain.starting_worldlets()
|
var starts: Array = offered_worldlets(OS.is_debug_build())
|
||||||
v.add_child(UITheme.label("Where", 15, UITheme.TEXT_DIM))
|
v.add_child(UITheme.label("Where", 15, UITheme.TEXT_DIM))
|
||||||
var pick := OptionButton.new()
|
var pick := OptionButton.new()
|
||||||
pick.custom_minimum_size = Vector2(360, 36)
|
pick.custom_minimum_size = Vector2(360, 36)
|
||||||
@@ -716,6 +716,19 @@ func _worldlet_picker(v: VBoxContainer) -> OptionButton:
|
|||||||
about.text = String(Terrain.load_worldlet(String(pick.get_item_metadata(i))).get("description", "")))
|
about.text = String(Terrain.load_worldlet(String(pick.get_item_metadata(i))).get("description", "")))
|
||||||
return pick
|
return pick
|
||||||
|
|
||||||
|
## The worldlets offered for a new world: the starts, and (running from the
|
||||||
|
## source, `debug`) the rest too, marked for testing, so a worldlet can be
|
||||||
|
## tried before the journey reaches it. A release build offers only the starts.
|
||||||
|
static func offered_worldlets(debug: bool) -> Array:
|
||||||
|
var out: Array = Terrain.starting_worldlets()
|
||||||
|
if debug:
|
||||||
|
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
|
## 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.
|
## seed, so once it exists the picker shows that one and stays put.
|
||||||
func _show_host_worldlet(existing: bool, meta_path: String) -> void:
|
func _show_host_worldlet(existing: bool, meta_path: String) -> void:
|
||||||
|
|||||||
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,7 @@ 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 _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 +418,10 @@ 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
|
||||||
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 +490,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 +676,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:
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ var _spawn := Vector3.ZERO
|
|||||||
var _spawn_yaw := 0.0
|
var _spawn_yaw := 0.0
|
||||||
var _pieces: Node3D
|
var _pieces: Node3D
|
||||||
var hud: CanvasLayer
|
var hud: CanvasLayer
|
||||||
|
var pier: Node3D ## the Cove's pier (scripts/world/pier.gd), or null
|
||||||
var _session_time := 0.0 ## seconds played since the last save
|
var _session_time := 0.0 ## seconds played since the last save
|
||||||
var _autosave_left := AUTOSAVE_SECONDS
|
var _autosave_left := AUTOSAVE_SECONDS
|
||||||
var _menu_cam: Camera3D
|
var _menu_cam: Camera3D
|
||||||
@@ -352,6 +353,15 @@ func _on_terrain_ready() -> void:
|
|||||||
var to_lake := lake - Vector2(_spawn.x, _spawn.z)
|
var to_lake := lake - Vector2(_spawn.x, _spawn.z)
|
||||||
_spawn_yaw = atan2(-to_lake.x, -to_lake.y)
|
_spawn_yaw = atan2(-to_lake.x, -to_lake.y)
|
||||||
player.water_level = terrain.water_level
|
player.water_level = terrain.water_level
|
||||||
|
# The pier (W3): off a beach, laid before the scenery so it keeps clear.
|
||||||
|
var pr := preload("res://scripts/world/pier.gd").new()
|
||||||
|
pr.name = "Pier"
|
||||||
|
add_child(pr)
|
||||||
|
if pr.place(terrain):
|
||||||
|
pier = pr
|
||||||
|
props.pier = pr
|
||||||
|
else:
|
||||||
|
pr.free()
|
||||||
props.place_on(terrain, player if not menu_mode else _menu_cam, _spawn)
|
props.place_on(terrain, player if not menu_mode else _menu_cam, _spawn)
|
||||||
# The wolf den: part of the scenery, so it's there from day 1 (empty).
|
# The wolf den: part of the scenery, so it's there from day 1 (empty).
|
||||||
den = preload("res://scripts/creatures/den.gd").new()
|
den = preload("res://scripts/creatures/den.gd").new()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ if [ ! -f .godot/global_script_class_cache.cfg ]; then
|
|||||||
echo "Importing the project first (fresh copy)..."
|
echo "Importing the project first (fresh copy)..."
|
||||||
godot --headless --path . --import >/dev/null 2>&1
|
godot --headless --path . --import >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_host test_tutorial test_tutorial_full test_hints test_updates test_audio test_items_more test_worldlets test_worldlet_pick test_water_bodies test_terrain_edges test_boundary; do
|
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_host test_tutorial test_tutorial_full test_hints test_updates test_audio test_items_more test_worldlets test_worldlet_pick test_water_bodies test_cove test_terrain_edges test_boundary; do
|
||||||
extra=""
|
extra=""
|
||||||
[ "$t" = "test_boundary" ] && extra="--fixed-fps 30"
|
[ "$t" = "test_boundary" ] && extra="--fixed-fps 30"
|
||||||
# A suite that waits on a world that failed to load would hang forever,
|
# A suite that waits on a world that failed to load would hang forever,
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ func _run() -> void:
|
|||||||
print("a beach (testbay)")
|
print("a beach (testbay)")
|
||||||
S.current_id = S.create_world("Bay", 1337, {"worldlet": "testbay"})
|
S.current_id = S.create_world("Bay", 1337, {"worldlet": "testbay"})
|
||||||
await _escape_all(BAY_DIRECTIONS, 90.0)
|
await _escape_all(BAY_DIRECTIONS, 90.0)
|
||||||
|
print("the Cove (all round)")
|
||||||
|
S.current_id = S.create_world("Cove", 1337, {"worldlet": "cove"})
|
||||||
|
await _escape_all(DIRECTIONS, -1.0)
|
||||||
for w in S.list_worlds():
|
for w in S.list_worlds():
|
||||||
S.delete_world(w.id)
|
S.delete_world(w.id)
|
||||||
DirAccess.remove_absolute(S.root_dir)
|
DirAccess.remove_absolute(S.root_dir)
|
||||||
|
|||||||
175
tests/test_cove.gd
Normal file
175
tests/test_cove.gd
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## W3 (docs/WORLDLETS_DESIGN.md §5): the Cove. A world made there has its
|
||||||
|
## bay, a pond and a pier; you walk from the sand onto the pier, cast off its
|
||||||
|
## end into the sea and catch a mackerel; cod only bite in deeper water; the
|
||||||
|
## sea fish cook like the lake's. The Cove isn't a start (you reach it on
|
||||||
|
## the journey), and its ways on are back to the Valley and on by sail.
|
||||||
|
## Run: godot --headless --path . --script res://tests/test_cove.gd
|
||||||
|
|
||||||
|
var _fails := 0
|
||||||
|
var _finished := false
|
||||||
|
var A: Node
|
||||||
|
var S: Node
|
||||||
|
var DB: Node
|
||||||
|
|
||||||
|
func check(cond: bool, what: String) -> void:
|
||||||
|
print((" ok " if cond else " FAIL ") + what)
|
||||||
|
if not cond:
|
||||||
|
_fails += 1
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
_run.call_deferred()
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
A = root.get_node("Actions")
|
||||||
|
S = root.get_node("Saves")
|
||||||
|
DB = root.get_node("ItemDB")
|
||||||
|
S.root_dir = "user://test_cove_worlds"
|
||||||
|
for w in S.list_worlds():
|
||||||
|
S.delete_world(w.id)
|
||||||
|
await _body()
|
||||||
|
check(_finished, "test ran to the end (no script errors)")
|
||||||
|
for w in S.list_worlds():
|
||||||
|
S.delete_world(w.id)
|
||||||
|
DirAccess.remove_absolute(S.root_dir)
|
||||||
|
print("RESULT: %s (%d failures)" % ["PASS" if _fails == 0 else "FAIL", _fails])
|
||||||
|
quit(1 if _fails > 0 else 0)
|
||||||
|
|
||||||
|
func _body() -> void:
|
||||||
|
var T: Script = load("res://scripts/world/terrain.gd")
|
||||||
|
print("the Cove")
|
||||||
|
var def: Dictionary = T.load_worldlet("cove")
|
||||||
|
check(def.get("name", "") == "Cove" and not bool(def.get("start", true)), "a worldlet, not a start (reached on the journey)")
|
||||||
|
S.current_id = S.create_world("Cove", 1337, {"worldlet": "cove"})
|
||||||
|
var world: Node3D = load("res://scenes/world.tscn").instantiate()
|
||||||
|
root.add_child(world)
|
||||||
|
for _i in range(8):
|
||||||
|
await physics_frame
|
||||||
|
var t: Node3D = world.terrain
|
||||||
|
var player: CharacterBody3D = world.player
|
||||||
|
world.day_night.frozen = true
|
||||||
|
world.weather.force("clear")
|
||||||
|
world.creatures.enabled = false
|
||||||
|
world.day_night.time = 0.5
|
||||||
|
world.day_night.apply(true)
|
||||||
|
player.survival.set_process(false)
|
||||||
|
check(world.worldlet_id() == "cove", "a world made in the Cove is the Cove")
|
||||||
|
var bodies: Array = world.water_bodies().map(func(b): return b.type)
|
||||||
|
check(bodies.has("sea") and bodies.has("lake"), "with the sea and a pond (%s)" % [bodies])
|
||||||
|
check(world.crossings().any(func(c): return c.to == "valley") and world.crossings().any(func(c): return c.by == "sail"),
|
||||||
|
"its ways on: back to the Valley, and on by sail")
|
||||||
|
check(world._spawn.y > t.water_level + 1.0, "you start on dry land")
|
||||||
|
check(world.den != null and world.barrow != null and world.stones != null, "the den, the barrow and the stones found places")
|
||||||
|
check(t.get_node_or_null("Water") == null, "one water surface (the bay is the sea)")
|
||||||
|
|
||||||
|
print("the pier")
|
||||||
|
var pier: Node3D = world.pier
|
||||||
|
check(pier != null and pier.is_in_group("pier"), "a pier")
|
||||||
|
if pier == null:
|
||||||
|
return
|
||||||
|
var len: float = pier.start.distance_to(pier.end)
|
||||||
|
check(len > 20.0, "running out over the bay (%.0f m)" % len)
|
||||||
|
var at_end: Dictionary = world.water_at(pier.end.x, pier.end.z)
|
||||||
|
check(at_end.get("type", "") == "sea" and float(at_end.depth) > 1.0, "into the sea (%.1f m deep at the end)" % float(at_end.get("depth", 0.0)))
|
||||||
|
check(absf(t.sample_height(pier.start.x, pier.start.z) - pier.start.y) < 0.3, "starting at the sand, so you walk straight on")
|
||||||
|
# Walk on from the sand: the deck holds you up above the water.
|
||||||
|
var on: Vector3 = pier.start - pier.dir * 1.5
|
||||||
|
on.y = t.sample_height(on.x, on.z) + 0.2
|
||||||
|
player.global_position = on
|
||||||
|
player.velocity = Vector3.ZERO
|
||||||
|
player.rotation = Vector3(0.0, atan2(-pier.dir.x, -pier.dir.z), 0.0)
|
||||||
|
Input.action_press("move_forward")
|
||||||
|
for _i in range(int(4.0 * Engine.physics_ticks_per_second)):
|
||||||
|
await physics_frame
|
||||||
|
Input.action_release("move_forward")
|
||||||
|
var p: Vector3 = player.global_position
|
||||||
|
var along: float = (p - pier.start).dot(pier.dir)
|
||||||
|
check(along > 10.0 and p.y > t.water_level + 0.5, "walk on from the beach and along it, dry (%.0f m out, %.1f m above the water)" % [along, p.y - t.water_level])
|
||||||
|
check(not world._in_current, "(no current up on the deck)")
|
||||||
|
|
||||||
|
print("sea fishing")
|
||||||
|
player.set_physics_process(false)
|
||||||
|
player.global_position = pier.end - pier.dir * 0.8
|
||||||
|
player.inventory.add("fishing_rod", 1)
|
||||||
|
_hold(player, "fishing_rod")
|
||||||
|
var out: Vector3 = pier.end + pier.dir * 8.0
|
||||||
|
out.y = t.water_level
|
||||||
|
var got := {}
|
||||||
|
var bobs := 0
|
||||||
|
for _try in range(25):
|
||||||
|
player.survival.stamina = 100.0
|
||||||
|
if not _cast(world, player, out):
|
||||||
|
break
|
||||||
|
var b := _bob_until(world, "floating", 5.0)
|
||||||
|
if b == null or b.state != "floating":
|
||||||
|
break
|
||||||
|
bobs += 1
|
||||||
|
if bobs == 1: check(b.water_type == "sea", "the float lands on the sea (%.1f m deep)" % b.water_depth)
|
||||||
|
b = _bob_until(world, "biting", 60.0)
|
||||||
|
if b == null or b.state != "biting":
|
||||||
|
break
|
||||||
|
var fish: String = b.fish
|
||||||
|
var before: int = player.inventory.count(fish)
|
||||||
|
if A.reel(player, world) and player.inventory.count(fish) == before + 1:
|
||||||
|
got[fish] = int(got.get(fish, 0)) + 1
|
||||||
|
if got.has("mackerel") and _try >= 6:
|
||||||
|
break
|
||||||
|
check(got.has("mackerel"), "you catch a mackerel off the pier (%s)" % [got])
|
||||||
|
check(got.keys().all(func(k): return k in ["mackerel", "cod"]), "(sea fish only: no perch in the sea)")
|
||||||
|
|
||||||
|
print("cod like it deep")
|
||||||
|
var bob: Node3D = load("res://scripts/fishing/bobber.gd").new()
|
||||||
|
bob.table = DB.water_fishing("sea")
|
||||||
|
bob.world = world
|
||||||
|
bob.rng.seed = 5
|
||||||
|
var shallow := {}
|
||||||
|
var deep := {}
|
||||||
|
for i in range(200):
|
||||||
|
bob.water_depth = 1.5
|
||||||
|
shallow[bob._pick_fish()] = true
|
||||||
|
bob.water_depth = 3.0
|
||||||
|
deep[bob._pick_fish()] = true
|
||||||
|
check(not shallow.has("cod") and shallow.has("mackerel"), "in shallow sea only mackerel bite (%s)" % [shallow.keys()])
|
||||||
|
check(deep.has("cod") and deep.has("mackerel"), "cast out deep and cod bite too")
|
||||||
|
bob.free()
|
||||||
|
|
||||||
|
print("cooking them")
|
||||||
|
for fish in ["mackerel", "cod"]:
|
||||||
|
var r: Dictionary = DB.recipes.get("grilled_" + fish, {})
|
||||||
|
check(r.get("station", "") == "campfire" and r.get("inputs", {}).has(fish) and not DB.get_item("grilled_" + fish).is_empty(),
|
||||||
|
"grilled %s, at a campfire" % fish)
|
||||||
|
check(DB.recipes.get("salted_" + fish, {}).get("output", "") == "salted_fish", "and %s salts into salted fish" % fish)
|
||||||
|
check(not DB.get_item("mackerel").get("food", {}).is_empty() and DB.get_item("cod").get("food", {}).get("diet", {}).has("fish"),
|
||||||
|
"the raw fish count as fish in your diet")
|
||||||
|
world.queue_free()
|
||||||
|
await process_frame
|
||||||
|
_finished = true
|
||||||
|
|
||||||
|
func _cast(world: Node3D, player: Node3D, toward: Vector3) -> bool:
|
||||||
|
var origin: Vector3 = player.global_position + Vector3(0, 1.6, 0)
|
||||||
|
return A.cast_line(player, origin, toward - origin, world)
|
||||||
|
|
||||||
|
## Steps the float until it reaches `want` (or `secs` pass). Returns it.
|
||||||
|
func _bob_until(world: Node3D, want: String, secs: float) -> Node3D:
|
||||||
|
var tt := 0.0
|
||||||
|
while tt < secs:
|
||||||
|
var b: Node3D = world.bobber
|
||||||
|
if b == null or not is_instance_valid(b):
|
||||||
|
return null
|
||||||
|
if b.state == want:
|
||||||
|
return b
|
||||||
|
b.set_physics_process(false)
|
||||||
|
b.step(0.05)
|
||||||
|
tt += 0.05
|
||||||
|
return world.bobber
|
||||||
|
|
||||||
|
func _hold(player: Node, id: String) -> void:
|
||||||
|
var inv: Inventory = player.inventory
|
||||||
|
for i in range(inv.size()):
|
||||||
|
var s = inv.get_slot(i)
|
||||||
|
if s != null and s.id == id:
|
||||||
|
var tmp = inv.get_slot(0)
|
||||||
|
inv.set_slot(0, s)
|
||||||
|
inv.set_slot(i, tmp)
|
||||||
|
break
|
||||||
|
player.select(0)
|
||||||
1
tests/test_cove.gd.uid
Normal file
1
tests/test_cove.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cso32qbo6jvw3
|
||||||
@@ -118,7 +118,8 @@ func _body(world: Node3D) -> void:
|
|||||||
for f in lt.get("fish", []):
|
for f in lt.get("fish", []):
|
||||||
ids.append(f.id)
|
ids.append(f.id)
|
||||||
check(ids == ["perch", "trout"] and lt.min_depth == 0.5, "the lake's fish come from data/fish.json (%s)" % [ids])
|
check(ids == ["perch", "trout"] and lt.min_depth == 0.5, "the lake's fish come from data/fish.json (%s)" % [ids])
|
||||||
check(DB.water_fishing("sea").is_empty(), "this island has no sea fish defined (the cliffs are in the way)")
|
var st: Dictionary = DB.water_fishing("sea")
|
||||||
|
check(st.get("fish", []).map(func(f): return f.id) == ["mackerel", "cod"], "the sea's fish: mackerel and cod (caught in the Cove; tests/test_cove.gd)")
|
||||||
|
|
||||||
print("casting")
|
print("casting")
|
||||||
sv.stamina = 100.0
|
sv.stamina = 100.0
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func _initialize() -> void:
|
|||||||
|
|
||||||
## Each worldlet's edges (W2): the Valley, and a test worldlet with two
|
## Each worldlet's edges (W2): the Valley, and a test worldlet with two
|
||||||
## ponds and a beach (tests/worldlets/testbay.json).
|
## ponds and a beach (tests/worldlets/testbay.json).
|
||||||
const WORLDLETS := ["valley", "testbay"]
|
const WORLDLETS := ["valley", "cove", "testbay"]
|
||||||
|
|
||||||
func _all() -> void:
|
func _all() -> void:
|
||||||
var failed := 0
|
var failed := 0
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func _body(S: Node, Net: Node) -> void:
|
|||||||
var TS: Script = load("res://scripts/ui/title_screen.gd")
|
var TS: Script = load("res://scripts/ui/title_screen.gd")
|
||||||
print("the worldlets to start in")
|
print("the worldlets to start in")
|
||||||
var starts: Array = T.starting_worldlets()
|
var starts: Array = T.starting_worldlets()
|
||||||
check(starts.size() == 1 and String(starts[0].id) == "valley", "only the Valley (%s)" % [starts.map(func(w): return w.id)])
|
check(starts.size() == 1 and String(starts[0].id) == "valley", "only the Valley (%s; the Cove is reached on the journey)" % [starts.map(func(w): return w.id)])
|
||||||
check(T.worldlets().all(func(w): return w.has("start")), "every worldlet says whether it's a start")
|
check(T.worldlets().all(func(w): return w.has("start")), "every worldlet says whether it's a start")
|
||||||
check(T.worldlet_name("valley") == "Valley", "the Valley's name, for the menus")
|
check(T.worldlet_name("valley") == "Valley", "the Valley's name, for the menus")
|
||||||
check(T.worldlet_name("nowhere") == "nowhere", "(an unknown one: its id)")
|
check(T.worldlet_name("nowhere") == "nowhere", "(an unknown one: its id)")
|
||||||
@@ -51,9 +51,14 @@ func _body(S: Node, Net: Node) -> void:
|
|||||||
root.add_child(title)
|
root.add_child(title)
|
||||||
for _i in range(3):
|
for _i in range(3):
|
||||||
await process_frame
|
await process_frame
|
||||||
|
var release: Array = TS.offered_worldlets(false)
|
||||||
|
check(release.size() == 1 and String(release[0].id) == "valley", "a release build offers only the Valley")
|
||||||
|
var dev: Array = TS.offered_worldlets(true)
|
||||||
|
check(String(dev[0].id) == "valley" and dev.any(func(w): return w.id == "cove" and String(w.name) == "Cove (testing)"),
|
||||||
|
"running from the source, the Cove too, marked for testing (%s)" % [dev.map(func(w): return w.name)])
|
||||||
var pick: OptionButton = title._worldlet_pick
|
var pick: OptionButton = title._worldlet_pick
|
||||||
check(pick != null and pick.item_count == 1 and pick.get_item_text(0) == "Valley", "a \"Where\" picker with the Valley in it")
|
check(pick != null and pick.item_count == dev.size() and pick.get_item_text(0) == "Valley", "a \"Where\" picker, on the Valley first")
|
||||||
check(pick.disabled, "which sits still with only one choice")
|
check(not pick.disabled, "(with more than one to choose from)")
|
||||||
check(TS.picked_worldlet(pick) == "valley", "and gives the Valley")
|
check(TS.picked_worldlet(pick) == "valley", "and gives the Valley")
|
||||||
var id: String = S.create_world("Pick test", 4242, {"worldlet": TS.picked_worldlet(pick)})
|
var id: String = S.create_world("Pick test", 4242, {"worldlet": TS.picked_worldlet(pick)})
|
||||||
var meta: Dictionary = S.list_worlds().filter(func(w): return w.id == id)[0] if not id.is_empty() else {}
|
var meta: Dictionary = S.list_worlds().filter(func(w): return w.id == id)[0] if not id.is_empty() else {}
|
||||||
@@ -69,7 +74,7 @@ func _body(S: Node, Net: Node) -> void:
|
|||||||
check(hp != null and hp.get_item_text(hp.selected) == "Valley", "a \"Where\" picker too, on the Valley")
|
check(hp != null and hp.get_item_text(hp.selected) == "Valley", "a \"Where\" picker too, on the Valley")
|
||||||
title._show("host")
|
title._show("host")
|
||||||
await process_frame
|
await process_frame
|
||||||
check(hp.disabled and TS.picked_worldlet(hp) == "valley", "a new island: the Valley (the only start)")
|
check(not hp.disabled and TS.picked_worldlet(hp) == "valley", "a new island: the Valley to start with, free to change")
|
||||||
# Once the island exists it keeps its worldlet: the picker shows that one.
|
# Once the island exists it keeps its worldlet: the picker shows that one.
|
||||||
DirAccess.make_dir_recursive_absolute(DIR + "/server/worlds/server-world")
|
DirAccess.make_dir_recursive_absolute(DIR + "/server/worlds/server-world")
|
||||||
var f := FileAccess.open(DIR + "/server/worlds/server-world/meta.json", FileAccess.WRITE)
|
var f := FileAccess.open(DIR + "/server/worlds/server-world/meta.json", FileAccess.WRITE)
|
||||||
|
|||||||
Reference in New Issue
Block a user