Files
devx-rp/resources/[devx]/devx_characters/web/app.js
T
2026-08-05 01:29:39 +02:00

276 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'
}[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}`));