@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.
- package/LICENSE +21 -0
- package/README.md +327 -2
- package/css/answer-key.css +200 -0
- package/css/editor.css +1797 -0
- package/css/player.css +782 -0
- package/css/stage.css +490 -0
- package/dist/dnd-player-element.js +1758 -0
- package/dist/dnd.js +2041 -0
- package/dist/editor.css +3278 -0
- package/dist/editor.d.ts +23 -0
- package/dist/editor.js +8506 -0
- package/dist/index.d.ts +102 -0
- package/dist/player.css +782 -0
- package/dist/player.d.ts +1 -0
- package/dist/player.js +2041 -0
- package/dist/player.umd.cjs +1758 -0
- package/index.html +203 -0
- package/js/app.js +188 -0
- package/js/core/AnswerKeyManager.js +494 -0
- package/js/core/EditorCore.js +220 -0
- package/js/core/HistoryManager.js +194 -0
- package/js/core/LayerManager.js +86 -0
- package/js/core/ObjectManager.js +468 -0
- package/js/core/SelectionManager.js +459 -0
- package/js/core/StageManager.js +295 -0
- package/js/data/SampleProjects.js +883 -0
- package/js/engine/TransformEngine.js +632 -0
- package/js/engine/ValidationEngine.js +147 -0
- package/js/player/ActivityEngine.js +1272 -0
- package/js/player/ExportManager.js +820 -0
- package/js/ui/MiniToolbar.js +1546 -0
- package/js/ui/PropertyPanel.js +776 -0
- package/js/ui/UIManager.js +596 -0
- package/js/utils/AudioSynthesizer.js +111 -0
- package/package.json +69 -4
- package/src/ActivityPlayer.js +309 -0
- package/src/DNDEditor.js +217 -0
- package/src/components/DNDPlayerElement.js +191 -0
- package/src/index.js +12 -0
- package/src/player.js +13 -0
|
@@ -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
|
+
}));
|