a45e358e83
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.
135 lines
3.5 KiB
TypeScript
135 lines
3.5 KiB
TypeScript
import type {
|
|
Match3DItemSnapshot,
|
|
Match3DTraySlot,
|
|
} from '../../../packages/shared/src/contracts/match3dRuntime';
|
|
|
|
type Match3DTrayOccupant = {
|
|
itemInstanceId: string;
|
|
itemTypeId: string;
|
|
visualKey: string;
|
|
};
|
|
|
|
export type Match3DTrayInsertionPlan = {
|
|
slotIndex: number;
|
|
traySlots: Match3DTraySlot[];
|
|
};
|
|
|
|
function resolveMatch3DTraySlotOrder(traySlots: Match3DTraySlot[]) {
|
|
return [...traySlots].sort((left, right) => left.slotIndex - right.slotIndex);
|
|
}
|
|
|
|
function resolveMatch3DTrayOccupants(traySlots: Match3DTraySlot[]) {
|
|
return resolveMatch3DTraySlotOrder(traySlots).flatMap((slot) =>
|
|
slot.itemInstanceId && slot.itemTypeId && slot.visualKey
|
|
? [
|
|
{
|
|
itemInstanceId: slot.itemInstanceId,
|
|
itemTypeId: slot.itemTypeId,
|
|
visualKey: slot.visualKey,
|
|
},
|
|
]
|
|
: [],
|
|
);
|
|
}
|
|
|
|
function rebuildMatch3DTraySlots(
|
|
traySlots: Match3DTraySlot[],
|
|
occupants: Match3DTrayOccupant[],
|
|
) {
|
|
return resolveMatch3DTraySlotOrder(traySlots).map((slot, index) => {
|
|
const occupant = occupants[index];
|
|
return occupant
|
|
? {
|
|
slotIndex: slot.slotIndex,
|
|
itemInstanceId: occupant.itemInstanceId,
|
|
itemTypeId: occupant.itemTypeId,
|
|
visualKey: occupant.visualKey,
|
|
}
|
|
: { slotIndex: slot.slotIndex };
|
|
});
|
|
}
|
|
|
|
export function buildMatch3DTrayInsertionPlan(
|
|
traySlots: Match3DTraySlot[],
|
|
item: Pick<
|
|
Match3DItemSnapshot,
|
|
'itemInstanceId' | 'itemTypeId' | 'visualKey'
|
|
>,
|
|
): Match3DTrayInsertionPlan | null {
|
|
const orderedSlots = resolveMatch3DTraySlotOrder(traySlots);
|
|
const occupants = resolveMatch3DTrayOccupants(orderedSlots);
|
|
if (occupants.length >= orderedSlots.length) {
|
|
return null;
|
|
}
|
|
|
|
let lastSameTypeIndex = -1;
|
|
for (let index = occupants.length - 1; index >= 0; index -= 1) {
|
|
if (occupants[index]?.itemTypeId === item.itemTypeId) {
|
|
lastSameTypeIndex = index;
|
|
break;
|
|
}
|
|
}
|
|
const insertionIndex =
|
|
lastSameTypeIndex >= 0 ? lastSameTypeIndex + 1 : occupants.length;
|
|
occupants.splice(insertionIndex, 0, {
|
|
itemInstanceId: item.itemInstanceId,
|
|
itemTypeId: item.itemTypeId,
|
|
visualKey: item.visualKey,
|
|
});
|
|
|
|
return {
|
|
slotIndex: orderedSlots[insertionIndex]?.slotIndex ?? insertionIndex,
|
|
traySlots: rebuildMatch3DTraySlots(orderedSlots, occupants),
|
|
};
|
|
}
|
|
|
|
export function syncMatch3DItemTraySlotIndexes(
|
|
items: Match3DItemSnapshot[],
|
|
traySlots: Match3DTraySlot[],
|
|
) {
|
|
const slotByItemId = new Map(
|
|
traySlots.flatMap((slot) =>
|
|
slot.itemInstanceId
|
|
? [[slot.itemInstanceId, slot.slotIndex] as const]
|
|
: [],
|
|
),
|
|
);
|
|
|
|
return items.map((item) =>
|
|
item.state === 'InTray' && slotByItemId.has(item.itemInstanceId)
|
|
? {
|
|
...item,
|
|
traySlotIndex: slotByItemId.get(item.itemInstanceId),
|
|
}
|
|
: item.state === 'Cleared'
|
|
? {
|
|
...item,
|
|
traySlotIndex: null,
|
|
}
|
|
: item,
|
|
);
|
|
}
|
|
|
|
export function compactMatch3DTraySlots(traySlots: Match3DTraySlot[]) {
|
|
return rebuildMatch3DTraySlots(
|
|
resolveMatch3DTraySlotOrder(traySlots),
|
|
resolveMatch3DTrayOccupants(traySlots),
|
|
);
|
|
}
|
|
|
|
export function resolveMatch3DTrayItemIdToSlotIndexMap(
|
|
traySlots: Match3DTraySlot[],
|
|
) {
|
|
return new Map(
|
|
resolveMatch3DTraySlotOrder(traySlots).flatMap((slot) =>
|
|
slot.itemInstanceId ? [[slot.itemInstanceId, slot.slotIndex] as const] : [],
|
|
),
|
|
);
|
|
}
|
|
|
|
export function resolveMatch3DTraySlotRectIndexOrder(
|
|
traySlots: Match3DTraySlot[],
|
|
) {
|
|
return resolveMatch3DTraySlotOrder(traySlots).map((slot) => slot.slotIndex);
|
|
}
|