@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,1758 @@
1
+ /**
2
+ * @mhmttosun/dnd - Player Bundle (UMD / CommonJS)
3
+ * Lisans: MIT (c) 2026 mhmttosun
4
+ */
5
+ (function (global, factory) {
6
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
7
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
8
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.DND = global.DND || {}));
9
+ })(this, (function (exports) {
10
+ 'use strict';
11
+
12
+ var windowObj = typeof window !== 'undefined' ? window : {};
13
+ windowObj.DND = windowObj.DND || {};
14
+
15
+ /**
16
+ * AudioSynthesizer - Tarayıcı Web Audio API kullanarak harici ses dosyalarına
17
+ * ihtiyaç duymadan temiz, profesyonel ses efektleri üretir.
18
+ * Tamamen çevrimdışı çalışır.
19
+ */
20
+ class AudioSynthesizer {
21
+ constructor() {
22
+ this.ctx = null;
23
+ this.enabled = true;
24
+ }
25
+
26
+ init() {
27
+ if (!this.ctx && (window.AudioContext || window.webkitAudioContext)) {
28
+ const AudioCtx = window.AudioContext || window.webkitAudioContext;
29
+ this.ctx = new AudioCtx();
30
+ }
31
+ if (this.ctx && this.ctx.state === 'suspended') {
32
+ this.ctx.resume();
33
+ }
34
+ }
35
+
36
+ setEnabled(val) {
37
+ this.enabled = !!val;
38
+ }
39
+
40
+ playTone(freq, type, duration, delay = 0, gainLevel = 0.15) {
41
+ if (!this.enabled) return;
42
+ try {
43
+ this.init();
44
+ if (!this.ctx) return;
45
+
46
+ const startTime = this.ctx.currentTime + delay;
47
+ const osc = this.ctx.createOscillator();
48
+ const gain = this.ctx.createGain();
49
+
50
+ osc.type = type;
51
+ osc.frequency.setValueAtTime(freq, startTime);
52
+
53
+ gain.gain.setValueAtTime(gainLevel, startTime);
54
+ gain.gain.exponentialRampToValueAtTime(0.0001, startTime + duration);
55
+
56
+ osc.connect(gain);
57
+ gain.connect(this.ctx.destination);
58
+
59
+ osc.start(startTime);
60
+ osc.stop(startTime + duration);
61
+ } catch (e) {
62
+ // Ses hatası sessizce geçilir
63
+ }
64
+ }
65
+
66
+ // Doğru cevap sesi: Neşeli, yükselen majör akor (C5, E5, G5)
67
+ playCorrect() {
68
+ if (!this.enabled) return;
69
+ this.playTone(523.25, 'triangle', 0.12, 0, 0.18); // C5
70
+ this.playTone(659.25, 'triangle', 0.14, 0.08, 0.18); // E5
71
+ this.playTone(783.99, 'triangle', 0.25, 0.16, 0.22); // G5
72
+ }
73
+
74
+ // Yanlış cevap sesi: Yumuşak alçalan uyarı tonu
75
+ playWrong() {
76
+ if (!this.enabled) return;
77
+ this.playTone(320, 'sine', 0.12, 0, 0.15);
78
+ this.playTone(260, 'sine', 0.22, 0.09, 0.15);
79
+ }
80
+
81
+ // Nesneyi eline alma / tıklama çıt sesi
82
+ playPickup() {
83
+ if (!this.enabled) return;
84
+ this.playTone(480, 'sine', 0.05, 0, 0.08);
85
+ }
86
+
87
+ // Nesneyi bırakma sesi
88
+ playDrop() {
89
+ if (!this.enabled) return;
90
+ this.playTone(360, 'sine', 0.07, 0, 0.1);
91
+ }
92
+
93
+ // Etkinlik tamamlandığında kutlama fanfarı
94
+ playComplete() {
95
+ if (!this.enabled) return;
96
+ const notes = [
97
+ { f: 523.25, d: 0.12, t: 0 }, // C5
98
+ { f: 659.25, d: 0.12, t: 0.1 }, // E5
99
+ { f: 783.99, d: 0.14, t: 0.2 }, // G5
100
+ { f: 1046.50, d: 0.45, t: 0.32 } // C6
101
+ ];
102
+ notes.forEach(n => {
103
+ this.playTone(n.f, 'triangle', n.d, n.t, 0.2);
104
+ });
105
+ }
106
+
107
+ // Web Audio Context kapatma ve bellek temizliği (Lifecycle Destroy)
108
+ close() {
109
+ if (this.ctx && typeof this.ctx.close === 'function') {
110
+ try {
111
+ this.ctx.close();
112
+ } catch (e) {}
113
+ this.ctx = null;
114
+ }
115
+ }
116
+ }
117
+
118
+ windowObj.DND.AudioSynthesizer = AudioSynthesizer;
119
+
120
+ /**
121
+ * ValidationEngine - Proje JSON verisi, cevap anahtarı ve nesne tutarlılığı
122
+ * doğrulamalarını gerçekleştirir. Hatalı ve eksik durumları tespit eder.
123
+ */
124
+ class ValidationEngine {
125
+ static get VALID_ACTIVITY_TYPES() {
126
+ return ['image-labeling', 'matching', 'categorization', 'sequencing'];
127
+ }
128
+
129
+ static get VALID_ROLES() {
130
+ return ['decorative', 'draggable', 'target', 'category', 'sequence_slot'];
131
+ }
132
+
133
+ /**
134
+ * Bir projenin şemasını doğrular
135
+ * @param {Object} projectData
136
+ * @returns {{ valid: boolean, errors: string[], warnings: string[] }}
137
+ */
138
+ static validateProject(projectData) {
139
+ const errors = [];
140
+ const warnings = [];
141
+
142
+ if (!projectData || typeof projectData !== 'object') {
143
+ return { valid: false, errors: ['Proje verisi geçerli bir JSON nesnesi değil.'], warnings };
144
+ }
145
+
146
+ // Sürüm kontrolü
147
+ if (!projectData.version) {
148
+ warnings.push('Proje sürüm bilgisi bulunamadı, varsayılan sürüm (1.0) kabul edildi.');
149
+ }
150
+
151
+ // Proje başlığı ve türü
152
+ if (!projectData.project || typeof projectData.project !== 'object') {
153
+ errors.push('Proje meta verisi (project alanı) eksik.');
154
+ } else {
155
+ if (!projectData.project.name || typeof projectData.project.name !== 'string') {
156
+ warnings.push('Proje adı belirtilmemiş.');
157
+ }
158
+ if (!this.VALID_ACTIVITY_TYPES.includes(projectData.project.activityType)) {
159
+ errors.push(`Geçersiz etkinlik türü: ${projectData.project.activityType}. Desteklenenler: ${this.VALID_ACTIVITY_TYPES.join(', ')}`);
160
+ }
161
+ }
162
+
163
+ // Sahne (Stage) kontrolleri
164
+ if (!projectData.stage || typeof projectData.stage !== 'object') {
165
+ errors.push('Sahne ayarları (stage alanı) eksik.');
166
+ } else {
167
+ if (typeof projectData.stage.width !== 'number' || projectData.stage.width <= 0) {
168
+ errors.push('Sahne genişliği pozitif bir sayı olmalıdır.');
169
+ }
170
+ if (typeof projectData.stage.height !== 'number' || projectData.stage.height <= 0) {
171
+ errors.push('Sahne yüksekliği pozitif bir sayı olmalıdır.');
172
+ }
173
+ }
174
+
175
+ // Nesneler (Objects) kontrolleri
176
+ if (!Array.isArray(projectData.objects)) {
177
+ errors.push('Nesneler listesi (objects alanı) bir dizi olmalıdır.');
178
+ } else {
179
+ const idSet = new Set();
180
+ let draggablesCount = 0;
181
+ let targetsCount = 0;
182
+
183
+ projectData.objects.forEach((obj, idx) => {
184
+ if (!obj.id) {
185
+ errors.push(`Nesne #${idx + 1} için 'id' alanı eksik.`);
186
+ } else if (idSet.has(obj.id)) {
187
+ errors.push(`Mükerrer nesne ID'si tespit edildi: '${obj.id}'.`);
188
+ } else {
189
+ idSet.add(obj.id);
190
+ }
191
+
192
+ if (typeof obj.x !== 'number' || typeof obj.y !== 'number') {
193
+ errors.push(`'${obj.id || idx}' nesnesinin X ve Y koordinatları sayı olmalıdır.`);
194
+ }
195
+
196
+ if (obj.role === 'draggable') draggablesCount++;
197
+ if (['target', 'category', 'sequence_slot'].includes(obj.role)) targetsCount++;
198
+ });
199
+
200
+ if (draggablesCount === 0) {
201
+ warnings.push('Etkinlikte henüz hiç sürüklenebilir nesne bulunmuyor.');
202
+ }
203
+ if (targetsCount === 0) {
204
+ warnings.push('Etkinlikte henüz hiç hedef alan bulunmuyor.');
205
+ }
206
+
207
+ // Cevap anahtarı kontrolü
208
+ const answerKey = projectData.answerKey || {};
209
+ for (const [dragId, targetId] of Object.entries(answerKey)) {
210
+ if (!idSet.has(dragId)) {
211
+ warnings.push(`Cevap anahtarındaki '${dragId}' ID'li sürüklenebilir nesne sahnede bulunamadı.`);
212
+ }
213
+ if (projectData.project && projectData.project.activityType !== 'sequencing') {
214
+ if (!idSet.has(targetId)) {
215
+ warnings.push(`Cevap anahtarındaki '${targetId}' ID'li hedef nesne sahnede bulunamadı.`);
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ return {
222
+ valid: errors.length === 0,
223
+ errors,
224
+ warnings
225
+ };
226
+ }
227
+
228
+ /**
229
+ * HTML dışa aktarmadan önce cevap anahtarının oynanabilirlik durumunu kontrol eder
230
+ */
231
+ static validatePlayability(projectData) {
232
+ const issues = [];
233
+ const objects = projectData.objects || [];
234
+ const answerKey = projectData.answerKey || {};
235
+ const draggables = objects.filter(o => o.role === 'draggable');
236
+
237
+ if (draggables.length === 0) {
238
+ issues.push('En az bir adet sürüklenebilir nesne eklemelisiniz.');
239
+ }
240
+
241
+ const unassignedDraggables = draggables.filter(d => !answerKey[d.id]);
242
+ if (unassignedDraggables.length > 0) {
243
+ const names = unassignedDraggables.map(d => d.name || d.text || d.id).join(', ');
244
+ issues.push(`Şu sürüklenebilir nesnelerin cevap anahtarında hedefi belirlenmemiş: ${names}`);
245
+ }
246
+
247
+ return {
248
+ ready: issues.length === 0,
249
+ issues
250
+ };
251
+ }
252
+
253
+ /**
254
+ * XSS ve zararlı HTML girişlerini engellemek için metin temizleme
255
+ */
256
+ static sanitizeText(str) {
257
+ if (str === null || str === undefined) return '';
258
+ const temp = document.createElement('div');
259
+ temp.textContent = String(str);
260
+ return temp.innerHTML;
261
+ }
262
+ }
263
+
264
+ windowObj.DND.ValidationEngine = ValidationEngine;
265
+
266
+ /**
267
+ * ActivityEngine - Etkinlik Oynatıcı Motoru.
268
+ * Hem editör içi Önizleme (Preview) modunda hem de bağımsız dışa aktarılan
269
+ * HTML dosyasında çalışan gerçek zamanlı öğrenci deneyimi motorudur.
270
+ *
271
+ * Dört etkinlik türünü eksiksiz destekler:
272
+ * 1. Görsel Üzerinde Etiketleme
273
+ * 2. Sürükle-Bırak Eşleştirme
274
+ * 3. Kategorilere Ayırma
275
+ * 4. Sürükleyerek Sıralama
276
+ */
277
+ class ActivityEngine {
278
+ constructor(containerEl, projectData, options = {}) {
279
+ this.container = containerEl;
280
+ this.projectData = JSON.parse(JSON.stringify(projectData)); // Orijinal veriyi korumak için derin kopya
281
+ this.options = Object.assign({
282
+ isEmbedded: false,
283
+ stagePadding: null,
284
+ onComplete: null,
285
+ onExit: null,
286
+ onStep: null,
287
+ onDrop: null,
288
+ onReset: null
289
+ }, options);
290
+
291
+ this.eventListeners = {};
292
+ this.audio = new window.DND.AudioSynthesizer();
293
+ this.audio.setEnabled(this.projectData.settings?.soundEnabled !== false);
294
+
295
+ // Oyun durumları
296
+ this.placedMap = {}; // { [draggableId]: targetId }
297
+ this.originPositions = {}; // { [draggableId]: { x, y } }
298
+ this.score = 0;
299
+ this.attempts = 0;
300
+ this.correctCount = 0;
301
+ this.isCompleted = false;
302
+
303
+ // Etkinlik Modu: 'learning' (Öğrenme - Anlık) veya 'assessment' (Değerlendirme - Toplu Kontrol)
304
+ this.playMode = this.options.playMode || this.projectData.settings?.playMode || 'learning';
305
+ this.isRetryMode = false;
306
+ this.lockedTargetIds = new Set();
307
+
308
+ // Sürükleme değişkenleri
309
+ this.activeDragId = null;
310
+ this.dragStartPos = { x: 0, y: 0 };
311
+ this.dragElemStart = { x: 0, y: 0 };
312
+ this.activePointerId = null;
313
+ this.currentScale = 1.0;
314
+
315
+ // Klavye erişilebilirliği için seçili nesne
316
+ this.keyboardSelectedId = null;
317
+
318
+ // Geri bildirim durumu ve zamanlayıcısı
319
+ this.feedbackTimeout = null;
320
+ this.currentFeedback = null;
321
+
322
+ // Yaşam döngüsü dinleyici referansları (Bellek sızıntılarını ve duplicate listener'ları önler)
323
+ this.onResizeBound = () => this.updateScale();
324
+ this.onPointerMoveBound = (e) => this.handlePointerMove(e);
325
+ this.onPointerUpBound = (e) => this.handlePointerUp(e);
326
+ this.onPointerCancelBound = (e) => this.handlePointerCancel(e);
327
+ this.onKeyDownBound = (e) => this.handleKeyDown(e);
328
+
329
+ this.init();
330
+ }
331
+
332
+ init() {
333
+ this.renderGameLayout();
334
+ this.setupResizeHandler();
335
+ this.setupPointerEvents();
336
+ this.setupKeyboardEvents();
337
+ this.prepareItems();
338
+ }
339
+
340
+ // Yaşam döngüsü temizliği: SPA, React, Vue unmount durumlarında tüm dinleyicileri ve sesi temizler
341
+ destroy() {
342
+ if (this.resizeObserver) {
343
+ this.resizeObserver.disconnect();
344
+ this.resizeObserver = null;
345
+ }
346
+ if (this.feedbackTimeout) {
347
+ clearTimeout(this.feedbackTimeout);
348
+ this.feedbackTimeout = null;
349
+ }
350
+ if (this.onResizeBound) {
351
+ window.removeEventListener('resize', this.onResizeBound);
352
+ }
353
+ if (this.onPointerMoveBound) {
354
+ window.removeEventListener('pointermove', this.onPointerMoveBound);
355
+ }
356
+ if (this.onPointerUpBound) {
357
+ window.removeEventListener('pointerup', this.onPointerUpBound);
358
+ }
359
+ if (this.onPointerCancelBound) {
360
+ window.removeEventListener('pointercancel', this.onPointerCancelBound);
361
+ }
362
+ if (this.container && this.onKeyDownBound) {
363
+ this.container.removeEventListener('keydown', this.onKeyDownBound);
364
+ }
365
+ if (this.audio && typeof this.audio.close === 'function') {
366
+ this.audio.close();
367
+ }
368
+ this.emit('destroy');
369
+ this.eventListeners = {};
370
+ if (this.container) {
371
+ this.container.innerHTML = '';
372
+ }
373
+ this.isDestroyed = true;
374
+ }
375
+
376
+ // Olay Dinleme ve Yayınlama (Event Emitter API)
377
+ on(event, callback) {
378
+ if (!this.eventListeners[event]) this.eventListeners[event] = [];
379
+ this.eventListeners[event].push(callback);
380
+ return () => this.off(event, callback);
381
+ }
382
+
383
+ off(event, callback) {
384
+ if (!this.eventListeners || !this.eventListeners[event]) return;
385
+ this.eventListeners[event] = this.eventListeners[event].filter(cb => cb !== callback);
386
+ }
387
+
388
+ emit(event, data) {
389
+ const handlerName = 'on' + event.charAt(0).toUpperCase() + event.slice(1);
390
+ if (this.options && typeof this.options[handlerName] === 'function') {
391
+ try {
392
+ this.options[handlerName](data);
393
+ } catch (err) {
394
+ console.error(`[ActivityEngine] Options callback error in ${event}:`, err);
395
+ }
396
+ }
397
+ if (this.eventListeners && this.eventListeners[event]) {
398
+ this.eventListeners[event].forEach(cb => {
399
+ try {
400
+ cb(data);
401
+ } catch (err) {
402
+ console.error(`[ActivityEngine] Event listener error in ${event}:`, err);
403
+ }
404
+ });
405
+ }
406
+ }
407
+
408
+ // Anlık etkinlik durumunu döner
409
+ getState() {
410
+ const accuracy = this.totalDraggables > 0
411
+ ? Math.round((this.correctCount / this.totalDraggables) * 100)
412
+ : 0;
413
+ return {
414
+ isCompleted: this.isCompleted,
415
+ score: accuracy,
416
+ attempts: this.attempts,
417
+ correctCount: this.correctCount,
418
+ totalDraggables: this.totalDraggables,
419
+ placedMap: Object.assign({}, this.placedMap)
420
+ };
421
+ }
422
+
423
+ setState(value) {
424
+ if (!value || typeof value !== 'object' || !value.placedMap || typeof value.placedMap !== 'object') {
425
+ throw new TypeError('Geçersiz DND oynatıcı durumu.');
426
+ }
427
+ const objects = new Map((this.projectData.objects || []).map(object => [object.id, object]));
428
+ const nextPlaced = {};
429
+ for (const [dragId, targetId] of Object.entries(value.placedMap)) {
430
+ const drag = objects.get(dragId);
431
+ const target = objects.get(targetId);
432
+ if (drag?.role !== 'draggable' || !['target', 'category', 'sequence_slot'].includes(target?.role)) {
433
+ throw new TypeError('Oynatıcı durumu bilinmeyen bir nesne içeriyor.');
434
+ }
435
+ nextPlaced[dragId] = targetId;
436
+ }
437
+ this.prepareItems();
438
+ this.placedMap = nextPlaced;
439
+ this.attempts = Number.isSafeInteger(value.attempts) && value.attempts >= 0 ? value.attempts : 0;
440
+ this.correctCount = 0;
441
+ for (const [dragId, targetId] of Object.entries(nextPlaced)) {
442
+ const target = objects.get(targetId);
443
+ this.snapToTarget(dragId, target);
444
+ const isCorrect = this.projectData.answerKey?.[dragId] === targetId;
445
+ const dragEl = this.container.querySelector(`[data-id="${dragId}"]`);
446
+ if (isCorrect) {
447
+ this.correctCount++;
448
+ dragEl?.classList.add('is-correct');
449
+ if (this.projectData.settings?.correctAnswerBehavior === 'lock') {
450
+ dragEl?.classList.add('is-locked');
451
+ dragEl?.setAttribute('tabindex', '-1');
452
+ }
453
+ }
454
+ }
455
+ this.isCompleted = this.totalDraggables > 0 && this.correctCount === this.totalDraggables && value.isCompleted === true;
456
+ this.setCompletionResultVisible(this.isCompleted);
457
+ this.updateStats();
458
+ return this.getState();
459
+ }
460
+
461
+ renderGameLayout() {
462
+ this.container.innerHTML = `
463
+ <div class="player-wrapper">
464
+ ${this.options.isEmbedded ? `
465
+ <header class="player-header player-header-minimal">
466
+ <button class="player-btn exit-btn" id="btn-exit-preview" title="Önizlemeden Çık ve Editöre Dön">
467
+ ✕ Editöre Dön
468
+ </button>
469
+ </header>
470
+ ` : ''}
471
+
472
+ <main class="player-viewport" id="player-viewport">
473
+ <div class="player-stage-container" id="player-stage-container">
474
+ <div class="player-stage-board" id="player-stage-board">
475
+ <div class="player-stage-scaler" id="player-stage-scaler">
476
+ <div class="player-stage" id="player-stage">
477
+ <div class="player-targets-layer" id="player-targets"></div>
478
+ <div class="player-draggables-layer" id="player-draggables"></div>
479
+ </div>
480
+ </div>
481
+
482
+ <!-- Kontrol Alanı (Açık Zemin, Hiyerarşik Butonlar) -->
483
+ <div class="player-controls" id="player-controls">
484
+ <div class="player-mode-switch" role="group" aria-label="Etkinlik Modu">
485
+ <button type="button" class="mode-btn ${this.playMode === 'learning' ? 'active' : ''}" id="btn-mode-learning" data-mode="learning" title="Öğrenme Modu (Anlık Geri Bildirim)">
486
+ Öğrenme
487
+ </button>
488
+ <button type="button" class="mode-btn ${this.playMode === 'assessment' ? 'active' : ''}" id="btn-mode-assessment" data-mode="assessment" title="Değerlendirme Modu (Toplu Kontrol)">
489
+ Değerlendirme
490
+ </button>
491
+ </div>
492
+
493
+ <div class="player-actions">
494
+ <button class="player-btn reset-btn secondary-btn" id="btn-reset-game" title="Yeniden Başlat">
495
+ <span class="btn-icon">↺</span> Sıfırla
496
+ </button>
497
+ <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;'}">
498
+ ✓ Kontrol Et
499
+ </button>
500
+ </div>
501
+ </div>
502
+ </div>
503
+
504
+ <!-- Tamamlanma Sonuç Kartı (Sahneyi kapatmadan normal akışta gösterilir) -->
505
+ <section class="completion-result-card" id="completion-result" aria-labelledby="completion-result-title" aria-live="polite" hidden>
506
+ <div class="completion-result-message">
507
+ <span class="completion-result-icon" aria-hidden="true">
508
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
509
+ <circle cx="12" cy="12" r="9"></circle>
510
+ <path d="m8 12 2.5 2.5L16 9"></path>
511
+ </svg>
512
+ </span>
513
+ <h2 class="completion-result-title" id="completion-result-title">Harika, hepsi doğru!</h2>
514
+ </div>
515
+ <button type="button" class="completion-replay-btn" id="btn-play-again">
516
+ <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">
517
+ <path d="M3 12a9 9 0 1 0 3-6.7L3 8"></path>
518
+ <path d="M3 3v5h5"></path>
519
+ </svg>
520
+ <span>Tekrar Oyna</span>
521
+ </button>
522
+ </section>
523
+
524
+ <!-- Alt Yardımcı İpucu Metni (Ayrı koyu şerit değil, sade yardımcı metin) -->
525
+ <footer class="player-footer" id="player-footer">
526
+ <div class="player-hint" id="player-hint">
527
+ 💡 Öğeleri sürükleyerek doğru hedeflerle eşleştirin.
528
+ </div>
529
+ </footer>
530
+ </div>
531
+ </main>
532
+ </div>
533
+ `;
534
+
535
+ // Buton dinleyicileri
536
+ const modeLearningBtn = this.container.querySelector('#btn-mode-learning');
537
+ if (modeLearningBtn) {
538
+ modeLearningBtn.onclick = () => this.setPlayMode('learning');
539
+ }
540
+
541
+ const modeAssessmentBtn = this.container.querySelector('#btn-mode-assessment');
542
+ if (modeAssessmentBtn) {
543
+ modeAssessmentBtn.onclick = () => this.setPlayMode('assessment');
544
+ }
545
+
546
+ const resetBtn = this.container.querySelector('#btn-reset-game');
547
+ if (resetBtn) resetBtn.onclick = () => this.resetGame();
548
+
549
+ const exitBtn = this.container.querySelector('#btn-exit-preview');
550
+ if (exitBtn) {
551
+ exitBtn.onclick = () => {
552
+ if (typeof this.options.onExit === 'function') this.options.onExit();
553
+ };
554
+ }
555
+
556
+ const checkBtn = this.container.querySelector('#btn-check-answers');
557
+ if (checkBtn) {
558
+ checkBtn.onclick = () => {
559
+ if (this.isRetryMode) {
560
+ this.retryWrongAnswers();
561
+ } else {
562
+ this.checkAllAnswers();
563
+ }
564
+ };
565
+ }
566
+
567
+ const playAgainBtn = this.container.querySelector('#btn-play-again');
568
+ if (playAgainBtn) playAgainBtn.onclick = () => this.resetGame();
569
+ }
570
+
571
+ getActivityTypeLabel() {
572
+ const type = this.projectData.project.activityType;
573
+ if (type === 'image-labeling') return 'Görsel Etiketleme';
574
+ if (type === 'matching') return 'Eşleştirme';
575
+ if (type === 'categorization') return 'Kategorilere Ayırma';
576
+ if (type === 'sequencing') return 'Sıralama';
577
+ return 'Etkinlik';
578
+ }
579
+
580
+ getPlayMode() {
581
+ return this.playMode;
582
+ }
583
+
584
+ setPlayMode(mode) {
585
+ if (mode !== 'learning' && mode !== 'assessment') return;
586
+ if (this.playMode === mode) return;
587
+ this.playMode = mode;
588
+ this.resetGame();
589
+ this.updateModeUI();
590
+ this.emit('playModeChange', { mode: this.playMode });
591
+ }
592
+
593
+ addStatusBadge(dragEl, type) {
594
+ if (!dragEl) return;
595
+ this.removeStatusBadge(dragEl);
596
+ const badge = document.createElement('span');
597
+ badge.className = `player-status-badge badge-${type}`;
598
+ badge.textContent = type === 'correct' ? '✓' : '✕';
599
+ dragEl.appendChild(badge);
600
+ }
601
+
602
+ removeStatusBadge(dragEl) {
603
+ if (!dragEl) return;
604
+ const badge = dragEl.querySelector('.player-status-badge');
605
+ if (badge) badge.remove();
606
+ }
607
+
608
+ updateModeUI() {
609
+ const learnBtn = this.container.querySelector('#btn-mode-learning');
610
+ const assessBtn = this.container.querySelector('#btn-mode-assessment');
611
+ const checkBtn = this.container.querySelector('#btn-check-answers');
612
+
613
+ if (learnBtn) learnBtn.classList.toggle('active', this.playMode === 'learning');
614
+ if (assessBtn) assessBtn.classList.toggle('active', this.playMode === 'assessment');
615
+
616
+ if (checkBtn) {
617
+ if (this.playMode === 'learning') {
618
+ checkBtn.style.display = 'none';
619
+ } else {
620
+ checkBtn.style.display = 'inline-flex';
621
+ this.setCheckButtonState('check');
622
+ }
623
+ }
624
+ }
625
+
626
+ setCheckButtonState(state) {
627
+ const checkBtn = this.container.querySelector('#btn-check-answers');
628
+ if (!checkBtn) return;
629
+
630
+ if (state === 'retry') {
631
+ this.isRetryMode = true;
632
+ checkBtn.textContent = '🔄 Tekrar Dene';
633
+ checkBtn.title = 'Yanlış seçenekleri geri gönderip tekrar deneyin';
634
+ checkBtn.className = 'player-btn retry-btn';
635
+ } else {
636
+ this.isRetryMode = false;
637
+ checkBtn.textContent = '✓ Kontrol Et';
638
+ checkBtn.title = 'Cevapları Kontrol Et';
639
+ checkBtn.className = 'player-btn primary-btn check-answers-btn';
640
+ }
641
+ }
642
+
643
+ setupResizeHandler() {
644
+ this.stageEl = this.container.querySelector('#player-stage');
645
+ this.stageScalerEl = this.container.querySelector('#player-stage-scaler');
646
+ this.stageBoardEl = this.container.querySelector('#player-stage-board');
647
+ this.completionResultEl = this.container.querySelector('#completion-result');
648
+ this.viewportEl = this.container.querySelector('#player-viewport');
649
+ this.stageContainerEl = this.container.querySelector('#player-stage-container') || this.viewportEl;
650
+
651
+ this.onResizeBound = () => this.updateScale();
652
+ window.addEventListener('resize', this.onResizeBound);
653
+
654
+ if (typeof window !== 'undefined' && window.ResizeObserver && this.stageContainerEl) {
655
+ this.resizeObserver = new window.ResizeObserver(() => {
656
+ this.updateScale();
657
+ });
658
+ this.resizeObserver.observe(this.stageContainerEl);
659
+ }
660
+
661
+ this.updateScale();
662
+ if (typeof requestAnimationFrame !== 'undefined') {
663
+ requestAnimationFrame(() => this.updateScale());
664
+ }
665
+ setTimeout(() => this.updateScale(), 30);
666
+ }
667
+
668
+ updateScale() {
669
+ if (!this.stageEl || !this.viewportEl) return;
670
+
671
+ const stageW = (this.projectData.stage && this.projectData.stage.width) || 1200;
672
+ const stageH = (this.projectData.stage && this.projectData.stage.height) || 675;
673
+
674
+ this.stageEl.style.width = `${stageW}px`;
675
+ this.stageEl.style.height = `${stageH}px`;
676
+
677
+ // Arka plan stilleri
678
+ this.stageEl.style.backgroundColor = (this.projectData.stage && this.projectData.stage.backgroundColor) || '#ffffff';
679
+ if (this.projectData.stage && this.projectData.stage.backgroundImage) {
680
+ this.stageEl.style.backgroundImage = `url("${this.projectData.stage.backgroundImage}")`;
681
+ this.stageEl.style.backgroundSize = 'contain';
682
+ this.stageEl.style.backgroundPosition = 'center';
683
+ this.stageEl.style.backgroundRepeat = 'no-repeat';
684
+ }
685
+
686
+ const targetContainer = this.stageContainerEl || this.viewportEl;
687
+ const vpRect = targetContainer.getBoundingClientRect();
688
+ if (vpRect.width === 0 || vpRect.height === 0) {
689
+ if (typeof requestAnimationFrame !== 'undefined') {
690
+ requestAnimationFrame(() => this.updateScale());
691
+ }
692
+ return;
693
+ }
694
+
695
+ const requestedPadding = this.options.stagePadding;
696
+ const numericPadding = Number(requestedPadding);
697
+ const pad = requestedPadding !== null
698
+ && requestedPadding !== undefined
699
+ && Number.isFinite(numericPadding)
700
+ && numericPadding >= 0
701
+ ? numericPadding
702
+ : (vpRect.width < 600 ? 16 : 32);
703
+ // player-controls (~48px) + player-footer (~36px) = ~84px dikey alan payı
704
+ const extraH = 88;
705
+ const availW = Math.max(vpRect.width - pad, 60);
706
+ const availH = Math.max(vpRect.height - pad - extraH, 60);
707
+
708
+ this.currentScale = Math.min(availW / stageW, availH / stageH);
709
+ const scaledW = Math.round(stageW * this.currentScale);
710
+ const scaledH = Math.round(stageH * this.currentScale);
711
+
712
+ if (this.stageScalerEl) {
713
+ this.stageScalerEl.style.width = `${scaledW}px`;
714
+ this.stageScalerEl.style.height = `${scaledH}px`;
715
+ }
716
+
717
+ if (this.stageBoardEl) {
718
+ this.stageBoardEl.style.width = `${scaledW}px`;
719
+ }
720
+
721
+ if (this.completionResultEl) {
722
+ this.completionResultEl.style.width = `${scaledW}px`;
723
+ }
724
+
725
+ this.stageEl.style.transform = `scale(${this.currentScale})`;
726
+ this.stageEl.style.transformOrigin = 'top left';
727
+ }
728
+
729
+ clientToStage(clientX, clientY) {
730
+ const rect = this.stageEl.getBoundingClientRect();
731
+ const stageW = this.projectData.stage.width || 1200;
732
+ const scale = rect.width / stageW;
733
+ return {
734
+ x: (clientX - rect.left) / scale,
735
+ y: (clientY - rect.top) / scale
736
+ };
737
+ }
738
+
739
+ prepareItems() {
740
+ const targetsLayer = this.container.querySelector('#player-targets');
741
+ const draggablesLayer = this.container.querySelector('#player-draggables');
742
+ targetsLayer.innerHTML = '';
743
+ draggablesLayer.innerHTML = '';
744
+
745
+ const objects = this.projectData.objects || [];
746
+ const draggables = objects.filter(o => o.role === 'draggable');
747
+ const nonDraggables = objects.filter(o => o.role !== 'draggable');
748
+
749
+ this.totalDraggables = draggables.length;
750
+ const statTotal = this.container.querySelector('#stat-total');
751
+ if (statTotal) statTotal.textContent = this.totalDraggables;
752
+ const statCorrect = this.container.querySelector('#stat-correct');
753
+ if (statCorrect) statCorrect.textContent = '0';
754
+ const statAttempts = this.container.querySelector('#stat-attempts');
755
+ if (statAttempts) statAttempts.textContent = '0';
756
+
757
+ // Sabit, hedef ve dekoratif nesneleri çiz
758
+ nonDraggables.forEach(obj => {
759
+ const el = this.createDOMObject(obj, false);
760
+ targetsLayer.appendChild(el);
761
+ });
762
+
763
+ // Eğer ayarlarda karıştırma aktifse sürüklenebilir nesnelerin konumlarını karıştır
764
+ let renderDraggables = [...draggables];
765
+ if (this.projectData.settings?.shuffleDraggables) {
766
+ const positions = draggables.map(d => ({ x: d.x, y: d.y }));
767
+ // Fisher-Yates karıştırma
768
+ for (let i = positions.length - 1; i > 0; i--) {
769
+ const j = Math.floor(Math.random() * (i + 1));
770
+ [positions[i], positions[j]] = [positions[j], positions[i]];
771
+ }
772
+ renderDraggables.forEach((d, idx) => {
773
+ d.currentX = positions[idx].x;
774
+ d.currentY = positions[idx].y;
775
+ });
776
+ } else {
777
+ renderDraggables.forEach(d => {
778
+ d.currentX = d.x;
779
+ d.currentY = d.y;
780
+ });
781
+ }
782
+
783
+ // Başlangıç konumlarını karıştırma sonrası ekrandaki gerçek yerleriyle kaydet
784
+ renderDraggables.forEach(d => {
785
+ this.originPositions[d.id] = { x: d.currentX, y: d.currentY };
786
+ });
787
+
788
+ // Sürüklenebilir nesneleri çiz
789
+ renderDraggables.forEach(obj => {
790
+ const el = this.createDOMObject(obj, true);
791
+ draggablesLayer.appendChild(el);
792
+ });
793
+
794
+ // Değerlendirme modu UI ayarı
795
+ this.updateModeUI();
796
+ }
797
+
798
+ createDOMObject(obj, isDraggable = false) {
799
+ const el = document.createElement('div');
800
+ el.className = `player-object ${isDraggable ? 'is-draggable' : 'is-target'}`;
801
+ el.setAttribute('data-id', obj.id);
802
+ el.setAttribute('data-role', obj.role);
803
+ el.setAttribute('tabindex', isDraggable ? '0' : '-1');
804
+
805
+ const posX = isDraggable && obj.currentX !== undefined ? obj.currentX : obj.x;
806
+ const posY = isDraggable && obj.currentY !== undefined ? obj.currentY : obj.y;
807
+
808
+ el.style.width = `${obj.width}px`;
809
+ el.style.height = `${obj.height}px`;
810
+ el.style.transform = `translate(${posX}px, ${posY}px) rotate(${obj.rotation || 0}deg)`;
811
+ el.style.zIndex = isDraggable ? (obj.zIndex || 10) + 10 : (obj.zIndex || 1);
812
+
813
+ // Stiller
814
+ el.style.backgroundColor = obj.backgroundColor || 'transparent';
815
+ el.style.borderColor = obj.borderColor || 'transparent';
816
+ el.style.borderWidth = `${obj.borderWidth || 0}px`;
817
+ el.style.borderStyle = obj.borderStyle || 'solid';
818
+ el.style.borderRadius = `${obj.borderRadius || 0}px`;
819
+ el.style.boxShadow = obj.boxShadow || 'none';
820
+
821
+ // İçerik
822
+ const content = document.createElement('div');
823
+ content.className = 'player-object-content';
824
+ content.style.fontFamily = obj.fontFamily || 'sans-serif';
825
+ content.style.fontSize = `${obj.fontSize || 16}px`;
826
+ content.style.fontWeight = obj.fontWeight || 'normal';
827
+ content.style.color = obj.textColor || '#1e293b';
828
+ content.style.padding = `${obj.padding !== undefined ? obj.padding : 8}px`;
829
+
830
+ // Dikey hizalama (Top / Middle / Bottom)
831
+ if (obj.verticalAlign === 'top') {
832
+ content.style.justifyContent = 'flex-start';
833
+ } else if (obj.verticalAlign === 'bottom') {
834
+ content.style.justifyContent = 'flex-end';
835
+ } else {
836
+ content.style.justifyContent = 'center';
837
+ }
838
+
839
+ // Yatay hizalama (Left / Center / Right)
840
+ const textAlign = obj.textAlign || 'center';
841
+ content.style.textAlign = textAlign;
842
+ if (textAlign === 'left') {
843
+ content.style.alignItems = 'flex-start';
844
+ } else if (textAlign === 'right') {
845
+ content.style.alignItems = 'flex-end';
846
+ } else {
847
+ content.style.alignItems = 'center';
848
+ }
849
+
850
+ if (obj.type === 'line') {
851
+ el.style.backgroundColor = 'transparent';
852
+ el.style.borderColor = 'transparent';
853
+ el.style.borderWidth = '0px';
854
+ el.style.boxShadow = 'none';
855
+ content.style.padding = '0px';
856
+ content.innerHTML = this.renderLineSVG(obj);
857
+ } else if (obj.type === 'image' && obj.src) {
858
+ const img = document.createElement('img');
859
+ img.src = obj.src;
860
+ img.draggable = false;
861
+ content.appendChild(img);
862
+ if (obj.text) {
863
+ const cap = document.createElement('div');
864
+ cap.className = 'image-caption';
865
+ cap.style.width = '100%';
866
+ cap.style.textAlign = textAlign;
867
+ cap.textContent = obj.text;
868
+ content.appendChild(cap);
869
+ }
870
+ } else if (obj.role === 'category') {
871
+ const header = document.createElement('div');
872
+ header.className = 'cat-title';
873
+ header.style.width = '100%';
874
+ header.style.textAlign = textAlign;
875
+ header.textContent = obj.text || obj.name;
876
+ content.appendChild(header);
877
+
878
+ const itemsContainer = document.createElement('div');
879
+ itemsContainer.className = 'category-slots-container';
880
+ itemsContainer.setAttribute('data-category-id', obj.id);
881
+ content.appendChild(itemsContainer);
882
+ } else if (obj.role === 'sequence_slot') {
883
+ const slotNum = document.createElement('div');
884
+ slotNum.className = 'slot-badge';
885
+ slotNum.textContent = (obj.slotIndex !== undefined ? obj.slotIndex + 1 : 1);
886
+ content.appendChild(slotNum);
887
+ if (obj.text) {
888
+ const slotText = document.createElement('div');
889
+ slotText.className = 'slot-text';
890
+ slotText.style.width = '100%';
891
+ slotText.style.textAlign = textAlign;
892
+ slotText.textContent = obj.text;
893
+ content.appendChild(slotText);
894
+ }
895
+ } else {
896
+ if (obj.text) {
897
+ const textSpan = document.createElement('span');
898
+ textSpan.className = 'player-text-span';
899
+ textSpan.style.width = '100%';
900
+ textSpan.style.display = 'block';
901
+ textSpan.style.textAlign = textAlign;
902
+ textSpan.textContent = obj.text;
903
+ content.appendChild(textSpan);
904
+ }
905
+ }
906
+
907
+ el.appendChild(content);
908
+
909
+ // Durum ikonu göstergesi (doğru / yanlış işareti)
910
+ const statusBadge = document.createElement('div');
911
+ statusBadge.className = 'status-badge';
912
+ el.appendChild(statusBadge);
913
+
914
+ return el;
915
+ }
916
+
917
+ // Pointer Events API ile Sürükle Bırak
918
+ setupPointerEvents() {
919
+ const draggablesLayer = this.container.querySelector('#player-draggables');
920
+
921
+ draggablesLayer.addEventListener('pointerdown', (e) => {
922
+ const itemEl = e.target.closest('.player-object.is-draggable');
923
+ if (!itemEl || itemEl.classList.contains('is-locked')) return;
924
+
925
+ e.preventDefault();
926
+ e.stopPropagation();
927
+
928
+ const id = itemEl.getAttribute('data-id');
929
+ this.activeDragId = id;
930
+ this.activePointerId = e.pointerId;
931
+
932
+ const stagePos = this.clientToStage(e.clientX, e.clientY);
933
+ this.dragStartPos = stagePos;
934
+
935
+ // Nesnenin mevcut transform koordinatlarını al
936
+ const styleTransform = itemEl.style.transform;
937
+ const match = /translate\(([-\d.]+)px,\s*([-\d.]+)px\)/.exec(styleTransform);
938
+ const currX = match ? parseFloat(match[1]) : 0;
939
+ const currY = match ? parseFloat(match[2]) : 0;
940
+
941
+ this.dragElemStart = { x: currX, y: currY };
942
+
943
+ itemEl.classList.add('is-dragging');
944
+ itemEl.style.zIndex = '9999';
945
+
946
+ try {
947
+ itemEl.setPointerCapture(e.pointerId);
948
+ } catch (err) {}
949
+
950
+ this.audio.playPickup();
951
+ });
952
+
953
+ window.addEventListener('pointermove', this.onPointerMoveBound);
954
+ window.addEventListener('pointerup', this.onPointerUpBound);
955
+ window.addEventListener('pointercancel', this.onPointerCancelBound);
956
+ }
957
+
958
+ handlePointerMove(e) {
959
+ if (!this.activeDragId || this.activePointerId !== e.pointerId) return;
960
+
961
+ const itemEl = this.container.querySelector(`[data-id="${this.activeDragId}"]`);
962
+ if (!itemEl) return;
963
+
964
+ const currentStagePos = this.clientToStage(e.clientX, e.clientY);
965
+ const dx = currentStagePos.x - this.dragStartPos.x;
966
+ const dy = currentStagePos.y - this.dragStartPos.y;
967
+
968
+ const objData = this.getObj(this.activeDragId);
969
+ const stageW = (this.projectData.stage && this.projectData.stage.width) || 1200;
970
+ const stageH = (this.projectData.stage && this.projectData.stage.height) || 675;
971
+ const objW = (objData && objData.width) || 60;
972
+ const objH = (objData && objData.height) || 40;
973
+
974
+ const nextX = Math.round(Math.max(0, Math.min(stageW - objW, this.dragElemStart.x + dx)));
975
+ const nextY = Math.round(Math.max(0, Math.min(stageH - objH, this.dragElemStart.y + dy)));
976
+
977
+ itemEl.style.transform = `translate(${nextX}px, ${nextY}px) scale(1.05)`;
978
+
979
+ // Hedef algılama ve vurgulama (Hover highlight)
980
+ const dropTarget = this.findDropTarget(nextX, nextY, (objData && objData.width) || 60, (objData && objData.height) || 40);
981
+ this.highlightHoverTarget(dropTarget ? dropTarget.id : null);
982
+ }
983
+
984
+ handlePointerUp(e) {
985
+ if (!this.activeDragId || this.activePointerId !== e.pointerId) return;
986
+ this.finalizeDrop();
987
+ }
988
+
989
+ handlePointerCancel(e) {
990
+ if (!this.activeDragId || this.activePointerId !== e.pointerId) return;
991
+ this.finalizeDrop();
992
+ }
993
+
994
+ // Klavye Erişilebilirliği (Space/Enter ile seç, hedefte bırak)
995
+ setupKeyboardEvents() {
996
+ this.container.addEventListener('keydown', this.onKeyDownBound);
997
+ }
998
+
999
+ handleKeyDown(e) {
1000
+ const activeEl = document.activeElement;
1001
+ if (!activeEl) return;
1002
+
1003
+ if (e.key === ' ' || e.key === 'Enter') {
1004
+ if (activeEl.classList.contains('is-draggable') && !activeEl.classList.contains('is-locked')) {
1005
+ e.preventDefault();
1006
+ const id = activeEl.getAttribute('data-id');
1007
+ if (this.keyboardSelectedId === id) {
1008
+ // Seçimi bırak
1009
+ this.keyboardSelectedId = null;
1010
+ activeEl.classList.remove('keyboard-active');
1011
+ } else {
1012
+ // Nesneyi eline al
1013
+ if (this.keyboardSelectedId) {
1014
+ const prev = this.container.querySelector(`[data-id="${this.keyboardSelectedId}"]`);
1015
+ if (prev) prev.classList.remove('keyboard-active');
1016
+ }
1017
+ this.keyboardSelectedId = id;
1018
+ activeEl.classList.add('keyboard-active');
1019
+ this.audio.playPickup();
1020
+ this.showHint('Öğeyi seçtiniz. Şimdi Tab tuşu ile hedef alana gidip Boşluk tuşuna basarak bırakın.');
1021
+ }
1022
+ } else if (activeEl.classList.contains('is-target') && this.keyboardSelectedId) {
1023
+ e.preventDefault();
1024
+ const targetId = activeEl.getAttribute('data-id');
1025
+ this.executeDrop(this.keyboardSelectedId, targetId);
1026
+ const itemEl = this.container.querySelector(`[data-id="${this.keyboardSelectedId}"]`);
1027
+ if (itemEl) itemEl.classList.remove('keyboard-active');
1028
+ this.keyboardSelectedId = null;
1029
+ }
1030
+ }
1031
+ }
1032
+
1033
+ finalizeDrop() {
1034
+ const itemEl = this.container.querySelector(`[data-id="${this.activeDragId}"]`);
1035
+ if (!itemEl) return;
1036
+
1037
+ itemEl.classList.remove('is-dragging');
1038
+ itemEl.style.zIndex = '100';
1039
+
1040
+ try {
1041
+ if (this.activePointerId !== null && itemEl.hasPointerCapture && itemEl.hasPointerCapture(this.activePointerId)) {
1042
+ itemEl.releasePointerCapture(this.activePointerId);
1043
+ }
1044
+ } catch (err) {}
1045
+
1046
+ const styleTransform = itemEl.style.transform;
1047
+ const match = /translate\(([-\d.]+)px,\s*([-\d.]+)px\)/.exec(styleTransform);
1048
+ const currX = match ? parseFloat(match[1]) : 0;
1049
+ const currY = match ? parseFloat(match[2]) : 0;
1050
+
1051
+ const objData = this.getObj(this.activeDragId);
1052
+ const dropTarget = this.findDropTarget(currX, currY, (objData && objData.width) || 60, (objData && objData.height) || 40);
1053
+
1054
+ this.highlightHoverTarget(null);
1055
+
1056
+ const dragId = this.activeDragId;
1057
+ this.activeDragId = null;
1058
+ this.activePointerId = null;
1059
+
1060
+ if (dropTarget) {
1061
+ this.executeDrop(dragId, dropTarget.id);
1062
+ } else {
1063
+ // Hedef dışına bırakıldıysa başlangıç konumuna dön
1064
+ this.returnToOrigin(dragId);
1065
+ this.audio.playDrop();
1066
+ }
1067
+ }
1068
+
1069
+ // Bırakılan hedefin tespiti (AABB ve Merkez noktası çakışması)
1070
+ findDropTarget(itemX, itemY, itemW, itemH) {
1071
+ const itemMidX = itemX + itemW / 2;
1072
+ const itemMidY = itemY + itemH / 2;
1073
+
1074
+ const targets = (this.projectData.objects || []).filter(o =>
1075
+ ['target', 'category', 'sequence_slot'].includes(o.role) && !this.lockedTargetIds.has(o.id)
1076
+ );
1077
+
1078
+ // Öncelik: Merkez noktasını içinde barındıran hedef
1079
+ for (const t of targets) {
1080
+ if (itemMidX >= t.x && itemMidX <= t.x + t.width &&
1081
+ itemMidY >= t.y && itemMidY <= t.y + t.height) {
1082
+ return t;
1083
+ }
1084
+ }
1085
+
1086
+ // İkinci öncelik: En az %40 kesişim olan hedef
1087
+ for (const t of targets) {
1088
+ const interLeft = Math.max(itemX, t.x);
1089
+ const interTop = Math.max(itemY, t.y);
1090
+ const interRight = Math.min(itemX + itemW, t.x + t.width);
1091
+ const interBottom = Math.min(itemY + itemH, t.y + t.height);
1092
+
1093
+ if (interRight > interLeft && interBottom > interTop) {
1094
+ const interArea = (interRight - interLeft) * (interBottom - interTop);
1095
+ const itemArea = itemW * itemH;
1096
+ if (interArea / itemArea > 0.35) {
1097
+ return t;
1098
+ }
1099
+ }
1100
+ }
1101
+
1102
+ return null;
1103
+ }
1104
+
1105
+ highlightHoverTarget(targetId) {
1106
+ this.container.querySelectorAll('.player-object.is-target').forEach(el => {
1107
+ const id = el.getAttribute('data-id');
1108
+ if (targetId && id === targetId && !this.lockedTargetIds.has(id)) {
1109
+ el.classList.add('hover-active');
1110
+ } else {
1111
+ el.classList.remove('hover-active');
1112
+ }
1113
+ });
1114
+ }
1115
+
1116
+ getObj(id) {
1117
+ return (this.projectData.objects || []).find(o => o.id === id);
1118
+ }
1119
+
1120
+ // Bir nesnenin bir hedefe bırakılma işlemi
1121
+ executeDrop(dragId, targetId) {
1122
+ const targetObj = this.getObj(targetId);
1123
+ const dragObj = this.getObj(dragId);
1124
+ if (!targetObj || !dragObj) return;
1125
+
1126
+ // Doğru eşleşmiş kilitli hedeflere nesne bırakılamaz
1127
+ if (this.lockedTargetIds.has(targetId)) return;
1128
+
1129
+ if (this.playMode === 'learning') {
1130
+ this.attempts++;
1131
+ this.updateStats();
1132
+ this.evaluateSingleDrop(dragId, targetId);
1133
+ } else {
1134
+ // Değerlendirme Modu: Kullanıcı tüm nesneleri sürükleyecek, sürüklediği yere yerleşecek.
1135
+ // Eğer hedef kategori değilse ve üzerinde henüz doğrulanmamış başka bir öğe varsa, eski öğeyi başlangıç yerine gönder
1136
+ if (targetObj.role !== 'category') {
1137
+ const prevDragId = Object.keys(this.placedMap).find(id => this.placedMap[id] === targetId && id !== dragId);
1138
+ if (prevDragId) {
1139
+ this.returnToOrigin(prevDragId);
1140
+ }
1141
+ }
1142
+
1143
+ this.attempts++;
1144
+ this.updateStats();
1145
+ this.snapToTarget(dragId, targetObj);
1146
+ this.placedMap[dragId] = targetId;
1147
+ this.audio.playDrop();
1148
+ this.emit('drop', { dragId, targetId, isPending: true });
1149
+ this.emit('step', this.getState());
1150
+ }
1151
+ }
1152
+
1153
+ evaluateSingleDrop(dragId, targetId) {
1154
+ const correctTargetId = this.projectData.answerKey ? this.projectData.answerKey[dragId] : null;
1155
+ const isCorrect = correctTargetId === targetId;
1156
+
1157
+ const dragEl = this.container.querySelector(`[data-id="${dragId}"]`);
1158
+ const targetObj = this.getObj(targetId);
1159
+ const targetEl = this.container.querySelector(`[data-id="${targetId}"]`);
1160
+
1161
+ if (isCorrect) {
1162
+ // DOĞRU CEVAP
1163
+ this.audio.playCorrect();
1164
+ this.placedMap[dragId] = targetId;
1165
+ this.correctCount++;
1166
+ this.updateStats();
1167
+ this.setFeedback('Harika! Doğru eşleştirme.', 'success', 2500);
1168
+
1169
+ this.snapToTarget(dragId, targetObj);
1170
+
1171
+ if (dragEl) {
1172
+ dragEl.classList.remove('is-incorrect');
1173
+ dragEl.classList.add('is-correct');
1174
+
1175
+ if (this.projectData.settings?.correctAnswerBehavior === 'lock') {
1176
+ dragEl.classList.add('is-locked');
1177
+ dragEl.setAttribute('tabindex', '-1');
1178
+ }
1179
+ this.addStatusBadge(dragEl, 'correct');
1180
+ }
1181
+
1182
+ if (targetObj && targetObj.role !== 'category') {
1183
+ this.lockedTargetIds.add(targetId);
1184
+ if (targetEl) targetEl.classList.add('is-target-locked');
1185
+ }
1186
+
1187
+ this.emit('drop', { dragId, targetId, isCorrect: true });
1188
+ this.emit('step', this.getState());
1189
+ this.checkCompletion();
1190
+ } else {
1191
+ // YANLIŞ CEVAP
1192
+ this.audio.playWrong();
1193
+ this.setFeedback('Tekrar deneyin! Yanlış eşleştirme.', 'error', 2500);
1194
+
1195
+ if (dragEl) {
1196
+ dragEl.classList.add('is-incorrect');
1197
+ this.addStatusBadge(dragEl, 'incorrect');
1198
+ setTimeout(() => {
1199
+ if (dragEl) {
1200
+ dragEl.classList.remove('is-incorrect');
1201
+ this.removeStatusBadge(dragEl);
1202
+ }
1203
+ }, 600);
1204
+ }
1205
+
1206
+ const wrongBehavior = (this.projectData.settings && this.projectData.settings.wrongAnswerBehavior) || 'return';
1207
+ if (wrongBehavior === 'return') {
1208
+ this.returnToOrigin(dragId);
1209
+ } else {
1210
+ this.snapToTarget(dragId, targetObj);
1211
+ this.placedMap[dragId] = targetId;
1212
+ }
1213
+
1214
+ this.emit('drop', { dragId, targetId, isCorrect: false });
1215
+ this.emit('step', this.getState());
1216
+ }
1217
+ }
1218
+
1219
+ // Hedefe Hizalama / Yerleştirme
1220
+ snapToTarget(dragId, targetObj) {
1221
+ const dragEl = this.container.querySelector(`[data-id="${dragId}"]`);
1222
+ const dragObj = this.getObj(dragId);
1223
+ if (!dragEl || !dragObj) return;
1224
+
1225
+ let targetX = targetObj.x;
1226
+ let targetY = targetObj.y;
1227
+
1228
+ if (targetObj.role === 'category') {
1229
+ // Kategori içine yerleştirme: kategori içindeki mevcut öğe sayısına göre gridleme
1230
+ const existingItems = Object.keys(this.placedMap).filter(id => this.placedMap[id] === targetObj.id && id !== dragId);
1231
+ const slotIndex = existingItems.length;
1232
+
1233
+ const col = slotIndex % 2;
1234
+ const row = Math.floor(slotIndex / 2);
1235
+
1236
+ targetX = targetObj.x + 20 + col * (dragObj.width + 10);
1237
+ targetY = targetObj.y + 60 + row * (dragObj.height + 10);
1238
+ } else if (targetObj.role === 'sequence_slot' || targetObj.role === 'target') {
1239
+ // Merkeze hizalama
1240
+ targetX = targetObj.x + (targetObj.width - dragObj.width) / 2;
1241
+ targetY = targetObj.y + (targetObj.height - dragObj.height) / 2;
1242
+ }
1243
+
1244
+ dragEl.style.transition = 'transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1)';
1245
+ dragEl.style.transform = `translate(${Math.round(targetX)}px, ${Math.round(targetY)}px) rotate(0deg)`;
1246
+
1247
+ setTimeout(() => {
1248
+ if (dragEl) dragEl.style.transition = '';
1249
+ }, 260);
1250
+ }
1251
+
1252
+ returnToOrigin(dragId) {
1253
+ const dragEl = this.container.querySelector(`[data-id="${dragId}"]`);
1254
+ const origin = this.originPositions[dragId];
1255
+ if (!dragEl || !origin) return;
1256
+
1257
+ delete this.placedMap[dragId];
1258
+ this.removeStatusBadge(dragEl);
1259
+ if (dragEl) dragEl.classList.remove('is-incorrect');
1260
+
1261
+ if (dragEl.classList.contains('is-correct')) {
1262
+ dragEl.classList.remove('is-correct');
1263
+ dragEl.classList.remove('is-locked');
1264
+ dragEl.setAttribute('tabindex', '0');
1265
+ this.correctCount = Math.max(0, this.correctCount - 1);
1266
+ this.updateStats();
1267
+ }
1268
+
1269
+ const origObj = this.getObj(dragId);
1270
+ const rot = (origObj && origObj.rotation) ? origObj.rotation : 0;
1271
+
1272
+ dragEl.style.transition = 'transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1)';
1273
+ dragEl.style.transform = `translate(${origin.x}px, ${origin.y}px) rotate(${rot}deg) scale(1)`;
1274
+
1275
+ setTimeout(() => {
1276
+ if (dragEl) {
1277
+ dragEl.style.transition = '';
1278
+ dragEl.style.zIndex = origObj && origObj.zIndex ? `${origObj.zIndex + 10}` : '20';
1279
+ }
1280
+ }, 360);
1281
+ }
1282
+
1283
+ // Toplu Değerlendirme (Değerlendirme Modunda Kontrol Et butonu ile)
1284
+ checkAllAnswers() {
1285
+ const answerKey = this.projectData.answerKey || {};
1286
+ let correct = 0;
1287
+ this.attempts++;
1288
+
1289
+ const placedCount = Object.keys(this.placedMap).length;
1290
+ if (placedCount === 0) {
1291
+ this.setFeedback('Henüz bir nesne yerleştirmediniz. Sürükleyip bırakın.', 'warning', 3000);
1292
+ return;
1293
+ }
1294
+
1295
+ let hasWrong = false;
1296
+ for (const [dragId, targetId] of Object.entries(this.placedMap)) {
1297
+ const isRight = answerKey[dragId] === targetId;
1298
+ const dragEl = this.container.querySelector(`[data-id="${dragId}"]`);
1299
+ const targetObj = this.getObj(targetId);
1300
+ const targetEl = this.container.querySelector(`[data-id="${targetId}"]`);
1301
+
1302
+ if (isRight) {
1303
+ correct++;
1304
+ if (dragEl) {
1305
+ dragEl.classList.remove('is-incorrect');
1306
+ dragEl.classList.add('is-correct');
1307
+ dragEl.classList.add('is-locked');
1308
+ dragEl.setAttribute('tabindex', '-1');
1309
+ this.addStatusBadge(dragEl, 'correct');
1310
+ }
1311
+ if (targetObj && targetObj.role !== 'category') {
1312
+ this.lockedTargetIds.add(targetId);
1313
+ if (targetEl) targetEl.classList.add('is-target-locked');
1314
+ }
1315
+ } else {
1316
+ hasWrong = true;
1317
+ if (dragEl) {
1318
+ dragEl.classList.remove('is-correct');
1319
+ dragEl.classList.add('is-incorrect');
1320
+ this.addStatusBadge(dragEl, 'incorrect');
1321
+ }
1322
+ }
1323
+ }
1324
+
1325
+ this.correctCount = correct;
1326
+ this.updateStats();
1327
+
1328
+ if (correct === this.totalDraggables) {
1329
+ this.isCompleted = true;
1330
+ this.audio.playComplete();
1331
+ this.showCompletionResult();
1332
+ } else {
1333
+ this.audio.playWrong();
1334
+ this.setFeedback(`${correct} / ${this.totalDraggables} doğru. Yanlışları Tekrar Dene butonuyla düzeltebilirsiniz.`, 'warning', 4000);
1335
+ this.showHint('Bazı eşleştirmeler hatalı. "Tekrar Dene" butonuna basarak yanlışları eski yerine gönderebilirsiniz.');
1336
+ if (hasWrong) {
1337
+ this.setCheckButtonState('retry');
1338
+ }
1339
+ }
1340
+ }
1341
+
1342
+ // Değerlendirme Modunda Yanlışları Eski Yerine Gönderip Tekrar Dene Döngüsü
1343
+ retryWrongAnswers() {
1344
+ const answerKey = this.projectData.answerKey || {};
1345
+ const wrongIds = [];
1346
+
1347
+ for (const [dragId, targetId] of Object.entries(this.placedMap)) {
1348
+ if (answerKey[dragId] !== targetId) {
1349
+ wrongIds.push(dragId);
1350
+ }
1351
+ }
1352
+
1353
+ wrongIds.forEach(dragId => {
1354
+ const dragEl = this.container.querySelector(`[data-id="${dragId}"]`);
1355
+ if (dragEl) {
1356
+ dragEl.classList.remove('is-incorrect');
1357
+ this.removeStatusBadge(dragEl);
1358
+ }
1359
+ this.returnToOrigin(dragId);
1360
+ });
1361
+
1362
+ this.setCheckButtonState('check');
1363
+ this.showHint('Kalan nesneleri boş hedeflere sürükleyip tekrar kontrol edin.');
1364
+ }
1365
+
1366
+ checkCompletion() {
1367
+ if (this.correctCount >= this.totalDraggables && this.totalDraggables > 0) {
1368
+ if (!this.isCompleted) {
1369
+ this.isCompleted = true;
1370
+ this.audio.playComplete();
1371
+ setTimeout(() => this.showCompletionResult(), 400);
1372
+ }
1373
+ }
1374
+ }
1375
+
1376
+ setCompletionResultVisible(isVisible) {
1377
+ const resultCard = this.container.querySelector('#completion-result');
1378
+ const controls = this.container.querySelector('#player-controls');
1379
+ const footer = this.container.querySelector('#player-footer');
1380
+ const replayButton = this.container.querySelector('#btn-play-again');
1381
+
1382
+ if (resultCard) resultCard.hidden = !isVisible;
1383
+ if (controls) controls.hidden = isVisible;
1384
+ if (footer) footer.hidden = isVisible;
1385
+
1386
+ if (isVisible && replayButton && typeof replayButton.focus === 'function') {
1387
+ try {
1388
+ replayButton.focus({ preventScroll: true });
1389
+ } catch (error) {
1390
+ replayButton.focus();
1391
+ }
1392
+ }
1393
+
1394
+ this.updateScale();
1395
+ }
1396
+
1397
+ showCompletionResult() {
1398
+ const accuracy = Math.round((this.totalDraggables / Math.max(this.attempts, this.totalDraggables)) * 100);
1399
+ this.setCompletionResultVisible(true);
1400
+
1401
+ const payload = {
1402
+ score: accuracy,
1403
+ attempts: this.attempts,
1404
+ correctCount: this.correctCount
1405
+ };
1406
+
1407
+ this.emit('complete', payload);
1408
+ }
1409
+
1410
+ updateStats() {
1411
+ const statCorrect = this.container.querySelector('#stat-correct');
1412
+ const statAttempts = this.container.querySelector('#stat-attempts');
1413
+ if (statCorrect) statCorrect.textContent = this.correctCount;
1414
+ if (statAttempts) statAttempts.textContent = this.attempts;
1415
+ }
1416
+
1417
+ showHint(msg) {
1418
+ const hintEl = this.container.querySelector('#player-hint');
1419
+ if (hintEl) {
1420
+ hintEl.textContent = `💡 ${msg}`;
1421
+ }
1422
+ }
1423
+
1424
+ /**
1425
+ * Kontrol alanındaki geri bildirim mesajını günceller
1426
+ * @param {string} message - Gösterilecek mesaj metni
1427
+ * @param {'info'|'success'|'error'|'warning'} [type='info'] - Mesaj türü
1428
+ * @param {number} [duration=0] - Milisaniye cinsinden otomatik sıfırlanma süresi (0 ise kalıcı)
1429
+ */
1430
+ setFeedback(message, type = 'info', duration = 0) {
1431
+ const fbEl = this.container.querySelector('#player-feedback');
1432
+ const textEl = this.container.querySelector('#feedback-text');
1433
+ const iconEl = this.container.querySelector('#feedback-icon');
1434
+ if (!fbEl || !textEl) return;
1435
+
1436
+ this.currentFeedback = { message, type };
1437
+
1438
+ const icons = {
1439
+ info: '💬',
1440
+ success: '✓',
1441
+ error: '✕',
1442
+ warning: '⚠️'
1443
+ };
1444
+
1445
+ fbEl.className = `player-feedback feedback-${type}`;
1446
+ textEl.textContent = message;
1447
+ if (iconEl) {
1448
+ iconEl.textContent = icons[type] || '💬';
1449
+ }
1450
+
1451
+ if (this.feedbackTimeout) {
1452
+ clearTimeout(this.feedbackTimeout);
1453
+ this.feedbackTimeout = null;
1454
+ }
1455
+
1456
+ if (duration > 0) {
1457
+ this.feedbackTimeout = setTimeout(() => {
1458
+ this.resetFeedback();
1459
+ }, duration);
1460
+ }
1461
+ }
1462
+
1463
+ getFeedback() {
1464
+ return this.currentFeedback || { message: '', type: 'info' };
1465
+ }
1466
+
1467
+ resetFeedback() {
1468
+ this.setFeedback('Öğeleri doğru hedeflere sürükleyin', 'info');
1469
+ }
1470
+
1471
+ renderLineSVG(obj) {
1472
+ const w = Math.max(10, obj.width || 200);
1473
+ const h = Math.max(10, obj.height || 24);
1474
+ const cy = h / 2;
1475
+ const strokeColor = (obj.borderColor && obj.borderColor !== 'transparent') ? obj.borderColor : '#1e293b';
1476
+ const strokeWidth = (obj.borderWidth !== undefined && obj.borderWidth > 0) ? obj.borderWidth : 3;
1477
+ const strokeStyle = obj.borderStyle || 'solid';
1478
+
1479
+ let dashArray = 'none';
1480
+ if (strokeStyle === 'dashed') dashArray = '8 6';
1481
+ else if (strokeStyle === 'dotted') dashArray = '2 6';
1482
+
1483
+ const hasStartArrow = !!obj.arrowStart;
1484
+ const hasEndArrow = !!obj.arrowEnd;
1485
+ const arrowSize = Math.min(Math.max(8, strokeWidth * 3), w / 2.5);
1486
+
1487
+ const x1 = hasStartArrow ? arrowSize - 1 : 0;
1488
+ const x2 = hasEndArrow ? w - (arrowSize - 1) : w;
1489
+
1490
+ 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;">`;
1491
+
1492
+ // Ana Çizgi
1493
+ svgHtml += `<line x1="${x1}" y1="${cy}" x2="${x2}" y2="${cy}" stroke="${strokeColor}" stroke-width="${strokeWidth}" stroke-dasharray="${dashArray}" stroke-linecap="round" />`;
1494
+
1495
+ // Başlangıç Oku
1496
+ if (hasStartArrow) {
1497
+ svgHtml += `<polygon points="${arrowSize},${cy - arrowSize * 0.6} 0,${cy} ${arrowSize},${cy + arrowSize * 0.6}" fill="${strokeColor}" />`;
1498
+ }
1499
+
1500
+ // Bitiş Oku
1501
+ if (hasEndArrow) {
1502
+ svgHtml += `<polygon points="${w - arrowSize},${cy - arrowSize * 0.6} ${w},${cy} ${w - arrowSize},${cy + arrowSize * 0.6}" fill="${strokeColor}" />`;
1503
+ }
1504
+
1505
+ svgHtml += `</svg>`;
1506
+ return svgHtml;
1507
+ }
1508
+
1509
+ resetGame() {
1510
+ this.placedMap = {};
1511
+ this.score = 0;
1512
+ this.attempts = 0;
1513
+ this.correctCount = 0;
1514
+ this.isCompleted = false;
1515
+ this.lockedTargetIds.clear();
1516
+
1517
+ this.container.querySelectorAll('.player-status-badge').forEach(el => el.remove());
1518
+ this.container.querySelectorAll('.is-target-locked').forEach(el => el.classList.remove('is-target-locked'));
1519
+
1520
+ this.setCheckButtonState('check');
1521
+ this.updateStats();
1522
+ this.resetFeedback();
1523
+ this.setCompletionResultVisible(false);
1524
+
1525
+ this.prepareItems();
1526
+ this.emit('reset', this.getState());
1527
+ }
1528
+ }
1529
+
1530
+ windowObj.DND.ActivityEngine = ActivityEngine;
1531
+
1532
+ var ActivityPlayer = class {
1533
+ constructor(options) {
1534
+ this.options = Object.assign({
1535
+ sound: true,
1536
+ stagePadding: null,
1537
+ onComplete: null,
1538
+ onStep: null,
1539
+ onDrop: null,
1540
+ onReset: null,
1541
+ onReady: null
1542
+ }, options || {});
1543
+
1544
+ this.container = null;
1545
+ this.engine = null;
1546
+ this.isDestroyed = false;
1547
+
1548
+ if (this.options.target) {
1549
+ this.resolveContainer(this.options.target);
1550
+ }
1551
+
1552
+ if (this.options.data) {
1553
+ this.mount(this.options.data);
1554
+ } else if (this.options.src) {
1555
+ this.loadFromUrl(this.options.src);
1556
+ } else if (this.options.file) {
1557
+ this.loadFromFile(this.options.file);
1558
+ } else if (this.options.jsonString) {
1559
+ this.loadFromJsonString(this.options.jsonString);
1560
+ }
1561
+ }
1562
+
1563
+ resolveContainer(target) {
1564
+ if (typeof target === 'string') {
1565
+ this.container = document.querySelector(target);
1566
+ } else if (target instanceof HTMLElement) {
1567
+ this.container = target;
1568
+ }
1569
+ if (!this.container) {
1570
+ throw new Error('[ActivityPlayer] Hedef container bulunamadı: ' + target);
1571
+ }
1572
+ }
1573
+
1574
+ async loadFromUrl(url) {
1575
+ try {
1576
+ var response = await fetch(url);
1577
+ if (!response.ok) throw new Error('HTTP ' + response.status);
1578
+ var data = await response.json();
1579
+ this.mount(data);
1580
+ } catch (err) {
1581
+ console.error('[ActivityPlayer] Veri yüklenemedi:', err);
1582
+ if (this.container) {
1583
+ this.container.innerHTML = '<div style="padding:20px;color:red;">⚠️ Veri yüklenemedi: ' + err.message + '</div>';
1584
+ }
1585
+ throw err;
1586
+ }
1587
+ }
1588
+
1589
+ async loadFromFile(file) {
1590
+ if (!file) throw new Error('[ActivityPlayer] Dosya nesnesi boş olamaz.');
1591
+ try {
1592
+ var text = '';
1593
+ if (typeof file.text === 'function') {
1594
+ text = await file.text();
1595
+ } else {
1596
+ text = await new Promise((resolve, reject) => {
1597
+ var reader = new FileReader();
1598
+ reader.onload = () => resolve(reader.result);
1599
+ reader.onerror = () => reject(reader.error);
1600
+ reader.readAsText(file, 'utf-8');
1601
+ });
1602
+ }
1603
+ this.loadFromJsonString(text);
1604
+ } catch (err) {
1605
+ console.error('[ActivityPlayer] Dosya okunamadı:', err);
1606
+ if (this.container) {
1607
+ this.container.innerHTML = '<div style="padding:20px;color:red;">⚠️ Dosya okunamadı: ' + err.message + '</div>';
1608
+ }
1609
+ throw err;
1610
+ }
1611
+ }
1612
+
1613
+ loadFromJsonString(jsonString) {
1614
+ try {
1615
+ var data = typeof jsonString === 'string' ? JSON.parse(jsonString) : jsonString;
1616
+ this.mount(data);
1617
+ } catch (err) {
1618
+ console.error('[ActivityPlayer] JSON ayrıştırma hatası:', err);
1619
+ if (this.container) {
1620
+ this.container.innerHTML = '<div style="padding:20px;color:red;">⚠️ Geçersiz JSON: ' + err.message + '</div>';
1621
+ }
1622
+ throw err;
1623
+ }
1624
+ }
1625
+
1626
+ mount(projectData) {
1627
+ if (this.isDestroyed || !this.container) return;
1628
+ if (this.engine) {
1629
+ this.engine.destroy();
1630
+ this.engine = null;
1631
+ }
1632
+
1633
+ var engineOptions = {
1634
+ isEmbedded: false,
1635
+ stagePadding: this.options.stagePadding,
1636
+ onComplete: (res) => {
1637
+ if (typeof this.options.onComplete === 'function') this.options.onComplete(res);
1638
+ },
1639
+ onStep: (state) => {
1640
+ if (typeof this.options.onStep === 'function') this.options.onStep(state);
1641
+ },
1642
+ onDrop: (dropInfo) => {
1643
+ if (typeof this.options.onDrop === 'function') this.options.onDrop(dropInfo);
1644
+ },
1645
+ onReset: (state) => {
1646
+ if (typeof this.options.onReset === 'function') this.options.onReset(state);
1647
+ }
1648
+ };
1649
+
1650
+ if (projectData.settings) {
1651
+ projectData.settings.soundEnabled = this.options.sound !== false;
1652
+ }
1653
+
1654
+ this.engine = new ActivityEngine(this.container, projectData, engineOptions);
1655
+
1656
+ if (typeof this.options.onReady === 'function') {
1657
+ this.options.onReady(this);
1658
+ }
1659
+ }
1660
+
1661
+ on(event, callback) {
1662
+ return this.engine ? this.engine.on(event, callback) : function(){};
1663
+ }
1664
+
1665
+ off(event, callback) {
1666
+ if (this.engine) this.engine.off(event, callback);
1667
+ }
1668
+
1669
+ getState() {
1670
+ return this.engine ? this.engine.getState() : null;
1671
+ }
1672
+
1673
+ setSound(enabled) {
1674
+ if (this.engine && this.engine.audio) {
1675
+ this.engine.audio.setEnabled(!!enabled);
1676
+ }
1677
+ }
1678
+
1679
+ reset() {
1680
+ if (this.engine) this.engine.resetGame();
1681
+ }
1682
+
1683
+ destroy() {
1684
+ if (this.isDestroyed) return;
1685
+ if (this.engine) {
1686
+ this.engine.destroy();
1687
+ this.engine = null;
1688
+ }
1689
+ if (this.container) {
1690
+ this.container.innerHTML = '';
1691
+ }
1692
+ this.isDestroyed = true;
1693
+ }
1694
+ };
1695
+
1696
+ var DNDPlayerElement = class extends HTMLElement {
1697
+ static get observedAttributes() { return ['src', 'sound']; }
1698
+ constructor() {
1699
+ super();
1700
+ this._player = null;
1701
+ this._data = null;
1702
+ }
1703
+ connectedCallback() { this.render(); }
1704
+ disconnectedCallback() { if (this._player) { this._player.destroy(); this._player = null; } }
1705
+ attributeChangedCallback(name, oldValue, newValue) {
1706
+ if (oldValue === newValue || !this.isConnected) return;
1707
+ if (name === 'src' && newValue) this.initPlayer();
1708
+ else if (name === 'sound' && this._player) this._player.setSound(newValue !== 'false');
1709
+ }
1710
+ get data() { return this._data; }
1711
+ set data(val) { this._data = val; this.initPlayer(); }
1712
+ get player() { return this._player; }
1713
+ render() {
1714
+ this.style.display = 'block';
1715
+ this.style.position = 'relative';
1716
+ this.style.width = '100%';
1717
+ this.style.minHeight = this.style.minHeight || '500px';
1718
+ this.initPlayer();
1719
+ }
1720
+ initPlayer() {
1721
+ if (this._player) { this._player.destroy(); this._player = null; }
1722
+ var src = this.getAttribute('src');
1723
+ var soundAttr = this.getAttribute('sound');
1724
+ if (!src && !this._data) return;
1725
+ this._player = new ActivityPlayer({
1726
+ target: this,
1727
+ src: src || undefined,
1728
+ data: this._data || undefined,
1729
+ sound: soundAttr !== 'false',
1730
+ onComplete: (stats) => this.dispatchEvent(new CustomEvent('dnd:complete', { detail: stats, bubbles: true })),
1731
+ onStep: (state) => this.dispatchEvent(new CustomEvent('dnd:step', { detail: state, bubbles: true })),
1732
+ onDrop: (dropInfo) => this.dispatchEvent(new CustomEvent('dnd:drop', { detail: dropInfo, bubbles: true })),
1733
+ onReset: (state) => this.dispatchEvent(new CustomEvent('dnd:reset', { detail: state, bubbles: true })),
1734
+ onReady: (p) => this.dispatchEvent(new CustomEvent('dnd:ready', { detail: { player: p }, bubbles: true }))
1735
+ });
1736
+ }
1737
+ loadFile(file) {
1738
+ if (this._player) this._player.loadFromFile(file);
1739
+ else this._player = new ActivityPlayer({ target: this, file: file, sound: this.getAttribute('sound') !== 'false' });
1740
+ }
1741
+ loadJsonString(str) {
1742
+ if (this._player) this._player.loadFromJsonString(str);
1743
+ else this._player = new ActivityPlayer({ target: this, jsonString: str, sound: this.getAttribute('sound') !== 'false' });
1744
+ }
1745
+ reset() { if (this._player) this._player.reset(); }
1746
+ getState() { return this._player ? this._player.getState() : null; }
1747
+ };
1748
+
1749
+ if (typeof customElements !== 'undefined' && !customElements.get('dnd-player')) {
1750
+ customElements.define('dnd-player', DNDPlayerElement);
1751
+ }
1752
+
1753
+ exports.ActivityPlayer = ActivityPlayer;
1754
+ exports.DNDPlayerElement = DNDPlayerElement;
1755
+ exports.ActivityEngine = ActivityEngine;
1756
+ exports.AudioSynthesizer = AudioSynthesizer;
1757
+ exports.ValidationEngine = ValidationEngine;
1758
+ }));