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"), } const BuildPiece = preload("res://scripts/building/build_piece.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 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 has_bed_spawn := false var bed_spawn := Vector3.ZERO ## position of the bed you respawn at var _loading := false ## applying a save: defer support recompute func _ready() -> void: _pieces = Node3D.new() _pieces.name = "Pieces" add_child(_pieces) day_night = preload("res://scripts/world/day_night.gd").new() day_night.name = "DayNight" 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 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": hud = preload("res://scripts/ui/hud.gd").new() add_child(hud) hud.bind(player) if not menu_mode: player.survival.died.connect(_on_player_died) if Saves.has_world() and not menu_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 return w ## 0..1 heat from fires and a held torch. func heat_at(pos: Vector3) -> float: var h := 0.0 for n in get_tree().get_nodes_in_group("heat"): h = maxf(h, n.heat_at(pos)) if player.hands != null: h = maxf(h, player.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: 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()) if hud != null: hud.close_screens() 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() Actions.notify.emit("You died. Your belongings are in your grave." if has_items else "You died.") # --- 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 ## 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. func sleep(p: Node3D) -> void: 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 f in get_tree().get_nodes_in_group("heat"): if f.has_method("burn"): f.burn(secs) 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() 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 _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) 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) _maybe_capture() 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 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 var ok: bool = Saves.save_state(Saves.current_id, collect_save(), _session_time) if ok: _session_time = 0.0 Actions.notify.emit("Game saved" if ok else "Save FAILED") return ok func _notification(what: int) -> void: if what == NOTIFICATION_WM_CLOSE_REQUEST and Saves.has_world() and not menu_mode: 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 _dropped.get_children(): if d.is_queued_for_deletion() or d.taken: continue dropped.append({"item": d.item_id, "count": d.amount, "xf": _xf_to_array(d.global_transform)}) var pieces: Array = [] for piece in _pieces.get_children(): if piece.is_queued_for_deletion(): continue 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() pieces.append(entry) 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}, "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, } func apply_save(s: Dictionary) -> void: var p: Dictionary = s.get("player", {}) 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 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", []): if not ItemDB.get_item(d.get("item", "")).is_empty(): drop_item(d.item, int(d.get("count", 1)), _array_to_xf(d.xf)) 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", {})) 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", []): var def: Dictionary = ItemDB.get_item(e.get("item", "")) if def.is_empty() or not def.has("place"): continue 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) _loading = false build.recompute() # same terrain, same pieces: nothing collapses on load _update_grass_cuts() func _respawn() -> void: 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() func spawn_piece(def: Dictionary, xf: Transform3D) -> 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 piece.is_in_group("build") and not _loading: build.recompute() if piece.kind == "floor": _update_grass_cuts() 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) ## 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. func drop_item(id: String, count: int, xf: Transform3D) -> 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 _dropped.add_child(pk) pk.global_transform = xf return pk ## Casts a fishing float (Actions validated it). One at a time. func start_fishing(p: Node3D, origin: Vector3, vel: Vector3) -> Node3D: end_fishing() bobber = preload("res://scripts/fishing/bobber.gd").new() bobber.setup(self, p, origin, vel, randi()) add_child(bobber) return bobber func end_fishing() -> void: if bobber != null and is_instance_valid(bobber): bobber.queue_free() bobber = null ## 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) return pr ## 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: return 0 var weak: Array = build.recompute() 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) ## 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)