changed die to model
This commit is contained in:
parent
9b635ac49f
commit
50ab239115
19
index.html
19
index.html
@ -39,7 +39,17 @@
|
||||
<span class="icons">
|
||||
<i class="fa-solid fa-expand"></i>
|
||||
</span>
|
||||
<span class="dice bowl--black">
|
||||
<span class="icons" id="dice-button">
|
||||
<i class="fa-solid fa-dice"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<main></main>
|
||||
|
||||
<div class="modal" id="dice-modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-button">×</span>
|
||||
<div class="die-container">
|
||||
<div class="content">
|
||||
<div class="die">
|
||||
<figure class="face face-1"></figure>
|
||||
@ -64,12 +74,9 @@
|
||||
<figure class="face face-20"></figure>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<main></main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script defer src="menu/menu.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
191
script.js
191
script.js
@ -2,9 +2,9 @@
|
||||
|
||||
// ---------- Config ----------
|
||||
const STARTING_LIFE = 20;
|
||||
const POISON_LOSS = 10; // poison counters to lose
|
||||
const COMMANDER_LOSS = 21; // commander damage from a single opponent to lose
|
||||
const NAME_MAX_CHARS = 18; // trim player names
|
||||
const POISON_LOSS = 10;
|
||||
const COMMANDER_LOSS = 21;
|
||||
const NAME_MAX_CHARS = 18;
|
||||
|
||||
// ---------- Global state ----------
|
||||
let players = [];
|
||||
@ -23,6 +23,9 @@ const poisonToggleButton = document.querySelector('#poison-toggle');
|
||||
const twoPlayerToggleButton = document.querySelector('.fa-user-group');
|
||||
const fullscreenButton = document.querySelector('.fa-expand');
|
||||
const navContainer = document.getElementById('navContainer');
|
||||
const diceButton = document.getElementById('dice-button');
|
||||
const diceModal = document.getElementById('dice-modal');
|
||||
const closeButton = document.querySelector('.close-button');
|
||||
|
||||
// ---------- Utilities ----------
|
||||
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
||||
@ -100,10 +103,63 @@ const createPlayerElement = (playerObj, index) => {
|
||||
|
||||
// ---------- Rendering / Persistence ----------
|
||||
const renderPlayers = () => {
|
||||
mainContainer.innerHTML = '';
|
||||
players.forEach((player, index) => {
|
||||
const el = createPlayerElement(player, index);
|
||||
mainContainer.appendChild(el);
|
||||
const currentPlayers = Array.from(mainContainer.children);
|
||||
|
||||
// Remove player elements that no longer have a corresponding player object.
|
||||
// This handles when a player is removed.
|
||||
currentPlayers.forEach((playerEl, index) => {
|
||||
if (!players[index]) {
|
||||
playerEl.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Iterate through the players state and either update existing DOM elements
|
||||
// or create and append new ones.
|
||||
players.forEach((playerObj, index) => {
|
||||
let playerEl = document.getElementById(`player${index + 1}`);
|
||||
if (playerEl) {
|
||||
// Player card exists, update its content instead of re-creating it.
|
||||
playerEl.querySelector('.name').textContent = sanitizeName(playerObj.name);
|
||||
playerEl.querySelector('.life-count').textContent = playerObj.life;
|
||||
setLifeRingOn(playerEl, playerObj.life);
|
||||
|
||||
const specialCountersDiv = playerEl.querySelector('.special-counters');
|
||||
|
||||
const poisonCounterHTML = isPoisonMatch ? `
|
||||
<div class="poison-counter">
|
||||
<span class="counter-label">Poison:</span>
|
||||
<button class="poison-btn poison-down" data-player-index="${index}">-</button>
|
||||
<span class="poison-value" data-max="${POISON_LOSS}">${playerObj.poison}</span>
|
||||
<button class="poison-btn poison-up" data-player-index="${index}">+</button>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
let commanderDamageHTML = '';
|
||||
if (isCommanderMatch) {
|
||||
commanderDamageHTML = players.map((opponent, oppIndex) => {
|
||||
if (index === oppIndex) return '';
|
||||
const val = playerObj.commanderDamage?.[oppIndex] ?? 0;
|
||||
return `
|
||||
<div class="commander-damage-counter">
|
||||
<span class="commander-name" data-opponent-index="${oppIndex}">${sanitizeName(opponent.name)}</span>
|
||||
<div class="commander-controls">
|
||||
<button class="commander-btn commander-down" data-player-index="${index}" data-opponent-index="${oppIndex}">-</button>
|
||||
<span class="commander-value" data-player-index="${index}" data-opponent-index="${oppIndex}" data-max="${COMMANDER_LOSS}">${val}</span>
|
||||
<button class="commander-btn commander-up" data-player-index="${index}" data-opponent-index="${oppIndex}">+</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
specialCountersDiv.innerHTML = `${poisonCounterHTML} ${commanderDamageHTML}`;
|
||||
|
||||
} else {
|
||||
// Player card does not exist, create a new one.
|
||||
const newEl = createPlayerElement(playerObj, index);
|
||||
mainContainer.appendChild(newEl);
|
||||
}
|
||||
checkElimination(index);
|
||||
});
|
||||
|
||||
document.body.classList.toggle('is-commander-mode', isCommanderMatch);
|
||||
@ -111,9 +167,6 @@ const renderPlayers = () => {
|
||||
document.body.classList.toggle('is-poison-mode', isPoisonMatch);
|
||||
|
||||
initLifeRings();
|
||||
|
||||
// After DOM exists, check elimination state for each
|
||||
players.forEach((_, i) => checkElimination(i));
|
||||
};
|
||||
|
||||
const saveState = () => {
|
||||
@ -287,7 +340,7 @@ const handleLifeChange = (event) => {
|
||||
const btn = event.target.closest('.life-btn');
|
||||
const card = event.target.closest('.player');
|
||||
if (!btn || !card) return;
|
||||
if (card.classList.contains('is-out')) return; // ignore eliminated
|
||||
if (card.classList.contains('is-out')) return;
|
||||
|
||||
const playerIndex = Array.from(mainContainer.children).indexOf(card);
|
||||
const amount = num(btn.dataset.amount, 0);
|
||||
@ -310,12 +363,9 @@ const handlePoisonChange = (event) => {
|
||||
if (playerIndex < 0) return;
|
||||
|
||||
const amount = btn.classList.contains('poison-up') ? 1 : -1;
|
||||
players[playerIndex].poison += amount;
|
||||
players[playerIndex].poison = clamp(players[playerIndex].poison + amount, 0, POISON_LOSS);
|
||||
|
||||
const poisonValue = document.querySelector(`#player${playerIndex + 1} .poison-value`);
|
||||
if (poisonValue) poisonValue.textContent = players[playerIndex].poison;
|
||||
|
||||
checkElimination(playerIndex);
|
||||
renderPlayers(); // Rerender to update the value and check elimination
|
||||
saveState();
|
||||
};
|
||||
|
||||
@ -332,12 +382,9 @@ const handleCommanderChange = (event) => {
|
||||
if (!players[playerIndex].commanderDamage[oppIndex]) {
|
||||
players[playerIndex].commanderDamage[oppIndex] = 0;
|
||||
}
|
||||
players[playerIndex].commanderDamage[oppIndex] += amount;
|
||||
players[playerIndex].commanderDamage[oppIndex] = clamp(players[playerIndex].commanderDamage[oppIndex] + amount, 0, COMMANDER_LOSS);
|
||||
|
||||
const valEl = document.querySelector(`.commander-value[data-player-index="${playerIndex}"][data-opponent-index="${oppIndex}"]`);
|
||||
if (valEl) valEl.textContent = players[playerIndex].commanderDamage[oppIndex];
|
||||
|
||||
checkElimination(playerIndex);
|
||||
renderPlayers(); // Rerender to update the value and check elimination
|
||||
saveState();
|
||||
};
|
||||
|
||||
@ -419,7 +466,6 @@ resetButton?.addEventListener('click', async () => {
|
||||
|
||||
// Poison toggle (normal click)
|
||||
poisonToggleButton?.addEventListener('click', () => {
|
||||
// If long-press fired, its handler swallows this click in capture phase.
|
||||
isPoisonMatch = !isPoisonMatch;
|
||||
renderPlayers();
|
||||
saveState();
|
||||
@ -470,12 +516,29 @@ if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.
|
||||
if (navContainer) navContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
// ---------- Die Roller Modal Logic ----------
|
||||
diceButton?.addEventListener('click', () => {
|
||||
diceModal.style.display = 'flex';
|
||||
});
|
||||
|
||||
closeButton?.addEventListener('click', () => {
|
||||
diceModal.style.display = 'none';
|
||||
});
|
||||
|
||||
window.addEventListener('click', (event) => {
|
||||
if (event.target === diceModal) {
|
||||
diceModal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- D20 no-jump roller with bowl wobble ----------
|
||||
(() => {
|
||||
const die = document.querySelector('.die');
|
||||
const dieContainer = document.querySelector('.die-container');
|
||||
if (!dieContainer) return;
|
||||
const die = dieContainer.querySelector('.die');
|
||||
|
||||
if (!die) return;
|
||||
|
||||
// Wrap .die in .roller once
|
||||
if (!die.parentElement.classList.contains('roller')) {
|
||||
const roller = document.createElement('div');
|
||||
roller.className = 'roller';
|
||||
@ -484,7 +547,6 @@ if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.
|
||||
}
|
||||
const roller = die.parentElement;
|
||||
|
||||
// NEW: wrap .roller in .track once (for bowl translation)
|
||||
if (!roller.parentElement.classList.contains('track')) {
|
||||
const track = document.createElement('div');
|
||||
track.className = 'track';
|
||||
@ -497,9 +559,7 @@ if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.
|
||||
let lastFace = null;
|
||||
let timer = null;
|
||||
|
||||
// Read duration from CSS var (--roll-ms)
|
||||
const msStr = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue('--roll-ms').trim();
|
||||
const msStr = getComputedStyle(document.documentElement).getPropertyValue('--roll-ms').trim();
|
||||
const ANIM_MS = Number(msStr.replace('ms','')) || 1500;
|
||||
|
||||
function randomFace() {
|
||||
@ -519,14 +579,11 @@ if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.
|
||||
function rollOnce(targetFace) {
|
||||
clearTimeout(timer);
|
||||
|
||||
// Set final face FIRST (hidden under spin/wobble)
|
||||
const face = typeof targetFace === 'number' ? targetFace : randomFace();
|
||||
setFace(face);
|
||||
|
||||
// Randomize direction of bowl wobble
|
||||
track.classList.toggle('reverse', Math.random() < 0.5);
|
||||
|
||||
// Start spin + wobble
|
||||
roller.classList.add('rolling');
|
||||
track.classList.add('rolling');
|
||||
|
||||
@ -537,21 +594,22 @@ if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.
|
||||
}, ANIM_MS);
|
||||
}
|
||||
|
||||
// Click to roll
|
||||
(document.querySelector('.content') || roller).addEventListener('click', () => rollOnce());
|
||||
dieContainer.addEventListener('click', (e) => {
|
||||
// Prevent event from bubbling up to close the modal
|
||||
e.stopPropagation();
|
||||
rollOnce();
|
||||
});
|
||||
|
||||
// Hotkeys (optional)
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const k = e.key.toLowerCase();
|
||||
if (k === 'r') rollOnce();
|
||||
if (k === 'f') {
|
||||
const v = parseInt(prompt('Roll to face (1–20):') || '', 10);
|
||||
if (v) rollOnce(v);
|
||||
if (k === 'r' && diceModal.style.display === 'flex') {
|
||||
rollOnce();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
if (!die.hasAttribute('data-face')) setFace(1);
|
||||
window.rollTo = (n) => rollOnce(n); // handy for console/testing
|
||||
window.rollTo = (n) => rollOnce(n);
|
||||
})();
|
||||
|
||||
// ---------- Easter Egg: long-press the skull to toggle Infect Mode ----------
|
||||
@ -567,7 +625,7 @@ if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.
|
||||
document.body.classList.toggle('infect-mode', on);
|
||||
try { localStorage.setItem('infectMode', on ? '1' : '0'); } catch {}
|
||||
}
|
||||
// Restore persisted
|
||||
|
||||
try { setInfectMode(localStorage.getItem('infectMode') === '1'); } catch {}
|
||||
|
||||
function onDown(e){
|
||||
@ -578,11 +636,13 @@ if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.
|
||||
icon.classList.add('egg-hold');
|
||||
t = setTimeout(()=> { armed = true; }, HOLD_MS);
|
||||
}
|
||||
|
||||
function onMove(e){
|
||||
if (!t) return;
|
||||
const p = (e.touches && e.touches[0]) || e;
|
||||
if (Math.hypot(p.clientX - startX, p.clientY - startY) > MOVE_CANCEL) cancel();
|
||||
}
|
||||
|
||||
function onUp(){
|
||||
if (!t) return;
|
||||
clearTimeout(t); t = null;
|
||||
@ -590,14 +650,13 @@ if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.
|
||||
|
||||
if (armed){
|
||||
armed = false;
|
||||
swallowClick = true; // prevent the normal click handler after long-press
|
||||
swallowClick = true;
|
||||
|
||||
const willEnable = !document.body.classList.contains('infect-mode');
|
||||
setInfectMode(willEnable);
|
||||
|
||||
vibrate(willEnable ? [20,40,20] : 28);
|
||||
|
||||
// Optional confetti hook
|
||||
const rect = icon.getBoundingClientRect();
|
||||
const x = rect.left + rect.width/2, y = rect.top + rect.height/2;
|
||||
if (typeof window.confettiBurstAt === 'function') window.confettiBurstAt(x, y, 70);
|
||||
@ -611,17 +670,16 @@ if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cancel(){
|
||||
clearTimeout(t); t = null; armed = false;
|
||||
icon.classList.remove('egg-hold');
|
||||
}
|
||||
|
||||
// Pointer events (mouse & touch)
|
||||
icon.addEventListener('pointerdown', onDown);
|
||||
icon.addEventListener('pointermove', onMove);
|
||||
['pointerup','pointercancel','pointerleave'].forEach(ev => icon.addEventListener(ev, onUp));
|
||||
|
||||
// Swallow the click if it immediately follows a successful long-press
|
||||
icon.addEventListener('click', (e)=>{
|
||||
if (swallowClick){
|
||||
e.preventDefault(); e.stopImmediatePropagation();
|
||||
@ -629,16 +687,57 @@ if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.
|
||||
}
|
||||
}, true);
|
||||
|
||||
// Avoid context menu popups on long-press (Android Chrome etc.)
|
||||
icon.addEventListener('contextmenu', e => e.preventDefault());
|
||||
})();
|
||||
|
||||
// ---------- Keyboard: small accessibility niceties ----------
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Ctrl/Cmd+F toggles fullscreen (optional)
|
||||
if (e.code === 'KeyF' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
if (document.fullscreenElement) document.exitFullscreen();
|
||||
else document.documentElement.requestFullscreen();
|
||||
}
|
||||
});
|
||||
|
||||
// --- Fit bowl+die to the toolbar using height and true remaining width ---
|
||||
(() => {
|
||||
const bar = document.getElementById('buttonWrapper');
|
||||
const dice = bar?.querySelector('.dice');
|
||||
|
||||
const toNum = v => {
|
||||
const n = parseFloat(v);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
};
|
||||
|
||||
function resizeDie(){
|
||||
if (!bar || !dice) return;
|
||||
|
||||
const root = document.documentElement;
|
||||
const csBar = getComputedStyle(bar);
|
||||
|
||||
const innerW = bar.clientWidth - toNum(csBar.paddingLeft) - toNum(csBar.paddingRight);
|
||||
const innerH = bar.clientHeight - toNum(csBar.paddingTop) - toNum(csBar.paddingBottom);
|
||||
const gap = toNum(csBar.columnGap || csBar.gap || 0);
|
||||
|
||||
let usedBefore = 0, countBefore = 0;
|
||||
for (const el of bar.children) {
|
||||
if (el === dice) break;
|
||||
usedBefore += el.getBoundingClientRect().width;
|
||||
countBefore++;
|
||||
}
|
||||
if (countBefore > 0) usedBefore += gap * countBefore;
|
||||
|
||||
const remainingW = Math.max(0, innerW - usedBefore);
|
||||
const ratio = parseFloat(getComputedStyle(root).getPropertyValue('--bowl-ratio')) || 1.55;
|
||||
|
||||
const dieFromHeight = innerH / ratio;
|
||||
const dieFromWidth = remainingW / ratio;
|
||||
const die = Math.max(70, Math.min(200, Math.floor(Math.min(dieFromHeight, dieFromWidth)))); root.style.setProperty('--die-size', die + 'px');
|
||||
}
|
||||
|
||||
const ro = new ResizeObserver(resizeDie);
|
||||
if (bar) ro.observe(bar);
|
||||
window.addEventListener('resize', resizeDie, { passive:true });
|
||||
document.addEventListener('visibilitychange', resizeDie);
|
||||
resizeDie();
|
||||
})();
|
||||
Loading…
x
Reference in New Issue
Block a user