Files
Zauberwald/src/forest/scene.ts
T
gurix 559d57a013 feat(05-01): extend Progress type with currentForestPage, add paginated renderForestScene
- Add currentForestPage: number field to Progress interface in types.ts
- Export SLOTS_PER_PAGE = 12 constant from scene.ts
- Change renderForestScene signature to accept page: number parameter
- Slice elements by page (start/end = page * SLOTS_PER_PAGE)
- Update navigation UI (forest-nav) after rendering
- Add migrateProgress entry for currentForestPage in app.ts
- Fix Progress object literals in db.test.ts and screens.ts to include new field
2026-03-29 22:57:32 +02:00

161 lines
4.7 KiB
TypeScript

import type { Progress, LevelStatus } from "../types";
import { levels } from "../game/levels";
import { getProgress, getForestElements } from "../storage/db";
import { playForestElementSound } from "../audio/sounds";
let activeObjectUrls: string[] = [];
export const SLOTS_PER_PAGE = 12;
export async function renderForestScene(
db: IDBDatabase,
page: number,
newElementId?: number,
): Promise<void> {
const grid = document.getElementById("forest-grid");
if (!grid) return;
// Revoke old object URLs to prevent memory leaks
for (const url of activeObjectUrls) {
URL.revokeObjectURL(url);
}
activeObjectUrls = [];
const elements = await getForestElements(db);
grid.innerHTML = "";
// Render elements for the given page (4x3 grid per page)
const start = page * SLOTS_PER_PAGE;
const end = start + SLOTS_PER_PAGE;
const toRender = elements.slice(start, end);
for (const element of toRender) {
const slot = document.createElement("div");
slot.className = "forest__element";
const img = document.createElement("img");
const objectUrl = URL.createObjectURL(element.imageBlob);
activeObjectUrls.push(objectUrl);
img.src = objectUrl;
img.alt = element.description;
// Random offset +-10-20px for natural placement
const offsetX =
(Math.random() > 0.5 ? 1 : -1) * (10 + Math.random() * 10);
const offsetY =
(Math.random() > 0.5 ? 1 : -1) * (10 + Math.random() * 10);
img.style.transform = `translate(${Math.round(offsetX)}px, ${Math.round(offsetY)}px)`;
// Animate newly added element
if (element.id === newElementId) {
slot.classList.add("forest__element--new");
playForestElementSound();
}
slot.appendChild(img);
grid.appendChild(slot);
}
// Update navigation UI
const totalPages = Math.max(1, Math.ceil(elements.length / SLOTS_PER_PAGE));
const navContainer = document.getElementById("forest-nav");
if (navContainer) {
const prevBtn = document.getElementById(
"forest-nav-prev",
) as HTMLButtonElement;
const nextBtn = document.getElementById(
"forest-nav-next",
) as HTMLButtonElement;
const pageText = document.getElementById("forest-nav-text");
if (totalPages <= 1) {
navContainer.style.display = "none";
} else {
navContainer.style.display = "flex";
prevBtn.disabled = page <= 0;
nextBtn.disabled = page >= totalPages - 1;
if (pageText) pageText.textContent = `Wald ${page + 1} von ${totalPages}`;
}
}
}
export function getLevelStatus(
levelNum: number,
progress: Progress,
): LevelStatus {
if (progress.completedLevels.includes(levelNum)) return "completed";
if (levelNum === progress.currentLevel) return "current";
return "locked";
}
export function renderLevelMap(
container: HTMLElement,
progress: Progress,
onLevelClick: (level: number) => void,
): void {
container.innerHTML = "";
levels.forEach((level) => {
const status = getLevelStatus(level.level, progress);
const card = document.createElement("button");
card.className = `level-card level-card--${status}`;
card.dataset.level = String(level.level);
const statusIcon =
status === "completed" ? "\u2713" : status === "current" ? "\u2192" : "\uD83D\uDD12";
const keysDisplay = level.newKeys
.filter((k) => k !== " ")
.map((k) => k.toUpperCase())
.join(" ");
card.innerHTML = `
<span class="level-card__number">Stufe ${level.level}</span>
<span class="level-card__keys">${keysDisplay}${level.newKeys.includes(" ") ? " + Leertaste" : ""}</span>
<span class="level-card__status">${statusIcon}</span>
`;
// Only current and completed levels are clickable
if (status !== "locked") {
card.addEventListener("click", () => onLevelClick(level.level));
} else {
card.disabled = true;
}
container.appendChild(card);
});
}
const MAX_LEVEL = 6;
export async function initForestScreen(
db: IDBDatabase,
onStartLesson: (level: number) => void,
): Promise<void> {
const mapContainer = document.getElementById("level-map")!;
const continueBtn = document.getElementById(
"continue-btn",
) as HTMLButtonElement;
const progress = await getProgress(db);
if (!progress) return;
renderLevelMap(mapContainer, progress, onStartLesson);
const currentPage = progress.currentForestPage ?? 0;
await renderForestScene(db, currentPage);
const allComplete = levels.every((l) =>
progress.completedLevels.includes(l.level),
);
if (allComplete) {
continueBtn.textContent = "Alle Stufen geschafft!";
continueBtn.disabled = true;
continueBtn.classList.add("forest__continue-btn--done");
} else {
continueBtn.textContent = "Weiter üben";
continueBtn.disabled = false;
continueBtn.classList.remove("forest__continue-btn--done");
continueBtn.onclick = () =>
onStartLesson(Math.min(progress.currentLevel, MAX_LEVEL));
}
}