276 lines
11 KiB
JavaScript
276 lines
11 KiB
JavaScript
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 = `
|
||
<span class="slot-number">${String(slot).padStart(2, '0')}</span>
|
||
<span class="slot-content">
|
||
<strong>${escapeHtml(character.firstName)} ${escapeHtml(character.lastName)}</strong>
|
||
<small>${character.introCompleted ? 'Einwohner von DevX-City' : 'Neue Ankunft'}</small>
|
||
</span>
|
||
<span class="slot-arrow">›</span>`;
|
||
card.addEventListener('click', () => selectCharacter(character.id));
|
||
} else {
|
||
card.innerHTML = `
|
||
<span class="slot-number">${String(slot).padStart(2, '0')}</span>
|
||
<span class="slot-content"><strong>Freier Charakterplatz</strong><small>Neue Identität erstellen</small></span>
|
||
<span class="slot-plus">+</span>`;
|
||
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}`));
|