const resource = typeof GetParentResourceName === 'function' ? GetParentResourceName() : 'devx_characters'; const app = document.querySelector('#app'); const charactersEl = document.querySelector('#characters'); const createOverlay = document.querySelector('#createOverlay'); const deleteOverlay = document.querySelector('#deleteOverlay'); const createForm = document.querySelector('#createForm'); const newButton = document.querySelector('#newCharacter'); const playButton = document.querySelector('#playCharacter'); const deleteButton = document.querySelector('#deleteCharacter'); const sexSelect = document.querySelector('#sexSelect'); const messageEl = document.querySelector('#message'); const slotCounter = document.querySelector('#slotCounter'); const selectedName = document.querySelector('#selectedName'); const selectedDob = document.querySelector('#selectedDob'); const selectedSlot = document.querySelector('#selectedSlot'); const selectedPhone = document.querySelector('#selectedPhone'); const selectedStatus = document.querySelector('#selectedStatus'); const dialogue = document.querySelector('#dialogue'); const dialogueSpeaker = document.querySelector('#dialogueSpeaker'); const dialogueText = document.querySelector('#dialogueText'); const createButton = createForm.querySelector('button[type="submit"]'); const deleteConfirmInput = document.querySelector('#deleteConfirmInput'); const confirmDeleteButton = document.querySelector('#confirmDelete'); let maxCharacters = 3; let characters = []; let selectedId = null; let deleteTarget = null; async function post(event, data = {}) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); try { const response = await fetch(`https://${resource}/${event}`, { method: 'POST', headers: { 'Content-Type': 'application/json; charset=UTF-8' }, body: JSON.stringify(data), signal: controller.signal }); const body = await response.text(); if (!response.ok) throw new Error(`NUI ${event}: HTTP ${response.status}`); if (!body) return { ok: true }; return JSON.parse(body); } catch (error) { if (error.name === 'AbortError') throw new Error('Der Server hat nach 30 Sekunden nicht geantwortet.'); throw error; } finally { clearTimeout(timeout); } } function escapeHtml(value) { return String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char])); } function message(text = '', type = 'error') { messageEl.textContent = text; messageEl.dataset.type = type; messageEl.classList.toggle('hidden', !text); } function selectedCharacter() { return characters.find(character => Number(character.id) === Number(selectedId)) || null; } function formatDate(value) { if (typeof value === 'number' || /^\d{11,}$/.test(String(value || ''))) { const date = new Date(Number(value)); if (!Number.isNaN(date.getTime())) return date.toLocaleDateString('de-DE'); } const match = String(value || '').match(/^(\d{4})-(\d{2})-(\d{2})/); return match ? `${match[3]}.${match[2]}.${match[1]}` : String(value || '–'); } function previewCharacter(character) { const payload = character ? { model: character.model, sex: character.sex } : { model: sexSelect.value === 'f' ? 'mp_f_freemode_01' : 'mp_m_freemode_01', sex: sexSelect.value }; post('preview', payload).catch(error => message(`Vorschaufehler: ${error.message}`)); } function updateDetails() { const character = selectedCharacter(); const hasCharacter = Boolean(character); selectedName.textContent = hasCharacter ? `${character.firstName} ${character.lastName}` : 'Kein Charakter'; selectedDob.textContent = hasCharacter ? formatDate(character.dateOfBirth) : '–'; selectedSlot.textContent = hasCharacter ? `Slot ${character.slot}` : '–'; selectedPhone.textContent = hasCharacter ? (character.phoneNumber || 'Wird erstellt') : '–'; selectedStatus.textContent = hasCharacter ? (character.introCompleted ? 'Einwohner' : 'Neue Ankunft') : '–'; playButton.disabled = !hasCharacter; deleteButton.disabled = !hasCharacter; } function selectCharacter(characterId, shouldPreview = true) { selectedId = characterId == null ? null : Number(characterId); document.querySelectorAll('.character-slot').forEach(card => { card.classList.toggle('active', Number(card.dataset.characterId) === Number(selectedId)); }); updateDetails(); if (shouldPreview) previewCharacter(selectedCharacter()); } function openCreator() { if (characters.length >= maxCharacters) return; message(''); createForm.reset(); createOverlay.classList.remove('hidden'); previewCharacter(null); setTimeout(() => createForm.querySelector('input')?.focus(), 50); } function closeCreator() { createOverlay.classList.add('hidden'); createForm.reset(); previewCharacter(selectedCharacter()); } function openDelete() { deleteTarget = selectedCharacter(); if (!deleteTarget) return; document.querySelector('#deleteDescription').textContent = `${deleteTarget.firstName} ${deleteTarget.lastName} wird mit Bankkonto, Telefonnummer und Fortschritt dauerhaft gelöscht.`; deleteConfirmInput.value = ''; confirmDeleteButton.disabled = true; deleteOverlay.classList.remove('hidden'); setTimeout(() => deleteConfirmInput.focus(), 50); } function closeDelete() { deleteOverlay.classList.add('hidden'); deleteTarget = null; deleteConfirmInput.value = ''; } function render() { charactersEl.innerHTML = ''; slotCounter.textContent = `${characters.length} / ${maxCharacters}`; for (let slot = 1; slot <= maxCharacters; slot += 1) { const character = characters.find(entry => Number(entry.slot) === slot); const card = document.createElement('button'); card.type = 'button'; card.className = `character-slot ${character ? '' : 'vacant'}`; if (character) { card.dataset.characterId = character.id; card.innerHTML = ` ${String(slot).padStart(2, '0')} ${escapeHtml(character.firstName)} ${escapeHtml(character.lastName)} ${character.introCompleted ? 'Einwohner von DevX-City' : 'Neue Ankunft'} `; card.addEventListener('click', () => selectCharacter(character.id)); } else { card.innerHTML = ` ${String(slot).padStart(2, '0')} Freier CharakterplatzNeue Identität erstellen +`; card.addEventListener('click', openCreator); } charactersEl.appendChild(card); } newButton.disabled = characters.length >= maxCharacters; if (!selectedCharacter()) selectedId = characters[0]?.id ?? null; selectCharacter(selectedId, true); } playButton.addEventListener('click', async () => { const character = selectedCharacter(); if (!character) return; playButton.disabled = true; message('Charakter wird geladen …', 'info'); try { const result = await post('select', { characterId: Number(character.id) }); if (!result.ok) { message(result.error || 'Charakter konnte nicht geladen werden.'); playButton.disabled = false; } } catch (error) { message(`Verbindungsfehler: ${error.message}`); playButton.disabled = false; } }); deleteButton.addEventListener('click', openDelete); document.querySelector('#cancelDelete').addEventListener('click', closeDelete); deleteConfirmInput.addEventListener('input', () => { confirmDeleteButton.disabled = deleteConfirmInput.value.trim().toUpperCase() !== 'LÖSCHEN'; }); confirmDeleteButton.addEventListener('click', async () => { if (!deleteTarget || deleteConfirmInput.value.trim().toUpperCase() !== 'LÖSCHEN') return; confirmDeleteButton.disabled = true; const targetId = Number(deleteTarget.id); closeDelete(); message('Charakter wird gelöscht …', 'info'); try { const result = await post('delete', { characterId: targetId }); if (!result.ok) { message(result.error || 'Löschen fehlgeschlagen.'); return; } characters = Array.isArray(result.characters) ? result.characters : characters.filter(entry => Number(entry.id) !== targetId); selectedId = characters[0]?.id ?? null; render(); message('Charakter wurde dauerhaft gelöscht.', 'success'); } catch (error) { message(`Verbindungsfehler: ${error.message}`); } }); newButton.addEventListener('click', openCreator); document.querySelector('#cancelCreate').addEventListener('click', closeCreator); document.querySelector('#cancelCreateBottom').addEventListener('click', closeCreator); sexSelect.addEventListener('change', () => previewCharacter(null)); createForm.addEventListener('submit', async event => { event.preventDefault(); message('Charakter wird erstellt …', 'info'); createButton.disabled = true; const oldLabel = createButton.textContent; createButton.textContent = 'Wird erstellt …'; try { const result = await post('create', Object.fromEntries(new FormData(createForm))); if (!result.ok) { message(result.error || 'Erstellung fehlgeschlagen.'); return; } characters = Array.isArray(result.characters) ? result.characters : characters; selectedId = Number(result.characterId); closeCreator(); render(); message(`Charakter erstellt. Telefonnummer: ${result.phoneNumber || 'wird geladen'}`, 'success'); } catch (error) { message(`Verbindungsfehler: ${error.message}`); } finally { createButton.disabled = false; createButton.textContent = oldLabel; } }); window.addEventListener('keydown', event => { if (event.key !== 'Escape') return; if (!deleteOverlay.classList.contains('hidden')) closeDelete(); else if (!createOverlay.classList.contains('hidden')) closeCreator(); }); window.addEventListener('message', event => { const data = event.data || {}; if (data.action === 'open') app.classList.remove('hidden'); if (data.action === 'close') { app.classList.add('hidden'); createOverlay.classList.add('hidden'); deleteOverlay.classList.add('hidden'); message(''); } if (data.action === 'characters') { characters = data.characters || []; maxCharacters = Number(data.maxCharacters) || 3; render(); } if (data.action === 'error') message(data.message || 'Unbekannter Fehler.'); if (data.action === 'dialogue') { dialogueSpeaker.textContent = data.speaker || 'Unbekannt'; dialogueText.textContent = data.text || ''; dialogue.classList.remove('hidden'); } if (data.action === 'dialogueHide') dialogue.classList.add('hidden'); }); post('ready').catch(error => message(`Startfehler: ${error.message}`));