git-svn-id: svn://losandesgames.com/alfheim-website@26 15359d88-9307-4e75-a9c1-e5686e5897df

This commit is contained in:
Vicente Ferrari Smith 2024-05-25 15:21:10 +00:00
parent a716bcfdea
commit abbcdb2ea6
35 changed files with 839 additions and 4 deletions

19
Makefile Normal file
View File

@ -0,0 +1,19 @@
build:
@echo "Building the website..."
go build -o bin/alfheimgame
cp -r ui bin
cp -r static bin
cp favicon.ico bin
GOOS=linux GOARCH=amd64 go build -o bin/linux_amd64/alfheimgame
cp -r ui bin/linux_amd64
cp -r static bin/linux_amd64
cp favicon.ico bin/linux_amd64
.PHONY: deploy
deploy:
rsync -rP --delete bin/linux_amd64 alfheim@alfheimgame.com:~
.PHONY: service
service:
rsync -P alfheimgame.service alfheim@alfheimgame.com:~
ssh -t alfheim@alfheimgame.com 'sudo mv ~/alfheimgame.service /etc/systemd/system && sudo systemctl enable alfheimgame && sudo systemctl restart alfheimgame'

24
alfheimgame.service Normal file
View File

@ -0,0 +1,24 @@
[Unit]
Description=alfheimgame.com website service
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=600
StartLimitBurst=5
[Service]
Type=exec
User=alfheim
Group=alfheim
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
EnvironmentFile=/etc/environment
WorkingDirectory=/home/alfheim/linux_amd64
ExecStart=/home/alfheim/linux_amd64/alfheimgame -addr ':8080'
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target

BIN
bin/alfheimgame Executable file

Binary file not shown.

BIN
bin/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
bin/linux_amd64/alfheimgame Executable file

Binary file not shown.

BIN
bin/linux_amd64/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M480-120v-80h280v-560H480v-80h280q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H480Zm-80-160-55-58 102-102H120v-80h327L345-622l55-58 200 200-200 200Z"/></svg>

After

Width:  |  Height:  |  Size: 279 B

View File

@ -0,0 +1,194 @@
html {
height: 100%;
padding: 8px;
/*background: no-repeat url(/static/image.png);
background-size: cover;
background-position: center;*/
background-color: black;
box-sizing: border-box;
}
body {
background-color: #3475CB;
font-family: "Vollkorn";
color: white;
margin-top: 0px;
margin-left: auto;
margin-right: auto;
margin-bottom: 0px;
min-height: 100%;
max-width: 900px;
}
header {
max-width: 100%;
}
h1 {
font-size: 6rem;
margin: 0px;
}
nav {
padding-left: 8px;
padding-right: 8px;
display: flex;
align-items: center;
justify-content: space-between;
}
.navbuttons a {
display: inline-block;
}
.maintitle {
color: white;
}
.loginbutton {
align-self: center;
display: flex;
background-color: ;
padding-top: 15px;
padding-right: 20px;
padding-bottom: 15px;
padding-left: 20px;
border-radius: 5px;
color: black;
/*box-shadow: 10px 10px 5px lightblue;*/
}
main {
margin: 0px;
display: flex;
flex-direction: column;
justify-content: start;
align-content: center;
min-height: 100%;
max-height: 100%;
margin-left: auto;
margin-right: auto;
}
p {
text-align: center;
}
.account-wrapper {
background: transparent;
border: 2px solid white;
backdrop-filter: blur(20px);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
border-radius: 15px;
padding: 30px 40px;
font-size: 1.5rem;
}
.wrapper {
background: transparent;
border: 2px solid white;
backdrop-filter: blur(20px);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
border-radius: 15px;
padding: 30px 40px;
}
.wrapper h1 {
font-size: 1.5rem;
text-align: center;
}
.wrapper .input-box {
width: 100%;
}
.input-box input {
box-sizing: border-box;
background: transparent;
width: 100%;
height: 100%;
outline: none;
border: 2px solid rgba(255, 255, 255, .2);
border-radius: 25px;
padding: 10px 45px 10px 20px
}
.input-box input::placeholder {
color: white;
}
.wrapper .input-error {
width: 100%;
}
.error {
color: rgba(240, 0, 0, .8);
}
.input-error input {
box-sizing: border-box;
background: transparent;
width: 100%;
height: 100%;
outline: none;
border: 2px solid rgba(255, 0, 0, .2);
border-radius: 25px;
padding: 10px 45px 10px 20px
}
.input-error input::placeholder {
color: red;
}
.login-btn-wrapper {
display: flex;
justify-content: center;
}
.wrapper .btn {
width: 100%;
outline: none;
border: none;
border-radius: 20px;
}
.wrapper .register-link {
display: flex;
justify-content: center;
}
input {
font-family: inherit;
}
footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
background-color: #132123;
color: white;
text-align: center;
}
a:link {
text-decoration: none;
}
.wrapper a:link {
text-decoration: none;
}
.wrapper a:visited {
text-decoration: none;
}
.wrapper a:hover {
text-decoration: underline;
}
video {
width: 100%;
height: auto;
}

Binary file not shown.

View File

@ -0,0 +1,22 @@
{{define "body"}}
<div class="account-wrapper">
<div>Username: {{.Account.Username}}</div>
<div>First name: {{.Account.Firstname}}</div>
<div>Last name: {{.Account.Lastname}}</div>
<div>Color: {{.Account.Color}}</div>
</div>
<div class="wrapper">
<form action="/deleteaccount" method="post">
<div class="login-btn-wrapper">
<input type="submit" value="Delete Account" class="btn">
</div>
</form>
<form method="POST" action="/managebilling">
<div class="login-btn-wrapper">
<input type="submit" value="Manage Billing" class="btn">
</div>
</form>
</div>
{{end}}

View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<!--
/\ | |--- | | |--- || |\ /|
/__\ | |-- |---| |-- || | \ / |
/ \ |___ | | | |___ || | \/ |
-->
<html lang="en">
<head>
<title>Alfheim</title>
<meta charset="utf-8" />
<meta name="description" content="A handcrafted economy and politics MMO." />
<meta name="author" content="Vicente Ferrari Smith" />
<meta name="keywords" content="Alfheim, indie, video game, mmo, colony, colony simulator, vicente ferrari smith, game, economy, politics, alfheim" />
<style>
@font-face {
font-family: "Vollkorn";
src: url(/static/Vollkorn-VariableFont_wght.ttf);
}
</style>
<link href="/static/style.css" rel="stylesheet" />
</head>
<body>
<header>
<nav>
<a href="/"><h1 class="maintitle">Alfheim</h1></a>
<div class="navbuttons">
{{/*{{if not .ActiveSubscription}}
<a href="/subscribe"><div class="loginbutton"><strong>Subscribe</strong></div></a>
{{end}}
{{if .AuthenticatedUser}}
<a href="/account"><div class="loginbutton"><strong>Account</strong></div></a>
<a href="/logout"><div class="loginbutton"><strong>Log out</strong></div></a>
{{else}}
<a href="/login"><div class="loginbutton"><strong>Log in</strong><img src="/static/login_24dp_FILL0_wght400_GRAD0_opsz24.svg" alt=""/></div></a>
<a href="/register"><div class="loginbutton"><strong>Register</strong></div></a>
{{end}}
*/}}
</div>
</nav>
</header>
<main>
{{template "body" .}}
</main>
<footer>
Alfheim &copy; 2024, Vicente Ferrari Smith, Los Andes Studios, Vienna, Austria. All rights reserved.
</footer>
</body>
</html>

View File

@ -0,0 +1,9 @@
{{define "body"}}
<p>Alfheim is a game about humanity: our own will, and our place in the world at large.</p>
<p>Alfheim is an MMO where you&mdash;the player&mdash;administers a medieval settlement of elves.
In an infinite world, and with thousands of other players, you will have to design an efficient command economy, if you wish to be able to compete with your enemies... Or your friends?</p>
<br \>
<video loop autoplay muted><source src="/static/video.mp4" type="video/mp4">Your browser does not support the video tag.</video>
<br \>
<img src="/static/image.png" alt="Alfheim" style="height: 100%; width: 100%; object-fit: contain">
{{end}}

View File

@ -0,0 +1,39 @@
{{define "body"}}
<div class="wrapper">
<form method="POST">
<h1>Log in</h1>
{{with .FormErrors.generic}}
<label class="error">{{.}}</label>
{{end}}
{{with .FormErrors.username}}
<label class="error">{{.}}</label>
{{end}}
<div {{if .FormErrors.username}}class="input-error"{{else}}class="input-box"{{end}}>
<input type="text" id="username" name="username" placeholder="Username" required>
</div>
<br />
{{with .FormErrors.password}}
<label class="error">{{.}}</label>
{{end}}
<div {{if .FormErrors.password}}class="input-error"{{else}}class="input-box"{{end}}>
<input type="password" id="password" name="password" placeholder="Password" required>
</div>
<br />
<div class="login-btn-wrapper">
<input type="submit" value="Log in" class="btn">
</div>
<br />
<a href="/forgotten">Have you forgotten your password?</a>
<div class="register-link">
<p>Don't have an account? <a href="/register">Register</a></p>
</div>
</form>
</div>
{{end}}

View File

@ -0,0 +1,11 @@
{{define "body"}}
<div class="wrapper">
<form method="POST">
<h1>Log out</h1>
<div class="login-btn-wrapper">
<input type="submit" value="Log out" class="btn">
</div>
</form>
</div>
{{end}}

View File

@ -0,0 +1,45 @@
{{define "body"}}
<div class="wrapper">
<form method="POST">
<h1>Register</h1>
{{with .FormErrors.username}}
<label class="error">{{.}}</label>
{{end}}
<div class="input-box" {{with .FormErrors.username}}class="input-error"{{end}}>
<input type="text" id="username" name="username" placeholder="Username" required>
</div>
<br />
<div class="input-box">
<input type="email" id="email" name="email" placeholder="Email" required>
</div>
<br />
<div class="input-box">
<input type="text" id="firstname" name="firstname" placeholder="First Name" required>
</div>
<br />
<div class="input-box">
<input type="text" id="lastname" name="lastname" placeholder="Last Name" required>
</div>
<br />
{{with .FormErrors.password}}
<label class="error">{{.}}</label>
{{end}}
<div class="input-box" {{with .FormErrors.password}}class="input-error"{{end}}>
<input type="password" id="password" name="password" placeholder="Password" required>
</div>
<br />
<div class="login-btn-wrapper">
<input type="submit" value="Register" class="btn">
</div>
</form>
</div>
{{end}}

View File

@ -0,0 +1,8 @@
{{define "body"}}
{{range .Prices}}
<div class="wrapper">
{{$price := divide .UnitAmountDecimal 100}}
{{.Currency}} {{printf "%.2f" $price}}
</div>
{{end}}
{{end}}

View File

@ -0,0 +1,5 @@
{{define "body"}}
<script async src="https://js.stripe.com/v3/pricing-table.js"></script>
<stripe-pricing-table pricing-table-id="prctbl_1PIaJpKUHKCjyTmcy1ONKQiT" publishable-key="pk_test_51PGebgKUHKCjyTmcyuhj7C5lfHfKFLCxRJ2opoqxL3mGEHSRaMvOHYKKgX4MdFfESW78dssjyunboUcFhg3LTwmn005PmzVIXw" customer-session-client-secret={{.ClientSecret}}>
</stripe-pricing-table>
{{end}}

Binary file not shown.

BIN
bin/static/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="M480-120v-80h280v-560H480v-80h280q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H480Zm-80-160-55-58 102-102H120v-80h327L345-622l55-58 200 200-200 200Z"/></svg>

After

Width:  |  Height:  |  Size: 279 B

194
bin/static/style.css Normal file
View File

@ -0,0 +1,194 @@
html {
height: 100%;
padding: 8px;
/*background: no-repeat url(/static/image.png);
background-size: cover;
background-position: center;*/
background-color: black;
box-sizing: border-box;
}
body {
background-color: #3475CB;
font-family: "Vollkorn";
color: white;
margin-top: 0px;
margin-left: auto;
margin-right: auto;
margin-bottom: 0px;
min-height: 100%;
max-width: 900px;
}
header {
max-width: 100%;
}
h1 {
font-size: 6rem;
margin: 0px;
}
nav {
padding-left: 8px;
padding-right: 8px;
display: flex;
align-items: center;
justify-content: space-between;
}
.navbuttons a {
display: inline-block;
}
.maintitle {
color: white;
}
.loginbutton {
align-self: center;
display: flex;
background-color: ;
padding-top: 15px;
padding-right: 20px;
padding-bottom: 15px;
padding-left: 20px;
border-radius: 5px;
color: black;
/*box-shadow: 10px 10px 5px lightblue;*/
}
main {
margin: 0px;
display: flex;
flex-direction: column;
justify-content: start;
align-content: center;
min-height: 100%;
max-height: 100%;
margin-left: auto;
margin-right: auto;
}
p {
text-align: center;
}
.account-wrapper {
background: transparent;
border: 2px solid white;
backdrop-filter: blur(20px);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
border-radius: 15px;
padding: 30px 40px;
font-size: 1.5rem;
}
.wrapper {
background: transparent;
border: 2px solid white;
backdrop-filter: blur(20px);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
border-radius: 15px;
padding: 30px 40px;
}
.wrapper h1 {
font-size: 1.5rem;
text-align: center;
}
.wrapper .input-box {
width: 100%;
}
.input-box input {
box-sizing: border-box;
background: transparent;
width: 100%;
height: 100%;
outline: none;
border: 2px solid rgba(255, 255, 255, .2);
border-radius: 25px;
padding: 10px 45px 10px 20px
}
.input-box input::placeholder {
color: white;
}
.wrapper .input-error {
width: 100%;
}
.error {
color: rgba(240, 0, 0, .8);
}
.input-error input {
box-sizing: border-box;
background: transparent;
width: 100%;
height: 100%;
outline: none;
border: 2px solid rgba(255, 0, 0, .2);
border-radius: 25px;
padding: 10px 45px 10px 20px
}
.input-error input::placeholder {
color: red;
}
.login-btn-wrapper {
display: flex;
justify-content: center;
}
.wrapper .btn {
width: 100%;
outline: none;
border: none;
border-radius: 20px;
}
.wrapper .register-link {
display: flex;
justify-content: center;
}
input {
font-family: inherit;
}
footer {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
background-color: #132123;
color: white;
text-align: center;
}
a:link {
text-decoration: none;
}
.wrapper a:link {
text-decoration: none;
}
.wrapper a:visited {
text-decoration: none;
}
.wrapper a:hover {
text-decoration: underline;
}
video {
width: 100%;
height: auto;
}

BIN
bin/static/video.mp4 Normal file

Binary file not shown.

22
bin/ui/account.html Normal file
View File

@ -0,0 +1,22 @@
{{define "body"}}
<div class="account-wrapper">
<div>Username: {{.Account.Username}}</div>
<div>First name: {{.Account.Firstname}}</div>
<div>Last name: {{.Account.Lastname}}</div>
<div>Color: {{.Account.Color}}</div>
</div>
<div class="wrapper">
<form action="/deleteaccount" method="post">
<div class="login-btn-wrapper">
<input type="submit" value="Delete Account" class="btn">
</div>
</form>
<form method="POST" action="/managebilling">
<div class="login-btn-wrapper">
<input type="submit" value="Manage Billing" class="btn">
</div>
</form>
</div>
{{end}}

54
bin/ui/base.html Normal file
View File

@ -0,0 +1,54 @@
<!DOCTYPE html>
<!--
/\ | |--- | | |--- || |\ /|
/__\ | |-- |---| |-- || | \ / |
/ \ |___ | | | |___ || | \/ |
-->
<html lang="en">
<head>
<title>Alfheim</title>
<meta charset="utf-8" />
<meta name="description" content="A handcrafted economy and politics MMO." />
<meta name="author" content="Vicente Ferrari Smith" />
<meta name="keywords" content="Alfheim, indie, video game, mmo, colony, colony simulator, vicente ferrari smith, game, economy, politics, alfheim" />
<style>
@font-face {
font-family: "Vollkorn";
src: url(/static/Vollkorn-VariableFont_wght.ttf);
}
</style>
<link href="/static/style.css" rel="stylesheet" />
</head>
<body>
<header>
<nav>
<a href="/"><h1 class="maintitle">Alfheim</h1></a>
<div class="navbuttons">
{{/*{{if not .ActiveSubscription}}
<a href="/subscribe"><div class="loginbutton"><strong>Subscribe</strong></div></a>
{{end}}
{{if .AuthenticatedUser}}
<a href="/account"><div class="loginbutton"><strong>Account</strong></div></a>
<a href="/logout"><div class="loginbutton"><strong>Log out</strong></div></a>
{{else}}
<a href="/login"><div class="loginbutton"><strong>Log in</strong><img src="/static/login_24dp_FILL0_wght400_GRAD0_opsz24.svg" alt=""/></div></a>
<a href="/register"><div class="loginbutton"><strong>Register</strong></div></a>
{{end}}
*/}}
</div>
</nav>
</header>
<main>
{{template "body" .}}
</main>
<footer>
Alfheim &copy; 2024, Vicente Ferrari Smith, Los Andes Studios, Vienna, Austria. All rights reserved.
</footer>
</body>
</html>

9
bin/ui/index.html Normal file
View File

@ -0,0 +1,9 @@
{{define "body"}}
<p>Alfheim is a game about humanity: our own will, and our place in the world at large.</p>
<p>Alfheim is an MMO where you&mdash;the player&mdash;administers a medieval settlement of elves.
In an infinite world, and with thousands of other players, you will have to design an efficient command economy, if you wish to be able to compete with your enemies... Or your friends?</p>
<br \>
<video loop autoplay muted><source src="/static/video.mp4" type="video/mp4">Your browser does not support the video tag.</video>
<br \>
<img src="/static/image.png" alt="Alfheim" style="height: 100%; width: 100%; object-fit: contain">
{{end}}

39
bin/ui/login.html Normal file
View File

@ -0,0 +1,39 @@
{{define "body"}}
<div class="wrapper">
<form method="POST">
<h1>Log in</h1>
{{with .FormErrors.generic}}
<label class="error">{{.}}</label>
{{end}}
{{with .FormErrors.username}}
<label class="error">{{.}}</label>
{{end}}
<div {{if .FormErrors.username}}class="input-error"{{else}}class="input-box"{{end}}>
<input type="text" id="username" name="username" placeholder="Username" required>
</div>
<br />
{{with .FormErrors.password}}
<label class="error">{{.}}</label>
{{end}}
<div {{if .FormErrors.password}}class="input-error"{{else}}class="input-box"{{end}}>
<input type="password" id="password" name="password" placeholder="Password" required>
</div>
<br />
<div class="login-btn-wrapper">
<input type="submit" value="Log in" class="btn">
</div>
<br />
<a href="/forgotten">Have you forgotten your password?</a>
<div class="register-link">
<p>Don't have an account? <a href="/register">Register</a></p>
</div>
</form>
</div>
{{end}}

11
bin/ui/logout.html Normal file
View File

@ -0,0 +1,11 @@
{{define "body"}}
<div class="wrapper">
<form method="POST">
<h1>Log out</h1>
<div class="login-btn-wrapper">
<input type="submit" value="Log out" class="btn">
</div>
</form>
</div>
{{end}}

45
bin/ui/register.html Normal file
View File

@ -0,0 +1,45 @@
{{define "body"}}
<div class="wrapper">
<form method="POST">
<h1>Register</h1>
{{with .FormErrors.username}}
<label class="error">{{.}}</label>
{{end}}
<div class="input-box" {{with .FormErrors.username}}class="input-error"{{end}}>
<input type="text" id="username" name="username" placeholder="Username" required>
</div>
<br />
<div class="input-box">
<input type="email" id="email" name="email" placeholder="Email" required>
</div>
<br />
<div class="input-box">
<input type="text" id="firstname" name="firstname" placeholder="First Name" required>
</div>
<br />
<div class="input-box">
<input type="text" id="lastname" name="lastname" placeholder="Last Name" required>
</div>
<br />
{{with .FormErrors.password}}
<label class="error">{{.}}</label>
{{end}}
<div class="input-box" {{with .FormErrors.password}}class="input-error"{{end}}>
<input type="password" id="password" name="password" placeholder="Password" required>
</div>
<br />
<div class="login-btn-wrapper">
<input type="submit" value="Register" class="btn">
</div>
</form>
</div>
{{end}}

8
bin/ui/subscribe.html Normal file
View File

@ -0,0 +1,8 @@
{{define "body"}}
{{range .Prices}}
<div class="wrapper">
{{$price := divide .UnitAmountDecimal 100}}
{{.Currency}} {{printf "%.2f" $price}}
</div>
{{end}}
{{end}}

View File

@ -0,0 +1,5 @@
{{define "body"}}
<script async src="https://js.stripe.com/v3/pricing-table.js"></script>
<stripe-pricing-table pricing-table-id="prctbl_1PIaJpKUHKCjyTmcy1ONKQiT" publishable-key="pk_test_51PGebgKUHKCjyTmcyuhj7C5lfHfKFLCxRJ2opoqxL3mGEHSRaMvOHYKKgX4MdFfESW78dssjyunboUcFhg3LTwmn005PmzVIXw" customer-session-client-secret={{.ClientSecret}}>
</stripe-pricing-table>
{{end}}

2
go.mod
View File

@ -12,5 +12,7 @@ require (
require (
github.com/gorilla/securecookie v1.1.2 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
)

20
main.go
View File

@ -12,10 +12,10 @@ import _ "github.com/lib/pq"
import "database/sql"
import "github.com/gorilla/sessions"
import "regexp"
//import "golang.org/x/crypto/bcrypt"
import "golang.org/x/crypto/acme/autocert"
import "crypto/tls"
import "github.com/stripe/stripe-go/v78"
//import "github.com/stripe/stripe-go/v78/customer"
var users *Usermodel
var subscriptions *SubscriptionModel
@ -27,6 +27,7 @@ var emailrx = regexp.MustCompile("/^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-
func main() {
addr := flag.String("addr", ":80", "HTTP network addr")
_ = addr
flag.Parse()
fmt.Println("Hello, Sailor!")
@ -83,6 +84,19 @@ func main() {
//mux.HandleFunc("/managebilling", require_authenticated_user(managebilling))
//mux.HandleFunc("/webhook", webhooks)
cert := &autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("alfheimgame.com", "www.alfheimgame.com"),
Cache: autocert.DirCache("certs"),
}
log.Fatal(http.ListenAndServe(*addr, secure_headers(mux)))
server := &http.Server{
Addr: ":443",
Handler: secure_headers(mux),
TLSConfig: &tls.Config{GetCertificate: cert.GetCertificate},
}
go http.ListenAndServe(":http", cert.HTTPHandler(nil))
log.Fatal(server.ListenAndServeTLS("", ""))
}

View File

@ -3,7 +3,7 @@
<p>Alfheim is an MMO where you&mdash;the player&mdash;administers a medieval settlement of elves.
In an infinite world, and with thousands of other players, you will have to design an efficient command economy, if you wish to be able to compete with your enemies... Or your friends?</p>
<br \>
<video autoplay muted><source src="/static/video.mp4" type="video/mp4">Your browser does not support the video tag.</video>
<video loop autoplay muted><source src="/static/video.mp4" type="video/mp4">Your browser does not support the video tag.</video>
<br \>
<img src="/static/image.png" alt="Alfheim" style="height: 100%; width: 100%; object-fit: contain">
{{end}}