@mhmttosun/dnd 0.0.0-stage → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1272 @@
1
+ /**
2
+ * ActivityEngine - Etkinlik Oynatıcı Motoru.
3
+ * Hem editör içi Önizleme (Preview) modunda hem de bağımsız dışa aktarılan
4
+ * HTML dosyasında çalışan gerçek zamanlı öğrenci deneyimi motorudur.
5
+ *
6
+ * Dört etkinlik türünü eksiksiz destekler:
7
+ * 1. Görsel Üzerinde Etiketleme
8
+ * 2. Sürükle-Bırak Eşleştirme
9
+ * 3. Kategorilere Ayırma
10
+ * 4. Sürükleyerek Sıralama
11
+ */
12
+ class ActivityEngine {
13
+ constructor(containerEl, projectData, options = {}) {
14
+ this.container = containerEl;
15
+ this.projectData = JSON.parse(JSON.stringify(projectData)); // Orijinal veriyi korumak için derin kopya
16
+ this.options = Object.assign({
17
+ isEmbedded: false,
18
+ stagePadding: null,
19
+ onComplete: null,
20
+ onExit: null,
21
+ onStep: null,
22
+ onDrop: null,
23
+ onReset: null
24
+ }, options);
25
+
26
+ this.eventListeners = {};
27
+ this.audio = new window.DND.AudioSynthesizer();
28
+ this.audio.setEnabled(this.projectData.settings?.soundEnabled !== false);
29
+
30
+ // Oyun durumları
31
+ this.placedMap = {}; // { [draggableId]: targetId }
32
+ this.originPositions = {}; // { [draggableId]: { x, y } }
33
+ this.score = 0;
34
+ this.attempts = 0;
35
+ this.correctCount = 0;
36
+ this.isCompleted = false;
37
+
38
+ // Etkinlik Modu: 'learning' (Öğrenme - Anlık) veya 'assessment' (Değerlendirme - Toplu Kontrol)
39
+ this.playMode = this.options.playMode || this.projectData.settings?.playMode || 'learning';
40
+ this.isRetryMode = false;
41
+ this.lockedTargetIds = new Set();
42
+
43
+ // Sürükleme değişkenleri
44
+ this.activeDragId = null;
45
+ this.dragStartPos = { x: 0, y: 0 };
46
+ this.dragElemStart = { x: 0, y: 0 };
47
+ this.activePointerId = null;
48
+ this.currentScale = 1.0;
49
+
50
+ // Klavye erişilebilirliği için seçili nesne
51
+ this.keyboardSelectedId = null;
52
+
53
+ // Geri bildirim durumu ve zamanlayıcısı
54
+ this.feedbackTimeout = null;
55
+ this.currentFeedback = null;
56
+
57
+ // Yaşam döngüsü dinleyici referansları (Bellek sızıntılarını ve duplicate listener'ları önler)
58
+ this.onResizeBound = () => this.updateScale();
59
+ this.onPointerMoveBound = (e) => this.handlePointerMove(e);
60
+ this.onPointerUpBound = (e) => this.handlePointerUp(e);
61
+ this.onPointerCancelBound = (e) => this.handlePointerCancel(e);
62
+ this.onKeyDownBound = (e) => this.handleKeyDown(e);
63
+
64
+ this.init();
65
+ }
66
+
67
+ init() {
68
+ this.renderGameLayout();
69
+ this.setupResizeHandler();
70
+ this.setupPointerEvents();
71
+ this.setupKeyboardEvents();
72
+ this.prepareItems();
73
+ }
74
+
75
+ // Yaşam döngüsü temizliği: SPA, React, Vue unmount durumlarında tüm dinleyicileri ve sesi temizler
76
+ destroy() {
77
+ if (this.resizeObserver) {
78
+ this.resizeObserver.disconnect();
79
+ this.resizeObserver = null;
80
+ }
81
+ if (this.feedbackTimeout) {
82
+ clearTimeout(this.feedbackTimeout);
83
+ this.feedbackTimeout = null;
84
+ }
85
+ if (this.onResizeBound) {
86
+ window.removeEventListener('resize', this.onResizeBound);
87
+ }
88
+ if (this.onPointerMoveBound) {
89
+ window.removeEventListener('pointermove', this.onPointerMoveBound);
90
+ }
91
+ if (this.onPointerUpBound) {
92
+ window.removeEventListener('pointerup', this.onPointerUpBound);
93
+ }
94
+ if (this.onPointerCancelBound) {
95
+ window.removeEventListener('pointercancel', this.onPointerCancelBound);
96
+ }
97
+ if (this.container && this.onKeyDownBound) {
98
+ this.container.removeEventListener('keydown', this.onKeyDownBound);
99
+ }
100
+ if (this.audio && typeof this.audio.close === 'function') {
101
+ this.audio.close();
102
+ }
103
+ this.emit('destroy');
104
+ this.eventListeners = {};
105
+ if (this.container) {
106
+ this.container.innerHTML = '';
107
+ }
108
+ this.isDestroyed = true;
109
+ }
110
+
111
+ // Olay Dinleme ve Yayınlama (Event Emitter API)
112
+ on(event, callback) {
113
+ if (!this.eventListeners[event]) this.eventListeners[event] = [];
114
+ this.eventListeners[event].push(callback);
115
+ return () => this.off(event, callback);
116
+ }
117
+
118
+ off(event, callback) {
119
+ if (!this.eventListeners || !this.eventListeners[event]) return;
120
+ this.eventListeners[event] = this.eventListeners[event].filter(cb => cb !== callback);
121
+ }
122
+
123
+ emit(event, data) {
124
+ const handlerName = 'on' + event.charAt(0).toUpperCase() + event.slice(1);
125
+ if (this.options && typeof this.options[handlerName] === 'function') {
126
+ try {
127
+ this.options[handlerName](data);
128
+ } catch (err) {
129
+ console.error(`[ActivityEngine] Options callback error in ${event}:`, err);
130
+ }
131
+ }
132
+ if (this.eventListeners && this.eventListeners[event]) {
133
+ this.eventListeners[event].forEach(cb => {
134
+ try {
135
+ cb(data);
136
+ } catch (err) {
137
+ console.error(`[ActivityEngine] Event listener error in ${event}:`, err);
138
+ }
139
+ });
140
+ }
141
+ }
142
+
143
+ // Anlık etkinlik durumunu döner
144
+ getState() {
145
+ const accuracy = this.totalDraggables > 0
146
+ ? Math.round((this.correctCount / this.totalDraggables) * 100)
147
+ : 0;
148
+ return {
149
+ isCompleted: this.isCompleted,
150
+ score: accuracy,
151
+ attempts: this.attempts,
152
+ correctCount: this.correctCount,
153
+ totalDraggables: this.totalDraggables,
154
+ placedMap: Object.assign({}, this.placedMap)
155
+ };
156
+ }
157
+
158
+ setState(value) {
159
+ if (!value || typeof value !== 'object' || !value.placedMap || typeof value.placedMap !== 'object') {
160
+ throw new TypeError('Geçersiz DND oynatıcı durumu.');
161
+ }
162
+ const objects = new Map((this.projectData.objects || []).map(object => [object.id, object]));
163
+ const nextPlaced = {};
164
+ for (const [dragId, targetId] of Object.entries(value.placedMap)) {
165
+ const drag = objects.get(dragId);
166
+ const target = objects.get(targetId);
167
+ if (drag?.role !== 'draggable' || !['target', 'category', 'sequence_slot'].includes(target?.role)) {
168
+ throw new TypeError('Oynatıcı durumu bilinmeyen bir nesne içeriyor.');
169
+ }
170
+ nextPlaced[dragId] = targetId;
171
+ }
172
+ this.prepareItems();
173
+ this.placedMap = nextPlaced;
174
+ this.attempts = Number.isSafeInteger(value.attempts) && value.attempts >= 0 ? value.attempts : 0;
175
+ this.correctCount = 0;
176
+ for (const [dragId, targetId] of Object.entries(nextPlaced)) {
177
+ const target = objects.get(targetId);
178
+ this.snapToTarget(dragId, target);
179
+ const isCorrect = this.projectData.answerKey?.[dragId] === targetId;
180
+ const dragEl = this.container.querySelector(`[data-id="${dragId}"]`);
181
+ if (isCorrect) {
182
+ this.correctCount++;
183
+ dragEl?.classList.add('is-correct');
184
+ if (this.projectData.settings?.correctAnswerBehavior === 'lock') {
185
+ dragEl?.classList.add('is-locked');
186
+ dragEl?.setAttribute('tabindex', '-1');
187
+ }
188
+ }
189
+ }
190
+ this.isCompleted = this.totalDraggables > 0 && this.correctCount === this.totalDraggables && value.isCompleted === true;
191
+ this.setCompletionResultVisible(this.isCompleted);
192
+ this.updateStats();
193
+ return this.getState();
194
+ }
195
+
196
+ renderGameLayout() {
197
+ this.container.innerHTML = `
198
+ <div class="player-wrapper">
199
+ ${this.options.isEmbedded ? `
200
+ <header class="player-header player-header-minimal">
201
+ <button class="player-btn exit-btn" id="btn-exit-preview" title="Önizlemeden Çık ve Editöre Dön">
202
+ ✕ Editöre Dön
203
+ </button>
204
+ </header>
205
+ ` : ''}
206
+
207
+ <main class="player-viewport" id="player-viewport">
208
+ <div class="player-stage-container" id="player-stage-container">
209
+ <div class="player-stage-board" id="player-stage-board">
210
+ <div class="player-stage-scaler" id="player-stage-scaler">
211
+ <div class="player-stage" id="player-stage">
212
+ <div class="player-targets-layer" id="player-targets"></div>
213
+ <div class="player-draggables-layer" id="player-draggables"></div>
214
+ </div>
215
+ </div>
216
+
217
+ <!-- Kontrol Alanı (Açık Zemin, Hiyerarşik Butonlar) -->
218
+ <div class="player-controls" id="player-controls">
219
+ <div class="player-mode-switch" role="group" aria-label="Etkinlik Modu">
220
+ <button type="button" class="mode-btn ${this.playMode === 'learning' ? 'active' : ''}" id="btn-mode-learning" data-mode="learning" title="Öğrenme Modu (Anlık Geri Bildirim)">
221
+ Öğrenme
222
+ </button>
223
+ <button type="button" class="mode-btn ${this.playMode === 'assessment' ? 'active' : ''}" id="btn-mode-assessment" data-mode="assessment" title="Değerlendirme Modu (Toplu Kontrol)">
224
+ Değerlendirme
225
+ </button>
226
+ </div>
227
+
228
+ <div class="player-actions">
229
+ <button class="player-btn reset-btn secondary-btn" id="btn-reset-game" title="Yeniden Başlat">
230
+ <span class="btn-icon">↺</span> Sıfırla
231
+ </button>
232
+ <button class="player-btn primary-btn check-answers-btn" id="btn-check-answers" title="Cevapları Kontrol Et" style="${this.playMode === 'learning' ? 'display: none;' : 'display: inline-flex;'}">
233
+ ✓ Kontrol Et
234
+ </button>
235
+ </div>
236
+ </div>
237
+ </div>
238
+
239
+ <!-- Tamamlanma Sonuç Kartı (Sahneyi kapatmadan normal akışta gösterilir) -->
240
+ <section class="completion-result-card" id="completion-result" aria-labelledby="completion-result-title" aria-live="polite" hidden>
241
+ <div class="completion-result-message">
242
+ <span class="completion-result-icon" aria-hidden="true">
243
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
244
+ <circle cx="12" cy="12" r="9"></circle>
245
+ <path d="m8 12 2.5 2.5L16 9"></path>
246
+ </svg>
247
+ </span>
248
+ <h2 class="completion-result-title" id="completion-result-title">Harika, hepsi doğru!</h2>
249
+ </div>
250
+ <button type="button" class="completion-replay-btn" id="btn-play-again">
251
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
252
+ <path d="M3 12a9 9 0 1 0 3-6.7L3 8"></path>
253
+ <path d="M3 3v5h5"></path>
254
+ </svg>
255
+ <span>Tekrar Oyna</span>
256
+ </button>
257
+ </section>
258
+
259
+ <!-- Alt Yardımcı İpucu Metni (Ayrı koyu şerit değil, sade yardımcı metin) -->
260
+ <footer class="player-footer" id="player-footer">
261
+ <div class="player-hint" id="player-hint">
262
+ 💡 Öğeleri sürükleyerek doğru hedeflerle eşleştirin.
263
+ </div>
264
+ </footer>
265
+ </div>
266
+ </main>
267
+ </div>
268
+ `;
269
+
270
+ // Buton dinleyicileri
271
+ const modeLearningBtn = this.container.querySelector('#btn-mode-learning');
272
+ if (modeLearningBtn) {
273
+ modeLearningBtn.onclick = () => this.setPlayMode('learning');
274
+ }
275
+
276
+ const modeAssessmentBtn = this.container.querySelector('#btn-mode-assessment');
277
+ if (modeAssessmentBtn) {
278
+ modeAssessmentBtn.onclick = () => this.setPlayMode('assessment');
279
+ }
280
+
281
+ const resetBtn = this.container.querySelector('#btn-reset-game');
282
+ if (resetBtn) resetBtn.onclick = () => this.resetGame();
283
+
284
+ const exitBtn = this.container.querySelector('#btn-exit-preview');
285
+ if (exitBtn) {
286
+ exitBtn.onclick = () => {
287
+ if (typeof this.options.onExit === 'function') this.options.onExit();
288
+ };
289
+ }
290
+
291
+ const checkBtn = this.container.querySelector('#btn-check-answers');
292
+ if (checkBtn) {
293
+ checkBtn.onclick = () => {
294
+ if (this.isRetryMode) {
295
+ this.retryWrongAnswers();
296
+ } else {
297
+ this.checkAllAnswers();
298
+ }
299
+ };
300
+ }
301
+
302
+ const playAgainBtn = this.container.querySelector('#btn-play-again');
303
+ if (playAgainBtn) playAgainBtn.onclick = () => this.resetGame();
304
+ }
305
+
306
+ getActivityTypeLabel() {
307
+ const type = this.projectData.project.activityType;
308
+ if (type === 'image-labeling') return 'Görsel Etiketleme';
309
+ if (type === 'matching') return 'Eşleştirme';
310
+ if (type === 'categorization') return 'Kategorilere Ayırma';
311
+ if (type === 'sequencing') return 'Sıralama';
312
+ return 'Etkinlik';
313
+ }
314
+
315
+ getPlayMode() {
316
+ return this.playMode;
317
+ }
318
+
319
+ setPlayMode(mode) {
320
+ if (mode !== 'learning' && mode !== 'assessment') return;
321
+ if (this.playMode === mode) return;
322
+ this.playMode = mode;
323
+ this.resetGame();
324
+ this.updateModeUI();
325
+ this.emit('playModeChange', { mode: this.playMode });
326
+ }
327
+
328
+ addStatusBadge(dragEl, type) {
329
+ if (!dragEl) return;
330
+ this.removeStatusBadge(dragEl);
331
+ const badge = document.createElement('span');
332
+ badge.className = `player-status-badge badge-${type}`;
333
+ badge.textContent = type === 'correct' ? '✓' : '✕';
334
+ dragEl.appendChild(badge);
335
+ }
336
+
337
+ removeStatusBadge(dragEl) {
338
+ if (!dragEl) return;
339
+ const badge = dragEl.querySelector('.player-status-badge');
340
+ if (badge) badge.remove();
341
+ }
342
+
343
+ updateModeUI() {
344
+ const learnBtn = this.container.querySelector('#btn-mode-learning');
345
+ const assessBtn = this.container.querySelector('#btn-mode-assessment');
346
+ const checkBtn = this.container.querySelector('#btn-check-answers');
347
+
348
+ if (learnBtn) learnBtn.classList.toggle('active', this.playMode === 'learning');
349
+ if (assessBtn) assessBtn.classList.toggle('active', this.playMode === 'assessment');
350
+
351
+ if (checkBtn) {
352
+ if (this.playMode === 'learning') {
353
+ checkBtn.style.display = 'none';
354
+ } else {
355
+ checkBtn.style.display = 'inline-flex';
356
+ this.setCheckButtonState('check');
357
+ }
358
+ }
359
+ }
360
+
361
+ setCheckButtonState(state) {
362
+ const checkBtn = this.container.querySelector('#btn-check-answers');
363
+ if (!checkBtn) return;
364
+
365
+ if (state === 'retry') {
366
+ this.isRetryMode = true;
367
+ checkBtn.textContent = '🔄 Tekrar Dene';
368
+ checkBtn.title = 'Yanlış seçenekleri geri gönderip tekrar deneyin';
369
+ checkBtn.className = 'player-btn retry-btn';
370
+ } else {
371
+ this.isRetryMode = false;
372
+ checkBtn.textContent = '✓ Kontrol Et';
373
+ checkBtn.title = 'Cevapları Kontrol Et';
374
+ checkBtn.className = 'player-btn primary-btn check-answers-btn';
375
+ }
376
+ }
377
+
378
+ setupResizeHandler() {
379
+ this.stageEl = this.container.querySelector('#player-stage');
380
+ this.stageScalerEl = this.container.querySelector('#player-stage-scaler');
381
+ this.stageBoardEl = this.container.querySelector('#player-stage-board');
382
+ this.completionResultEl = this.container.querySelector('#completion-result');
383
+ this.viewportEl = this.container.querySelector('#player-viewport');
384
+ this.stageContainerEl = this.container.querySelector('#player-stage-container') || this.viewportEl;
385
+
386
+ this.onResizeBound = () => this.updateScale();
387
+ window.addEventListener('resize', this.onResizeBound);
388
+
389
+ if (typeof window !== 'undefined' && window.ResizeObserver && this.stageContainerEl) {
390
+ this.resizeObserver = new window.ResizeObserver(() => {
391
+ this.updateScale();
392
+ });
393
+ this.resizeObserver.observe(this.stageContainerEl);
394
+ }
395
+
396
+ this.updateScale();
397
+ if (typeof requestAnimationFrame !== 'undefined') {
398
+ requestAnimationFrame(() => this.updateScale());
399
+ }
400
+ setTimeout(() => this.updateScale(), 30);
401
+ }
402
+
403
+ updateScale() {
404
+ if (!this.stageEl || !this.viewportEl) return;
405
+
406
+ const stageW = (this.projectData.stage && this.projectData.stage.width) || 1200;
407
+ const stageH = (this.projectData.stage && this.projectData.stage.height) || 675;
408
+
409
+ this.stageEl.style.width = `${stageW}px`;
410
+ this.stageEl.style.height = `${stageH}px`;
411
+
412
+ // Arka plan stilleri
413
+ this.stageEl.style.backgroundColor = (this.projectData.stage && this.projectData.stage.backgroundColor) || '#ffffff';
414
+ if (this.projectData.stage && this.projectData.stage.backgroundImage) {
415
+ this.stageEl.style.backgroundImage = `url("${this.projectData.stage.backgroundImage}")`;
416
+ this.stageEl.style.backgroundSize = 'contain';
417
+ this.stageEl.style.backgroundPosition = 'center';
418
+ this.stageEl.style.backgroundRepeat = 'no-repeat';
419
+ }
420
+
421
+ const targetContainer = this.stageContainerEl || this.viewportEl;
422
+ const vpRect = targetContainer.getBoundingClientRect();
423
+ if (vpRect.width === 0 || vpRect.height === 0) {
424
+ if (typeof requestAnimationFrame !== 'undefined') {
425
+ requestAnimationFrame(() => this.updateScale());
426
+ }
427
+ return;
428
+ }
429
+
430
+ const requestedPadding = this.options.stagePadding;
431
+ const numericPadding = Number(requestedPadding);
432
+ const pad = requestedPadding !== null
433
+ && requestedPadding !== undefined
434
+ && Number.isFinite(numericPadding)
435
+ && numericPadding >= 0
436
+ ? numericPadding
437
+ : (vpRect.width < 600 ? 16 : 32);
438
+ // player-controls (~48px) + player-footer (~36px) = ~84px dikey alan payı
439
+ const extraH = 88;
440
+ const availW = Math.max(vpRect.width - pad, 60);
441
+ const availH = Math.max(vpRect.height - pad - extraH, 60);
442
+
443
+ this.currentScale = Math.min(availW / stageW, availH / stageH);
444
+ const scaledW = Math.round(stageW * this.currentScale);
445
+ const scaledH = Math.round(stageH * this.currentScale);
446
+
447
+ if (this.stageScalerEl) {
448
+ this.stageScalerEl.style.width = `${scaledW}px`;
449
+ this.stageScalerEl.style.height = `${scaledH}px`;
450
+ }
451
+
452
+ if (this.stageBoardEl) {
453
+ this.stageBoardEl.style.width = `${scaledW}px`;
454
+ }
455
+
456
+ if (this.completionResultEl) {
457
+ this.completionResultEl.style.width = `${scaledW}px`;
458
+ }
459
+
460
+ this.stageEl.style.transform = `scale(${this.currentScale})`;
461
+ this.stageEl.style.transformOrigin = 'top left';
462
+ }
463
+
464
+ clientToStage(clientX, clientY) {
465
+ const rect = this.stageEl.getBoundingClientRect();
466
+ const stageW = this.projectData.stage.width || 1200;
467
+ const scale = rect.width / stageW;
468
+ return {
469
+ x: (clientX - rect.left) / scale,
470
+ y: (clientY - rect.top) / scale
471
+ };
472
+ }
473
+
474
+ prepareItems() {
475
+ const targetsLayer = this.container.querySelector('#player-targets');
476
+ const draggablesLayer = this.container.querySelector('#player-draggables');
477
+ targetsLayer.innerHTML = '';
478
+ draggablesLayer.innerHTML = '';
479
+
480
+ const objects = this.projectData.objects || [];
481
+ const draggables = objects.filter(o => o.role === 'draggable');
482
+ const nonDraggables = objects.filter(o => o.role !== 'draggable');
483
+
484
+ this.totalDraggables = draggables.length;
485
+ const statTotal = this.container.querySelector('#stat-total');
486
+ if (statTotal) statTotal.textContent = this.totalDraggables;
487
+ const statCorrect = this.container.querySelector('#stat-correct');
488
+ if (statCorrect) statCorrect.textContent = '0';
489
+ const statAttempts = this.container.querySelector('#stat-attempts');
490
+ if (statAttempts) statAttempts.textContent = '0';
491
+
492
+ // Sabit, hedef ve dekoratif nesneleri çiz
493
+ nonDraggables.forEach(obj => {
494
+ const el = this.createDOMObject(obj, false);
495
+ targetsLayer.appendChild(el);
496
+ });
497
+
498
+ // Eğer ayarlarda karıştırma aktifse sürüklenebilir nesnelerin konumlarını karıştır
499
+ let renderDraggables = [...draggables];
500
+ if (this.projectData.settings?.shuffleDraggables) {
501
+ const positions = draggables.map(d => ({ x: d.x, y: d.y }));
502
+ // Fisher-Yates karıştırma
503
+ for (let i = positions.length - 1; i > 0; i--) {
504
+ const j = Math.floor(Math.random() * (i + 1));
505
+ [positions[i], positions[j]] = [positions[j], positions[i]];
506
+ }
507
+ renderDraggables.forEach((d, idx) => {
508
+ d.currentX = positions[idx].x;
509
+ d.currentY = positions[idx].y;
510
+ });
511
+ } else {
512
+ renderDraggables.forEach(d => {
513
+ d.currentX = d.x;
514
+ d.currentY = d.y;
515
+ });
516
+ }
517
+
518
+ // Başlangıç konumlarını karıştırma sonrası ekrandaki gerçek yerleriyle kaydet
519
+ renderDraggables.forEach(d => {
520
+ this.originPositions[d.id] = { x: d.currentX, y: d.currentY };
521
+ });
522
+
523
+ // Sürüklenebilir nesneleri çiz
524
+ renderDraggables.forEach(obj => {
525
+ const el = this.createDOMObject(obj, true);
526
+ draggablesLayer.appendChild(el);
527
+ });
528
+
529
+ // Değerlendirme modu UI ayarı
530
+ this.updateModeUI();
531
+ }
532
+
533
+ createDOMObject(obj, isDraggable = false) {
534
+ const el = document.createElement('div');
535
+ el.className = `player-object ${isDraggable ? 'is-draggable' : 'is-target'}`;
536
+ el.setAttribute('data-id', obj.id);
537
+ el.setAttribute('data-role', obj.role);
538
+ el.setAttribute('tabindex', isDraggable ? '0' : '-1');
539
+
540
+ const posX = isDraggable && obj.currentX !== undefined ? obj.currentX : obj.x;
541
+ const posY = isDraggable && obj.currentY !== undefined ? obj.currentY : obj.y;
542
+
543
+ el.style.width = `${obj.width}px`;
544
+ el.style.height = `${obj.height}px`;
545
+ el.style.transform = `translate(${posX}px, ${posY}px) rotate(${obj.rotation || 0}deg)`;
546
+ el.style.zIndex = isDraggable ? (obj.zIndex || 10) + 10 : (obj.zIndex || 1);
547
+
548
+ // Stiller
549
+ el.style.backgroundColor = obj.backgroundColor || 'transparent';
550
+ el.style.borderColor = obj.borderColor || 'transparent';
551
+ el.style.borderWidth = `${obj.borderWidth || 0}px`;
552
+ el.style.borderStyle = obj.borderStyle || 'solid';
553
+ el.style.borderRadius = `${obj.borderRadius || 0}px`;
554
+ el.style.boxShadow = obj.boxShadow || 'none';
555
+
556
+ // İçerik
557
+ const content = document.createElement('div');
558
+ content.className = 'player-object-content';
559
+ content.style.fontFamily = obj.fontFamily || 'sans-serif';
560
+ content.style.fontSize = `${obj.fontSize || 16}px`;
561
+ content.style.fontWeight = obj.fontWeight || 'normal';
562
+ content.style.color = obj.textColor || '#1e293b';
563
+ content.style.padding = `${obj.padding !== undefined ? obj.padding : 8}px`;
564
+
565
+ // Dikey hizalama (Top / Middle / Bottom)
566
+ if (obj.verticalAlign === 'top') {
567
+ content.style.justifyContent = 'flex-start';
568
+ } else if (obj.verticalAlign === 'bottom') {
569
+ content.style.justifyContent = 'flex-end';
570
+ } else {
571
+ content.style.justifyContent = 'center';
572
+ }
573
+
574
+ // Yatay hizalama (Left / Center / Right)
575
+ const textAlign = obj.textAlign || 'center';
576
+ content.style.textAlign = textAlign;
577
+ if (textAlign === 'left') {
578
+ content.style.alignItems = 'flex-start';
579
+ } else if (textAlign === 'right') {
580
+ content.style.alignItems = 'flex-end';
581
+ } else {
582
+ content.style.alignItems = 'center';
583
+ }
584
+
585
+ if (obj.type === 'line') {
586
+ el.style.backgroundColor = 'transparent';
587
+ el.style.borderColor = 'transparent';
588
+ el.style.borderWidth = '0px';
589
+ el.style.boxShadow = 'none';
590
+ content.style.padding = '0px';
591
+ content.innerHTML = this.renderLineSVG(obj);
592
+ } else if (obj.type === 'image' && obj.src) {
593
+ const img = document.createElement('img');
594
+ img.src = obj.src;
595
+ img.draggable = false;
596
+ content.appendChild(img);
597
+ if (obj.text) {
598
+ const cap = document.createElement('div');
599
+ cap.className = 'image-caption';
600
+ cap.style.width = '100%';
601
+ cap.style.textAlign = textAlign;
602
+ cap.textContent = obj.text;
603
+ content.appendChild(cap);
604
+ }
605
+ } else if (obj.role === 'category') {
606
+ const header = document.createElement('div');
607
+ header.className = 'cat-title';
608
+ header.style.width = '100%';
609
+ header.style.textAlign = textAlign;
610
+ header.textContent = obj.text || obj.name;
611
+ content.appendChild(header);
612
+
613
+ const itemsContainer = document.createElement('div');
614
+ itemsContainer.className = 'category-slots-container';
615
+ itemsContainer.setAttribute('data-category-id', obj.id);
616
+ content.appendChild(itemsContainer);
617
+ } else if (obj.role === 'sequence_slot') {
618
+ const slotNum = document.createElement('div');
619
+ slotNum.className = 'slot-badge';
620
+ slotNum.textContent = (obj.slotIndex !== undefined ? obj.slotIndex + 1 : 1);
621
+ content.appendChild(slotNum);
622
+ if (obj.text) {
623
+ const slotText = document.createElement('div');
624
+ slotText.className = 'slot-text';
625
+ slotText.style.width = '100%';
626
+ slotText.style.textAlign = textAlign;
627
+ slotText.textContent = obj.text;
628
+ content.appendChild(slotText);
629
+ }
630
+ } else {
631
+ if (obj.text) {
632
+ const textSpan = document.createElement('span');
633
+ textSpan.className = 'player-text-span';
634
+ textSpan.style.width = '100%';
635
+ textSpan.style.display = 'block';
636
+ textSpan.style.textAlign = textAlign;
637
+ textSpan.textContent = obj.text;
638
+ content.appendChild(textSpan);
639
+ }
640
+ }
641
+
642
+ el.appendChild(content);
643
+
644
+ // Durum ikonu göstergesi (doğru / yanlış işareti)
645
+ const statusBadge = document.createElement('div');
646
+ statusBadge.className = 'status-badge';
647
+ el.appendChild(statusBadge);
648
+
649
+ return el;
650
+ }
651
+
652
+ // Pointer Events API ile Sürükle Bırak
653
+ setupPointerEvents() {
654
+ const draggablesLayer = this.container.querySelector('#player-draggables');
655
+
656
+ draggablesLayer.addEventListener('pointerdown', (e) => {
657
+ const itemEl = e.target.closest('.player-object.is-draggable');
658
+ if (!itemEl || itemEl.classList.contains('is-locked')) return;
659
+
660
+ e.preventDefault();
661
+ e.stopPropagation();
662
+
663
+ const id = itemEl.getAttribute('data-id');
664
+ this.activeDragId = id;
665
+ this.activePointerId = e.pointerId;
666
+
667
+ const stagePos = this.clientToStage(e.clientX, e.clientY);
668
+ this.dragStartPos = stagePos;
669
+
670
+ // Nesnenin mevcut transform koordinatlarını al
671
+ const styleTransform = itemEl.style.transform;
672
+ const match = /translate\(([-\d.]+)px,\s*([-\d.]+)px\)/.exec(styleTransform);
673
+ const currX = match ? parseFloat(match[1]) : 0;
674
+ const currY = match ? parseFloat(match[2]) : 0;
675
+
676
+ this.dragElemStart = { x: currX, y: currY };
677
+
678
+ itemEl.classList.add('is-dragging');
679
+ itemEl.style.zIndex = '9999';
680
+
681
+ try {
682
+ itemEl.setPointerCapture(e.pointerId);
683
+ } catch (err) {}
684
+
685
+ this.audio.playPickup();
686
+ });
687
+
688
+ window.addEventListener('pointermove', this.onPointerMoveBound);
689
+ window.addEventListener('pointerup', this.onPointerUpBound);
690
+ window.addEventListener('pointercancel', this.onPointerCancelBound);
691
+ }
692
+
693
+ handlePointerMove(e) {
694
+ if (!this.activeDragId || this.activePointerId !== e.pointerId) return;
695
+
696
+ const itemEl = this.container.querySelector(`[data-id="${this.activeDragId}"]`);
697
+ if (!itemEl) return;
698
+
699
+ const currentStagePos = this.clientToStage(e.clientX, e.clientY);
700
+ const dx = currentStagePos.x - this.dragStartPos.x;
701
+ const dy = currentStagePos.y - this.dragStartPos.y;
702
+
703
+ const objData = this.getObj(this.activeDragId);
704
+ const stageW = (this.projectData.stage && this.projectData.stage.width) || 1200;
705
+ const stageH = (this.projectData.stage && this.projectData.stage.height) || 675;
706
+ const objW = (objData && objData.width) || 60;
707
+ const objH = (objData && objData.height) || 40;
708
+
709
+ const nextX = Math.round(Math.max(0, Math.min(stageW - objW, this.dragElemStart.x + dx)));
710
+ const nextY = Math.round(Math.max(0, Math.min(stageH - objH, this.dragElemStart.y + dy)));
711
+
712
+ itemEl.style.transform = `translate(${nextX}px, ${nextY}px) scale(1.05)`;
713
+
714
+ // Hedef algılama ve vurgulama (Hover highlight)
715
+ const dropTarget = this.findDropTarget(nextX, nextY, (objData && objData.width) || 60, (objData && objData.height) || 40);
716
+ this.highlightHoverTarget(dropTarget ? dropTarget.id : null);
717
+ }
718
+
719
+ handlePointerUp(e) {
720
+ if (!this.activeDragId || this.activePointerId !== e.pointerId) return;
721
+ this.finalizeDrop();
722
+ }
723
+
724
+ handlePointerCancel(e) {
725
+ if (!this.activeDragId || this.activePointerId !== e.pointerId) return;
726
+ this.finalizeDrop();
727
+ }
728
+
729
+ // Klavye Erişilebilirliği (Space/Enter ile seç, hedefte bırak)
730
+ setupKeyboardEvents() {
731
+ this.container.addEventListener('keydown', this.onKeyDownBound);
732
+ }
733
+
734
+ handleKeyDown(e) {
735
+ const activeEl = document.activeElement;
736
+ if (!activeEl) return;
737
+
738
+ if (e.key === ' ' || e.key === 'Enter') {
739
+ if (activeEl.classList.contains('is-draggable') && !activeEl.classList.contains('is-locked')) {
740
+ e.preventDefault();
741
+ const id = activeEl.getAttribute('data-id');
742
+ if (this.keyboardSelectedId === id) {
743
+ // Seçimi bırak
744
+ this.keyboardSelectedId = null;
745
+ activeEl.classList.remove('keyboard-active');
746
+ } else {
747
+ // Nesneyi eline al
748
+ if (this.keyboardSelectedId) {
749
+ const prev = this.container.querySelector(`[data-id="${this.keyboardSelectedId}"]`);
750
+ if (prev) prev.classList.remove('keyboard-active');
751
+ }
752
+ this.keyboardSelectedId = id;
753
+ activeEl.classList.add('keyboard-active');
754
+ this.audio.playPickup();
755
+ this.showHint('Öğeyi seçtiniz. Şimdi Tab tuşu ile hedef alana gidip Boşluk tuşuna basarak bırakın.');
756
+ }
757
+ } else if (activeEl.classList.contains('is-target') && this.keyboardSelectedId) {
758
+ e.preventDefault();
759
+ const targetId = activeEl.getAttribute('data-id');
760
+ this.executeDrop(this.keyboardSelectedId, targetId);
761
+ const itemEl = this.container.querySelector(`[data-id="${this.keyboardSelectedId}"]`);
762
+ if (itemEl) itemEl.classList.remove('keyboard-active');
763
+ this.keyboardSelectedId = null;
764
+ }
765
+ }
766
+ }
767
+
768
+ finalizeDrop() {
769
+ const itemEl = this.container.querySelector(`[data-id="${this.activeDragId}"]`);
770
+ if (!itemEl) return;
771
+
772
+ itemEl.classList.remove('is-dragging');
773
+ itemEl.style.zIndex = '100';
774
+
775
+ try {
776
+ if (this.activePointerId !== null && itemEl.hasPointerCapture && itemEl.hasPointerCapture(this.activePointerId)) {
777
+ itemEl.releasePointerCapture(this.activePointerId);
778
+ }
779
+ } catch (err) {}
780
+
781
+ const styleTransform = itemEl.style.transform;
782
+ const match = /translate\(([-\d.]+)px,\s*([-\d.]+)px\)/.exec(styleTransform);
783
+ const currX = match ? parseFloat(match[1]) : 0;
784
+ const currY = match ? parseFloat(match[2]) : 0;
785
+
786
+ const objData = this.getObj(this.activeDragId);
787
+ const dropTarget = this.findDropTarget(currX, currY, (objData && objData.width) || 60, (objData && objData.height) || 40);
788
+
789
+ this.highlightHoverTarget(null);
790
+
791
+ const dragId = this.activeDragId;
792
+ this.activeDragId = null;
793
+ this.activePointerId = null;
794
+
795
+ if (dropTarget) {
796
+ this.executeDrop(dragId, dropTarget.id);
797
+ } else {
798
+ // Hedef dışına bırakıldıysa başlangıç konumuna dön
799
+ this.returnToOrigin(dragId);
800
+ this.audio.playDrop();
801
+ }
802
+ }
803
+
804
+ // Bırakılan hedefin tespiti (AABB ve Merkez noktası çakışması)
805
+ findDropTarget(itemX, itemY, itemW, itemH) {
806
+ const itemMidX = itemX + itemW / 2;
807
+ const itemMidY = itemY + itemH / 2;
808
+
809
+ const targets = (this.projectData.objects || []).filter(o =>
810
+ ['target', 'category', 'sequence_slot'].includes(o.role) && !this.lockedTargetIds.has(o.id)
811
+ );
812
+
813
+ // Öncelik: Merkez noktasını içinde barındıran hedef
814
+ for (const t of targets) {
815
+ if (itemMidX >= t.x && itemMidX <= t.x + t.width &&
816
+ itemMidY >= t.y && itemMidY <= t.y + t.height) {
817
+ return t;
818
+ }
819
+ }
820
+
821
+ // İkinci öncelik: En az %40 kesişim olan hedef
822
+ for (const t of targets) {
823
+ const interLeft = Math.max(itemX, t.x);
824
+ const interTop = Math.max(itemY, t.y);
825
+ const interRight = Math.min(itemX + itemW, t.x + t.width);
826
+ const interBottom = Math.min(itemY + itemH, t.y + t.height);
827
+
828
+ if (interRight > interLeft && interBottom > interTop) {
829
+ const interArea = (interRight - interLeft) * (interBottom - interTop);
830
+ const itemArea = itemW * itemH;
831
+ if (interArea / itemArea > 0.35) {
832
+ return t;
833
+ }
834
+ }
835
+ }
836
+
837
+ return null;
838
+ }
839
+
840
+ highlightHoverTarget(targetId) {
841
+ this.container.querySelectorAll('.player-object.is-target').forEach(el => {
842
+ const id = el.getAttribute('data-id');
843
+ if (targetId && id === targetId && !this.lockedTargetIds.has(id)) {
844
+ el.classList.add('hover-active');
845
+ } else {
846
+ el.classList.remove('hover-active');
847
+ }
848
+ });
849
+ }
850
+
851
+ getObj(id) {
852
+ return (this.projectData.objects || []).find(o => o.id === id);
853
+ }
854
+
855
+ // Bir nesnenin bir hedefe bırakılma işlemi
856
+ executeDrop(dragId, targetId) {
857
+ const targetObj = this.getObj(targetId);
858
+ const dragObj = this.getObj(dragId);
859
+ if (!targetObj || !dragObj) return;
860
+
861
+ // Doğru eşleşmiş kilitli hedeflere nesne bırakılamaz
862
+ if (this.lockedTargetIds.has(targetId)) return;
863
+
864
+ if (this.playMode === 'learning') {
865
+ this.attempts++;
866
+ this.updateStats();
867
+ this.evaluateSingleDrop(dragId, targetId);
868
+ } else {
869
+ // Değerlendirme Modu: Kullanıcı tüm nesneleri sürükleyecek, sürüklediği yere yerleşecek.
870
+ // Eğer hedef kategori değilse ve üzerinde henüz doğrulanmamış başka bir öğe varsa, eski öğeyi başlangıç yerine gönder
871
+ if (targetObj.role !== 'category') {
872
+ const prevDragId = Object.keys(this.placedMap).find(id => this.placedMap[id] === targetId && id !== dragId);
873
+ if (prevDragId) {
874
+ this.returnToOrigin(prevDragId);
875
+ }
876
+ }
877
+
878
+ this.attempts++;
879
+ this.updateStats();
880
+ this.snapToTarget(dragId, targetObj);
881
+ this.placedMap[dragId] = targetId;
882
+ this.audio.playDrop();
883
+ this.emit('drop', { dragId, targetId, isPending: true });
884
+ this.emit('step', this.getState());
885
+ }
886
+ }
887
+
888
+ evaluateSingleDrop(dragId, targetId) {
889
+ const correctTargetId = this.projectData.answerKey ? this.projectData.answerKey[dragId] : null;
890
+ const isCorrect = correctTargetId === targetId;
891
+
892
+ const dragEl = this.container.querySelector(`[data-id="${dragId}"]`);
893
+ const targetObj = this.getObj(targetId);
894
+ const targetEl = this.container.querySelector(`[data-id="${targetId}"]`);
895
+
896
+ if (isCorrect) {
897
+ // DOĞRU CEVAP
898
+ this.audio.playCorrect();
899
+ this.placedMap[dragId] = targetId;
900
+ this.correctCount++;
901
+ this.updateStats();
902
+ this.setFeedback('Harika! Doğru eşleştirme.', 'success', 2500);
903
+
904
+ this.snapToTarget(dragId, targetObj);
905
+
906
+ if (dragEl) {
907
+ dragEl.classList.remove('is-incorrect');
908
+ dragEl.classList.add('is-correct');
909
+
910
+ if (this.projectData.settings?.correctAnswerBehavior === 'lock') {
911
+ dragEl.classList.add('is-locked');
912
+ dragEl.setAttribute('tabindex', '-1');
913
+ }
914
+ this.addStatusBadge(dragEl, 'correct');
915
+ }
916
+
917
+ if (targetObj && targetObj.role !== 'category') {
918
+ this.lockedTargetIds.add(targetId);
919
+ if (targetEl) targetEl.classList.add('is-target-locked');
920
+ }
921
+
922
+ this.emit('drop', { dragId, targetId, isCorrect: true });
923
+ this.emit('step', this.getState());
924
+ this.checkCompletion();
925
+ } else {
926
+ // YANLIŞ CEVAP
927
+ this.audio.playWrong();
928
+ this.setFeedback('Tekrar deneyin! Yanlış eşleştirme.', 'error', 2500);
929
+
930
+ if (dragEl) {
931
+ dragEl.classList.add('is-incorrect');
932
+ this.addStatusBadge(dragEl, 'incorrect');
933
+ setTimeout(() => {
934
+ if (dragEl) {
935
+ dragEl.classList.remove('is-incorrect');
936
+ this.removeStatusBadge(dragEl);
937
+ }
938
+ }, 600);
939
+ }
940
+
941
+ const wrongBehavior = (this.projectData.settings && this.projectData.settings.wrongAnswerBehavior) || 'return';
942
+ if (wrongBehavior === 'return') {
943
+ this.returnToOrigin(dragId);
944
+ } else {
945
+ this.snapToTarget(dragId, targetObj);
946
+ this.placedMap[dragId] = targetId;
947
+ }
948
+
949
+ this.emit('drop', { dragId, targetId, isCorrect: false });
950
+ this.emit('step', this.getState());
951
+ }
952
+ }
953
+
954
+ // Hedefe Hizalama / Yerleştirme
955
+ snapToTarget(dragId, targetObj) {
956
+ const dragEl = this.container.querySelector(`[data-id="${dragId}"]`);
957
+ const dragObj = this.getObj(dragId);
958
+ if (!dragEl || !dragObj) return;
959
+
960
+ let targetX = targetObj.x;
961
+ let targetY = targetObj.y;
962
+
963
+ if (targetObj.role === 'category') {
964
+ // Kategori içine yerleştirme: kategori içindeki mevcut öğe sayısına göre gridleme
965
+ const existingItems = Object.keys(this.placedMap).filter(id => this.placedMap[id] === targetObj.id && id !== dragId);
966
+ const slotIndex = existingItems.length;
967
+
968
+ const col = slotIndex % 2;
969
+ const row = Math.floor(slotIndex / 2);
970
+
971
+ targetX = targetObj.x + 20 + col * (dragObj.width + 10);
972
+ targetY = targetObj.y + 60 + row * (dragObj.height + 10);
973
+ } else if (targetObj.role === 'sequence_slot' || targetObj.role === 'target') {
974
+ // Merkeze hizalama
975
+ targetX = targetObj.x + (targetObj.width - dragObj.width) / 2;
976
+ targetY = targetObj.y + (targetObj.height - dragObj.height) / 2;
977
+ }
978
+
979
+ dragEl.style.transition = 'transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1)';
980
+ dragEl.style.transform = `translate(${Math.round(targetX)}px, ${Math.round(targetY)}px) rotate(0deg)`;
981
+
982
+ setTimeout(() => {
983
+ if (dragEl) dragEl.style.transition = '';
984
+ }, 260);
985
+ }
986
+
987
+ returnToOrigin(dragId) {
988
+ const dragEl = this.container.querySelector(`[data-id="${dragId}"]`);
989
+ const origin = this.originPositions[dragId];
990
+ if (!dragEl || !origin) return;
991
+
992
+ delete this.placedMap[dragId];
993
+ this.removeStatusBadge(dragEl);
994
+ if (dragEl) dragEl.classList.remove('is-incorrect');
995
+
996
+ if (dragEl.classList.contains('is-correct')) {
997
+ dragEl.classList.remove('is-correct');
998
+ dragEl.classList.remove('is-locked');
999
+ dragEl.setAttribute('tabindex', '0');
1000
+ this.correctCount = Math.max(0, this.correctCount - 1);
1001
+ this.updateStats();
1002
+ }
1003
+
1004
+ const origObj = this.getObj(dragId);
1005
+ const rot = (origObj && origObj.rotation) ? origObj.rotation : 0;
1006
+
1007
+ dragEl.style.transition = 'transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1)';
1008
+ dragEl.style.transform = `translate(${origin.x}px, ${origin.y}px) rotate(${rot}deg) scale(1)`;
1009
+
1010
+ setTimeout(() => {
1011
+ if (dragEl) {
1012
+ dragEl.style.transition = '';
1013
+ dragEl.style.zIndex = origObj && origObj.zIndex ? `${origObj.zIndex + 10}` : '20';
1014
+ }
1015
+ }, 360);
1016
+ }
1017
+
1018
+ // Toplu Değerlendirme (Değerlendirme Modunda Kontrol Et butonu ile)
1019
+ checkAllAnswers() {
1020
+ const answerKey = this.projectData.answerKey || {};
1021
+ let correct = 0;
1022
+ this.attempts++;
1023
+
1024
+ const placedCount = Object.keys(this.placedMap).length;
1025
+ if (placedCount === 0) {
1026
+ this.setFeedback('Henüz bir nesne yerleştirmediniz. Sürükleyip bırakın.', 'warning', 3000);
1027
+ return;
1028
+ }
1029
+
1030
+ let hasWrong = false;
1031
+ for (const [dragId, targetId] of Object.entries(this.placedMap)) {
1032
+ const isRight = answerKey[dragId] === targetId;
1033
+ const dragEl = this.container.querySelector(`[data-id="${dragId}"]`);
1034
+ const targetObj = this.getObj(targetId);
1035
+ const targetEl = this.container.querySelector(`[data-id="${targetId}"]`);
1036
+
1037
+ if (isRight) {
1038
+ correct++;
1039
+ if (dragEl) {
1040
+ dragEl.classList.remove('is-incorrect');
1041
+ dragEl.classList.add('is-correct');
1042
+ dragEl.classList.add('is-locked');
1043
+ dragEl.setAttribute('tabindex', '-1');
1044
+ this.addStatusBadge(dragEl, 'correct');
1045
+ }
1046
+ if (targetObj && targetObj.role !== 'category') {
1047
+ this.lockedTargetIds.add(targetId);
1048
+ if (targetEl) targetEl.classList.add('is-target-locked');
1049
+ }
1050
+ } else {
1051
+ hasWrong = true;
1052
+ if (dragEl) {
1053
+ dragEl.classList.remove('is-correct');
1054
+ dragEl.classList.add('is-incorrect');
1055
+ this.addStatusBadge(dragEl, 'incorrect');
1056
+ }
1057
+ }
1058
+ }
1059
+
1060
+ this.correctCount = correct;
1061
+ this.updateStats();
1062
+
1063
+ if (correct === this.totalDraggables) {
1064
+ this.isCompleted = true;
1065
+ this.audio.playComplete();
1066
+ this.showCompletionResult();
1067
+ } else {
1068
+ this.audio.playWrong();
1069
+ this.setFeedback(`${correct} / ${this.totalDraggables} doğru. Yanlışları Tekrar Dene butonuyla düzeltebilirsiniz.`, 'warning', 4000);
1070
+ this.showHint('Bazı eşleştirmeler hatalı. "Tekrar Dene" butonuna basarak yanlışları eski yerine gönderebilirsiniz.');
1071
+ if (hasWrong) {
1072
+ this.setCheckButtonState('retry');
1073
+ }
1074
+ }
1075
+ }
1076
+
1077
+ // Değerlendirme Modunda Yanlışları Eski Yerine Gönderip Tekrar Dene Döngüsü
1078
+ retryWrongAnswers() {
1079
+ const answerKey = this.projectData.answerKey || {};
1080
+ const wrongIds = [];
1081
+
1082
+ for (const [dragId, targetId] of Object.entries(this.placedMap)) {
1083
+ if (answerKey[dragId] !== targetId) {
1084
+ wrongIds.push(dragId);
1085
+ }
1086
+ }
1087
+
1088
+ wrongIds.forEach(dragId => {
1089
+ const dragEl = this.container.querySelector(`[data-id="${dragId}"]`);
1090
+ if (dragEl) {
1091
+ dragEl.classList.remove('is-incorrect');
1092
+ this.removeStatusBadge(dragEl);
1093
+ }
1094
+ this.returnToOrigin(dragId);
1095
+ });
1096
+
1097
+ this.setCheckButtonState('check');
1098
+ this.showHint('Kalan nesneleri boş hedeflere sürükleyip tekrar kontrol edin.');
1099
+ }
1100
+
1101
+ checkCompletion() {
1102
+ if (this.correctCount >= this.totalDraggables && this.totalDraggables > 0) {
1103
+ if (!this.isCompleted) {
1104
+ this.isCompleted = true;
1105
+ this.audio.playComplete();
1106
+ setTimeout(() => this.showCompletionResult(), 400);
1107
+ }
1108
+ }
1109
+ }
1110
+
1111
+ setCompletionResultVisible(isVisible) {
1112
+ const resultCard = this.container.querySelector('#completion-result');
1113
+ const controls = this.container.querySelector('#player-controls');
1114
+ const footer = this.container.querySelector('#player-footer');
1115
+ const replayButton = this.container.querySelector('#btn-play-again');
1116
+
1117
+ if (resultCard) resultCard.hidden = !isVisible;
1118
+ if (controls) controls.hidden = isVisible;
1119
+ if (footer) footer.hidden = isVisible;
1120
+
1121
+ if (isVisible && replayButton && typeof replayButton.focus === 'function') {
1122
+ try {
1123
+ replayButton.focus({ preventScroll: true });
1124
+ } catch (error) {
1125
+ replayButton.focus();
1126
+ }
1127
+ }
1128
+
1129
+ this.updateScale();
1130
+ }
1131
+
1132
+ showCompletionResult() {
1133
+ const accuracy = Math.round((this.totalDraggables / Math.max(this.attempts, this.totalDraggables)) * 100);
1134
+ this.setCompletionResultVisible(true);
1135
+
1136
+ const payload = {
1137
+ score: accuracy,
1138
+ attempts: this.attempts,
1139
+ correctCount: this.correctCount
1140
+ };
1141
+
1142
+ this.emit('complete', payload);
1143
+ }
1144
+
1145
+ updateStats() {
1146
+ const statCorrect = this.container.querySelector('#stat-correct');
1147
+ const statAttempts = this.container.querySelector('#stat-attempts');
1148
+ if (statCorrect) statCorrect.textContent = this.correctCount;
1149
+ if (statAttempts) statAttempts.textContent = this.attempts;
1150
+ }
1151
+
1152
+ showHint(msg) {
1153
+ const hintEl = this.container.querySelector('#player-hint');
1154
+ if (hintEl) {
1155
+ hintEl.textContent = `💡 ${msg}`;
1156
+ }
1157
+ }
1158
+
1159
+ /**
1160
+ * Kontrol alanındaki geri bildirim mesajını günceller
1161
+ * @param {string} message - Gösterilecek mesaj metni
1162
+ * @param {'info'|'success'|'error'|'warning'} [type='info'] - Mesaj türü
1163
+ * @param {number} [duration=0] - Milisaniye cinsinden otomatik sıfırlanma süresi (0 ise kalıcı)
1164
+ */
1165
+ setFeedback(message, type = 'info', duration = 0) {
1166
+ const fbEl = this.container.querySelector('#player-feedback');
1167
+ const textEl = this.container.querySelector('#feedback-text');
1168
+ const iconEl = this.container.querySelector('#feedback-icon');
1169
+ if (!fbEl || !textEl) return;
1170
+
1171
+ this.currentFeedback = { message, type };
1172
+
1173
+ const icons = {
1174
+ info: '💬',
1175
+ success: '✓',
1176
+ error: '✕',
1177
+ warning: '⚠️'
1178
+ };
1179
+
1180
+ fbEl.className = `player-feedback feedback-${type}`;
1181
+ textEl.textContent = message;
1182
+ if (iconEl) {
1183
+ iconEl.textContent = icons[type] || '💬';
1184
+ }
1185
+
1186
+ if (this.feedbackTimeout) {
1187
+ clearTimeout(this.feedbackTimeout);
1188
+ this.feedbackTimeout = null;
1189
+ }
1190
+
1191
+ if (duration > 0) {
1192
+ this.feedbackTimeout = setTimeout(() => {
1193
+ this.resetFeedback();
1194
+ }, duration);
1195
+ }
1196
+ }
1197
+
1198
+ getFeedback() {
1199
+ return this.currentFeedback || { message: '', type: 'info' };
1200
+ }
1201
+
1202
+ resetFeedback() {
1203
+ this.setFeedback('Öğeleri doğru hedeflere sürükleyin', 'info');
1204
+ }
1205
+
1206
+ renderLineSVG(obj) {
1207
+ const w = Math.max(10, obj.width || 200);
1208
+ const h = Math.max(10, obj.height || 24);
1209
+ const cy = h / 2;
1210
+ const strokeColor = (obj.borderColor && obj.borderColor !== 'transparent') ? obj.borderColor : '#1e293b';
1211
+ const strokeWidth = (obj.borderWidth !== undefined && obj.borderWidth > 0) ? obj.borderWidth : 3;
1212
+ const strokeStyle = obj.borderStyle || 'solid';
1213
+
1214
+ let dashArray = 'none';
1215
+ if (strokeStyle === 'dashed') dashArray = '8 6';
1216
+ else if (strokeStyle === 'dotted') dashArray = '2 6';
1217
+
1218
+ const hasStartArrow = !!obj.arrowStart;
1219
+ const hasEndArrow = !!obj.arrowEnd;
1220
+ const arrowSize = Math.min(Math.max(8, strokeWidth * 3), w / 2.5);
1221
+
1222
+ const x1 = hasStartArrow ? arrowSize - 1 : 0;
1223
+ const x2 = hasEndArrow ? w - (arrowSize - 1) : w;
1224
+
1225
+ let svgHtml = `<svg class="object-line-bar" width="100%" height="100%" viewBox="0 0 ${w} ${h}" preserveAspectRatio="none" style="overflow: visible; display: block; pointer-events: none;">`;
1226
+
1227
+ // Ana Çizgi
1228
+ svgHtml += `<line x1="${x1}" y1="${cy}" x2="${x2}" y2="${cy}" stroke="${strokeColor}" stroke-width="${strokeWidth}" stroke-dasharray="${dashArray}" stroke-linecap="round" />`;
1229
+
1230
+ // Başlangıç Oku
1231
+ if (hasStartArrow) {
1232
+ svgHtml += `<polygon points="${arrowSize},${cy - arrowSize * 0.6} 0,${cy} ${arrowSize},${cy + arrowSize * 0.6}" fill="${strokeColor}" />`;
1233
+ }
1234
+
1235
+ // Bitiş Oku
1236
+ if (hasEndArrow) {
1237
+ svgHtml += `<polygon points="${w - arrowSize},${cy - arrowSize * 0.6} ${w},${cy} ${w - arrowSize},${cy + arrowSize * 0.6}" fill="${strokeColor}" />`;
1238
+ }
1239
+
1240
+ svgHtml += `</svg>`;
1241
+ return svgHtml;
1242
+ }
1243
+
1244
+ resetGame() {
1245
+ this.placedMap = {};
1246
+ this.score = 0;
1247
+ this.attempts = 0;
1248
+ this.correctCount = 0;
1249
+ this.isCompleted = false;
1250
+ this.lockedTargetIds.clear();
1251
+
1252
+ this.container.querySelectorAll('.player-status-badge').forEach(el => el.remove());
1253
+ this.container.querySelectorAll('.is-target-locked').forEach(el => el.classList.remove('is-target-locked'));
1254
+
1255
+ this.setCheckButtonState('check');
1256
+ this.updateStats();
1257
+ this.resetFeedback();
1258
+ this.setCompletionResultVisible(false);
1259
+
1260
+ this.prepareItems();
1261
+ this.emit('reset', this.getState());
1262
+ }
1263
+ }
1264
+
1265
+ // Universal namespace export
1266
+ if (typeof module !== 'undefined' && module.exports) {
1267
+ module.exports = { ActivityEngine };
1268
+ }
1269
+ if (typeof window !== 'undefined') {
1270
+ window.DND = window.DND || {};
1271
+ window.DND.ActivityEngine = ActivityEngine;
1272
+ }