Add generationStatus and match3d/runtime fixes
Introduce persistent generationStatus to work summaries (puzzle & match3d) and propagate generation recovery rules across docs and frontend/backends so "generating" is restored from server-side work summary rather than ephemeral front-end notices. Update API server image/asset handling (improve match3d material sheet green/alpha decontamination and promote generatedItemAssets background fields) and add runtime improvements: alpha-based hotspot hit-testing, tray insertion/three-match animation behavior, and session re-read on client-side VectorEngine timeouts/lock-screen interruptions. Many docs, tests and related frontend modules updated/added to reflect these contract and behavior changes.
This commit is contained in:
@@ -5,10 +5,16 @@ import type {
|
||||
Match3DRunSnapshot,
|
||||
Match3DTraySlot,
|
||||
} from '../../../packages/shared/src/contracts/match3dRuntime';
|
||||
import {
|
||||
buildMatch3DTrayInsertionPlan,
|
||||
compactMatch3DTraySlots,
|
||||
syncMatch3DItemTraySlotIndexes,
|
||||
} from './match3dTrayLayout';
|
||||
|
||||
const MATCH3D_TRAY_SLOT_COUNT = 7;
|
||||
const MATCH3D_LOCAL_DURATION_MS = 600_000;
|
||||
const MATCH3D_MAX_ITEM_TYPE_COUNT = 25;
|
||||
const MATCH3D_ITEMS_PER_CLEAR = 3;
|
||||
const MATCH3D_LOCAL_BASE_RADIUS = 0.072;
|
||||
const MATCH3D_LOCAL_BOARD_CENTER = 0.5;
|
||||
const MATCH3D_LOCAL_BOARD_RADIUS = 0.5;
|
||||
@@ -373,10 +379,6 @@ function recomputeClickable(items: Match3DItemSnapshot[]) {
|
||||
});
|
||||
}
|
||||
|
||||
function findNextTrayIndex(traySlots: Match3DTraySlot[]) {
|
||||
return traySlots.find((slot) => !slot.itemInstanceId)?.slotIndex ?? -1;
|
||||
}
|
||||
|
||||
function countClearedItems(items: Match3DItemSnapshot[]) {
|
||||
return items.filter((item) => item.state === 'Cleared').length;
|
||||
}
|
||||
@@ -410,27 +412,28 @@ function resolveRunStatus(run: Match3DRunSnapshot): Match3DRunSnapshot {
|
||||
};
|
||||
}
|
||||
|
||||
function settleMatchedTrayItems(run: Match3DRunSnapshot) {
|
||||
const slotsByType = new Map<string, Match3DTraySlot[]>();
|
||||
for (const slot of run.traySlots) {
|
||||
if (!slot.itemTypeId || !slot.itemInstanceId) {
|
||||
continue;
|
||||
}
|
||||
slotsByType.set(slot.itemTypeId, [
|
||||
...(slotsByType.get(slot.itemTypeId) ?? []),
|
||||
slot,
|
||||
]);
|
||||
}
|
||||
|
||||
const matchedSlots = [...slotsByType.values()].find(
|
||||
(slots) => slots.length >= 3,
|
||||
);
|
||||
function settleMatchedTrayItems(
|
||||
run: Match3DRunSnapshot,
|
||||
itemTypeId: string,
|
||||
) {
|
||||
const matchedSlots = run.traySlots
|
||||
.filter(
|
||||
(slot) =>
|
||||
slot.itemInstanceId && slot.itemTypeId && slot.itemTypeId === itemTypeId,
|
||||
)
|
||||
.slice(0, MATCH3D_ITEMS_PER_CLEAR);
|
||||
if (!matchedSlots) {
|
||||
return {
|
||||
run,
|
||||
clearedItemInstanceIds: [] as string[],
|
||||
};
|
||||
}
|
||||
if (matchedSlots.length < MATCH3D_ITEMS_PER_CLEAR) {
|
||||
return {
|
||||
run,
|
||||
clearedItemInstanceIds: [] as string[],
|
||||
};
|
||||
}
|
||||
|
||||
const clearedItemInstanceIds = matchedSlots
|
||||
.slice(0, 3)
|
||||
@@ -439,26 +442,36 @@ function settleMatchedTrayItems(run: Match3DRunSnapshot) {
|
||||
Boolean(itemInstanceId),
|
||||
);
|
||||
const clearedSet = new Set(clearedItemInstanceIds);
|
||||
const nextRun = {
|
||||
...run,
|
||||
traySlots: run.traySlots.map((slot) =>
|
||||
const compactedTraySlots = compactMatch3DTraySlots(
|
||||
run.traySlots.map((slot) =>
|
||||
slot.itemInstanceId && clearedSet.has(slot.itemInstanceId)
|
||||
? { slotIndex: slot.slotIndex }
|
||||
: slot,
|
||||
),
|
||||
);
|
||||
const nextRun = {
|
||||
...run,
|
||||
traySlots: compactedTraySlots,
|
||||
items: run.items.map((item) =>
|
||||
clearedSet.has(item.itemInstanceId)
|
||||
? {
|
||||
...item,
|
||||
state: 'Cleared' as const,
|
||||
clickable: false,
|
||||
traySlotIndex: null,
|
||||
}
|
||||
: item,
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
run: nextRun,
|
||||
run: {
|
||||
...nextRun,
|
||||
items: syncMatch3DItemTraySlotIndexes(
|
||||
nextRun.items,
|
||||
nextRun.traySlots,
|
||||
),
|
||||
},
|
||||
clearedItemInstanceIds,
|
||||
};
|
||||
}
|
||||
@@ -509,31 +522,27 @@ export function buildLocalMatch3DOptimisticRun(
|
||||
const targetItem = run.items.find(
|
||||
(item) => item.itemInstanceId === itemInstanceId,
|
||||
);
|
||||
const nextTrayIndex = findNextTrayIndex(run.traySlots);
|
||||
if (!targetItem || targetItem.state !== 'InBoard' || nextTrayIndex < 0) {
|
||||
if (!targetItem || targetItem.state !== 'InBoard') {
|
||||
return run;
|
||||
}
|
||||
const insertion = buildMatch3DTrayInsertionPlan(run.traySlots, targetItem);
|
||||
if (!insertion) {
|
||||
return run;
|
||||
}
|
||||
const nextItems = run.items.map((item) =>
|
||||
item.itemInstanceId === itemInstanceId
|
||||
? {
|
||||
...item,
|
||||
state: 'Flying' as const,
|
||||
clickable: false,
|
||||
traySlotIndex: insertion.slotIndex,
|
||||
}
|
||||
: item,
|
||||
);
|
||||
return {
|
||||
...run,
|
||||
items: run.items.map((item) =>
|
||||
item.itemInstanceId === itemInstanceId
|
||||
? {
|
||||
...item,
|
||||
state: 'Flying' as const,
|
||||
clickable: false,
|
||||
}
|
||||
: item,
|
||||
),
|
||||
traySlots: run.traySlots.map((slot) =>
|
||||
slot.slotIndex === nextTrayIndex
|
||||
? {
|
||||
slotIndex: slot.slotIndex,
|
||||
itemInstanceId: targetItem.itemInstanceId,
|
||||
itemTypeId: targetItem.itemTypeId,
|
||||
visualKey: targetItem.visualKey,
|
||||
}
|
||||
: slot,
|
||||
),
|
||||
items: syncMatch3DItemTraySlotIndexes(nextItems, insertion.traySlots),
|
||||
traySlots: insertion.traySlots,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -582,8 +591,8 @@ export async function confirmLocalMatch3DClick(
|
||||
clearedItemInstanceIds: [],
|
||||
};
|
||||
}
|
||||
const nextTrayIndex = findNextTrayIndex(run.traySlots);
|
||||
if (nextTrayIndex < 0) {
|
||||
const insertion = buildMatch3DTrayInsertionPlan(timedRun.traySlots, targetItem);
|
||||
if (!insertion) {
|
||||
const failedRun = {
|
||||
...timedRun,
|
||||
status: 'Failed' as const,
|
||||
@@ -601,27 +610,22 @@ export async function confirmLocalMatch3DClick(
|
||||
const movedRun: Match3DRunSnapshot = {
|
||||
...timedRun,
|
||||
snapshotVersion: run.snapshotVersion + 1,
|
||||
items: timedRun.items.map((item) =>
|
||||
item.itemInstanceId === targetItem.itemInstanceId
|
||||
? {
|
||||
...item,
|
||||
state: 'InTray' as const,
|
||||
clickable: false,
|
||||
}
|
||||
: item,
|
||||
),
|
||||
traySlots: timedRun.traySlots.map((slot) =>
|
||||
slot.slotIndex === nextTrayIndex
|
||||
? {
|
||||
slotIndex: slot.slotIndex,
|
||||
itemInstanceId: targetItem.itemInstanceId,
|
||||
itemTypeId: targetItem.itemTypeId,
|
||||
visualKey: targetItem.visualKey,
|
||||
}
|
||||
: slot,
|
||||
items: syncMatch3DItemTraySlotIndexes(
|
||||
timedRun.items.map((item) =>
|
||||
item.itemInstanceId === targetItem.itemInstanceId
|
||||
? {
|
||||
...item,
|
||||
state: 'InTray' as const,
|
||||
clickable: false,
|
||||
traySlotIndex: insertion.slotIndex,
|
||||
}
|
||||
: item,
|
||||
),
|
||||
insertion.traySlots,
|
||||
),
|
||||
traySlots: insertion.traySlots,
|
||||
};
|
||||
const settled = settleMatchedTrayItems(movedRun);
|
||||
const settled = settleMatchedTrayItems(movedRun, targetItem.itemTypeId);
|
||||
const nextRun = resolveRunStatus({
|
||||
...settled.run,
|
||||
items: recomputeClickable(settled.run.items),
|
||||
|
||||
Reference in New Issue
Block a user