clawmate 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +441 -442
- package/main/ai-bridge.js +59 -59
- package/main/ai-connector.js +60 -60
- package/main/autostart.js +6 -6
- package/main/desktop-path.js +4 -4
- package/main/file-command-parser.js +46 -46
- package/main/file-ops.js +27 -27
- package/main/index.js +17 -17
- package/main/ipc-handlers.js +24 -24
- package/main/manifest.js +2 -2
- package/main/platform.js +16 -16
- package/main/smart-file-ops.js +64 -64
- package/main/store.js +1 -1
- package/main/telegram.js +137 -137
- package/main/tray.js +61 -61
- package/main/updater.js +13 -13
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/preload/preload.js +18 -18
- package/renderer/css/effects.css +6 -6
- package/renderer/css/pet.css +8 -8
- package/renderer/css/speech.css +5 -5
- package/renderer/first-run.html +14 -14
- package/renderer/index.html +4 -4
- package/renderer/js/ai-controller.js +91 -91
- package/renderer/js/app.js +24 -24
- package/renderer/js/browser-watcher.js +32 -32
- package/renderer/js/character.js +33 -33
- package/renderer/js/interactions.js +21 -21
- package/renderer/js/memory.js +60 -60
- package/renderer/js/metrics.js +141 -141
- package/renderer/js/mode-manager.js +13 -13
- package/renderer/js/pet-engine.js +236 -236
- package/renderer/js/speech.js +19 -19
- package/renderer/js/state-machine.js +23 -23
- package/renderer/js/time-aware.js +15 -15
- package/renderer/launcher.html +8 -8
- package/shared/constants.js +11 -11
- package/shared/messages.js +130 -130
- package/shared/personalities.js +44 -44
- package/skills/launch-pet/index.js +57 -47
- package/skills/launch-pet/skill.json +12 -23
package/renderer/js/speech.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* -
|
|
4
|
-
* -
|
|
5
|
-
* -
|
|
2
|
+
* Speech bubble system
|
|
3
|
+
* - Typewriter effect (30ms/char)
|
|
4
|
+
* - Hold for 5s -> fade out
|
|
5
|
+
* - Mode-based styling: Pet (rounded, red border) / Incarnation (angular, teal glow)
|
|
6
6
|
*/
|
|
7
7
|
const Speech = (() => {
|
|
8
8
|
const CHAR_DELAY = 30;
|
|
@@ -19,18 +19,18 @@ const Speech = (() => {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
* - bottom(
|
|
24
|
-
* - left(
|
|
25
|
-
* - right(
|
|
26
|
-
* - top(
|
|
27
|
-
* -
|
|
22
|
+
* Calculate speech bubble coordinates based on character position (edge)
|
|
23
|
+
* - bottom (floor): above character
|
|
24
|
+
* - left (left wall): to character's right (head side)
|
|
25
|
+
* - right (right wall): to character's left (head side)
|
|
26
|
+
* - top (ceiling): below character
|
|
27
|
+
* - jumping/falling/rappelling: above character (default)
|
|
28
28
|
*/
|
|
29
29
|
function getBubblePosition(pos) {
|
|
30
30
|
const edge = pos.edge;
|
|
31
31
|
const mode = pos.movementMode;
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// Always show above when in mid-air
|
|
34
34
|
if (mode === 'jumping' || mode === 'falling' || mode === 'rappelling') {
|
|
35
35
|
return { left: pos.x - 30, top: pos.y - 60 };
|
|
36
36
|
}
|
|
@@ -49,10 +49,10 @@ const Speech = (() => {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
52
|
+
* Clamp coordinates to prevent speech bubble from going off-screen
|
|
53
53
|
*/
|
|
54
54
|
function clampBubblePosition(left, top) {
|
|
55
|
-
const maxW = window.innerWidth - 200; //
|
|
55
|
+
const maxW = window.innerWidth - 200; // Account for approximate bubble width
|
|
56
56
|
const maxH = window.innerHeight - 50;
|
|
57
57
|
return {
|
|
58
58
|
left: Math.max(5, Math.min(left, maxW)),
|
|
@@ -61,7 +61,7 @@ const Speech = (() => {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
function show(text) {
|
|
64
|
-
hide(); //
|
|
64
|
+
hide(); // Remove existing bubble
|
|
65
65
|
|
|
66
66
|
const container = document.getElementById('speech-container');
|
|
67
67
|
const pos = PetEngine.getPosition();
|
|
@@ -69,7 +69,7 @@ const Speech = (() => {
|
|
|
69
69
|
const bubble = document.createElement('div');
|
|
70
70
|
bubble.className = `speech-bubble speech-${mode}`;
|
|
71
71
|
|
|
72
|
-
//
|
|
72
|
+
// Calculate bubble position per edge + off-screen prevention clamp
|
|
73
73
|
const rawPos = getBubblePosition(pos);
|
|
74
74
|
const clamped = clampBubblePosition(rawPos.left, rawPos.top);
|
|
75
75
|
bubble.style.left = clamped.left + 'px';
|
|
@@ -82,7 +82,7 @@ const Speech = (() => {
|
|
|
82
82
|
container.appendChild(bubble);
|
|
83
83
|
currentBubble = bubble;
|
|
84
84
|
|
|
85
|
-
//
|
|
85
|
+
// Typewriter effect
|
|
86
86
|
let charIndex = 0;
|
|
87
87
|
typeTimer = setInterval(() => {
|
|
88
88
|
if (charIndex < text.length) {
|
|
@@ -94,7 +94,7 @@ const Speech = (() => {
|
|
|
94
94
|
}
|
|
95
95
|
}, CHAR_DELAY);
|
|
96
96
|
|
|
97
|
-
//
|
|
97
|
+
// Auto-hide
|
|
98
98
|
const totalTypeTime = text.length * CHAR_DELAY;
|
|
99
99
|
hideTimer = setTimeout(() => {
|
|
100
100
|
if (currentBubble) {
|
|
@@ -116,14 +116,14 @@ const Speech = (() => {
|
|
|
116
116
|
function updatePosition() {
|
|
117
117
|
if (!currentBubble) return;
|
|
118
118
|
const pos = PetEngine.getPosition();
|
|
119
|
-
//
|
|
119
|
+
// Recalculate bubble position per edge + off-screen prevention
|
|
120
120
|
const rawPos = getBubblePosition(pos);
|
|
121
121
|
const clamped = clampBubblePosition(rawPos.left, rawPos.top);
|
|
122
122
|
currentBubble.style.left = clamped.left + 'px';
|
|
123
123
|
currentBubble.style.top = clamped.top + 'px';
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
// ---
|
|
126
|
+
// --- Message selection helpers ---
|
|
127
127
|
function randomFrom(arr) {
|
|
128
128
|
return arr[Math.floor(Math.random() * arr.length)];
|
|
129
129
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Pet behavior finite state machine (FSM)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* IDLE
|
|
6
|
-
* IDLE
|
|
7
|
-
* IDLE
|
|
8
|
-
*
|
|
4
|
+
* State transition diagram:
|
|
5
|
+
* IDLE -> WALKING -> CLIMBING_UP -> CEILING_WALK
|
|
6
|
+
* IDLE <- PLAYING <- CLIMBING_DOWN <- (ceiling/wall)
|
|
7
|
+
* IDLE -> SLEEPING (23:00~06:00)
|
|
8
|
+
* Interrupts: click->INTERACTING, cursor->SCARED/EXCITED
|
|
9
9
|
*/
|
|
10
10
|
const StateMachine = (() => {
|
|
11
11
|
const STATES = {
|
|
@@ -20,13 +20,13 @@ const StateMachine = (() => {
|
|
|
20
20
|
INTERACTING: 'interacting',
|
|
21
21
|
SCARED: 'scared',
|
|
22
22
|
EXCITED: 'excited',
|
|
23
|
-
JUMPING: 'jumping', //
|
|
24
|
-
RAPPELLING: 'rappelling', //
|
|
25
|
-
FALLING: 'falling', //
|
|
26
|
-
CUSTOM: 'custom', //
|
|
23
|
+
JUMPING: 'jumping', // Parabolic jump in progress (controlled by physics engine)
|
|
24
|
+
RAPPELLING: 'rappelling', // Descending on thread
|
|
25
|
+
FALLING: 'falling', // Free fall under gravity
|
|
26
|
+
CUSTOM: 'custom', // Custom movement pattern in progress (Movement Registry)
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
//
|
|
29
|
+
// Min/max duration per state (ms)
|
|
30
30
|
const DURATIONS = {
|
|
31
31
|
[STATES.IDLE]: { min: 2000, max: 5000 },
|
|
32
32
|
[STATES.WALKING]: { min: 3000, max: 8000 },
|
|
@@ -39,10 +39,10 @@ const StateMachine = (() => {
|
|
|
39
39
|
[STATES.INTERACTING]: { min: 1500, max: 3000 },
|
|
40
40
|
[STATES.SCARED]: { min: 1000, max: 2000 },
|
|
41
41
|
[STATES.EXCITED]: { min: 1500, max: 3000 },
|
|
42
|
-
[STATES.JUMPING]: { min: 500, max: 2000 }, //
|
|
43
|
-
[STATES.RAPPELLING]: { min: 2000, max: 8000 }, //
|
|
44
|
-
[STATES.FALLING]: { min: 200, max: 1000 }, //
|
|
45
|
-
[STATES.CUSTOM]: { min: 500, max: 30000 }, //
|
|
42
|
+
[STATES.JUMPING]: { min: 500, max: 2000 }, // Jump flight time
|
|
43
|
+
[STATES.RAPPELLING]: { min: 2000, max: 8000 }, // Rappel descent time
|
|
44
|
+
[STATES.FALLING]: { min: 200, max: 1000 }, // Fall time
|
|
45
|
+
[STATES.CUSTOM]: { min: 500, max: 30000 }, // Custom movement (varies by pattern)
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
let currentState = STATES.IDLE;
|
|
@@ -51,7 +51,7 @@ const StateMachine = (() => {
|
|
|
51
51
|
let personality = null;
|
|
52
52
|
let onStateChange = null;
|
|
53
53
|
|
|
54
|
-
//
|
|
54
|
+
// Base transition probabilities (adjusted by personality)
|
|
55
55
|
const BASE_TRANSITIONS = {
|
|
56
56
|
[STATES.IDLE]: [
|
|
57
57
|
{ state: STATES.WALKING, weight: 0.5 },
|
|
@@ -63,7 +63,7 @@ const StateMachine = (() => {
|
|
|
63
63
|
{ state: STATES.CLIMBING_UP, weight: 0.2 },
|
|
64
64
|
{ state: STATES.WALKING, weight: 0.25 },
|
|
65
65
|
{ state: STATES.PLAYING, weight: 0.2 },
|
|
66
|
-
{ state: STATES.JUMPING, weight: 0.05 }, //
|
|
66
|
+
{ state: STATES.JUMPING, weight: 0.05 }, // Occasional jump (low probability)
|
|
67
67
|
],
|
|
68
68
|
[STATES.CLIMBING_UP]: [
|
|
69
69
|
{ state: STATES.CEILING_WALK, weight: 0.5 },
|
|
@@ -72,7 +72,7 @@ const StateMachine = (() => {
|
|
|
72
72
|
[STATES.CEILING_WALK]: [
|
|
73
73
|
{ state: STATES.CLIMBING_DOWN, weight: 0.4 },
|
|
74
74
|
{ state: STATES.CEILING_WALK, weight: 0.4 },
|
|
75
|
-
{ state: STATES.RAPPELLING, weight: 0.2 }, //
|
|
75
|
+
{ state: STATES.RAPPELLING, weight: 0.2 }, // Rappel descent from ceiling
|
|
76
76
|
],
|
|
77
77
|
[STATES.CLIMBING_DOWN]: [
|
|
78
78
|
{ state: STATES.WALKING, weight: 0.5 },
|
|
@@ -103,21 +103,21 @@ const StateMachine = (() => {
|
|
|
103
103
|
{ state: STATES.IDLE, weight: 0.5 },
|
|
104
104
|
{ state: STATES.WALKING, weight: 0.5 },
|
|
105
105
|
],
|
|
106
|
-
//
|
|
106
|
+
// After jump: idle or walking on landing
|
|
107
107
|
[STATES.JUMPING]: [
|
|
108
108
|
{ state: STATES.IDLE, weight: 0.5 },
|
|
109
109
|
{ state: STATES.WALKING, weight: 0.5 },
|
|
110
110
|
],
|
|
111
|
-
//
|
|
111
|
+
// After rappel: fall or land
|
|
112
112
|
[STATES.RAPPELLING]: [
|
|
113
113
|
{ state: STATES.FALLING, weight: 0.3 },
|
|
114
114
|
{ state: STATES.IDLE, weight: 0.7 },
|
|
115
115
|
],
|
|
116
|
-
//
|
|
116
|
+
// After fall: idle on landing
|
|
117
117
|
[STATES.FALLING]: [
|
|
118
118
|
{ state: STATES.IDLE, weight: 1.0 },
|
|
119
119
|
],
|
|
120
|
-
//
|
|
120
|
+
// After custom movement: idle or walking
|
|
121
121
|
[STATES.CUSTOM]: [
|
|
122
122
|
{ state: STATES.IDLE, weight: 0.6 },
|
|
123
123
|
{ state: STATES.WALKING, weight: 0.4 },
|
|
@@ -153,7 +153,7 @@ const StateMachine = (() => {
|
|
|
153
153
|
if (forceState) {
|
|
154
154
|
currentState = forceState;
|
|
155
155
|
} else {
|
|
156
|
-
//
|
|
156
|
+
// Sleep time check (23:00~06:00)
|
|
157
157
|
const hour = new Date().getHours();
|
|
158
158
|
if (hour >= 23 || hour < 6) {
|
|
159
159
|
if (currentState !== STATES.SLEEPING && Math.random() < 0.3) {
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* -
|
|
4
|
-
* -
|
|
5
|
-
* -
|
|
2
|
+
* Time-based behavior change system
|
|
3
|
+
* - Morning greetings, lunch alerts, sleeping at night
|
|
4
|
+
* - Random idle chatter
|
|
5
|
+
* - Random tip messages
|
|
6
6
|
*/
|
|
7
7
|
const TimeAware = (() => {
|
|
8
8
|
let lastGreetingHour = -1;
|
|
9
9
|
let lastChatterTime = 0;
|
|
10
10
|
let lastTipTime = 0;
|
|
11
|
-
const CHATTER_COOLDOWN = 60000; // 1
|
|
12
|
-
const TIP_COOLDOWN = 5 * 60000; // 5
|
|
11
|
+
const CHATTER_COOLDOWN = 60000; // 1 minute minimum interval
|
|
12
|
+
const TIP_COOLDOWN = 5 * 60000; // 5 minute minimum interval
|
|
13
13
|
let chatterChance = 0.15;
|
|
14
14
|
|
|
15
15
|
function init() {
|
|
16
|
-
//
|
|
16
|
+
// Greet on start
|
|
17
17
|
showTimeGreeting();
|
|
18
|
-
//
|
|
19
|
-
setInterval(tick, 30000); // 30
|
|
18
|
+
// Periodic check
|
|
19
|
+
setInterval(tick, 30000); // Every 30 seconds
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function setChatterChance(chance) {
|
|
@@ -27,13 +27,13 @@ const TimeAware = (() => {
|
|
|
27
27
|
const hour = new Date().getHours();
|
|
28
28
|
const now = Date.now();
|
|
29
29
|
|
|
30
|
-
//
|
|
30
|
+
// Detect time period change -> greet
|
|
31
31
|
if (hour !== lastGreetingHour && [6, 12, 18, 23].includes(hour)) {
|
|
32
32
|
showTimeGreeting();
|
|
33
33
|
lastGreetingHour = hour;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// Sleep time check (23:00~06:00)
|
|
37
37
|
if (hour >= 23 || hour < 6) {
|
|
38
38
|
const state = StateMachine.getState();
|
|
39
39
|
if (state !== 'sleeping' && state !== 'interacting') {
|
|
@@ -44,7 +44,7 @@ const TimeAware = (() => {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
//
|
|
47
|
+
// Idle chatter when in idle state
|
|
48
48
|
const state = StateMachine.getState();
|
|
49
49
|
if (state === 'idle' && now - lastChatterTime > CHATTER_COOLDOWN) {
|
|
50
50
|
if (Math.random() < chatterChance) {
|
|
@@ -57,7 +57,7 @@ const TimeAware = (() => {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
//
|
|
60
|
+
// Tip messages (less frequently)
|
|
61
61
|
if (now - lastTipTime > TIP_COOLDOWN && Math.random() < 0.05) {
|
|
62
62
|
const tip = Speech.getTipMessage();
|
|
63
63
|
if (tip) {
|
|
@@ -75,7 +75,7 @@ const TimeAware = (() => {
|
|
|
75
75
|
|
|
76
76
|
function showSleepEffect() {
|
|
77
77
|
const pet = document.getElementById('pet-container');
|
|
78
|
-
// Z-z-z
|
|
78
|
+
// Add Z-z-z sleep effect
|
|
79
79
|
for (let i = 0; i < 3; i++) {
|
|
80
80
|
const z = document.createElement('div');
|
|
81
81
|
z.className = 'sleep-z';
|
|
@@ -83,7 +83,7 @@ const TimeAware = (() => {
|
|
|
83
83
|
z.style.animationDelay = (i * 0.5) + 's';
|
|
84
84
|
pet.appendChild(z);
|
|
85
85
|
}
|
|
86
|
-
// 5
|
|
86
|
+
// Remove after 5 seconds
|
|
87
87
|
setTimeout(() => {
|
|
88
88
|
pet.querySelectorAll('.sleep-z').forEach(el => el.remove());
|
|
89
89
|
}, 5000);
|
package/renderer/launcher.html
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
2
|
+
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<title>ClawMate Launcher</title>
|
|
@@ -12,27 +12,27 @@
|
|
|
12
12
|
<span class="claw-icon">\u{1F99E}</span>
|
|
13
13
|
</div>
|
|
14
14
|
<h1>ClawMate</h1>
|
|
15
|
-
<p class="subtitle"
|
|
15
|
+
<p class="subtitle">A living Claw on your desktop</p>
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
18
|
<div class="launcher-body">
|
|
19
19
|
<div class="mode-card" id="mode-pet" onclick="selectMode('pet')">
|
|
20
20
|
<div class="mode-icon">\u{1F990}</div>
|
|
21
21
|
<h2>Clawby</h2>
|
|
22
|
-
<p
|
|
23
|
-
<span class="mode-desc"
|
|
22
|
+
<p>Cute companion</p>
|
|
23
|
+
<span class="mode-desc">A playful little lobster roaming your desktop!</span>
|
|
24
24
|
</div>
|
|
25
25
|
|
|
26
26
|
<div class="mode-card" id="mode-incarnation" onclick="selectMode('incarnation')">
|
|
27
27
|
<div class="mode-icon">\u{1F980}</div>
|
|
28
28
|
<h2>Claw</h2>
|
|
29
|
-
<p
|
|
30
|
-
<span class="mode-desc"
|
|
29
|
+
<p>An entity given physical form</p>
|
|
30
|
+
<span class="mode-desc">The calm and bold Claw descends upon your desktop.</span>
|
|
31
31
|
</div>
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
34
|
<button class="start-btn" id="start-btn" disabled onclick="startPet()">
|
|
35
|
-
|
|
35
|
+
Select a mode
|
|
36
36
|
</button>
|
|
37
37
|
</div>
|
|
38
38
|
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
document.getElementById('mode-' + mode).classList.add('selected');
|
|
46
46
|
const btn = document.getElementById('start-btn');
|
|
47
47
|
btn.disabled = false;
|
|
48
|
-
btn.textContent = mode === 'pet' ? 'Clawby
|
|
48
|
+
btn.textContent = mode === 'pet' ? 'Start Clawby!' : 'Awaken Claw!';
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
async function startPet() {
|
package/shared/constants.js
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Character size (16x16 pixels -> 4x scale)
|
|
2
2
|
const PIXEL_SIZE = 4;
|
|
3
3
|
const GRID_SIZE = 16;
|
|
4
4
|
const CHAR_SIZE = PIXEL_SIZE * GRID_SIZE; // 64px
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// Movement speed (px/frame)
|
|
7
7
|
const BASE_SPEED = 1.5;
|
|
8
8
|
const CLIMB_SPEED = 1.0;
|
|
9
9
|
|
|
10
|
-
//
|
|
10
|
+
// Speech bubble
|
|
11
11
|
const SPEECH_CHAR_DELAY = 30; // ms per character
|
|
12
|
-
const SPEECH_DISPLAY_TIME = 5000; // ms
|
|
13
|
-
const SPEECH_FADE_TIME = 500; // ms
|
|
12
|
+
const SPEECH_DISPLAY_TIME = 5000; // ms display duration
|
|
13
|
+
const SPEECH_FADE_TIME = 500; // ms fade out
|
|
14
14
|
|
|
15
|
-
//
|
|
15
|
+
// File operation safeguards
|
|
16
16
|
const MAX_FILES_PER_SESSION = 3;
|
|
17
|
-
const FILE_MOVE_COOLDOWN = 5 * 60 * 1000; // 5
|
|
17
|
+
const FILE_MOVE_COOLDOWN = 5 * 60 * 1000; // 5 minutes
|
|
18
18
|
const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
|
|
19
19
|
const EXCLUDED_EXTENSIONS = ['.exe', '.dll', '.sys', '.lnk', '.ini', '.bat', '.cmd', '.ps1'];
|
|
20
20
|
|
|
21
|
-
// FSM
|
|
21
|
+
// FSM states
|
|
22
22
|
const STATES = {
|
|
23
23
|
IDLE: 'idle',
|
|
24
24
|
WALKING: 'walking',
|
|
@@ -33,7 +33,7 @@ const STATES = {
|
|
|
33
33
|
EXCITED: 'excited',
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// Screen edges
|
|
37
37
|
const EDGES = {
|
|
38
38
|
BOTTOM: 'bottom',
|
|
39
39
|
LEFT: 'left',
|
|
@@ -41,13 +41,13 @@ const EDGES = {
|
|
|
41
41
|
TOP: 'top',
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
//
|
|
44
|
+
// Directions
|
|
45
45
|
const DIRECTIONS = {
|
|
46
46
|
LEFT: -1,
|
|
47
47
|
RIGHT: 1,
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
//
|
|
50
|
+
// Color palette
|
|
51
51
|
const COLORS = {
|
|
52
52
|
pet: {
|
|
53
53
|
primary: '#ff4f40',
|