M1b: survival stats, day/night, fire fuel, cooking, foraging, torches, death + grave
- Health, stamina, hunger, warmth and wet with HUD bars, status tags and clock - 20-minute day/night cycle with moon, stars and lit clouds - Campfire fuel (2 min start, 15 min max), heat radius, cooking needs a lit fire - Berry bushes (regrow 10 min), mushrooms, roasted mushroom, torch - Stamina costs for sprint, jump and harvest hits - Death respawns at island start and leaves a grave bag - Saves include stats, time of day, fire fuel, regrowth timers and graves - New test_survival_stats suite; tests fail if a script error aborts them
This commit is contained in:
116
scripts/world/day_night.gd
Normal file
116
scripts/world/day_night.gd
Normal file
@@ -0,0 +1,116 @@
|
||||
extends Node
|
||||
## Day/night cycle. Moves the sun along its arc and drives sky colours, sun and
|
||||
## moon light, ambient light and fog from the time of day.
|
||||
##
|
||||
## Time is a fraction of a day (0 = midnight, 0.25 = 06:00 sunrise,
|
||||
## 0.5 = noon, 0.75 = 18:00 sunset). One day lasts `day_length` real seconds
|
||||
## (20 min). Saved with the world (see world.gd collect_save).
|
||||
|
||||
signal new_day(day: int)
|
||||
|
||||
@export var day_length := 1200.0
|
||||
@export var sun_tilt_deg := 35.0 ## arc leans toward +Z, so noon sun sits at 55°
|
||||
const DAY_BIAS := 0.2
|
||||
|
||||
var day := 1
|
||||
var time := 8.0 / 24.0
|
||||
var frozen := false ## title-screen backdrop: hold a fixed time
|
||||
|
||||
var sun: DirectionalLight3D
|
||||
var moon: DirectionalLight3D
|
||||
var env: Environment
|
||||
var sky_mat: ShaderMaterial
|
||||
var daylight := 1.0 ## 0 at night, 1 in full day (smooth)
|
||||
var sun_height := 1.0 ## sin of sun elevation (-1..1)
|
||||
var _sky_timer := 0.0
|
||||
|
||||
# Palette: [zenith, horizon, fog]
|
||||
const DAY_COLS := [Color(0.24, 0.44, 0.72), Color(0.72, 0.79, 0.85), Color(0.70, 0.77, 0.85)]
|
||||
const DUSK_COLS := [Color(0.22, 0.27, 0.48), Color(0.98, 0.62, 0.38), Color(0.82, 0.62, 0.50)]
|
||||
const NIGHT_COLS := [Color(0.02, 0.03, 0.07), Color(0.07, 0.09, 0.16), Color(0.07, 0.09, 0.14)]
|
||||
|
||||
func setup(world_sun: DirectionalLight3D, environment: Environment) -> void:
|
||||
sun = world_sun
|
||||
env = environment
|
||||
sky_mat = env.sky.sky_material as ShaderMaterial
|
||||
moon = DirectionalLight3D.new()
|
||||
moon.name = "Moon"
|
||||
moon.light_color = Color(0.55, 0.65, 1.0)
|
||||
moon.shadow_enabled = false
|
||||
moon.directional_shadow_max_distance = 120.0
|
||||
moon.sky_mode = DirectionalLight3D.SKY_MODE_LIGHT_ONLY # keep the sky shader's LIGHT0 = sun
|
||||
sun.add_sibling(moon)
|
||||
apply(true)
|
||||
|
||||
func hours() -> float:
|
||||
return time * 24.0
|
||||
|
||||
func clock_text() -> String:
|
||||
var h := hours()
|
||||
return "Day %d %02d:%02d" % [day, int(h), int(fmod(h, 1.0) * 60.0)]
|
||||
|
||||
func is_night() -> bool:
|
||||
return daylight < 0.3
|
||||
|
||||
func advance(seconds: float) -> void:
|
||||
time += seconds / day_length
|
||||
while time >= 1.0:
|
||||
time -= 1.0
|
||||
day += 1
|
||||
new_day.emit(day)
|
||||
apply(false)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not frozen:
|
||||
advance(delta)
|
||||
|
||||
## Updates lights every call; the sky material (which triggers a radiance
|
||||
## rebuild) only a few times a second unless `force`.
|
||||
func apply(force: bool) -> void:
|
||||
if sun == null:
|
||||
return
|
||||
var a := (time - 0.25) * TAU
|
||||
var t := deg_to_rad(sun_tilt_deg)
|
||||
# +DAY_BIAS lifts the whole arc: sunrise ~05:00, sunset ~19:00, so days
|
||||
# are longer than nights and dusk lingers.
|
||||
var to_sun := Vector3(cos(a), sin(a) * cos(t) + DAY_BIAS, sin(a) * sin(t)).normalized()
|
||||
sun_height = to_sun.y
|
||||
daylight = smoothstep(-0.2, 0.25, to_sun.y)
|
||||
var low := 1.0 - smoothstep(0.05, 0.45, absf(to_sun.y)) # 1 near the horizon
|
||||
|
||||
sun.global_basis = Basis.looking_at(-to_sun if to_sun.y > -0.2 else Vector3.DOWN, Vector3.FORWARD)
|
||||
sun.light_energy = 1.8 * smoothstep(-0.03, 0.15, to_sun.y)
|
||||
sun.light_color = Color(1.0, 0.95, 0.83).lerp(Color(1.0, 0.58, 0.32), low)
|
||||
sun.visible = sun.light_energy > 0.01
|
||||
sun.shadow_enabled = sun.visible
|
||||
|
||||
var moon_dir := Vector3(-to_sun.x, absf(to_sun.y) * 0.8 + 0.25, -to_sun.z * 0.5).normalized()
|
||||
moon.global_basis = Basis.looking_at(-moon_dir, Vector3.FORWARD)
|
||||
moon.light_energy = 0.38 * (1.0 - daylight)
|
||||
moon.visible = moon.light_energy > 0.01
|
||||
moon.shadow_enabled = moon.visible and not sun.visible
|
||||
|
||||
env.ambient_light_energy = lerpf(0.3, 1.0, daylight)
|
||||
env.fog_light_energy = lerpf(0.25, 1.0, daylight)
|
||||
env.volumetric_fog_density = lerpf(0.0025, 0.0045, daylight)
|
||||
|
||||
_sky_timer -= get_process_delta_time()
|
||||
if not force and _sky_timer > 0.0:
|
||||
return
|
||||
_sky_timer = 0.25
|
||||
var cols: Array = []
|
||||
for i in range(3):
|
||||
var dayc: Color = DAY_COLS[i].lerp(DUSK_COLS[i], low * daylight)
|
||||
cols.append(NIGHT_COLS[i].lerp(dayc, daylight))
|
||||
env.fog_light_color = cols[2]
|
||||
if sky_mat != null:
|
||||
sky_mat.set_shader_parameter("zenith_col", cols[0])
|
||||
sky_mat.set_shader_parameter("horizon_col", cols[1])
|
||||
sky_mat.set_shader_parameter("energy", lerpf(0.35, 1.0, daylight))
|
||||
sky_mat.set_shader_parameter("star_strength", 1.0 - smoothstep(0.0, 0.35, daylight))
|
||||
sky_mat.set_shader_parameter("cloud_lit", Color(0.16, 0.18, 0.24).lerp(Color(1, 1, 1), daylight))
|
||||
sky_mat.set_shader_parameter("sun_tint", Color(1.0, 0.86, 0.62).lerp(Color(1.0, 0.55, 0.3), low))
|
||||
|
||||
## 0..100 "how warm the air is" before fires, shelter and clothes.
|
||||
func air_warmth() -> float:
|
||||
return lerpf(22.0, 68.0, daylight)
|
||||
1
scripts/world/day_night.gd.uid
Normal file
1
scripts/world/day_night.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://2vp5n05qr6yn
|
||||
59
scripts/world/forage.gd
Normal file
59
scripts/world/forage.gd
Normal file
@@ -0,0 +1,59 @@
|
||||
extends Area3D
|
||||
## A berry bush you can forage with E: berries plus some fiber. It's then bare
|
||||
## until it regrows (`regrow_seconds` of play time). Walk-through, on
|
||||
## collision layer 2 like pickups. Regrow timers are saved (props.forage).
|
||||
|
||||
const ItemMeshes = preload("res://scripts/items/item_meshes.gd")
|
||||
|
||||
var uid := ""
|
||||
var regrow_seconds := 600.0
|
||||
var regrow_left := 0.0 ## 0 = ripe
|
||||
var rng := RandomNumberGenerator.new()
|
||||
var _mesh: MeshInstance3D
|
||||
|
||||
func setup(material: Material) -> void:
|
||||
collision_layer = 2
|
||||
collision_mask = 0
|
||||
monitoring = false
|
||||
_mesh = MeshInstance3D.new()
|
||||
_mesh.material_override = material
|
||||
add_child(_mesh)
|
||||
var cs := CollisionShape3D.new()
|
||||
var sph := SphereShape3D.new()
|
||||
sph.radius = 0.7
|
||||
cs.shape = sph
|
||||
cs.position = Vector3(0, 0.5, 0)
|
||||
add_child(cs)
|
||||
_refresh()
|
||||
|
||||
func ripe() -> bool:
|
||||
return regrow_left <= 0.0
|
||||
|
||||
func _refresh() -> void:
|
||||
_mesh.mesh = ItemMeshes.build("berry_bush" if ripe() else "berry_bush_bare")
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if regrow_left > 0.0:
|
||||
regrow_left = maxf(regrow_left - delta, 0.0)
|
||||
if regrow_left <= 0.0:
|
||||
_refresh()
|
||||
|
||||
func interact_prompt(_player: Node) -> String:
|
||||
if ripe():
|
||||
return "Berry bush [E] Pick berries"
|
||||
return "Berry bush (regrowing, %d min)" % ceili(regrow_left / 60.0)
|
||||
|
||||
func interact(player: Node) -> void:
|
||||
Actions.forage(player, self)
|
||||
|
||||
## Called by Actions.forage. Returns what was picked.
|
||||
func harvest() -> Dictionary:
|
||||
if not ripe():
|
||||
return {}
|
||||
regrow_left = regrow_seconds
|
||||
_refresh()
|
||||
return {"berries": rng.randi_range(2, 4), "fiber": rng.randi_range(1, 2)}
|
||||
|
||||
func restore(left: float) -> void:
|
||||
regrow_left = left
|
||||
_refresh()
|
||||
1
scripts/world/forage.gd.uid
Normal file
1
scripts/world/forage.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://ceb8l5850ldj4
|
||||
@@ -13,6 +13,7 @@ extends Node3D
|
||||
const ProcMesh = preload("res://scripts/world/proc_mesh.gd")
|
||||
const ResourceNodeScript = preload("res://scripts/world/resource_node.gd")
|
||||
const PickupScript = preload("res://scripts/world/pickup.gd")
|
||||
const ForageScript = preload("res://scripts/world/forage.gd")
|
||||
|
||||
const CELL := 48.0
|
||||
const GRASS_TILE := 12.0
|
||||
@@ -40,6 +41,7 @@ var _resources: Node3D ## harvestable trees/boulders (each its own body)
|
||||
## creation order gives the same id to the same tree on every load.
|
||||
var resource_nodes: Dictionary = {} ## uid -> resource node
|
||||
var pickups: Dictionary = {} ## uid -> pickup
|
||||
var forage: Dictionary = {} ## uid -> berry bush (forage.gd)
|
||||
|
||||
var _bark_mat: StandardMaterial3D
|
||||
var _leaf_mat: ShaderMaterial
|
||||
@@ -500,8 +502,9 @@ func _scatter_pickups() -> void:
|
||||
add_child(root)
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = world_seed + 6
|
||||
var want := {"stick": 260, "stone": 170, "flint": 80}
|
||||
var radius: Dictionary = {"stick": 0.35, "stone": 0.25, "flint": 0.25}
|
||||
# Order matters: new kinds go at the END so existing saves keep their ids.
|
||||
var want := {"stick": 260, "stone": 170, "flint": 80, "mushroom": 110}
|
||||
var radius: Dictionary = {"stick": 0.35, "stone": 0.25, "flint": 0.25, "mushroom": 0.25}
|
||||
for id: String in want:
|
||||
var mesh := ItemDB.mesh_for(id)
|
||||
var placed := 0
|
||||
@@ -525,6 +528,8 @@ func _scatter_pickups() -> void:
|
||||
"flint":
|
||||
var shore := 1.0 - smoothstep(terrain.water_level + 0.5, terrain.water_level + 4.0, h)
|
||||
chance = 0.06 + shore * 0.9 + (1.0 - n.y) * 2.0
|
||||
"mushroom":
|
||||
chance = _forest(x, z) * 0.7 # deep forest only
|
||||
# A few right by the spawn so the first craft is quick.
|
||||
if Vector2(x - _spawn.x, z - _spawn.z).length() < 18.0:
|
||||
chance += 0.5
|
||||
@@ -537,6 +542,48 @@ func _scatter_pickups() -> void:
|
||||
root.add_child(pk)
|
||||
pk.global_transform = Transform3D(_ground_basis(n, 1.0, yaw, 1.0), Vector3(x, h - 0.02, z))
|
||||
placed += 1
|
||||
_scatter_berry_bushes(root)
|
||||
|
||||
## Berry bushes along forest edges and in meadows, plus a couple near spawn.
|
||||
func _scatter_berry_bushes(root: Node3D) -> void:
|
||||
var rng := RandomNumberGenerator.new()
|
||||
rng.seed = world_seed + 7
|
||||
var placed := 0
|
||||
var tries := 0
|
||||
while placed < 70 and tries < 8000:
|
||||
tries += 1
|
||||
var x := rng.randf_range(-terrain.play_radius, terrain.play_radius)
|
||||
var z := rng.randf_range(-terrain.play_radius, terrain.play_radius)
|
||||
var yaw := rng.randf() * TAU
|
||||
var keep := rng.randf()
|
||||
if not terrain.inside_boundary(x, z, 4.0):
|
||||
continue
|
||||
var h: float = terrain.sample_height(x, z)
|
||||
var n: Vector3 = terrain.sample_normal(x, z)
|
||||
if h < terrain.water_level + 0.6 or n.y < 0.85:
|
||||
continue
|
||||
var f := _forest(x, z)
|
||||
var chance := 0.12 + (1.0 - absf(f - 0.45) * 2.0) * 0.5
|
||||
var near_spawn := Vector2(x - _spawn.x, z - _spawn.z).length()
|
||||
if near_spawn < 6.0:
|
||||
continue
|
||||
if near_spawn < 25.0:
|
||||
chance += 0.4
|
||||
if keep > chance:
|
||||
continue
|
||||
var bush: Area3D = ForageScript.new()
|
||||
bush.uid = "b%d" % forage.size()
|
||||
bush.rng.seed = hash(bush.uid) ^ world_seed
|
||||
bush.setup(_leaf_mat_static())
|
||||
root.add_child(bush)
|
||||
bush.global_transform = Transform3D(_ground_basis(n, 0.5, yaw, rng.randf_range(0.9, 1.2)), Vector3(x, h - 0.05, z))
|
||||
forage[bush.uid] = bush
|
||||
placed += 1
|
||||
|
||||
## Berry-bush meshes carry their own vertex colours (including red berries),
|
||||
## so they use the plain item material rather than the wind foliage shader.
|
||||
func _leaf_mat_static() -> Material:
|
||||
return ItemDB.item_material()
|
||||
|
||||
# --- grass streaming --------------------------------------------------------
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@ extends Node3D
|
||||
## Dev hook: `godot --path . -- --shot=/abs/prefix` renders a few fixed views to
|
||||
## <prefix>_<view>.png and quits. Handy for checking visual changes.
|
||||
|
||||
@export var sun_elevation_deg: float = 34.0
|
||||
@export var sun_azimuth_deg: float = 40.0 ## 0 = behind the spawn view, + = to its right
|
||||
|
||||
@onready var terrain: Node3D = $Terrain
|
||||
@onready var props: Node3D = $Props
|
||||
@@ -22,6 +20,7 @@ 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"),
|
||||
}
|
||||
|
||||
const AUTOSAVE_SECONDS := 120.0
|
||||
@@ -38,12 +37,20 @@ 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
|
||||
|
||||
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)
|
||||
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()
|
||||
@@ -54,10 +61,11 @@ func _ready() -> void:
|
||||
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
|
||||
_aim_sun()
|
||||
player.set_physics_process(false)
|
||||
set_physics_process(false)
|
||||
if terrain.built:
|
||||
@@ -65,11 +73,44 @@ func _ready() -> void:
|
||||
else:
|
||||
terrain.ready_built.connect(_on_terrain_ready)
|
||||
|
||||
func _aim_sun() -> void:
|
||||
var el := deg_to_rad(sun_elevation_deg)
|
||||
var az := deg_to_rad(sun_azimuth_deg)
|
||||
var to_sun := Vector3(sin(az) * cos(el), sin(el), cos(az) * cos(el))
|
||||
sun.global_basis = Basis.looking_at(-to_sun, Vector3.UP)
|
||||
# --- survival environment ---------------------------------------------------
|
||||
|
||||
## 0..100 air warmth at `pos` before fires: time of day, minus altitude.
|
||||
func air_warmth(pos: Vector3) -> float:
|
||||
var w: float = day_night.air_warmth()
|
||||
w -= maxf(pos.y - 12.0, 0.0) * 0.9
|
||||
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))
|
||||
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()
|
||||
_respawn()
|
||||
player.survival.reset_after_death()
|
||||
Actions.notify.emit("You died. Your belongings are in your grave." if has_items else "You died.")
|
||||
|
||||
func _on_terrain_ready() -> void:
|
||||
var t0 := Time.get_ticks_msec()
|
||||
@@ -146,6 +187,11 @@ func collect_save() -> Dictionary:
|
||||
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]
|
||||
@@ -158,6 +204,8 @@ func collect_save() -> 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()
|
||||
pieces.append(entry)
|
||||
return {
|
||||
"player": {
|
||||
@@ -166,7 +214,10 @@ func collect_save() -> Dictionary:
|
||||
"pitch": cam.rotation.x,
|
||||
"inventory": player.inventory.to_data(),
|
||||
"selected": player.selected,
|
||||
"stats": player.survival.to_data(),
|
||||
},
|
||||
"time": {"day": day_night.day, "time": day_night.time},
|
||||
"forage_regrow": regrow,
|
||||
"harvested": harvested,
|
||||
"pickups_taken": taken,
|
||||
"pieces": pieces,
|
||||
@@ -181,6 +232,17 @@ func apply_save(s: Dictionary) -> void:
|
||||
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)
|
||||
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):
|
||||
@@ -195,6 +257,8 @@ func apply_save(s: Dictionary) -> void:
|
||||
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)
|
||||
|
||||
func _respawn() -> void:
|
||||
player.global_position = _spawn + Vector3(0.0, 1.5, 0.0)
|
||||
@@ -322,6 +386,19 @@ func _capture_survival(prefix: String, cam: Camera3D) -> void:
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user