Add help modal, changelog, map visits, Dockerfile, CI
- Help modal (? button or keyboard shortcut): how-to-play in Swedish, flavor text, full changelog sourced from src/data/version.ts - src/data/version.ts: versioned changelog entries, in-universe Malmö voice - Map zone clicks: each location on the Leaflet map is now clickable. Unlocked cult areas give resources + log entries. Locked locations show lore hints. 30s cooldown per location so it's a decision, not a spam button. - Dockerfile + nginx.conf + docker-compose.yml (port 5684) - Gitea CI workflow (identical to 2048 — derives image from repo path, needs TKNTKN secret, pushes :latest and :sha tags) - README.md with mechanics summary and docker instructions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
57
.gitea/workflows/build.yml
Normal file
57
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
name: Docker
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Derive image name and tags
|
||||||
|
id: meta
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SERVER_URL="${GITHUB_SERVER_URL:-${GITEA_SERVER_URL:-}}"
|
||||||
|
REPO="${GITHUB_REPOSITORY:-${GITEA_REPOSITORY:-}}"
|
||||||
|
SHA="${GITHUB_SHA:-${GITEA_SHA:-}}"
|
||||||
|
|
||||||
|
if [[ -z "${SERVER_URL}" || -z "${REPO}" || -z "${SHA}" ]]; then
|
||||||
|
echo "Missing SERVER_URL/REPO/SHA env vars." >&2
|
||||||
|
echo "SERVER_URL='${SERVER_URL}' REPO='${REPO}' SHA='${SHA}'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
HOST=$(echo "${SERVER_URL}" | sed 's|https://||;s|http://||')
|
||||||
|
IMAGE=$(echo "${HOST}/${REPO}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
SHORT_SHA=$(echo "${SHA}" | cut -c1-7)
|
||||||
|
|
||||||
|
echo "host=${HOST}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Log in to Gitea container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ steps.meta.outputs.host }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.TKNTKN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ steps.meta.outputs.image }}:latest
|
||||||
|
${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.short_sha }}
|
||||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM node:22-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY --from=build /app/dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
40
README.md
Normal file
40
README.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Djupet
|
||||||
|
|
||||||
|
Lovecraftian idle cult management game set in Malmö, Sweden.
|
||||||
|
|
||||||
|
You run a small cult in a basement in Rosengård. The goal is to complete the ritual before the investigators close in. The Turning Torso is involved. The ducks at Ribersborg know more than they let on.
|
||||||
|
|
||||||
|
Inspired by Anders Fager, Kult, and Call of Cthulhu.
|
||||||
|
|
||||||
|
**Play:** [djupet.games.goonk.se](https://djupet.games.goonk.se)
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
Vanilla TypeScript + Vite. No framework. Leaflet for the map.
|
||||||
|
|
||||||
|
## Dev
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
npm run dev # localhost:5681
|
||||||
|
npm run typecheck
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mechanics
|
||||||
|
|
||||||
|
- **Andakt** — passive income, spend on upgrades
|
||||||
|
- **Dread** — builds as the ritual deepens, unlocks areas and events
|
||||||
|
- **Hemlighet** — your exposure level; reaches zero and you lose
|
||||||
|
- **Kaos** — triggers chaotic events at 100, resets, repeat
|
||||||
|
- **Ritual** — reach 100 to win; unlocked via the Manuscript
|
||||||
|
|
||||||
|
Click locations on the Malmö map to visit them. There is a cooldown.
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Serves on port `5684` by default. Override via `HOST_PORT` in `.env`.
|
||||||
6
docker-compose.yml
Normal file
6
docker-compose.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: ${IMAGE:-djupet:local}
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${HOST_PORT:-5684}:80"
|
||||||
10
nginx.conf
Normal file
10
nginx.conf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/css application/javascript application/json image/svg+xml;
|
||||||
|
}
|
||||||
35
src/data/version.ts
Normal file
35
src/data/version.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export const VERSION = '0.2.0';
|
||||||
|
|
||||||
|
export interface ChangelogEntry {
|
||||||
|
version: string;
|
||||||
|
date: string;
|
||||||
|
changes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
{
|
||||||
|
version: '0.2.0',
|
||||||
|
date: '2026-07-07',
|
||||||
|
changes: [
|
||||||
|
'Klicka på platser på kartan för att besöka dem — varje besök lämnar spår',
|
||||||
|
'Välkomnar Anders Fager, Kult och Call of Cthulhu till händelsepoolen',
|
||||||
|
'Utökad händelsepool: ICA-kassörskor som inte blinkar, SJ-tåg som stannar i tunnlar som inte finns, en stad på Öresunds botten med belysning tänd',
|
||||||
|
'Ny uppgradering: Paktens Förnyelse (någon tecknade den redan — vi förnyar den bara)',
|
||||||
|
'Uppgraderingsbeskrivningar omskrivna till rätt register: vardagssvenska, kosmisk fasa, inga utropstecken',
|
||||||
|
'Autospar var 10:e sekund och vid varje köp, versionsnyckeln djupet_v001',
|
||||||
|
'Logg: senaste händelse överst',
|
||||||
|
'Knappklick fungerar nu (renderingen bytte ut DOM 30 gånger per sekund)',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '0.1.0',
|
||||||
|
date: '2026-07-05',
|
||||||
|
changes: [
|
||||||
|
'Källarmötet grundades i Rosengård',
|
||||||
|
'Kartan över Malmö aktiverades — Turning Torso, Värnhemstorget, Øresundsbron',
|
||||||
|
'Ritualsystemet implementerat: andakt, dread, hemlighet, kaos',
|
||||||
|
'Göran från Discord kom med av misstag och är fortfarande kvar',
|
||||||
|
'Den Sovande är ännu inte vaken',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -152,6 +152,85 @@ export function tick(state: State): State {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LocationVisit {
|
||||||
|
text: string;
|
||||||
|
effect: Partial<{ andakt: number; dread: number; kaos: number; ritual: number; hemlighet: number }>;
|
||||||
|
requiresArea?: keyof State['areas'];
|
||||||
|
lockedText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOCATION_VISITS: Record<string, LocationVisit> = {
|
||||||
|
rosengard: {
|
||||||
|
text: 'Du sitter i källaren. Lamporna flimrar i takt med ditt hjärta. Eller tvärtom. +andakt',
|
||||||
|
effect: { andakt: 15 },
|
||||||
|
requiresArea: 'rosengard',
|
||||||
|
lockedText: 'En fuktig källarlokal i Rosengård. Du kan se ljuset under dörren. Ni är inte redo än.',
|
||||||
|
},
|
||||||
|
varnhem: {
|
||||||
|
text: 'Cirkeln är lite större sedan du var här sist. Du ritade den inte. +dread +ritual',
|
||||||
|
effect: { dread: 8, ritual: 2 },
|
||||||
|
requiresArea: 'varnhem',
|
||||||
|
lockedText: 'Värnhemstorget ser normalt ut. Det är ett dåligt tecken.',
|
||||||
|
},
|
||||||
|
torso: {
|
||||||
|
text: 'Byggnaden pekar. Du tittar dit den pekar. Du tittar bort igen. Det hjälper inte. +dread +ritual',
|
||||||
|
effect: { dread: 15, ritual: 5 },
|
||||||
|
requiresArea: 'torso',
|
||||||
|
lockedText: 'Turning Torso vrider sig i vinden. Inte i vinden. I något annat.',
|
||||||
|
},
|
||||||
|
oresund: {
|
||||||
|
text: 'Vattnet sjunker lite långsammare just nu. Det vet att du stod här. +dread',
|
||||||
|
effect: { dread: 5 },
|
||||||
|
lockedText: 'Öresund. Danskarna på andra sidan verkar inte märka någonting. Det är det mest oroande av allt.',
|
||||||
|
},
|
||||||
|
ribersborg: {
|
||||||
|
text: 'Änderna tittar på dig. En av dem nickar. Du nickar tillbaka. Det var ett misstag. +andakt',
|
||||||
|
effect: { andakt: 8 },
|
||||||
|
lockedText: 'Stranden är tom nu. Änderna sitter stilla. De väntar på rätt tidpunkt.',
|
||||||
|
},
|
||||||
|
davidshall: {
|
||||||
|
text: 'Källarlokalen luktar fortfarande. Du andas djupare än du borde. +andakt',
|
||||||
|
effect: { andakt: 10 },
|
||||||
|
lockedText: 'Davidshallsgatan. Kakel från 1923. Bakom det finns något som väljer att vänta.',
|
||||||
|
},
|
||||||
|
stortorget: {
|
||||||
|
text: 'Du vet inte varför du kom hit. Du vet inte hur länge du stod här. +kaos',
|
||||||
|
effect: { kaos: 8 },
|
||||||
|
lockedText: 'Stortorget är fullt av folk som inte vet vad de gör här heller. Du passar in.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const VISIT_COOLDOWN_TICKS = 900; // 30s at 30fps
|
||||||
|
|
||||||
|
export function visitLocation(state: State, id: string, cooldowns: Map<string, number>): { state: State; cooldowns: Map<string, number> } {
|
||||||
|
const visit = LOCATION_VISITS[id];
|
||||||
|
if (!visit) return { state, cooldowns };
|
||||||
|
|
||||||
|
const now = state.tick;
|
||||||
|
if ((cooldowns.get(id) ?? 0) > now) return { state, cooldowns };
|
||||||
|
|
||||||
|
const s = { ...state, areas: { ...state.areas }, upgrades: new Set(state.upgrades), log: [...state.log], usedEventIds: new Set(state.usedEventIds) };
|
||||||
|
|
||||||
|
const areaLocked = visit.requiresArea && !s.areas[visit.requiresArea];
|
||||||
|
const text = areaLocked ? visit.lockedText : visit.text;
|
||||||
|
|
||||||
|
if (!areaLocked) {
|
||||||
|
if (visit.effect.andakt) s.andakt += visit.effect.andakt;
|
||||||
|
if (visit.effect.dread) s.dread += visit.effect.dread;
|
||||||
|
if (visit.effect.kaos) s.kaos = Math.min(100, s.kaos + visit.effect.kaos);
|
||||||
|
if (visit.effect.ritual && s.ritualUnlocked) s.ritual = Math.min(100, s.ritual + visit.effect.ritual);
|
||||||
|
if (visit.effect.hemlighet) s.hemlighet = Math.max(0, Math.min(100, s.hemlighet + visit.effect.hemlighet));
|
||||||
|
}
|
||||||
|
|
||||||
|
s.log.unshift({ tick: now, text: `⬡ ${text}`, kind: 'system' });
|
||||||
|
if (s.log.length > 40) s.log = s.log.slice(0, 40);
|
||||||
|
|
||||||
|
const newCooldowns = new Map(cooldowns);
|
||||||
|
newCooldowns.set(id, now + VISIT_COOLDOWN_TICKS);
|
||||||
|
|
||||||
|
return { state: s, cooldowns: newCooldowns };
|
||||||
|
}
|
||||||
|
|
||||||
export function saveState(state: State): void {
|
export function saveState(state: State): void {
|
||||||
try {
|
try {
|
||||||
const serial = {
|
const serial = {
|
||||||
|
|||||||
11
src/main.ts
11
src/main.ts
@@ -1,9 +1,10 @@
|
|||||||
import './style.css';
|
import './style.css';
|
||||||
import { createState, tick, buyUpgrade, saveState, loadState, clearSave } from './engine.ts';
|
import { createState, tick, buyUpgrade, visitLocation, saveState, loadState, clearSave } from './engine.ts';
|
||||||
import { initUI, renderState } from './ui.ts';
|
import { initUI, renderState } from './ui.ts';
|
||||||
import { initMap, updateMap } from './map.ts';
|
import { initMap, updateMap } from './map.ts';
|
||||||
|
|
||||||
let state = loadState() ?? createState();
|
let state = loadState() ?? createState();
|
||||||
|
let visitCooldowns = new Map<string, number>();
|
||||||
let lastTime = 0;
|
let lastTime = 0;
|
||||||
const TARGET_FPS = 30;
|
const TARGET_FPS = 30;
|
||||||
const FRAME_MS = 1000 / TARGET_FPS;
|
const FRAME_MS = 1000 / TARGET_FPS;
|
||||||
@@ -18,12 +19,18 @@ function start(): void {
|
|||||||
() => {
|
() => {
|
||||||
clearSave();
|
clearSave();
|
||||||
state = createState();
|
state = createState();
|
||||||
|
visitCooldowns = new Map();
|
||||||
document.getElementById('overlay')!.classList.remove('visible');
|
document.getElementById('overlay')!.classList.remove('visible');
|
||||||
renderState(state);
|
renderState(state);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
initMap('map');
|
initMap('map', (locationId) => {
|
||||||
|
const result = visitLocation(state, locationId, visitCooldowns);
|
||||||
|
state = result.state;
|
||||||
|
visitCooldowns = result.cooldowns;
|
||||||
|
renderState(state);
|
||||||
|
});
|
||||||
renderState(state);
|
renderState(state);
|
||||||
|
|
||||||
requestAnimationFrame(loop);
|
requestAnimationFrame(loop);
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ function dreadRadius(level: number): number {
|
|||||||
return [6, 10, 16, 24][Math.min(level, 3)];
|
return [6, 10, 16, 24][Math.min(level, 3)];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initMap(containerId: string): void {
|
export function initMap(containerId: string, onClick: (id: string) => void): void {
|
||||||
if (!L) return;
|
if (!L) return;
|
||||||
map = L.map(containerId, {
|
map = L.map(containerId, {
|
||||||
center: [55.597, 13.002],
|
center: [55.597, 13.002],
|
||||||
@@ -55,6 +55,7 @@ export function initMap(containerId: string): void {
|
|||||||
fillOpacity: 0.3,
|
fillOpacity: 0.3,
|
||||||
weight: 1,
|
weight: 1,
|
||||||
}).addTo(map).bindTooltip(loc.label, { className: 'map-tooltip', permanent: false });
|
}).addTo(map).bindTooltip(loc.label, { className: 'map-tooltip', permanent: false });
|
||||||
|
m.on('click', () => onClick(loc.id));
|
||||||
markers.set(loc.id, m);
|
markers.set(loc.id, m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
122
src/style.css
122
src/style.css
@@ -269,6 +269,128 @@ body {
|
|||||||
.phase-tag.mid { color: var(--accent); border-color: var(--accent); }
|
.phase-tag.mid { color: var(--accent); border-color: var(--accent); }
|
||||||
.phase-tag.late { color: var(--dread); border-color: var(--dread); }
|
.phase-tag.late { color: var(--dread); border-color: var(--dread); }
|
||||||
|
|
||||||
|
/* ── Help button ── */
|
||||||
|
.help-btn {
|
||||||
|
margin-left: auto;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--dim);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
padding: 3px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
.help-btn:hover { border-color: var(--accent); color: var(--accent); }
|
||||||
|
|
||||||
|
/* ── Help modal ── */
|
||||||
|
.help-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(6, 6, 14, 0.88);
|
||||||
|
z-index: 200;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.help-modal.visible { display: flex; }
|
||||||
|
|
||||||
|
.help-modal-inner {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
width: min(640px, 92vw);
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-modal-title {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--dim);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 4px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
.help-close:hover { color: var(--text); }
|
||||||
|
|
||||||
|
.help-modal-body {
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 18px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-section { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
|
||||||
|
.help-section-title {
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: var(--dim);
|
||||||
|
padding-bottom: 4px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-list {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.help-list li::before { content: '— '; color: var(--dim); }
|
||||||
|
.help-list strong { color: var(--accent); }
|
||||||
|
.help-list em { color: var(--dread); font-style: normal; }
|
||||||
|
|
||||||
|
.help-flavor {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--dim);
|
||||||
|
line-height: 1.8;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-changelog-entry {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-changelog-version {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-changelog-date {
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--dim);
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
#app { grid-template-columns: 1fr; }
|
#app { grid-template-columns: 1fr; }
|
||||||
.panel { border-right: none; border-bottom: 1px solid var(--border); }
|
.panel { border-right: none; border-bottom: 1px solid var(--border); }
|
||||||
|
|||||||
49
src/ui.ts
49
src/ui.ts
@@ -1,11 +1,53 @@
|
|||||||
import type { State } from './engine.ts';
|
import type { State } from './engine.ts';
|
||||||
import { UPGRADES } from './upgrades.ts';
|
import { UPGRADES } from './upgrades.ts';
|
||||||
|
import { VERSION, CHANGELOG } from './data/version.ts';
|
||||||
|
|
||||||
export function initUI(app: HTMLElement, onUpgrade: (id: string) => void, onRestart: () => void): void {
|
export function initUI(app: HTMLElement, onUpgrade: (id: string) => void, onRestart: () => void): void {
|
||||||
app.innerHTML = `
|
app.innerHTML = `
|
||||||
<div id="header">
|
<div id="header">
|
||||||
<div class="brand">DJUPET</div>
|
<div class="brand">DJUPET</div>
|
||||||
<div class="brand-sub">Malmö, Sverige · <span id="phase-tag" class="phase-tag early">Tidigt skede</span></div>
|
<div class="brand-sub">Malmö, Sverige · <span id="phase-tag" class="phase-tag early">Tidigt skede</span></div>
|
||||||
|
<button id="help-btn" class="help-btn" title="Hjälp & Ändringslogg">? v${VERSION}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="help-modal" class="help-modal">
|
||||||
|
<div class="help-modal-inner" id="help-modal-inner">
|
||||||
|
<div class="help-modal-header">
|
||||||
|
<span class="help-modal-title">DJUPET — Hjälp & Ändringslogg</span>
|
||||||
|
<button id="help-close" class="help-close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="help-modal-body">
|
||||||
|
<section class="help-section">
|
||||||
|
<div class="help-section-title">Hur man spelar</div>
|
||||||
|
<ul class="help-list">
|
||||||
|
<li><strong>Andakt</strong> ackumuleras passivt. Använd den för att köpa åtgärder i vänsterpanelen.</li>
|
||||||
|
<li><strong>Dread</strong> ökar när ritualen fördjupas. Hög dread låser upp nya platser och händelser.</li>
|
||||||
|
<li><strong>Hemlighet</strong> sjunker när ni blir för uppenbara. Når den noll är det slut.</li>
|
||||||
|
<li><strong>Kaos</strong> byggs upp naturligt och utlöser kaotiska händelser när det når 100.</li>
|
||||||
|
<li><strong>Ritual</strong> låses upp via Skriften. Nå 100 för att fullborda ceremonin.</li>
|
||||||
|
<li>Klicka på <em>platser på kartan</em> för att besöka dem. Varje besök lämnar spår.</li>
|
||||||
|
<li>Händelser inträffar automatiskt. De flesta är dåliga. En del är värre.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section class="help-section">
|
||||||
|
<div class="help-section-title">Om spelet</div>
|
||||||
|
<p class="help-flavor">
|
||||||
|
Djupet är ett idle-spel om en kult i Malmö, Sverige.<br>
|
||||||
|
Inspirerat av Anders Fager, Kult och Call of Cthulhu.<br>
|
||||||
|
Den Sovande är inte vaken ännu. Jobba på det.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section class="help-section">
|
||||||
|
<div class="help-section-title">Ändringslogg</div>
|
||||||
|
${CHANGELOG.map(e => `
|
||||||
|
<div class="help-changelog-entry">
|
||||||
|
<div class="help-changelog-version">v${e.version} <span class="help-changelog-date">${e.date}</span></div>
|
||||||
|
<ul class="help-list">${e.changes.map(c => `<li>${c}</li>`).join('')}</ul>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel" id="panel-left">
|
<div class="panel" id="panel-left">
|
||||||
@@ -77,6 +119,13 @@ export function initUI(app: HTMLElement, onUpgrade: (id: string) => void, onRest
|
|||||||
|
|
||||||
document.getElementById('overlay-btn')!.addEventListener('click', onRestart);
|
document.getElementById('overlay-btn')!.addEventListener('click', onRestart);
|
||||||
|
|
||||||
|
const helpModal = document.getElementById('help-modal')!;
|
||||||
|
const helpInner = document.getElementById('help-modal-inner')!;
|
||||||
|
document.getElementById('help-btn')!.addEventListener('click', () => helpModal.classList.toggle('visible'));
|
||||||
|
document.getElementById('help-close')!.addEventListener('click', () => helpModal.classList.remove('visible'));
|
||||||
|
helpModal.addEventListener('click', (e) => { if (!helpInner.contains(e.target as Node)) helpModal.classList.remove('visible'); });
|
||||||
|
document.addEventListener('keydown', (e) => { if (e.key === '?' || (e.key === 'Escape' && helpModal.classList.contains('visible'))) helpModal.classList.toggle('visible'); });
|
||||||
|
|
||||||
// Split the map container: map on top, log below
|
// Split the map container: map on top, log below
|
||||||
const mapContainer = document.getElementById('map-container')!;
|
const mapContainer = document.getElementById('map-container')!;
|
||||||
mapContainer.style.cssText = 'display:flex;flex-direction:column;overflow:hidden;height:100%;';
|
mapContainer.style.cssText = 'display:flex;flex-direction:column;overflow:hidden;height:100%;';
|
||||||
|
|||||||
Reference in New Issue
Block a user