From 04b9d17826fe7b6eb36d8d474f17bf2e14641653 Mon Sep 17 00:00:00 2001 From: Paul Bailey Date: Sat, 7 Nov 2026 13:24:46 +0000 Subject: [PATCH] A better download page: a hero over the Cove with the download for your computer, the trailer, what you'll do, what's new and how to report a bug, screenshots (tools/site/make_shots.sh), downloads, and playing with friends with the ports --- README.md | 3 +- tools/site/index.html | 393 ++++++++++++++++++++++++++++++--------- tools/site/make_shots.sh | 16 ++ tools/site/make_site.sh | 16 +- tools/site/shots.gd | 126 +++++++++++++ tools/site/shots.gd.uid | 1 + 6 files changed, 462 insertions(+), 93 deletions(-) create mode 100755 tools/site/make_shots.sh create mode 100644 tools/site/shots.gd create mode 100644 tools/site/shots.gd.uid diff --git a/README.md b/README.md index 5e70908..4d32301 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,8 @@ Export Templates). The version number is `application/short_version` in ## Trailer and download page ```bash tools/trailer/make_trailer.sh # build/Emberwild-trailer.mp4: recorded in-game (Movie Maker), needs ffmpeg -tools/site/make_site.sh # build/site/: index.html + poster.jpg + trailer.mp4 for the current release +tools/site/make_shots.sh # build/shots/*.jpg: the page's screenshots, rendered in a window +tools/site/make_site.sh # build/site/: index.html, guide.html, the shots, poster and trailer for the current release ``` The page's download links point at the Gitea release named in `build/version.json`, so rebuild it after each `tools/export.sh` release. diff --git a/tools/site/index.html b/tools/site/index.html index 6e07ed4..d726e7f 100644 --- a/tools/site/index.html +++ b/tools/site/index.html @@ -4,123 +4,338 @@ Emberwild: a self-hosted co-op survival game - + + + + - - + + + -
-

EMBERWILD

-

Survive the island. Build a home.

- -
- + + + + +
+
+ Version {{VERSION}} · playtest +

EMBERWILD

+

Survive the island. Build a home. Find what lies beyond.

+

A low-poly survival game for Mac and Windows. Gather, hunt and build, hold out + through wolf moons and hard winters, then dig down through the sea cliffs to the Cove. + Alone, or with friends on a server you run yourself.

+ +

For macOS (Apple silicon and Intel) and Windows (64-bit).

-
-
-

Download version {{VERSION}}

-
-
-

macOS

-

Apple silicon and Intel · {{MAC_SIZE}}

- Download for Mac -

First launch: right-click the app and choose Open - (it isn't notarised by Apple yet).

-
-
-

Windows

-

64-bit · {{WIN_SIZE}}

- Download for Windows -

Unzip, then run Emberwild.exe. If SmartScreen - warns, choose More info > Run anyway (it isn't signed yet).

+
+
+
+

The trailer

+

Under a minute, recorded in the game.

+
+
-

Released {{DATE}}. {{NOTES}} The game tells you on its title - screen when a newer version is out. - All releases and checksums.

-
-

What it is

-
    -
  • Gather, craft and cook; keep warm, fed and dry.
  • -
  • Hunt deer, boar and rabbits; fish the lake.
  • -
  • Build a home from floors, walls and roofs that snap together.
  • -
  • Wolves move in, and every seventh night the moon turns red.
  • -
  • Weather that matters: storms, blizzards and hard winters.
  • -
  • A bear, a barrow, and the Old Stag at the standing stones.
  • -
  • Dig down through the sea cliffs to the Cove, and fish off its pier.
  • -
  • A wreck on the Cove's beach, and a pirate treasure for each player to dig up.
  • -
  • On a server, friends can be in different lands at once.
  • -
  • A map that fills in as you explore, and a compass.
  • -
  • Play alone, or host a server for friends on your own machine.
  • -
  • A guided tutorial with Old Tam the hermit.
  • -
+
+
+

What you'll do

+

Every tree, animal and hut is built from low-poly shapes in the game's + own code, under a lot of weather.

+
+
+ A sunny meadow with deer among the trees +

Survive

+

Gather, craft and cook. Keep warm, fed and dry, and eat a mix of meat, fish and + greens. Food goes off; salt and smoke keep it.

+
+
+ A wooden hut with a campfire at dusk +

Build

+

Floors, walls and roofs that snap together, in wood and then stone. A roof, a + fire and a bed make a home you'll be glad of at night.

+
+
+ Wolves circling a campfire under a red moon +

Hold out

+

Wolves move in, and every seventh night the moon turns red. There's a bear in + the deep woods, a barrow, and the Old Stag at the standing stones.

+
+
+ A marker stone in the Cove's hills at dusk, with a drowned sailor among the trees +

Explore

+

Dig through the sea cliffs to the Cove: a sandy bay, a pier for sea fishing, a + wreck, and a pirate treasure for each player, guarded by the drowned crew.

+
+
+
-
-

Playing with friends

-

Choose Host Game on the title screen, and friends choose Join Game - with your address (UDP port 24242). Everyone needs the same version. On the same - network, or over Tailscale, that's all. For friends over the internet, forward - UDP 24242 and 24243 on your router: the Cove runs as its own server on the - next port, started when someone first goes there. A spare computer can run a - dedicated server from the same download: - Emberwild --headless -- --server. - More in the guide.

+
+
+

New in {{VERSION}}

+

The Cove gets its own. This is a playtest build: thanks for trying it.

+
+
    +
  • A wreck on the Cove's beach, with a treasure map in the captain's sea chest for each player.
  • +
  • Your own X on your map: dig up the pirate chest for a ship's compass, a spyglass, a cutlass and gold.
  • +
  • The drowned crew rise to guard it. Dig by day if you're wise.
  • +
  • Crabs on the sand from dusk till dawn, and gulls after any food you leave out.
  • +
  • Build a rowing boat from driftwood, seagrass rope and pitch, and tie it up at the pier.
  • +
  • Report a bug, straight from the pause menu.
  • +
+ +
+
+
+ +
+
+ +

Straight from the game. Click one to see it full size.

+ +
+
+ +
+
+

Download version {{VERSION}}

+

Free. Released {{DATE}}. The game tells you on its title screen when a newer version is out.

+
+
+

macOS · your computer

+

Apple silicon and Intel · {{MAC_SIZE}}

+ Download for Mac +

Unzip it. The first time, right-click the app and choose Open + (it isn't notarised by Apple yet).

+
+
+

Windows · your computer

+

64-bit · {{WIN_SIZE}}

+ Download for Windows +

Unzip, then run Emberwild.exe. If SmartScreen warns, + choose More info > Run anyway (it isn't signed yet).

+
+
+

All releases, release notes and checksums.

+
+
+ +
+
+

Playing with friends

+

No accounts and no company servers: one of you hosts, on your own computer.

+
    +
  1. Host Game on the title screen. Pick a name, a rules preset (co-op, rivals + or adversarial) and a password if you like.
  2. +
  3. Friends choose Join Game and type your address. Everyone needs the same + version.
  4. +
  5. On the same network, or over Tailscale, that's it. Over the internet, forward the + ports below on your router.
  6. +
+ + + + + + + +
The Cove runs as its own server on the next port, started when someone first goes there.
UDP portFor
24242The Valley (the server you join)
24243The Cove
+

A spare computer can run a dedicated server from the same download: + Emberwild --headless -- --server. + More in the guide.

+
-
Emberwild {{VERSION}}. Made with Godot. The guide.
+ diff --git a/tools/site/make_shots.sh b/tools/site/make_shots.sh new file mode 100755 index 0000000..e9ba053 --- /dev/null +++ b/tools/site/make_shots.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# The download page's screenshots (tools/site/shots.gd), rendered in a window: +# +# tools/site/make_shots.sh # -> build/shots/*.jpg (make_site.sh copies them) +# +# Takes under a minute. Run it again when the game's look changes. + +cd "$(dirname "$0")/../.." || exit 1 +. tools/godot_env.sh +mkdir -p build/shots +rm -f build/shots/*.jpg +"$GODOT" --path . --resolution 1280x720 --script tools/site/shots.gd -- "$PWD/build/shots" > build/shots/render.log 2>&1 +if grep -q "SCRIPT ERROR" build/shots/render.log || [ -z "$(ls build/shots/*.jpg 2>/dev/null)" ]; then + echo "rendering the screenshots failed: see build/shots/render.log"; exit 1 +fi +grep "^shots:" build/shots/render.log diff --git a/tools/site/make_site.sh b/tools/site/make_site.sh index c799d8e..bb95b69 100755 --- a/tools/site/make_site.sh +++ b/tools/site/make_site.sh @@ -7,6 +7,7 @@ # 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) +# build/site/shots/ the screenshots (tools/site/make_shots.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. @@ -14,15 +15,23 @@ cd "$(dirname "$0")/../.." || exit 1 REPO=${REPO:-https://git.psbailey.uk/Admin/Emberwild} +SITE=${SITE:-https://emberwild-95k.pages.dev} # (where the page is served: for link previews) +SHOTS="meadow hut wolves winter cove treasure crabs" [ -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; } +for s in $SHOTS; do + [ -f "build/shots/$s.jpg" ] || { echo "no build/shots/$s.jpg: run tools/site/make_shots.sh first"; exit 1; } +done 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' +mkdir -p build/site/shots +for s in $SHOTS; do cp "build/shots/$s.jpg" build/site/shots/; done +python3 - "$REPO" "$SITE" <<'EOF' import html, json, sys repo = sys.argv[1].rstrip("/") +site = sys.argv[2].rstrip("/") v = json.load(open("build/version.json")) tag = "v" + v["version"] def dl(plat): @@ -36,7 +45,7 @@ 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", + "RELEASES_URL": repo + "/releases", "REPO_URL": repo, "SITE_URL": site, }.items(): page = page.replace("{{%s}}" % k, html.escape(str(val), quote=True)) assert "{{" not in page, "a placeholder was left unfilled" @@ -55,7 +64,8 @@ rm -f "$DATA" # Cloudflare Pages refuses any file over 25 MiB: catch it here, not at deploy. LIMIT=26214400 -for f in build/site/*; do +for f in build/site/* build/site/shots/*; do + [ -f "$f" ] || continue s=$(stat -f %z "$f" 2>/dev/null || stat -c %s "$f") if [ "$s" -ge "$LIMIT" ]; then echo "$f is $s bytes: over Cloudflare Pages' 25 MiB limit (re-encode the trailer smaller)"; exit 1 diff --git a/tools/site/shots.gd b/tools/site/shots.gd new file mode 100644 index 0000000..a5b0341 --- /dev/null +++ b/tools/site/shots.gd @@ -0,0 +1,126 @@ +extends "res://tools/trailer/trailer.gd" +## The download page's screenshots (tools/site/make_shots.sh): the trailer's +## shots, each caught as one still instead of filmed, plus a couple of the +## Cove's own (the treasure dig and its drowned crew, crabs on the sand). +## godot --path . --resolution 1280x720 --script tools/site/shots.gd -- +## Writes .jpg for each, 1280x720. + +## How far into each trailer shot the still is taken (0..1). +const AT := {"meadow": 0.3, "hut": 0.8, "wolves": 0.15, "winter": 0.5, "cove": 0.85} + +var _out := "" +var _name := "" +var made: Array[String] = [] + +func _run() -> void: + var args := OS.get_cmdline_user_args() + _out = args[0] if not args.is_empty() else ProjectSettings.globalize_path("res://build/shots") + DirAccess.make_dir_recursive_absolute(_out) + var H: Node = root.get_node("Hints") + H.path = "user://shots_hints.json" + H.load_prefs() + H.enabled = false + var S: Node = root.get_node("Saves") + S.root_dir = "user://shots_worlds" + _overlay() + await _enter_world("") + _calm() + await _stage() + for s in [["meadow", _shot_meadow], ["hut", _shot_hut], ["wolves", _shot_wolves], ["winter", _shot_blizzard]]: + _name = s[0] + await (s[1] as Callable).call() + await _enter_world("cove") + _calm() + _name = "cove" + await _shot_cove() + _name = "treasure" + await _shot_treasure() + _name = "crabs" + await _shot_crabs() + for w in S.list_worlds(): + S.delete_world(w.id) + DirAccess.remove_absolute(S.root_dir) + DirAccess.remove_absolute(H.path) + print("shots: ", ", ".join(made)) + quit(0) + +## No wildlife wandering into frame, nothing coming for food left out. +func _calm() -> void: + world.creatures.enabled = false + for c in world.creatures.get_children(): + c.queue_free() + if world.thieves != null: + world.thieves.enabled = false + if world.shore != null: + world.shore.enabled = false + +## One still, part-way through the shot `f` would film; no captions, no fades. +func _shot(secs: float, f: Callable, _caption: String = "", _fade: float = 0.5) -> void: + var t := secs * float(AT.get(_name, 0.6)) + for _i in range(4): + f.call(t, t / secs) + _bare() + await process_frame + world.props._update_grass(100000) + var wait := 1.2 if _name == "winter" else 0.6 # (let the snow fill the air) + var left := wait + while left > 0.0: + f.call(t, t / secs) + _bare() + await process_frame + left -= 1.0 / 60.0 + await RenderingServer.frame_post_draw + var img := root.get_viewport().get_texture().get_image() + var path := _out.path_join(_name + ".jpg") + img.save_jpg(path, 0.84) + made.append(_name) + +func _bare() -> void: + for l in [big, sub, cap]: + l.modulate.a = 0.0 + black.modulate.a = 0.0 + +## The Cove's treasure at dusk: your mound by its marker stone, and the +## drowned crew risen round it. +func _shot_treasure() -> void: + var p: Node3D = world.player + p.inventory.add("treasure_map", 1) + await create_timer(1.0).timeout # (the mound shows once the map's in your bag) + var tr: Node3D = world.treasure + var m: Node3D = tr.mounds.get(world.my_id()) if tr != null else null + if m == null: + return + tr.force_crew = true + tr._raise_crew(world.my_id(), m.global_position) + var eye: Vector3 = _g(m.global_position + Vector3(3.6, 0, 2.8), 1.6) + for g in tr.crews[world.my_id()].crew: + g.set_physics_process(false) + var to: Vector3 = eye - g.global_position + g.rotation.y = atan2(-to.x, -to.z) + await _shot(1.0, func(_t: float, _k: float): + _clock(18.1) + cam.global_position = eye + cam.look_at(m.global_position + Vector3(0, 0.6, 0)) + _park_player(m.global_position + Vector3(1.2, 0, 0))) + +## Crabs on the sand at dusk, the pier behind. +func _shot_crabs() -> void: + var b := _g(Vector3(4.0, 0, 77.0)) + var offs := [Vector3(0, 0, 0), Vector3(1.2, 0, 0.6), Vector3(-0.9, 0, 1.1), Vector3(2.2, 0, -0.5)] + for i in range(offs.size()): + var c: Node3D = world.shore.spawn(b + offs[i]) if world.shore != null else null + if c == null: + continue + c.set_physics_process(false) + c.yaw = 0.6 * i + c.rotation.y = c.yaw + if i == 0: + c.state = "charge" + c.speed = 1.2 + c._animate(0.1) + var eye := _g(b + Vector3(-1.0, 0, 2.4), 1.7) # (from down the sand, looking back up the beach) + await _shot(1.0, func(_t: float, _k: float): + _clock(18.4) + cam.global_position = eye + cam.look_at(b + Vector3(0.5, 0.0, 0.2)) + _park_player(eye)) diff --git a/tools/site/shots.gd.uid b/tools/site/shots.gd.uid new file mode 100644 index 0000000..ab8e384 --- /dev/null +++ b/tools/site/shots.gd.uid @@ -0,0 +1 @@ +uid://bll0xov1lash4