Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions modules/game/components/Piece.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,29 @@ withDefaults(defineProps<Props>(), {
selectedPositions: () => [],
});

const emit = defineEmits(["swap", "drag-enter", "select"]);
const emit = defineEmits(["swap", "drag-enter", "drag-start", "drag-end", "select"]);

const startDrag = (
evt: DragEvent & { dataTransfer?: DataTransfer },
evt: DragEvent & { dataTransfer?: DataTransfer | null },
position: string,
selectedPositions: number[]
) => {
if (!evt.dataTransfer) return;

emit("drag-start", { position, selectedPositions });

evt.dataTransfer.dropEffect = "move";
evt.dataTransfer.effectAllowed = "move";
evt.dataTransfer.setData("position", position);
// Przekaż listę zaznaczonych slotów, żeby drop wiedział o grupie
// Przekaz listę zaznaczonych slotów, żeby drop wiedział o grupie

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three Polish comment spelling errors were introduced in this diff (diacritical marks removed):

  1. Line 33: "Przekaz" should be "Przekaż" (missing ż)
  2. Line 36: "przeciagamy" should be "przeciągamy" (missing ą)
  3. Line 106: "Nakladka" should be "Nakładka" (missing ł)

Copilot uses AI. Check for mistakes.
evt.dataTransfer.setData("selectedPositions", JSON.stringify(selectedPositions));

// Customowy ghost image gdy przeciągamy grupę
// Customowy ghost image gdy przeciagamy grupę

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling error: "przeciagamy" is missing the Polish diacritic ą and should be "przeciągamy".

Suggested change
// Customowy ghost image gdy przeciagamy grupę
// Customowy ghost image gdy przeciągamy grupę

Copilot uses AI. Check for mistakes.
if (selectedPositions.length > 1) {
const ghost = document.createElement("div");
ghost.style.cssText =
"position:fixed;top:-200px;left:-200px;display:flex;align-items:center;justify-content:center;background:rgba(59,130,246,0.9);color:white;font-weight:bold;font-size:16px;border-radius:10px;padding:8px 16px;pointer-events:none;box-shadow:0 4px 12px rgba(0,0,0,0.3);gap:6px;";
ghost.innerHTML = `<span style="font-size:18px;">${selectedPositions.length}</span><span style="font-size:12px;opacity:0.9;">klocki</span>`;
ghost.innerHTML = `<span style="font-size:18px;">x${selectedPositions.length}</span><span style="font-size:12px;opacity:0.9;">klocki</span>`;
document.body.appendChild(ghost);
evt.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2);
setTimeout(() => {
Expand All @@ -47,11 +50,22 @@ const startDrag = (
const onDragEnter = (evt: DragEvent, position: number | string) => {
evt.preventDefault();
evt.stopPropagation();
emit("drag-enter", { position });

const draggedPosition = evt.dataTransfer?.getData("position");
const rawSelectedPositions = evt.dataTransfer?.getData("selectedPositions") ?? "";

let selectedPositions: number[] = [];
try {
selectedPositions = JSON.parse(rawSelectedPositions);
} catch {
selectedPositions = [];
}

emit("drag-enter", { position, draggedPosition, selectedPositions });
};

const onDrop = (
evt: DragEvent & { dataTransfer?: DataTransfer },
evt: DragEvent & { dataTransfer?: DataTransfer | null },
positionCurrent: number
) => {
if (!evt.dataTransfer) return;
Expand All @@ -78,6 +92,8 @@ const onPieceClick = (evt: MouseEvent, position: number) => {
@dragover.prevent
@dragenter="onDragEnter($event, item.position)"
:key="item.position"
:data-highlighted="isHighlight ? 'true' : 'false'"
:data-selected="isSelected ? 'true' : 'false'"
:class="[
isHighlight && !isSelected && 'opacity-40',
isSelected
Expand All @@ -87,7 +103,7 @@ const onPieceClick = (evt: MouseEvent, position: number) => {
class="cursor-grab m-[1px] relative select-none transition-transform duration-100"
@click="onPieceClick($event, item.position)"
>
<!-- Nakładka zaznaczenia -->
<!-- Nakladka zaznaczenia -->

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling error: "Nakladka" is missing the Polish diacritic ł and should be "Nakładka".

Suggested change
<!-- Nakladka zaznaczenia -->
<!-- Nakładka zaznaczenia -->

Copilot uses AI. Check for mistakes.
<div
v-if="isSelected"
class="absolute inset-0 bg-blue-500/30 pointer-events-none z-10 mix-blend-multiply"
Expand All @@ -103,6 +119,7 @@ const onPieceClick = (evt: MouseEvent, position: number) => {
draggable="true"
class="overflow-hidden"
@dragstart="startDrag($event, String(item.position), selectedPositions)"
@dragend="emit('drag-end')"
:style="{
backgroundPosition: item.backgroundPosition,
width: item.width,
Expand Down
160 changes: 139 additions & 21 deletions modules/game/composables/useEventGame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ export async function useEventGame(

function toggleSelection(position: number, multi: boolean) {
if (!multi) {
// Klik bez modyfikatora odznaczamy wszystko i ewentualnie zaznaczamy ten klocek
// Klik bez modyfikatora -> odznaczamy wszystko i ewentualnie zaznaczamy ten klocek
if (data.selectedPositions.includes(position)) {
data.selectedPositions = [];
} else {
data.selectedPositions = [position];
}
} else {
// Klik z Shift/Ctrl toggle jednego elementu
// Klik z Shift/Ctrl -> toggle jednego elementu
if (data.selectedPositions.includes(position)) {
data.selectedPositions = data.selectedPositions.filter(
(p: number) => p !== position
Expand All @@ -52,22 +52,128 @@ export async function useEventGame(
data.selectedPositions = [];
}

function clearHighlight() {
data.highlightPositions = [];
}

let dragContext: { draggedPos: number | null; selectedSlots: number[] } = {
draggedPos: null,
selectedSlots: [],
};

function onDragStart({
position,
selectedPositions,
}: {
position: number | string;
selectedPositions: number[];
}) {
const draggedPos = Number(position);
if (Number.isNaN(draggedPos)) return;

const selectedSlots = selectedPositions
.map(Number)
.filter((slot) => !Number.isNaN(slot));

dragContext = {
draggedPos,
selectedSlots,
};
}

function clearDragContext() {
dragContext = {
draggedPos: null,
selectedSlots: [],
};
}

function onDragEnd() {
clearHighlight();
clearDragContext();
}

function resolveDragContext(
draggedPosition?: number | string,
selectedPositions?: number[]
) {
const draggedPosFromPayload = Number(draggedPosition);
const selectedSlotsFromPayload = (selectedPositions ?? [])
.map(Number)
.filter((slot) => !Number.isNaN(slot));

const hasGroupPayload =
selectedSlotsFromPayload.length > 1 &&
!Number.isNaN(draggedPosFromPayload) &&
selectedSlotsFromPayload.includes(draggedPosFromPayload);

if (hasGroupPayload) {
return {
draggedPos: draggedPosFromPayload,
selectedSlots: selectedSlotsFromPayload,
};
}

return {
draggedPos: dragContext.draggedPos,
selectedSlots: dragContext.selectedSlots,
};
}

// ─── Drag highlight ───────────────────────────────────────────────────────

let time: ReturnType<typeof setTimeout>;
function onDragEnter({ position }: { position: number | string }) {
function onDragEnter({
position,
draggedPosition,
selectedPositions,
}: {
position: number | string;
draggedPosition?: number | string;
selectedPositions?: number[];
}) {
clearTimeout(time);
time = setTimeout(() => {
data.highlight = position;
const targetPos = Number(position);
if (Number.isNaN(targetPos)) {
clearHighlight();
return;
}

const { draggedPos, selectedSlots } = resolveDragContext(
draggedPosition,
selectedPositions
);
const isGroupDrag =
selectedSlots.length > 1 &&
draggedPos !== null &&
selectedSlots.includes(draggedPos);

if (!isGroupDrag) {
data.highlightPositions = [targetPos];
return;
}

if (!isGroupMoveValid(draggedPos, targetPos, selectedSlots)) {
clearHighlight();
return;
}

const offset = targetPos - draggedPos;
const targetSlots = selectedSlots.map((slot) => slot + offset);
// Opcja A: podświetlamy tylko docelowe sloty spoza bieżącej selekcji.
data.highlightPositions = targetSlots.filter(
(slot) => !selectedSlots.includes(slot)
);
}, 0);
}

// ─── Walidacja granic grupowego przesunięcia ──────────────────────────────

/**
* Sprawdza, czy przesunięcie grupy klocków o `offset` pozycji jest legalne:
* żaden klocek nie wychodzi poza tablicę
* żaden klocek nie zawija" przez granicę wiersza
* - żaden klocek nie wychodzi poza tablicę
* - żaden klocek nie "zawija" przez granicę wiersza
*/
function isGroupMoveValid(
draggedOriginalPos: number,
Expand All @@ -86,7 +192,9 @@ export async function useEventGame(
const oldRow = Math.floor(pos / c);
const newRow = Math.floor(newPos / c);
const rowDiff = newRow - oldRow;
const expectedRowDiff = Math.floor((draggedOriginalPos + offset) / c) - Math.floor(draggedOriginalPos / c);
const expectedRowDiff =
Math.floor((draggedOriginalPos + offset) / c) -
Math.floor(draggedOriginalPos / c);
if (rowDiff !== expectedRowDiff) return false;
}
return true;
Expand All @@ -109,7 +217,7 @@ export async function useEventGame(
// Nie rób nic jeśli upuszczono na to samo miejsce
if (draggedOriginalPos === targetPos) return;

// Grupowy drag: więcej niż 1 zaznaczony I przeciągany klocek jest w zaznaczeniu
// Grupowy drag: więcej niż 1 zaznaczony i przeciągany klocek jest w zaznaczeniu
const isGroupDrag =
selectedPositions &&
selectedPositions.length > 1 &&
Expand All @@ -118,10 +226,12 @@ export async function useEventGame(
if (isGroupDrag && selectedPositions) {
onGroupSwap(draggedOriginalPos, targetPos, selectedPositions);
} else {
// Pojedynczy swap zawsze tylko dwa klocki zamieniają się miejscami
// Pojedynczy swap - zawsze tylko dwa klocki zamieniają się miejscami
clearSelection();
onSingleSwap(draggedOriginalPos, targetPos);
}

clearDragContext();
}

function onSingleSwap(pos1: number, pos2: number) {
Expand All @@ -142,7 +252,7 @@ export async function useEventGame(

// Posortuj według slotu aby siatka renderowała się w porządku
data.shuffledPieces = [...newPieces].sort((a, b) => a.position - b.position);
data.highlight = "";
clearHighlight();
data.moves = data.moves + 1;
clearSelection();
if (checkIfIsCorrect()) {
Expand All @@ -160,7 +270,7 @@ export async function useEventGame(
if (!pieces) return;

if (!isGroupMoveValid(draggedOriginalPos, targetPos, selectedSlots)) {
data.highlight = "";
clearHighlight();
return;
}

Expand All @@ -169,8 +279,9 @@ export async function useEventGame(
const targetSlots = selectedSlots.map((s) => s + offset);

// Klocki, które są w strefie docelowej ale NIE są częścią zaznaczenia
const displacedPieces = pieces.filter((p: ImagePieces) =>
targetSlots.includes(p.position) && !selectedSlots.includes(p.position)
const displacedPieces = pieces.filter(
(p: ImagePieces) =>
targetSlots.includes(p.position) && !selectedSlots.includes(p.position)
);

// Klocki na wolnych slotach źródłowych (zwolnione przez grupę)
Expand All @@ -179,26 +290,30 @@ export async function useEventGame(
);

// Przypisz przemieszczonym klockom wolne sloty źródłowe
const displacedMapping: DisplacedMapping[] = displacedPieces.map((p: ImagePieces, i: number) => ({
piece: p,
newSlot: freeSourceSlots[i],
}));
const displacedMapping: DisplacedMapping[] = displacedPieces.map(
(p: ImagePieces, i: number) => ({
piece: p,
newSlot: freeSourceSlots[i],
})
);

const newPieces = pieces.map((p: ImagePieces) => {
// Klocek z zaznaczonej grupy przesuń o offset
// Klocek z zaznaczonej grupy -> przesuń o offset
if (selectedSlots.includes(p.position)) {
return { ...p, position: p.position + offset };
}
// Przemieszczony klocek → dostaje wolny slot
const displaced = displacedMapping.find((d: DisplacedMapping) => d.piece.position === p.position);
// Przemieszczony klocek -> dostaje wolny slot
const displaced = displacedMapping.find(
(d: DisplacedMapping) => d.piece.position === p.position
);
if (displaced) {
return { ...p, position: displaced.newSlot };
}
return p;
});

data.shuffledPieces = [...newPieces].sort((a, b) => a.position - b.position);
data.highlight = "";
clearHighlight();
data.moves = data.moves + 1;
// Po ruchu grupowym aktualizuj selectedPositions do nowych slotów
data.selectedPositions = targetSlots;
Expand All @@ -215,8 +330,11 @@ export async function useEventGame(
stopStopwatch,
resetStopwatch,
onDragEnter,
onDragStart,
onDragEnd,
onSwap,
toggleSelection,
clearSelection,
clearHighlight,
};
}
3 changes: 1 addition & 2 deletions modules/game/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ export interface GameData {
isFinishedModalOpened: boolean;
isPauseModalOpened: boolean;
shuffledPieces: ImagePieces[] | undefined;
highlight: number | string;
highlightPositions: number[];
heightScreen: number;
moves: number;
selectedPositions: number[];
}

Loading
Loading