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:
@@ -80,7 +80,8 @@ Export Templates). The version number is `application/short_version` in
|
|||||||
## Trailer and download page
|
## Trailer and download page
|
||||||
```bash
|
```bash
|
||||||
tools/trailer/make_trailer.sh # build/Emberwild-trailer.mp4: recorded in-game (Movie Maker), needs ffmpeg
|
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
|
The page's download links point at the Gitea release named in
|
||||||
`build/version.json`, so rebuild it after each `tools/export.sh` release.
|
`build/version.json`, so rebuild it after each `tools/export.sh` release.
|
||||||
|
|||||||
@@ -4,123 +4,338 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Emberwild: a self-hosted co-op survival game</title>
|
<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:title" content="Emberwild">
|
||||||
<meta property="og:description" content="Survive the island. Build a home. Self-hosted co-op survival.">
|
<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="poster.jpg">
|
<meta property="og:image" content="{{SITE_URL}}/poster.jpg">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg: #15110d; --panel: #221b14; --line: #3a2e22;
|
--bg: #15110d; --bg2: #1b1510; --panel: #221b14; --line: #3a2e22;
|
||||||
--text: #f1e8da; --dim: #b8a892; --ember: #e8833a; --ember-hi: #f6a35c;
|
--text: #f1e8da; --dim: #bcae98; --ember: #e8833a; --ember-hi: #f6a35c;
|
||||||
}
|
}
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
|
html { scroll-behavior: smooth; }
|
||||||
body { margin: 0; background: var(--bg); color: var(--text);
|
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 { color: var(--ember-hi); }
|
||||||
a:focus-visible, button:focus-visible { outline: 3px solid var(--ember-hi); outline-offset: 3px; }
|
a:focus-visible, button:focus-visible, video:focus-visible { outline: 3px solid var(--ember-hi); outline-offset: 3px; }
|
||||||
.wrap { max-width: 980px; margin: 0 auto; padding: 0 20px; }
|
img { max-width: 100%; height: auto; display: block; }
|
||||||
header { padding: 48px 0 20px; text-align: center; }
|
.skip { position: absolute; left: -9999px; top: 8px; background: var(--ember); color: #1a120a; padding: 8px 14px; border-radius: 6px; z-index: 10; }
|
||||||
h1 { font-size: clamp(44px, 9vw, 84px); letter-spacing: .06em; margin: 0; font-weight: 600; }
|
.skip:focus { left: 8px; }
|
||||||
.tag { color: var(--dim); font-size: 20px; margin: 6px 0 0; }
|
.wrap { max-width: 1080px; margin: 0 auto; padding: 0 22px; }
|
||||||
.guide-link { margin: 10px 0 0; font-size: 17px; }
|
|
||||||
.video { margin: 28px 0 0; border: 1px solid var(--line); border-radius: 10px; overflow: hidden;
|
/* Top bar */
|
||||||
background: #000; aspect-ratio: 16 / 9; }
|
nav.top { position: absolute; inset: 0 0 auto 0; z-index: 2; }
|
||||||
.video video { width: 100%; height: 100%; display: block; }
|
nav.top .wrap { display: flex; align-items: center; gap: 22px; padding-top: 16px; padding-bottom: 16px; }
|
||||||
section { margin: 44px 0; }
|
nav.top .brand { font-weight: 700; letter-spacing: .14em; color: var(--text); text-decoration: none; margin-right: auto; }
|
||||||
h2 { font-size: 26px; margin: 0 0 14px; font-weight: 600; }
|
nav.top a.l { color: var(--text); text-decoration: none; font-size: 15px; opacity: .9; }
|
||||||
.downloads { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; }
|
nav.top a.l:hover { color: var(--ember-hi); }
|
||||||
.dl { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 20px; }
|
@media (max-width: 620px) { nav.top a.l.opt { display: none; } }
|
||||||
.dl.mine { border-color: var(--ember); }
|
|
||||||
.dl h3 { margin: 0 0 4px; font-size: 20px; }
|
/* Hero */
|
||||||
.dl p { margin: 0 0 14px; color: var(--dim); font-size: 15px; }
|
.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;
|
.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); }
|
.btn:hover { background: var(--ember-hi); }
|
||||||
.note { color: var(--dim); font-size: 14px; margin-top: 12px; }
|
.btn.big { font-size: 19px; padding: 15px 28px; }
|
||||||
.note code { color: var(--text); }
|
.btn.ghost { background: rgba(21,17,13,.55); color: var(--text); border-color: rgba(241,232,218,.35); }
|
||||||
ul.features { columns: 2 260px; padding-left: 20px; margin: 0; }
|
.btn.ghost:hover { border-color: var(--ember-hi); color: var(--ember-hi); background: rgba(21,17,13,.75); }
|
||||||
ul.features li { margin: 0 0 6px; break-inside: avoid; }
|
.hero .small { color: var(--dim); font-size: 14px; margin: 12px 0 0; }
|
||||||
footer { border-top: 1px solid var(--line); padding: 24px 0 40px; color: var(--dim); font-size: 14px; }
|
.hero .small a { color: var(--text); }
|
||||||
@media (prefers-reduced-motion: reduce) { * { scroll-behavior: auto; } }
|
|
||||||
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="wrap">
|
<a class="skip" href="#main">Skip to the page</a>
|
||||||
<h1>EMBERWILD</h1>
|
|
||||||
<p class="tag">Survive the island. Build a home.</p>
|
<nav class="top" aria-label="Site">
|
||||||
<p class="guide-link"><a href="guide.html">Read the guide</a>: how to play, and everything in the game.</p>
|
<div class="wrap">
|
||||||
<div class="video">
|
<a class="brand" href="#">EMBERWILD</a>
|
||||||
<video controls preload="metadata" poster="poster.jpg" aria-label="Emberwild trailer">
|
<a class="l opt" href="#trailer">Trailer</a>
|
||||||
<source src="trailer.mp4" type="video/mp4">
|
<a class="l opt" href="#world">The game</a>
|
||||||
Your browser can't play this video. <a href="trailer.mp4">Download the trailer</a>.
|
<a class="l" href="guide.html">Guide</a>
|
||||||
</video>
|
<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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="wrap">
|
<main id="main">
|
||||||
<section aria-labelledby="dl-title">
|
<section id="trailer" aria-labelledby="trailer-title">
|
||||||
<h2 id="dl-title">Download version {{VERSION}}</h2>
|
<div class="wrap">
|
||||||
<div class="downloads">
|
<h2 id="trailer-title">The trailer</h2>
|
||||||
<div class="dl" data-os="mac">
|
<p class="lead">Under a minute, recorded in the game.</p>
|
||||||
<h3>macOS</h3>
|
<div class="video">
|
||||||
<p>Apple silicon and Intel · {{MAC_SIZE}}</p>
|
<video controls preload="metadata" poster="poster.jpg" aria-label="Emberwild trailer">
|
||||||
<a class="btn" href="{{MAC_URL}}">Download for Mac</a>
|
<source src="trailer.mp4" type="video/mp4">
|
||||||
<p class="note">First launch: right-click the app and choose <b>Open</b>
|
Your browser can't play this video. <a href="trailer.mp4">Download the trailer</a>.
|
||||||
(it isn't notarised by Apple yet).</p>
|
</video>
|
||||||
</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>
|
||||||
</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>
|
||||||
|
|
||||||
<section aria-labelledby="what-title">
|
<section id="world" class="alt" aria-labelledby="world-title">
|
||||||
<h2 id="what-title">What it is</h2>
|
<div class="wrap">
|
||||||
<ul class="features">
|
<h2 id="world-title">What you'll do</h2>
|
||||||
<li>Gather, craft and cook; keep warm, fed and dry.</li>
|
<p class="lead">Every tree, animal and hut is built from low-poly shapes in the game's
|
||||||
<li>Hunt deer, boar and rabbits; fish the lake.</li>
|
own code, under a lot of weather.</p>
|
||||||
<li>Build a home from floors, walls and roofs that snap together.</li>
|
<div class="cards">
|
||||||
<li>Wolves move in, and every seventh night the moon turns red.</li>
|
<article class="card">
|
||||||
<li>Weather that matters: storms, blizzards and hard winters.</li>
|
<img src="shots/meadow.jpg" alt="A sunny meadow with deer among the trees" width="1280" height="720" loading="lazy">
|
||||||
<li>A bear, a barrow, and the Old Stag at the standing stones.</li>
|
<div class="body"><h3>Survive</h3>
|
||||||
<li>Dig down through the sea cliffs to the Cove, and fish off its pier.</li>
|
<p>Gather, craft and cook. Keep warm, fed and dry, and eat a mix of meat, fish and
|
||||||
<li>A wreck on the Cove's beach, and a pirate treasure for each player to dig up.</li>
|
greens. Food goes off; salt and smoke keep it.</p></div>
|
||||||
<li>On a server, friends can be in different lands at once.</li>
|
</article>
|
||||||
<li>A map that fills in as you explore, and a compass.</li>
|
<article class="card">
|
||||||
<li>Play alone, or host a server for friends on your own machine.</li>
|
<img src="shots/hut.jpg" alt="A wooden hut with a campfire at dusk" width="1280" height="720" loading="lazy">
|
||||||
<li>A guided tutorial with Old Tam the hermit.</li>
|
<div class="body"><h3>Build</h3>
|
||||||
</ul>
|
<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>
|
||||||
|
|
||||||
<section aria-labelledby="host-title">
|
<section id="new" aria-labelledby="new-title">
|
||||||
<h2 id="host-title">Playing with friends</h2>
|
<div class="wrap">
|
||||||
<p>Choose <b>Host Game</b> on the title screen, and friends choose <b>Join Game</b>
|
<h2 id="new-title">New in {{VERSION}}</h2>
|
||||||
with your address (UDP port 24242). Everyone needs the same version. On the same
|
<p class="lead">The Cove gets its own. This is a playtest build: thanks for trying it.</p>
|
||||||
network, or over Tailscale, that's all. For friends over the internet, forward
|
<div class="new">
|
||||||
<b>UDP 24242 and 24243</b> on your router: the Cove runs as its own server on the
|
<ul class="ticks">
|
||||||
next port, started when someone first goes there. A spare computer can run a
|
<li>A wreck on the Cove's beach, with a treasure map in the captain's sea chest for each player.</li>
|
||||||
dedicated server from the same download:
|
<li>Your own X on your map: dig up the pirate chest for a ship's compass, a spyglass, a cutlass and gold.</li>
|
||||||
<code>Emberwild --headless -- --server</code>.
|
<li>The drowned crew rise to guard it. Dig by day if you're wise.</li>
|
||||||
<a href="guide.html#multiplayer">More in the guide</a>.</p>
|
<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> > <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>
|
</section>
|
||||||
</main>
|
</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>
|
<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 () {
|
(function () {
|
||||||
var ua = navigator.userAgent;
|
var ua = navigator.userAgent;
|
||||||
var os = /Mac/i.test(ua) && !/iPhone|iPad/i.test(ua) ? "mac" : /Win/i.test(ua) ? "win" : "";
|
var os = /iPhone|iPad|Android/i.test(ua) ? "" : /Mac/i.test(ua) ? "mac" : /Win/i.test(ua) ? "win" : "";
|
||||||
var card = os && document.querySelector('.dl[data-os="' + os + '"]');
|
if (!os) return;
|
||||||
if (card) { card.classList.add("mine"); card.parentNode.insertBefore(card, card.parentNode.firstChild); }
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
16
tools/site/make_shots.sh
Executable file
16
tools/site/make_shots.sh
Executable 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
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
# build/site/index.html the page, with this release's download links
|
# build/site/index.html the page, with this release's download links
|
||||||
# build/site/poster.jpg the trailer's title frame
|
# build/site/poster.jpg the trailer's title frame
|
||||||
# build/site/trailer.mp4 the trailer (tools/trailer/make_trailer.sh)
|
# 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
|
# 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.
|
# the page lives elsewhere and links to the files on the Gitea release.
|
||||||
@@ -14,15 +15,23 @@
|
|||||||
|
|
||||||
cd "$(dirname "$0")/../.." || exit 1
|
cd "$(dirname "$0")/../.." || exit 1
|
||||||
REPO=${REPO:-https://git.psbailey.uk/Admin/Emberwild}
|
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/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; }
|
[ -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
|
rm -rf build/site && mkdir -p build/site
|
||||||
cp build/Emberwild-trailer.mp4 build/site/trailer.mp4
|
cp build/Emberwild-trailer.mp4 build/site/trailer.mp4
|
||||||
cp build/trailer/poster.jpg build/site/poster.jpg
|
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
|
import html, json, sys
|
||||||
repo = sys.argv[1].rstrip("/")
|
repo = sys.argv[1].rstrip("/")
|
||||||
|
site = sys.argv[2].rstrip("/")
|
||||||
v = json.load(open("build/version.json"))
|
v = json.load(open("build/version.json"))
|
||||||
tag = "v" + v["version"]
|
tag = "v" + v["version"]
|
||||||
def dl(plat):
|
def dl(plat):
|
||||||
@@ -36,7 +45,7 @@ page = open("tools/site/index.html").read()
|
|||||||
for k, val in {
|
for k, val in {
|
||||||
"VERSION": v["version"], "DATE": v.get("date", ""), "NOTES": v.get("notes", ""),
|
"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,
|
"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():
|
}.items():
|
||||||
page = page.replace("{{%s}}" % k, html.escape(str(val), quote=True))
|
page = page.replace("{{%s}}" % k, html.escape(str(val), quote=True))
|
||||||
assert "{{" not in page, "a placeholder was left unfilled"
|
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.
|
# Cloudflare Pages refuses any file over 25 MiB: catch it here, not at deploy.
|
||||||
LIMIT=26214400
|
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")
|
s=$(stat -f %z "$f" 2>/dev/null || stat -c %s "$f")
|
||||||
if [ "$s" -ge "$LIMIT" ]; then
|
if [ "$s" -ge "$LIMIT" ]; then
|
||||||
echo "$f is $s bytes: over Cloudflare Pages' 25 MiB limit (re-encode the trailer smaller)"; exit 1
|
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
126
tools/site/shots.gd
Normal 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
1
tools/site/shots.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bll0xov1lash4
|
||||||
Reference in New Issue
Block a user