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

This commit is contained in:
Paul Bailey
2026-11-07 13:24:46 +00:00
parent 3b611a6d34
commit 04b9d17826
6 changed files with 462 additions and 93 deletions

View File

@@ -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.

View File

@@ -4,123 +4,338 @@
<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 name="description" content="Emberwild is a low-poly survival game: gather, hunt, build a home and hold out through the night, then dig through the sea cliffs to the Cove. Alone, or with friends on your own server.">
<meta name="theme-color" content="#15110d">
<meta property="og:type" content="website">
<meta property="og:url" content="{{SITE_URL}}/">
<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">
<meta property="og:description" content="Survive the island. Build a home. Self-hosted co-op survival for Mac and Windows.">
<meta property="og:image" content="{{SITE_URL}}/poster.jpg">
<meta name="twitter:card" content="summary_large_image">
<style>
:root {
--bg: #15110d; --panel: #221b14; --line: #3a2e22;
--text: #f1e8da; --dim: #b8a892; --ember: #e8833a; --ember-hi: #f6a35c;
--bg: #15110d; --bg2: #1b1510; --panel: #221b14; --line: #3a2e22;
--text: #f1e8da; --dim: #bcae98; --ember: #e8833a; --ember-hi: #f6a35c;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { margin: 0; background: var(--bg); color: var(--text);
font: 17px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
font: 17px/1.6 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; }
.guide-link { margin: 10px 0 0; font-size: 17px; }
.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; }
a:focus-visible, button:focus-visible, video:focus-visible { outline: 3px solid var(--ember-hi); outline-offset: 3px; }
img { max-width: 100%; height: auto; display: block; }
.skip { position: absolute; left: -9999px; top: 8px; background: var(--ember); color: #1a120a; padding: 8px 14px; border-radius: 6px; z-index: 10; }
.skip:focus { left: 8px; }
.wrap { max-width: 1080px; margin: 0 auto; padding: 0 22px; }
/* Top bar */
nav.top { position: absolute; inset: 0 0 auto 0; z-index: 2; }
nav.top .wrap { display: flex; align-items: center; gap: 22px; padding-top: 16px; padding-bottom: 16px; }
nav.top .brand { font-weight: 700; letter-spacing: .14em; color: var(--text); text-decoration: none; margin-right: auto; }
nav.top a.l { color: var(--text); text-decoration: none; font-size: 15px; opacity: .9; }
nav.top a.l:hover { color: var(--ember-hi); }
@media (max-width: 620px) { nav.top a.l.opt { display: none; } }
/* Hero */
.hero { position: relative; min-height: min(88vh, 760px); display: flex; align-items: flex-end;
background: #0d0a07 url("shots/cove.jpg") center 35% / cover no-repeat; }
.hero::before { content: ""; position: absolute; inset: 0;
background: linear-gradient(180deg, rgba(13,10,7,.55) 0%, rgba(13,10,7,.15) 35%, rgba(21,17,13,.85) 78%, var(--bg) 100%); }
.hero .wrap { position: relative; padding-bottom: 56px; width: 100%; }
.pill { display: inline-block; font-size: 14px; padding: 3px 12px; border-radius: 999px;
background: rgba(232,131,58,.18); border: 1px solid rgba(246,163,92,.55); color: var(--ember-hi); }
.hero h1 { font-size: clamp(52px, 11vw, 112px); line-height: 1; letter-spacing: .07em; margin: 14px 0 8px; font-weight: 700;
text-shadow: 0 2px 24px rgba(0,0,0,.55); }
.hero .tag { font-size: clamp(19px, 2.6vw, 25px); margin: 0 0 6px; text-shadow: 0 1px 10px rgba(0,0,0,.7); }
.hero .sub { color: var(--dim); margin: 0 0 26px; max-width: 640px; text-shadow: 0 1px 8px rgba(0,0,0,.8); }
.ctas { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
.btn { display: inline-block; background: var(--ember); color: #1a120a; text-decoration: none;
font-weight: 600; padding: 11px 20px; border-radius: 8px; }
font-weight: 650; padding: 12px 22px; border-radius: 9px; border: 1px solid transparent; }
.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; } }
.btn.big { font-size: 19px; padding: 15px 28px; }
.btn.ghost { background: rgba(21,17,13,.55); color: var(--text); border-color: rgba(241,232,218,.35); }
.btn.ghost:hover { border-color: var(--ember-hi); color: var(--ember-hi); background: rgba(21,17,13,.75); }
.hero .small { color: var(--dim); font-size: 14px; margin: 12px 0 0; }
.hero .small a { color: var(--text); }
section { padding: 56px 0; }
section.alt { background: var(--bg2); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
h2 { font-size: clamp(26px, 3.4vw, 34px); line-height: 1.2; margin: 0 0 8px; font-weight: 650; }
.lead { color: var(--dim); margin: 0 0 26px; max-width: 720px; }
/* Trailer */
.video { border: 1px solid var(--line); border-radius: 12px; overflow: hidden; background: #000; aspect-ratio: 16 / 9;
box-shadow: 0 20px 60px rgba(0,0,0,.45); }
.video video { width: 100%; height: 100%; display: block; }
/* Feature cards */
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 18px; }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; display: flex; flex-direction: column; }
.card img { aspect-ratio: 16 / 9; object-fit: cover; width: 100%; }
.card .body { padding: 16px 18px 18px; }
.card h3 { margin: 0 0 6px; font-size: 20px; }
.card p { margin: 0; color: var(--dim); font-size: 15.5px; }
/* What's new */
.new { display: grid; grid-template-columns: 1.3fr 1fr; gap: 28px; align-items: start; }
@media (max-width: 820px) { .new { grid-template-columns: 1fr; } }
ul.ticks { list-style: none; padding: 0; margin: 0; }
ul.ticks li { padding: 8px 0 8px 30px; position: relative; border-bottom: 1px solid var(--line); }
ul.ticks li:last-child { border-bottom: 0; }
ul.ticks li::before { content: ""; position: absolute; left: 4px; top: 17px; width: 10px; height: 10px; border-radius: 50%;
background: var(--ember); box-shadow: 0 0 10px rgba(232,131,58,.8); }
.callout { background: var(--panel); border: 1px solid var(--ember); border-radius: 12px; padding: 20px 22px; }
.callout h3 { margin: 0 0 8px; font-size: 20px; }
.callout p { margin: 0 0 10px; color: var(--dim); font-size: 15.5px; }
.callout p:last-child { margin: 0; }
kbd { font: 600 13px/1 ui-monospace, SFMono-Regular, Menlo, monospace; background: #2f251b; border: 1px solid #4a3b2c;
border-bottom-width: 2px; border-radius: 5px; padding: 3px 6px; color: var(--text); }
/* Gallery */
.gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 14px; margin: 0; }
.gallery figure { margin: 0; position: relative; border-radius: 10px; overflow: hidden; border: 1px solid var(--line); background: #000; }
.gallery a { display: block; }
.gallery img { aspect-ratio: 16 / 9; object-fit: cover; width: 100%; transition: transform .35s ease; }
.gallery a:hover img { transform: scale(1.03); }
.gallery figcaption { position: absolute; left: 0; right: 0; bottom: 0; padding: 22px 14px 10px; font-size: 14.5px;
background: linear-gradient(transparent, rgba(0,0,0,.78)); pointer-events: none; }
/* Downloads */
.downloads { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px; }
.dl { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 22px; }
.dl.mine { border-color: var(--ember); box-shadow: 0 0 0 1px var(--ember) inset; }
.dl h3 { margin: 0 0 2px; font-size: 21px; }
.dl .meta { margin: 0 0 16px; color: var(--dim); font-size: 15px; }
.dl .yours { display: none; font-size: 13px; color: var(--ember-hi); margin-left: 8px; font-weight: 600; }
.dl.mine .yours { display: inline; }
.note { color: var(--dim); font-size: 14.5px; margin: 14px 0 0; }
.note code, td code, p code { color: var(--text); background: #2a2119; padding: 1px 6px; border-radius: 5px; font-size: .92em; }
/* Friends */
.steps { counter-reset: s; display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 18px; margin: 0 0 22px; padding: 0; list-style: none; }
.steps li { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 18px 18px 18px 60px; position: relative; color: var(--dim); font-size: 15.5px; }
.steps li b { color: var(--text); }
.steps li::before { counter-increment: s; content: counter(s); position: absolute; left: 18px; top: 16px; width: 28px; height: 28px;
border-radius: 50%; background: var(--ember); color: #1a120a; font-weight: 700; display: grid; place-items: center; font-size: 15px; }
table { border-collapse: collapse; width: 100%; max-width: 640px; font-size: 15.5px; }
th, td { text-align: left; padding: 9px 12px; border-bottom: 1px solid var(--line); }
th { color: var(--dim); font-weight: 600; }
footer { border-top: 1px solid var(--line); padding: 28px 0 44px; color: var(--dim); font-size: 14.5px; }
footer .wrap { display: flex; flex-wrap: wrap; gap: 10px 26px; justify-content: space-between; }
footer nav a { margin-right: 18px; }
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
.gallery img { transition: none; }
.gallery a:hover img { transform: none; }
}
</style>
</head>
<body>
<header class="wrap">
<h1>EMBERWILD</h1>
<p class="tag">Survive the island. Build a home.</p>
<p class="guide-link"><a href="guide.html">Read the guide</a>: how to play, and everything in the game.</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>
<a class="skip" href="#main">Skip to the page</a>
<nav class="top" aria-label="Site">
<div class="wrap">
<a class="brand" href="#">EMBERWILD</a>
<a class="l opt" href="#trailer">Trailer</a>
<a class="l opt" href="#world">The game</a>
<a class="l" href="guide.html">Guide</a>
<a class="l" href="#download">Download</a>
</div>
</nav>
<header class="hero">
<div class="wrap">
<span class="pill">Version {{VERSION}} · playtest</span>
<h1>EMBERWILD</h1>
<p class="tag">Survive the island. Build a home. Find what lies beyond.</p>
<p class="sub">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.</p>
<div class="ctas">
<a class="btn big" id="hero-dl" href="#download">Download free</a>
<a class="btn ghost" href="#trailer">Watch the trailer</a>
<a class="btn ghost" href="guide.html">Read the guide</a>
</div>
<p class="small" id="hero-other">For macOS (Apple silicon and Intel) and Windows (64-bit).</p>
</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> &gt; <b>Run anyway</b> (it isn't signed yet).</p>
<main id="main">
<section id="trailer" aria-labelledby="trailer-title">
<div class="wrap">
<h2 id="trailer-title">The trailer</h2>
<p class="lead">Under a minute, recorded in the game.</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>
</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>Dig down through the sea cliffs to the Cove, and fish off its pier.</li>
<li>A wreck on the Cove's beach, and a pirate treasure for each player to dig up.</li>
<li>On a server, friends can be in different lands at once.</li>
<li>A map that fills in as you explore, and a compass.</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 id="world" class="alt" aria-labelledby="world-title">
<div class="wrap">
<h2 id="world-title">What you'll do</h2>
<p class="lead">Every tree, animal and hut is built from low-poly shapes in the game's
own code, under a lot of weather.</p>
<div class="cards">
<article class="card">
<img src="shots/meadow.jpg" alt="A sunny meadow with deer among the trees" width="1280" height="720" loading="lazy">
<div class="body"><h3>Survive</h3>
<p>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.</p></div>
</article>
<article class="card">
<img src="shots/hut.jpg" alt="A wooden hut with a campfire at dusk" width="1280" height="720" loading="lazy">
<div class="body"><h3>Build</h3>
<p>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.</p></div>
</article>
<article class="card">
<img src="shots/wolves.jpg" alt="Wolves circling a campfire under a red moon" width="1280" height="720" loading="lazy">
<div class="body"><h3>Hold out</h3>
<p>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.</p></div>
</article>
<article class="card">
<img src="shots/treasure.jpg" alt="A marker stone in the Cove's hills at dusk, with a drowned sailor among the trees" width="1280" height="720" loading="lazy">
<div class="body"><h3>Explore</h3>
<p>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.</p></div>
</article>
</div>
</div>
</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. On the same
network, or over Tailscale, that's all. For friends over the internet, forward
<b>UDP 24242 and 24243</b> 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:
<code>Emberwild --headless -- --server</code>.
<a href="guide.html#multiplayer">More in the guide</a>.</p>
<section id="new" aria-labelledby="new-title">
<div class="wrap">
<h2 id="new-title">New in {{VERSION}}</h2>
<p class="lead">The Cove gets its own. This is a playtest build: thanks for trying it.</p>
<div class="new">
<ul class="ticks">
<li>A wreck on the Cove's beach, with a treasure map in the captain's sea chest for each player.</li>
<li>Your own X on your map: dig up the pirate chest for a ship's compass, a spyglass, a cutlass and gold.</li>
<li>The drowned crew rise to guard it. Dig by day if you're wise.</li>
<li>Crabs on the sand from dusk till dawn, and gulls after any food you leave out.</li>
<li>Build a rowing boat from driftwood, seagrass rope and pitch, and tie it up at the pier.</li>
<li>Report a bug, straight from the pause menu.</li>
</ul>
<aside class="callout" aria-labelledby="bug-title">
<h3 id="bug-title">Found a bug?</h3>
<p>Press <kbd>Esc</kbd> in the game, then <b>Report a bug</b>. It opens a new issue
on <a href="{{REPO_URL}}/issues">our Gitea</a>, already filled in with your
version, computer and where you are. Say what happened and press Create Issue.</p>
<p>You'll need a free account there. <a href="{{REPO_URL}}/src/branch/master/docs/PLAYTEST.md">What to try, and what isn't a bug yet</a>.</p>
</aside>
</div>
</div>
</section>
<section class="alt" aria-labelledby="gallery-title">
<div class="wrap">
<h2 id="gallery-title">Screenshots</h2>
<p class="lead">Straight from the game. Click one to see it full size.</p>
<div class="gallery">
<figure><a href="shots/cove.jpg"><img src="shots/cove.jpg" alt="The Cove's bay and pier on a clear morning, a rowing boat tied up" width="1280" height="720" loading="lazy"></a>
<figcaption>The Cove, and a boat at the pier</figcaption></figure>
<figure><a href="shots/crabs.jpg"><img src="shots/crabs.jpg" alt="Red crabs on the sand at dusk" width="1280" height="720" loading="lazy"></a>
<figcaption>Crabs come up the beach at dusk</figcaption></figure>
<figure><a href="shots/winter.jpg"><img src="shots/winter.jpg" alt="A hut in a snowy clearing in a blizzard" width="1280" height="720" loading="lazy"></a>
<figcaption>Weather the winter</figcaption></figure>
<figure><a href="shots/hut.jpg"><img src="shots/hut.jpg" alt="A wooden hut with a campfire at dusk" width="1280" height="720" loading="lazy"></a>
<figcaption>A home of your own</figcaption></figure>
<figure><a href="shots/wolves.jpg"><img src="shots/wolves.jpg" alt="Wolves circling a campfire under a red moon" width="1280" height="720" loading="lazy"></a>
<figcaption>The wolf moon</figcaption></figure>
<figure><a href="shots/treasure.jpg"><img src="shots/treasure.jpg" alt="A marker stone and a mound of loose earth in the hills at dusk" width="1280" height="720" loading="lazy"></a>
<figcaption>X marks the spot</figcaption></figure>
</div>
</div>
</section>
<section id="download" aria-labelledby="dl-title">
<div class="wrap">
<h2 id="dl-title">Download version {{VERSION}}</h2>
<p class="lead">Free. Released {{DATE}}. The game tells you on its title screen when a newer version is out.</p>
<div class="downloads">
<div class="dl" data-os="mac">
<h3>macOS <span class="yours">· your computer</span></h3>
<p class="meta">Apple silicon and Intel · {{MAC_SIZE}}</p>
<a class="btn" href="{{MAC_URL}}">Download for Mac</a>
<p class="note">Unzip it. The first time, 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 <span class="yours">· your computer</span></h3>
<p class="meta">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> &gt; <b>Run anyway</b> (it isn't signed yet).</p>
</div>
</div>
<p class="note"><a href="{{RELEASES_URL}}">All releases, release notes and checksums</a>.</p>
</div>
</section>
<section id="friends" class="alt" aria-labelledby="host-title">
<div class="wrap">
<h2 id="host-title">Playing with friends</h2>
<p class="lead">No accounts and no company servers: one of you hosts, on your own computer.</p>
<ol class="steps">
<li><b>Host Game</b> on the title screen. Pick a name, a rules preset (co-op, rivals
or adversarial) and a password if you like.</li>
<li>Friends choose <b>Join Game</b> and type your address. Everyone needs the same
version.</li>
<li>On the same network, or over Tailscale, that's it. Over the internet, forward the
ports below on your router.</li>
</ol>
<table>
<caption class="note" style="caption-side: bottom; text-align: left;">The Cove runs as its own server on the next port, started when someone first goes there.</caption>
<thead><tr><th scope="col">UDP port</th><th scope="col">For</th></tr></thead>
<tbody>
<tr><td><code>24242</code></td><td>The Valley (the server you join)</td></tr>
<tr><td><code>24243</code></td><td>The Cove</td></tr>
</tbody>
</table>
<p class="note">A spare computer can run a dedicated server from the same download:
<code>Emberwild --headless -- --server</code>.
<a href="guide.html#multiplayer">More in the guide</a>.</p>
</div>
</section>
</main>
<footer class="wrap">Emberwild {{VERSION}}. Made with Godot. <a href="guide.html">The guide</a>.</footer>
<footer>
<div class="wrap">
<nav aria-label="More">
<a href="guide.html">The guide</a>
<a href="{{RELEASES_URL}}">Releases</a>
<a href="{{REPO_URL}}/issues">Report a bug</a>
<a href="{{REPO_URL}}">Source</a>
</nav>
<span>Emberwild {{VERSION}}. Made with Godot.</span>
</div>
</footer>
<script>
// Point out the download for the visitor's own computer (it still works without this).
// The visitor's own computer: its download first and marked, and the
// hero's button goes straight to it. (Without this, the page still works.)
(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); }
var os = /iPhone|iPad|Android/i.test(ua) ? "" : /Mac/i.test(ua) ? "mac" : /Win/i.test(ua) ? "win" : "";
if (!os) return;
var card = document.querySelector('.dl[data-os="' + os + '"]');
if (!card) return;
card.classList.add("mine");
card.parentNode.insertBefore(card, card.parentNode.firstChild);
var link = card.querySelector("a.btn");
var hero = document.getElementById("hero-dl");
hero.href = link.href;
hero.textContent = os === "mac" ? "Download for Mac" : "Download for Windows";
var other = document.getElementById("hero-other");
other.innerHTML = "Free · {{VERSION}} · " + card.querySelector(".meta").textContent.split("·").pop().trim() +
' · <a href="#download">' + (os === "mac" ? "Windows" : "Mac") + " and install notes</a>";
})();
</script>
</body>

16
tools/site/make_shots.sh Executable file
View File

@@ -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

View File

@@ -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

126
tools/site/shots.gd Normal file
View File

@@ -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 -- <out dir>
## Writes <name>.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))

1
tools/site/shots.gd.uid Normal file
View File

@@ -0,0 +1 @@
uid://bll0xov1lash4