Extract goals module
This commit is contained in:
150
src/main.js
150
src/main.js
@@ -136,8 +136,6 @@
|
||||
let score = 0;
|
||||
let highScore = 0;
|
||||
let longestChainRecord = 0;
|
||||
const goalMilestoneThresholds = [0.5, 0.75, 0.9];
|
||||
let announcedGoalMilestones = new Set();
|
||||
let clearedCount = 0;
|
||||
let clearedByColor = {};
|
||||
let gameOver = false;
|
||||
@@ -284,6 +282,16 @@
|
||||
isGameOver: () => gameOver,
|
||||
ballBaseline: BALL_BASELINE,
|
||||
});
|
||||
const goals = window.PhysilinksGoals.create({
|
||||
config,
|
||||
getCurrentScene: () => currentScene,
|
||||
getScore: () => score,
|
||||
getClearedCount: () => clearedCount,
|
||||
getClearedByColor: () => clearedByColor,
|
||||
getTimerEndMs: () => timerEndMs,
|
||||
ui,
|
||||
});
|
||||
const normalizeColor = goals.normalizeColor;
|
||||
|
||||
const restartGame = () => {
|
||||
spawnSystem.stopSpawner();
|
||||
@@ -294,7 +302,7 @@
|
||||
score = 0;
|
||||
clearedCount = 0;
|
||||
clearedByColor = {};
|
||||
announcedGoalMilestones.clear();
|
||||
goals.resetMilestones();
|
||||
endDrag();
|
||||
const winCond = currentScene?.config?.winCondition;
|
||||
if (winCond?.type === "timer") {
|
||||
@@ -332,7 +340,7 @@
|
||||
}
|
||||
spawnSystem.startSpawner();
|
||||
}
|
||||
showGoalIntro();
|
||||
goals.showGoalIntro();
|
||||
};
|
||||
|
||||
const setHighlight = (body, on) => {
|
||||
@@ -416,7 +424,7 @@
|
||||
|
||||
const checkWinCondition = () => {
|
||||
if (levelWon) return;
|
||||
const goal = getGoalState();
|
||||
const goal = goals.getGoalState();
|
||||
ui.setGoal(goal || { label: "—", progress: 0 });
|
||||
if (!goal || !goal.met) return;
|
||||
applyWinEffects();
|
||||
@@ -709,143 +717,15 @@
|
||||
highScore,
|
||||
activeColor: chain.color,
|
||||
});
|
||||
const goal = getGoalState();
|
||||
const goal = goals.getGoalState();
|
||||
ui.setGoal(goal || { label: "—", progress: 0 });
|
||||
maybeAnnounceGoalProgress(goal);
|
||||
goals.maybeAnnounceGoalProgress(goal);
|
||||
};
|
||||
|
||||
const buildLegend = () => {
|
||||
ui.buildLegend(config.palette);
|
||||
};
|
||||
|
||||
const normalizeColor = (c) => (c || "").trim().toLowerCase();
|
||||
|
||||
const getGoalState = () => {
|
||||
const winCond = currentScene?.config?.winCondition;
|
||||
if (!winCond) return null;
|
||||
if (winCond.type === "timer") {
|
||||
const duration = winCond.durationSec ?? 120;
|
||||
const now = Date.now();
|
||||
const end = timerEndMs || now + duration * 1000;
|
||||
const remainingMs = Math.max(0, end - now);
|
||||
const remainingSec = Math.ceil(remainingMs / 1000);
|
||||
const elapsed = Math.max(0, duration - remainingSec);
|
||||
return {
|
||||
label: `${String(Math.floor(remainingSec / 60)).padStart(2, "0")}:${String(remainingSec % 60).padStart(2, "0")}`,
|
||||
progress: duration > 0 ? (100 * elapsed) / duration : 0,
|
||||
met: remainingMs <= 0,
|
||||
hint: "Survive until the timer ends",
|
||||
summary: `Survive for ${duration} seconds`,
|
||||
};
|
||||
}
|
||||
if (winCond.type === "clearCount") {
|
||||
const target = winCond.target ?? 0;
|
||||
const remaining = Math.max(0, target - clearedCount);
|
||||
return {
|
||||
label: `Clear ${target} balls (${remaining} left)`,
|
||||
progress: target > 0 ? (100 * clearedCount) / target : 0,
|
||||
met: clearedCount >= target,
|
||||
hint: "Clear the required balls",
|
||||
summary: `Clear ${target} balls`,
|
||||
};
|
||||
}
|
||||
if (winCond.type === "score") {
|
||||
const target = winCond.target ?? 0;
|
||||
const remaining = Math.max(0, target - score);
|
||||
return {
|
||||
label: `Score ${target} (${remaining} left)`,
|
||||
progress: target > 0 ? (100 * score) / target : 0,
|
||||
met: score >= target,
|
||||
hint: "Reach the score target",
|
||||
summary: `Score ${target} points`,
|
||||
};
|
||||
}
|
||||
if (winCond.type === "colorClear" && Array.isArray(winCond.targets)) {
|
||||
const targets = winCond.targets.map((t) => ({
|
||||
color: normalizeColor(t.color),
|
||||
count: t.count || 0,
|
||||
}));
|
||||
const totalTarget = targets.reduce((sum, t) => sum + t.count, 0);
|
||||
let totalAchieved = 0;
|
||||
const parts = targets.map((t) => {
|
||||
const achieved = Math.max(
|
||||
0,
|
||||
clearedByColor[normalizeColor(t.color)] || 0,
|
||||
);
|
||||
const got = Math.min(t.count, achieved);
|
||||
totalAchieved += got;
|
||||
const remaining = Math.max(0, t.count - got);
|
||||
return `${got}/${t.count} (${remaining} left)`;
|
||||
});
|
||||
return {
|
||||
label: parts.join(" • "),
|
||||
progress: totalTarget > 0 ? (100 * totalAchieved) / totalTarget : 0,
|
||||
met: targets.every(
|
||||
(t) =>
|
||||
(clearedByColor[normalizeColor(t.color)] || 0) >= (t.count || 0),
|
||||
),
|
||||
colors: targets.map((t) => t.color),
|
||||
hint: "Clear the target colors",
|
||||
summary: `Clear target colors`,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const formatGoalMessage = (goal, { includeProgress = true } = {}) => {
|
||||
if (!goal) return null;
|
||||
const title = goal.summary || goal.hint || goal.label || "Goal";
|
||||
const pieces = [`Goal: ${title}`];
|
||||
if (includeProgress && Number.isFinite(goal.progress)) {
|
||||
const pct = Math.max(0, Math.min(100, Math.round(goal.progress)));
|
||||
pieces.push(`${pct}% complete`);
|
||||
}
|
||||
return pieces.join(" • ");
|
||||
};
|
||||
|
||||
const maybeAnnounceGoalProgress = (goal) => {
|
||||
if (!goal || !Number.isFinite(goal.progress)) return;
|
||||
const fraction = Math.max(0, Math.min(1, goal.progress / 100));
|
||||
for (const threshold of goalMilestoneThresholds) {
|
||||
if (fraction >= threshold && !announcedGoalMilestones.has(threshold)) {
|
||||
announcedGoalMilestones.add(threshold);
|
||||
const text = config.messages?.text || formatGoalMessage(goal);
|
||||
if (!text) return;
|
||||
const colors =
|
||||
(Array.isArray(config.messages?.colors) && config.messages.colors) ||
|
||||
goal?.colors ||
|
||||
null;
|
||||
ui.showFloatingMessage(
|
||||
{ text, colors },
|
||||
{
|
||||
durationMs: config.messages.durationMs,
|
||||
position: config.messages.position,
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const showGoalIntro = () => {
|
||||
const goal = getGoalState();
|
||||
const text =
|
||||
config.messages?.text ||
|
||||
formatGoalMessage(goal, { includeProgress: false });
|
||||
if (!text) return;
|
||||
const colors =
|
||||
(Array.isArray(config.messages?.colors) && config.messages.colors) ||
|
||||
goal?.colors ||
|
||||
null;
|
||||
ui.showFloatingMessage(
|
||||
{ text, colors },
|
||||
{
|
||||
durationMs: config.messages.durationMs,
|
||||
position: config.messages.position,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const clampBodiesIntoView = (prevWidth, prevHeight) => {
|
||||
const scaleX = width / (prevWidth || width);
|
||||
const scaleY = height / (prevHeight || height);
|
||||
|
||||
Reference in New Issue
Block a user