Files
Genarrative/src/services/match3d-runtime/match3dTrayLayout.ts
T
JenkenB a45e358e83 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.
2026-05-16 22:59:02 +08:00

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);
}