Erstes Commit

This commit is contained in:
2026-02-19 18:37:05 +01:00
commit 74a5b5bbb7
97 changed files with 1792 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
backup
hugo/public

9
Dockerfile Normal file
View File

@@ -0,0 +1,9 @@
FROM floryn90/hugo:0.155.1-alpine AS builder
WORKDIR /src
USER root
COPY hugo /src
RUN hugo --minify
FROM nginx:1.28.0-alpine-slim
COPY --from=builder /src/public /usr/share/nginx/html
HEALTHCHECK --interval=1m --timeout=10s --start-period=10s CMD wget -q -O /dev/null http://localhost/ || exit 1

1
README.md Normal file
View File

@@ -0,0 +1 @@
# alphabreed website

BIN
dev-files/favicons.xcf Normal file

Binary file not shown.

50
docker-compose.yml Normal file
View File

@@ -0,0 +1,50 @@
services:
website:
build: .
image: alphabreed:v1.0.2
restart: unless-stopped
networks:
- frontend
pocketbase:
image: adrianmusante/pocketbase:0.35
restart: unless-stopped
environment:
- POCKETBASE_DEBUG=false
- POCKETBASE_ADMIN_EMAIL=${EMAIL}
- POCKETBASE_ADMIN_PASSWORD=${PASSWORD}
volumes:
- pocketbase_data:/pocketbase
networks:
- frontend
gokapi:
image: f0rc3/gokapi:v2.1.0
restart: unless-stopped
environment:
- TZ=Europe/Berlin
volumes:
- gokapi_data:/app/data
- gokapi_config:/app/config
networks:
- frontend
ddns_updater:
image: cloubit/ddns-updater:v1.1
environment:
- APIURL_BASE=https://ydns.io/api/v1/update/?host=
- DDOMAIN1=alphabreed.ydns.eu
- ENABLE_IPV4=true
- UPDATE_DELAY=1800
restart: unless-stopped
volumes:
pocketbase_data:
gokapi_data:
gokapi_config:
networks:
frontend:
name: frontend
external: true

0
hugo/.hugo_build.lock Normal file
View File

View File

@@ -0,0 +1,6 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
---

40
hugo/config.toml Normal file
View File

@@ -0,0 +1,40 @@
baseURL = "https://www.alphabreed.com/"
languageCode = "de-de"
title = "alphabreed"
disableKinds = ["taxonomy"]
[params]
apibaseurl = "http://192.168.1.2/api"
[outputs]
page = ["HTML"]
home = ["HTML"]
section = ["HTML"]
[markup]
[markup.goldmark]
[markup.goldmark.renderer]
unsafe = true
[menus]
[[menus.main]]
name = "Raspberry Pi"
pageRef = "/pi/"
weight = 10
[[menus.main]]
name = "Gameplays"
pageRef = "/gameplays/"
weight = 20
[[menus.main]]
name = "Interactive Fiction"
pageRef = "/if/"
weight = 30
[[menus.main]]
name = "Comics"
pageRef = "/comics/"
weight = 40
[[menus.main]]
name = "Bilder"
pageRef = "/bilder/"
weight = 50
[[menus.main]]
name = "Impressum"
pageRef = "/impressum/"
weight = 60

11
hugo/content/_index.md Normal file
View File

@@ -0,0 +1,11 @@
---
title: "Über alphabreed"
description: "Eine private Seite über Interactive Fiction, Comics, Bilder und Kurzgeschichten."
---
# über alphabreed
_alphabreed_ ist meine kleine Website zu verschiedenen Themen, die mich gerade interessieren. Die Inhalte ändern sich nicht sehr häufig, können sich dafür aber auch ser radikal ändern. Die aktuellen Themenbereiche finden sich im Menü.
Die Seite wird nun lokal auf meinem [Raspberry Pi](/pi/) gehostet. Dadurch kann beim Hosting nicht nur Energie gespart werden, sondern ich habe auch viel besseren Zugriff auf Server und Software.
Da der Übertragungsweg nun meine eigene Internetverbindung nutzt kann es zu gelegentlichen Ausfällen kommen, die aber auch relativ schnell wieder behoben sein sollten.

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,12 @@
---
title: "Bilder"
description: "Meine Bilder, jetzt auf DeviantArt"
---
<h1 id="bilder">Bilder</h1>
<p>Ich habe mich dazu entschlossen, meine Bilder in Zukunft nur noch auf <em>DeviantArt</em> einzustellen, damit ich sie nicht doppelt pflegen muss. Ihr könnt sie in meiner <a href="https://masterjazzman.deviantart.com/" target="_blank">DeviantArt Galerie</a> betrachten.</p>
<p style="margin-top:100px"><a href="https://masterjazzman.deviantart.com/" target="_blank" style="font-size:0">
<img src="eragon-and-arya.jpg" style="width:30%; height:auto; transform:rotate(-10deg);" />
<img src="oh-please-do-stay-for-dinner.jpg" style="width:30%; height:auto; transform:rotate(10deg); margin-left:-5%;" />
<img src="shall-i-compare-thee.jpg" style="width:25%; height:auto; transform:rotate(-7deg); margin-left:-5%;" />
<img src="happy-easter.jpg" style="width:30%; height:auto; transform:rotate(10deg); margin-left:-5%;" />
</a></p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,176 @@
---
title: "Bookmarks"
---
<main x-data="{
store: null,
categories: [],
openCategoryId: '',
slideInOpen: false,
showFavletCode: false,
editCategory: null,
editBookmark: null,
host: '',
async updateBookmarks() {
const result = await this.store.getBookmarks();
const grouped = Object.groupBy(result, (bookmark) => bookmark.expand.category.id);
this.categories = Object.values(grouped).map((group) => {
const category = group[0].expand.category;
return {
id: category.id,
name: category.name,
bookmarks: group.map((bookmark) => ({
category: category.id,
id: bookmark.id,
name: bookmark.name,
url: bookmark.url
}))
};
});
this.categories.sort((a, b) => a.name.localeCompare(b.name));
},
openCategory(id, element) {
if (this.openCategoryId == id) {
this.openCategoryId = '';
return;
}
this.openCategoryId = id;
element.scrollIntoView(true);
},
async saveCategory() {
if (!this.editCategory || !this.editCategory.name)
return;
await this.store.updateBookmarkCategory(
this.editCategory.id, this.editCategory.name
);
this.slideInOpen = false;
await this.updateBookmarks();
},
async deleteCategory() {
if (!this.editCategory)
return;
await this.store.deleteBookmarkCategory(this.editCategory.id);
this.slideInOpen = false;
await this.updateBookmarks();
},
async saveBookmark() {
let categoryId = this.editBookmark.category || this.categories[0].id;
if (!this.editBookmark || !this.editBookmark.name || !this.editBookmark.url || (!categoryId && !this.editBookmark.newcategory))
return;
if (this.editBookmark.newcategory)
categoryId = await this.store.createBookmarkCategory(this.editBookmark.newcategory);
if (this.editBookmark.id) {
await this.store.updateBookmark(
this.editBookmark.id,
categoryId,
this.editBookmark.name,
this.editBookmark.url
);
}
else {
await this.store.createBookmark(
categoryId,
this.editBookmark.name,
this.editBookmark.url
);
}
this.slideInOpen = false;
this.openCategoryId = categoryId;
await this.updateBookmarks();
},
async deleteBookmark() {
if (!this.editBookmark)
return;
await this.store.deleteBookmark(this.editBookmark.id);
this.slideInOpen = false;
await this.updateBookmarks();
},
async init() {
this.store = Alpine.store('alphabreed');
this.store.backUrl = '/bookmarks/';
this.host = window.location.protocol + '//' + window.location.host;
const tokenOK = await this.store.checkToken();
if (tokenOK)
this.updateBookmarks();
}
}">
<p><button @click="editBookmark = {}; editCategory = null; showFavletCode = false; slideInOpen = true;">+ Bookmark</button></p>
<span id="favlet" @click="showFavletCode = true; editCategory = null; editBookmark = null; slideInOpen = true;">Generate Favlet code</span>
<div class="categories">
<template x-for="category in categories" :key="category.id">
<div class="category" :class="{'open': category.id == openCategoryId}">
<h1>
<span class="title" x-text="category.name" @click="openCategory(category.id, $el)"></span>
<span class="badge" x-text="category.bookmarks.length"></span>
<span class="edit" @click="editCategory = Object.assign({}, category); editBookmark = null; showFavletCode = false; slideInOpen = true;"></span>
</h1>
<ul class="bookmarks">
<template x-for="bookmark in category.bookmarks" :key="bookmark.id">
<li class="bookmark">
<div class="editicon" @click="editBookmark = Object.assign({}, bookmark); editCategory = null; showFavletCode = false; slideInOpen = true;">
<img :src="'https://www.google.com/s2/favicons?domain=' + encodeURIComponent(bookmark.url)">
</div>
<a class="link" :href="bookmark.url" target="_blank">
<span x-text="bookmark.name"></span>
</a>
</li>
</template>
</ul>
</div>
</template>
</div>
<div id="slidein" :class="{'open': slideInOpen}">
<span class="closer" @click="slideInOpen = false;"></span>
<div class="inner">
<template x-if="showFavletCode">
<div>
<p>Favlet code:</p>
<p class="favletcode" x-text='"javascript:(function(w,e){w.open(\""+host+"/bookmarks/add/?name=\"+e(document.title)+\"&url=\"+e(w.location.href),\"_blank\",\"height=500,width=500,location=0,menubar=0,scrollbars=0,status=0,titlebar=0,toolbar=0\")})(window,encodeURIComponent)"'></p>
</div>
</template>
<template x-if="editCategory">
<div>
<div class="field textfield">
<label for="categoryname">Category name</label>
<input id="categoryname" type="text" x-model="editCategory.name">
</div>
<div class="field buttons">
<button @click="saveCategory()">Save</button>
</div>
<div class="field buttons">
<button @click="deleteCategory()">Delete</button>
</div>
</div>
</template>
<template x-if="editBookmark">
<div>
<div class="field textfield">
<label for="bookmarkname">Bookmark name</label>
<input id="bookmarkname" type="text" x-model="editBookmark.name">
</div>
<div class="field textfield">
<label for="bookmarkurl">URL</label>
<input id="bookmarkurl" type="text" x-model="editBookmark.url">
</div>
<div class="field selectfield">
<label for="bookmarkcategory">Category</label>
<select id="bookmarkcategory" x-model="editBookmark.category">
<template x-for="category in categories" :key="category.id">
<option :value="category.id" x-text="category.name" :selected="category.id == editBookmark.category">
</template>
</select>
</div>
<div class="field textfield">
<label for="bookmarknewcategory">New category</label>
<input id="bookmarknewcategory" type="text" x-model="editBookmark.newcategory">
</div>
<div class="field buttons">
<button @click="saveBookmark()">Save</button>
</div>
<div class="field buttons">
<button @click="deleteBookmark()">Delete</button>
</div>
</div>
</template>
</div>
</div>
</main>

View File

@@ -0,0 +1,61 @@
---
title: "Add Bookmark"
---
<main x-data="{
store: null,
categories: [],
name: '',
url: '',
categoryId: '',
newcategory: '',
async saveBookmark() {
if (!this.categoryId)
this.categoryId = this.categories[0].id;
if (!this.name || !this.url || (!this.categoryId && !this.newcategory))
return;
if (this.newcategory)
this.categoryId = await this.store.createBookmarkCategory(this.newcategory);
const result = await this.store.createBookmark(
this.categoryId, this.name, this.url
);
if (result)
window.close();
},
async init() {
this.store = Alpine.store('alphabreed');
this.store.backUrl = '/bookmarks/add/';
const params = new URLSearchParams(window.location.search);
this.name = params.get('name') || '';
this.url = params.get('url') || '';
const tokenOK = await this.store.checkToken();
if (tokenOK)
this.categories = await this.store.getBookmarkCategories();
}
}">
<div id="addcontent">
<div class="field textfield">
<label for="bookmarkname">Bookmark name</label>
<input id="bookmarkname" type="text" x-model="name">
</div>
<div class="field textfield">
<label for="bookmarkurl">URL</label>
<input id="bookmarkurl" type="text" x-model="url">
</div>
<div class="field selectfield">
<label for="bookmarkcategory">Category</label>
<select id="bookmarkcategory" x-model="categoryId">
<template x-for="category in categories" :key="category.id">
<option :value="category.id" x-text="category.name" :selected="category.id == categoryId">
</template>
</select>
</div>
<div class="field textfield">
<label for="bookmarknewcategory">New category</label>
<input id="bookmarknewcategory" type="text" x-model="newcategory">
</div>
<div class="field buttons">
<button @click="saveBookmark()">Save</button>
</div>
</div>
</main>

View File

@@ -0,0 +1,44 @@
{{ $basePath := "content/comics" }}
{{ range (os.ReadDir $basePath) }}
{{ if .IsDir }}
{{ $comicDir := .Name }}
{{ $fullPath := printf "%s/%s" $basePath $comicDir }}
{{ $indexContent := readFile (printf "%s/_index.md" $fullPath) }}
{{ $comicData := transform.Unmarshal $indexContent }}
{{ $images := slice }}
{{ range (os.ReadDir $fullPath) }}
{{ if findRE "\\.(jpg|jpeg|png|webp|gif)$" .Name }}
{{ $images = $images | append .Name }}
{{ end }}
{{ end }}
{{ $images = sort $images }}
{{ $imageCount := len $images }}
{{ range $index, $image := $images }}
{{ if gt $index 0 }}
{{ $pageNum := math.Add $index 1 }}
{{ $nextPage := "" }}
{{ $lastPage := "" }}
{{ if lt $pageNum $imageCount }}
{{ $nextPage = printf "../%d/" (math.Add $pageNum 1) }}
{{ $lastPage = printf "../%d/" $imageCount }}
{{ end }}
{{ $prevPage := "../" }}
{{ if gt $pageNum 2 }}
{{ $prevPage = printf "../%d/" $index }}
{{ end }}
{{ $page := dict
"path" (printf "%s/%d" $comicDir $pageNum)
"title" (printf "%s, Seite %d" $comicData.title $pageNum)
"description" $comicData.description
"params" (dict
"image" $image
"next" $nextPage
"prev" $prevPage
"last" $lastPage
)
}}
{{ $.AddPage $page }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}

View File

@@ -0,0 +1,7 @@
---
title: "Comics"
layout: "comiclist"
---
# Comics
Hier gibt es meine verschiedenen Comics zu sehen. Einige Seiten haben Bildfehler, die auf einen früheren Festplattencrash zurückzuführen sind. Ich bitte diese kleinen Makel zu verzeihen. Für alle Comics wird Kenntnis der englischen Sprache vorausgesetzt.

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -0,0 +1,4 @@
---
title: "Alte Comics"
description: "Meine ersten Veruche, Comics zu zeichnen. Zu schade, um sie einfach verschwinden zu lassen."
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -0,0 +1,4 @@
---
title: "Shadow of Jupiter"
description: "Eine kleine postapokalyptische Kurzgeschichte über Liebe, Vertrauen, Verlust und die Hoffung auf eine bessere Zukunft."
---

View File

@@ -0,0 +1,40 @@
{{ $basePath := "content/gameplays" }}
{{ $site := .Site }}
{{ range (os.ReadDir $basePath) }}
{{ if .IsDir }}
{{ $gameplayDir := .Name }}
{{ $fullPath := printf "%s/%s" $basePath $gameplayDir }}
{{ $indexContent := readFile (printf "%s/_index.md" $fullPath) }}
{{ $indexParts := split $indexContent "---" }}
{{ $gameplayData := index $indexParts 1 | transform.Unmarshal }}
{{ $gameplayBody := index $indexParts 2 }}
{{ $episodeCount := len $gameplayData.episodes }}
{{ range $index, $episode := $gameplayData.episodes }}
{{ if gt $index 0 }}
{{ $pageNum := math.Add $index 1 }}
{{ $content := dict
"mediaType" "text/markdown"
"value" $gameplayBody
}}
{{ $episodeName := "" }}
{{ if $episode.title }}
{{ $episodeName = $episode.title }}
{{ end }}
{{ $page := dict
"path" (printf "%s/%d" $gameplayDir $pageNum)
"title" (printf "%s, Folge %d" $gameplayData.title $pageNum)
"description" $episode.description
"content" $content
"params" (dict
"video" $episode.video
"gameplay" $gameplayData.fullname
"name" $episodeName
"index" $index
"episodes" $gameplayData.episodes
)
}}
{{ $.AddPage $page }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}

View File

@@ -0,0 +1,7 @@
---
title: "Gameplays"
layout: "gameplaylist"
---
# Gameplays
Hier gibt es eine Liste meiner jüngsten Gameplay-Videos. Die Liste wird immer sehr kurz sein, denn auf meinem armen [Raspberry Pi](/pi/) steht nicht genug Platz für größere Mengen an Videos zur verfügung. Also einfach ab und zu mal vorbei schauen, ob es was neues gibt.

View File

@@ -0,0 +1,13 @@
---
title: "Journey"
fullname: "Journey"
episodes:
- description: "Journey ist ein sehr schönes, aber auch sehr kurzes Spiel. Daher gibt es nur eine Episode."
video: "https://files.alphabreed.com/dh/rAYEddHw5RdsFvW/journey.mp4"
---
Entwickler: [thatgamecompany](https://thatgamecompany.com/)\
Herausgeber: [Annapurna Interactive](https://www.annapurnainteractive.com/)
> In Journey erkundest du eine alte, mysteriöse Welt, schwebst über Ruinen und gleitest über die Sandlandschaft, um ihre Geheimnisse zu entdecken. Du kannst allein spielen oder mit einem Mitreisenden, um die riesige Welt gemeinsam zu erkunden. Mit atemberaubender Grafik und für den Grammy nominierter Musik liefert Journey ein fantastisches Erlebnis wie kein anderes Spiel.
Quelle: [Steam](https://store.steampowered.com/app/638230/Journey/)

View File

@@ -0,0 +1,55 @@
---
title: "ZanZarah"
fullname: "ZanZarah: The Hidden Portal"
episodes:
- title: "Die Prophezeiung"
description: "Amy wird aus ihrem langweiligen Leben in London in eine Welt voller Phantasie und Fabelwesen geworfen."
video: "https://files.alphabreed.com/dh/L93A2fpRBzhO1y5/zanzarah1.mp4"
- title: "Pixie-Plage"
description: "ZanZarah wird von grausigen Pixies heimgesucht. Wenn doch bloss jemand etwas dagegen unternehmen könnte..."
video: "https://files.alphabreed.com/dh/SRy81csM6ZfeMsQ/zanzarah2.mp4"
- title: "Tiralin"
description: "Am Rande des Waldes erhebt sich die majestätische Stadt der Elfen. Es gibt viel zu erkunden."
video: "https://files.alphabreed.com/dh/v9s7yG3i0dNIPAW/zanzarah3.mp4"
- title: "Die Belagerung von Dunmore"
description: "Die Stadt im Sumpf ist ohnehin kein einladender Ort, und dann wird sie auch noch von Schattenelfen angegriffen."
video: "https://files.alphabreed.com/dh/QYOqID6qr3lDeiy/zanzarah4.mp4"
- title: "Unkraut-Ex!"
description: "Mit der Feenkarte der Natur können wir uns endlich um die lästigen Dornenbüsche kümmern."
video: "https://files.alphabreed.com/dh/gzp98mkBrb9F2vb/zanzarah5.mp4"
- title: "Zu viel Eis ist ungesund"
description: "Die Eis-Feen der Schattenelfen setzen unserer Gruppe stark zu, aber zu guterletzt bleiben wir siegreich."
video: "https://files.alphabreed.com/dh/JXpJLa7txAKEiMv/zanzarah6.mp4"
- title: "Prüfung im Turm der Zwerge"
description: "Die Zwerge bewachen den Elementarschlüssel der Erde und geben ihn nur heraus, wenn wir uns als würdig erweisen."
video: "https://files.alphabreed.com/dh/HEscBlbiXcGGfbe/zanzarah8.mp4"
- title: "Unterwegs zur Bergspitze"
description: "Wir durchstreifen die Bergwelt, helfen einigen Zwergen und kommen auf den schneebedeckten Gipfeln an."
video: "https://files.alphabreed.com/dh/DGJYIFvOtvV81S4/zanzarah9.mp4"
- title: "Die ohne Ton aber mit Lasse"
description: "Auf der Bergspitze gibt es endlich auch einige Eis-Feen zu fangen und in einer abgelegenen Höhle befreien wir den Zwerg Lasse, der nach seinem Vater dem Zwergenmeister sucht und uns für seine Rettung den Elementarschlüssel der Luft schenkt."
video: "https://files.alphabreed.com/dh/R0AAo6HwGpjDosv/zanzarah10.mp4"
- title: "In die Wolken geschraubt"
description: "Nach einem Kurzen Abstecher zur Berghütte nutzen wir den ersten der Zwergenaufzüge um das Wolkenreich zu betreten."
video: "https://files.alphabreed.com/dh/FzMdaJtm0miYQwc/zanzarah11.mp4"
- title: "Der Weiße Druide"
description: "Der Weiße Druide, der Beschützer von ZanZarah, wird von fiesen Schattenelfen angegriffen. Amy eilt zur Rettung."
video: "https://files.alphabreed.com/dh/H9AxbUkVGeK1MZp/zanzarah12.mp4"
- title: "Herrin der Arena"
description: "Wir beweisen uns in der Alten Arena des Wolkenreichs gegen einen Schattenelfen mit mächtigen Dunkelfeen. Offenbar stehen auch die Zwerge mit den Schattenelfen im Bunde. Verquerer und verquerer."
video: "https://files.alphabreed.com/dh/f8fph7hzuyG15JR/zanzarah13.mp4"
- title: "Dunkle Höhlen"
description: "Nach dem Höhenflug im Wolkenreich geht es nun zurück nach Tiralin und dann in die Tiefen der Erde, zur Zwergenstadt Monagham."
video: "https://files.alphabreed.com/dh/N1xVVKEDOfdv9i4/zanzarah14.mp4"
- title: "Das Turnier im Großen Baum"
description: "Jemand muss ins Schattenreich vordringen und die Dunklen Mächte besiegen. Ein Turnier im Großen Baum des Sumpfes soll entscheiden, wer dieses zweifelhafte Privileg haben darf."
video: "https://files.alphabreed.com/dh/zi82zNfK9aH1xa2/zanzarah15.mp4"
---
Entwickler: [Funatics](https://www.funatics.de/)\
Herausgeber: [Daedalic Entertainment](https://www.daedalic.com/)
> Eine begeisternde Geschichte von zwei Welten… die eine ist eine Welt der Phantasie, die andere ist jene Welt, wie wir sie kennen. Es gibt nur einen Menschen, der in der Lage ist, die beiden Welten wieder zu vereinen: Ein 18 Jahre altes Mädchen, das noch nichts von ihrer Macht und ihre Bedeutung für beide Reiche ahnt…
>
> Führe Amy in die Welt von Zanzarah; der Magie und des Kampfes. Erforsche das Reich mystischer Fabelwesen und nutze die stetig wachsende Macht deiner Heldin, um die hier lebenden Feen und Dämonen zu fangen und zu erobern. Steige mit ihnen in die Arena, wenn sich dir Kobolde oder andere magische Wesen in den Weg stellen.
Quelle: [Steam](https://store.steampowered.com/app/384570/Zanzarah_The_Hidden_Portal/)

10
hugo/content/if/_index.md Normal file
View File

@@ -0,0 +1,10 @@
---
title: "Interactive Fiction"
---
# Interactive Fiction
Bei «Interactive Fiction» handelt es sich um die Form der digitalen Unterhaltung, die allgemein auch unter dem Begriff «Textadventures» bekannt ist. Der Spieler schlüpft dabei zumeist in die Rolle des Protagonisten in einer interaktiven Geschichte und kann mittels Tastaturkommandos in seiner virtuellen Umwelt agieren. Zwar ist die Blütezeit der kommerziellen Textadventures schon ein paar Jahre vergangen, doch erfreuen sie sich in Fankreisen immer noch großer Beliebtheit.
Diese Seite spiegelt meine Ansichten zu den Themen Spieldesign und Erstellung von Interactive Fiction wieder, aber diese Prinzipien sind natürlich in keinster Weise in Stein gemeißelt. Es steht jedem Autor frei, sie nach Bedarf anzupassen oder komplett zu ignorieren.
In Zukunft soll dieser Bereich auch noch um die Rubrik «Implementation» erweitert werden. Dort werde ich dann auf eher technische Aspekte und die eigentliche Programmierung eingehen. Für die Beispiele werde ich Inform 6 verwenden, da dies auch die Sprache meiner Wahl zum erstellen von Textadventures ist.

68
hugo/content/if/design.md Normal file
View File

@@ -0,0 +1,68 @@
---
title: "Designrichtlinien"
description: "Richtlinien für das Design von Interactive Fiction."
menu: "if"
---
# Designrichtlinien
Mit den Möglichkeiten eines aktuellen Textadventure-Entwicklungssystems ein kurzes Spiel auf die Beine zu stellen ist relativ einfach, insbesondere da dazu keine weiteren Fähigkeiten außer der reinen Programmierung nötig sind. Ein Spiel erstellen zu können muss allerdings noch lange nicht heissen, dass es dabei auch tatsächlich gut wird. Wie auch bei jeder anderen Art von Computerspielen muss das Design eines Textadventures gut durchdacht werden und alle seine Elemente müssen fliessend zusammenarbeiten, um dem Spieler ein fesselndes Spieleerlebnis bieten zu können.
Eine gute Ausdrucksfähigkeit kommt dem Spiel natürlich zugute, ist aber meisstens eher nebensächlich. Erfahrungsgemäss sind Spieler viel eher bereit, einen schlechten Umgang mit Prosa zu verzeihen als konzeptionelle Fehler zu ignorieren. Es ist naheliegend: Was die Grafik für ein Grafikadventure ist, ist der Text für ein Textadventure. Auch die schönste Grafik allein macht kein gutes Spiel, wenn die Bedienung zu umständlich ist oder man permanent dazu angehalten wird Dinge zu tun, auf die man beim ersten mal schon keine Lust hatte. Andererseits kann eine gute Grafik - und analog dazu gute Prosa - durchaus dazu gereichen, ein Spiel mit einem stimmigen und gut funktionierenden Konzept aus der Masse der übrigen Spiele herauszuheben. Im folgenden werde ich einige Punkte ansprechen, auf die man meiner Meinung nach insbesondere beim Design von Textadventures achten sollte.
## Zwischensequenzen
Der Einsatz von Zwischensequenzen erscheint naheliegend, vor allem wenn man versucht, eine dichte Story zu übermitteln. Diese Mentalität entspringt vermutlich zum Teil aus Erfahrungen mit aktuellen 3D-Spielen, in denen die Storyelemente häufig in filmartigen Sequenzen ohne Interaktionsmöglichkeiten erzählt werden. Dabei wird aber leider ausser Acht gelassen, dass es sich im Rest dieser Spiele zumeinst um völlig andere, viel straffere Spielkonzepte handelt, als sie im durchschnittlichen Textadventure reproduziert werden könnten.
In einem modernen Egoshooter sind die Zwischensequenzen eine Gelegenheit sich kurz von dem ganzen Gerenne und Geschiesse zuvor zu erholen und gleichzeitig mit einem weiteren Teil der Story belohnt zu werden. Bei einem Textadventure geht dieses Prinzip nicht auf. Dort entfaltet sich die Sequenz nicht fließend wie ein Film sondern brachial wie ein gewaltiger Block Text, der plötzlich auf dem Bildschirm erscheint und den Spieler ersteinmal einschüchtert: «Ohje, DAS muss ich mir jetzt alles durchlesen?».
Es besteht natürlich die Möglichkeit, diesen Effekt bedeutend abzuschwächen, indem man die Sequenz beispielsweise in mehrere kleine Häppchen unterteilt, die der Spieler immer erst durch das Drücken einer Taste bestätigen muss, um den nächsten Abschnitt angezeigt zu bekommen. Das funktioniert natürlich und wird auch häufig in Textadventures eingesetzt.
Ich wage jedoch zu behaupten, dass das stupide drücken einer Taste kein besonders gutes Designelement für ein Spiel darstellt. Das Genre heisst nicht umsonst «Interactive Fiction» und nicht «Push-A-Button Fiction». Derartige Sequenzen sind in einer eher statischen Form der Prosa wie beispielsweise einem Buch gut aufgehoben, haben in einem Textadventure nichts zu suchen.
Eine wesentlich bessere Lösung ist es, den Spieler die Sequenz ausspielen zu lassen. Anstatt einen automatisch ablaufenden Dialog ablaufen zu lassen könnte man den Spieler die Informationen von seinem virtuellen Gegenüber interaktiv erfragen lassen. Anstatt den Spieler in letzter Sekunde einer gefährlichen Situation automatisch entkommen zu lassen könnte man sich die Sequenz rundenweise entfalten lassen, wärhend der Spieler auf jedes neu aufkommende Problem passend reagieren muss, um zu überleben. Dies bedeutet für den Autor natürlich einen wesentlich größeren Programmieraufwand, der es allerdings absolut wert ist. Seine Spieler werden es ihm danken. Vor der möglichen Konsequenz des Spielertodes sollte man als Autor an dieser Stelle auch nicht unbedingt zurückschrecken, was uns zum nächsten Punkt bringt:
## Tod und Auferstehung
In der frühen Zeit der Textadventures war der Spielertod ein Element, das man an jeder Ecke der Spielwelt angetroffen hat. Oft war die Konsequenz des Ablebens anhand der auslösenden Handlung noch nicht einmal nachvollziehbar. Wenn man beispielsweise auf einem Waldweg die linke anstatt der rechten Abzweigung nahm, so war es durchaus möglich, auf der Stelle zu sterben weil dort ein menschenfressendes Monster gelauert hat, welches man vorher nicht bemerkt hat. Eine solche Spielwelt zu erkunden konnte also mitunter recht frustrierend werden.
Dies führte in späteren Text- und Grafikadventures dann dazu, dass es überhaupt nicht mehr möglich war zu sterben. Aufgrund der eingeschränkten Interaktionsmöglichkeiten in Grafikadventures ist dies ein durchaus vertretbares Konzept, greift in Textadventures aber nur bedingt. Es gibt dort einfach keinen wirklich unbestreitbaren Grund, dem Spieler Aktionen zu verweigern, die seie Spielfigur eigentlich auszuführen in der Lage sein müsste. Der Befehl, jetzt bitteschön in den Abrund zu springen, sollte also auch zu eben dieser Aktion und dem Spielertod als logischer Konsequenz führen, anstatt eine sinnfreie Ablehnung wie «Ich will nicht.» anzuzeigen. Es besteht absolut keine Notwendigkeit, den Spieler krampfhaft am Leben zu halten, wenn er sich bewusst in eine offensichtlich lebensbedrohliche Situation begibt.
Auf unvorhersehbare Tode sollte man allerdings tatsächlich verzichten, da diese wirklich nur Frustration schüren und so gut wie nie positiv von Spielern aufgenommen werden. Bei Toden, die zwar bei näherem betrachten logisch, auf den ersten Blick aber nicht unbedingt offensichtlich sind, gibt es mehrere Möglichkeiten: Man sollte dem Spieler entweder eine oder mehrere Runden Zeit geben, sich der Situation wieder zu entziehen oder man sollte vorher zumindest einmal und offensichtlich darauf hinweisen, dass gewisse Handlungen an dieser Stelle der Gesundheit abträglich sein könnten. Bei einer Luftschleuse, die ins Vakuum des Alls führt, könnte man in der Beschreibung etwa erwähnen, dass sich dahinter Luftleerer Raum befindet in dem Atmen schwierig wird. Alternativ könnte man den Spieler die Schleuse öffnen lassen, worauf die Luft aus dem Raum entweicht, aber erst nach 3 Runden vollkommen leer ist und zum Tod führt. Zeit genug, die Schleuse wieder zu schliessen, so man keinen Selbstmord begehen will.
Sollte auf diese Weise doch einmal der Tod eingetreten sein, so ist dies im Normalfall auch kein Beinbruch. Die meissten Textadventure-Systeme stellen standartmässig die Möglichkeit zur Verfügung, die letzte Runde ungeschehen zu machen. Dies rettet den Spieler zwar nicht aus schon längere Zeit ausweglosen Situationen, aber man kann eine einzelne Fehlentscheidung auf diese Weise rückgängig machen und stattdessen etwas anderes probieren. Dies ist eine elegante Interaktionsmöglichkeit, die es dem Spieler auch erlaubt etwas herumzuexperimentieren, ohne gleich fürchten zu müssen das Spiel zu verlieren. Diese Option sollte man als Autor definitiv in seinem Spielekonzept berücksichtigen und damit arbeiten, es sei denn sie behindert das restliche Konzept. In dem Fall sollte man es besser komplett deaktivieren und am Anfang des Spieles oder in den Hilfe-Ausgaben darauf hinweisen, um den Spieler gar nicht erst in falscher Sicherheit zu wiegen.
Im Rahmen der Story und des Spielkonzepts gibt es natürlich noch viele weitere Möglichkeiten, den Spieler sterben oder auferstehen zu lassen. Diese müssen dann aber von den jeweiligen Autore explizit einprogrammiert werden und deren Regeln müssen entsprechend individuell von Spiel zu Spiel betrachtet werden. Ein hervorragendes Beispiel hierfür ist das Spiel «Shrapnel» von Adam Cadre, in dem die Fähigkeit des Spielers zu sterben und wieder aufzuerstehen integral notwendig zum erreichen des Spielziels ist.
## Beschreibungen
Beschreibungen von Gegenständen und Räumen sind ein sehr zentraler Aspekt von Textadventures. Entsprechend wichtig ist es demnach, sie ordentlich darzustellen. Schlechte Beschreibungen gibt es leider wie Sand am Meer, gute muss man aber oft mit der Lupe suchen. Im allgemeinen sollte man auf Formulierungen wie «Du stehst in …» oder «Dies ist der Raum XY» besser verzichten. Beides ist über alle Maßen offensichtlich und trägt nichts zur Aussage des Textes bei. Die allgemeine Beschreibung eines Raumes sollte vielmehr versuchen, die Atmosphäre einzufangen. Eine simple Auflistung aller Einrichtungsgegenstände erfüllt diesen Zweck meisstens nicht sehr gut, weshalb Autoren sich ruhig auch etwas poetisch angehauchtere Formulierungen ausdenken können. Man hat zum Beispiel eine ziemlich gute Vorstellung davon, wie das Büro eines heruntergekommenen Privatdetektivs der 60er Jahre beschrieben werden sollte. Dies muss man nur noch auf andere Räume übertragen und schon hat man ein «Feeling» für die Beschreibung von Räumen.
Gegenstände, mit denen der Spieler interagieren soll oder die auf andere Weise wichtig für die Spielwelt oder die Story sind, sollten gesondert von der Hauptbeschreibung des Raumes erwähnt werden. Die «initial»-Routine von Objekten in Inform 6 wäre dazu beispielsweise der richtige Ort. Bei alltäglichen Gegenständen, deren Aussehen oder Zweck im Raum offensichtlich ist, kann man sich diese Beschreibungen ganz sparen und sie nur zusammengefasst in einem einzigen Satz erwähnen lassen. Die meissten Entwicklungssysteme tun dies automatisch, wenn man keine spezielle Beschreibung hinterlegt hat.
Allgemein sollte man als Autor bei der Raumbeschreibung Zurückhaltung üben. Eine solche Beschreibung überfliegt den Raum schliesslich nur auf den ersten Blick. Details zu jedem Gegenstand kann sich der Spieler auf Wunsch durch näheres Untersuchen beschaffen. Dies trägt sehr zur Spieltiefe bei, da der Spieler das Gefühl bekommt, die Welt wirklich zu entdecken anstatt sie nur vorgesetzt zu bekommen. Ein netter Nebeneffekt ist natürlich auch, dass Raumbeschreibungen auf diese Art kurz und Übersichtlich gehalten werden. Auch hier gilt: In der Kürze liegt die Würze!
## Rätsel
Rätsel sollten sich immer in die Szene einpassen um nicht künstlich zu wirken. Nichts ist schlimmer für einen Spieler als in einer spannenden Atmosphäre plötzlich dazu genötigt zu werden, irgendein zusammenhangloses Logikpuzzle zu lösen, um mit der Geschichte fortfahren zu dürfen. Der Spieler wird zu etwas gezwungen, woran er nicht interessiert ist und was er möglicherweise noch nicht einmal so ohne weiteres bewältigen kann. Diese Form von Rätsel sollte man auf jeden Fall vermeiden.
Darüber hinaus können die Rätsel so einfach oder komplex werden wie man möchte. Soll das Spiel schwierig oder Rätsellastig werden dann baut man lange und komplexe Rätsel ein, andernfalls eben einfache und kurze.
Man sollte nur auf zwei Punkte achten: selbst wenn sie nur sehr einfach sind müssen auf jeden Fall Rätsel eingebaut werden, damit das Spiel nicht langweilig wird. Desweiteren sollte man Konzepte für komplexe Rätsel nicht überstrapazieren. So mach es z.B. keinen Sinn, ein Rätsel einzubauen, bei dem man mit 20 Handgriffen eine Kaffeemaschine reparieren muss. Da sollte man sich schon etwas besseres einfallen lassen.
## Dialoge
Grundlegend gibt es zwei Möglichkeiten, ein Dialogsystem zu implementieren: Fragen- oder Menübasiert. Dazu kommen natürlich noch die verschiedensten Mischformen der zwei. Bei der Fragen-basierten Variante wird jede Dialog-Sequenz durch eine explizite Frage oder Aussage des Spielers der Form «Frag den Lehrer nach dem Buch» oder «Erzähl dem Lehrer von dem Buch» ausgelöst. Dies hat den Vorteil dass es sich gut in den Spielfluss integrieren lässt, aber den Nachteil, dass der Spieler oft nicht weiss, was er nun alles fragen oder sagen kann. Das kann zu frustrierendem Herumprobieren führen.
Bei der zweiten Alternative wird häufig das Dialogsystem über eine Aufforderung der Art «Sprich mit dem Lehrer» aufgerufen und die eigentliche Frage oder Aussage dann aus einem Menü ausgewählt. Vortelhaft ist dabei vor allem, dass der Spieler immer weiß was er sagen kann und keine Möglichkeit übersieht. Nachteilig ist vor allem dass es unter Umständen sehr künstlich anmutet und der Spieler nicht das Gefühl hat, den Dialog wirklich durch eigenen Willen zu lenken.
Letztendlich ist die Wahl wieder eine Frage des eigenen Geschmacks. Ich persönlich bevorzuge die Fragen-Variante, allerdings solle man dann stets deutlich darauf hinweisen, was mögliche Gesprächsthemen sind.
## Quips und Beats
Die Konzepte sind eigentlich offensichtlich, aber jedes Kind braucht eben einen Namen. Bei Quips handelt es sich dabei lediglich um die einzelnen Dialogpassagen, die durch einen Aufruf des Systems (Frage oder Menüpunkt, s.o.) Sie sind das gängige Mittel um längere Dialoge thematisch in kleine Häppchen zu unterteilen, die von dem Spieler leichter überblickt werden können.
Beats hingegen repräsentieren sozusagen den Herzschlag, in dem sich die Welt ein Stück weiter bewegt. Für gewöhnlich löst jede Spieleraktion einen Beat aus um zu simulieren, dass während der Spieler etwas tut der Rest der Welt auch nicht untätig bleibt.
In besonderen Situationen kann es allerdings wünschenswert sein, dass eine Aktion keinen Beat auslöst oder vielleicht sogar mehrere. Beispielsweise wären Situationen denkbar die sehr zeitkritisch sind und in denen look und examine keinen Beat auslösen sollen, schon allein um zu verhindern dass der Spieler nach jeder dieser Aktionen freiwillig ein undo eingibt um keine wertvolle Zeit zu verlieren.
Besonders lange Aktionen könnten über ihren Verlauf hinweg mehrere Beats auslösen oder in einer Konversation können mehrere ausgelöst werden um den Gesprächsteilnehmer die Möglichkeit zu geben, über ein automatisiertes Script-System eigene Kommentare abzugeben. Dies würde dann in begrenztem Rahmen sogar eine Konversation mit mehreren Personen gleichzeitig erlauben.

View File

@@ -0,0 +1,24 @@
---
title: "Spieleempfehlungen"
description: "Spieleempfehlungen aus dem Genre der Interactive Fiction."
menu: "if"
---
# Spieleempfehlungen
Im folgenden werde ich einige Textadventures anderer Autoren vorstellen. Für die Links zu den Spielen selber verlinke ich sofern möglich auf die Homepage des jeweiligen Autors und andernfalls auf das _Interactive Fiction Archive_. Zum Starten der Spiele ist in der Regel ein Interpreter für das entsprechende Story-Format notwendig. Gute Optionen stelle ich im Abschnitt [Tipps für Einsteiger](/if/tipps/) vor.
## [Photopia](http://www.adamcadre.ac/if.html#Photopia) von Adam Cadre
In Photopia begleitet man ein junges Mädchen durch gewisse Schlüsselmomente in ihrem Leben, die immer wieder durchsetzt sind mit fantastischen Geschichten aus ihrer Vorstellung. Die Geschichte ist sehr linear aufgebaut und selbst wenn dem Spieler Entscheidungsmöglichkeiten vorgegaukelt werden kann er keinerlei Einfluss auf den Verlauf der Geschehnisse nehmen. Das Spiel besitzt ein menügesteuertes Dialogsystem und verzichtet beinahe komplett auf jegliche Form von Rätsel. Was in jeder Situation zu tun ist wird entweder spätestens nach kurzem überlegen aus der Situation heraus offensichtlich oder wird nach ein paar Runden automatisch gelöst. Großartig an Photopia ist aber vor allem die Erzählung. Adam Cadre spielt gekonnt mit verschiedensten Stilelementen um Situationen zu schaffen, die den Spieler emotional stark an die Hauptfigur fesseln.
## [Shrapnel](http://www.adamcadre.ac/if.html#Shrapnel) von Adam Cadre
Eine verstörende Geschichte über die Bewohner eines einsamen Hauses auf dem Land und eines neu angekommenen Gastes, in dessen Rolle der Spieler schlüpft. Im Laufe der Geschichte wird schnell klar, dass dort einiges im Argen liegt und sogar die Realität selber sich über alle Maßen seltsam verhält. Shrapnel ist das beste Beispiel für einen unkonventionellen Umgang mit dem Tod der Spielerfigur, das ich kenne. Wie auch Photopia ist es sehr linear und bietet so gut wie keine wirklichen Rätsel. Die Story selber ist sehr verwirrend und häufig vermutet man einen Fehler gemacht zu haben, wenn dies jedoch in Wahrheit nur der vollkommen normale Spielverlauf ist. Hat man sich aber erstmal mit diesen unwägbarkeiten abgefunden bietet Shrapnel ein wirklich interessantes Spielerlebnis.
## [Spider and Web](https://www.eblong.com/zarf/if.html#tangle) von Andrew Plotkin
In dieser Geschichte stellt der Spieler scheinbar einen Touristen in einer fremden Stadt dar, der sich unglücklicherweise verlaufen hat. Oder steckt vielleicht doch mehr dahinter? Andrew Plotkin spielt gekonnt mit der Unwissenheit, die der Spieler am Anfang der Geschichte von seiner Spielfigur hat. Es wird viel mit Flashbacks gearbeitet, so dass der Spieler häufig zwischen zwei verschiedenen Zeitabschnitten hin und her springt und immer ein wenig mehr über seine eigene Spielfigur erfährt. Das Spiel verwendet vorwiegend recht simple Kombinationsrätsel und ein Dialogsystem, das zumeist die anderen Figuren reden lässt und dem Spieler nur “Ja” und “Nein” als Antwortmöglichkeiten erlaubt. Dies funktioniert in dem Setting jedoch erstaunlich gut und kann elegant darüber hinwegtäuschen, dass das Spiel dennoch vollkommen Linear ist.
## [Starrider](http://ifarchive.org/indexes/if-archiveXgamesXzcodeXgerman.html) von Max Kalus
Eines der leider nur sehr wenigen guten deutschen Textadventures. Man spielt einen blinden Passagier an bord eines Raumschiffs, der, als er endlich sein Versteck verlässt, die Crew des Schiffs tot vorfindet. Es handelt sich um ein klassisches Textdaventure mit einer soliden Story, das stark von dem gut durchdachten Universum von Kalus Pen&Paper Rollenspiel _Chalybes_ profitiert. Die Rätsel funktionieren weitesgehend sehr gut, fallen in einzelnen Fällen aber auch mal übermäßig knifflig und weit hergeholt aus. Nichtsdestotrotz ein gutes Spiel, um das man im deutschsprachigen IF-Raum eigentlich gar nicht herum kommt.

24
hugo/content/if/links.md Normal file
View File

@@ -0,0 +1,24 @@
---
title: "Weitere Links"
description: "Interessante Links zum Thema Interactive Fiction."
menu: "if"
---
# Weitere Links
Die folgenden Links stellen eine kleine Auswahl an Seiten dar, die im Zusammenhang mit Interactive Fiction für Spieler und Autoren von Interesse sein könnten.
## [The Interactive Fiction Archive](http://www.ifarchive.org/)
Eine zentrale Anlaufstelle für alles, was mit Interactive Fiction zu tun hat. Von Spielen über Autorensysteme bis hin zu einem weitreichenden Sortiment an themenbezogenen Artikeln kann man hier alles finden, was das Herz begehrt. Die Präsentation ist zwar sehr rudimentär, jedoch eignet es sich hervorragend zum Herumstöbern.
## [Brass Lantern](http://www.brasslantern.org/)
Diese Seite stellt eine große Vielfalt an Artikeln über IF und angelehnte Themen zur Verfügung. Behandelt werden beispielsweise Inhalt und Stil von Computerspielen mit dem Fokus auf Textadventures, aber auch aktuelle Entwicklungen in der Szene sowie Ankündigungen und Resultate von Wettbewerben. Informativ und übersichtlich ist sie immer einen Blick wert, wurde allerdings offenbar auch schon eine Weile nicht mehr aktualisiert.
## [Emily Shorts Interactive Storytelling](http://emshort.wordpress.com/)
Der offizielle Blog von Emily Short, einer der bekanntesten IF Autorinnen und Mitentwicklerin der Programmiersprache _Inform 7_. Sie schreibt dort hauptsächlich über stilistische Aspekte des Designs von Computerspielen im allgemeinen und Textadventures im besonderen. Auch informiert sie regelmässig über Neuerungen in der Entwicklung von Inform 7.
## [textfire.de](http://www.textfire.de/)
Eine gute deutsche Seite, die die technischen Aspekte von IF beleuchtet. Es werden Tipps für Autoren gegeben die vom Inhalt über den Stil bis zur Implementation reichen. Leider wird die Seite nicht mehr aktiv weiter geführt, so dass einige der Informationen bereits veraltet sind.

View File

@@ -0,0 +1,24 @@
---
title: "Autorensysteme"
description: "Eine Liste populärer Autorensysteme für Interactive Fiction."
menu: "if"
---
# Autorensysteme
Es gibt heutzutage die verschiedensten Hilfsmittel um Textadventures zu erstellen. Sie reichen von einfachen Compilern bis hin zu aufwändigen integrierten Entwicklungsumgebungen. Nachfolgend möchte ich eine kleine Auswahl davon vorstellen. Von den reinen Fähigkeiten her haben die meisten dieser Autorensysteme selten klare Vorteile gegenüber anderen, was die Wahl eines Systems im Allgemeinen zu einer Frage der persönlichen Vorlieben macht.
## [Inform 7](http://www.inform7.com/)
Inform 7 besitzt eine integrierte Entwicklungsumgebung (IDE), die die Erstellung des Programmcodes stark vereinfacht. Der Code selber besitzt eine Sprachsyntax, die an die englische Umgangssprache angelehnt und so unter Umständen etwas eingänglicher für Nicht-Programmierer ist. Diese Syntax ist auf dem älteren Inform 6 Code aufgesetzt und produziert als Ausgabe wahlweise die verschiedenen Z-code-Formate oder das neuere Glulx-Format. Inform 7 ist ein sehr gutes Autorensystem und vereint in sich alles was man benötigt um ein Textadventure zu programmieren. Das System lässt sich sehr einfach mit Plugins für spezielle Anwendungen erweitern. Für Programmierer ist die ungewöhnliche umgangssprachliche Syntax stark gewöhnungsbedürftig, aber nach einer gewissen Eingewöhnungszeit kann sie dennoch gut funktionieren.
## [Inform 6](http://www.inform-fiction.org/)
Im Gegensatz zu Inform 7 ist Inform 6 keine komplette Entwicklungsumgebung sondern nur ein Compiler. Der Programmcode muss in einem Texteditor erstellt werden und kann dann auf Kommandozeilen-Ebene in Z-code übersetzt werden. Der Code wird in einer klassischen formalisierten Programmiersprache erstellt, die mit der umgangssprachlichen Syntax von Intorm 7 nichts gemeinsam hat. Versierte Programmierer werden sich hier daher wesentlich mehr zu Hause fühlen als bei der Nachfolgerversion. Auch für Inform 6 gibt es eine Reihe von Erweiterungen, die jedoch weit weniger komfortabel eingebunden werden können. Hat man das Ganze allerdings erstmal nach seinen Wünschen eingerichtet funktioniert es sehr gut als Autorensystem.
## [TADS](http://www.tads.org/)
Das Text Adventure Developement System (kurz TADS) bietet eine direkte Alternative zu Inform 6 und ungefähr den gleichen Funktionsumfang. Die Syntax ist ebenso formalisiert, sieht jedoch komplett anders aus. Ebenso gibt es Erweiterungen die es um bestimmte Funktionen erweitern können. Als Ausgabe produziert es keinen Z-code sondern eben das TADS-eigene Story-Format, für das man einen anderen Interpreter benötigt. Viele gute Textadventures wurden mit TADS geschrieben und es schwören ebenso viele Autoren auf darauf wie auf Inform 6.
## [Adrift](http://www.adrift.co/)
Bei Adrift handelt es sich wieder um eine komplette Entwicklungsumgebung, die darauf ausgerichtet ist möglichst einfach bedient werden zu können. Der Programmcode wird beinahe komplett von den verschieden Editoren verborgen. Neue Räume und Aktionen können einfach mit einer Eingabemaske erstellt werden. Auf diese Weise geht es sehr schnell und einfach, ein simples Textadventure zu erstellen. Darin liegt aber auch gleichzeitig das Problem: alles was von Standardfunktionen abweicht ist schwierig umzusetzen und neue Kommandos können nur sehr unzureichend definiert werden. Das Erstellen von Spielen ist somit zwar vereifacht worden, die Ergebnisse lassen von technischer Seite jedoch häufig zu wünschen übrig. Nichtsdestotrotz eignet es sich gut für Anfänger im Erstellen von Textadventures. Die Adrift Entwicklungsumgebung ist mittlerweile ebenfalls kostenlos. Um Spenden wird gebeten.

247
hugo/content/if/tipps.md Normal file
View File

@@ -0,0 +1,247 @@
---
title: "Tipps für Einsteiger"
description: "Tipps für Einsteiger ins Genre der Interactive Fiction."
menu: "if"
---
# Tipps für Einsteiger
Zum Spielen von Interactive Fiction sind einige Grundlagen erforderlich. Diese will ich hier ein wenig beleuchten.
## Spiele downloaden
Wenn man sich entschlossen hat, sich mit Interactive Fiction auseinander zu setzen, benötigt man sinnvollerweise zuallererst ein Spiel. Das [Interactive Fiction Archive](http://www.ifarchive.org/indexes/if-archiveXgames.html) stellt eine große Auswahl von Spielen zum freien Download zur Verfügung, allerdings ist es dort etwas unübersichtlich und genauere Informationen zu den Spielen erhält man dort auch nicht. Als Entscheidungshilfe kann an dieser Stelle [Bafs Guide to the IF Archive](http://www.wurb.com/if/) dienen. Dort kann man die Spiele nicht nur komfortabel nach verschiedenen Kriterien durchsuchen sondern sich auch kurze Reviews zu jedem Spiel durchlesen. Der Guide verlinkt direkt auf das IF-Archive für die Downloads der Spiele und zeigt auf, in welchem Format sie vorliegen.
## Starten der Spiele
Hat man nun das Spiel der Wahl heruntergeladen dann steht man in den meissten Fällen vor dem Problem, dass es nicht in einer direkt ausführbaren Form (z.B. .exe-Datei) sondern als Story-Datei vorliegt. Für solche Spiele benötigt man einen passenden Interpreter, ein Programm welches in der Lage ist, das Spiel abzuspielen. Welchen Interpreter man für ein bestimmtes Spiel benötigt kann man entweder den beigefügten Informationen (z.B. README-Datei oder Bafs Guide) entnehmen oder man muss sie recherchieren. Als Anhaltspunkt kann folgende Liste der gängisgsten Dateinamens-Suffixen von Story-Dateien dienen:
<table>
<thead>
<tr>
<th>Suffix</th>
<th>Story-Format</th>
</tr>
</thead>
<tbody>
<tr>
<td>.a3c, .acd</td>
<td>Alan</td>
</tr>
<tr>
<td>.agx</td>
<td>AGT</td>
</tr>
<tr>
<td>.cbm</td>
<td>Quill</td>
</tr>
<tr>
<td>.d$$, .da1, .da2, .da3, .da4, .da5, .ttl</td>
<td>AGT</td>
</tr>
<tr>
<td>.gam, .t3</td>
<td>TADS</td>
</tr>
<tr>
<td>.hex</td>
<td>Hugo</td>
</tr>
<tr>
<td>.mag, .gfx</td>
<td>Magnetic Scrolls</td>
</tr>
<tr>
<td>.taf</td>
<td>Adrift</td>
</tr>
<tr>
<td>.tag</td>
<td>T.A.G.</td>
</tr>
<tr>
<td>.ulx, .gblorb, .glb</td>
<td>Glulx</td>
</tr>
<tr>
<td>.z3, .z4, .z5, .z6, .z8, .zblorb, .zlb</td>
<td>Z-code</td>
</tr>
</tbody>
</table>
Weiss man aber erstmal in welchem Format das Spiel vorliegt dann kann man sich einfach einen passenden Interpreter aussuchen. Hier zwei Multitalente, die beide ein großes Spektrum der gängigen Story-Formate abdecken:
**[Gargoyle](http://ccxvii.net/gargoyle/) (Windows, Linux)**\
Unterstützte Story-Formate:\
_Adrift, AGT, Alan, Glulx, Hugo, Level 9, Magnetic Scrolls, TADS, Z-code_
**[Spatterlight](http://ccxvii.net/spatterlight/) (Mac OS X)**\
Unsterstützte Story-Formate:\
_Adrift, AdvSys, AGT, Alan, Glulx, Hugo, Level 9, Magnetic Scrolls, Quill, TADS, Z-code_
Natürlich gibt es noch wesentlich mehr Interpreter mit den verschiedensten Eigenschaften und und darüber hinaus noch viel mehr Autorensysteme mit den unterschiedlichsten Datei-Formaten.
In der Regel ist es allerdings eine gute Idee, sich einen der multifunktionalen Interpreter wie diese beiden zu besorgen. Wenn man kein allzu exotisches Spiel spielen will können sie es mit recht hoher Wahrschenlichkeit ausführen.
Der größte Nachteil dieser Interpreter ist allerdings, dass sie Spezialfähigkeitden der einzelnen Systeme wie z.B. Grafiken häufig nicht unterstützen. In solchen Fällen benötigt man dann doch einen spezialisierten Interpreter und sollte sich einfach mal im IF-Archive umsehen.
## Erste Schritte
Die erste Herausforderung eines neuen Spielers in einem Textadventure ist die Bewegung. Standardmäßig wird dies der Einfachheit halber über die Himmelsrichtungen sowie ein Paar weitere Spezialfälle geregelt. Da die Bewegung ein integraler Bestandteil so gut wie jedes Textadventures ist, gibt es für alle Bewegungsrichtungen auch Abkürzungen. Richtungen und Abkürzungen können der folgenden Tabelle entnommen werden:
<table>
<thead>
<tr>
<th>Richtung</th>
<th>Kommando</th>
</tr>
</thead>
<tbody>
<tr>
<td>Norden</td>
<td>north/n</td>
</tr>
<tr>
<td>Süden</td>
<td>south/s</td>
</tr>
<tr>
<td>Westen</td>
<td>west/w</td>
</tr>
<tr>
<td>Osten</td>
<td>east/e</td>
</tr>
<tr>
<td>Nordwesten</td>
<td>northwest/nw</td>
</tr>
<tr>
<td>Nordosten</td>
<td>northeast/ne</td>
</tr>
<tr>
<td>Südwesten</td>
<td>southwest/sw</td>
</tr>
<tr>
<td>Südosten</td>
<td>southeast/se</td>
</tr>
<tr>
<td>Aufwärts</td>
<td>up/u</td>
</tr>
<tr>
<td>Abwärts</td>
<td>down/d</td>
</tr>
<tr>
<td>Hinein</td>
<td>in</td>
</tr>
<tr>
<td>Heraus</td>
<td>out</td>
</tr>
</tbody>
</table>
Die Orienterung anhand von Himmelsrichtungen mag auf den ersten Blick etwas seltsam anmuten, denn wer läuft schon permanent mit einem Kompass durch die Gegend? Bei näherer Betrachtung macht es aber durchaus Sinn, wenn man bedenkt, dass es sich dabei nur um ein Hilfsmittel handelt um eine allgemeingültige Form der Orientierung zu implementieren. Es ist also nicht wichtig dass man bei «north» auch wirklich nach Norden geht, sondern dass man in die gleiche Richtung geht, egal ob man den Raum von Links oder Rechts betreten hat. Die Kommandos “links” und “rechts” wären dafür nicht geeignet, da es sich dabei um eine Richtung handelt, die abängig von der derzeitigen Blickrichtung des Spielers ist.
Natürlich kann man in Textadventures nicht nur durch die Gegend laufen sondern kann auch mit ihr interagieren, z.B. Dinge aufheben oder Knöpfe drücken. Dazu gibt es eine recht lange Liste von Kommandos die bei Kenntnis der englischen Sprache eigentlich weitesgehend selbsterklärend sind. Hier ein kleiner Auszug der gängisgsten:
<table>
<thead>
<tr>
<th>Kommando</th>
<th>Tätigkeit</th>
</tr>
</thead>
<tbody>
<tr>
<td>inventory/i</td>
<td>Gegenstände im Inventar auflisten</td>
</tr>
<tr>
<td>look/l</td>
<td>Im Raum umschauen / Raumbeschreibung anzeigen lassen</td>
</tr>
<tr>
<td>examine/x <em>Gegenstand</em></td>
<td>Einen Gegenstand genauer untersuchen</td>
</tr>
<tr>
<td>wait/z</td>
<td>Eine Runde warten / Keine Aktion ausführen</td>
</tr>
<tr>
<td>take/get <em>Gegenstand</em></td>
<td>Einen Gegenstand in Reichweite mitnehmen</td>
</tr>
<tr>
<td>put <em>Gegenstand</em> in/on <em>Ort</em></td>
<td>Einen Gegenstand aus dem Inventar an einen anderen Ort legen</td>
</tr>
<tr>
<td>drop <em>Gegenstand</em></td>
<td>Einen Gegenstand aus dem Inventar fallen lassen</td>
</tr>
<tr>
<td>push <em>Gegenstand</em></td>
<td>Einen Gegenstand drücken oder schieben</td>
</tr>
<tr>
<td>pull <em>Gegenstand</em></td>
<td>An einem Gegenstand ziehen</td>
</tr>
<tr>
<td>open <em>Gegenstand</em></td>
<td>Einen Gegenstand öffnen, falls möglich</td>
</tr>
<tr>
<td>close <em>Gegenstand</em></td>
<td>Einen Gegenstand schließen, falls möglich</td>
</tr>
<tr>
<td>hit/attack <em>Gegenstand</em></td>
<td>Einen Gegenstand oder eine Person angreifen</td>
</tr>
<tr>
<td>talk to <em>Person</em></td>
<td>Eine Person ansprechen</td>
</tr>
<tr>
<td>ask <em>Person</em> about <em>Thema</em></td>
<td>Eine Person nach einem bestimmten Thema fragen</td>
</tr>
<tr>
<td>tell <em>Person</em> of/about <em>Thema</em></td>
<td>Einer Person etwas über ein bestimmtes Thema erzählen</td>
</tr>
</tbody>
</table>
Dies sind natürlich bei weitem nicht alle Befehle eines Standard-Textadventures, vor allem da jeder Autor sein Spiel beliebig um neue Kommandos erweitern kann, aber sie geben vielleicht einen ungefähren Eindruck davon, was alles möglich ist. Bei den ersten vier Einträgen der Tabelle handelt es sich um sehr häufig gebrauchte Kommados, weshalb die wie auch schon die Bewegungsrichtungen über Abkürzungen verfügen.
## Die Welt verstehen
Kann man sich erstmal bewegen hat man die schwierigste Hürde eigentlich schon überwunden. Vermutlich hat man die grundlegenden Konzepte der Anzeige auch bereits begriffen, aber hier nocheinmal in aller Kürze:
Die Spielwelt ist in Bereiche aufgeteilt, die «Räume» genannt werden. Dabei muss es sich nicht zwingend um tatsächliche Räume in einem Gebäude handeln, sondern es sind auch andere Orte wie z.B. ein Seeufer als virtueller «Raum» denkbar.
Wenn man einen Raum betritt wird die Beschreibung des Raumes ausgegeben. Diese beinhaltet das allgemeine Aussehen des Raumes, seine prominenten Einrichtungsgegenstände (Tische, Schränke, usw.) sowie die Türen oder anderweitige Passagen, die aus ihm hinaus in andere Räume führen. Letzteres wird manchmal auch vergessen, jedoch handelt es sich dann in den meissten Fällen um einen Designfehler des Autors. Man kann sich die Beschreibung des Raumes, in dem man sich befindet, mittels des look-Kommandos jederzeit erneut anzeigen lassen.
Mit den meisten Dingen, die in der Raumbeschreibung erwähnt werden, kann man daraufhin interagieren. Das wichtigste Hilfsmittel ist dabei das examine-Kommando. Mit seiner Hilfe kann man sich genauere Informationen zu den Gegenständen beschaffen und unter Umständen auf diese Weise sogar noch mehr Gegenstände entdecken. Ein wichtiges Buch wird so vielleicht nur erwähnt (und so für den Spieler existent) nachdem er ein Bücherregal genauer untersucht hat.
Nachdem man sich durch Umsehen und Untersuchen ein gutes Bild von seiner Umgebung gemacht hat, kann man anfangen sie zu verändern, Dinge mitzunehmen, Schränke zu öffnen und Knöpfe zu drücken. Hier ist der Einfallsreichtum des Spielers gefragt, um die Rätsel zu lösen, die der Autor für ihn ausgestreut hat.
Von allzu todesmutigen Taten sollte man vielleicht besser absehen, denn im Gegensatz zu den meissten neueren Point & Klick Adventures kann man in vielen Textadventures durchaus sterben. Normalerweise ist das kein großes Problem und kann sofort korrigiert werden, indem man den letzten Zug ungeschehen macht. Dennoch sollte man vorsichtig sein.
## Fazit
m allgemeinen lassen sich alle Tipps eines Grafikadventures auch auf ein Textadventure übertragen: Untersuche alles, sprich mit jedem, nimm alles mit was du finden kannst und kombiniere es wenn angebracht mit allen neuen Gegenständen.
Das sollte an Tipps ausreichen um euch auf den Weg zu bringen. Nun gehet hin und spielt ein paar Textadventures!

16
hugo/content/impressum.md Normal file
View File

@@ -0,0 +1,16 @@
---
title: "Impressum"
description: "Impressum, Datenschutz und Kontakt"
---
# Impressum
Stefan Mühlinghaus\
Waldenburger Weg 43\
58511 Lüdenscheid\
<a href="&#x6d;&#x61;il&#x74;&#x6f;&#58;j&#x61;zz&#x6d;&#97;&#110;&#x40;&#97;lp&#x68;&#x61;b&#114;&#101;&#101;&#100;.c&#x6f;m">j&#x61;zz&#x6d;&#97;&#110;&#x40;&#97;lp&#x68;&#x61;b&#114;&#101;&#101;&#100;.c&#x6f;m</a>
## Datenschutz
Auf dieser Website werden keine personenbezogenen Daten gesammelt oder verarbeitet. Zur Sicherstellung der Funktionsweise werden Zugriffe protokolliert, aber die zugehörigen IP-Adressen werden anonymisiert gespeichert. Eine eventuelle Kontaktaufnahme per E-Mail wird nicht über die Website abgewickelt.
**Wer mir eine E-Mail schreibt und nicht möchte, dass ich seine personenbezogenen Daten in meinem E-Mail Programm speichere, soll dies bitte in der E-Mail kenntlich machen. Eine nachträgliche Löschung ist selbstverständlich auch möglich.**

45
hugo/content/login.html Normal file
View File

@@ -0,0 +1,45 @@
---
title: "Login"
type: "login"
---
<article x-data="{
email: '',
password: '',
error: '',
pending: false,
async submitLogin(event) {
event.preventDefault();
if (!this.email || !this.password) {
this.error = 'Bitte E-Mail und Passwort angeben.';
return;
}
const data = new FormData();
data.append('identity', this.email);
data.append('password', this.password);
this.pending = true;
const response = await fetch(
'/api/collections/users/auth-with-password',
{ method: 'POST', body: data }
);
const result = await response.json();
this.pending = false;
if (!result.token) {
this.error = 'Login fehlgeschlagen';
return;
}
this.error = '';
localStorage.setItem('alphabreedtoken', result.token);
localStorage.setItem('alphabreedid', result.record.id);
const params = new URLSearchParams(window.location.search);
window.location = params.has('back') ? params.get('back') : '/';
}
}">
<p style="color:red; text-align:center;" x-show="error" x-text="error"></p>
<form @submit="submitLogin">
<label for="email">E-Mail</label>
<input type="email" id="email" autocomplete="email" x-model="email">
<label for="password">Passwort</label>
<input type="password" id="password" x-model="password">
<button type="submit" :aria-busy="pending ? 'true' : 'false'">Login</button>
</form>
</article>

42
hugo/content/notes.html Normal file
View File

@@ -0,0 +1,42 @@
---
title: "Notes"
type: "notes"
---
<main x-data="{
store: null,
notes: '',
needCreate: false,
loaded: false,
async getNotes() {
const notes = await this.store.getNotes();
this.loaded = true;
if (notes === false) {
this.needCreate = true;
return;
}
this.notes = notes;
},
async saveNotes() {
if (!this.loaded)
return;
if (this.needCreate) {
const result = await this.store.createNotes(this.notes);
if (result)
this.needCreate = false;
}
else {
await this.store.updateNotes(this.notes);
}
},
async init() {
this.store = Alpine.store('alphabreed');
this.store.backUrl = '/notes/';
const tokenOK = await this.store.checkToken();
if (!tokenOK)
return;
await this.getNotes();
}
}" @keydown.window.prevent.stop.ctrl.s="saveNotes">
<textarea autocomplete="off" spellcheck="false" autofocus placeholder="Write notes here..." x-model="notes" @input="$store.alphabreed.pending = true" @input.debounce.500ms="saveNotes"></textarea>
<div id="pending" :class="{'show': $store.alphabreed.pending}"></div>
</main>

23
hugo/content/pi/index.md Normal file
View File

@@ -0,0 +1,23 @@
---
title: "Raspberry Pi"
description: "Mein lokaler Download- und Webserver"
---
# Raspberry Pi Unboxing
Der Raspberry Pi aka. “Mein Neuer Lokaler Download- und Webserver” ist da!
![Schachtel](schachtel.webp)
Die Schachtel ist sooo winzig. Etwa die Größe einer großen Zigarettenschachtel. Ist auch nicht von der Raspberry Pi Foundation selber sondern “Supplied under licence by RS Components Ltd and Allied Electronics Inc.”. Weisse bescheid.
![Offene Schachtel](offene-schachtel.webp)
In der Schachtel gibs auch gar kein großes Trara. Da liegt einfach nur die Platine drin und dann noch ein Zettel mit dem üblichen “Nicht in die Badewanne werfen oder aufessen” Blabla in 10 Sprachen.
![Platine](platine.webp)
Der Pi selber ist ein niedliches kleines Ding. Passt mit etwas Spielraum in die Zigarettenschachtel und riecht wie frisch zusammengelötet. Kann man sogar auf Entfernung riechen.
![Zubehör](zubehoer.webp)
Ein bisschen Zubehör, damit man das ganze auch benutzen kann. Offizielles Raspberry Pi Stromkabel mit 2,5 Ampere und eine 32 GB MicroSD Karte von SanDisk. Als Betriebssystem dient zur Zeit Raspbian Minmal. Nicht zu sehen (weil vergessen) ist der SD Card Reader von Transcend zum beschreiben der Karte. Alles in allem ein Kostenpunkt von ca. 60€. Nicht schlecht.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -0,0 +1,25 @@
<script>
(function() {
const comicRegex = /\/comics\/([a-zA-Z0-9]+)\//;
let referrerMatches = document.referrer.match(comicRegex);
let locationMatches = document.location.href.match(comicRegex);
if (!referrerMatches || !referrerMatches[1]
|| !locationMatches || !locationMatches[1]
|| referrerMatches[1] != locationMatches[1]
)
return;
const numberRegex = /\/comics\/[A-Za-z0-9]+\/(\d+)\//;
referrerMatches = document.referrer.match(numberRegex);
locationMatches = document.location.href.match(numberRegex);
let referrerNum = 0;
if (referrerMatches && referrerMatches[1])
referrerNum = parseInt(referrerMatches[1], 10);
let locationNum = 0;
if (locationMatches && locationMatches[1])
locationNum = parseInt(locationMatches[1], 10);
if (locationNum > referrerNum)
document.documentElement.classList.add('forward');
else
document.documentElement.classList.add('back');
})();
</script>

43
hugo/layouts/baseof.html Normal file
View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html class="no-js" lang="de">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>{{ block "title" . }}{{ .Site.Title }}{{ end }}</title>
{{ with .Params.description -}}
<meta name="description" content="{{ . }}">
{{- end }}
<meta name="author" content="Stefan Mühlinghaus">
<meta name="viewport" content="initial-scale=1, user-scalable=no">
<link rel="icon" type="image/vnd.microsoft.icon" href="/img/favicon.ico">
{{ block "htmlheadbefore" . }}{{ end }}
<link rel="preload" href="/css/main.css" as="style">
<link rel="stylesheet" href="/css/main.css">
<script>(function(H){H.className=H.className.replace(/\bno-js\b/,'js')})(document.documentElement)</script>
<script defer src="/js/menu.js"></script>
{{ block "htmlheadafter" . }}{{ end }}
</head>
<body>
<!--[if lt IE 8]><p class="browsehappy">Sie benutzen einen <strong>veralteten</strong> Browser. Bitte <a href="http://browsehappy.com/">aktualisieren Sie Ihren Browser</a> um Ihre Erfahrung auf alphabreed zu verbessern. <![endif]-->
<header>
<a href="/" title="alphabreed Startseite"> <img src="/img/alphabreed.png" alt="alphabreed"></a>
<nav>
<ul>
{{- $page := . }}
{{- range site.Menus.main }}
{{- if $page.IsMenuCurrent .Menu . }}
<li class="active"><a href="{{.URL}}">{{.Name}}</a></li>
{{- else if $page.HasMenuCurrent .Menu . }}
<li class="active"><a href="{{.URL}}">{{.Name}}</a></li>
{{- else }}
<li><a href="{{.URL}}">{{.Name}}</a></li>
{{- end }}
{{- end }}
</ul>
</nav>
</header>
<div id="main">
{{ block "main" . }}{{ .Content }}{{ end }}
</div>
</body>
</html>

View File

@@ -0,0 +1,3 @@
{{ define "title" }}
{{- .Title }} :: {{ .Site.Title -}}
{{ end }}

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html class="no-js" lang="de">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>{{ block "title" . }}{{ .Site.Title }}{{ end }}</title>
<meta name="author" content="Stefan Mühlinghaus">
<meta name="viewport" content="initial-scale=1, user-scalable=no">
<link rel="icon" type="image/vnd.microsoft.icon" href="/img/favicon-bookmarks.ico">
<link rel="preload" href="/css/bookmarks.css" as="style">
<link rel="stylesheet" href="/css/bookmarks.css">
<script>(function(H){H.className=H.className.replace(/\bno-js\b/,'js')})(document.documentElement)</script>
<script defer src="/js/alpine-alphabreed.js"></script>
<script defer src="/js/alpine-3.15.3.min.js"></script>
</head>
<body>
{{ block "main" . }}{{ .Content }}{{ end }}
</body>
</html>

View File

@@ -0,0 +1,9 @@
{{ define "main" }}
{{ .Content }}
{{ range .Pages }}
<div class="box">
<h2><a href="{{ .RelPermalink }}">{{ .Title }}</a></h2>
<p>{{ .Description }}</p>
</div>
{{ end }}
{{ end }}

View File

@@ -0,0 +1,44 @@
{{ define "title" }}{{ .Title }}, Seite 1 :: {{ .Site.Title }}{{ end }}
{{ define "htmlheadbefore" }}
{{ $images := .Resources.ByType "image" }}
{{ $imageCount := len $images }}
{{ $firstImage := index $images 0 }}
<link rel="preload" href="{{ $firstImage.RelPermalink }}" as="image">
{{ if gt $imageCount 1 }}
<link rel="next" href="2/">
{{ end }}
{{ partial "comicpageslide.html" . }}
{{ end }}
{{ define "main" }}
{{ $images := .Resources.ByType "image" }}
{{ $imageCount := len $images }}
{{ $firstImage := index $images 0 }}
<h1>{{ .Title }}, Seite 1</h1>
<nav class="comicnav">
<span></span>
<span></span>
{{ if gt $imageCount 1 }}
<a class="next" href="2/">Weiter</a>
<a class="last" href="{{ $imageCount }}/">Ende</a>
{{ else }}
<span></span>
<span></span>
{{ end }}
</nav>
<div id="comicpage">
<img src="{{ $firstImage.RelPermalink }}">
</div>
<nav class="comicnav">
<span></span>
<span></span>
{{ if gt $imageCount 1 }}
<a class="next" href="2/">Weiter</a>
<a class="last" href="{{ $imageCount }}/">Ende</a>
{{ else }}
<span></span>
<span></span>
{{ end }}
</nav>
{{ end }}

View File

@@ -0,0 +1,36 @@
{{ define "_partials/comicnav.html" }}
<nav class="comicnav">
<a class="first" href="../">Anfang</a>
<a class="prev" href="{{ .Params.prev }}">Zurück</a>
{{ if .Params.next }}
<a class="next" href="{{ .Params.next }}">Weiter</a>
{{ else }}
<span></span>
{{ end }}
{{ if .Params.last }}
<a class="last" href="{{ .Params.last }}">Ende</a>
{{ else }}
<span></span>
{{ end }}
</nav>
{{ end }}
{{ define "title" }}{{ .Title }} :: {{ .Site.Title }}{{ end }}
{{ define "htmlheadbefore" }}
<link rel="preload" href="../{{ .Params.image }}" as="image">
<link rel="prev" href="{{ .Params.prev }}">
{{ if .Params.next }}
<link rel="next" href="{{ .Params.next }}">
{{ end }}
{{ partial "comicpageslide.html" . }}
{{ end }}
{{ define "main" }}
<h1>{{ .Title }}</h1>
{{ partial "comicnav.html" . }}
<div id="comicpage">
<img src="../{{ .Params.image }}">
</div>
{{ partial "comicnav.html" . }}
{{ end }}

View File

@@ -0,0 +1,11 @@
{{ define "main" }}
<nav class="submenu box">
<h2>Bereiche:</h2>
<ul>
{{ range .Pages }}
<li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
{{ end }}
</ul>
</nav>
{{ .Content }}
{{ end }}

View File

@@ -0,0 +1,47 @@
{{ define "title" }}{{ .Params.fullname }}, Folge 1 :: {{ .Site.Title }}{{ end }}
{{ define "main" }}
{{ $episodes := .Params.episodes }}
{{ $episodeCount := len $episodes }}
{{ $firstEpisode := index $episodes 0 }}
{{ $gameplayPage := . }}
<nav class="submenu box">
<h2>Bereiche:</h2>
<ul>
{{ range .Parent.Pages }}
{{ if .Eq $gameplayPage }}
<li class="active">
{{ else }}
<li>
{{ end }}
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
</li>
{{ end }}
</ul>
</nav>
<h1>{{ .Params.fullname }}</h1>
{{ if $firstEpisode.title }}
<h2>{{ $firstEpisode.title }}</h2>
{{ end }}
{{ if $firstEpisode.description }}
<p>{{ $firstEpisode.description }}</p>
{{ end }}
<p><video src="{{ $firstEpisode.video }}" type="video/mp4" playsinline controls style="width:100%; aspect-ratio:16 / 9; display:block;"></video></p>
{{ if gt $episodeCount 1 }}
<div class="box videos">
{{ range $index, $episode := $episodes }}
{{ $episodeNum := math.Add $index 1 }}
{{ if eq $index 0 }}
<div class="active">
<a href="./">Folge 01 {{ $episode.title }}</a>
</div>
{{ else }}
<div>
<a href="{{ $episodeNum }}/">Folge {{ printf "%02d" $episodeNum }} {{ $episode.title }}</a>
</div>
{{ end }}
{{ end }}
</div>
{{ end }}
{{ .Content }}
{{ end }}

View File

@@ -0,0 +1,54 @@
{{ define "title" }}{{ .Title }} :: {{ .Site.Title }}{{ end }}
{{ define "main" }}
{{ $episodes := .Params.episodes }}
{{ $episodeCount := len $episodes }}
{{ $episodeIndex := .Params.index }}
{{ $gameplayPage := .Parent }}
<nav class="submenu box">
<h2>Bereiche:</h2>
<ul>
{{ range .Parent.Parent.Pages }}
{{ if .Eq $gameplayPage }}
<li class="active">
{{ else }}
<li>
{{ end }}
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
</li>
{{ end }}
</ul>
</nav>
<h1>{{ .Params.gameplay }}</h1>
{{ if .Params.name }}
<h2>{{ .Params.name }}</h2>
{{ end }}
<p>{{ .Description }}</p>
<p><video src="{{ .Params.video }}" type="video/mp4" playsinline controls style="width:100%; aspect-ratio:16 / 9; display:block;"></video></p>
{{ if gt $episodeCount 1 }}
<div class="box videos">
{{ range $index, $episode := $episodes }}
{{ $episodeNum := math.Add $index 1 }}
{{ $isCurrent := eq $index $episodeIndex }}
{{ if eq $index 0 }}
{{ if $isCurrent }}
<div class="active">
{{ else }}
<div>
{{ end }}
<a href="../">Folge 01 {{ $episode.title }}</a>
</div>
{{ else }}
{{ if $isCurrent }}
<div class="active">
{{ else }}
<div>
{{ end }}
<a href="../{{ $episodeNum }}/">Folge {{ printf "%02d" $episodeNum }} {{ $episode.title }}</a>
</div>
{{ end }}
{{ end }}
</div>
{{ end }}
{{ .Content }}
{{ end }}

3
hugo/layouts/home.html Normal file
View File

@@ -0,0 +1,3 @@
{{ define "main" }}
{{ .Content }}
{{ end }}

19
hugo/layouts/if/all.html Normal file
View File

@@ -0,0 +1,19 @@
{{ define "title" }}
{{- .Title }} :: {{ .Site.Title -}}
{{ end }}
{{ define "main" }}
<nav class="submenu box">
<h2>Bereiche:</h2>
<ul>
{{- $page := . }}
{{- range site.Menus.if }}
{{- if $page.IsMenuCurrent .Menu . }}
<li class="active"><a href="{{.URL}}">{{.Name}}</a></li>
{{- else }}
<li><a href="{{.URL}}">{{.Name}}</a></li>
{{- end }}
{{- end }}
</ul>
</nav>
{{ .Content }}
{{ end }}

View File

@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html class="no-js" lang="de">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>{{ block "title" . }}{{ .Site.Title }}{{ end }}</title>
{{ with .Params.description -}}
<meta name="description" content="{{ . }}">
{{- end }}
<meta name="author" content="Stefan Mühlinghaus">
<meta name="viewport" content="initial-scale=1, user-scalable=no">
<link rel="icon" type="image/vnd.microsoft.icon" href="/img/favicon.ico">
<link rel="preload" href="/css/pico-2.1.1.min.css" as="style">
<link rel="stylesheet" href="/css/pico-2.1.1.min.css">
<script>(function(H){H.className=H.className.replace(/\bno-js\b/,'js')})(document.documentElement)</script>
<script defer src="/js/alpine-3.15.3.min.js"></script>
</head>
<body>
<main class="container">
{{ .Content }}
</main>
</body>
</html>

View File

@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>{{ block "title" . }}{{ .Site.Title }}{{ end }}</title>
<meta name="author" content="Stefan Mühlinghaus">
<meta name="viewport" content="initial-scale=1, user-scalable=no">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="theme-color" content="#222">
<meta name="robots" content="noindex, nofollow">
<link rel="icon" type="image/vnd.microsoft.icon" href="/img/favicon-notes.ico">
<script defer src="/js/alpine-alphabreed.js"></script>
<script defer src="/js/alpine-3.15.3.min.js"></script>
<style>
:root {
--color-hl: #a19bff;
--color-bg: #ccc;
--color-fg: #333;
--color-cs: black;
}
@media (prefers-color-scheme: dark) {
:root {
--color-fg: #ccc;
--color-bg: #333;
--color-cs: white;
}
}
::selection {
background-color:var(--color-hl); color:#333;
}
html, body, form, textarea { padding:0; margin:0;
height:100%; }
html { background-color:var(--color-bg); color:var(--color-fg);
caret-color:var(--color-cs); font-family:monospace;
font-size:15px; }
main { height:100%; }
textarea { border:none; padding:20px; width:100%;
font-family:inherit; box-sizing:border-box; resize:none;
background-color:inherit; color:inherit;
font-size:inherit; outline:none; }
#pending { position:fixed; top:10px; right:10px; width:20px;
height:20px; border:3px solid transparent; border-radius:20px;
border-color:var(--color-hl) var(--color-hl) transparent transparent;
opacity:0; transition:opacity 0.5s; transition-delay:0.5s;
animation:0.7s linear infinite rotate; }
#pending.show { opacity:1; }
@keyframes rotate {
from { transform:rotate(0deg); }
to { transform:rotate(359deg); }
}
</style>
</head>
<body>
{{ .Content }}
</body>
</html>

6
hugo/layouts/single.html Normal file
View File

@@ -0,0 +1,6 @@
{{ define "title" }}
{{- .Title }} :: {{ .Site.Title -}}
{{ end }}
{{ define "main" }}
{{ .Content }}
{{ end }}

View File

@@ -0,0 +1,114 @@
:root {
--color-hl: #a19bff;
--color-fg: #333;
--color-fg2: #666;
--color-bg: #ccc;
--color-bg2: #999;
}
@media (prefers-color-scheme: dark) {
:root {
--color-fg: #ccc;
--color-fg2: #999;
--color-bg: #333;
--color-bg2: #666;
}
}
::selection {
background-color:var(--color-hl); color:#333;
}
html { overflow-y:scroll; background-color:var(--color-bg); }
body { padding:4vw; margin:0; font-family:sans-serif;
color:var(--color-fg); accent-clor:var(--color-hl); position:relative; }
a { color:inherit; text-decoration:none; -webkit-user-select:none;
-moz-user-select:none; -ms-user-select:none; user-select:none; }
.edit { display:block; color:var(--color-hl); text-align:center; text-decoration:none;
width:30px; line-height:30px; display:block; transition:all 0.2s;
cursor:pointer; margin:0; padding:0; position:absolute; top:0; right:0;
border-radius:100px; }
.edit:hover { color:var(--color-bg); background:var(--color-hl);
text-decoration:none; }
.category { overflow:hidden; }
.category h1 { font-size:15px; margin:0; line-height:30px; outline:none;
position:relative; padding:0 35px 0 0;
border-bottom:1px solid var(--color-fg); }
.category h1 a:hover { text-decoration:none; }
.category .title { cursor:pointer; transition:color 0.2s; }
.category .title:hover { color:var(--color-hl); }
.category .badge { display:inline-block; font-weight:normal;
font-size:8px; padding:0 5px; border:1px solid var(--color-bg2);
border-radius:100px; line-height:14px; text-align:center;
min-width:11px; margin-left:10px; vertical-align:middle;
color:var(--color-fg2); font-family:monospace; }
.bookmarks { display:none; list-style:none; margin:0; padding:30px 0; }
.category.open .bookmarks { display:block; }
.bookmark { display:block; padding:0; position:relative;
border-bottom:1px dotted var(--color-bg2); }
.bookmark .link { color:inherit; display:block; min-height:20px;
line-height:20px; padding:2px 0; }
.bookmark .link:hover { background-color:#ffffff0f; }
.bookmark .editicon { width:20px; height:20px; float:left;
margin-right:5px; border-radius:2px; position:relative;
cursor:pointer; margin-top:2px; transition:all 0.2s; }
.bookmark .editicon img { width:16px; height:16px; position:absolute;
top:2px; left:2px; }
.bookmark .editicon:hover { background-color:var(--color-hl); }
button,
.button { display:inline-block; padding:5px 20px; text-decoration:none;
color:var(--color-fg); background:transparent; font-size:inherit;
border:1px solid var(--color-fg); cursor:pointer; transition:all 0.2s;
-webkit-user-select:none; -moz-user-select:none; -ms-user-select:none;
user-selct:none; }
button:hover,
.button:hover { color:var(--color-hl); border-color:var(--color-hl); }
.buttons { margin-top:20px; }
#slidein { position:fixed; top:0; right:0; width:300px; height:100%;
transform:translateX(100%); transition:transform 0.3s;
background:rgba(0,0,0,0.2); backdrop-filter:blur(10px); }
#slidein.open { transform:translateX(0); }
#slidein .closer { position:absolute; top:10px; right:10px; width:30px;
height:30px; cursor:pointer; }
#slidein .closer:before,
#slidein .closer:after { display:block; content:""; width:30px;
height:1px; background:var(--color-fg); position:absolute; top:50%;
left:0; }
#slidein .closer:before { transform:rotate(45deg); }
#slidein .closer:after { transform:rotate(-45deg); }
#slidein .inner { padding:50px 20px 20px 20px; color:var(--color-fg); }
.field label { display:block; margin:10px 0 3px 0; }
.field label:after { display:inline; content:":"; }
.field input[type="text"],
.field select { display:block; width:100%; box-sizing:border-box;
padding:3px 5px; border:1px solid var(--color-fg);
color:var(--color-fg); background:var(--color-bg); font-size:inherit; }
#favlet { position:absolute; top:10px; right:10px; color:var(--color-bg2);
text-decoration:none; font-size:10px; cursor:pointer;
transition:color 0.2s; }
#favlet:hover { color:var(--color-hl); }
.favletcode { word-break:break-all; }
#addcontent { padding:5vw; }
/*
@media screen and (min-width:500px) {
.categories { margin:-10px; }
.categories:after { display:table; content:""; clear:left; }
.category { float:left; width:50%; box-sizing:border-box; padding:0 10px; }
.category:nth-of-type(2n+1) { clear:left; }
}
@media screen and (min-width:750px) {
.category { width:33.33%; }
.category:nth-of-type(2n+1) { clear:none; }
.category:nth-of-type(3n+1) { clear:left; }
}
@media screen and (min-width:1000px) {
.category { width:25%; }
.category:nth-of-type(3n+1) { clear:none; }
.category:nth-of-type(4n+1) { clear:left; }
}
*/

35
hugo/static/css/main.css Normal file

File diff suppressed because one or more lines are too long

4
hugo/static/css/pico-2.1.1.min.css vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" id="svg8" version="1.1" viewBox="0 0 200 30" height="30" width="200">
<defs id="defs2"/>
<metadata id="metadata5">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g transform="translate(0,-303.77081)" id="layer1">
<path id="path817" d="m 50,303.77081 -40,14.70001 40,15.29999 z" style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.69282031px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
<rect y="303.77081" x="0" height="29.999996" width="10" id="rect819" style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.62703514;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"/>
<path id="path817-3" d="M 100.00001,303.77081 60,318.47082 l 40.00001,15.29999 z" style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.69282031px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
<path id="path817-3-6" d="M 100,303.77081 140.00001,318.47082 100,333.77081 Z" style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.69282031px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
<path id="path817-3-6-7" d="M 150,303.77081 190.00001,318.47082 150,333.77081 Z" style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.69282031px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
<rect y="303.77081" x="190" height="29.999996" width="10" id="rect819-5" style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2.62703514;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
hugo/static/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

5
hugo/static/js/alpine-3.15.3.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,134 @@
document.addEventListener('alpine:init', () => {
Alpine.store('alphabreed', {
backUrl: '/',
pending: false,
async apiCall(method, url, data) {
this.pending = true;
const options = {
method: method,
headers: { 'Authorization': localStorage.getItem('alphabreedtoken') }
};
if (data) {
options.body = new FormData();
for (let key in data)
if (data.hasOwnProperty(key))
options.body.append(key, data[key]);
}
const response = await fetch(url, options);
this.pending = false;
if (!response.ok)
return false;
if (response.status == 204)
return true;
return await response.json();
},
async checkToken() {
const token = localStorage.getItem('alphabreedtoken');
if (!token) {
window.location = '/login/?back='+encodeURIComponent(this.backUrl);
return false;
}
const result = await this.apiCall('POST', '/api/collections/users/auth-refresh');
if (!result.token) {
window.location = '/login/?back='+encodeURIComponent(this.backUrl);
return false;
}
localStorage.setItem('alphabreedtoken', result.token);
localStorage.setItem('alphabreedid', result.record.id);
return true;
},
async createBookmarkCategory(name) {
const result = await this.apiCall('POST',
'/api/collections/bookmarkcategories/records',
{ owner: localStorage.getItem('alphabreedid'), name: name }
);
return result.id;
},
async updateBookmarkCategory(id, name) {
await this.apiCall('PATCH',
'/api/collections/bookmarkcategories/records/'+encodeURIComponent(id),
{ owner: localStorage.getItem('alphabreedid'), name: name }
);
},
async deleteBookmarkCategory(id) {
await this.apiCall('DELETE',
'/api/collections/bookmarkcategories/records/'+encodeURIComponent(id)
);
},
async createBookmark(categoryId, name, url) {
const result = await this.apiCall('POST',
'/api/collections/bookmarks/records',
{
owner: localStorage.getItem('alphabreedid'),
category: categoryId,
name: name,
url: url
}
);
return result.id;
},
async updateBookmark(id, categoryId, name, url) {
await this.apiCall('PATCH',
'/api/collections/bookmarks/records/'+encodeURIComponent(id),
{
owner: localStorage.getItem('alphabreedid'),
category: categoryId,
name: name,
url: url
}
);
},
async deleteBookmark(id) {
await this.apiCall('DELETE',
'/api/collections/bookmarks/records/'+encodeURIComponent(id)
);
},
async getBookmarkCategories() {
const result = await this.apiCall('GET',
'/api/collections/bookmarkcategories/records?perPage=9999&fields=id,name&sort=name'
);
return result.items;
},
async getBookmarks() {
const result = await this.apiCall('GET',
'/api/collections/bookmarks/records?perPage=1000&expand=category&fields=id,name,url,expand.category.id,expand.category.name&sort=name'
);
return result.items;
},
async getNotes() {
const owner = localStorage.getItem('alphabreedid');
const result = await this.apiCall('GET',
'/api/collections/notes/records/'+encodeURIComponent(owner)+'?fields=notes'
);
if (result === false)
return false;
return result.notes;
},
async createNotes(notes) {
return await this.apiCall('POST',
'/api/collections/notes/records',
{ id: localStorage.getItem('alphabreedid'), notes: notes }
);
},
async updateNotes(notes) {
const owner = localStorage.getItem('alphabreedid');
return await this.apiCall('PATCH',
'/api/collections/notes/records/'+encodeURIComponent(owner),
{ notes: notes }
);
},
});
});

15
hugo/static/js/menu.js Normal file
View File

@@ -0,0 +1,15 @@
(function() {
var onHamburger = function(e) {
e.preventDefault();
var open = hamburger.className == 'open';
hamburger.className = open ? '' : 'open';
nav.className = open ? '' : 'open';
};
var nav = document.querySelector('header nav');
var hamburger = document.createElement('div');
hamburger.id = 'hamburger';
hamburger.innerHTML = '<div class="top"></div><div class="middle"></div><div class="bottom"></div>';
hamburger.onclick = onHamburger;
document.querySelector('header').appendChild(hamburger);
})();