- 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
161 lines
4.7 KiB
TypeScript
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));
|
|
}
|
|
}
|