diff --git a/CHANGELOG.md b/CHANGELOG.md
index e9c9271..d51a7db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Clean up corrupted Unicode Mojibake artifacts in media/sidebar.html (Fixes #28).
- Update .vscodeignore to exclude internal scripts/tests/CI and add Content-Security-Policy to sidebar webview (Fixes #29).
- Fix pet mood duration and unlock typing combo meter in Feed/Play commands (Fixes #23).
+- Fix accessory positioning and eye-blink transform-origins for Cat and Robot pet species (Fixes #27).
---
diff --git a/src/dashboard.js b/src/dashboard.js
index 22f5868..8a3f294 100644
--- a/src/dashboard.js
+++ b/src/dashboard.js
@@ -88,7 +88,7 @@ function _updatePanel(statsEngine, petName, currentMood) {
const petSpecies = config.get('petSpecies', 'alien');
const { getPetSvg, getAccessoriesSvg } = require('./pets');
- const petSvgRaw = getPetSvg(petSpecies).replace('__ACCESSORIES__', getAccessoriesSvg(stats.level));
+ const petSvgRaw = getPetSvg(petSpecies).replace('__ACCESSORIES__', getAccessoriesSvg(stats.level, petSpecies));
const milestoneData = MILESTONES.map(m => ({
id: m.id,
diff --git a/src/pets.js b/src/pets.js
index 3eb294c..3ee3940 100644
--- a/src/pets.js
+++ b/src/pets.js
@@ -34,13 +34,13 @@ function getPetSvg(species) {
-
+
-
+
@@ -99,11 +99,11 @@ function getPetSvg(species) {
-
+
-
+
@@ -151,10 +151,10 @@ function getPetSvg(species) {
-
+
-
+
@@ -222,11 +222,39 @@ function getPetSvg(species) {
/**
* Returns accessory SVGs based on level.
*/
-function getAccessoriesSvg(level) {
+/**
+ * Returns accessory SVGs based on level and pet species.
+ *
+ * @param {number} level
+ * @param {string} [species='alien']
+ * @returns {string}
+ */
+function getAccessoriesSvg(level, species = 'alien') {
let svg = '';
// Level 5: Sunglasses
if (level >= 5) {
- svg += \`
+ if (species === 'robot') {
+ svg += `
+
+
+
+
+
+
+
+ `;
+ } else if (species === 'cat') {
+ svg += `
+
+
+
+
+
+
+
+ `;
+ } else {
+ svg += `
@@ -234,21 +262,61 @@ function getAccessoriesSvg(level) {
- \`;
+ `;
+ }
}
// Level 10: Top Hat
if (level >= 10 && level < 15) {
- svg += \`
+ if (species === 'cat') {
+ svg += `
+
+
+
+
+
+ `;
+ } else if (species === 'robot') {
+ svg += `
+
+
+
+
+
+ `;
+ } else {
+ svg += `
- \`;
+ `;
+ }
}
// Level 15: Party Hat
if (level >= 15 && level < 20) {
- svg += \`
+ if (species === 'cat') {
+ svg += `
+
+
+
+
+
+
+
+ `;
+ } else if (species === 'robot') {
+ svg += `
+
+
+
+
+
+
+
+ `;
+ } else {
+ svg += `
@@ -256,11 +324,33 @@ function getAccessoriesSvg(level) {
- \`;
+ `;
+ }
}
// Level 20: Crown
if (level >= 20) {
- svg += \`
+ if (species === 'cat') {
+ svg += `
+
+
+
+
+
+
+
+ `;
+ } else if (species === 'robot') {
+ svg += `
+
+
+
+
+
+
+
+ `;
+ } else {
+ svg += `
@@ -268,9 +358,9 @@ function getAccessoriesSvg(level) {
- \`;
+ `;
+ }
}
-
return svg;
}
diff --git a/src/sidebar.js b/src/sidebar.js
index e13d768..5c543e8 100644
--- a/src/sidebar.js
+++ b/src/sidebar.js
@@ -74,7 +74,7 @@ class SidebarProvider {
const currentMood = this.getCurrentMood();
const petName = this.getPetName();
- const petSvgRaw = getPetSvg(petSpecies).replace('__ACCESSORIES__', getAccessoriesSvg(stats.level));
+ const petSvgRaw = getPetSvg(petSpecies).replace('__ACCESSORIES__', getAccessoriesSvg(stats.level, petSpecies));
const htmlPath = path.join(__dirname, '..', 'media', 'sidebar.html');
let html = 'Sidebar HTML not found.';
diff --git a/tests/pets.test.js b/tests/pets.test.js
new file mode 100644
index 0000000..b565b5a
--- /dev/null
+++ b/tests/pets.test.js
@@ -0,0 +1,50 @@
+'use strict';
+
+const assert = require('node:assert/strict');
+const test = require('node:test');
+const { getPetSvg, getAccessoriesSvg } = require('../src/pets');
+
+test('Issue #27: getPetSvg applies species-specific eye-blink transform-origins', () => {
+ const alienSvg = getPetSvg('alien');
+ assert.ok(alienSvg.includes('class="eye-l" style="transform-origin: 57px 55px"'), 'alien left eye origin');
+ assert.ok(alienSvg.includes('class="eye-r" style="transform-origin: 83px 55px"'), 'alien right eye origin');
+
+ const catSvg = getPetSvg('cat');
+ assert.ok(catSvg.includes('class="eye-l" style="transform-origin: 58px 60px"'), 'cat left eye origin');
+ assert.ok(catSvg.includes('class="eye-r" style="transform-origin: 82px 60px"'), 'cat right eye origin');
+
+ const robotSvg = getPetSvg('robot');
+ assert.ok(robotSvg.includes('class="eye-l" style="transform-origin: 58px 48px"'), 'robot left eye origin');
+ assert.ok(robotSvg.includes('class="eye-r" style="transform-origin: 82px 48px"'), 'robot right eye origin');
+});
+
+test('Issue #27: getAccessoriesSvg adapts coordinates based on pet species', () => {
+ // Level 5: Sunglasses
+ const robotSunglasses = getAccessoriesSvg(5, 'robot');
+ assert.ok(robotSunglasses.includes('transform="translate(0, -10)"'), 'robot sunglasses shifted up to eye level');
+
+ const catSunglasses = getAccessoriesSvg(5, 'cat');
+ assert.ok(catSunglasses.includes('transform="translate(0, 3)"'), 'cat sunglasses positioned to cat eye level');
+
+ const defaultSunglasses = getAccessoriesSvg(5);
+ assert.ok(defaultSunglasses.includes('acc-sunglasses'), 'default sunglasses rendered');
+ assert.equal(defaultSunglasses.includes('transform='), false, 'default alien sunglasses do not need translate transform');
+
+ // Level 10: Top Hat
+ const catTopHat = getAccessoriesSvg(10, 'cat');
+ assert.ok(catTopHat.includes('rect x="50" y="36" width="40"'), 'cat top hat sits between ears');
+
+ const robotTopHat = getAccessoriesSvg(10, 'robot');
+ assert.ok(robotTopHat.includes('transform="translate(0, -5)"'), 'robot top hat positioned on head');
+
+ // Level 15: Party Hat
+ const catPartyHat = getAccessoriesSvg(15, 'cat');
+ assert.ok(catPartyHat.includes('points="70,12 54,38 86,38"'), 'cat party hat base fitted between ears');
+
+ // Level 20: Crown
+ const catCrown = getAccessoriesSvg(20, 'cat');
+ assert.ok(catCrown.includes('M 52 38 L 55 18'), 'cat crown fits between ears');
+
+ const robotCrown = getAccessoriesSvg(20, 'robot');
+ assert.ok(robotCrown.includes('transform="translate(0, -8)"'), 'robot crown shifted for head antenna');
+});