Trailer tool (Movie Maker + ffmpeg) and a static download page built per release
This commit is contained in:
117
tools/site/index.html
Normal file
117
tools/site/index.html
Normal file
@@ -0,0 +1,117 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Emberwild: a self-hosted co-op survival game</title>
|
||||
<meta name="description" content="Emberwild is a low-poly survival game: gather, hunt, build a home and hold out through the night, alone or with friends on your own server.">
|
||||
<meta property="og:title" content="Emberwild">
|
||||
<meta property="og:description" content="Survive the island. Build a home. Self-hosted co-op survival.">
|
||||
<meta property="og:image" content="poster.jpg">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #15110d; --panel: #221b14; --line: #3a2e22;
|
||||
--text: #f1e8da; --dim: #b8a892; --ember: #e8833a; --ember-hi: #f6a35c;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--bg); color: var(--text);
|
||||
font: 17px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
|
||||
a { color: var(--ember-hi); }
|
||||
a:focus-visible, button:focus-visible { outline: 3px solid var(--ember-hi); outline-offset: 3px; }
|
||||
.wrap { max-width: 980px; margin: 0 auto; padding: 0 20px; }
|
||||
header { padding: 48px 0 20px; text-align: center; }
|
||||
h1 { font-size: clamp(44px, 9vw, 84px); letter-spacing: .06em; margin: 0; font-weight: 600; }
|
||||
.tag { color: var(--dim); font-size: 20px; margin: 6px 0 0; }
|
||||
.video { margin: 28px 0 0; border: 1px solid var(--line); border-radius: 10px; overflow: hidden;
|
||||
background: #000; aspect-ratio: 16 / 9; }
|
||||
.video video { width: 100%; height: 100%; display: block; }
|
||||
section { margin: 44px 0; }
|
||||
h2 { font-size: 26px; margin: 0 0 14px; font-weight: 600; }
|
||||
.downloads { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; }
|
||||
.dl { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 20px; }
|
||||
.dl.mine { border-color: var(--ember); }
|
||||
.dl h3 { margin: 0 0 4px; font-size: 20px; }
|
||||
.dl p { margin: 0 0 14px; color: var(--dim); font-size: 15px; }
|
||||
.btn { display: inline-block; background: var(--ember); color: #1a120a; text-decoration: none;
|
||||
font-weight: 600; padding: 11px 20px; border-radius: 8px; }
|
||||
.btn:hover { background: var(--ember-hi); }
|
||||
.note { color: var(--dim); font-size: 14px; margin-top: 12px; }
|
||||
.note code { color: var(--text); }
|
||||
ul.features { columns: 2 260px; padding-left: 20px; margin: 0; }
|
||||
ul.features li { margin: 0 0 6px; break-inside: avoid; }
|
||||
footer { border-top: 1px solid var(--line); padding: 24px 0 40px; color: var(--dim); font-size: 14px; }
|
||||
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="wrap">
|
||||
<h1>EMBERWILD</h1>
|
||||
<p class="tag">Survive the island. Build a home.</p>
|
||||
<div class="video">
|
||||
<video controls preload="metadata" poster="poster.jpg" aria-label="Emberwild trailer">
|
||||
<source src="trailer.mp4" type="video/mp4">
|
||||
Your browser can't play this video. <a href="trailer.mp4">Download the trailer</a>.
|
||||
</video>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="wrap">
|
||||
<section aria-labelledby="dl-title">
|
||||
<h2 id="dl-title">Download version {{VERSION}}</h2>
|
||||
<div class="downloads">
|
||||
<div class="dl" data-os="mac">
|
||||
<h3>macOS</h3>
|
||||
<p>Apple silicon and Intel · {{MAC_SIZE}}</p>
|
||||
<a class="btn" href="{{MAC_URL}}">Download for Mac</a>
|
||||
<p class="note">First launch: right-click the app and choose <b>Open</b>
|
||||
(it isn't notarised by Apple yet).</p>
|
||||
</div>
|
||||
<div class="dl" data-os="win">
|
||||
<h3>Windows</h3>
|
||||
<p>64-bit · {{WIN_SIZE}}</p>
|
||||
<a class="btn" href="{{WIN_URL}}">Download for Windows</a>
|
||||
<p class="note">Unzip, then run <code>Emberwild.exe</code>. If SmartScreen
|
||||
warns, choose <b>More info</b> > <b>Run anyway</b> (it isn't signed yet).</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="note">Released {{DATE}}. {{NOTES}} The game tells you on its title
|
||||
screen when a newer version is out.
|
||||
<a href="{{RELEASES_URL}}">All releases and checksums</a>.</p>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="what-title">
|
||||
<h2 id="what-title">What it is</h2>
|
||||
<ul class="features">
|
||||
<li>Gather, craft and cook; keep warm, fed and dry.</li>
|
||||
<li>Hunt deer, boar and rabbits; fish the lake.</li>
|
||||
<li>Build a home from floors, walls and roofs that snap together.</li>
|
||||
<li>Wolves move in, and every seventh night the moon turns red.</li>
|
||||
<li>Weather that matters: storms, blizzards and hard winters.</li>
|
||||
<li>A bear, a barrow, and the Old Stag at the standing stones.</li>
|
||||
<li>Play alone, or host a server for friends on your own machine.</li>
|
||||
<li>A guided tutorial with Old Tam the hermit.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="host-title">
|
||||
<h2 id="host-title">Playing with friends</h2>
|
||||
<p>Choose <b>Host Game</b> on the title screen, and friends choose <b>Join Game</b>
|
||||
with your address (UDP port 24242). Everyone needs the same version. A spare
|
||||
computer can run a dedicated server from the same download:
|
||||
<code>Emberwild --headless -- --server</code>.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="wrap">Emberwild {{VERSION}}. Made with Godot.</footer>
|
||||
|
||||
<script>
|
||||
// Point out the download for the visitor's own computer (it still works without this).
|
||||
(function () {
|
||||
var ua = navigator.userAgent;
|
||||
var os = /Mac/i.test(ua) && !/iPhone|iPad/i.test(ua) ? "mac" : /Win/i.test(ua) ? "win" : "";
|
||||
var card = os && document.querySelector('.dl[data-os="' + os + '"]');
|
||||
if (card) { card.classList.add("mine"); card.parentNode.insertBefore(card, card.parentNode.firstChild); }
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
45
tools/site/make_site.sh
Executable file
45
tools/site/make_site.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/bin/sh
|
||||
# Builds the download page into build/site/ for the release in
|
||||
# build/version.json (made by tools/export.sh):
|
||||
#
|
||||
# tools/site/make_site.sh
|
||||
#
|
||||
# build/site/index.html the page, with this release's download links
|
||||
# build/site/poster.jpg the trailer's title frame
|
||||
# build/site/trailer.mp4 the trailer (tools/trailer/make_trailer.sh)
|
||||
#
|
||||
# Copy the folder to any static web host. Gitea itself won't serve HTML, so
|
||||
# the page lives elsewhere and links to the files on the Gitea release.
|
||||
# Rebuild it after each release (the links name the version).
|
||||
|
||||
cd "$(dirname "$0")/../.." || exit 1
|
||||
REPO=${REPO:-https://git.psbailey.uk/Admin/Emberwild}
|
||||
[ -f build/version.json ] || { echo "no build/version.json: run tools/export.sh first"; exit 1; }
|
||||
[ -f build/Emberwild-trailer.mp4 ] || { echo "no trailer: run tools/trailer/make_trailer.sh first"; exit 1; }
|
||||
|
||||
rm -rf build/site && mkdir -p build/site
|
||||
cp build/Emberwild-trailer.mp4 build/site/trailer.mp4
|
||||
cp build/trailer/poster.jpg build/site/poster.jpg
|
||||
python3 - "$REPO" <<'EOF'
|
||||
import html, json, sys
|
||||
repo = sys.argv[1].rstrip("/")
|
||||
v = json.load(open("build/version.json"))
|
||||
tag = "v" + v["version"]
|
||||
def dl(plat):
|
||||
d = v["downloads"].get(plat)
|
||||
if not d:
|
||||
return repo + "/releases", "see all releases"
|
||||
return "%s/releases/download/%s/%s" % (repo, tag, d["file"]), "%.0f MB" % (d["size"] / 1e6)
|
||||
mac_url, mac_size = dl("macos")
|
||||
win_url, win_size = dl("windows")
|
||||
page = open("tools/site/index.html").read()
|
||||
for k, val in {
|
||||
"VERSION": v["version"], "DATE": v.get("date", ""), "NOTES": v.get("notes", ""),
|
||||
"MAC_URL": mac_url, "MAC_SIZE": mac_size, "WIN_URL": win_url, "WIN_SIZE": win_size,
|
||||
"RELEASES_URL": repo + "/releases",
|
||||
}.items():
|
||||
page = page.replace("{{%s}}" % k, html.escape(str(val), quote=True))
|
||||
assert "{{" not in page, "a placeholder was left unfilled"
|
||||
open("build/site/index.html", "w").write(page)
|
||||
print("built build/site/ for %s:\n %s\n %s" % (v["version"], mac_url, win_url))
|
||||
EOF
|
||||
36
tools/trailer/make_trailer.sh
Executable file
36
tools/trailer/make_trailer.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/bin/sh
|
||||
# Records the test trailer (tools/trailer/trailer.gd) with Godot's Movie
|
||||
# Maker and encodes it, with a generated music pad under the game's sound:
|
||||
#
|
||||
# tools/trailer/make_trailer.sh # -> build/Emberwild-trailer.mp4 (+ poster.jpg)
|
||||
#
|
||||
# Needs ffmpeg. Takes about a minute: the shots are rendered frame by frame
|
||||
# at 30 fps whatever the machine's speed, so the result never stutters.
|
||||
|
||||
cd "$(dirname "$0")/../.." || exit 1
|
||||
. tools/godot_env.sh
|
||||
command -v ffmpeg >/dev/null || { echo "needs ffmpeg (brew install ffmpeg)"; exit 1; }
|
||||
|
||||
mkdir -p build/trailer
|
||||
RAW=build/trailer/raw.avi
|
||||
OUT=build/Emberwild-trailer.mp4
|
||||
rm -f "$RAW"
|
||||
"$GODOT" --path . --resolution 1280x720 --fixed-fps 30 --write-movie "$RAW" \
|
||||
--script tools/trailer/trailer.gd -- --no-creatures > build/trailer/render.log 2>&1
|
||||
if [ ! -f "$RAW" ] || grep -q "SCRIPT ERROR" build/trailer/render.log; then
|
||||
echo "recording failed: see build/trailer/render.log"; exit 1
|
||||
fi
|
||||
|
||||
D=$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$RAW")
|
||||
OUT_AT=$(echo "$D - 2.5" | bc)
|
||||
# A slow A-minor-ish drone, left and right a little apart, echoed and soft.
|
||||
PAD="0.06*(sin(2*PI*110*t)+0.7*sin(2*PI*164.8*t)+0.5*sin(2*PI*220*t)+0.35*sin(2*PI*261.6*t)*(0.5+0.5*sin(2*PI*0.08*t)))*(0.75+0.25*sin(2*PI*0.21*t))"
|
||||
PAD_R="0.06*(sin(2*PI*110.4*t)+0.7*sin(2*PI*165.2*t)+0.5*sin(2*PI*219.6*t)+0.35*sin(2*PI*262*t)*(0.5+0.5*cos(2*PI*0.08*t)))*(0.75+0.25*cos(2*PI*0.21*t))"
|
||||
ffmpeg -v error -y -i "$RAW" -f lavfi -i "aevalsrc='$PAD|$PAD_R':s=48000:d=$D" \
|
||||
-filter_complex "[1:a]lowpass=f=1800,aecho=0.8:0.7:180|340:0.35|0.25,afade=t=in:d=2.5,afade=t=out:st=$OUT_AT:d=2.5[m];[0:a]volume=0.8[g];[g][m]amix=inputs=2:duration=first:normalize=0[a]" \
|
||||
-map 0:v -map "[a]" -c:v libx264 -preset slow -crf 20 -pix_fmt yuv420p \
|
||||
-c:a aac -b:a 160k -movflags +faststart "$OUT" || exit 1
|
||||
# The title, for the download page's poster.
|
||||
ffmpeg -v error -y -ss 4.5 -i "$OUT" -frames:v 1 -q:v 3 build/trailer/poster.jpg
|
||||
rm -f "$RAW"
|
||||
echo "built $OUT ($(du -h "$OUT" | cut -f1 | tr -d ' '), ${D%.*} s) and build/trailer/poster.jpg"
|
||||
274
tools/trailer/trailer.gd
Normal file
274
tools/trailer/trailer.gd
Normal file
@@ -0,0 +1,274 @@
|
||||
extends SceneTree
|
||||
## Temporary: a ~32 s test trailer, recorded with Movie Maker:
|
||||
## godot --path . --fixed-fps 30 --write-movie out.avi --script tools/trailer/trailer.gd -- --no-creatures
|
||||
## Six shots: dawn flyover + title, morning meadow, a hut at dusk, the wolf
|
||||
## moon, a blizzard, and an end card.
|
||||
|
||||
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
|
||||
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
|
||||
_overlay()
|
||||
await _stage()
|
||||
await _shot_dawn()
|
||||
await _shot_meadow()
|
||||
await _shot_hut()
|
||||
await _shot_wolves()
|
||||
await _shot_blizzard()
|
||||
await _shot_end()
|
||||
DirAccess.remove_absolute(H.path)
|
||||
quit(0)
|
||||
|
||||
# --- 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)
|
||||
|
||||
func _shot_end() -> void:
|
||||
big.text = "EMBERWILD"
|
||||
sub.text = "Self-hosted co-op survival · 0.1.0"
|
||||
var b := Basis(Vector3.UP, yaw)
|
||||
await _shot(6.0, func(t: float, k: float):
|
||||
_clock(17.6 + k * 0.4)
|
||||
cam.global_position = _g(hut_c + b * Vector3(10, 0, 9), 0.0) + Vector3(0, 6.0 + k * 10.0, 0) + b * Vector3(0, 0, k * 10.0)
|
||||
cam.look_at(hut_c + Vector3(0, 1.0, 0))
|
||||
_park_player(cam.global_position)
|
||||
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)
|
||||
Reference in New Issue
Block a user