7.2 KiB
den.js
An invisible kobold lives on your website.
You never see him. You see his paw prints, his little dialogue, and — after ten minutes of quiet — the bed he drags out to sleep in. He walks to your cursor, gets bored and wanders off to sniff at things, remembers you between visits, and depending on how well he knows you he will either steal your buttons and run, or bring them to you as gifts.
One script tag. No dependencies. No build step required.
<script src="/den.js"></script>
That's it. That's the integration.
What he does
Movement. He has a position, a heading, and a turn rate, so he can't snap around instantly — chase him with your cursor and the prints carve an arc as he wheels to follow. He trots when you're close, sprints when you're far, and lays prints by distance travelled rather than on a timer, so his stride stays physically consistent at any framerate.
Moods. chase when your cursor moves, rest when he arrives, wander when
he gets bored, errand when he's been sent somewhere, mischief when he's
committing a crime. Emote bubbles float off him — ❗ when he notices you,
👃 while sniffing, 💤 while dozing, hearts when he's fond of you.
Sleep. Ten minutes with no input and he yawns, walks the tight circle every canid walks before lying down, and a bed fades in with a breathing lump under the blanket. Any keypress, click, scroll, or mouse twitch wakes him — groggily.
Memory. Visit count, lifetime pets, and his name persist in localStorage.
Return after a week and he says so. Pet him ten times and he decides you're
friends; at twenty-five he starts lingering near your cursor instead of
wandering off, and his stealing turns into gift-giving.
A shell. A >_ den tab sits in the corner of every page. Open it and you can
come, sit, pet, fetch, sleep, where, status, or name him.
Presence. Optional. With den.php on the server he'll notice how many other
people are on the site and mention that he can smell them.
Install
Copy src/den.js (or a build from build/) to your web root and add the script
tag. Nothing else is required — he injects his own CSS and brings his own audio.
For the presence counter, also copy src/den.php to your web root, change the
secret at the top, and make sure PHP can write one file next to it:
touch ../owo_presence.txt
chown www-data:www-data ../owo_presence.txt
The storage file lives one level above the document root so it can never be fetched over HTTP.
Page integration
Everything beyond the script tag is declarative markup.
Points of interest
Mark anything you want him to visit and comment on. Pipe-separate alternate lines and he'll pick one at random:
<div class="panel" data-den-poi="yip (sign it)|yip yip (write something)">
When he decides to wander, there's a 45% chance he heads for a POI that's currently on screen instead of a random point. He skips whichever one he visited last, so he explores rather than fixating.
Stealable things
Nothing is stealable unless you say so:
<span class="badge" data-den-steal>free gifs</span>
He self-limits to elements between 8–220px wide and 8–90px tall, and only ones currently visible. The original element is hidden and a fixed-position clone follows him, so the page layout doesn't reflow while he's got it. Everything always comes back — there are timeouts on every phase, and clicking him returns the item instantly.
Showing the presence count
<span data-den-others></span>
Filled in on load and refreshed every 60 seconds.
Events
Pages tell him what happened; he decides how to feel about it.
denEvent('celebrate', { text: 'YIP!! (a new signature!!)' });
denEvent('investigate', { el: '#guestbook', text: 'yip? (what is this)' });
denEvent('investigate', { x: 400, y: 300, run: true });
denEvent('alarm', { text: 'YIP?! (INTRUDER)' });
denEvent('mischief');
denEvent('sleepy');
denEvent('zoomies');
celebrate is a happy spin with a heart burst. investigate sends him on an
errand — while on one he ignores your cursor, because he's busy. alarm startles
him into a panicked sprint.
Lower-level helpers, if you want to drive him directly:
denSay('yip!!'); // speech bubble at his current position
denYip(); // the synthesized squeak
denSetName('crumb'); // returns the sanitized name, or null
denGetName();
denOthers; // number, current presence count
A worked example
Redirect after a successful form post with a marker, then:
const q = new URLSearchParams(location.search);
if (q.get('signed') === '1') {
history.replaceState(null, '', location.pathname);
setTimeout(() => denEvent('celebrate', { text: 'YIP!! (for the hoard!!)' }), 3400);
}
The delay lets him finish climbing out of his burrow first.
Secret codes
Typed anywhere outside a text field:
| type | he says |
|---|---|
yip |
yip!! |
owo |
*notices u* |
uwu |
yip (no) |
kobold |
yip!! (that is me) |
shrine |
YIP?! (how do u know about that) |
zoomies |
ten seconds of chaos |
sleep |
bedtime |
| ↑↑↓↓←→←→BA | same as zoomies |
Building
Optional — src/den.js runs fine as-is. Builds are for size and, if you want it,
hiding the surprises above from anyone who opens view-source.
npm install -g terser javascript-obfuscator
./build.fish
| file | size | notes |
|---|---|---|
src/den.js |
33 KB | readable source, the one you edit |
build/den.min.js |
20 KB | minified; recommended |
build/den.obf.js |
88 KB | string-encoded and control-flow flattened |
The obfuscated build hides all his dialogue and secrets from casual reading, at 4x the file size. Nothing stops a determined reader — this is spoiler protection, not security.
build.fish also bumps the ?v=N cache-buster in any .php file that
references a build, because forgetting that will cost you an afternoon
convincing yourself the code is broken when the browser is simply serving you
last week's kobold.
Testing
test/sim.js runs him headless in jsdom with a mocked clock, so you can step
through behaviour that would otherwise take ten minutes of sitting still:
npm install jsdom
node test/sim.js # readable source
node test/sim.js build/den.obf.js # verify a build still works
It drives a full mischief cycle and asserts the stolen element is restored.
Notes
- Respects
prefers-reduced-motion— under it he doesn't spawn at all, and the API becomes no-ops so your page code never errors. - Everything is
position: fixedwithpointer-events: none, so he can't intercept clicks or affect layout. - Petting ignores clicks on links, buttons, and form fields.
- If
den.phpis missing, presence silently no-ops. - He injects
text-align: lefton his own UI, because inheritable properties leak in from host pages. If something of his looks off on a new page, check what the host'sbodyis setting.
License
MIT — see LICENSE.
Built for owo.ing. If you put him on your site, he's yours now. Name him something good.