1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2 as
// published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// Run: node desktop/companion/test_mood.js
// Mood.js is a QML library, so its .pragma line is stripped before node
// evaluates it.
const fs = require("fs");
const assert = require("assert");
const src = fs.readFileSync(__dirname + "/Mood.js", "utf8")
.replace(/^\.pragma library$/m, "");
const M = new Function(src +
"\nreturn { T, click, clickFrame, pick, outfit, mockAllowed, canSpeak, monologue };")();
const NOW = 10000000;
// A snapshot with nothing going on, overridden per case.
const calm = over => Object.assign({
now: NOW, destructAt: 0, dodging: false, shoved: false, shovedAt: 0, recording: false, busy: false,
repliedAt: 0, clicks: { count: 0, at: 0 }, mockAt: 0, typingAt: 0, wakeAt: 0,
monologue: false, idle: 0,
}, over);
const state = over => M.pick(calm(over)).state;
const frame = over => M.pick(calm(over)).frame;
// Nothing going on: the rest pose.
assert.deepStrictEqual(M.pick(calm({})), { state: "base", frame: "base" });
// The priority table, top to bottom: with every trigger from a row down
// active at once, that row wins. Merged so a higher row's values win.
const ladder = [
["destruct", { destructAt: NOW - 10 }],
["scorched", { destructAt: NOW - M.T.destruct - 10 }],
["dodge", { dodging: true }],
["shoved", { shoved: true }],
["listen", { recording: true }],
["think", { busy: true }],
["wave", { repliedAt: NOW - 10 }],
["clicks", { clicks: { count: 2, at: NOW - 10 } }],
["mock", { mockAt: NOW - 10 }],
["watch", { typingAt: NOW - 10 }],
["stretch", { wakeAt: NOW - 10 }],
["muse", { monologue: true }],
["lying", { idle: 3 }],
];
for (let i = 0; i < ladder.length; i++) {
const all = Object.assign({}, ...ladder.slice(i).map(l => l[1]).reverse());
assert.strictEqual(state(all), ladder[i][0], "everything from " + ladder[i][0] + " down");
}
// Shoved: knocked off balance for a moment, then mad beside the balloon
// for as long as it stays.
assert.deepStrictEqual(M.pick(calm({ shoved: true, shovedAt: NOW - 10 })), { state: "shoved", frame: "shoved" });
assert.deepStrictEqual(M.pick(calm({ shoved: true, shovedAt: NOW - M.T.shoveHold })), { state: "shoved", frame: "mad" });
assert.strictEqual(frame({ shoved: true, shovedAt: 0 }), "shoved");
// Idle levels and their frames.
assert.deepStrictEqual(M.pick(calm({ idle: 1 })), { state: "bored", frame: "sleepy" });
assert.deepStrictEqual(M.pick(calm({ idle: 2 })), { state: "yawn", frame: "yawn" });
assert.deepStrictEqual(M.pick(calm({ idle: 3 })), { state: "lying", frame: "lying" });
// Holds end on time.
assert.strictEqual(state({ repliedAt: NOW - M.T.waveHold }), "base");
assert.strictEqual(state({ typingAt: NOW - M.T.typingHold }), "base");
assert.strictEqual(state({ wakeAt: NOW - M.T.wakeHold }), "base");
assert.strictEqual(state({ clicks: { count: 3, at: NOW - M.T.clickHold } }), "base");
assert.strictEqual(state({ destructAt: NOW - M.T.destruct - M.T.scorched }), "base");
// The mock points first, then smirks.
assert.strictEqual(frame({ mockAt: NOW - 10 }), "point");
assert.strictEqual(frame({ mockAt: NOW - M.T.mockHold / 2 - 10 }), "smirk");
assert.strictEqual(state({ mockAt: NOW - M.T.mockHold }), "base");
// A click burst: within burstGap it grows, past it it starts over.
let c = { count: 0, at: 0 };
c = M.click(c, 1000);
assert.deepStrictEqual(c, { count: 1, at: 1000 });
c = M.click(c, 1000 + M.T.burstGap);
assert.strictEqual(c.count, 2);
c = M.click(c, c.at + M.T.burstGap + 1);
assert.strictEqual(c.count, 1);
c = { count: 0, at: 0 };
for (let i = 0; i < 7; i++) c = M.click(c, 5000 + i * 300);
assert.strictEqual(c.count, 7);
// 1 smiles, 2-6 blush harder.
assert.deepStrictEqual([1, 2, 3, 4, 5, 6].map(M.clickFrame),
["smile", "blush1", "blush1", "blush2", "blush2", "blush3"]);
// Outfit: the dress until the flash is over, then the leotard for
// scorched + skirtless, then the dress again.
assert.strictEqual(M.outfit(0, NOW), "dress");
assert.strictEqual(M.outfit(NOW - 10, NOW), "dress");
assert.strictEqual(M.outfit(NOW - M.T.destruct, NOW), "leotard");
const bare = M.T.destruct + M.T.scorched + M.T.skirtless;
assert.strictEqual(M.outfit(NOW - bare + 10, NOW), "leotard");
assert.strictEqual(M.outfit(NOW - bare, NOW), "dress");
// Cooldowns.
assert.strictEqual(M.mockAllowed(0, NOW), true);
assert.strictEqual(M.mockAllowed(NOW - M.T.mockCooldown + 1, NOW), false);
assert.strictEqual(M.mockAllowed(NOW - M.T.mockCooldown, NOW), true);
assert.strictEqual(M.canSpeak(0, NOW), true);
assert.strictEqual(M.canSpeak(NOW - M.T.lineCooldown + 1, NOW), false);
assert.strictEqual(M.canSpeak(NOW - M.T.lineCooldown, NOW), true);
// The monologue: 1 in 4 on boredom, 1 in 3 on being shown, once an hour.
assert.strictEqual(M.monologue("bored", 0, NOW, 0.24), true);
assert.strictEqual(M.monologue("bored", 0, NOW, 0.25), false);
assert.strictEqual(M.monologue("shown", 0, NOW, 0.33), true);
assert.strictEqual(M.monologue("shown", 0, NOW, 0.34), false);
assert.strictEqual(M.monologue("other", 0, NOW, 0), false);
assert.strictEqual(M.monologue("bored", NOW - M.T.monologueCooldown + 1, NOW, 0), false);
assert.strictEqual(M.monologue("bored", NOW - M.T.monologueCooldown, NOW, 0), true);
console.log("ok");
|