From be34bd2b2d24c3df96aeaa4bc088873b626c0c81 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 11 Jul 2026 07:02:01 -0400 Subject: [PATCH] feat(tavern): a real cat (Pet Cats Pack, CC-0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old 'cat' used a char-goblin sprite (green monster). Replaced with actual pixel cats from the Pet Cats Pack (CC-0, commercial-ok): - html/assets/cats/cat1..6-idle.png — 6 cats, 10-frame 50x50 idle strips. - catSprites.js: loads + animates a cat sprite (RK.drawCatCanvas), flips to face left. - tileRenderer: draws entities with kind 'cat' via the cat sprite path (shadow + frame). - tavernLife: spawns a RANDOM cat that wanders the customer floor (not behind the bar), injected as kind 'cat' with its catSprite id. Degrades to a villager if catSprites.js isn't loaded. - tavern.html loads catSprites.js. Client-only (no server change). Suite 57 suites / 413 tests green. --- html/assets/cats/cat1-idle.png | Bin 0 -> 968 bytes html/assets/cats/cat2-idle.png | Bin 0 -> 847 bytes html/assets/cats/cat3-idle.png | Bin 0 -> 963 bytes html/assets/cats/cat4-idle.png | Bin 0 -> 976 bytes html/assets/cats/cat5-idle.png | Bin 0 -> 1022 bytes html/assets/cats/cat6-idle.png | Bin 0 -> 1001 bytes html/js/render/catSprites.js | 44 +++++++++++++++++++++++++++++++++ html/js/render/tavernLife.js | 15 ++++++++--- html/js/render/tileRenderer.js | 5 ++++ html/tavern.html | 1 + 10 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 html/assets/cats/cat1-idle.png create mode 100644 html/assets/cats/cat2-idle.png create mode 100644 html/assets/cats/cat3-idle.png create mode 100644 html/assets/cats/cat4-idle.png create mode 100644 html/assets/cats/cat5-idle.png create mode 100644 html/assets/cats/cat6-idle.png create mode 100644 html/js/render/catSprites.js diff --git a/html/assets/cats/cat1-idle.png b/html/assets/cats/cat1-idle.png new file mode 100644 index 0000000000000000000000000000000000000000..cdecebd2277c7a7bdd7fc5c308add1600bc62c1d GIT binary patch literal 968 zcmeAS@N?(olHy`uVBq!ia0y~yVEh7P8*#7!$yEZH(}5IYage(c!@6@aFBuq^n><|{ zLn`LHy|XcUO1aGOkN!EzEkW$ET8^6TR;*SfMH|X=mbwdw`VHyAJeq53|%c4fHg-5naS86G85+}tB~UD&wq%IbNx zJMRAylRfU+o8h8Q7Rep==l8V0mntP^x^Nmw&F7BBnQ)$GOe8nfdTE^^P7Wz3w55l+*_*b`R!Z0>@J1e%|KJ9 zzR+mx+q7lrW0SM7vS;1u7F^HwjF+N*X%pk~gXNz`JW>f%)y|*`AZ+-%l+yoW=8cN7Y}y|M$(V{djS~ zkT3S7#j4j}}imQ9&F1zwl8S`)bYtNpG zJvlwTb}rB#$>-~CFX@#%%`Gl%&t{nOckj>af{j_#$2T0VDSPLY zHu+yw`(7o@lN%bZZtGLCQd`dbdsSY>?>%0-49l}8zmj;_Xt!hMq?DiE_b;E6^6}Zv zReZbt+*JR2+@3ww=Dek^V~bfvNHan?9%F0alaQI_y2Wx z^%3bME0^}lo_sjX<+w^|V!Wy8mclsIyEArg6*_lNSpLOp@4Z=hWopHnjobMqnyr)$ zGmG=vx^rRN{{(00{0YwMZLAD7OfuKCI(6Y&?Nw1<0T~-LTNZKLYv{f+cQ4c4-8xmwPU2e`>o0`v_7pEWV>oZJ z+vLe-dd@TJ^(cKVdZupKDF`%`5SZI{{PE3o#~D7%y0Yp0tBN;g)&8CEUfLn0=f%q_ zXjQaWS>Ln-ER(h2UqRfm+z0F- zsXtj>SJ=$c-8SV{PM~{S$&;mReY3vZefHhz*}Zjt#iL~8WY2!HtB?En-QSPhaPN*6 z$r{;}dB<;VkDN0{?7Paw(xjI+WMwNgmpU(+A z_0O-L#=ECI_gTXsTYG6?^5wYS`&K`W(_epKcHX-6^0o61&FcSeVSoNr(w4|;7xzS1 z>VE3@dBp2+JkaPD?R>JgKS+JM>vBBiJ$u!WzYoNly{=rHC}+HJn>)z0%fHobH@W@d z#;Nv4msa0+e(%~T-%Qzfx5d}|Bc~Ys`+sqwxa{tWThDplUcc)_MC|+Z7vAUP&$qe8 z^Xv2P^83ye+qSU=MY&&B>aX}^=J@X4_Qa`v$MdcjtluT&b$h+is~hFJb5CqG(b@ie;Vj-|JPxxBuNhyIr0OufAlZbSt{#jUTUUYK? jjlVK1gwiGh!>@1RZcCP{J&HDu0V(!$^>bP0l+XkK=~0Z0 literal 0 HcmV?d00001 diff --git a/html/assets/cats/cat3-idle.png b/html/assets/cats/cat3-idle.png new file mode 100644 index 0000000000000000000000000000000000000000..54b20a36deb394f10151f97f53e61fcc6e02b764 GIT binary patch literal 963 zcmeAS@N?(olHy`uVBq!ia0y~yVEh7P8*#7!$yEZH(}5IYage(c!@6@aFBuq^Ydu{Y zLn`LHy>l>2+FjJ_jS7H=VC*T~N%ob6G&k!Y@HwybDFDG+G6sia4eSC4D$6 z(PLn7U8G1SEAF8F1@9M*{sJeyJ$M%txBkkBNxO^>e|L1)crCH+;<+BOl}ylq@=EHz(Mmr7 z*~|+Q!`!3Sy$+q-cQxma{zGT`mFD$FH|^g)|4w&cmHTzk$LsgoWXj3UpR0a#k@Io0 zDfcc;dtG&P+2Uw@`L6Dcsq5N;rxhJPSh45BlBlw+Gaj)-y0}M`{P{oa?B{J$uCES# z@nYM@=y&Sw?brY9eR*{A@{f;hUY0&yV(WMGU`5Q+dmB%w^zYcEI^~f|Wa&Y#t;wQR z+t#;yUX^#Ec5jzP^4+yxuWr0p{(}8+q3HYju1B6SyuKa8|L*?kS2L{3?Q;A4KOW8S z|0HAms_pX0ln*C=mxmsCdfwd4r8vm;&%@)-eoS6`_}R~p=x6nQ#!CD9I##o4JGtNY zvRMD@`Oon9ntNZKY>RW9vhm%uUw7EeH!VEBU*dTB^Rp+fe*1Lw^|PH9Gq!x#VRy!S zj-Og{N5>tjT~b*)eJ7N8pW_a@ZtW=hSL5NHSF@g9yZdkH2L5%mr~mGD?_c$>K;rPj z569Fdb?-R3ySO2RC_`hm@SxuSU4e57B&hzhIr?gJ(So81itM)$hd&66~w>z`h^X!gTe~DWM4f({$DC literal 0 HcmV?d00001 diff --git a/html/assets/cats/cat4-idle.png b/html/assets/cats/cat4-idle.png new file mode 100644 index 0000000000000000000000000000000000000000..537b019a5dc859d4f476a436a8dbaad8ca107508 GIT binary patch literal 976 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 J%Q~loCIFs~z~=w} literal 0 HcmV?d00001 diff --git a/html/js/render/catSprites.js b/html/js/render/catSprites.js new file mode 100644 index 0000000..a7b2b39 --- /dev/null +++ b/html/js/render/catSprites.js @@ -0,0 +1,44 @@ +(function (root) { + 'use strict'; + var RK = root.RK = root.RK || {}; + + // Pet Cats Pack (CC-0). Each idle strip is 10 frames of 50x50 laid out horizontally. + var FRAMES = 10, FW = 50, FH = 50, FRAME_MS = 150; + var CATS = ['cat1', 'cat2', 'cat3', 'cat4', 'cat5', 'cat6']; + var images = {}; + + function imgFor(id) { + if (!CATS.includes(id)) id = 'cat3'; + if (!images[id]) { + var im = new Image(); + im.src = 'assets/cats/' + id + '-idle.png'; + images[id] = im; + } + return images[id]; + } + + RK.catIds = function () { return CATS.slice(); }; + RK.randomCatId = function () { return CATS[Math.floor(Math.random() * CATS.length)]; }; + RK.isCat = function (id) { return CATS.indexOf(id) !== -1; }; + + // Draw an animated cat centered in its cell, feet near the bottom of the tile. Flips to face left. + RK.drawCatCanvas = function (ctx, e, cell, now) { + var im = imgFor(e.catSprite || 'cat3'); + if (!im.complete || !im.naturalWidth) return false; + var frame = Math.floor((now || 0) / FRAME_MS) % FRAMES; + var sx = frame * FW; + var size = cell * 1.25; // a touch bigger than a tile + var dx = e.x * cell + (cell - size) / 2; + var dy = e.y * cell + cell - size + cell * 0.12; + ctx.save(); + ctx.imageSmoothingEnabled = false; + if (e.facing === 'left') { + ctx.translate(dx + size, dy); ctx.scale(-1, 1); + ctx.drawImage(im, sx, 0, FW, FH, 0, 0, size, size); + } else { + ctx.drawImage(im, sx, 0, FW, FH, dx, dy, size, size); + } + ctx.restore(); + return true; + }; +})(window); diff --git a/html/js/render/tavernLife.js b/html/js/render/tavernLife.js index 7b09adb..556bc1e 100644 --- a/html/js/render/tavernLife.js +++ b/html/js/render/tavernLife.js @@ -78,7 +78,8 @@ return { minY: 3, maxY: rows - 2, minX: 1, maxX: cols - 2 }; } function catArea(cols, rows) { - return { minY: 1, maxY: rows - 1, minX: 1, maxX: cols - 2 }; + // The customer floor (in front of the bar) — not behind the counter, not the walls. + return { minY: 4, maxY: rows - 2, minX: 1, maxX: cols - 2 }; } // ---- Speech bubbles --------------------------------------------------------- @@ -220,8 +221,13 @@ })); } - // (No "cat": it used the char-goblin sprite, which read as a green monster stuck in the - // wall. Left out until there's an actual pet/critter sprite.) + // A tavern cat — a random pixel cat from the Pet Cats Pack that wanders the floor. + var catSpot = findFloor(4, rows - 4); + var cat = makeNPC('cat', 'cat', null, catSpot.x, catSpot.y, { + label: null, color: '#e8a060', role: 'cat', roamArea: catArea(cols, rows) + }); + cat.catSprite = (RK.randomCatId ? RK.randomCatId() : 'cat3'); + this.npcs.push(cat); }; TavernLife.prototype._pickTarget = function (npc, scene) { @@ -303,9 +309,10 @@ scene.entities.push({ id: npc.id, x: npc.x, y: npc.y, - kind: 'avatar', + kind: npc.role === 'cat' ? 'cat' : 'avatar', avatar: npc.avatar, appearance: npc.appearance, + catSprite: npc.catSprite || null, color: npc.color, char: npc.role === 'cat' ? 'c' : '@', facing: npc.facing, diff --git a/html/js/render/tileRenderer.js b/html/js/render/tileRenderer.js index ca08242..bf5f78b 100644 --- a/html/js/render/tileRenderer.js +++ b/html/js/render/tileRenderer.js @@ -188,6 +188,11 @@ ctx.stroke(); continue; } + // Tavern cat — a standalone animated sprite (Pet Cats Pack), not a char-atlas avatar. + if (e.kind === 'cat') { + drawEntityShadow(ctx, e.x * cell + cell / 2, e.y * cell + cell * 0.9, cell * 0.28, cell * 0.11); + if (window.RK && RK.drawCatCanvas && RK.drawCatCanvas(ctx, e, cell, now)) continue; + } // Character-like entity: soft ground shadow, then the sprite (or fallback circle). drawEntityShadow(ctx, e.x * cell + cell / 2, e.y * cell + cell * 0.92, cell * 0.34, cell * 0.14); // Roguelike character sprite, or premium animated skin; else the fallback circle. diff --git a/html/tavern.html b/html/tavern.html index ccb64ef..538e121 100644 --- a/html/tavern.html +++ b/html/tavern.html @@ -13,6 +13,7 @@ +