The Cove's wreck: driftwood and salt on the beach, the captain's sea chest (a treasure map, nails, salt; once each), each player's own X on the map; online too
This commit is contained in:
@@ -1073,6 +1073,26 @@
|
|||||||
"description": "Crusted on the rocks above the sea cliffs. Cures meat and fish so they keep for weeks.",
|
"description": "Crusted on the rocks above the sea cliffs. Cures meat and fish so they keep for weeks.",
|
||||||
"mesh": "salt"
|
"mesh": "salt"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "driftwood",
|
||||||
|
"name": "Driftwood",
|
||||||
|
"category": "resource",
|
||||||
|
"stack": 30,
|
||||||
|
"description": "Sea-bleached wood washed up on the Cove's beach. It burns well, and it's light and tough: what a boat will be built from.",
|
||||||
|
"mesh": "driftwood",
|
||||||
|
"fuel": 48.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "treasure_map",
|
||||||
|
"name": "Treasure Map",
|
||||||
|
"category": "tool",
|
||||||
|
"stack": 1,
|
||||||
|
"description": "A torn chart from a wrecked ship's sea chest, with an X inked in the hills above the Cove. Right-click to read it: the X goes on your map (N).",
|
||||||
|
"mesh": "treasure_map",
|
||||||
|
"read": {
|
||||||
|
"marks": "treasure"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "smoking_rack",
|
"id": "smoking_rack",
|
||||||
"name": "Smoking Rack",
|
"name": "Smoking Rack",
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
{"type": "ocean_edge"}
|
{"type": "ocean_edge"}
|
||||||
],
|
],
|
||||||
"climate": {"snowline": 42.0},
|
"climate": {"snowline": 42.0},
|
||||||
"places": [{"type": "den"}, {"type": "pier", "at": "beach"}],
|
"places": [{"type": "den"}, {"type": "pier", "at": "beach"}, {"type": "wreck"}],
|
||||||
"next": [
|
"next": [
|
||||||
{"to": "valley", "by": "tunnel", "where": "sea_cave"},
|
{"to": "valley", "by": "tunnel", "where": "sea_cave"},
|
||||||
{"to": "highlands", "by": "sail", "where": "pier"}
|
{"to": "highlands", "by": "sail", "where": "pier"}
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ static func build(key: String) -> ArrayMesh:
|
|||||||
var m: ArrayMesh
|
var m: ArrayMesh
|
||||||
match key:
|
match key:
|
||||||
"stick": m = _stick()
|
"stick": m = _stick()
|
||||||
|
"driftwood": m = _driftwood()
|
||||||
|
"treasure_map": m = _treasure_map()
|
||||||
"stone": m = _pebble(11, STONE, Vector3(0.14, 0.10, 0.12))
|
"stone": m = _pebble(11, STONE, Vector3(0.14, 0.10, 0.12))
|
||||||
"flint": m = _pebble(23, FLINT, Vector3(0.12, 0.07, 0.15), 0.5)
|
"flint": m = _pebble(23, FLINT, Vector3(0.12, 0.07, 0.15), 0.5)
|
||||||
"wood": m = _wood()
|
"wood": m = _wood()
|
||||||
@@ -184,6 +186,26 @@ static func _stick() -> ArrayMesh:
|
|||||||
P._cylinder(st, Vector3(0.05, 0.03, 0.0), Vector3(0.2, 0.05, 0.1), 0.012, 0.006, 4, HANDLE, r)
|
P._cylinder(st, Vector3(0.05, 0.03, 0.0), Vector3(0.2, 0.05, 0.1), 0.012, 0.006, 4, HANDLE, r)
|
||||||
return P._commit(st)
|
return P._commit(st)
|
||||||
|
|
||||||
|
## A sea-bleached branch, twisted, with a stub of a side branch.
|
||||||
|
static func _driftwood() -> ArrayMesh:
|
||||||
|
var st := P._begin()
|
||||||
|
var r := _rng(41)
|
||||||
|
const BLEACHED := Color(0.72, 0.68, 0.6)
|
||||||
|
const GREY := Color(0.58, 0.55, 0.5)
|
||||||
|
P._cylinder(st, Vector3(-0.5, 0.06, -0.04), Vector3(0.05, 0.08, 0.05), 0.07, 0.06, 6, BLEACHED, r, -1.0, GREY)
|
||||||
|
P._cylinder(st, Vector3(0.05, 0.08, 0.05), Vector3(0.48, 0.05, -0.02), 0.06, 0.035, 6, BLEACHED, r, -1.0, GREY)
|
||||||
|
P._cylinder(st, Vector3(0.1, 0.09, 0.05), Vector3(0.22, 0.12, 0.24), 0.03, 0.015, 5, GREY, r)
|
||||||
|
return P._commit(st)
|
||||||
|
|
||||||
|
## A rolled-up, torn sheet of old parchment tied with string.
|
||||||
|
static func _treasure_map() -> ArrayMesh:
|
||||||
|
var st := P._begin()
|
||||||
|
var r := _rng(43)
|
||||||
|
const PARCH := Color(0.82, 0.74, 0.55)
|
||||||
|
P._cylinder(st, Vector3(-0.16, 0.04, 0), Vector3(0.16, 0.04, 0), 0.04, 0.04, 8, PARCH, r, -1.0, PARCH.darkened(0.15))
|
||||||
|
P._cylinder(st, Vector3(-0.01, 0.04, 0), Vector3(0.01, 0.04, 0), 0.043, 0.043, 8, Color(0.45, 0.12, 0.08), r)
|
||||||
|
return P._commit(st)
|
||||||
|
|
||||||
static func _wood() -> ArrayMesh:
|
static func _wood() -> ArrayMesh:
|
||||||
var st := P._begin()
|
var st := P._begin()
|
||||||
var r := _rng(9)
|
var r := _rng(9)
|
||||||
|
|||||||
@@ -363,6 +363,11 @@ func _run(p: Node3D, what: String, a: Array) -> void:
|
|||||||
"tunnel_prop":
|
"tunnel_prop":
|
||||||
if w.tunnel != null:
|
if w.tunnel != null:
|
||||||
w.tunnel.set_prop(p, _i(a, 0))
|
w.tunnel.set_prop(p, _i(a, 0))
|
||||||
|
"wreck_chest":
|
||||||
|
# The wreck's sea chest: each player's share, once.
|
||||||
|
if w.wreck != null and p.global_position.distance_to(w.wreck.chest.global_position) < 4.0:
|
||||||
|
if w.wreck.open_for(p, String(p.player_id)):
|
||||||
|
WorldNet.wreck_state(w.wreck.to_data())
|
||||||
"tunnel_go":
|
"tunnel_go":
|
||||||
# Down into the cliff tunnel, or back out at the top: a teleport
|
# Down into the cliff tunnel, or back out at the top: a teleport
|
||||||
# the server makes (the tunnel's under the island), from nearby.
|
# the server makes (the tunnel's under the island), from nearby.
|
||||||
|
|||||||
@@ -377,6 +377,18 @@ func tunnel_state(d: Dictionary) -> void:
|
|||||||
if Lands.hub:
|
if Lands.hub:
|
||||||
Lands.push_tunnel(int(d.get("dug", 0)))
|
Lands.push_tunnel(int(d.get("dug", 0)))
|
||||||
|
|
||||||
|
## Who's had their share of the wreck's sea chest changed: everyone here
|
||||||
|
## sees whether it's empty for them.
|
||||||
|
func wreck_state(d: Dictionary) -> void:
|
||||||
|
_all("_s2c_wreck", [d])
|
||||||
|
mark_changed()
|
||||||
|
|
||||||
|
@rpc("authority", "call_remote", "reliable")
|
||||||
|
func _s2c_wreck(d: Dictionary) -> void:
|
||||||
|
if not is_client() or world.wreck == null:
|
||||||
|
return
|
||||||
|
world.wreck.load_data(d)
|
||||||
|
|
||||||
@rpc("authority", "call_remote", "reliable")
|
@rpc("authority", "call_remote", "reliable")
|
||||||
func _s2c_tunnel(d: Dictionary) -> void:
|
func _s2c_tunnel(d: Dictionary) -> void:
|
||||||
if not is_client() or world.tunnel == null:
|
if not is_client() or world.tunnel == null:
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ func _physics_process(delta: float) -> void:
|
|||||||
_set_prompt("[RMB] Eat %s" % def.name)
|
_set_prompt("[RMB] Eat %s" % def.name)
|
||||||
elif def.has("wear"):
|
elif def.has("wear"):
|
||||||
_set_prompt("[RMB] Wear %s" % def.name)
|
_set_prompt("[RMB] Wear %s" % def.name)
|
||||||
|
elif def.has("read"):
|
||||||
|
_set_prompt("[RMB] Read the %s" % String(def.name).to_lower())
|
||||||
elif def.has("bow"):
|
elif def.has("bow"):
|
||||||
var n: int = Actions.ammo_count(player, def.bow)
|
var n: int = Actions.ammo_count(player, def.bow)
|
||||||
_set_prompt("Drawing %d%%" % roundi(draw_power() * 100.0) if _drawing else "[hold LMB] Draw (%d arrows)" % n)
|
_set_prompt("Drawing %d%%" % roundi(draw_power() * 100.0) if _drawing else "[hold LMB] Draw (%d arrows)" % n)
|
||||||
@@ -262,6 +264,8 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||||||
Actions.eat(player, player.selected)
|
Actions.eat(player, player.selected)
|
||||||
elif d.has("wear"):
|
elif d.has("wear"):
|
||||||
Actions.wear(player, player.selected)
|
Actions.wear(player, player.selected)
|
||||||
|
elif d.has("read"):
|
||||||
|
player.get_parent().read_map(player) # (the treasure map: your X, on your map)
|
||||||
elif d.get("weapon", {}).has("throw"):
|
elif d.get("weapon", {}).has("throw"):
|
||||||
_throw()
|
_throw()
|
||||||
elif event.is_action_pressed("rotate_piece"):
|
elif event.is_action_pressed("rotate_piece"):
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ const MARKS := {
|
|||||||
"build": [Color(0.62, 0.45, 0.28), "Your buildings"],
|
"build": [Color(0.62, 0.45, 0.28), "Your buildings"],
|
||||||
"camp": [Color(1.00, 0.80, 0.35), "Camp"],
|
"camp": [Color(1.00, 0.80, 0.35), "Camp"],
|
||||||
"grave": [Color(0.85, 0.85, 0.85), "Your things"],
|
"grave": [Color(0.85, 0.85, 0.85), "Your things"],
|
||||||
|
"wreck": [Color(0.55, 0.38, 0.22), "The wreck"],
|
||||||
|
"treasure": [Color(0.9, 0.12, 0.1), "X: your treasure"],
|
||||||
}
|
}
|
||||||
|
|
||||||
var world: Node3D
|
var world: Node3D
|
||||||
@@ -132,10 +134,16 @@ func _draw_marks() -> void:
|
|||||||
for m in world.map_marks():
|
for m in world.map_marks():
|
||||||
var at := _to_map(m.pos)
|
var at := _to_map(m.pos)
|
||||||
var col: Color = MARKS.get(m.kind, [Color.WHITE])[0]
|
var col: Color = MARKS.get(m.kind, [Color.WHITE])[0]
|
||||||
|
if m.kind == "treasure":
|
||||||
|
# An inked X, as on the old chart.
|
||||||
|
for d in [Vector2(1, 1), Vector2(1, -1)]:
|
||||||
|
_marks.draw_line(at - d * 8.0, at + d * 8.0, Color(0, 0, 0, 0.8), 5.0)
|
||||||
|
_marks.draw_line(at - d * 8.0, at + d * 8.0, col, 3.0)
|
||||||
|
continue
|
||||||
var r := 3.0 if m.kind == "build" else 6.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), col)
|
||||||
_marks.draw_rect(Rect2(at - Vector2(r, r), Vector2(r, r) * 2.0), Color(0, 0, 0, 0.7), false, 1.0)
|
_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"]:
|
if m.kind in ["den", "barrow", "stones", "seam", "pier", "cave", "wreck"]:
|
||||||
_marks.draw_string(font, at + Vector2(9, 5), String(m.label), HORIZONTAL_ALIGNMENT_LEFT, -1, 13, col)
|
_marks.draw_string(font, at + Vector2(9, 5), String(m.label), HORIZONTAL_ALIGNMENT_LEFT, -1, 13, col)
|
||||||
# Other players in this land: only on a co-op server (with PvP on, the
|
# Other players in this land: only on a co-op server (with PvP on, the
|
||||||
# map never gives anyone away). Green dots with their names.
|
# map never gives anyone away). Green dots with their names.
|
||||||
|
|||||||
@@ -664,9 +664,9 @@ func _scatter_pickups() -> void:
|
|||||||
rng.seed = world_seed + 6
|
rng.seed = world_seed + 6
|
||||||
# Order matters: new kinds go at the END so existing saves keep their ids.
|
# Order matters: new kinds go at the END so existing saves keep their ids.
|
||||||
var want := {"stick": 260, "stone": 170, "flint": 80, "mushroom": 110, "feather": 70, "fiber": 150,
|
var want := {"stick": 260, "stone": 170, "flint": 80, "mushroom": 110, "feather": 70, "fiber": 150,
|
||||||
"iron_nugget": 70, "clay": 60, "salt": 40}
|
"iron_nugget": 70, "clay": 60, "salt": 40, "driftwood": 45}
|
||||||
var radius: Dictionary = {"stick": 0.35, "stone": 0.25, "flint": 0.25, "mushroom": 0.25, "feather": 0.25, "fiber": 0.45,
|
var radius: Dictionary = {"stick": 0.35, "stone": 0.25, "flint": 0.25, "mushroom": 0.25, "feather": 0.25, "fiber": 0.45,
|
||||||
"iron_nugget": 0.25, "clay": 0.3, "salt": 0.25}
|
"iron_nugget": 0.25, "clay": 0.3, "salt": 0.25, "driftwood": 0.5}
|
||||||
for id: String in want:
|
for id: String in want:
|
||||||
# Fibre grows as a plant (wild flax) you pick, 2 fibre a clump.
|
# Fibre grows as a plant (wild flax) you pick, 2 fibre a clump.
|
||||||
var mesh: Mesh = ItemMeshes.build("fiber_plant") if id == "fiber" else ItemDB.mesh_for(id)
|
var mesh: Mesh = ItemMeshes.build("fiber_plant") if id == "fiber" else ItemDB.mesh_for(id)
|
||||||
@@ -709,10 +709,17 @@ func _scatter_pickups() -> void:
|
|||||||
var shore: float = 1.0 - smoothstep(terrain.water_level + 0.4, terrain.water_level + 1.8, h)
|
var shore: float = 1.0 - smoothstep(terrain.water_level + 0.4, terrain.water_level + 1.8, h)
|
||||||
chance = shore * 0.95 if by_lake else 0.0
|
chance = shore * 0.95 if by_lake else 0.0
|
||||||
"salt":
|
"salt":
|
||||||
# Crusts on the cliff tops above the sea (P4).
|
# Crusts on the cliff tops above the sea (P4), or (the Cove)
|
||||||
|
# on the sand just above the waterline.
|
||||||
chance = 0.95 if terrain.sea_weight(x, z) > 0.6 and _near_rim(x, z) > 0.35 else 0.0
|
chance = 0.95 if terrain.sea_weight(x, z) > 0.6 and _near_rim(x, z) > 0.35 else 0.0
|
||||||
|
if terrain.beach_weight(x, z) > 0.6 and h < terrain.water_level + 1.2:
|
||||||
|
chance = 0.6
|
||||||
|
"driftwood":
|
||||||
|
# Washed up along a beach (the Cove), near the waterline:
|
||||||
|
# what a boat's hull is made of.
|
||||||
|
chance = 0.95 if terrain.beach_weight(x, z) > 0.6 and h < terrain.water_level + 1.6 else 0.0
|
||||||
# A few right by the spawn so the first craft is quick.
|
# A few right by the spawn so the first craft is quick.
|
||||||
if Vector2(x - _spawn.x, z - _spawn.z).length() < 18.0 and not id in ["iron_nugget", "clay", "salt"]:
|
if Vector2(x - _spawn.x, z - _spawn.z).length() < 18.0 and not id in ["iron_nugget", "clay", "salt", "driftwood"]:
|
||||||
chance += 0.5
|
chance += 0.5
|
||||||
if keep > chance:
|
if keep > chance:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ var _pieces: Node3D
|
|||||||
var hud: CanvasLayer
|
var hud: CanvasLayer
|
||||||
var pier: Node3D ## the Cove's pier (scripts/world/pier.gd), or null
|
var pier: Node3D ## the Cove's pier (scripts/world/pier.gd), or null
|
||||||
var tunnel: Node3D ## the cliff tunnel (scripts/world/tunnel.gd, J1), or null
|
var tunnel: Node3D ## the cliff tunnel (scripts/world/tunnel.gd, J1), or null
|
||||||
|
var wreck: Node3D ## the Cove's wreck (scripts/world/wreck.gd, 0.6.0), or null
|
||||||
var sea_cave: Node3D ## the Cove's end of it (scripts/world/sea_cave.gd), or null
|
var sea_cave: Node3D ## the Cove's end of it (scripts/world/sea_cave.gd), or null
|
||||||
var traveling := false ## on the way to another worldlet (the scene is about to change)
|
var traveling := false ## on the way to another worldlet (the scene is about to change)
|
||||||
var _session_time := 0.0 ## seconds played since the last save
|
var _session_time := 0.0 ## seconds played since the last save
|
||||||
@@ -365,6 +366,15 @@ func _on_terrain_ready() -> void:
|
|||||||
props.pier = pr
|
props.pier = pr
|
||||||
else:
|
else:
|
||||||
pr.free()
|
pr.free()
|
||||||
|
# The wreck (0.6.0): on the Cove's beach, with the sea chest's treasure maps.
|
||||||
|
var wk := preload("res://scripts/world/wreck.gd").new()
|
||||||
|
wk.name = "Wreck"
|
||||||
|
add_child(wk)
|
||||||
|
if not menu_mode and wk.place(self):
|
||||||
|
wreck = wk
|
||||||
|
props.keep_clear.append([wk.global_position, 9.0])
|
||||||
|
else:
|
||||||
|
wk.free()
|
||||||
# The journey's ways on (J1): the Valley's cliff tunnel and the Cove's sea
|
# The journey's ways on (J1): the Valley's cliff tunnel and the Cove's sea
|
||||||
# cave. On a server too (J3): the server decides the digging and the
|
# cave. On a server too (J3): the server decides the digging and the
|
||||||
# crossings, and every player's game has the same rock to see and walk.
|
# crossings, and every player's game has the same rock to see and walk.
|
||||||
@@ -690,6 +700,7 @@ func collect_save() -> Dictionary:
|
|||||||
"barrow": barrow.save_data() if barrow != null else {},
|
"barrow": barrow.save_data() if barrow != null else {},
|
||||||
"stones": stones.save_data() if stones != null else {},
|
"stones": stones.save_data() if stones != null else {},
|
||||||
"tunnel": tunnel.to_data() if tunnel != null else {},
|
"tunnel": tunnel.to_data() if tunnel != null else {},
|
||||||
|
"wreck": wreck.to_data() if wreck != null else {},
|
||||||
"map": map_data(),
|
"map": map_data(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,6 +717,7 @@ func _shared_state(harvested: Dictionary, regrow: Dictionary, taken: Array, drop
|
|||||||
"barrow": barrow.save_data() if barrow != null else {},
|
"barrow": barrow.save_data() if barrow != null else {},
|
||||||
"stones": stones.save_data() if stones != null else {},
|
"stones": stones.save_data() if stones != null else {},
|
||||||
"tunnel": tunnel.to_data() if tunnel != null else {},
|
"tunnel": tunnel.to_data() if tunnel != null else {},
|
||||||
|
"wreck": wreck.to_data() if wreck != null else {},
|
||||||
}
|
}
|
||||||
|
|
||||||
## The live pieces (placed and not being removed).
|
## The live pieces (placed and not being removed).
|
||||||
@@ -839,6 +851,8 @@ func _apply_save(s: Dictionary, with_player: bool) -> void:
|
|||||||
stones.load_data(s.get("stones", {}))
|
stones.load_data(s.get("stones", {}))
|
||||||
if tunnel != null:
|
if tunnel != null:
|
||||||
tunnel.load_data(s.get("tunnel", {}))
|
tunnel.load_data(s.get("tunnel", {}))
|
||||||
|
if wreck != null:
|
||||||
|
wreck.load_data(s.get("wreck", {}))
|
||||||
if with_player:
|
if with_player:
|
||||||
load_map_data(String(s.get("map", "")))
|
load_map_data(String(s.get("map", "")))
|
||||||
# The bear (bear_range.gd): applied when it's set up, after loading.
|
# The bear (bear_range.gd): applied when it's set up, after loading.
|
||||||
@@ -971,6 +985,12 @@ func map_marks() -> Array:
|
|||||||
add.call("pier", (pier.start + pier.end) * 0.5, "The pier")
|
add.call("pier", (pier.start + pier.end) * 0.5, "The pier")
|
||||||
if sea_cave != null:
|
if sea_cave != null:
|
||||||
add.call("cave", sea_cave.global_position, "Sea cave")
|
add.call("cave", sea_cave.global_position, "Sea cave")
|
||||||
|
if wreck != null:
|
||||||
|
add.call("wreck", wreck.global_position, "The wreck")
|
||||||
|
# Your X: shown while you carry the map (even where you haven't been).
|
||||||
|
var x := treasure_spot(my_id())
|
||||||
|
if x != Vector3.INF and player.inventory.count("treasure_map") > 0:
|
||||||
|
out.append({"kind": "treasure", "pos": x, "label": "X"})
|
||||||
for piece in all_pieces():
|
for piece in all_pieces():
|
||||||
var id: String = String(piece.get("item_id"))
|
var id: String = String(piece.get("item_id"))
|
||||||
if piece.is_in_group("bed"):
|
if piece.is_in_group("bed"):
|
||||||
@@ -983,6 +1003,70 @@ func map_marks() -> Array:
|
|||||||
add.call("grave", piece.global_position, "Your things")
|
add.call("grave", piece.global_position, "Your things")
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
# --- the pirate treasure (0.6.0, docs/WORLDLETS_DESIGN.md §5) ----------------------
|
||||||
|
|
||||||
|
var _treasure_spots: Dictionary = {} ## player id -> Vector3 (worked out once)
|
||||||
|
|
||||||
|
## Where player `id`'s treasure is buried (each player's map has its own X):
|
||||||
|
## a dry, gentle spot in the Cove's hills, picked from the world's seed and
|
||||||
|
## the player's id, clear of the water, the stream and the other places.
|
||||||
|
## INF where there's no treasure (no wreck in this land).
|
||||||
|
func treasure_spot(id: String) -> Vector3:
|
||||||
|
if wreck == null:
|
||||||
|
return Vector3.INF
|
||||||
|
if _treasure_spots.has(id):
|
||||||
|
return _treasure_spots[id]
|
||||||
|
var spots := treasure_spots()
|
||||||
|
var at: Vector3 = Vector3.INF if spots.is_empty() else spots[posmod(hash(id), spots.size())]
|
||||||
|
_treasure_spots[id] = at
|
||||||
|
return at
|
||||||
|
|
||||||
|
## The world's hiding places for treasure: up to 12, at least 25 m apart,
|
||||||
|
## picked from its seed (so each player's X is one of these, and different
|
||||||
|
## players' are usually far apart).
|
||||||
|
var _hiding: Array = []
|
||||||
|
func treasure_spots() -> Array:
|
||||||
|
if not _hiding.is_empty() or wreck == null:
|
||||||
|
return _hiding
|
||||||
|
var t: Node3D = terrain
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = t.noise_seed * 7919 + 2024
|
||||||
|
var avoid: Array = [wreck.global_position, _spawn]
|
||||||
|
for n in [den, pier, sea_cave]:
|
||||||
|
if n != null and n.is_inside_tree():
|
||||||
|
avoid.append(n.global_position if n != pier else (pier.start + pier.end) * 0.5)
|
||||||
|
for _try in range(3000):
|
||||||
|
if _hiding.size() >= 12:
|
||||||
|
break
|
||||||
|
var a := rng.randf() * TAU
|
||||||
|
var r: float = sqrt(rng.randf()) * t.play_radius * 0.85
|
||||||
|
var x: float = cos(a) * r
|
||||||
|
var z: float = sin(a) * r
|
||||||
|
if not t.inside_boundary(x, z, 14.0) or t.beach_weight(x, z) > 0.3 or t.near_stream(x, z, 4.0):
|
||||||
|
continue
|
||||||
|
var h: float = t.sample_height(x, z)
|
||||||
|
if h < t.water_level + 2.0 or t.sample_normal(x, z).y < 0.92 or h > t.snow_height - 6.0:
|
||||||
|
continue
|
||||||
|
if avoid.any(func(q): return Vector2(q.x - x, q.z - z).length() < 22.0):
|
||||||
|
continue
|
||||||
|
if _hiding.any(func(q): return Vector2(q.x - x, q.z - z).length() < 25.0):
|
||||||
|
continue
|
||||||
|
_hiding.append(Vector3(x, h, z))
|
||||||
|
return _hiding
|
||||||
|
|
||||||
|
## The id of the player at this game (yours; a server's character's own).
|
||||||
|
func my_id() -> String:
|
||||||
|
return String(Net.get_identity().player_id)
|
||||||
|
|
||||||
|
## Right-click with the treasure map: your X goes on your map, and it opens.
|
||||||
|
func read_map(p: Node3D) -> void:
|
||||||
|
if wreck == null:
|
||||||
|
Actions.notify.emit("The X is in the hills above the Cove: read it there")
|
||||||
|
return
|
||||||
|
Actions.notify.emit("X marks the spot, in the hills above the bay: it's on your map now (N). Take a pickaxe")
|
||||||
|
if hud != null and hud.map != null and not hud.map.is_open():
|
||||||
|
hud.map.open()
|
||||||
|
|
||||||
## In the cliff tunnel (J1): no daylight, no weather, sheltered.
|
## In the cliff tunnel (J1): no daylight, no weather, sheltered.
|
||||||
func underground_at(p: Vector3) -> bool:
|
func underground_at(p: Vector3) -> bool:
|
||||||
return tunnel != null and tunnel.contains(p)
|
return tunnel != null and tunnel.contains(p)
|
||||||
|
|||||||
149
scripts/world/wreck.gd
Normal file
149
scripts/world/wreck.gd
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
extends Node3D
|
||||||
|
## The Cove's wreck (docs/WORLDLETS_DESIGN.md §5, 0.6.0): an old ship driven
|
||||||
|
## up on the sand at the far end of the bay from the sea cave, her bow in the
|
||||||
|
## shallows, one side stove in. Inside, the captain's sea chest: a torn
|
||||||
|
## treasure map and a few useful things, **once for each player** (single-
|
||||||
|
## player and online alike: who's had theirs is saved with the Cove).
|
||||||
|
##
|
||||||
|
## Placed from the terrain alone, like the pier, so it's the same for
|
||||||
|
## everyone; only `opened` (who's had their share) is saved.
|
||||||
|
|
||||||
|
const Pier = preload("res://scripts/world/pier.gd")
|
||||||
|
const OAK := Color(0.36, 0.25, 0.16)
|
||||||
|
const DARK := Color(0.24, 0.17, 0.11)
|
||||||
|
const WET := Color(0.2, 0.18, 0.15)
|
||||||
|
const IRON := Color(0.22, 0.22, 0.24)
|
||||||
|
## What's in the sea chest, for each player once.
|
||||||
|
const LOOT := {"treasure_map": 1, "nails": 4, "salt": 2}
|
||||||
|
|
||||||
|
var world: Node3D
|
||||||
|
var opened: Array = [] ## the player ids who've had their share
|
||||||
|
var chest: StaticBody3D
|
||||||
|
var front := Vector3.ZERO ## where you stand to reach into the chest
|
||||||
|
|
||||||
|
func place(w: Node3D) -> bool:
|
||||||
|
world = w
|
||||||
|
var t: Node3D = w.terrain
|
||||||
|
if not w.has_place("wreck") or not t._has_beach:
|
||||||
|
return false
|
||||||
|
# The far end of the beach from the sea cave, where the sand meets the water.
|
||||||
|
var a := deg_to_rad(t.beach_angle_deg - t.beach_half_width_deg * 0.8)
|
||||||
|
var dir := Vector2(cos(a), sin(a))
|
||||||
|
var r: float = t._shore_d() - t._rim_offset(dir.x, dir.y)
|
||||||
|
var at2 := dir * (r - 1.0)
|
||||||
|
var at := Vector3(at2.x, maxf(t.sample_height(at2.x, at2.y), t.water_level - 0.3), at2.y)
|
||||||
|
# Bow out to sea (+Z local), a little askew, heeled over on her side.
|
||||||
|
var out := Vector3(dir.x, 0.0, dir.y).rotated(Vector3.UP, 0.35)
|
||||||
|
global_transform = Transform3D(Basis.looking_at(-out, Vector3.UP), at)
|
||||||
|
_build()
|
||||||
|
add_to_group("wreck")
|
||||||
|
return true
|
||||||
|
|
||||||
|
func _build() -> void:
|
||||||
|
var st := SurfaceTool.new()
|
||||||
|
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||||
|
var body := StaticBody3D.new()
|
||||||
|
body.name = "Hull"
|
||||||
|
add_child(body)
|
||||||
|
var heel := Basis(Vector3(0, 0, 1), deg_to_rad(-14.0))
|
||||||
|
var parts: Array = [
|
||||||
|
# [centre, size, colour] in the hull's frame (+Z the bow, +X starboard)
|
||||||
|
[Vector3(0, 0.12, 0), Vector3(0.35, 0.25, 9.0), DARK], # keel
|
||||||
|
[Vector3(-1.45, 0.75, 0), Vector3(0.14, 1.3, 8.2), OAK], # port side, whole
|
||||||
|
[Vector3(1.45, 0.75, 2.6), Vector3(0.14, 1.3, 3.0), OAK], # starboard, forward
|
||||||
|
[Vector3(1.45, 0.55, -3.0), Vector3(0.14, 0.9, 2.2), OAK], # starboard, aft (stove in between)
|
||||||
|
[Vector3(0, 0.75, -4.1), Vector3(2.9, 1.3, 0.14), OAK], # transom
|
||||||
|
[Vector3(0, 0.05, 0), Vector3(2.8, 0.1, 7.6), WET], # bilge boards
|
||||||
|
[Vector3(0, 1.8, 1.4), Vector3(0.22, 2.6, 0.22), DARK], # mast stump
|
||||||
|
]
|
||||||
|
for p in parts:
|
||||||
|
var c: Vector3 = heel * p[0]
|
||||||
|
Pier._box(st, c, p[1], p[2])
|
||||||
|
var cs := CollisionShape3D.new()
|
||||||
|
var box := BoxShape3D.new()
|
||||||
|
box.size = p[1]
|
||||||
|
cs.shape = box
|
||||||
|
cs.transform = Transform3D(heel, c)
|
||||||
|
body.add_child(cs)
|
||||||
|
# Ribs over the open hull, some broken off short.
|
||||||
|
for i in range(6):
|
||||||
|
var z := -3.4 + i * 1.3
|
||||||
|
for side in [-1.0, 1.0]:
|
||||||
|
if side > 0.0 and z > -1.8 and z < 1.0:
|
||||||
|
continue # (the stove-in side)
|
||||||
|
var h := 1.9 if (i + int(side)) % 3 != 0 else 1.2
|
||||||
|
Pier._box(st, heel * Vector3(side * 1.3, h * 0.5 + 0.5, z), Vector3(0.12, h, 0.14), DARK)
|
||||||
|
# Planks strewn on the sand on the open side.
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = 919
|
||||||
|
for i in range(5):
|
||||||
|
var off := Vector3(2.6 + rng.randf() * 2.5, 0.05, -2.0 + rng.randf() * 4.0)
|
||||||
|
Pier._box(st, off, Vector3(0.3, 0.06, 1.6 + rng.randf()), OAK.darkened(rng.randf() * 0.3))
|
||||||
|
st.generate_normals()
|
||||||
|
var mi := MeshInstance3D.new()
|
||||||
|
mi.name = "Ship"
|
||||||
|
mi.mesh = st.commit()
|
||||||
|
var mat := StandardMaterial3D.new()
|
||||||
|
mat.vertex_color_use_as_albedo = true
|
||||||
|
mat.roughness = 0.95
|
||||||
|
mi.material_override = mat
|
||||||
|
add_child(mi)
|
||||||
|
# The captain's sea chest, wedged inside by the break.
|
||||||
|
chest = StaticBody3D.new()
|
||||||
|
chest.name = "SeaChest"
|
||||||
|
chest.collision_layer = 1 | 2
|
||||||
|
chest.set_script(preload("res://scripts/world/wreck_chest.gd"))
|
||||||
|
chest.wreck = self
|
||||||
|
var cst := SurfaceTool.new()
|
||||||
|
cst.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||||
|
Pier._box(cst, Vector3(0, 0.25, 0), Vector3(0.9, 0.5, 0.55), Color(0.3, 0.2, 0.12))
|
||||||
|
Pier._box(cst, Vector3(0, 0.55, 0), Vector3(0.92, 0.12, 0.57), Color(0.27, 0.18, 0.1))
|
||||||
|
for x in [-0.32, 0.32]:
|
||||||
|
Pier._box(cst, Vector3(x, 0.32, 0), Vector3(0.06, 0.64, 0.6), IRON)
|
||||||
|
cst.generate_normals()
|
||||||
|
var cmi := MeshInstance3D.new()
|
||||||
|
cmi.mesh = cst.commit()
|
||||||
|
cmi.material_override = mat
|
||||||
|
chest.add_child(cmi)
|
||||||
|
var ccs := CollisionShape3D.new()
|
||||||
|
var cb := BoxShape3D.new()
|
||||||
|
cb.size = Vector3(0.9, 0.62, 0.55)
|
||||||
|
ccs.shape = cb
|
||||||
|
ccs.position = Vector3(0, 0.31, 0)
|
||||||
|
chest.add_child(ccs)
|
||||||
|
add_child(chest)
|
||||||
|
chest.transform = Transform3D(heel * Basis(Vector3.UP, 0.4), heel * Vector3(0.5, 0.12, -0.4))
|
||||||
|
front = global_transform * (heel * Vector3(2.4, 0.0, -0.4))
|
||||||
|
front.y = world.terrain.sample_height(front.x, front.z)
|
||||||
|
|
||||||
|
# --- the sea chest -------------------------------------------------------------
|
||||||
|
|
||||||
|
## Whether player `id` has had their share of the chest.
|
||||||
|
func had(id: String) -> bool:
|
||||||
|
return id in opened
|
||||||
|
|
||||||
|
## Gives player `p` (id `id`) their share: the map and the rest, once.
|
||||||
|
## Returns false (and says why) when they've had it, or their bag's too full.
|
||||||
|
func open_for(p: Node, id: String) -> bool:
|
||||||
|
if had(id):
|
||||||
|
Actions.tell(p, "The sea chest's empty: you've had what was in it")
|
||||||
|
return false
|
||||||
|
for item in LOOT:
|
||||||
|
if p.inventory.space_for(item) < int(LOOT[item]):
|
||||||
|
Actions.tell(p, "Make room in your bag first")
|
||||||
|
return false
|
||||||
|
for item in LOOT:
|
||||||
|
p.inventory.add(item, int(LOOT[item]))
|
||||||
|
opened.append(id)
|
||||||
|
Actions.tell(p, "In the captain's sea chest: a torn treasure map, %d nails and %d salt. Right-click the map to read it" % [
|
||||||
|
int(LOOT.nails), int(LOOT.salt)])
|
||||||
|
Sfx.at("door_open", chest.global_position)
|
||||||
|
if Net.is_server():
|
||||||
|
WorldNet.mark_changed()
|
||||||
|
return true
|
||||||
|
|
||||||
|
func to_data() -> Dictionary:
|
||||||
|
return {"opened": opened.duplicate()}
|
||||||
|
|
||||||
|
func load_data(d: Dictionary) -> void:
|
||||||
|
opened = (d.get("opened", []) as Array).duplicate()
|
||||||
1
scripts/world/wreck.gd.uid
Normal file
1
scripts/world/wreck.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dydutxxckscny
|
||||||
23
scripts/world/wreck_chest.gd
Normal file
23
scripts/world/wreck_chest.gd
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
extends StaticBody3D
|
||||||
|
## The wreck's sea chest (wreck.gd): what you look at and press E on. Online,
|
||||||
|
## your game asks the server, which gives you your share once.
|
||||||
|
|
||||||
|
var wreck: Node3D
|
||||||
|
|
||||||
|
func interact_prompt(player: Node) -> String:
|
||||||
|
if wreck.had(_id(player)):
|
||||||
|
return "The captain's sea chest (empty: you've had what was in it)"
|
||||||
|
return "[E] Open the captain's sea chest"
|
||||||
|
|
||||||
|
func interact(player: Node) -> void:
|
||||||
|
if wreck.world.is_online():
|
||||||
|
PlayerNet.ask("wreck_chest", [])
|
||||||
|
return
|
||||||
|
wreck.open_for(player, _id(player))
|
||||||
|
|
||||||
|
## Whose share: the player's id (a server's character, or, at home, yours).
|
||||||
|
static func _id(player: Node) -> String:
|
||||||
|
var pid: Variant = player.get("player_id")
|
||||||
|
if pid != null and not String(pid).is_empty():
|
||||||
|
return String(pid)
|
||||||
|
return String(Net.get_identity().player_id)
|
||||||
1
scripts/world/wreck_chest.gd.uid
Normal file
1
scripts/world/wreck_chest.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://oeh0v78qjmnh
|
||||||
@@ -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_net_lands 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_tunnel test_map 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_net_lands 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_wreck test_tunnel test_map 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,
|
||||||
|
|||||||
@@ -148,6 +148,15 @@ func _body() -> void:
|
|||||||
check(world.sea_cave.open(), "the sea cave leads back up (the hub says the tunnel's through)")
|
check(world.sea_cave.open(), "the sea cave leads back up (the hub says the tunnel's through)")
|
||||||
var bob := await _bob({"do": "report"})
|
var bob := await _bob({"do": "report"})
|
||||||
check(not bob.has("error") and not bob.is_empty(), "Bob carries on in the Valley, on the hub (%s)" % bob.get("error", "ok"))
|
check(not bob.has("error") and not bob.is_empty(), "Bob carries on in the Valley, on the hub (%s)" % bob.get("error", "ok"))
|
||||||
|
print("the wreck's sea chest, online")
|
||||||
|
await _tp(world.wreck.front + Vector3(0, 0.1, 0))
|
||||||
|
_toasts.clear()
|
||||||
|
world.wreck.chest.interact(player)
|
||||||
|
await _until(func(): return player.inventory.count("treasure_map") == 1, 4.0)
|
||||||
|
check(player.inventory.count("treasure_map") == 1, "Alice's share: the server gives her a treasure map")
|
||||||
|
await _until(func(): return world.wreck.had(world.my_id()), 3.0)
|
||||||
|
check(world.wreck.chest.interact_prompt(player).contains("empty"), "and her game knows the chest's empty for her now")
|
||||||
|
check(world.map_marks().any(func(m): return m.kind == "treasure"), "her X is on her map")
|
||||||
await _across_lands(bob_bed, bob_bed_at)
|
await _across_lands(bob_bed, bob_bed_at)
|
||||||
|
|
||||||
print("back up to the Valley")
|
print("back up to the Valley")
|
||||||
|
|||||||
117
tests/test_wreck.gd
Normal file
117
tests/test_wreck.gd
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
extends SceneTree
|
||||||
|
## 0.6.0 (docs/WORLDLETS_DESIGN.md §5): the Cove's wreck and the treasure map.
|
||||||
|
## The wreck is on the Cove's beach (and not in the Valley); driftwood and
|
||||||
|
## salt wash up on the sand; the captain's sea chest gives each player a
|
||||||
|
## treasure map, nails and salt, once; the map's X is that player's own (and
|
||||||
|
## somewhere else for the next player); reading the map tells you, and the X
|
||||||
|
## is on your N map while you carry it; who's had their share is saved.
|
||||||
|
## Run: godot --headless --path . --script res://tests/test_wreck.gd
|
||||||
|
|
||||||
|
var _fails := 0
|
||||||
|
var _finished := false
|
||||||
|
var S: Node
|
||||||
|
var A: Node
|
||||||
|
var _toasts: Array[String] = []
|
||||||
|
|
||||||
|
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:
|
||||||
|
S = root.get_node("Saves")
|
||||||
|
A = root.get_node("Actions")
|
||||||
|
A.notify.connect(func(t): _toasts.append(t))
|
||||||
|
S.root_dir = "user://test_wreck_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 _load() -> Node3D:
|
||||||
|
var w: Node3D = load("res://scenes/world.tscn").instantiate()
|
||||||
|
root.add_child(w)
|
||||||
|
for _i in range(8):
|
||||||
|
await physics_frame
|
||||||
|
w.creatures.enabled = false
|
||||||
|
w.player.set_physics_process(false)
|
||||||
|
w.player.survival.set_process(false)
|
||||||
|
return w
|
||||||
|
|
||||||
|
func _drop(w: Node3D) -> void:
|
||||||
|
w.queue_free()
|
||||||
|
for _i in range(3):
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
func _body() -> void:
|
||||||
|
print("the Valley")
|
||||||
|
S.current_id = S.create_world("Valley", 1337)
|
||||||
|
var w: Node3D = await _load()
|
||||||
|
check(w.wreck == null, "no wreck in the Valley")
|
||||||
|
check(not w.props.pickups.values().any(func(p): return p.item_id == "driftwood"), "and no driftwood")
|
||||||
|
await _drop(w)
|
||||||
|
|
||||||
|
print("the wreck")
|
||||||
|
S.current_id = S.create_world("Cove", 1337, {"worldlet": "cove"})
|
||||||
|
w = await _load()
|
||||||
|
var t: Node3D = w.terrain
|
||||||
|
var wk: Node3D = w.wreck
|
||||||
|
check(wk != null and t.beach_weight(wk.global_position.x, wk.global_position.z) > 0.5, "a wreck on the Cove's beach")
|
||||||
|
if wk == null:
|
||||||
|
return
|
||||||
|
check(wk.global_position.distance_to(w.sea_cave.global_position) > 60.0, "at the far end of the bay from the sea cave (%.0f m)" % wk.global_position.distance_to(w.sea_cave.global_position))
|
||||||
|
check(w.water_at(wk.front.x, wk.front.z).is_empty() or float(w.water_at(wk.front.x, wk.front.z).depth) < 0.6, "you can stand beside it to reach the chest")
|
||||||
|
var drift: Array = w.props.pickups.values().filter(func(p): return p.item_id == "driftwood")
|
||||||
|
check(drift.size() >= 20 and drift.all(func(p): return t.beach_weight(p.global_position.x, p.global_position.z) > 0.5),
|
||||||
|
"driftwood along the sand (%d)" % drift.size())
|
||||||
|
check(w.props.pickups.values().any(func(p): return p.item_id == "salt"), "and salt crusts by the waterline")
|
||||||
|
|
||||||
|
print("the captain's sea chest")
|
||||||
|
var p: Node3D = w.player
|
||||||
|
var me: String = w.my_id()
|
||||||
|
p.global_position = wk.front
|
||||||
|
check(wk.chest.interact_prompt(p).begins_with("[E] Open"), "[E] Open the captain's sea chest")
|
||||||
|
wk.chest.interact(p)
|
||||||
|
check(p.inventory.count("treasure_map") == 1 and p.inventory.count("nails") >= 4 and p.inventory.count("salt") >= 2,
|
||||||
|
"a torn treasure map, nails and salt")
|
||||||
|
check(_toasts.any(func(s): return String(s).begins_with("In the captain's sea chest")), "(and it says so)")
|
||||||
|
_toasts.clear()
|
||||||
|
wk.chest.interact(p)
|
||||||
|
check(p.inventory.count("treasure_map") == 1 and _toasts.has("The sea chest's empty: you've had what was in it"), "once: the second time it's empty")
|
||||||
|
check(wk.open_for(p, "someone-else"), "someone else gets their own share")
|
||||||
|
p.inventory.remove("treasure_map", 1) # (it went in this bag: theirs, not ours)
|
||||||
|
|
||||||
|
print("the map")
|
||||||
|
var x: Vector3 = w.treasure_spot(me)
|
||||||
|
check(x != Vector3.INF and t.inside_boundary(x.x, x.z, 10.0) and x.y > t.water_level + 1.5, "your X: dry ground in the hills (%s)" % x)
|
||||||
|
var others := 0
|
||||||
|
for i in range(8):
|
||||||
|
if w.treasure_spot("player-%d" % i).distance_to(x) > 20.0:
|
||||||
|
others += 1
|
||||||
|
check(others >= 5, "and other players' X are mostly somewhere else (%d of 8)" % others)
|
||||||
|
check(w.treasure_spot(me) == x, "(the same every time for you)")
|
||||||
|
check(w.map_marks().any(func(m): return m.kind == "treasure" and m.pos == x), "on your map while you carry it, even where you haven't been")
|
||||||
|
_toasts.clear()
|
||||||
|
w.read_map(p)
|
||||||
|
check(_toasts.any(func(s): return String(s).begins_with("X marks the spot")), "reading it: X marks the spot")
|
||||||
|
p.inventory.remove("treasure_map", 1)
|
||||||
|
check(not w.map_marks().any(func(m): return m.kind == "treasure"), "(no map, no X)")
|
||||||
|
p.inventory.add("treasure_map", 1)
|
||||||
|
w.explore(wk.global_position)
|
||||||
|
check(w.map_marks().any(func(m): return m.kind == "wreck"), "the wreck's on the map once you've seen it")
|
||||||
|
|
||||||
|
print("saved")
|
||||||
|
w.save_now()
|
||||||
|
await _drop(w)
|
||||||
|
w = await _load()
|
||||||
|
check(w.wreck.had(me) and w.wreck.had("someone-else") and not w.wreck.had("a-stranger"), "who's had their share is kept")
|
||||||
|
await _drop(w)
|
||||||
|
_finished = true
|
||||||
1
tests/test_wreck.gd.uid
Normal file
1
tests/test_wreck.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://chnwnd4xdcolj
|
||||||
Reference in New Issue
Block a user