252 lines
8.1 KiB
Markdown
252 lines
8.1 KiB
Markdown
# 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.
|
||
|
||
```html
|
||
<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, treats, and his name persist in
|
||
`localStorage`. Return after a week and he says so. Reach ten points of trust
|
||
(pets + treats) and he decides you're friends; at twenty-five he lingers near
|
||
your cursor instead of wandering off, and his stealing turns into gift-giving.
|
||
|
||
**Drift.** What he mutters while wandering depends on how you've treated him. Feed
|
||
him and he's content. Visit repeatedly without ever touching him and he starts
|
||
asking whether anyone's there.
|
||
|
||
**A shell.** A `>_ den` tab sits in the corner of every page. Open it and you can
|
||
`come`, `sit`, `pet`, `treat`, `fetch`, `rain`, `snow`, `sleep`, `where`,
|
||
`status`, or `name` him.
|
||
|
||
**Weather.** `denWeather('rain')` and he bolts for the nearest POI to wait it out,
|
||
sulking under cover until it stops. `denWeather('snow')` and he loses his mind —
|
||
a burst of speed, and his prints linger three times as long in it.
|
||
|
||
**Treats.** Drop one and he sprints for it. Treats build trust the same way pets
|
||
do, and he gets visibly hungry if he hasn't been fed in a while.
|
||
|
||
**Scrolling.** He's standing on the page, not floating above it — scroll and he
|
||
gets carried along, has to scramble back, and arrives out of breath.
|
||
|
||
**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:
|
||
|
||
```sh
|
||
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:
|
||
|
||
```html
|
||
<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:
|
||
|
||
```html
|
||
<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
|
||
|
||
```html
|
||
<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.
|
||
|
||
```js
|
||
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');
|
||
denEvent('rain'); // also 'snow', 'clear'
|
||
denEvent('treat', { x: 400, y: 300 }); // or { el: '#bowl' }
|
||
```
|
||
|
||
`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:
|
||
|
||
```js
|
||
denSay('yip!!'); // speech bubble at his current position
|
||
denYip(); // the synthesized squeak
|
||
denSetName('crumb'); // returns the sanitized name, or null
|
||
denGetName();
|
||
denWeather('snow'); // 'rain' | 'snow' | 'clear'
|
||
denTreat(x, y); // drop a treat at a point
|
||
denOthers; // number, current presence count
|
||
```
|
||
|
||
### A worked example
|
||
|
||
Redirect after a successful form post with a marker, then:
|
||
|
||
```js
|
||
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.
|
||
|
||
```sh
|
||
npm install -g terser javascript-obfuscator
|
||
./build.fish
|
||
```
|
||
|
||
| file | size | notes |
|
||
| --- | --- | --- |
|
||
| `src/den.js` | 42 KB | readable source, the one you edit |
|
||
| `build/den.min.js` | 25 KB | minified; recommended |
|
||
| `build/den.obf.js` | 112 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:
|
||
|
||
```sh
|
||
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: fixed` with `pointer-events: none`, so he can't
|
||
intercept clicks or affect layout.
|
||
- Petting ignores clicks on links, buttons, and form fields.
|
||
- If `den.php` is missing, presence silently no-ops.
|
||
- He injects `text-align: left` on 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's `body` is setting.
|
||
|
||
---
|
||
|
||
## License
|
||
|
||
MIT — see [LICENSE](LICENSE).
|
||
|
||
Built for [owo.ing](https://owo.ing). If you put him on your site, he's yours
|
||
now. Name him something good.
|