349 lines
12 KiB
GDScript
349 lines
12 KiB
GDScript
extends SceneTree
|
|
## The trailer (~46 s), recorded with Movie Maker (tools/trailer/make_trailer.sh):
|
|
## godot --path . --fixed-fps 30 --write-movie out.avi --script tools/trailer/trailer.gd -- --no-creatures
|
|
## Eight shots: dawn flyover + title, morning meadow, a hut at dusk, the wolf
|
|
## moon, a blizzard, down the cliff tunnel, the Cove's pier, and an end card
|
|
## over the bay (with the version from project.godot).
|
|
|
|
const FPS := 30.0
|
|
const UITheme = preload("res://scripts/ui/theme.gd")
|
|
|
|
var world: Node3D
|
|
var cam: Camera3D
|
|
var dn: Node
|
|
var black: ColorRect
|
|
var big: Label
|
|
var sub: Label
|
|
var cap: Label
|
|
var hut_c := Vector3.ZERO
|
|
var fire_p := Vector3.ZERO
|
|
var yaw := 0.0
|
|
|
|
func _initialize() -> void:
|
|
_run.call_deferred()
|
|
|
|
func _h(x: float, z: float) -> float:
|
|
return world.terrain.sample_height(x, z)
|
|
|
|
func _g(p: Vector3, up: float = 0.0) -> Vector3:
|
|
return Vector3(p.x, _h(p.x, p.z) + up, p.z)
|
|
|
|
func _run() -> void:
|
|
var H: Node = root.get_node("Hints")
|
|
H.path = "user://trailer_hints.json" # (never your real tips)
|
|
H.load_prefs()
|
|
H.enabled = false
|
|
# (Its own save folder, so the Cove can be loaded as a world of its own.)
|
|
var S: Node = root.get_node("Saves")
|
|
S.root_dir = "user://trailer_worlds"
|
|
_overlay()
|
|
await _enter_world("")
|
|
await _stage()
|
|
await _shot_dawn()
|
|
await _shot_meadow()
|
|
await _shot_hut()
|
|
await _shot_wolves()
|
|
await _shot_blizzard()
|
|
await _shot_tunnel()
|
|
await _enter_world("cove")
|
|
await _shot_cove()
|
|
await _shot_end()
|
|
for w in S.list_worlds():
|
|
S.delete_world(w.id)
|
|
DirAccess.remove_absolute(S.root_dir)
|
|
DirAccess.remove_absolute(H.path)
|
|
quit(0)
|
|
|
|
## Loads the world to film: the Valley with no save (""), or a fresh world
|
|
## in `worldlet`. Hides the HUD and takes the camera.
|
|
func _enter_world(worldlet: String) -> void:
|
|
var S: Node = root.get_node("Saves")
|
|
if world != null:
|
|
black.modulate.a = 1.0
|
|
world.queue_free()
|
|
for _i in range(3):
|
|
await process_frame
|
|
S.current_id = "" if worldlet.is_empty() else S.create_world("Trailer", 1337, {"worldlet": worldlet})
|
|
world = load("res://scenes/world.tscn").instantiate()
|
|
root.add_child(world)
|
|
for _i in range(8):
|
|
await process_frame
|
|
var p: CharacterBody3D = world.player
|
|
p.set_physics_process(false)
|
|
p.survival.set_process(false)
|
|
if world.hud != null:
|
|
world.hud.visible = false
|
|
if world.has_node("HUD"):
|
|
world.get_node("HUD").visible = false
|
|
if world.wolves != null:
|
|
world.wolves.enabled = false
|
|
dn = world.day_night
|
|
dn.frozen = true
|
|
world.weather.set_process(false)
|
|
world.weather.force("clear")
|
|
cam = Camera3D.new()
|
|
cam.fov = 62
|
|
world.add_child(cam)
|
|
cam.current = true
|
|
|
|
# --- set dressing --------------------------------------------------------------------
|
|
|
|
func _stage() -> void:
|
|
var spawn: Vector3 = world.player.global_position
|
|
var site: Vector3 = world.build.debug_find_site(spawn + Vector3(10, 0, 6), 3.0)
|
|
yaw = 0.6
|
|
var hut: Dictionary = await world.build.debug_build_hut(site, yaw)
|
|
hut_c = site
|
|
if hut.has("pieces") and not hut.pieces.is_empty():
|
|
hut_c = hut.pieces[0].global_transform * Vector3(1, 0, 1)
|
|
var b := Basis(Vector3.UP, yaw)
|
|
fire_p = _g(hut_c + b * Vector3(0, 0, 5.5))
|
|
var fire: Node3D = world.spawn_piece(ItemDBRef().get_item("campfire"), Transform3D(Basis(), fire_p))
|
|
fire.fuel = fire.MAX_FUEL
|
|
for _i in range(5):
|
|
await physics_frame
|
|
|
|
func ItemDBRef() -> Node:
|
|
return root.get_node("ItemDB")
|
|
|
|
func _park_player(at: Vector3) -> void:
|
|
world.player.global_position = at # (the weather particles follow the player)
|
|
|
|
# --- overlay -------------------------------------------------------------------------
|
|
|
|
func _overlay() -> void:
|
|
var ui := CanvasLayer.new()
|
|
ui.layer = 50
|
|
root.add_child(ui)
|
|
var r := Control.new()
|
|
r.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
ui.add_child(r)
|
|
big = UITheme.label("", 84)
|
|
sub = UITheme.label("", 26, Color(0.95, 0.9, 0.8))
|
|
cap = UITheme.label("", 34)
|
|
for l in [big, sub, cap]:
|
|
l.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
l.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
l.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
l.add_theme_constant_override("outline_size", 6 if l == big else 3)
|
|
l.add_theme_color_override("font_outline_color", Color(0, 0, 0, 0.55))
|
|
l.add_theme_color_override("font_shadow_color", Color(0, 0, 0, 0.5))
|
|
l.add_theme_constant_override("shadow_offset_x", 2)
|
|
l.add_theme_constant_override("shadow_offset_y", 2)
|
|
l.modulate.a = 0.0
|
|
r.add_child(l)
|
|
big.offset_top = -70
|
|
# The smaller lines sit on a soft dark box, so they read over snow and sun.
|
|
var box := StyleBoxFlat.new()
|
|
box.bg_color = Color(0.04, 0.03, 0.02, 0.55)
|
|
box.set_corner_radius_all(8)
|
|
box.content_margin_left = 22
|
|
box.content_margin_right = 22
|
|
box.content_margin_top = 8
|
|
box.content_margin_bottom = 10
|
|
for l in [sub, cap]:
|
|
l.add_theme_stylebox_override("normal", box)
|
|
l.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
|
l.grow_vertical = Control.GROW_DIRECTION_BOTH
|
|
sub.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
|
sub.offset_top = 46
|
|
sub.offset_bottom = 46
|
|
cap.set_anchors_and_offsets_preset(Control.PRESET_CENTER_BOTTOM)
|
|
cap.grow_vertical = Control.GROW_DIRECTION_BEGIN
|
|
cap.offset_top = -70
|
|
cap.offset_bottom = -70
|
|
black = ColorRect.new()
|
|
black.color = Color.BLACK
|
|
black.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
|
r.add_child(black)
|
|
|
|
## 0..1 ramp: in over `fin` seconds from `a`, out over `fout` ending at `b`.
|
|
static func _env(t: float, a: float, b: float, fin: float = 0.6, fout: float = 0.6) -> float:
|
|
if t < a or t > b:
|
|
return 0.0
|
|
return clampf(minf((t - a) / fin, (b - t) / fout), 0.0, 1.0)
|
|
|
|
## Runs `secs` of a shot: `f(t, k)` each frame (t seconds, k = 0..1); fades
|
|
## from and to black at its ends.
|
|
func _shot(secs: float, f: Callable, caption: String = "", fade: float = 0.5) -> void:
|
|
var n := int(secs * FPS)
|
|
cap.text = caption
|
|
for i in range(n):
|
|
var t := i / FPS
|
|
f.call(t, t / secs)
|
|
black.modulate.a = 1.0 - _env(t, 0.0, secs, fade, fade)
|
|
cap.modulate.a = _env(t, 0.6, secs - 0.3, 0.5, 0.5) if not caption.is_empty() else 0.0
|
|
await process_frame
|
|
|
|
func _clock(hours: float) -> void:
|
|
dn.time = hours / 24.0
|
|
dn.apply(true)
|
|
|
|
# --- the shots -------------------------------------------------------------------------
|
|
|
|
func _shot_dawn() -> void:
|
|
big.text = "EMBERWILD"
|
|
sub.text = "Survive the island. Build a home."
|
|
var c := Vector3(0, 0, -10)
|
|
await _shot(7.5, func(t: float, k: float):
|
|
_clock(7.4 + k * 0.8)
|
|
var a := 2.2 + k * 0.55
|
|
cam.global_position = c + Vector3(cos(a), 0, sin(a)) * 115.0 + Vector3(0, 58.0 - k * 10.0, 0)
|
|
cam.look_at(c + Vector3(0, 6, 0))
|
|
_park_player(cam.global_position)
|
|
big.modulate.a = _env(t, 1.4, 6.6, 1.0, 0.8)
|
|
sub.modulate.a = _env(t, 2.4, 6.6, 1.0, 0.8), "", 0.9)
|
|
|
|
func _shot_meadow() -> void:
|
|
var from := _g(hut_c + Vector3(-30, 0, -22), 2.2)
|
|
var to := _g(hut_c + Vector3(-12, 0, -8), 1.8)
|
|
var cre: Node = world.creatures
|
|
var herd: Array = []
|
|
for i in range(3):
|
|
var at := _g(hut_c + Vector3(-14 + i * 2.5, 0, -2 + i * 1.5))
|
|
var d = cre.spawn("deer" if i < 2 else "rabbit", at)
|
|
if d != null:
|
|
herd.append(d)
|
|
await _shot(5.0, func(_t: float, k: float):
|
|
_clock(9.0)
|
|
var pos := from.lerp(to, k)
|
|
pos.y = maxf(pos.y, _h(pos.x, pos.z) + 1.8)
|
|
cam.global_position = pos
|
|
cam.look_at(_g(hut_c + Vector3(-14, 0, 0), 1.0))
|
|
_park_player(pos), "Gather. Hunt. Cook.")
|
|
for d in herd:
|
|
if is_instance_valid(d):
|
|
d.queue_free()
|
|
|
|
func _shot_hut() -> void:
|
|
var b := Basis(Vector3.UP, yaw)
|
|
var from := _g(hut_c + b * Vector3(9, 0, 13), 3.2)
|
|
var to := _g(hut_c + b * Vector3(4, 0, 9.5), 2.0)
|
|
await _shot(6.0, func(_t: float, k: float):
|
|
_clock(18.25 + k * 0.4)
|
|
var e := k * k * (3.0 - 2.0 * k)
|
|
cam.global_position = from.lerp(to, e)
|
|
cam.look_at((hut_c + fire_p) * 0.5 + Vector3(0, 1.3, 0))
|
|
_park_player(cam.global_position), "Build a home.")
|
|
|
|
func _shot_wolves() -> void:
|
|
dn.day = 7
|
|
# Three wolves, walked round the far side of the fire by hand (in the
|
|
# fire's light, in frame) rather than left to their own circling.
|
|
var pack: Array = []
|
|
for i in range(3):
|
|
var w: Node3D = load("res://scripts/creatures/tutorial_wolf.gd").new()
|
|
w.setup("wolf", world, _g(fire_p + Vector3(7, 0, 0)), 900 + i)
|
|
world.add_child(w)
|
|
w.set_physics_process(false)
|
|
w.set_process(false)
|
|
pack.append(w)
|
|
for _i in range(5):
|
|
await physics_frame
|
|
# A warm fill from the fire's side, so the circling pack reads at night.
|
|
var fill := OmniLight3D.new()
|
|
fill.light_color = Color(1.0, 0.62, 0.35)
|
|
fill.light_energy = 2.2
|
|
fill.omni_range = 18.0
|
|
fill.shadow_enabled = true
|
|
world.add_child(fill)
|
|
fill.global_position = fire_p + Vector3(0, 3.5, 0)
|
|
await _shot(5.5, func(_t: float, k: float):
|
|
_clock(21.6)
|
|
var a := yaw + 2.4 + k * 0.35
|
|
cam.global_position = _g(fire_p + Vector3(cos(a), 0, sin(a)) * 3.4, 1.7)
|
|
cam.look_at(_g(fire_p - Vector3(cos(a), 0, sin(a)) * 6.0, 0.4))
|
|
_park_player(cam.global_position)
|
|
var far := a + PI
|
|
for i in range(pack.size()):
|
|
var wa := far - 0.75 + i * 0.6 + k * 0.45
|
|
var r := 5.0 + i * 0.7
|
|
var wp := _g(fire_p + Vector3(cos(wa), 0, sin(wa)) * r)
|
|
var to := _g(fire_p + Vector3(cos(wa + 0.2), 0, sin(wa + 0.2)) * r) - wp
|
|
pack[i].global_position = wp
|
|
pack[i].rotation.y = atan2(-to.x, -to.z), "Hold out through the night.") # (creatures face -Z, as creature.gd)
|
|
fill.queue_free()
|
|
for w in pack:
|
|
if is_instance_valid(w):
|
|
w.queue_free()
|
|
dn.day = 3
|
|
|
|
func _shot_blizzard() -> void:
|
|
world.weather.force("blizzard")
|
|
var b := Basis(Vector3.UP, yaw)
|
|
for _i in range(60):
|
|
await process_frame # (let the snow build up)
|
|
await _shot(4.5, func(_t: float, k: float):
|
|
_clock(13.0)
|
|
var a := yaw - 0.4 - k * 0.25
|
|
cam.global_position = _g(hut_c + Vector3(cos(a), 0, sin(a)) * 14.0, 3.0)
|
|
cam.look_at(hut_c + Vector3(0, 1.5, 0))
|
|
_park_player(cam.global_position), "Weather the winter.")
|
|
world.weather.force("clear")
|
|
if world.weather_fx != null:
|
|
world.weather_fx.visible = false # (no leftover snow on the end card)
|
|
|
|
## Down the cliff tunnel (J1): dug through and propped, lit by torch stands
|
|
## you've set, the camera walking down the first stretch in the dark.
|
|
func _shot_tunnel() -> void:
|
|
var t: Node3D = world.tunnel
|
|
if t == null:
|
|
return
|
|
t.opened = true
|
|
t.dug = t.STRETCHES
|
|
for k in range(1, t.STRETCHES + 1):
|
|
t.props[k] = true
|
|
t._rebuild()
|
|
var ts: Dictionary = ItemDBRef().get_item("torch_stand")
|
|
var candle: Dictionary = ItemDBRef().get_item("tallow_candle")
|
|
var land1: Vector3 = t.ORIGIN + Vector3(t._landing_x(1), t._floor_y(1), t._lane_z(1))
|
|
world.spawn_piece(ts, Transform3D(Basis(), t._mouth(1) + Vector3(-0.5, 0, 0.9)))
|
|
world.spawn_piece(ts, Transform3D(Basis(), land1 + Vector3(0.6, 0, -0.8)))
|
|
world.spawn_piece(candle, Transform3D(Basis(), t._mouth(1) + Vector3(3.0, -2.0, -0.95)))
|
|
world.spawn_piece(candle, Transform3D(Basis(), t._mouth(2) + Vector3(-0.4, 0, 0.95)))
|
|
for _i in range(5):
|
|
await physics_frame
|
|
var from: Vector3 = t.top + Vector3(-0.6, 1.65, 0)
|
|
var to: Vector3 = land1 + Vector3(-1.4, 1.65, 0)
|
|
dn.underground = 1.0
|
|
await _shot(5.5, func(_t: float, k: float):
|
|
dn.underground = 1.0
|
|
_clock(13.0)
|
|
var e := k * k * (3.0 - 2.0 * k)
|
|
var pos := from.lerp(to, e)
|
|
cam.global_position = pos
|
|
cam.look_at(pos + Vector3(3.0, -1.4 + e * 0.9, 0.0) + Vector3(0, 0, lerpf(0.0, 1.2, e)))
|
|
_park_player(pos), "Dig through the cliffs.")
|
|
dn.underground = 0.0
|
|
|
|
## The Cove (W3): morning over the bay, along the pier.
|
|
func _shot_cove() -> void:
|
|
var pier: Node3D = world.pier
|
|
if pier == null:
|
|
return
|
|
var side: Vector3 = pier.dir.cross(Vector3.UP)
|
|
# (From out over the bay, with the stream's mouth along the shore.)
|
|
var from: Vector3 = pier.end + pier.dir * 18.0 + side * 14.0 + Vector3(0, 2.5, 0)
|
|
var to: Vector3 = pier.end + pier.dir * 6.0 + side * 7.0 + Vector3(0, 1.6, 0)
|
|
await _shot(6.0, func(_t: float, k: float):
|
|
_clock(8.2 + k * 0.3)
|
|
var e := k * k * (3.0 - 2.0 * k)
|
|
cam.global_position = from.lerp(to, e)
|
|
cam.look_at(pier.start.lerp(pier.end, 0.35) + Vector3(0, 1.0, 0))
|
|
_park_player(Vector3(pier.start.x, pier.start.y + 1.0, pier.start.z)), "Find what lies beyond.")
|
|
|
|
func _shot_end() -> void:
|
|
big.text = "EMBERWILD"
|
|
sub.text = "Self-hosted co-op survival · %s" % String(ProjectSettings.get_setting("application/config/version", ""))
|
|
# Rising over the Cove's bay, looking back at the land.
|
|
var pier: Node3D = world.pier
|
|
var c: Vector3 = Vector3.ZERO if pier == null else pier.start
|
|
var out: Vector3 = Vector3(0, 0, 1) if pier == null else pier.dir
|
|
await _shot(6.0, func(t: float, k: float):
|
|
_clock(17.6 + k * 0.4)
|
|
cam.global_position = c + out * (40.0 + k * 14.0) + Vector3(0, 8.0 + k * 14.0, 0)
|
|
cam.look_at(c - out * 30.0 + Vector3(0, 4.0, 0))
|
|
_park_player(c + Vector3(0, 1.0, 0))
|
|
big.modulate.a = _env(t, 0.8, 5.6, 0.8, 0.6)
|
|
sub.modulate.a = _env(t, 1.6, 5.6, 0.8, 0.6), "", 0.7)
|