extends Node3D ## The pirate treasure (0.6.0, docs/WORLDLETS_DESIGN.md ยง5). Each player's ## treasure map (the wreck's sea chest) has its own X (world.treasure_spot). ## While you carry your map, a mound of loose earth by a marker stone shows ## at your X (only to you); a pickaxe digs it, and the pirate chest comes up: ## a ship's compass, a spyglass, a cutlass and pirate gold. The map's used up. ## Once for each player. ## ## The drowned crew guard it: digging at night brings them up round you on ## the first blow; by day they rise as the chest comes up. They're the Cove's ## barrow guards (guard.gd, kind "drowned", at a crew_site.gd). ## ## Single-player, all here. Online, the server keeps whose treasure's been ## dug and how far, gives the loot and runs the crew; your game only shows ## your own mound and asks the server to dig it. const MoundScript = preload("res://scripts/world/treasure_mound.gd") const CrewSite = preload("res://scripts/world/crew_site.gd") const GuardScript = preload("res://scripts/creatures/guard.gd") const RockPile = preload("res://scripts/world/rock_pile.gd") const ProcMesh = preload("res://scripts/world/proc_mesh.gd") const LOOT := {"ship_compass": 1, "spyglass": 1, "cutlass": 1, "pirate_gold": 3} const CREW := 3 const DIG_HP := 18.0 var world: Node3D var dug: Array = [] ## the player ids whose treasure's been dug up var left: Dictionary = {} ## player id -> what's left of their mound (dug a bit) var mounds: Dictionary = {} ## player id -> treasure_mound.gd var crews: Dictionary = {} ## player id -> crew_site.gd, while they're up var force_crew := false ## (tests) raise the crew even with creatures off var _look_left := 0.0 func setup(w: Node3D) -> bool: world = w if w.wreck == null: return false Actions.harvested.connect(_on_harvested) add_to_group("treasure") return true # --- the mounds --------------------------------------------------------------------- ## Player `id`'s mound (made if need be), or null if theirs is dug or they ## have no X here. func mound_for(id: String) -> StaticBody3D: if id in dug: return null if mounds.has(id) and is_instance_valid(mounds[id]): return mounds[id] var at: Vector3 = world.treasure_spot(id) if at == Vector3.INF: return null var m: StaticBody3D = MoundScript.new() m.name = "Mound_%s" % id.substr(0, 8) m.treasure = self m.owner_id = id m.uid = "treasure:" + id m.max_hp = DIG_HP m.hp = clampf(float(left.get(id, DIG_HP)), 0.5, DIG_HP) m.collision_layer = 1 m.add_to_group("harvestable") var cs := CollisionShape3D.new() var box := BoxShape3D.new() box.size = Vector3(1.6, 0.6, 1.6) cs.shape = box cs.position = Vector3(0, 0.2, 0) m.add_child(cs) if not world.server_mode: _dress(m, id) add_child(m) m.global_position = at mounds[id] = m return m ## How a mound looks: loose earth, a marker stone, two sticks crossed in it. func _dress(m: Node3D, id: String) -> void: var st := ProcMesh._begin() var r := RandomNumberGenerator.new() r.seed = hash(id) var n := FastNoiseLite.new() n.seed = hash(id) % 1000 n.frequency = 1.5 ProcMesh._blob(st, Vector3(0, 0.05, 0), Vector3(0.9, 0.28, 0.8), n, 0.3, Color(0.34, 0.25, 0.16), r, 0) ProcMesh._cylinder(st, Vector3(-0.25, 0.1, -0.25), Vector3(0.25, 0.7, 0.25), 0.03, 0.025, 4, Color(0.4, 0.3, 0.18), r) ProcMesh._cylinder(st, Vector3(0.25, 0.1, -0.25), Vector3(-0.25, 0.7, 0.25), 0.03, 0.025, 4, Color(0.4, 0.3, 0.18), r) var mi := MeshInstance3D.new() mi.mesh = ProcMesh._commit(st) var mat := StandardMaterial3D.new() mat.vertex_color_use_as_albedo = true mat.vertex_color_is_srgb = true mat.roughness = 1.0 mi.material_override = mat m.add_child(mi) RockPile.add(m, null, hash(id) % 997, Vector3(1.3, 0.35, 0.2), Vector3(0.35, 0.8, 0.3), 0.6, RockPile.material(world.terrain), true) ## (Your game) your mound shows while you carry your map and it's not dug. func _process(delta: float) -> void: if world.server_mode: return _look_left -= delta if _look_left > 0.0: return _look_left = 0.5 var me: String = world.my_id() var want: bool = not me in dug and world.player.inventory.count("treasure_map") > 0 if want: mound_for(me) elif mounds.has(me) and is_instance_valid(mounds[me]): mounds[me].queue_free() mounds.erase(me) # --- digging ------------------------------------------------------------------------ ## A blow on a mound (treasure_mound.gd): remembered, and at night the first ## one brings the crew up. func mound_hit(m: Node) -> void: left[m.owner_id] = m.hp if Net.is_server(): WorldNet.treasure_state(to_data()) if _night() and not crews.has(m.owner_id): _raise_crew(m.owner_id, m.global_position) func _on_harvested(p: Node, node: Node, felled: bool) -> void: if not felled or node == null or node.get_script() != MoundScript or node.treasure != self: return _dug_up(p, String(node.owner_id), node.global_position) ## The chest comes up: the loot for the player who dug it, the map used up. func _dug_up(p: Node, id: String, at: Vector3) -> void: if id in dug: return dug.append(id) left.erase(id) if mounds.has(id) and is_instance_valid(mounds[id]): mounds[id].queue_free() mounds.erase(id) p.inventory.remove("treasure_map", 1) var spilled := false for item in LOOT: var rest: int = p.inventory.add(item, int(LOOT[item])) if rest > 0: spilled = true world.drop_item(item, rest, Transform3D(Basis(), at + Vector3(0, 0.3, 0))) Actions.tell(p, "The pirate chest! A ship's compass, a spyglass, a cutlass and %d pirate gold%s" % [ int(LOOT.pirate_gold), " (some at your feet: your bag's full)" if spilled else ""]) Sfx.at("door_open", at) if not crews.has(id): _raise_crew(id, at) if crews.has(id): Actions.tell(p, "Something's clawing its way up out of the ground...") if Net.is_server(): WorldNet.treasure_state(to_data()) func _night() -> bool: var h: float = world.day_night.hours() return h >= 19.5 or h < 5.5 ## The drowned crew rise round `at` (where creatures run: here alone, or the ## server; not with --no-creatures, unless a test asks). func _raise_crew(id: String, at: Vector3) -> void: if world.is_online(): return var running: bool = force_crew or (not "--no-creatures" in OS.get_cmdline_user_args() \ and (world.server_mode or world.creatures.enabled)) if not running: return var site: Node3D = CrewSite.new() site.name = "Crew_%s" % id.substr(0, 8) site.world = world site.spot = at add_child(site) var t: Node3D = world.terrain for i in range(CREW): var a := TAU * i / CREW + float(hash(id) % 100) * 0.06 var post := at + Vector3(cos(a), 0, sin(a)) * 5.0 post.y = t.sample_height(post.x, post.z) var g: AnimatableBody3D = GuardScript.new() g.setup("drowned", world, post, hash(id) % 10000 + i) g.barrow = site g.post = post g.post_yaw = atan2(-(at - post).x, -(at - post).z) g.state = "charge" g.yaw = g.post_yaw g.rotation.y = g.yaw g.top_level = true site.add_child(g) g.global_position = post site.crew.append(g) crews[id] = site site.tree_exited.connect(func(): crews.erase(id)) # --- saving ------------------------------------------------------------------------ func to_data() -> Dictionary: return {"dug": dug.duplicate(), "left": left.duplicate()} func load_data(d: Dictionary) -> void: dug = (d.get("dug", []) as Array).duplicate() left = (d.get("left", {}) as Dictionary).duplicate() for id in mounds.keys(): var m: Node = mounds[id] if not is_instance_valid(m): mounds.erase(id) elif id in dug: m.queue_free() mounds.erase(id) else: m.hp = clampf(float(left.get(id, DIG_HP)), 0.5, DIG_HP)