drag & drop
Browse files
src/lib/components/Pages/Pictuary.svelte
CHANGED
@@ -1,16 +1,19 @@
|
|
1 |
<script lang="ts">
|
2 |
import { onMount } from 'svelte';
|
3 |
import { getAllMonsters } from '$lib/db/monsters';
|
4 |
-
import { getAllPicletInstances, getRosterPiclets } from '$lib/db/piclets';
|
5 |
import type { Monster, PicletInstance } from '$lib/db/schema';
|
6 |
import PicletCard from '../Piclets/PicletCard.svelte';
|
7 |
import EmptySlotCard from '../Piclets/EmptySlotCard.svelte';
|
8 |
import DiscoveredCard from '../Piclets/DiscoveredCard.svelte';
|
|
|
|
|
9 |
|
10 |
let rosterPiclets: PicletInstance[] = $state([]);
|
11 |
let storagePiclets: PicletInstance[] = $state([]);
|
12 |
let discoveredMonsters: Monster[] = $state([]);
|
13 |
let isLoading = $state(true);
|
|
|
14 |
|
15 |
// Map roster positions for easy access
|
16 |
let rosterMap = $derived(() => {
|
@@ -23,20 +26,15 @@
|
|
23 |
return map;
|
24 |
});
|
25 |
|
26 |
-
|
27 |
try {
|
28 |
-
// Load all piclet instances
|
29 |
const allInstances = await getAllPicletInstances();
|
30 |
|
31 |
-
// Separate roster and storage
|
32 |
rosterPiclets = allInstances.filter(p => p.isInRoster);
|
33 |
storagePiclets = allInstances.filter(p => !p.isInRoster);
|
34 |
|
35 |
-
// Load all discovered monsters (for now, all generated monsters)
|
36 |
-
// In a real game, this would track which monsters have been encountered
|
37 |
const allMonsters = await getAllMonsters();
|
38 |
|
39 |
-
// Filter out monsters that have been caught (have instances)
|
40 |
const caughtTypeIds = new Set(allInstances.map(p => p.typeId));
|
41 |
discoveredMonsters = allMonsters.filter(m =>
|
42 |
!caughtTypeIds.has(m.name.toLowerCase().replace(/\s+/g, '-'))
|
@@ -46,28 +44,64 @@
|
|
46 |
} finally {
|
47 |
isLoading = false;
|
48 |
}
|
|
|
|
|
|
|
|
|
49 |
});
|
50 |
|
51 |
function handleRosterClick(position: number) {
|
52 |
const piclet = rosterMap().get(position);
|
53 |
if (piclet) {
|
54 |
-
// TODO: Navigate to piclet detail page
|
55 |
console.log('View piclet:', piclet);
|
56 |
} else {
|
57 |
-
// TODO: Show add to roster dialog
|
58 |
console.log('Add piclet to position:', position);
|
59 |
}
|
60 |
}
|
61 |
|
62 |
function handleStorageClick(piclet: PicletInstance) {
|
63 |
-
// TODO: Navigate to piclet detail page
|
64 |
console.log('View storage piclet:', piclet);
|
65 |
}
|
66 |
|
67 |
function handleDiscoveredClick(monster: Monster) {
|
68 |
-
// TODO: Navigate to discovered piclet detail page
|
69 |
console.log('View discovered monster:', monster);
|
70 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
71 |
</script>
|
72 |
|
73 |
<div class="pictuary-page">
|
@@ -99,18 +133,16 @@
|
|
99 |
<h2>Roster</h2>
|
100 |
<div class="roster-grid">
|
101 |
{#each Array(6) as _, position}
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
/>
|
113 |
-
{/if}
|
114 |
{/each}
|
115 |
</div>
|
116 |
</section>
|
@@ -126,10 +158,13 @@
|
|
126 |
</div>
|
127 |
<div class="horizontal-scroll">
|
128 |
{#each storagePiclets.slice(0, 10) as piclet}
|
129 |
-
<
|
130 |
instance={piclet}
|
131 |
size={100}
|
|
|
132 |
onClick={() => handleStorageClick(piclet)}
|
|
|
|
|
133 |
/>
|
134 |
{/each}
|
135 |
</div>
|
|
|
1 |
<script lang="ts">
|
2 |
import { onMount } from 'svelte';
|
3 |
import { getAllMonsters } from '$lib/db/monsters';
|
4 |
+
import { getAllPicletInstances, getRosterPiclets, moveToRoster, swapRosterPositions, moveToStorage } from '$lib/db/piclets';
|
5 |
import type { Monster, PicletInstance } from '$lib/db/schema';
|
6 |
import PicletCard from '../Piclets/PicletCard.svelte';
|
7 |
import EmptySlotCard from '../Piclets/EmptySlotCard.svelte';
|
8 |
import DiscoveredCard from '../Piclets/DiscoveredCard.svelte';
|
9 |
+
import DraggablePicletCard from '../Piclets/DraggablePicletCard.svelte';
|
10 |
+
import RosterSlot from '../Piclets/RosterSlot.svelte';
|
11 |
|
12 |
let rosterPiclets: PicletInstance[] = $state([]);
|
13 |
let storagePiclets: PicletInstance[] = $state([]);
|
14 |
let discoveredMonsters: Monster[] = $state([]);
|
15 |
let isLoading = $state(true);
|
16 |
+
let currentlyDragging: PicletInstance | null = $state(null);
|
17 |
|
18 |
// Map roster positions for easy access
|
19 |
let rosterMap = $derived(() => {
|
|
|
26 |
return map;
|
27 |
});
|
28 |
|
29 |
+
async function loadPiclets() {
|
30 |
try {
|
|
|
31 |
const allInstances = await getAllPicletInstances();
|
32 |
|
|
|
33 |
rosterPiclets = allInstances.filter(p => p.isInRoster);
|
34 |
storagePiclets = allInstances.filter(p => !p.isInRoster);
|
35 |
|
|
|
|
|
36 |
const allMonsters = await getAllMonsters();
|
37 |
|
|
|
38 |
const caughtTypeIds = new Set(allInstances.map(p => p.typeId));
|
39 |
discoveredMonsters = allMonsters.filter(m =>
|
40 |
!caughtTypeIds.has(m.name.toLowerCase().replace(/\s+/g, '-'))
|
|
|
44 |
} finally {
|
45 |
isLoading = false;
|
46 |
}
|
47 |
+
}
|
48 |
+
|
49 |
+
onMount(() => {
|
50 |
+
loadPiclets();
|
51 |
});
|
52 |
|
53 |
function handleRosterClick(position: number) {
|
54 |
const piclet = rosterMap().get(position);
|
55 |
if (piclet) {
|
|
|
56 |
console.log('View piclet:', piclet);
|
57 |
} else {
|
|
|
58 |
console.log('Add piclet to position:', position);
|
59 |
}
|
60 |
}
|
61 |
|
62 |
function handleStorageClick(piclet: PicletInstance) {
|
|
|
63 |
console.log('View storage piclet:', piclet);
|
64 |
}
|
65 |
|
66 |
function handleDiscoveredClick(monster: Monster) {
|
|
|
67 |
console.log('View discovered monster:', monster);
|
68 |
}
|
69 |
+
|
70 |
+
function handleDragStart(instance: PicletInstance) {
|
71 |
+
currentlyDragging = instance;
|
72 |
+
}
|
73 |
+
|
74 |
+
function handleDragEnd() {
|
75 |
+
currentlyDragging = null;
|
76 |
+
}
|
77 |
+
|
78 |
+
async function handleRosterDrop(position: number, dragData: any) {
|
79 |
+
if (!dragData.instanceId) return;
|
80 |
+
|
81 |
+
try {
|
82 |
+
const draggedPiclet = [...rosterPiclets, ...storagePiclets].find(p => p.id === dragData.instanceId);
|
83 |
+
if (!draggedPiclet) return;
|
84 |
+
|
85 |
+
const targetPiclet = rosterMap().get(position);
|
86 |
+
|
87 |
+
if (dragData.fromRoster && targetPiclet) {
|
88 |
+
// Swap two roster positions
|
89 |
+
await swapRosterPositions(
|
90 |
+
dragData.instanceId,
|
91 |
+
dragData.fromPosition,
|
92 |
+
targetPiclet.id!,
|
93 |
+
position
|
94 |
+
);
|
95 |
+
} else {
|
96 |
+
// Move to roster (possibly replacing existing)
|
97 |
+
await moveToRoster(dragData.instanceId, position);
|
98 |
+
}
|
99 |
+
|
100 |
+
await loadPiclets();
|
101 |
+
} catch (err) {
|
102 |
+
console.error('Failed to handle drop:', err);
|
103 |
+
}
|
104 |
+
}
|
105 |
</script>
|
106 |
|
107 |
<div class="pictuary-page">
|
|
|
133 |
<h2>Roster</h2>
|
134 |
<div class="roster-grid">
|
135 |
{#each Array(6) as _, position}
|
136 |
+
<RosterSlot
|
137 |
+
{position}
|
138 |
+
piclet={rosterMap().get(position)}
|
139 |
+
size={100}
|
140 |
+
onDrop={handleRosterDrop}
|
141 |
+
onPicletClick={(piclet) => handleRosterClick(position)}
|
142 |
+
onEmptyClick={handleRosterClick}
|
143 |
+
onDragStart={handleDragStart}
|
144 |
+
onDragEnd={handleDragEnd}
|
145 |
+
/>
|
|
|
|
|
146 |
{/each}
|
147 |
</div>
|
148 |
</section>
|
|
|
158 |
</div>
|
159 |
<div class="horizontal-scroll">
|
160 |
{#each storagePiclets.slice(0, 10) as piclet}
|
161 |
+
<DraggablePicletCard
|
162 |
instance={piclet}
|
163 |
size={100}
|
164 |
+
showDetails={true}
|
165 |
onClick={() => handleStorageClick(piclet)}
|
166 |
+
onDragStart={handleDragStart}
|
167 |
+
onDragEnd={handleDragEnd}
|
168 |
/>
|
169 |
{/each}
|
170 |
</div>
|
src/lib/components/Piclets/DraggablePicletCard.svelte
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import type { PicletInstance } from '$lib/db/schema';
|
3 |
+
import PicletCard from './PicletCard.svelte';
|
4 |
+
|
5 |
+
interface Props {
|
6 |
+
instance: PicletInstance;
|
7 |
+
size?: number;
|
8 |
+
showDetails?: boolean;
|
9 |
+
onDragStart?: (instance: PicletInstance) => void;
|
10 |
+
onDragEnd?: () => void;
|
11 |
+
onClick?: () => void;
|
12 |
+
}
|
13 |
+
|
14 |
+
let { instance, size = 100, showDetails = true, onDragStart, onDragEnd, onClick }: Props = $props();
|
15 |
+
|
16 |
+
let isDragging = $state(false);
|
17 |
+
|
18 |
+
function handleDragStart(e: DragEvent) {
|
19 |
+
isDragging = true;
|
20 |
+
|
21 |
+
// Set drag data
|
22 |
+
e.dataTransfer!.effectAllowed = 'move';
|
23 |
+
e.dataTransfer!.setData('application/json', JSON.stringify({
|
24 |
+
instanceId: instance.id,
|
25 |
+
fromRoster: instance.isInRoster,
|
26 |
+
fromPosition: instance.rosterPosition
|
27 |
+
}));
|
28 |
+
|
29 |
+
// Create a custom drag image
|
30 |
+
const dragImage = e.currentTarget as HTMLElement;
|
31 |
+
const clone = dragImage.cloneNode(true) as HTMLElement;
|
32 |
+
clone.style.transform = 'scale(1.1)';
|
33 |
+
clone.style.opacity = '0.8';
|
34 |
+
document.body.appendChild(clone);
|
35 |
+
e.dataTransfer!.setDragImage(clone, size / 2, size / 2);
|
36 |
+
setTimeout(() => document.body.removeChild(clone), 0);
|
37 |
+
|
38 |
+
onDragStart?.(instance);
|
39 |
+
}
|
40 |
+
|
41 |
+
function handleDragEnd() {
|
42 |
+
isDragging = false;
|
43 |
+
onDragEnd?.();
|
44 |
+
}
|
45 |
+
</script>
|
46 |
+
|
47 |
+
<div
|
48 |
+
draggable="true"
|
49 |
+
ondragstart={handleDragStart}
|
50 |
+
ondragend={handleDragEnd}
|
51 |
+
class="draggable-wrapper"
|
52 |
+
class:dragging={isDragging}
|
53 |
+
>
|
54 |
+
<PicletCard {instance} {size} {showDetails} {onClick} />
|
55 |
+
</div>
|
56 |
+
|
57 |
+
<style>
|
58 |
+
.draggable-wrapper {
|
59 |
+
cursor: move;
|
60 |
+
}
|
61 |
+
|
62 |
+
.draggable-wrapper.dragging {
|
63 |
+
opacity: 0.5;
|
64 |
+
}
|
65 |
+
|
66 |
+
.draggable-wrapper.dragging :global(.piclet-card) {
|
67 |
+
transform: scale(0.9);
|
68 |
+
}
|
69 |
+
</style>
|
src/lib/components/Piclets/EmptySlotCard.svelte
CHANGED
@@ -11,7 +11,7 @@
|
|
11 |
<button
|
12 |
class="empty-slot"
|
13 |
class:highlighted={isHighlighted}
|
14 |
-
style="width: {size}px; height: {size}px;"
|
15 |
onclick={onClick}
|
16 |
type="button"
|
17 |
>
|
|
|
11 |
<button
|
12 |
class="empty-slot"
|
13 |
class:highlighted={isHighlighted}
|
14 |
+
style="width: {size}px; height: {size + 30}px;"
|
15 |
onclick={onClick}
|
16 |
type="button"
|
17 |
>
|
src/lib/components/Piclets/RosterSlot.svelte
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import type { PicletInstance } from '$lib/db/schema';
|
3 |
+
import DraggablePicletCard from './DraggablePicletCard.svelte';
|
4 |
+
import EmptySlotCard from './EmptySlotCard.svelte';
|
5 |
+
|
6 |
+
interface Props {
|
7 |
+
position: number;
|
8 |
+
piclet?: PicletInstance;
|
9 |
+
size?: number;
|
10 |
+
onDrop?: (position: number, dragData: any) => void;
|
11 |
+
onPicletClick?: (piclet: PicletInstance) => void;
|
12 |
+
onEmptyClick?: (position: number) => void;
|
13 |
+
onDragStart?: (instance: PicletInstance) => void;
|
14 |
+
onDragEnd?: () => void;
|
15 |
+
}
|
16 |
+
|
17 |
+
let {
|
18 |
+
position,
|
19 |
+
piclet,
|
20 |
+
size = 100,
|
21 |
+
onDrop,
|
22 |
+
onPicletClick,
|
23 |
+
onEmptyClick,
|
24 |
+
onDragStart,
|
25 |
+
onDragEnd
|
26 |
+
}: Props = $props();
|
27 |
+
|
28 |
+
let isDragOver = $state(false);
|
29 |
+
let canAcceptDrop = $state(false);
|
30 |
+
|
31 |
+
function handleDragOver(e: DragEvent) {
|
32 |
+
e.preventDefault();
|
33 |
+
|
34 |
+
// Check if we can accept the drop
|
35 |
+
const data = e.dataTransfer?.getData('application/json');
|
36 |
+
if (data) {
|
37 |
+
isDragOver = true;
|
38 |
+
canAcceptDrop = true;
|
39 |
+
e.dataTransfer!.dropEffect = 'move';
|
40 |
+
}
|
41 |
+
}
|
42 |
+
|
43 |
+
function handleDragLeave() {
|
44 |
+
isDragOver = false;
|
45 |
+
canAcceptDrop = false;
|
46 |
+
}
|
47 |
+
|
48 |
+
function handleDrop(e: DragEvent) {
|
49 |
+
e.preventDefault();
|
50 |
+
isDragOver = false;
|
51 |
+
canAcceptDrop = false;
|
52 |
+
|
53 |
+
const data = e.dataTransfer?.getData('application/json');
|
54 |
+
if (data) {
|
55 |
+
const dragData = JSON.parse(data);
|
56 |
+
onDrop?.(position, dragData);
|
57 |
+
}
|
58 |
+
}
|
59 |
+
|
60 |
+
function handleClick() {
|
61 |
+
if (piclet) {
|
62 |
+
onPicletClick?.(piclet);
|
63 |
+
} else {
|
64 |
+
onEmptyClick?.(position);
|
65 |
+
}
|
66 |
+
}
|
67 |
+
</script>
|
68 |
+
|
69 |
+
<div
|
70 |
+
class="roster-slot"
|
71 |
+
ondragover={handleDragOver}
|
72 |
+
ondragleave={handleDragLeave}
|
73 |
+
ondrop={handleDrop}
|
74 |
+
class:drag-over={isDragOver}
|
75 |
+
>
|
76 |
+
{#if piclet}
|
77 |
+
<DraggablePicletCard
|
78 |
+
instance={piclet}
|
79 |
+
{size}
|
80 |
+
showDetails={false}
|
81 |
+
onClick={handleClick}
|
82 |
+
{onDragStart}
|
83 |
+
{onDragEnd}
|
84 |
+
/>
|
85 |
+
{:else}
|
86 |
+
<EmptySlotCard
|
87 |
+
{size}
|
88 |
+
isHighlighted={isDragOver && canAcceptDrop}
|
89 |
+
onClick={handleClick}
|
90 |
+
/>
|
91 |
+
{/if}
|
92 |
+
</div>
|
93 |
+
|
94 |
+
<style>
|
95 |
+
.roster-slot {
|
96 |
+
position: relative;
|
97 |
+
}
|
98 |
+
|
99 |
+
.roster-slot.drag-over::after {
|
100 |
+
content: '';
|
101 |
+
position: absolute;
|
102 |
+
inset: -4px;
|
103 |
+
border: 2px solid #007bff;
|
104 |
+
border-radius: 16px;
|
105 |
+
background: rgba(0, 123, 255, 0.1);
|
106 |
+
pointer-events: none;
|
107 |
+
}
|
108 |
+
</style>
|