mirror of
https://github.com/shadoll/dev.shadoll.git
synced 2026-08-28 11:33:15 +00:00
feat: enhance logo settings with bump thresholds and visibility options
This commit is contained in:
+41
-4
@@ -156,6 +156,7 @@
|
|||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<label class="setting-label" for="speedSlider">gradient speed</label>
|
<label class="setting-label" for="speedSlider">gradient speed</label>
|
||||||
<div class="slider-wrapper">
|
<div class="slider-wrapper">
|
||||||
|
<span class="slider-current" id="speedValue">2</span>
|
||||||
<span class="slider-label">slow</span>
|
<span class="slider-label">slow</span>
|
||||||
<input type="range" id="speedSlider" min="1" max="10" value="2" class="slider" />
|
<input type="range" id="speedSlider" min="1" max="10" value="2" class="slider" />
|
||||||
<span class="slider-label">fast</span>
|
<span class="slider-label">fast</span>
|
||||||
@@ -183,6 +184,7 @@
|
|||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<label class="setting-label" for="moveSpeedSlider">movement speed</label>
|
<label class="setting-label" for="moveSpeedSlider">movement speed</label>
|
||||||
<div class="slider-wrapper">
|
<div class="slider-wrapper">
|
||||||
|
<span class="slider-current" id="moveSpeedValue">5</span>
|
||||||
<span class="slider-label">slow</span>
|
<span class="slider-label">slow</span>
|
||||||
<input type="range" id="moveSpeedSlider" min="1" max="50" value="5" class="slider" />
|
<input type="range" id="moveSpeedSlider" min="1" max="50" value="5" class="slider" />
|
||||||
<span class="slider-label">fast</span>
|
<span class="slider-label">fast</span>
|
||||||
@@ -192,6 +194,7 @@
|
|||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<label class="setting-label" for="spawnRateSlider">spawn rate</label>
|
<label class="setting-label" for="spawnRateSlider">spawn rate</label>
|
||||||
<div class="slider-wrapper">
|
<div class="slider-wrapper">
|
||||||
|
<span class="slider-current" id="spawnRateValue">5</span>
|
||||||
<span class="slider-label">rare</span>
|
<span class="slider-label">rare</span>
|
||||||
<input type="range" id="spawnRateSlider" min="1" max="10" value="5" class="slider" />
|
<input type="range" id="spawnRateSlider" min="1" max="10" value="5" class="slider" />
|
||||||
<span class="slider-label">frequent</span>
|
<span class="slider-label">frequent</span>
|
||||||
@@ -201,6 +204,7 @@
|
|||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<label class="setting-label" for="virusKillSlider">virus lethality</label>
|
<label class="setting-label" for="virusKillSlider">virus lethality</label>
|
||||||
<div class="slider-wrapper">
|
<div class="slider-wrapper">
|
||||||
|
<span class="slider-current" id="virusKillValue">90%</span>
|
||||||
<span class="slider-label">0%</span>
|
<span class="slider-label">0%</span>
|
||||||
<input type="range" id="virusKillSlider" min="0" max="100" value="90" class="slider" />
|
<input type="range" id="virusKillSlider" min="0" max="100" value="90" class="slider" />
|
||||||
<span class="slider-label">100%</span>
|
<span class="slider-label">100%</span>
|
||||||
@@ -210,6 +214,7 @@
|
|||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<label class="setting-label" for="bugChanceSlider">bug rarity</label>
|
<label class="setting-label" for="bugChanceSlider">bug rarity</label>
|
||||||
<div class="slider-wrapper">
|
<div class="slider-wrapper">
|
||||||
|
<span class="slider-current" id="bugChanceValue">8%</span>
|
||||||
<span class="slider-label">0%</span>
|
<span class="slider-label">0%</span>
|
||||||
<input type="range" id="bugChanceSlider" min="0" max="100" value="8" class="slider" />
|
<input type="range" id="bugChanceSlider" min="0" max="100" value="8" class="slider" />
|
||||||
<span class="slider-label">100%</span>
|
<span class="slider-label">100%</span>
|
||||||
@@ -219,6 +224,7 @@
|
|||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<label class="setting-label" for="bugCountSlider">bug number</label>
|
<label class="setting-label" for="bugCountSlider">bug number</label>
|
||||||
<div class="slider-wrapper">
|
<div class="slider-wrapper">
|
||||||
|
<span class="slider-current" id="bugCountValue">2</span>
|
||||||
<span class="slider-label">0</span>
|
<span class="slider-label">0</span>
|
||||||
<input type="range" id="bugCountSlider" min="0" max="10" value="2" class="slider" />
|
<input type="range" id="bugCountSlider" min="0" max="10" value="2" class="slider" />
|
||||||
<span class="slider-label">10</span>
|
<span class="slider-label">10</span>
|
||||||
@@ -227,16 +233,16 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── debug ─────────────────────────────── -->
|
<!-- ── logo ─────────────────────────────── -->
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<p class="settings-section__label">debug</p>
|
<p class="settings-section__label">logo</p>
|
||||||
|
|
||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<span class="setting-label">letter hit count</span>
|
<span class="setting-label">letter hit count</span>
|
||||||
<div class="toggle-wrapper">
|
<div class="toggle-wrapper">
|
||||||
<span class="toggle-label">off</span>
|
<span class="toggle-label">off</span>
|
||||||
<label class="toggle" for="debugHitCountToggle" aria-label="Show letter hit counts">
|
<label class="toggle" for="letterHitCountToggle" aria-label="Show letter hit counts">
|
||||||
<input type="checkbox" id="debugHitCountToggle" />
|
<input type="checkbox" id="letterHitCountToggle" />
|
||||||
<span class="toggle-track">
|
<span class="toggle-track">
|
||||||
<span class="toggle-thumb"></span>
|
<span class="toggle-thumb"></span>
|
||||||
</span>
|
</span>
|
||||||
@@ -244,6 +250,37 @@
|
|||||||
<span class="toggle-label">on</span>
|
<span class="toggle-label">on</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-group">
|
||||||
|
<span class="setting-label">detach number</span>
|
||||||
|
<div class="toggle-wrapper">
|
||||||
|
<span class="toggle-label">off</span>
|
||||||
|
<label class="toggle" for="letterThresholdToggle" aria-label="Show detach thresholds">
|
||||||
|
<input type="checkbox" id="letterThresholdToggle" />
|
||||||
|
<span class="toggle-track">
|
||||||
|
<span class="toggle-thumb"></span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<span class="toggle-label">on</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-group">
|
||||||
|
<label class="setting-label" for="thresholdMinInput">min detach</label>
|
||||||
|
<div class="slider-wrapper">
|
||||||
|
<span class="slider-current" id="thresholdMinValue">50</span>
|
||||||
|
<span class="slider-label">0</span>
|
||||||
|
<input type="range" id="thresholdMinInput" min="0" max="5000" value="50" class="slider" />
|
||||||
|
<span class="slider-label">5000</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="setting-group">
|
||||||
|
<label class="setting-label" for="thresholdMaxInput">max detach</label>
|
||||||
|
<div class="slider-wrapper">
|
||||||
|
<span class="slider-current" id="thresholdMaxValue">100</span>
|
||||||
|
<span class="slider-label">0</span>
|
||||||
|
<input type="range" id="thresholdMaxInput" min="0" max="5000" value="100" class="slider" />
|
||||||
|
<span class="slider-label">5000</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-2
@@ -76,9 +76,9 @@ export const DEFAULTS = {
|
|||||||
/** Word base velocity in px/frame (slow / heavy feel) */
|
/** Word base velocity in px/frame (slow / heavy feel) */
|
||||||
LOGO_WORD_BASE_SPEED: 0.08,
|
LOGO_WORD_BASE_SPEED: 0.08,
|
||||||
/** Min bump count before ejection threshold can be reached */
|
/** Min bump count before ejection threshold can be reached */
|
||||||
LOGO_BUMP_THRESHOLD_MIN: 500,
|
LOGO_BUMP_THRESHOLD_MIN: 1000,
|
||||||
/** Max bump count before ejection threshold */
|
/** Max bump count before ejection threshold */
|
||||||
LOGO_BUMP_THRESHOLD_MAX: 5000,
|
LOGO_BUMP_THRESHOLD_MAX: 1200,
|
||||||
/** Spring constant pulling an ejected letter back toward its slot */
|
/** Spring constant pulling an ejected letter back toward its slot */
|
||||||
LOGO_SPRING_K: 0.04,
|
LOGO_SPRING_K: 0.04,
|
||||||
/** Per-frame velocity damping on ejected letters (0–1) */
|
/** Per-frame velocity damping on ejected letters (0–1) */
|
||||||
|
|||||||
@@ -155,8 +155,17 @@ export class EvolutionController {
|
|||||||
this.#entities.forEach(e => e.destroy());
|
this.#entities.forEach(e => e.destroy());
|
||||||
this.#entities = [];
|
this.#entities = [];
|
||||||
this.#startTime = Date.now(); // reset lifetime counter
|
this.#startTime = Date.now(); // reset lifetime counter
|
||||||
|
|
||||||
|
// also tell the logo word to forget its bump counts; the visual labels
|
||||||
|
// should go back to `0` immediately
|
||||||
|
this.#logo?.resetCounters();
|
||||||
|
this.#logo?.resetThresholds();
|
||||||
|
|
||||||
// Wipe saved state so a page reload starts fresh
|
// Wipe saved state so a page reload starts fresh
|
||||||
try { localStorage.removeItem(EVOLUTION_STORAGE_KEY); } catch { /* ignore */ }
|
try { localStorage.removeItem(EVOLUTION_STORAGE_KEY); } catch { /* ignore */ }
|
||||||
|
|
||||||
|
// notify anyone listening (settings controller) that evolution was cleared
|
||||||
|
document.dispatchEvent(new Event('evolutionCleared'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stop all timers and the rAF loop; remove all entities from the DOM. */
|
/** Stop all timers and the rAF loop; remove all entities from the DOM. */
|
||||||
|
|||||||
@@ -41,11 +41,28 @@ export class LogoController {
|
|||||||
/** @type {number} */ #wordVy = 0;
|
/** @type {number} */ #wordVy = 0;
|
||||||
|
|
||||||
/** @type {LogoLetter[]} */ #letters = [];
|
/** @type {LogoLetter[]} */ #letters = [];
|
||||||
|
/** @type {boolean} */ #thresholdVisible = false;
|
||||||
/** @type {HTMLElement|null} */ #container = null;
|
/** @type {HTMLElement|null} */ #container = null;
|
||||||
/** @type {boolean} */ #ready = false;
|
/** @type {boolean} */ #ready = false;
|
||||||
|
|
||||||
// ── Public API ─────────────────────────────────────────────
|
// ── Public API ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Randomise the bump threshold for every letter using the current min/max
|
||||||
|
* settings and update the on‑screen labels. Called when settings change or
|
||||||
|
* when evolution is restarted.
|
||||||
|
*/
|
||||||
|
resetThresholds() {
|
||||||
|
for (const letter of this.#letters) {
|
||||||
|
letter.bumpThreshold = DEFAULTS.LOGO_BUMP_THRESHOLD_MIN
|
||||||
|
+ Math.floor(Math.random()
|
||||||
|
* (DEFAULTS.LOGO_BUMP_THRESHOLD_MAX - DEFAULTS.LOGO_BUMP_THRESHOLD_MIN));
|
||||||
|
if (letter.thresholdEl) {
|
||||||
|
letter.thresholdEl.textContent = String(letter.bumpThreshold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mount all letter DOM nodes and initialise physics.
|
* Mount all letter DOM nodes and initialise physics.
|
||||||
*
|
*
|
||||||
@@ -167,6 +184,14 @@ export class LogoController {
|
|||||||
const ejVx = -nx * DEFAULTS.LOGO_EJECT_IMPULSE;
|
const ejVx = -nx * DEFAULTS.LOGO_EJECT_IMPULSE;
|
||||||
const ejVy = -ny * DEFAULTS.LOGO_EJECT_IMPULSE;
|
const ejVy = -ny * DEFAULTS.LOGO_EJECT_IMPULSE;
|
||||||
letter.eject(ejVx, ejVy);
|
letter.eject(ejVx, ejVy);
|
||||||
|
// after detaching, reset bump counter and pick a fresh threshold
|
||||||
|
letter.bumpCount = 0;
|
||||||
|
letter.bumpThreshold = DEFAULTS.LOGO_BUMP_THRESHOLD_MIN
|
||||||
|
+ Math.floor(Math.random()
|
||||||
|
* (DEFAULTS.LOGO_BUMP_THRESHOLD_MAX - DEFAULTS.LOGO_BUMP_THRESHOLD_MIN));
|
||||||
|
if (letter.thresholdEl) {
|
||||||
|
letter.thresholdEl.textContent = String(letter.bumpThreshold);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,6 +204,9 @@ export class LogoController {
|
|||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
serialise() {
|
serialise() {
|
||||||
|
// Note: we deliberately do **not** persist each letter's bumpThreshold.
|
||||||
|
// When the page reloads we want thresholds to be freshly chosen from the
|
||||||
|
// current min/max settings rather than sticking to whatever they were.
|
||||||
return {
|
return {
|
||||||
wordX: this.#wordX,
|
wordX: this.#wordX,
|
||||||
wordY: this.#wordY,
|
wordY: this.#wordY,
|
||||||
@@ -188,7 +216,7 @@ export class LogoController {
|
|||||||
slotIndex: l.slotIndex,
|
slotIndex: l.slotIndex,
|
||||||
iconName: l.iconName,
|
iconName: l.iconName,
|
||||||
bumpCount: l.bumpCount,
|
bumpCount: l.bumpCount,
|
||||||
bumpThreshold: l.bumpThreshold,
|
// bumpThreshold intentionally omitted
|
||||||
ejected: l.ejected,
|
ejected: l.ejected,
|
||||||
x: l.x,
|
x: l.x,
|
||||||
y: l.y,
|
y: l.y,
|
||||||
@@ -217,6 +245,55 @@ export class LogoController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the visibility of the bump-threshold numbers on every letter.
|
||||||
|
* @param {boolean} visible
|
||||||
|
*/
|
||||||
|
setThresholdVisible(visible) {
|
||||||
|
this.#thresholdVisible = visible;
|
||||||
|
for (const letter of this.#letters) {
|
||||||
|
letter.setThresholdVisible(visible);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of the current bump counts for each letter (slot order).
|
||||||
|
* Used by SettingsController so the zero values are included when persisting.
|
||||||
|
* @returns {number[]}
|
||||||
|
*/
|
||||||
|
getHitCounts() {
|
||||||
|
return this.#letters.map(l => l.bumpCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply previously saved bump counts back onto the letters. If the array is
|
||||||
|
* shorter/longer than the current word, it is truncated or padded with zeros.
|
||||||
|
* @param {number[]} counts
|
||||||
|
*/
|
||||||
|
setHitCounts(counts) {
|
||||||
|
if (!Array.isArray(counts)) return;
|
||||||
|
for (let i = 0; i < this.#letters.length; i++) {
|
||||||
|
this.#letters[i].bumpCount = counts[i] ?? 0;
|
||||||
|
if (this.#letters[i].debugEl) {
|
||||||
|
this.#letters[i].debugEl.textContent = String(this.#letters[i].bumpCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset all bump counters back to zero (used when evolution is cleared).
|
||||||
|
*/
|
||||||
|
resetCounters() {
|
||||||
|
for (const letter of this.#letters) {
|
||||||
|
letter.bumpCount = 0;
|
||||||
|
if (letter.debugEl) letter.debugEl.textContent = '0';
|
||||||
|
// remove any red proximity colouring so letters appear white again
|
||||||
|
if (typeof letter._resetProximityColor === 'function') {
|
||||||
|
letter._resetProximityColor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Private ────────────────────────────────────────────────
|
// ── Private ────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Set up a brand-new word at the viewport centre with a random direction. */
|
/** Set up a brand-new word at the viewport centre with a random direction. */
|
||||||
@@ -242,13 +319,20 @@ export class LogoController {
|
|||||||
this.#wordVx = state.wordVx ?? 0;
|
this.#wordVx = state.wordVx ?? 0;
|
||||||
this.#wordVy = state.wordVy ?? DEFAULTS.LOGO_WORD_BASE_SPEED;
|
this.#wordVy = state.wordVy ?? DEFAULTS.LOGO_WORD_BASE_SPEED;
|
||||||
|
|
||||||
|
// Ignore saved bumpThreshold; always regenerate using current settings
|
||||||
this.#letters = WORD_ICONS.map((iconName, i) => {
|
this.#letters = WORD_ICONS.map((iconName, i) => {
|
||||||
const saved = state.letters?.[i];
|
const saved = state.letters?.[i];
|
||||||
return new LogoLetter({
|
const letter = new LogoLetter({
|
||||||
iconName,
|
iconName,
|
||||||
slotIndex: i,
|
slotIndex: i,
|
||||||
bumpThreshold: saved?.bumpThreshold,
|
// no bumpThreshold argument -> constructor uses DEFAULTS range
|
||||||
});
|
});
|
||||||
|
// restore bumpCount/ejected/position if present
|
||||||
|
if (saved) {
|
||||||
|
letter.bumpCount = saved.bumpCount ?? 0;
|
||||||
|
letter.ejected = saved.ejected ?? false;
|
||||||
|
}
|
||||||
|
return letter;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,11 +349,21 @@ export class LogoController {
|
|||||||
await letter.mount(this.#container);
|
await letter.mount(this.#container);
|
||||||
if (!letter.mounted) return;
|
if (!letter.mounted) return;
|
||||||
|
|
||||||
|
// apply current threshold visibility immediately
|
||||||
|
if (this.#thresholdVisible) {
|
||||||
|
letter.setThresholdVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
const saved = savedLetterStates?.[i];
|
const saved = savedLetterStates?.[i];
|
||||||
if (saved) {
|
if (saved) {
|
||||||
letter.bumpCount = saved.bumpCount ?? 0;
|
letter.bumpCount = saved.bumpCount ?? 0;
|
||||||
letter.ejected = saved.ejected ?? false;
|
letter.ejected = saved.ejected ?? false;
|
||||||
|
|
||||||
|
// reflect proximity colour based on the restored bump count
|
||||||
|
if (typeof letter._updateProximityColor === 'function') {
|
||||||
|
letter._updateProximityColor();
|
||||||
|
}
|
||||||
|
|
||||||
if (saved.ejected) {
|
if (saved.ejected) {
|
||||||
letter.x = saved.x ?? letter.x;
|
letter.x = saved.x ?? letter.x;
|
||||||
letter.y = saved.y ?? letter.y;
|
letter.y = saved.y ?? letter.y;
|
||||||
|
|||||||
+52
-1
@@ -39,6 +39,7 @@ export class LogoLetter {
|
|||||||
/** @type {HTMLElement|null} */ el = null;
|
/** @type {HTMLElement|null} */ el = null;
|
||||||
/** @type {HTMLElement|null} */ bodyEl = null;
|
/** @type {HTMLElement|null} */ bodyEl = null;
|
||||||
/** @type {HTMLElement|null} */ debugEl = null;
|
/** @type {HTMLElement|null} */ debugEl = null;
|
||||||
|
/** @type {HTMLElement|null} */ thresholdEl = null;
|
||||||
/** @type {boolean} */ mounted = false;
|
/** @type {boolean} */ mounted = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,7 +81,13 @@ export class LogoLetter {
|
|||||||
this.debugEl.className = 'logo-letter__debug';
|
this.debugEl.className = 'logo-letter__debug';
|
||||||
this.debugEl.textContent = '0';
|
this.debugEl.textContent = '0';
|
||||||
|
|
||||||
|
// threshold label sits above the letter
|
||||||
|
this.thresholdEl = document.createElement('span');
|
||||||
|
this.thresholdEl.className = 'logo-letter__threshold';
|
||||||
|
this.thresholdEl.textContent = String(this.bumpThreshold);
|
||||||
|
|
||||||
this.el.appendChild(this.bodyEl);
|
this.el.appendChild(this.bodyEl);
|
||||||
|
this.el.appendChild(this.thresholdEl);
|
||||||
this.el.appendChild(this.debugEl);
|
this.el.appendChild(this.debugEl);
|
||||||
container.appendChild(this.el);
|
container.appendChild(this.el);
|
||||||
this.mounted = true;
|
this.mounted = true;
|
||||||
@@ -177,8 +184,19 @@ export class LogoLetter {
|
|||||||
onBump() {
|
onBump() {
|
||||||
this.bumpCount++;
|
this.bumpCount++;
|
||||||
if (this.debugEl) this.debugEl.textContent = String(this.bumpCount);
|
if (this.debugEl) this.debugEl.textContent = String(this.bumpCount);
|
||||||
|
|
||||||
|
// colour the letter based on how close it is to the threshold; we only
|
||||||
|
// start showing red when there are 10 or fewer hits remaining, interpolating
|
||||||
|
// from white→red as the count rises.
|
||||||
|
this._updateProximityColor();
|
||||||
|
|
||||||
this._flashHit();
|
this._flashHit();
|
||||||
return !this.ejected && this.bumpCount >= this.bumpThreshold;
|
|
||||||
|
// notify listeners that a bump occurred so the settings storage can update
|
||||||
|
document.dispatchEvent(new CustomEvent('logoLetterBumped'));
|
||||||
|
|
||||||
|
// detach only on *next* hit after threshold reached (bumpCount > threshold)
|
||||||
|
return !this.ejected && this.bumpCount > this.bumpThreshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -208,6 +226,8 @@ export class LogoLetter {
|
|||||||
* (DEFAULTS.LOGO_BUMP_THRESHOLD_MAX - DEFAULTS.LOGO_BUMP_THRESHOLD_MIN));
|
* (DEFAULTS.LOGO_BUMP_THRESHOLD_MAX - DEFAULTS.LOGO_BUMP_THRESHOLD_MIN));
|
||||||
this.el?.classList.remove('logo-letter--ejected');
|
this.el?.classList.remove('logo-letter--ejected');
|
||||||
if (this.debugEl) this.debugEl.textContent = '0';
|
if (this.debugEl) this.debugEl.textContent = '0';
|
||||||
|
if (this.thresholdEl) this.thresholdEl.textContent = String(this.bumpThreshold);
|
||||||
|
this._resetProximityColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -218,6 +238,37 @@ export class LogoLetter {
|
|||||||
this.el?.classList.toggle('logo-letter--show-debug', visible);
|
this.el?.classList.toggle('logo-letter--show-debug', visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show or hide the threshold text.
|
||||||
|
* @param {boolean} visible
|
||||||
|
*/
|
||||||
|
setThresholdVisible(visible) {
|
||||||
|
this.el?.classList.toggle('logo-letter--show-threshold', visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the body colour to indicate proximity to the bump threshold.
|
||||||
|
* Once the count is within 10 of the threshold we fade from white→red; when
|
||||||
|
* further away the colour is left to CSS default.
|
||||||
|
* This function may be called any time bumpCount/threshold changes.
|
||||||
|
*/
|
||||||
|
_updateProximityColor() {
|
||||||
|
if (!this.bodyEl || this.bumpThreshold <= 0) return;
|
||||||
|
// interpolate from white at 0 hits to solid red at threshold
|
||||||
|
const ratio = Math.min(this.bumpCount / this.bumpThreshold, 1);
|
||||||
|
const gb = Math.round(255 * (1 - ratio));
|
||||||
|
this.bodyEl.style.color = `rgb(255,${gb},${gb})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear any proximity colouring so the CSS default can take over again.
|
||||||
|
*/
|
||||||
|
_resetProximityColor() {
|
||||||
|
if (this.bodyEl) {
|
||||||
|
this.bodyEl.style.color = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Remove this letter from the DOM. */
|
/** Remove this letter from the DOM. */
|
||||||
destroy() {
|
destroy() {
|
||||||
this.el?.remove();
|
this.el?.remove();
|
||||||
|
|||||||
+185
-38
@@ -8,14 +8,27 @@
|
|||||||
* saved state on the next page load so the system continues as configured.
|
* saved state on the next page load so the system continues as configured.
|
||||||
*
|
*
|
||||||
* Reset:
|
* Reset:
|
||||||
* - resetBtn clears localStorage and restores DEFAULTS for settings only.
|
* - resetBtn clears the settings storage and restores DEFAULTS for the
|
||||||
* Evolution state is reset separately via the guide panel.
|
* *settings* that are exposed in the modal (gradient, animation, logo
|
||||||
|
* hit‑count visibility, etc). This button does **not** touch the evolution
|
||||||
|
* simulation state (entity population, saved logo hit counts, timers, etc).
|
||||||
|
* The guide panel is responsible for restarting/clearing the evolution.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { DEFAULTS } from './constants.js';
|
import { DEFAULTS } from './constants.js';
|
||||||
|
|
||||||
const STORAGE_KEY = 'devpage:settings';
|
const STORAGE_KEY = 'devpage:settings';
|
||||||
|
|
||||||
|
// settings storage keys
|
||||||
|
const STORAGE_HIT_COUNT_KEY = 'letterHitCountVisible';
|
||||||
|
const STORAGE_THRESHOLD_KEY = 'letterThresholdVisible';
|
||||||
|
const STORAGE_BUMP_MIN_KEY = 'bumpThresholdMin';
|
||||||
|
const STORAGE_BUMP_MAX_KEY = 'bumpThresholdMax';
|
||||||
|
|
||||||
|
// capture original constants so reset can restore later
|
||||||
|
const ORIGINAL_BUMP_MIN = DEFAULTS.LOGO_BUMP_THRESHOLD_MIN;
|
||||||
|
const ORIGINAL_BUMP_MAX = DEFAULTS.LOGO_BUMP_THRESHOLD_MAX;
|
||||||
|
|
||||||
export class SettingsController {
|
export class SettingsController {
|
||||||
/** @type {import('./gradient.js').GradientController} */
|
/** @type {import('./gradient.js').GradientController} */
|
||||||
#gradient;
|
#gradient;
|
||||||
@@ -26,6 +39,9 @@ export class SettingsController {
|
|||||||
/** @type {import('./logoController.js').LogoController|null} */
|
/** @type {import('./logoController.js').LogoController|null} */
|
||||||
#logo = null;
|
#logo = null;
|
||||||
|
|
||||||
|
/** @type {number[]|null} */
|
||||||
|
#savedLetterHitCounts = null; // stored when loadSaved runs before logo is ready
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {import('./gradient.js').GradientController} gradient
|
* @param {import('./gradient.js').GradientController} gradient
|
||||||
* @param {import('./evolution.js').EvolutionController} evolution
|
* @param {import('./evolution.js').EvolutionController} evolution
|
||||||
@@ -35,6 +51,26 @@ export class SettingsController {
|
|||||||
this.#evolution = evolution;
|
this.#evolution = evolution;
|
||||||
this.#bindEvents();
|
this.#bindEvents();
|
||||||
this.#syncControls();
|
this.#syncControls();
|
||||||
|
|
||||||
|
// persist counts whenever a letter is bumped so storage reflects live
|
||||||
|
// values, not just whatever was saved by the last manual change.
|
||||||
|
document.addEventListener('logoLetterBumped', () => this.#saveState());
|
||||||
|
|
||||||
|
// when the evolution system is restarted via the guide panel we want to
|
||||||
|
// clear the stored letter hit counts (they belong to simulation state,
|
||||||
|
// not permanent settings). remove the field so loadSaved() won't reapply it
|
||||||
|
document.addEventListener('evolutionCleared', () => {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (raw) {
|
||||||
|
const s = JSON.parse(raw);
|
||||||
|
if (s && typeof s === 'object' && 'letterHitCounts' in s) {
|
||||||
|
delete s.letterHitCounts;
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Public ──────────────────────────────────────────────────
|
// ── Public ──────────────────────────────────────────────────
|
||||||
@@ -47,8 +83,17 @@ export class SettingsController {
|
|||||||
*/
|
*/
|
||||||
setLogoController(logo) {
|
setLogoController(logo) {
|
||||||
this.#logo = logo;
|
this.#logo = logo;
|
||||||
const toggle = /** @type {HTMLInputElement} */ (document.getElementById('debugHitCountToggle'));
|
const toggle = /** @type {HTMLInputElement} */ (document.getElementById('letterHitCountToggle'));
|
||||||
if (toggle) logo.setDebugVisible(toggle.checked);
|
if (toggle) logo.setDebugVisible(toggle.checked);
|
||||||
|
const thrToggle = /** @type {HTMLInputElement} */ (document.getElementById('letterThresholdToggle'));
|
||||||
|
if (thrToggle) logo.setThresholdVisible(thrToggle.checked);
|
||||||
|
|
||||||
|
// if we previously loaded hit counts before the logo was available,
|
||||||
|
// reapply them now so the labels display correctly immediately on load
|
||||||
|
if (this.#savedLetterHitCounts && typeof logo.setHitCounts === 'function') {
|
||||||
|
logo.setHitCounts(this.#savedLetterHitCounts);
|
||||||
|
this.#savedLetterHitCounts = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,10 +113,35 @@ export class SettingsController {
|
|||||||
if (typeof s.virusKillChance === 'number') this.#evolution.setVirusKillChance(s.virusKillChance);
|
if (typeof s.virusKillChance === 'number') this.#evolution.setVirusKillChance(s.virusKillChance);
|
||||||
if (typeof s.bugSpawnChance === 'number') this.#evolution.setBugSpawnChance(s.bugSpawnChance);
|
if (typeof s.bugSpawnChance === 'number') this.#evolution.setBugSpawnChance(s.bugSpawnChance);
|
||||||
if (typeof s.bugMaxCount === 'number') this.#evolution.setBugMaxCount(s.bugMaxCount);
|
if (typeof s.bugMaxCount === 'number') this.#evolution.setBugMaxCount(s.bugMaxCount);
|
||||||
if (typeof s.debugHitCount === 'boolean') {
|
if (typeof s[STORAGE_HIT_COUNT_KEY] === 'boolean') {
|
||||||
const toggle = /** @type {HTMLInputElement} */ (document.getElementById('debugHitCountToggle'));
|
const toggle = /** @type {HTMLInputElement} */ (document.getElementById('letterHitCountToggle'));
|
||||||
if (toggle) toggle.checked = s.debugHitCount;
|
if (toggle) toggle.checked = s[STORAGE_HIT_COUNT_KEY];
|
||||||
this.#logo?.setDebugVisible(s.debugHitCount);
|
this.#logo?.setDebugVisible(s[STORAGE_HIT_COUNT_KEY]);
|
||||||
|
}
|
||||||
|
if (typeof s[STORAGE_THRESHOLD_KEY] === 'boolean') {
|
||||||
|
const toggle = /** @type {HTMLInputElement} */ (document.getElementById('letterThresholdToggle'));
|
||||||
|
if (toggle) toggle.checked = s[STORAGE_THRESHOLD_KEY];
|
||||||
|
this.#logo?.setThresholdVisible(s[STORAGE_THRESHOLD_KEY]);
|
||||||
|
}
|
||||||
|
if (typeof s[STORAGE_BUMP_MIN_KEY] === 'number') {
|
||||||
|
DEFAULTS.LOGO_BUMP_THRESHOLD_MIN = s[STORAGE_BUMP_MIN_KEY];
|
||||||
|
}
|
||||||
|
if (typeof s[STORAGE_BUMP_MAX_KEY] === 'number') {
|
||||||
|
DEFAULTS.LOGO_BUMP_THRESHOLD_MAX = s[STORAGE_BUMP_MAX_KEY];
|
||||||
|
}
|
||||||
|
// restore any saved hit‑count numbers (including zeros). if the
|
||||||
|
// LogoController isn't wired yet we keep the array so setLogoController()
|
||||||
|
// can apply it later.
|
||||||
|
if (Array.isArray(s.letterHitCounts)) {
|
||||||
|
if (this.#logo && typeof this.#logo.setHitCounts === 'function') {
|
||||||
|
this.#logo.setHitCounts(s.letterHitCounts);
|
||||||
|
} else {
|
||||||
|
this.#savedLetterHitCounts = s.letterHitCounts.slice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// after restoring bump range, ensure letters use it
|
||||||
|
if (this.#logo && typeof this.#logo.resetThresholds === 'function') {
|
||||||
|
this.#logo.resetThresholds();
|
||||||
}
|
}
|
||||||
} catch { /* corrupt storage — ignore */ }
|
} catch { /* corrupt storage — ignore */ }
|
||||||
this.#syncControls();
|
this.#syncControls();
|
||||||
@@ -90,21 +160,10 @@ export class SettingsController {
|
|||||||
const bugChanceSlider = /** @type {HTMLInputElement} */ (document.getElementById('bugChanceSlider'));
|
const bugChanceSlider = /** @type {HTMLInputElement} */ (document.getElementById('bugChanceSlider'));
|
||||||
const bugCountSlider = /** @type {HTMLInputElement} */ (document.getElementById('bugCountSlider'));
|
const bugCountSlider = /** @type {HTMLInputElement} */ (document.getElementById('bugCountSlider'));
|
||||||
const resetBtn = /** @type {HTMLButtonElement} */ (document.getElementById('resetBtn'));
|
const resetBtn = /** @type {HTMLButtonElement} */ (document.getElementById('resetBtn'));
|
||||||
const debugHitCountToggle = /** @type {HTMLInputElement} */ (document.getElementById('debugHitCountToggle'));
|
const letterHitCountToggle = /** @type {HTMLInputElement} */ (document.getElementById('letterHitCountToggle'));
|
||||||
|
const letterThresholdToggle = /** @type {HTMLInputElement} */ (document.getElementById('letterThresholdToggle'));
|
||||||
// ── Colour picker ──────────────────────────────────────
|
const thresholdMinInput = /** @type {HTMLInputElement} */ (document.getElementById('thresholdMinInput'));
|
||||||
colorPicker.addEventListener('input', (e) => {
|
const thresholdMaxInput = /** @type {HTMLInputElement} */ (document.getElementById('thresholdMaxInput'));
|
||||||
const hex = /** @type {HTMLInputElement} */ (e.target).value;
|
|
||||||
colorValue.textContent = hex;
|
|
||||||
this.#gradient.setColor(hex);
|
|
||||||
this.#saveState();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Gradient animation speed ───────────────────────────
|
|
||||||
speedSlider.addEventListener('input', (e) => {
|
|
||||||
this.#gradient.setSpeed(Number(/** @type {HTMLInputElement} */ (e.target).value));
|
|
||||||
this.#saveState();
|
|
||||||
});
|
|
||||||
|
|
||||||
// ── Rotation toggle ────────────────────────────────────
|
// ── Rotation toggle ────────────────────────────────────
|
||||||
rotToggle.addEventListener('change', (e) => {
|
rotToggle.addEventListener('change', (e) => {
|
||||||
@@ -112,56 +171,120 @@ export class SettingsController {
|
|||||||
this.#saveState();
|
this.#saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Gradient animation speed ───────────────────────────
|
||||||
|
speedSlider.addEventListener('input', (e) => {
|
||||||
|
const val = Number(/** @type {HTMLInputElement} */ (e.target).value);
|
||||||
|
this.#gradient.setSpeed(val);
|
||||||
|
document.getElementById('speedValue').textContent = String(val);
|
||||||
|
this.#saveState();
|
||||||
|
});
|
||||||
|
|
||||||
// ── Icon movement speed ────────────────────────────────
|
// ── Icon movement speed ────────────────────────────────
|
||||||
moveSpeedSlider.addEventListener('input', (e) => {
|
moveSpeedSlider.addEventListener('input', (e) => {
|
||||||
this.#evolution.setMoveSpeed(Number(/** @type {HTMLInputElement} */ (e.target).value));
|
const val = Number(/** @type {HTMLInputElement} */ (e.target).value);
|
||||||
|
this.#evolution.setMoveSpeed(val);
|
||||||
|
document.getElementById('moveSpeedValue').textContent = String(val);
|
||||||
this.#saveState();
|
this.#saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Spawn rate ─────────────────────────────────────────
|
// ── Spawn rate ─────────────────────────────────────────
|
||||||
spawnRateSlider.addEventListener('input', (e) => {
|
spawnRateSlider.addEventListener('input', (e) => {
|
||||||
this.#evolution.setSpawnRate(Number(/** @type {HTMLInputElement} */ (e.target).value));
|
const val = Number(/** @type {HTMLInputElement} */ (e.target).value);
|
||||||
|
this.#evolution.setSpawnRate(val);
|
||||||
|
document.getElementById('spawnRateValue').textContent = String(val);
|
||||||
this.#saveState();
|
this.#saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Virus lethality ────────────────────────────────────
|
// ── Virus lethality ────────────────────────────────────
|
||||||
virusKillSlider.addEventListener('input', (e) => {
|
virusKillSlider.addEventListener('input', (e) => {
|
||||||
this.#evolution.setVirusKillChance(Number(/** @type {HTMLInputElement} */ (e.target).value) / 100);
|
const num = Number(/** @type {HTMLInputElement} */ (e.target).value);
|
||||||
|
this.#evolution.setVirusKillChance(num / 100);
|
||||||
|
document.getElementById('virusKillValue').textContent = num + '%';
|
||||||
this.#saveState();
|
this.#saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Bug spawn chance ───────────────────────────────────
|
// ── Bug spawn chance ───────────────────────────────────
|
||||||
bugChanceSlider.addEventListener('input', (e) => {
|
bugChanceSlider.addEventListener('input', (e) => {
|
||||||
this.#evolution.setBugSpawnChance(Number(/** @type {HTMLInputElement} */ (e.target).value) / 100);
|
const num = Number(/** @type {HTMLInputElement} */ (e.target).value);
|
||||||
|
this.#evolution.setBugSpawnChance(num / 100);
|
||||||
|
document.getElementById('bugChanceValue').textContent = num + '%';
|
||||||
|
this.#saveState();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Bump threshold range ───────────────────────────────
|
||||||
|
thresholdMinInput.addEventListener('input', (e) => {
|
||||||
|
const val = Number(/** @type {HTMLInputElement} */ (e.target).value);
|
||||||
|
DEFAULTS.LOGO_BUMP_THRESHOLD_MIN = val;
|
||||||
|
document.getElementById('thresholdMinValue').textContent = String(val);
|
||||||
|
// keep min ≤ max
|
||||||
|
if (val > DEFAULTS.LOGO_BUMP_THRESHOLD_MAX) {
|
||||||
|
DEFAULTS.LOGO_BUMP_THRESHOLD_MAX = val;
|
||||||
|
thresholdMaxInput.value = String(val);
|
||||||
|
document.getElementById('thresholdMaxValue').textContent = String(val);
|
||||||
|
}
|
||||||
|
this.#logo?.resetThresholds();
|
||||||
|
this.#saveState();
|
||||||
|
});
|
||||||
|
thresholdMaxInput.addEventListener('input', (e) => {
|
||||||
|
const val = Number(/** @type {HTMLInputElement} */ (e.target).value);
|
||||||
|
DEFAULTS.LOGO_BUMP_THRESHOLD_MAX = val;
|
||||||
|
document.getElementById('thresholdMaxValue').textContent = String(val);
|
||||||
|
// keep max ≥ min
|
||||||
|
if (val < DEFAULTS.LOGO_BUMP_THRESHOLD_MIN) {
|
||||||
|
DEFAULTS.LOGO_BUMP_THRESHOLD_MIN = val;
|
||||||
|
thresholdMinInput.value = String(val);
|
||||||
|
document.getElementById('thresholdMinValue').textContent = String(val);
|
||||||
|
}
|
||||||
|
this.#logo?.resetThresholds();
|
||||||
this.#saveState();
|
this.#saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Bug max count ──────────────────────────────────────
|
// ── Bug max count ──────────────────────────────────────
|
||||||
bugCountSlider.addEventListener('input', (e) => {
|
bugCountSlider.addEventListener('input', (e) => {
|
||||||
this.#evolution.setBugMaxCount(Number(/** @type {HTMLInputElement} */ (e.target).value));
|
const num = Number(/** @type {HTMLInputElement} */ (e.target).value);
|
||||||
|
this.#evolution.setBugMaxCount(num);
|
||||||
|
document.getElementById('bugCountValue').textContent = String(num);
|
||||||
this.#saveState();
|
this.#saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Debug: letter hit count ────────────────────────────
|
// ── Logo: letter hit count ────────────────────────────
|
||||||
debugHitCountToggle.addEventListener('change', (e) => {
|
// The label on each logo letter shows how many bumps it has taken.
|
||||||
|
// Persist the visibility state along with the other settings.
|
||||||
|
letterHitCountToggle.addEventListener('change', (e) => {
|
||||||
const on = /** @type {HTMLInputElement} */ (e.target).checked;
|
const on = /** @type {HTMLInputElement} */ (e.target).checked;
|
||||||
this.#logo?.setDebugVisible(on);
|
this.#logo?.setDebugVisible(on);
|
||||||
this.#saveState();
|
this.#saveState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Logo: threshold number ────────────────────────────
|
||||||
|
// Shows how many bumps are required before ejection.
|
||||||
|
letterThresholdToggle.addEventListener('change', (e) => {
|
||||||
|
const on = /** @type {HTMLInputElement} */ (e.target).checked;
|
||||||
|
this.#logo?.setThresholdVisible(on);
|
||||||
|
this.#saveState();
|
||||||
|
});
|
||||||
|
|
||||||
// ── Reset to defaults ──────────────────────────────────
|
// ── Reset to defaults ──────────────────────────────────
|
||||||
resetBtn.addEventListener('click', () => {
|
resetBtn.addEventListener('click', () => {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
this.#gradient.setColor(DEFAULTS.GRADIENT_COLOR);
|
this.#gradient.setColor(DEFAULTS.GRADIENT_COLOR);
|
||||||
this.#gradient.setSpeed(DEFAULTS.GRADIENT_SPEED);
|
this.#gradient.setSpeed(DEFAULTS.GRADIENT_SPEED);
|
||||||
this.#gradient.toggleRotation(DEFAULTS.GRADIENT_ROTATION);
|
this.#gradient.toggleRotation(DEFAULTS.GRADIENT_ROTATION);
|
||||||
this.#evolution.setMoveSpeed(DEFAULTS.MOVE_SPEED);
|
// reset bump threshold range to original constants
|
||||||
this.#evolution.setSpawnRate(5);
|
DEFAULTS.LOGO_BUMP_THRESHOLD_MIN = ORIGINAL_BUMP_MIN;
|
||||||
this.#evolution.setVirusKillChance(DEFAULTS.VIRUS_KILL_CHANCE);
|
DEFAULTS.LOGO_BUMP_THRESHOLD_MAX = ORIGINAL_BUMP_MAX;
|
||||||
this.#evolution.setBugSpawnChance(DEFAULTS.BUG_SPAWN_CHANCE);
|
thresholdMinInput.value = String(DEFAULTS.LOGO_BUMP_THRESHOLD_MIN);
|
||||||
this.#evolution.setBugMaxCount(DEFAULTS.BUG_MAX_COUNT);
|
thresholdMaxInput.value = String(DEFAULTS.LOGO_BUMP_THRESHOLD_MAX);
|
||||||
debugHitCountToggle.checked = false;
|
|
||||||
|
// NOTE: moveSpeed/spawnRate/etc. are evolution configuration options that
|
||||||
|
// live in the same UI but are considered part of the simulation state.
|
||||||
|
// They are **not** reset here; clearing the evolution (including those
|
||||||
|
// values) is done via the guide panel.
|
||||||
|
letterHitCountToggle.checked = false;
|
||||||
this.#logo?.setDebugVisible(false);
|
this.#logo?.setDebugVisible(false);
|
||||||
|
// also clear threshold display toggle
|
||||||
|
letterThresholdToggle.checked = false;
|
||||||
|
this.#logo?.setThresholdVisible(false);
|
||||||
this.#syncControls();
|
this.#syncControls();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -169,6 +292,15 @@ export class SettingsController {
|
|||||||
/** Persist current controller state to localStorage. */
|
/** Persist current controller state to localStorage. */
|
||||||
#saveState() {
|
#saveState() {
|
||||||
try {
|
try {
|
||||||
|
// gather optional hit counts from the logo controller; we want to persist
|
||||||
|
// zeros as well so that a fresh word is restored exactly the same after a
|
||||||
|
// reload. The counts are also part of the evolution storage but keeping a
|
||||||
|
// copy here allows the settings key to mirror “all other settings”.
|
||||||
|
let counts = null;
|
||||||
|
if (this.#logo && typeof this.#logo.getHitCounts === 'function') {
|
||||||
|
counts = this.#logo.getHitCounts();
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({
|
||||||
gradientColor: this.#gradient.color,
|
gradientColor: this.#gradient.color,
|
||||||
gradientSpeed: this.#gradient.speed,
|
gradientSpeed: this.#gradient.speed,
|
||||||
@@ -178,7 +310,9 @@ export class SettingsController {
|
|||||||
virusKillChance: this.#evolution.virusKillChance,
|
virusKillChance: this.#evolution.virusKillChance,
|
||||||
bugSpawnChance: this.#evolution.bugSpawnChance,
|
bugSpawnChance: this.#evolution.bugSpawnChance,
|
||||||
bugMaxCount: this.#evolution.bugMaxCount,
|
bugMaxCount: this.#evolution.bugMaxCount,
|
||||||
debugHitCount: (/** @type {HTMLInputElement} */ (document.getElementById('debugHitCountToggle')))?.checked ?? false,
|
[STORAGE_HIT_COUNT_KEY]: (/** @type {HTMLInputElement} */ (document.getElementById('letterHitCountToggle')))?.checked ?? false,
|
||||||
|
[STORAGE_THRESHOLD_KEY]: (/** @type {HTMLInputElement} */ (document.getElementById('letterThresholdToggle')))?.checked ?? false,
|
||||||
|
letterHitCounts: counts,
|
||||||
}));
|
}));
|
||||||
} catch { /* quota exceeded or private browsing — ignore */ }
|
} catch { /* quota exceeded or private browsing — ignore */ }
|
||||||
}
|
}
|
||||||
@@ -197,17 +331,30 @@ export class SettingsController {
|
|||||||
const virusKillSlider = /** @type {HTMLInputElement} */ (document.getElementById('virusKillSlider'));
|
const virusKillSlider = /** @type {HTMLInputElement} */ (document.getElementById('virusKillSlider'));
|
||||||
const bugChanceSlider = /** @type {HTMLInputElement} */ (document.getElementById('bugChanceSlider'));
|
const bugChanceSlider = /** @type {HTMLInputElement} */ (document.getElementById('bugChanceSlider'));
|
||||||
const bugCountSlider = /** @type {HTMLInputElement} */ (document.getElementById('bugCountSlider'));
|
const bugCountSlider = /** @type {HTMLInputElement} */ (document.getElementById('bugCountSlider'));
|
||||||
const debugHitCountToggle = /** @type {HTMLInputElement} */ (document.getElementById('debugHitCountToggle'));
|
const letterHitCountToggle = /** @type {HTMLInputElement} */ (document.getElementById('letterHitCountToggle'));
|
||||||
|
|
||||||
colorPicker.value = this.#gradient.color;
|
colorPicker.value = this.#gradient.color;
|
||||||
colorValue.textContent = this.#gradient.color;
|
colorValue.textContent = this.#gradient.color;
|
||||||
speedSlider.value = String(this.#gradient.speed);
|
speedSlider.value = String(this.#gradient.speed);
|
||||||
|
document.getElementById('speedValue').textContent = String(this.#gradient.speed);
|
||||||
rotToggle.checked = this.#gradient.rotating;
|
rotToggle.checked = this.#gradient.rotating;
|
||||||
moveSpeedSlider.value = String(this.#evolution.moveSpeed);
|
moveSpeedSlider.value = String(this.#evolution.moveSpeed);
|
||||||
|
document.getElementById('moveSpeedValue').textContent = String(this.#evolution.moveSpeed);
|
||||||
spawnRateSlider.value = String(this.#evolution.spawnRate);
|
spawnRateSlider.value = String(this.#evolution.spawnRate);
|
||||||
|
document.getElementById('spawnRateValue').textContent = String(this.#evolution.spawnRate);
|
||||||
virusKillSlider.value = String(Math.round(this.#evolution.virusKillChance * 100));
|
virusKillSlider.value = String(Math.round(this.#evolution.virusKillChance * 100));
|
||||||
|
document.getElementById('virusKillValue').textContent = virusKillSlider.value + '%';
|
||||||
bugChanceSlider.value = String(Math.round(this.#evolution.bugSpawnChance * 100));
|
bugChanceSlider.value = String(Math.round(this.#evolution.bugSpawnChance * 100));
|
||||||
|
document.getElementById('bugChanceValue').textContent = bugChanceSlider.value + '%';
|
||||||
bugCountSlider.value = String(this.#evolution.bugMaxCount);
|
bugCountSlider.value = String(this.#evolution.bugMaxCount);
|
||||||
debugHitCountToggle.checked = false; // debug off by default on sync/reset
|
document.getElementById('bugCountValue').textContent = bugCountSlider.value;
|
||||||
|
thresholdMinInput.value = String(DEFAULTS.LOGO_BUMP_THRESHOLD_MIN);
|
||||||
|
document.getElementById('thresholdMinValue').textContent = thresholdMinInput.value;
|
||||||
|
thresholdMaxInput.value = String(DEFAULTS.LOGO_BUMP_THRESHOLD_MAX);
|
||||||
|
document.getElementById('thresholdMaxValue').textContent = thresholdMaxInput.value;
|
||||||
|
// leave the toggle alone; its state is already correct from the
|
||||||
|
// controllers / saved settings. forcibly turning it off here prevented the
|
||||||
|
// stored value from ever sticking.
|
||||||
|
// debugHitCountToggle.checked = false; // logo hit‑count debug off by default on sync/reset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,26 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Threshold label ───────────────────────────────────── */
|
||||||
|
.logo-letter__threshold {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 3px);
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-family: 'DM Mono', monospace;
|
||||||
|
font-size: 8px;
|
||||||
|
line-height: 1;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-letter--show-threshold .logo-letter__threshold {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Hit flash animation ─────────────────────────────────── */
|
/* ── Hit flash animation ─────────────────────────────────── */
|
||||||
@keyframes logo-letter-hit {
|
@keyframes logo-letter-hit {
|
||||||
0% { scale: 1; }
|
0% { scale: 1; }
|
||||||
|
|||||||
@@ -225,6 +225,29 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slider-current {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.72rem; /* larger for better legibility */
|
||||||
|
letter-spacing: 0.09em;
|
||||||
|
color: #fff !important; /* always white for readability */
|
||||||
|
text-shadow: 0 0 2px rgba(0,0,0,0.8);
|
||||||
|
user-select: none;
|
||||||
|
/* remove background so only text remains */
|
||||||
|
background: transparent;
|
||||||
|
padding: 0 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
width: 3ch; /* fixed column width */
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-wrapper {
|
||||||
|
display: grid;
|
||||||
|
/* columns: current-value fixed width, first label auto, slider flex, last label auto */
|
||||||
|
grid-template-columns: 3ch auto 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-height: 28px; /* ensure consistent row height */
|
||||||
|
}
|
||||||
/* Reset */
|
/* Reset */
|
||||||
.slider {
|
.slider {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user