Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@ Notable changes to **pixelpets**. All art and sound are original/procedural (no

## [Unreleased]

### Settings window
- **Sections instead of one long scroll.** Everything lived in a single column: eleven cards, about 3400px of it, inside a window fixed at 560px. Reaching the pomodoro or the calendar meant scrolling past ten cards you were not looking for, with nothing to aim at and no sense of how much was left. The window is now five tabs - Pet, Play, Sound, Focus, Feeds - and the tallest of them is 1130px, with Sound fitting on screen whole. The rail is keyboard-drivable (arrows, Home/End) and carries the ARIA a tablist is supposed to.
- **The window stopped calling itself pixelcat.** 0.3.0 renamed the app, the repo and the docs, but the settings header still read "pixelcat" - at people who had just chosen a dog.
- **A dog owner is no longer told about a cat.** The window hard-coded the cat's nouns, so a dog owner read "your cat", "the cat calls you by it", "Butterfly visits - a butterfly drops by and the cat plays with it" and "Test meow", while the tray - reading the species registry - already called that same toggle "Ball to chase". Those strings now live in `pets.js` beside the tray's, so the two windows cannot describe one toggle differently, and a test fails if any dog-facing string mentions a cat.
- **Settings can be made taller.** Width stays pinned to the one-column layout, but the height is draggable now (420 minimum, 640 default) rather than frozen at 560.
- **Keyboard focus is visible again.** The toggle switches are `appearance:none`, which had taken their focus ring with them, so tabbing through the window gave no clue which switch was about to be flipped.

### Fixes
- **The dog coat preview drew a cat.** Picking a breed relabelled everything correctly and then went on showing the previously drawn cat, because the preview read its palettes off `window.DOG_PATTERNS` - and `dog-sprite.js` is a classic script whose top-level `const`s are global *lexical* bindings that never become window properties. The lookup came back undefined and the draw bailed out early and silently, so the preview had never once rendered a dog.
- **The coat list could belong to the wrong species.** The first config and the first custom-coat list arrive as two independent IPC replies, and only the themes reply rebuilt the dropdown, so whenever it won the race a dog owner was offered the cat's coats.
- **Speech bubbles hold their text.** The panel was capped at 260px but the whole message was drawn anyway, so anything past roughly 44 characters spilled white text onto the wallpaper either side of the box, where it is unreadable. Reminders and the pinned note are allowed 80 characters and calendar event titles had no cap at all, so this was the normal case rather than an edge case. Messages now wrap, an over-long word is broken instead of left hanging, a message too long to show at all is ellipsised rather than silently cut, and the panel is clamped onto the screen with its tail still pointing at the pet - which matters because the pet's default resting spot is a screen corner.
- **Alerts no longer overwrite each other.** Two arriving together - two reminders set for the same minute, or a reminder landing during a calendar nudge - meant the second replaced the first on the spot, so the first could be gone milliseconds after it appeared. Only identical messages were ever suppressed, so two different alerts always collided. They now queue, and each gets its full time on screen, including while the pet is hunting or startled.
- **Calendar event titles are length-capped** before they become a bubble and a Windows toast. They come from someone else's calendar and, unlike reminders and the pinned note, arrived with no bound at all.
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -384,11 +384,12 @@ Ideas only; all code here is original to pixelpets.</sub>

## Custom coats

Design your own under Settings > Custom coats > "+ Add a custom coat": pick a
name, a body build (standard, slender, stocky, or fluffy), optional tabby
Design your own under Settings > Pet > Custom coats > "+ Add a custom coat": pick
a name, a body build (standard, slender, stocky, or fluffy), optional tabby
stripes, and eight colours (coat, marks, white, patch, eyes, nose, inner ear,
outline). Your coat shows up in the Coat dropdown and the tray menu next to the
14 built-ins.
14 built-ins. Custom coats are built from the cat's geometry, so they apply to
cats only; the Pet tab says so when a dog is selected.

Custom coats live in `themes.json` in your app-data folder
(`%APPDATA%/pixelcat/themes.json`) and can be hand-edited too:
Expand Down
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const sharedOverlay = {
// pets.js provides the species registry:
PET_SPECIES: 'readonly', SPECIES_IDS: 'readonly', speciesOf: 'readonly', coatsFor: 'readonly',
isSpecies: 'readonly', defaultCoatIndex: 'readonly', CAT_COATS: 'readonly', DOG_COATS: 'readonly',
SETTINGS_TEXT: 'readonly', settingsText: 'readonly',
BUILDS: 'readonly', TABBY: 'readonly', PATTERN_BUILD: 'readonly',
// art-frames.js provides the generated baked-pose table:
ART_FRAMES: 'readonly',
Expand Down
6 changes: 5 additions & 1 deletion src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,11 @@ function rebuildTrayMenu() {
function openSettings() {
if (settingsWin && !settingsWin.isDestroyed()) { settingsWin.show(); settingsWin.focus(); return; }
settingsWin = new BrowserWindow({
width: 400, height: 560, resizable: false, fullscreenable: false, maximizable: false,
// Width is pinned (the layout is designed for one column at 400), but height is
// now draggable: the tallest section still overflows 640px on a short screen and
// a fixed window left no way out of that but scrolling.
width: 400, height: 640, minWidth: 400, maxWidth: 400, minHeight: 420,
resizable: true, fullscreenable: false, maximizable: false,
title: 'pixelpets settings', skipTaskbar: false, alwaysOnTop: true,
icon: path.join(__dirname, '..', 'assets', 'icon.png'), // taskbar icon for the settings window
show: false, backgroundColor: '#191b22', // dark from the first paint - no white flash
Expand Down
56 changes: 55 additions & 1 deletion src/pets.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ const SPECIES = {
// The companion the pet plays with on its own once you step away.
playNoun: 'butterfly',
playToggleLabel: 'Butterfly visits',
// Vocabulary the settings window writes into SETTINGS_TEXT below.
noun: 'cat',
voice: 'meow',
voiceLine: 'meow & purr',
chase: 'pounces',
playArrival: 'drops by',
customCoatNote: 'Design your own and pick it from Coat above.',
},
dog: {
id: 'dog',
Expand All @@ -50,9 +57,56 @@ const SPECIES = {
giveChannel: 'ball',
playNoun: 'ball',
playToggleLabel: 'Ball to chase',
noun: 'dog',
voice: 'bark',
voiceLine: 'bark & pant',
chase: 'chases',
playArrival: 'rolls in',
// Custom coats are built from the cat's geometry (see populateCoats), so a dog
// owner needs to be told that rather than left staring at a list that never grows.
customCoatNote: 'Custom coats are cat-only for now. Switch to Cat to use one.',
},
};

// Every string in the settings window whose wording depends on the pet, keyed by
// the element id that displays it. The window used to hard-code the cat's nouns in
// its markup, so a dog owner read "the cat calls you by it" on rows the TRAY had
// already learned to call "Ball to chase" - two UIs disagreeing about one toggle.
//
// %token% is looked up on the species entry. The braces in the reminders hint are a
// DIFFERENT substitution (fillPlaceholders expands {name}/{time}/{date} at meow time)
// and must survive this pass untouched, which is why these use percent signs.
const SETTINGS_TEXT = {
petCardTitle: 'your %noun%',
nameLabel: 'Your name - the %noun% calls you by it',
coatLabel: '%coatNoun%',
huntSub: '%chase% when the mouse moves fast',
playTitle: '%playToggleLabel%',
playSub: 'a %playNoun% %playArrival% and the %noun% plays with it',
workModeSub: 'parks the %noun% in its rest corner on the taskbar & hides the %playNoun% while you work',
onTopSub: 'keep the %noun% above other windows',
soundSub: '%voiceLine% (synthesized)',
pomoSub: 'a pixel timer floats next to the %noun%; it stretches with you on breaks',
emailSub: 'the %noun% tells you when new mail arrives (IMAP)',
calSub: 'the %noun% reminds you before calendar events (.ics)',
remindersHint: 'The %noun% %voice%s your message at a set time. Placeholders: {name} {time} {date}',
pinnedNoteLabel: "Pinned note - stays above the %noun%'s head (leave empty to hide)",
coatsHint: '%customCoatNote%',
testSound: '🔊 Test %voice%',
};

// Resolve SETTINGS_TEXT for one species: { elementId: finalString }. An unknown
// %token% is left alone rather than blanked, so a typo shows up in the window as
// literal "%typo%" instead of silently deleting half a sentence.
function settingsText(species) {
const sp = speciesOf(species);
const out = {};
for (const [id, tpl] of Object.entries(SETTINGS_TEXT)) {
out[id] = tpl.replace(/%(\w+)%/g, (m, k) => (typeof sp[k] === 'string' ? sp[k] : m));
}
return out;
}

const SPECIES_IDS = Object.keys(SPECIES);
const isSpecies = (s) => Object.prototype.hasOwnProperty.call(SPECIES, s);
const speciesOf = (s) => SPECIES[isSpecies(s) ? s : 'cat'];
Expand All @@ -62,7 +116,7 @@ const defaultCoatIndex = (s) => {
return Math.max(0, sp.coats.indexOf(sp.defaultCoat));
};

const api = { SPECIES, SPECIES_IDS, CAT_COATS, DOG_COATS, isSpecies, speciesOf, coatsFor, defaultCoatIndex };
const api = { SPECIES, SPECIES_IDS, CAT_COATS, DOG_COATS, isSpecies, speciesOf, coatsFor, defaultCoatIndex, SETTINGS_TEXT, settingsText };

if (typeof module !== 'undefined' && module.exports) module.exports = api;
else if (typeof window !== 'undefined') Object.assign(window, api, { PET_SPECIES: SPECIES });
61 changes: 54 additions & 7 deletions src/settings-renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,46 @@
const $ = (id) => document.getElementById(id);
let cfg = null;

// ---- section rail ----------------------------------------------------------
// Everything used to live in one 3400px column inside a 560px window, so finding
// "pomodoro" meant scrolling past ten cards. Panels are shown by the `hidden`
// attribute alone; aria-selected and the roving tabindex follow it.
const TABS = Array.from(document.querySelectorAll('.tab'));
function selectTab(key, { focus = false } = {}) {
for (const t of TABS) {
const on = t.dataset.panel === key;
t.setAttribute('aria-selected', String(on));
t.tabIndex = on ? 0 : -1; // one stop for the whole rail, arrows move within it
const panel = $('panel-' + t.dataset.panel);
if (panel) panel.hidden = !on;
if (on && focus) t.focus();
}
window.scrollTo(0, 0); // a fresh panel starts at its top, not the last panel's offset
}
TABS.forEach((t, i) => {
t.addEventListener('click', () => selectTab(t.dataset.panel));
t.addEventListener('keydown', (e) => {
const step = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
let next = null;
if (step) next = TABS[(i + step + TABS.length) % TABS.length];
else if (e.key === 'Home') next = TABS[0];
else if (e.key === 'End') next = TABS[TABS.length - 1];
if (!next) return;
e.preventDefault();
selectTab(next.dataset.panel, { focus: true });
});
});
selectTab('pet');

// ---- per-species wording ---------------------------------------------------
// The strings live in pets.js next to the tray's, so the two windows cannot end up
// describing the same toggle differently (the settings window said "Butterfly
// visits" at dog owners while the tray already said "Ball to chase").
function applySpeciesText() {
const text = settingsText(curSpecies());
for (const [id, s] of Object.entries(text)) { const el = $(id); if (el) el.textContent = s; }
}

// Populate the coat dropdown from the built-in names plus any custom coats.
let themes = [];
// Which species the window is currently editing, and therefore which list of
Expand All @@ -18,23 +58,26 @@ function populateCoats() {
const base = dog ? (window.DOG_COATS || []) : (window.PATTERN_NAMES || []);
const names = dog ? base.slice() : base.concat(themes.map((t) => t.name));
names.forEach((name, i) => { const o = document.createElement('option'); o.value = String(i); o.textContent = name; sel.appendChild(o); });
const lbl = $('coatLabel'); if (lbl) lbl.textContent = dog ? 'Breed' : 'Coat';
if (cfg) sel.value = String(cfg[coatField()] || 0); else if (cur) sel.value = cur;
const spSel = $('species'); if (spSel) spSel.value = curSpecies();
// The Sound row names the voices you will actually hear, which are not the same
// two sounds for both species.
const sub = $('soundSub');
if (sub) sub.textContent = dog ? 'bark & pant (synthesized)' : 'meow & purr (synthesized)';
// Coat vs Breed, which voice the Sound row promises, whether custom coats apply:
// all of it is one lookup now.
applySpeciesText();
drawPreview();
}
function drawPreview() {
const P = window.PixelcatPreview, cv = $('coatPreview');
if (!P || !cv) return;
const i = Number($('pattern').value) || 0;
if (curSpecies() === 'dog') {
const pal = (window.DOG_PATTERNS || [])[i];
// Bare identifiers, NOT window.*: dog-sprite.js is a classic script whose top-level
// `const`s live in the global LEXICAL scope and never become window properties (it
// has no window-export branch, only module.exports). Reading window.DOG_PATTERNS
// got undefined, so this returned early and left the cat on the canvas - the dog
// coat preview never drew once. cat-preview.js's drawDog reads them the same way.
const pal = DOG_PATTERNS[i];
if (!pal || !P.drawDog) return;
P.drawDog(cv, pal, (window.DOG_PATTERN_BUILD || [])[i]);
P.drawDog(cv, pal, DOG_PATTERN_BUILD[i]);
return;
}
let pal, build, tabby;
Expand All @@ -61,6 +104,10 @@ function render() {
// Don't stomp the name field while the user is typing in it (a broadcast config
// echo would otherwise overwrite it with the normalized value and jump the caret).
if (document.activeElement !== $('name')) $('name').value = cfg.name || '';
// Rebuild from the config's species before reading a coat index out of it. The
// first config and the first theme list arrive as two independent IPC replies, so
// whenever the themes won the race a dog owner got the cat's coat list.
populateCoats();
$('pattern').value = String(cfg[coatField()] || 0);
$('breakMinutes').value = String(cfg.breakMinutes || 0);
$('followCursor').checked = !!cfg.followCursor;
Expand Down
Loading