extends Node3D ## World controller. ## ## Owns spawn ordering so the player never falls through ungenerated ground: ## waits for the terrain, places the scenery, then drops the player onto a dry, ## flat spot facing the lake. Also aims the sun, and respawns the player if they ## somehow leave the map or fall below it (a safety net behind the mountain rim ## and invisible wall in terrain.gd). ## ## Dev hook: `godot --path . -- --shot=/abs/prefix` renders a few fixed views to ## _.png and quits. Handy for checking visual changes. @onready var terrain: Node3D = $Terrain @onready var props: Node3D = $Props @onready var player: CharacterBody3D = $Player @onready var sun: DirectionalLight3D = $Sun const PIECES := { "campfire": preload("res://scripts/building/campfire.gd"), "workbench": preload("res://scripts/building/workbench.gd"), "chest": preload("res://scripts/building/chest.gd"), "grave": preload("res://scripts/building/grave.gd"), "build": preload("res://scripts/building/build_piece.gd"), "door": preload("res://scripts/building/door.gd"), "bed": preload("res://scripts/building/bed.gd"), "kiln": preload("res://scripts/building/processor.gd"), "smelter": preload("res://scripts/building/processor.gd"), "smoker": preload("res://scripts/building/processor.gd"), "forge": preload("res://scripts/building/forge.gd"), "hearth": preload("res://scripts/building/hearth.gd"), "rug": preload("res://scripts/building/rug.gd"), "stonecutter": preload("res://scripts/building/stonecutter.gd"), } const BuildPiece = preload("res://scripts/building/build_piece.gd") const UITheme = preload("res://scripts/ui/theme.gd") const AUTOSAVE_SECONDS := 120.0 ## Set by the title screen before adding the world: no player, no HUD, no ## saving, just a slow camera flight over the island as a menu backdrop. var menu_mode := false ## Set by WorldNet on a dedicated server: the shared island with nobody of its ## own in it (no player, HUD, animals or effects). It saves to the server's ## world slot. See world_net.gd. var server_mode := false var day_length := 1200.0 ## seconds per day (a server sets its own) var autosave_seconds := AUTOSAVE_SECONDS var _spawn := Vector3.ZERO var _spawn_yaw := 0.0 var _pieces: Node3D var hud: CanvasLayer var _session_time := 0.0 ## seconds played since the last save var _autosave_left := AUTOSAVE_SECONDS var _menu_cam: Camera3D var _menu_t := 0.0 var day_night: Node ## day_night.gd: time of day, sun, sky var weather: Node ## weather.gd: the weather model var weather_fx: Node3D ## weather_fx.gd: rain/snow particles, sound var build: Node ## build_system.gd: snapping, support, shelter var creatures: Node3D ## spawner.gd: wildlife around the player var _dropped: Node3D ## arrows, spears and other dropped items var den: Node3D ## den.gd: the wolves' cave at the foot of the rim var bobber: Node3D ## bobber.gd: the cast fishing float, if any var wolves: Node3D ## wolf_pack.gd (null if this seed has no den site) var bears: Node3D ## bear_range.gd: the island's bear (null online as a client) var thieves: Node3D ## thieves.gd: crows and foxes after food left out var hazards: Node3D ## hazards.gd: lightning on trees, hard-winter ice (P4) var stones: Node3D ## standing_stones.gd: the altar and the Old Stag (P5) var barrow: Node3D ## barrow.gd: the burial mound and its guards (scenery; guards where creatures run) var _saved_bear: Dictionary = {} var _saved_tutorial: Dictionary = {} ## the tutorial's progress (Guide), from the save ## its saved state, kept until bears exists (and re-saved if it never does) var net_sync: Node ## net_sync.gd when playing on a server, else null var has_bed_spawn := false var bed_spawn := Vector3.ZERO ## position of the bed you respawn at ## Playing on a server (multiplayer) rather than a local world. func is_online() -> bool: return not menu_mode and Net.state == "connected" var _loading := false ## applying a save: defer support recompute var _applying := false ## applying a save or snapshot: not news for the server's players ## Server only: changes to the island, wherever they came from (a player's ## request, a collapse, a death), for world_net.gd to send to everyone. signal piece_added(piece: Node3D) signal pieces_removed(gone: Array, fell: Array) signal dropped_added(item: Node3D) signal saved func _ready() -> void: _autosave_left = autosave_seconds _pieces = Node3D.new() _pieces.name = "Pieces" add_child(_pieces) day_night = preload("res://scripts/world/day_night.gd").new() day_night.name = "DayNight" if is_online() and Net.server_info.has("day_length_minutes"): day_length = float(Net.server_info.day_length_minutes) * 60.0 # same days, same weather day_night.day_length = day_length add_child(day_night) day_night.setup(sun, $WorldEnvironment.environment) weather = preload("res://scripts/world/weather.gd").new() weather.name = "Weather" add_child(weather) weather.snowline = terrain.snow_height - 8.0 weather.frozen = menu_mode weather.setup(terrain.noise_seed, day_night) day_night.weather = weather build = preload("res://scripts/building/build_system.gd").new() build.name = "Build" add_child(build) build.setup(self) _dropped = Node3D.new() _dropped.name = "Dropped" add_child(_dropped) creatures = preload("res://scripts/creatures/spawner.gd").new() creatures.name = "Creatures" creatures.enabled = false # until the scenery is placed add_child(creatures) if server_mode: # Nobody of our own: the player node stays, parked out of the way and # switched off, because a few shared functions expect one. player.process_mode = Node.PROCESS_MODE_DISABLED player.collision_layer = 0 player.position = Vector3(0, -500, 0) player.get_node("Camera3D").current = false $HUD.visible = false elif not menu_mode and DisplayServer.get_name() != "headless": weather_fx = preload("res://scripts/world/weather_fx.gd").new() weather_fx.name = "WeatherFX" add_child(weather_fx) weather_fx.setup(weather, player) if menu_mode: day_night.time = 9.5 / 24.0 day_night.frozen = true day_night.apply(true) player.process_mode = Node.PROCESS_MODE_DISABLED player.get_node("Camera3D").current = false _menu_cam = Camera3D.new() add_child(_menu_cam) _menu_cam.current = true $HUD.visible = false elif DisplayServer.get_name() != "headless" and not server_mode: hud = preload("res://scripts/ui/hud.gd").new() add_child(hud) hud.bind(player) if not menu_mode and not server_mode: player.survival.died.connect(_on_player_died) player.survival.hurt.connect(func(_amount, _from): Sfx.me("hurt")) if not server_mode: Music.start(self) # (the title screen's backdrop world too: calm music) if Saves.has_world() and not menu_mode and not server_mode: # Save on window close instead of quitting straight away. get_tree().auto_accept_quit = false player.set_physics_process(false) set_physics_process(false) if terrain.built: _on_terrain_ready() else: terrain.ready_built.connect(_on_terrain_ready) # --- survival environment --------------------------------------------------- ## 0..100 air warmth at `pos` before fires: time of day, minus altitude, ## plus the weather. A proper shelter keeps out most of the weather and adds a ## little warmth of its own. func air_warmth(pos: Vector3, sheltered: bool = false) -> float: var w: float = day_night.air_warmth() w -= maxf(pos.y - 12.0, 0.0) * 0.9 var ww: float = weather.warmth_at(pos) if sheltered: w += ww * 0.25 + 8.0 else: w += ww - weather.blizzard_chill(false) # (P4: the wind cuts through you) return w ## 0..1 heat from fires and a held torch. ## `holder` is whose torch counts (default: the local player; on a server, ## each player's character). func heat_at(pos: Vector3, holder: Node = null) -> float: var h := 0.0 for n in get_tree().get_nodes_in_group("heat"): h = maxf(h, n.heat_at(pos)) var who: Node = holder if holder != null else player if who.get("hands") != null: h = maxf(h, who.hands.held_heat()) return h ## Death: everything you carried goes into a grave where you fell, and you ## wake up at the island start. The world carries on. func _on_player_died() -> void: Hints.event("died") Sfx.me("death") var pos := player.global_position pos.y = terrain.sample_height(pos.x, pos.z) if pos.y < terrain.water_level: # Drowned or died in the lake: put the grave on the nearest dry shore. pos = terrain.find_spawn(Vector2(pos.x, pos.z)) # What you were wearing goes in the grave too. if not player.worn.is_empty() and player.inventory.add(player.worn, 1) == 0: player.worn = "" player.worn_changed.emit() var has_items := false for st in player.inventory.slots: if st != null: has_items = true if has_items: var grave := spawn_piece(ItemDB.get_item("grave"), Transform3D(Basis(Vector3.UP, player.rotation.y), pos)) grave.inventory.load_data(player.inventory.to_data()) player.inventory.load_data(Inventory.new(player.inventory.size()).to_data()) # The death screen: you stay down for a few seconds, then Respawn. # (Headless, with no screen, you wake straight away.) dead_waiting = true player.velocity = Vector3.ZERO player.set_physics_process(false) if hud != null: hud.close_screens() hud.show_death(player.survival.cause, _death_note(has_items, spawn_bed() != null), RESPAWN_DELAY, respawn_after_death) else: respawn_after_death() const RESPAWN_DELAY := 3.0 ## seconds on the death screen before you can respawn var dead_waiting := false ## dead, on the death screen static func _death_note(has_items: bool, to_bed: bool, penalty: String = "grave") -> String: var things := "You had nothing on you." if penalty == "keep": things = "You keep everything you were carrying." elif has_items: things = "Everything you carried is scattered where you fell: anyone can pick it up." if penalty == "drop_all" \ else "Your belongings are in a grave where you fell." return "%s\nYou'll wake %s." % [things, "at your bed" if to_bed else "at the island start"] ## Respawn (the death screen's button): at your bed, or the island start. func respawn_after_death() -> void: if not dead_waiting: return dead_waiting = false player.set_physics_process(true) var bed := spawn_bed() if bed != null: _respawn_at_bed(bed) else: _respawn() if has_bed_spawn: has_bed_spawn = false Actions.notify.emit("Your bed is gone, so you woke at the island start") player.survival.reset_after_death() ## Online: the server says we died. Same screen; Respawn asks the server. func died_online(text: String, has_items: bool, to_bed: bool, penalty: String = "grave") -> void: Hints.event("died") Sfx.me("death") dead_waiting = true player.velocity = Vector3.ZERO player.set_physics_process(false) if hud != null: hud.close_screens() hud.show_death(text, _death_note(has_items, to_bed, penalty), RESPAWN_DELAY, func(): PlayerNet.ask("respawn")) else: get_tree().create_timer(RESPAWN_DELAY + 0.1).timeout.connect(func(): PlayerNet.ask("respawn")) # --- beds and sleeping ------------------------------------------------------ ## The bed you respawn at, if it still exists. func spawn_bed() -> Node3D: if not has_bed_spawn: return null for p in _pieces.get_children(): if p.item_id == "bed" and not p.is_queued_for_deletion() and p.global_position.distance_to(bed_spawn) < 0.3: return p return null func set_bed_spawn(bed: Node3D) -> void: has_bed_spawn = true bed_spawn = bed.global_position func _respawn_at_bed(bed: Node3D) -> void: player.global_position = bed.global_position + Vector3(0, 0.9, 0) player.velocity = Vector3.ZERO player.rotation = Vector3(0.0, bed.global_basis.get_euler().y + PI, 0.0) player.get_node("Camera3D").rotation.x = 0.0 ## Online: the server says we died (or were moved) and puts us here. func respawn_from_server(at: Vector3, yaw: float) -> void: dead_waiting = false player.set_physics_process(true) if hud != null: hud.close_screens() hud.hide_death() player.global_position = at player.velocity = Vector3.ZERO player.rotation = Vector3(0.0, yaw, 0.0) player.get_node("Camera3D").rotation.x = 0.0 ## Why you can't sleep in `bed` right now ("" = you can). func sleep_block_reason(bed: Node3D) -> String: var h: float = day_night.hours() if h > 5.5 and h < 19.5: return "You can only sleep at night (after 19:30)" if not build.sheltered_at(bed.global_position): return "Your bed needs a roof and walls around it" if wolves != null: for w in wolves.alive(): if w.state in ["stalk", "lunge", "defend", "retreat"] and w.global_position.distance_to(bed.global_position) < 40.0: return "You can't sleep with wolves prowling outside" return "" ## Sleeps until 06:00: the clock, fires, regrowth and weather all move on. ## On a server you get into bed, and the night passes once enough players ## are in bed (the server's sleep rule, player_net.gd). func sleep(p: Node3D) -> void: if server_mode: PlayerNet.go_to_bed(p) return var secs: float = fposmod(6.0 - day_night.hours(), 24.0) / 24.0 * day_night.day_length if hud != null: hud.fade(func(): _finish_sleep(p, secs)) else: _finish_sleep(p, secs) func _finish_sleep(p: Node3D, secs: float) -> void: skip_time(secs) p.survival.after_sleep(secs) save_now() Actions.notify.emit("You slept until morning") ## Fast-forwards the world (not the player) by `secs`. func skip_time(secs: float) -> void: day_night.advance(secs) day_night.apply(true) weather.snap() props.skip_time(secs) for m in get_tree().get_nodes_in_group("processor"): m.step(secs) # (kilns and smelters work through the night) for f in get_tree().get_nodes_in_group("heat"): if f.has_method("burn"): f.burn(secs) spoil_all() # (food goes off overnight too) func _on_terrain_ready() -> void: var t0 := Time.get_ticks_msec() _spawn = terrain.find_spawn(Vector2.ZERO) var lake: Vector2 = terrain.lake_center var to_lake := lake - Vector2(_spawn.x, _spawn.z) _spawn_yaw = atan2(-to_lake.x, -to_lake.y) player.water_level = terrain.water_level 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). den = preload("res://scripts/creatures/den.gd").new() den.name = "WolfDen" add_child(den) if den.place(self, props._rock_mat): props.static_cuts.append([den.global_transform, 2.2, 3.4]) _update_grass_cuts() # The barrow: scenery too (P3), out on open ground. Its guards only run # where creatures do (_setup_bear). barrow = preload("res://scripts/world/barrow.gd").new() barrow.name = "Barrow" add_child(barrow) if barrow.place(self): _update_grass_cuts() # The standing stones (P5): where the Old Stag is called. stones = preload("res://scripts/world/standing_stones.gd").new() stones.name = "StandingStones" add_child(stones) if stones.place(self): _update_grass_cuts() if DisplayServer.get_name() != "headless": var fish := preload("res://scripts/fishing/lake_fish.gd").new() fish.name = "LakeFish" add_child(fish) fish.setup(terrain) print("[world] scenery placed in %d ms" % (Time.get_ticks_msec() - t0)) if menu_mode: return # Weather hazards (P4): lightning on trees, the hard winter's ice. Decided # here unless we're a player's game online (then the server tells us). hazards = preload("res://scripts/world/hazards.gd").new() hazards.name = "Hazards" add_child(hazards) hazards.setup(self, not is_online()) stones.deciding = not is_online() if server_mode: if Saves.has_world(): var saved: Dictionary = Saves.load_state(Saves.current_id) if not saved.is_empty(): apply_save(saved) # The server runs the wildlife and the wolves for everyone (M2.5): # animals live around the players, and players see them via # creature_net.gd. set_physics_process(false) # (no player of our own to keep on the map) creatures.setup(self) creatures.enabled = not "--no-creatures" in OS.get_cmdline_user_args() if den.found: wolves = preload("res://scripts/creatures/wolf_pack.gd").new() wolves.name = "Wolves" add_child(wolves) wolves.setup(self, den) _setup_bear() apply_rules() return _apply_world_rot() _respawn() if Saves.has_world(): var state: Dictionary = Saves.load_state(Saves.current_id) if not state.is_empty(): apply_save(state) print("[world] loaded '%s'" % Saves.current_id) player.set_physics_process(true) set_physics_process(true) creatures.setup(self) # Old Tam's tips (T4): your own world or a server's, not the tutorial's. if not server_mode and not menu_mode and not bool(Saves.read_meta(Saves.current_id).get("tutorial", false)) \ and not "--tutorial" in OS.get_cmdline_user_args(): Hints.start(self) if is_online(): _go_online() _maybe_capture() return if not "--no-creatures" in OS.get_cmdline_user_args(): creatures.populate() creatures.enabled = true if den.found: wolves = preload("res://scripts/creatures/wolf_pack.gd").new() wolves.name = "Wolves" add_child(wolves) wolves.setup(self, den) # The tutorial (docs/TUTORIAL_DESIGN.md): a world made for it (meta.json # "tutorial"), or any world with `-- --tutorial`. Its island is calm: no # wolf pack (just Old Tam's demo wolf), no bear, nothing stealing your food. var tutorial := bool(Saves.read_meta(Saves.current_id).get("tutorial", false)) or "--tutorial" in OS.get_cmdline_user_args() if tutorial and wolves != null: wolves.enabled = false if not "--no-creatures" in OS.get_cmdline_user_args() and not tutorial: _setup_bear() if tutorial: Guide.start(self, _saved_tutorial) _maybe_capture() ## The island's bear and its patch (bear_range.gd), and the crows and foxes ## after food left out (thieves.gd). The server runs them for everyone; ## players' games see them through creature_net.gd. func _setup_bear() -> void: thieves = preload("res://scripts/creatures/thieves.gd").new() thieves.name = "Thieves" add_child(thieves) thieves.setup(self) thieves.enabled = not "--no-creatures" in OS.get_cmdline_user_args() if barrow != null and barrow.found and thieves.enabled: barrow.start() bears = preload("res://scripts/creatures/bear_range.gd").new() bears.name = "Bear" add_child(bears) bears.setup(self) bears.load_data(_saved_bear) bears.enabled = not "--no-creatures" in OS.get_cmdline_user_args() if not bears.enabled and bears.bear != null: bears.bear.queue_free() # (a server started without wildlife) ## A server's rules for the island (at start-up, and after an admin's /set). func apply_rules() -> void: var R = preload("res://scripts/net/rules.gd") if wolves != null: wolves.den_day = R.grace_days() wolves.hunt_day = R.grace_days() + 1 wolves.pack_bonus = int(R.difficulty("pack")) for p in PlayerNet.proxies.values(): p.survival.drain_mult = R.difficulty("drain") Inventory.rot_speed = ROT_SPEEDS.get(R.rot(), 1.0) # --- food spoilage (P4, docs/PROGRESSION_DESIGN.md §5) ---------------------------- const ROT_SPEEDS := {"normal": 1.0, "slow": 0.5, "off": 0.0} const SPOIL_EVERY := 2.0 var _spoil_left := 0.0 ## Keeps Inventory's clock (food's "rot" stamps) on world time. Players' ## games online run it too (for showing freshness; the server does the rotting). func _tick_clock() -> void: Inventory.day_seconds = float(day_night.day_length) Inventory.clock = weather.world_seconds() ## The single-player world's rot setting, from its meta.json (a server's ## comes from its rules: apply_rules()). func _apply_world_rot() -> void: var r := String(Saves.read_meta(Saves.current_id).get("rot", "normal")) if Saves.has_world() else "normal" Inventory.rot_speed = ROT_SPEEDS.get(r, 1.0) ## Food rots: in everyone's bags, in chests, graves and racks, and lying on ## the ground (where rotten food just goes). Where the game decides (single ## player, a server); players' games online are sent the result. func spoil_all() -> void: if net_sync != null or menu_mode: return _tick_clock() for p in players(): if p.get("inventory") != null: p.inventory.spoil() for piece in all_pieces(): if piece.get("inventory") != null: piece.inventory.spoil() if Inventory.rot_speed <= 0.0: return for d in all_dropped(): if Inventory.keeps_days(d.item_id) <= 0.0: continue if not d.extra.has("rot"): d.extra.rot = Inventory.fresh_rot(d.item_id) elif Inventory.clock >= float(d.extra.rot): take_dropped(d) func _process(delta: float) -> void: if menu_mode: # Slow orbit over the valley, looking toward the lake. _menu_t += delta * 0.025 var r := 70.0 var p := Vector3(cos(_menu_t) * r, 0.0, sin(_menu_t) * r) p.y = maxf(terrain.sample_height(p.x, p.z), terrain.water_level) + 24.0 _menu_cam.global_position = p var lake: Vector2 = terrain.lake_center _menu_cam.look_at(Vector3(lake.x * 0.5, 2.0, lake.y * 0.5), Vector3.UP) return _tick_clock() _spoil_left -= delta if _spoil_left <= 0.0: _spoil_left = SPOIL_EVERY spoil_all() if not Saves.has_world() or get_tree().paused: return _session_time += delta _autosave_left -= delta if _autosave_left <= 0.0: save_now() # --- saving ----------------------------------------------------------------- ## Writes the current game to the active world slot. Returns false if this ## session isn't tied to a slot (e.g. running world.tscn directly). func save_now() -> bool: _autosave_left = autosave_seconds if not Saves.has_world(): return false if dead_waiting and not server_mode: respawn_after_death() # never save someone lying dead if hud != null: hud.hide_death() var ok: bool = Saves.save_state(Saves.current_id, collect_save(), _session_time) if ok: _session_time = 0.0 if server_mode: if not ok: printerr("[server] saving the world FAILED") saved.emit() # (the players' characters save with it) else: Actions.notify.emit("Game saved" if ok else "Save FAILED") return ok ## A server's world saves as it closes, however the server stops (Ctrl+C ## included), unless it has just saved. func _exit_tree() -> void: if server_mode and Saves.has_world() and _session_time > 0.0: save_now() if Guide.world == self: Guide.stop() if Hints.world == self: Hints.stop() Music.stop(self) func _notification(what: int) -> void: if what != NOTIFICATION_WM_CLOSE_REQUEST or menu_mode: return if net_sync != null: Net.left.disconnect(_on_net_left) Net.leave() get_tree().quit() elif Saves.has_world(): save_now() get_tree().quit() static func _xf_to_array(xf: Transform3D) -> Array: var b := xf.basis return [b.x.x, b.x.y, b.x.z, b.y.x, b.y.y, b.y.z, b.z.x, b.z.y, b.z.z, xf.origin.x, xf.origin.y, xf.origin.z] static func _array_to_xf(a: Array) -> Transform3D: return Transform3D(Vector3(a[0], a[1], a[2]), Vector3(a[3], a[4], a[5]), Vector3(a[6], a[7], a[8]), Vector3(a[9], a[10], a[11])) ## Everything the player changed. The rest regenerates from the seed. func collect_save() -> Dictionary: var cam: Camera3D = player.get_node("Camera3D") var harvested := {} for uid in props.resource_nodes: var n = props.resource_nodes[uid] if not is_instance_valid(n) or n.depleted: harvested[uid] = 0.0 elif n.hp < n.max_hp: harvested[uid] = n.hp var regrow := {} for uid in props.forage: var f = props.forage[uid] if is_instance_valid(f) and f.regrow_left > 0.0: regrow[uid] = f.regrow_left var taken: Array = [] for uid in props.pickups: var pk = props.pickups[uid] if not is_instance_valid(pk) or pk.taken: taken.append(uid) var dropped: Array = [] for d in all_dropped(): dropped.append(dropped_entry(d)) var pieces: Array = [] for piece in all_pieces(): pieces.append(piece_entry(piece)) if server_mode: return _shared_state(harvested, regrow, taken, dropped, pieces) return { "player": { "pos": [player.global_position.x, player.global_position.y, player.global_position.z], "yaw": player.rotation.y, "pitch": cam.rotation.x, "inventory": player.inventory.to_data(), "selected": player.selected, "stats": player.survival.to_data(), "worn": player.worn, }, "dropped": dropped, "time": {"day": day_night.day, "time": day_night.time}, "tutorial": Guide.save_data() if Guide.world == self else _saved_tutorial, "spawn": {"bed": has_bed_spawn, "pos": [bed_spawn.x, bed_spawn.y, bed_spawn.z]}, "forage_regrow": regrow, "regrow": props.regrow.duplicate(), "harvested": harvested, "pickups_taken": taken, "pieces": pieces, "bear": bears.save_data() if bears != null else _saved_bear, "barrow": barrow.save_data() if barrow != null else {}, "stones": stones.save_data() if stones != null else {}, } func _shared_state(harvested: Dictionary, regrow: Dictionary, taken: Array, dropped: Array, pieces: Array) -> Dictionary: return { "dropped": dropped, "time": {"day": day_night.day, "time": day_night.time}, "forage_regrow": regrow, "regrow": props.regrow.duplicate(), "harvested": harvested, "pickups_taken": taken, "pieces": pieces, "bear": bears.save_data() if bears != null else _saved_bear, "barrow": barrow.save_data() if barrow != null else {}, "stones": stones.save_data() if stones != null else {}, } ## The live pieces (placed and not being removed). func all_pieces() -> Array: return _pieces.get_children().filter(func(p): return not p.is_queued_for_deletion()) func all_dropped() -> Array: return _dropped.get_children().filter(func(d): return not d.is_queued_for_deletion() and not d.taken) ## One piece as saved (and as sent to players): its item, where it is, what's ## in it, its state (fuel, door open), and its net id if it has one. func piece_entry(piece: Node3D) -> Dictionary: var entry := {"item": piece.item_id, "xf": _xf_to_array(piece.global_transform)} if piece.get("inventory") != null: entry["inventory"] = piece.inventory.to_data() if piece.has_method("save_extra"): entry["extra"] = piece.save_extra() if piece.has_meta("nid"): entry["nid"] = int(piece.get_meta("nid")) if piece.has_meta("owner"): entry["owner"] = String(piece.get_meta("owner")) entry["owner_name"] = String(piece.get_meta("owner_name", "")) return entry func dropped_entry(d: Node3D) -> Dictionary: var e := {"item": d.item_id, "count": d.amount, "xf": _xf_to_array(d.global_transform)} if not d.extra.is_empty(): e["extra"] = d.extra if d.has_meta("nid"): e["nid"] = int(d.get_meta("nid")) return e ## The shared state a server sends a player who joins (everything but the ## server's own bookkeeping). func collect_shared() -> Dictionary: return collect_save() ## A joining player's copy of the server's island. func apply_shared(s: Dictionary) -> void: apply_save(s, false) props.regrow.clear() # the server says when things grow back func add_piece_entry(e: Dictionary) -> Node3D: var def: Dictionary = ItemDB.get_item(e.get("item", "")) if def.is_empty() or not def.has("place"): return null var piece := spawn_piece(def, _array_to_xf(e.xf)) if e.has("inventory") and piece.get("inventory") != null: piece.inventory.load_data(e.inventory) if e.has("extra") and piece.has_method("load_extra"): piece.load_extra(e.extra) if e.has("nid"): piece.set_meta("nid", int(e.nid)) if e.has("owner"): piece.set_meta("owner", String(e.owner)) piece.set_meta("owner_name", String(e.get("owner_name", ""))) return piece func add_dropped_entry(e: Dictionary) -> Node3D: if ItemDB.get_item(e.get("item", "")).is_empty(): return null var ex: Dictionary = {} if e.get("extra") is Dictionary: ex = Inventory.extra_of(e.extra) # (a worn tool's "dur", food's "rot") var d := _make_dropped(e.item, int(e.get("count", 1)), _array_to_xf(e.xf), ex) if e.has("nid"): d.set_meta("nid", int(e.nid)) return d ## Pieces the server took out (dismantled, collapsed, a grave emptied). No ## refunds here: the server sends those to whoever did it. func remove_pieces_net(gone: Array, collapsed_nids: Array) -> void: var any_build := false for p in gone: if p.item_id == "bed" and spawn_bed() == p: has_bed_spawn = false if p.is_in_group("build"): any_build = true if int(p.get_meta("nid", 0)) in collapsed_nids: _collapse_fx(p) _pieces.remove_child(p) p.queue_free() if any_build: build.recompute() _settle_loose_pieces() _update_grass_cuts() func apply_save(s: Dictionary, with_player: bool = true) -> void: _applying = true _apply_save(s, with_player) _applying = false func _apply_save(s: Dictionary, with_player: bool) -> void: var p: Dictionary = s.get("player", {}) if with_player else {} if p.has("pos"): player.global_position = Vector3(p.pos[0], p.pos[1] + 0.05, p.pos[2]) player.rotation = Vector3(0.0, float(p.get("yaw", 0.0)), 0.0) player.get_node("Camera3D").rotation.x = float(p.get("pitch", 0.0)) if with_player: if p.has("inventory"): player.inventory.load_data(p.inventory) player.select(int(p.get("selected", 0))) if p.has("stats"): player.survival.load_data(p.stats) player.worn = String(p.get("worn", "")) player.worn_changed.emit() for d in s.get("dropped", []): add_dropped_entry(d) var t: Dictionary = s.get("time", {}) if not t.is_empty(): day_night.day = int(t.get("day", 1)) day_night.time = float(t.get("time", day_night.time)) day_night.apply(true) var regrow: Dictionary = s.get("forage_regrow", {}) for uid in regrow: if props.forage.has(uid): props.forage[uid].restore(float(regrow[uid])) var harvested: Dictionary = s.get("harvested", {}) for uid in harvested: if props.resource_nodes.has(uid): props.resource_nodes[uid].restore(float(harvested[uid])) for uid in s.get("pickups_taken", []): if props.pickups.has(uid) and is_instance_valid(props.pickups[uid]): props.pickups[uid].collected() # Regrowth timers (older saves have none: everything gone starts a fresh timer). props.restore_regrow(s.get("regrow", {})) _saved_tutorial = s.get("tutorial", {}) if with_player else {} if barrow != null: barrow.load_data(s.get("barrow", {})) if stones != null: stones.load_data(s.get("stones", {})) # The bear (bear_range.gd): applied when it's set up, after loading. _saved_bear = s.get("bear", {}) if bears != null: bears.load_data(_saved_bear) var sp: Dictionary = s.get("spawn", {}) has_bed_spawn = bool(sp.get("bed", false)) if sp.has("pos"): bed_spawn = Vector3(sp.pos[0], sp.pos[1], sp.pos[2]) weather.snap() _loading = true for e in s.get("pieces", []): add_piece_entry(e) _loading = false build.recompute() # same terrain, same pieces: nothing collapses on load _update_grass_cuts() func _respawn() -> void: if net_sync != null: net_sync.mark_teleport() # tell the server this jump back to the spawn is legitimate player.global_position = _spawn + Vector3(0.0, 1.5, 0.0) player.velocity = Vector3.ZERO player.rotation = Vector3(0.0, _spawn_yaw, 0.0) player.get_node("Camera3D").rotation.x = 0.0 func _physics_process(_delta: float) -> void: var p := player.global_position var half: float = terrain.size * 0.5 if p.y < terrain.min_height - 30.0 or absf(p.x) > half or absf(p.z) > half \ or not terrain.inside_boundary(p.x, p.z, -4.0): push_warning("[world] player left the map at %s; respawning" % p) _respawn() # --- building --------------------------------------------------------------- ## Whether a piece can go at `xf`: inside the valley, on dry fairly flat ## ground (or a floor), and not overlapping trees, rocks or other pieces. ## Building pieces have their own rules (build_system.gd). func placement_valid(def: Dictionary, xf: Transform3D) -> bool: if BuildPiece.is_build(def): return build.check(def, xf).is_empty() var p := xf.origin if not terrain.inside_boundary(p.x, p.z, 1.5): return false if p.y < terrain.water_level + 0.15: return false var fp: Array = def.place.get("footprint", [1.0, 1.0]) var hx := float(fp[0]) * 0.5 var hz := float(fp[1]) * 0.5 # Something solid (ground or a floor) under each corner, close to the base. var space := get_world_3d().direct_space_state for c in [Vector3(-hx, 0, -hz), Vector3(hx, 0, -hz), Vector3(-hx, 0, hz), Vector3(hx, 0, hz)]: var w: Vector3 = xf * c var rq := PhysicsRayQueryParameters3D.create(w + Vector3(0, 0.45, 0), w - Vector3(0, 0.45, 0), 1, [player.get_rid()]) if space.intersect_ray(rq).is_empty(): return false # Overlap test against everything solid except the ground itself. var box := BoxShape3D.new() box.size = Vector3(hx * 2.0 - 0.1, 1.0, hz * 2.0 - 0.1) var q := PhysicsShapeQueryParameters3D.new() q.shape = box q.transform = Transform3D(xf.basis, p + Vector3(0, 0.75, 0)) q.collision_mask = 1 q.exclude = [terrain.get_node("TerrainBody").get_rid(), player.get_rid()] return get_world_3d().direct_space_state.intersect_shape(q, 1).is_empty() ## `extra` is the piece's own state (a grave's owner, a door left open...). func spawn_piece(def: Dictionary, xf: Transform3D, extra: Dictionary = {}) -> Node3D: var script: Script = PIECES.get(def.place.piece, preload("res://scripts/building/piece.gd")) var piece: StaticBody3D = script.new() piece.setup(def) _pieces.add_child(piece) piece.global_transform = xf if not extra.is_empty() and piece.has_method("load_extra"): piece.load_extra(extra) if piece.is_in_group("build") and not _loading: build.recompute() if piece.kind == "floor": _update_grass_cuts() if server_mode and not _applying: piece_added.emit(piece) return piece ## Grass doesn't grow up through floors. func _update_grass_cuts() -> void: var xfs: Array = [] for p in _pieces.get_children(): if p.is_in_group("build") and p.kind == "floor" and not p.is_queued_for_deletion(): xfs.append(p.global_transform) props.set_grass_cuts(xfs, 1.02) ## What water is at (x, z): {"type": "lake"|"sea", "surface": y, "depth": m}, ## or {} on dry land. The one place that knows this island's water bodies, so ## fishing (and later swimming, boats) work on any world that answers it. func water_at(x: float, z: float) -> Dictionary: var ground: float = terrain.sample_height(x, z) if terrain.inside_boundary(x, z, -2.0) and ground < terrain.water_level: if hazards != null and hazards.iced(x, z): return {} # (frozen over in a hard winter: cast further out) return {"type": "lake", "surface": terrain.water_level, "depth": terrain.water_level - ground} if terrain.sea_weight(x, z) > 0.5 and ground < terrain.sea_level: return {"type": "sea", "surface": terrain.sea_level, "depth": terrain.sea_level - ground} return {} # --- multiplayer ----------------------------------------------------------------- ## Joined a server: start where it says, follow its clock, show the others. ## Animals stay off until the server runs them (M2.5): otherwise everyone ## would see different wildlife. func _go_online() -> void: _spawn = Net.world_info.get("island_spawn", _spawn) var start: Vector3 = Net.world_info.get("spawn", _spawn) player.global_position = start + Vector3(0.0, 0.05, 0.0) if Net.world_info.has("yaw"): player.rotation.y = float(Net.world_info.yaw) # facing the way you were when you left net_sync = preload("res://scripts/net/net_sync.gd").new() net_sync.name = "NetSync" add_child(net_sync) net_sync.setup(self) Net.left.connect(_on_net_left) get_tree().auto_accept_quit = false # leave the server cleanly on window close # The island's changes come from the server from now on. props.regrow_enabled = false WorldNet.join_world(self) PlayerNet.join_world(self) # and our inventory and stats CreatureNet.join_world(self) # the server's animals, wolves, arrows and floats if hud != null: var bar := UITheme.label("Online: %s · T to chat (/help)" % Net.server_info.get("server_name", "server"), 14, UITheme.TEXT_DIM) bar.position = Vector2(16, 40) $HUD.add_child(bar) Actions.notify.emit("Connected to %s" % Net.server_info.get("server_name", "the server")) if Net.hosting: var at := Net.my_addresses() Actions.notify.emit("You're hosting. Friends join at %s" % (", ".join(PackedStringArray(at)) if not at.is_empty() else "this computer's address")) func _on_net_left(_reason: String) -> void: WorldNet.leave_world() PlayerNet.leave_world() CreatureNet.leave_world() if is_inside_tree(): get_tree().paused = false get_tree().auto_accept_quit = true get_tree().change_scene_to_file.call_deferred("res://scenes/title.tscn") ## Leaves `count` of an item lying in the world at `xf` (an arrow stuck in a ## tree, a thrown spear, meat that didn't fit). Saved with the world. ## Online only the server drops things (our game never does: returns null). ## `extra` is what's special about it (a worn tool's "dur"), kept when it's ## picked up again. func drop_item(id: String, count: int, xf: Transform3D, extra: Dictionary = {}) -> Node3D: if net_sync != null: push_warning("[world] drop_item(%s) on a client: only the server drops items" % id) return null return _make_dropped(id, count, xf, extra) ## Something other than a player took a dropped item (the bear ate it, a fox ## ran off with it). On a server, everyone's game is told it's gone. func take_dropped(pk: Node3D) -> void: if server_mode and pk.has_meta("nid"): WorldNet.taken("#%d" % int(pk.get_meta("nid"))) pk.collected() func _make_dropped(id: String, count: int, xf: Transform3D, extra: Dictionary = {}) -> Node3D: var pk: Area3D = preload("res://scripts/world/pickup.gd").new() pk.setup(id, ItemDB.mesh_for(id), ItemDB.item_material(), 0.3) pk.amount = count pk.dropped = true pk.extra = extra.duplicate() _dropped.add_child(pk) pk.global_transform = xf if server_mode and not _applying: dropped_added.emit(pk) return pk ## Casts a fishing float (Actions validated it). One at a time. ## One float per player (on a server, everyone can fish at once). `bobber` ## is the local player's. func start_fishing(p: Node3D, origin: Vector3, vel: Vector3) -> Node3D: end_fishing(p) var b: Node3D = preload("res://scripts/fishing/bobber.gd").new() b.setup(self, p, origin, vel, randi()) add_child(b) bobbers[p] = b if p == player: bobber = b return b func end_fishing(p: Node3D = null) -> void: if p == null: p = player if is_online() and p == player and bobber != null: PlayerNet.ask("reel") # (switched away from the rod: the server reels in) var b: Node3D = bobbers.get(p) bobbers.erase(p) if b != null and is_instance_valid(b): b.queue_free() if p == player: if bobber != null and is_instance_valid(bobber) and bobber != b: bobber.queue_free() # (online: the server's float, shown here) bobber = null func bobber_of(p: Node3D) -> Node3D: var b: Node3D = bobbers.get(p) return b if b != null and is_instance_valid(b) else null var bobbers: Dictionary = {} ## player -> their float # --- who's here ------------------------------------------------------------ ## A server's difficulty for one thing (rules.gd); 1 in single-player. func difficulty_mult(k: String) -> float: return preload("res://scripts/net/rules.gd").difficulty(k) if server_mode else 1.0 ## Everyone animals react to: the local player in single-player; on a ## server, every player's character that's alive (player_net.gd). func players() -> Array: if server_mode: return PlayerNet.proxies.values().filter(func(p): return is_instance_valid(p) and not p.survival.dead) return [player] ## The nearest of them to `pos`, or null if nobody's around (a server with ## everyone dead or gone). func nearest_player(pos: Vector3) -> Node3D: if not server_mode: return player var best: Node3D = null for p in players(): if best == null or p.global_position.distance_to(pos) < best.global_position.distance_to(pos): best = p return best ## Fires an arrow or thrown spear (Actions validated it). func launch(id: String, origin: Vector3, vel: Vector3, damage: float, shooter: Node3D) -> Node3D: var pr: Node3D = preload("res://scripts/creatures/projectile.gd").new() pr.setup(id, self, origin, vel, damage, shooter) add_child(pr) if server_mode: launched.emit(id, origin, vel, shooter) # everyone sees it fly (creature_net.gd) return pr signal launched(id: String, origin: Vector3, vel: Vector3, shooter: Node3D) ## Takes a piece out of the world (dismantled). Anything that loses its ## support collapses, and its item goes back to `refund_to` if there's room. ## Returns how many pieces collapsed. func remove_piece(piece: Node3D, refund_to: Node3D = null) -> int: if piece.item_id == "bed" and spawn_bed() == piece: has_bed_spawn = false Actions.notify.emit("Spawn point cleared") var was_build := piece.is_in_group("build") _pieces.remove_child(piece) piece.queue_free() if not was_build: if server_mode: pieces_removed.emit([piece], []) return 0 var weak: Array = build.recompute() if server_mode: pieces_removed.emit([piece] + weak, weak) for w in weak: if refund_to != null: Actions._give(refund_to, w.item_id, 1) _collapse_fx(w) _pieces.remove_child(w) w.queue_free() if not weak.is_empty(): build.recompute() Actions.notify.emit("%d piece%s collapsed" % [weak.size(), "" if weak.size() == 1 else "s"]) _settle_loose_pieces() _update_grass_cuts() return weak.size() ## A collapsing piece: a copy of its mesh tumbles down and sinks away. func _collapse_fx(piece: Node3D) -> void: if DisplayServer.get_name() == "headless": return var mi := MeshInstance3D.new() mi.mesh = piece.get_node("Mesh").mesh mi.material_override = ItemDB.item_material() add_child(mi) mi.global_transform = piece.global_transform var tw := mi.create_tween().set_parallel(true) tw.tween_property(mi, "global_position:y", mi.global_position.y - 4.0, 1.2).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) tw.tween_property(mi, "rotation:x", mi.rotation.x + randf_range(-0.8, 0.8), 1.2) tw.chain().tween_callback(mi.queue_free) ## Chests, fires and beds left hanging after a collapse drop to whatever is ## below them. func _settle_loose_pieces() -> void: var space := get_world_3d().direct_space_state for p in _pieces.get_children(): if p.is_in_group("build") or p.is_queued_for_deletion(): continue var at: Vector3 = p.global_position var rq := PhysicsRayQueryParameters3D.create(at + Vector3(0, 0.3, 0), at - Vector3(0, 0.5, 0), 1, [p.get_rid()]) if not space.intersect_ray(rq).is_empty(): continue rq = PhysicsRayQueryParameters3D.create(at, at - Vector3(0, 80, 0), 1, [p.get_rid(), player.get_rid()]) var hit := space.intersect_ray(rq) p.global_position.y = hit.position.y if not hit.is_empty() else terrain.sample_height(at.x, at.z) # --- dev screenshot hook ---------------------------------------------------- func _maybe_capture() -> void: var prefix := "" for a in OS.get_cmdline_user_args(): if a.begins_with("--shot="): prefix = a.trim_prefix("--shot=") if prefix.is_empty(): return var lake: Vector2 = terrain.lake_center # [name, eye position (y = height above ground), look-at point] ; null = spawn view var views := [ ["spawn", null, null], ["aerial", Vector3(0, 110, 150), Vector3(0, 0, -20)], ["lake", Vector3(lake.x - 30.0, 3.0, lake.y + 22.0), Vector3(lake.x, -2.0, lake.y)], ["forest", Vector3(-40, 1.7, 30), Vector3(-70, 4.0, 10)], ["rim", Vector3(0, 2.0, 70), Vector3(0, 25.0, 125)], ] # Sea-cliff views: from the cliff top out to sea, and from the water back at the cliff. var sa: float = terrain.sea_angle_deg var top: Vector2 = terrain.coast_point(sa + 12.0, -6.0) var out: Vector2 = terrain.coast_point(sa + 12.0, 200.0) views.append(["seacliff_top", Vector3(top.x, 1.7, top.y), Vector3(out.x, terrain.sea_level - 6.0, out.y)]) var boat: Vector2 = terrain.coast_point(sa - 6.0, 38.0) var face: Vector2 = terrain.coast_point(sa + 4.0, 0.0) var boat_eye := Vector3(boat.x, terrain.sea_level + 3.0 - terrain.sample_height(boat.x, boat.y), boat.y) views.append(["seacliff_from_sea", boat_eye, Vector3(face.x, terrain.sea_level + 8.0, face.y)]) # Along the cliff edge into the western headland (where the old pit was). var walk: Vector2 = terrain.coast_point(sa - 20.0, -8.0) var head: Vector2 = terrain.coast_point(sa - 52.0, 0.0) views.append(["headland", Vector3(walk.x, 1.7, walk.y), Vector3(head.x, 2.0, head.y)]) await get_tree().create_timer(4.0).timeout set_physics_process(false) # views may sit outside the walkable area var cam: Camera3D = player.get_node("Camera3D") for v in views: if v[1] != null: player.set_physics_process(false) var eye: Vector3 = v[1] eye.y += terrain.sample_height(eye.x, eye.z) var dir: Vector3 = (v[2] as Vector3) - eye player.global_position = eye - Vector3(0.0, cam.position.y, 0.0) player.rotation = Vector3(0.0, atan2(-dir.x, -dir.z), 0.0) cam.rotation.x = atan2(dir.y, Vector2(dir.x, dir.z).length()) props._update_grass(100000) await get_tree().create_timer(1.0).timeout await RenderingServer.frame_post_draw var path := "%s_%s.png" % [prefix, v[0]] get_viewport().get_texture().get_image().save_png(path) print("[world] saved ", path) await _capture_survival(prefix, cam) get_tree().quit() ## Survival views: a small camp in front of the spawn (axe in hand), then the ## inventory screen with a chest open. func _capture_survival(prefix: String, cam: Camera3D) -> void: _respawn() var inv: Inventory = player.inventory for pair in [["stone_axe", 1], ["stone_pickaxe", 1], ["campfire", 1], ["club", 1], ["wood", 23], ["stick", 12], ["stone", 17], ["flint", 5]]: inv.add(pair[0], pair[1]) var fwd := -player.global_basis.z var right := player.global_basis.x var chest: Node = null for k in [["campfire", 4.5, 0.0], ["workbench", 7.0, -2.8], ["chest", 6.5, 2.4]]: var p: Vector3 = player.global_position + fwd * float(k[1]) + right * float(k[2]) p.y = terrain.sample_height(p.x, p.z) var piece := spawn_piece(ItemDB.get_item(k[0]), Transform3D(Basis(Vector3.UP, player.rotation.y), p)) if k[0] == "chest": chest = piece player.select(0) cam.rotation.x = -0.22 await get_tree().create_timer(2.5).timeout await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png("%s_camp.png" % prefix) print("[world] saved %s_camp.png" % prefix) # Same camp at dusk and at night, torch in hand. inv.set_slot(1, {"id": "torch", "count": 1}) player.select(1) for shot in [["dusk", 18.3], ["night", 22.5]]: day_night.time = float(shot[1]) / 24.0 day_night.apply(true) await get_tree().create_timer(1.5).timeout await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png("%s_camp_%s.png" % [prefix, shot[0]]) print("[world] saved %s_camp_%s.png" % [prefix, shot[0]]) day_night.time = 9.0 / 24.0 day_night.apply(true) chest.inventory.add("wood", 40) chest.inventory.add("stone", 12) player.global_position = chest.global_position + fwd * -2.0 hud._screen.open(chest) await get_tree().create_timer(0.6).timeout await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png("%s_inventory.png" % prefix) print("[world] saved %s_inventory.png" % prefix) hud._screen.close() hud._menu.open() await get_tree().create_timer(0.4).timeout await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png("%s_menu.png" % prefix) print("[world] saved %s_menu.png" % prefix) hud._menu.close() await _capture_weather(prefix, cam) await _capture_creatures(prefix, cam) await _capture_wolves_fishing(prefix, cam) await _capture_players(prefix, cam) ## Two sample players (as other people would see them) at the camp. func _capture_players(prefix: String, cam: Camera3D) -> void: weather.force("clear") weather.snow_cover = 0.0 weather.ground_wet = 0.0 day_night.time = 10.0 / 24.0 day_night.apply(true) _respawn() player.set_physics_process(false) var fwd := -player.global_basis.z var right := player.global_basis.x var root := Node3D.new() add_child(root) var AvatarScript = preload("res://scripts/net/avatar.gd") var made: Array = [] for d in [["Freya", {"tunic": 2, "hair": 2, "beard": false}, 3.2, -0.9, "stone_axe", 1], ["Old Bjorn", {"tunic": 1, "hair": 4, "beard": true}, 4.4, 1.3, "", 0]]: var av: Node3D = AvatarScript.new() av.setup(made.size() + 2, {"name": d[0], "appearance": d[1]}) root.add_child(av) var p: Vector3 = player.global_position + fwd * float(d[2]) + right * float(d[3]) p.y = terrain.sample_height(p.x, p.z) var face: float = player.rotation.y + PI + float(d[3]) * 0.3 av.push(0.0, p, face, 0.0, d[5], d[4]) av.push(1.0, p + fwd * 0.01 * d[5], face, 0.0, d[5], d[4]) made.append(av) player.select(0) cam.rotation.x = -0.12 for i in range(30): for av in made: av.update_at(0.5 + i * 0.016, 0.016) await get_tree().process_frame await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png("%s_players.png" % prefix) print("[world] saved %s_players.png" % prefix) day_night.time = 19.2 / 24.0 day_night.apply(true) await get_tree().create_timer(0.5).timeout await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png("%s_players_dusk.png" % prefix) print("[world] saved %s_players_dusk.png" % prefix) root.queue_free() ## The den by day with the pack resting, wolves stalking at night, and ## fishing from the lake shore. func _capture_wolves_fishing(prefix: String, cam: Camera3D) -> void: if wolves == null: return for c in creatures.get_children(): c.queue_free() wolves.enabled = false day_night.day = 5 day_night.time = 10.0 / 24.0 day_night.apply(true) wolves.step(0.1) var out: Vector3 = (den.mouth_pos() - den.position).normalized() var eye: Vector3 = den.position + Basis(Vector3.UP, 0.35) * out * 11.0 eye.y = terrain.sample_height(eye.x, eye.z) + 1.7 player.select(0) _aim_camera(cam, eye, den.position + Vector3(0, 1.0, 0)) for w in wolves.alive(): w.set_physics_process(false) for _i in range(30): w._animate(0.1) await get_tree().create_timer(1.5).timeout await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png("%s_den.png" % prefix) print("[world] saved %s_den.png" % prefix) # Night: the pack circling you in the open. var site: Vector3 = build.debug_find_site(_spawn + Vector3(-10, 0, 10), 6.0) site.y = terrain.sample_height(site.x, site.z) day_night.time = 22.5 / 24.0 day_night.apply(true) var live: Array = wolves.alive() for i in range(live.size()): var a := -0.5 + i * 0.7 var p: Vector3 = site + Vector3(sin(a), 0, -cos(a)) * (7.0 + i * 1.5) live[i].global_position = Vector3(p.x, terrain.sample_height(p.x, p.z), p.z) live[i].state = "stalk" var to: Vector3 = site - p live[i].yaw = atan2(-to.x, -to.z) live[i].rotation.y = live[i].yaw live[i]._visual.position.y = 0.0 for l in live[i]._legs: l.rotation.x = 0.0 player.inventory.set_slot(1, {"id": "torch", "count": 1}) player.select(1) var e2: Vector3 = site + Vector3(0, 1.7, 0) _aim_camera(cam, e2, site + Vector3(0, 0.6, -8.0)) await get_tree().create_timer(1.5).timeout await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png("%s_wolves_night.png" % prefix) print("[world] saved %s_wolves_night.png" % prefix) for w in live: w.global_position = den.slot_pos(w.slot) # Fishing at the lake in the evening light, a fish on the line. day_night.time = 18.2 / 24.0 day_night.apply(true) var lake: Vector2 = terrain.lake_center var x: float = lake.x while terrain.sample_height(x, lake.y) < terrain.water_level + 0.4 and x < lake.x + 60.0: x += 0.5 var shore := Vector3(x + 1.0, 0, lake.y) shore.y = terrain.sample_height(shore.x, shore.z) player.inventory.set_slot(2, {"id": "fishing_rod", "count": 1}) player.select(2) var fe: Vector3 = shore + Vector3(0, 1.7, 0) # Midday, looking at the nearest fish, one mid-jump. var lf: Node = get_node_or_null("LakeFish") if lf != null and not lf._fish.is_empty(): day_night.time = 12.5 / 24.0 day_night.apply(true) var t_now := Time.get_ticks_msec() / 1000.0 var best := 0 for i in range(lf._fish.size()): if lf._swim_pos(i, t_now).distance_to(fe) < lf._swim_pos(best, t_now).distance_to(fe): best = i _aim_camera(cam, fe, lf._swim_pos(best, t_now)) lf._jump_left = 0.4 await get_tree().create_timer(0.75).timeout await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png("%s_lake_fish.png" % prefix) print("[world] saved %s_lake_fish.png (fish %.0f m away)" % [prefix, lf._swim_pos(best, t_now).distance_to(fe)]) day_night.time = 18.2 / 24.0 day_night.apply(true) _aim_camera(cam, fe, Vector3(lake.x + 6.0, terrain.water_level - 1.5, lake.y + 2.0)) await get_tree().create_timer(0.3).timeout start_fishing(player, player.hands.rod_tip(), Vector3.ZERO) bobber.set_physics_process(false) bobber.global_position = Vector3(lake.x + 9.0, terrain.water_level, lake.y + 1.5) bobber.state = "biting" await get_tree().create_timer(1.5).timeout await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png("%s_fishing.png" % prefix) print("[world] saved %s_fishing.png" % prefix) end_fishing() ## Wildlife views: a rabbit, a deer and a boar in a meadow, the bow drawn on ## a deer, and a carcass with an arrow in it. func _capture_creatures(prefix: String, cam: Camera3D) -> void: weather.force("clear") weather.snow_cover = 0.0 weather.ground_wet = 0.0 day_night.time = 9.5 / 24.0 day_night.apply(true) creatures.enabled = false for c in creatures.get_children(): c.queue_free() _respawn() player.survival.wet = 0.0 # Open meadow toward the lake, looking the same way as the spawn view. var site: Vector3 = build.debug_find_site(player.global_position - player.global_basis.z * 16.0 + player.global_basis.x * 10.0, 7.0) site.y = terrain.sample_height(site.x, site.z) var hb := Basis(Vector3.UP, player.rotation.y) var line: Array = [] for k in [["rabbit", Vector3(-1.6, 0, -4.0), 0.9], ["deer", Vector3(1.4, 0, -8.0), -1.2], ["boar", Vector3(-3.8, 0, -7.5), 2.1]]: var c: Node3D = creatures.spawn(k[0], site + hb * (k[1] as Vector3)) c.set_physics_process(false) c.yaw = player.rotation.y + float(k[2]) c.rotation.y = c.yaw line.append(c) var eye: Vector3 = site + hb * Vector3(0, 0, 1.0) eye.y = terrain.sample_height(eye.x, eye.z) + 1.7 player.set_physics_process(false) player.inventory.set_slot(2, {"id": "spear", "count": 1}) player.select(2) _aim_camera(cam, eye, site + hb * Vector3(-0.8, 0.4, -6.0)) await get_tree().create_timer(1.5).timeout await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png("%s_animals.png" % prefix) print("[world] saved %s_animals.png" % prefix) # Bow drawn on the deer. player.inventory.set_slot(3, {"id": "bow", "count": 1}) player.inventory.set_slot(4, {"id": "arrow", "count": 12}) player.select(3) var deer: Node3D = line[1] _aim_camera(cam, eye, deer.global_position + Vector3(0, 1.1, 0)) player.hands._drawing = true player.hands._draw_t = 0.8 await get_tree().create_timer(0.8).timeout await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png("%s_bow.png" % prefix) print("[world] saved %s_bow.png" % prefix) player.hands._drawing = false # A felled deer. deer.take_hit(100.0, eye, "arrow") player.select(0) var back := eye - deer.global_position back.y = 0.0 var near: Vector3 = deer.global_position + back.normalized() * 2.8 near.y = terrain.sample_height(near.x, near.z) + 1.7 _aim_camera(cam, near, deer.global_position + Vector3(0, 0.3, 0)) await get_tree().create_timer(1.0).timeout await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png("%s_carcass.png" % prefix) print("[world] saved %s_carcass.png" % prefix) ## Weather views: a snapped-together hut in clear weather, rain, a storm and ## snow, and the view out of its open door in the rain. func _capture_weather(prefix: String, cam: Camera3D) -> void: _respawn() player.set_physics_process(false) var yaw: float = player.rotation.y var hb := Basis(Vector3.UP, yaw) var site: Vector3 = build.debug_find_site(player.global_position - player.global_basis.z * 11.0, 3.0) var hut: Dictionary = await build.debug_build_hut(site, yaw) print("[world] demo hut: %d pieces, failed %s" % [hut.placed, hut.failed]) var top: float = hut.pieces[0].global_position.y spawn_piece(ItemDB.get_item("bed"), Transform3D(hb, site + hb * Vector3(1.0, top - site.y, -0.6))) var fire_at: Vector3 = site + hb * Vector3(-0.6, 0, 4.5) fire_at.y = terrain.sample_height(fire_at.x, fire_at.z) var fire: Node = spawn_piece(ItemDB.get_item("campfire"), Transform3D(hb, fire_at)) fire.fuel = fire.MAX_FUEL var door: Node = null for p in hut.pieces: if p.item_id == "wood_door": door = p player.select(4) # wood: nothing lit in hand var outside_eye: Vector3 = site + hb * Vector3(4.2, 0, 8.5) outside_eye.y = terrain.sample_height(outside_eye.x, outside_eye.z) + 1.7 var look: Vector3 = site + Vector3(0, 1.4, 0) day_night.time = 10.5 / 24.0 for shot in [["hut_clear", "clear", 10.5], ["hut_rain", "rain", 10.5], ["hut_storm", "storm", 16.0], ["hut_snow", "snow", 11.0]]: weather.force(shot[1]) if shot[1] == "snow": weather.snow_cover = 1.0 weather.ground_wet = 0.0 day_night.time = float(shot[2]) / 24.0 day_night.apply(true) _aim_camera(cam, outside_eye, look) await get_tree().create_timer(2.0).timeout if shot[1] == "storm": weather.flash = 0.8 day_night.apply(true) await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png("%s_%s.png" % [prefix, shot[0]]) print("[world] saved %s_%s.png" % [prefix, shot[0]]) # From inside, out through the open door, in the rain. weather.force("rain") weather.snow_cover = 0.0 day_night.time = 17.5 / 24.0 day_night.apply(true) door.set_open(true, false) var in_eye: Vector3 = site + hb * Vector3(0.6, top - site.y + 1.6, -1.2) _aim_camera(cam, in_eye, site + hb * Vector3(1.2, top - site.y + 1.2, 6.0)) await get_tree().create_timer(2.0).timeout await RenderingServer.frame_post_draw get_viewport().get_texture().get_image().save_png("%s_hut_inside.png" % prefix) print("[world] saved %s_hut_inside.png" % prefix) weather.force("") func _aim_camera(cam: Camera3D, eye: Vector3, at: Vector3) -> void: var dir := at - eye player.global_position = eye - Vector3(0.0, cam.position.y, 0.0) player.rotation = Vector3(0.0, atan2(-dir.x, -dir.z), 0.0) cam.rotation.x = atan2(dir.y, Vector2(dir.x, dir.z).length()) props._update_grass(100000)