@clawlabz/clawskin 1.0.0

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.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * CharacterSprite.js — Manages character rendering in a scene
3
+ * Composites sprite frames from SpriteGenerator and handles positioning
4
+ */
5
+ class CharacterSprite {
6
+ constructor(config, x, y, scale = 3) {
7
+ this.config = config || CharacterSprite.defaultConfig();
8
+ this.x = x;
9
+ this.y = y;
10
+ this.scale = scale;
11
+ this.spriteSheet = null;
12
+ this.animManager = new AnimationManager();
13
+ this.bubbleManager = new BubbleManager();
14
+ this.generator = new SpriteGenerator();
15
+ this.petCanvas = null;
16
+ this.petFrame = 0;
17
+ this.petTimer = 0;
18
+ this.regenerate();
19
+ }
20
+
21
+ static defaultConfig() {
22
+ return {
23
+ skinColor: SpriteGenerator.SKIN_TONES[0],
24
+ hairType: 0,
25
+ hairColor: SpriteGenerator.HAIR_COLORS[0],
26
+ outfitType: 'hoodie',
27
+ outfitColorIdx: 0,
28
+ accessory: null,
29
+ pet: null
30
+ };
31
+ }
32
+
33
+ static randomConfig() {
34
+ const outfitTypes = Object.keys(SpriteGenerator.OUTFIT_COLORS);
35
+ const accessories = [null, 'glasses', 'hat', 'headphones', 'cap'];
36
+ const pets = [null, 'cat', 'dog', 'robot'];
37
+ return {
38
+ skinColor: SpriteGenerator.SKIN_TONES[Math.floor(Math.random() * 5)],
39
+ hairType: Math.floor(Math.random() * 5),
40
+ hairColor: SpriteGenerator.HAIR_COLORS[Math.floor(Math.random() * 7)],
41
+ outfitType: outfitTypes[Math.floor(Math.random() * outfitTypes.length)],
42
+ outfitColorIdx: Math.floor(Math.random() * 5),
43
+ accessory: accessories[Math.floor(Math.random() * accessories.length)],
44
+ pet: pets[Math.floor(Math.random() * pets.length)]
45
+ };
46
+ }
47
+
48
+ regenerate() {
49
+ this.generator.cache.clear();
50
+ this.spriteSheet = this.generator.generateCharacter(this.config);
51
+ }
52
+
53
+ updateConfig(newConfig) {
54
+ this.config = { ...this.config, ...newConfig };
55
+ this.regenerate();
56
+ }
57
+
58
+ setState(state) {
59
+ this.animManager.setState(state);
60
+ }
61
+
62
+ showBubble(text, type, duration) {
63
+ this.bubbleManager.show(text, type, duration);
64
+ }
65
+
66
+ update(dt) {
67
+ this.animManager.update(dt);
68
+ this.bubbleManager.update(dt);
69
+ }
70
+
71
+ render(ctx) {
72
+ if (!this.spriteSheet) return;
73
+
74
+ const frame = this.animManager.getCurrentFrame();
75
+ const sx = frame * 32;
76
+
77
+ ctx.save();
78
+ ctx.imageSmoothingEnabled = false;
79
+
80
+ // Draw character only — pets are now independent entities managed by PetManager
81
+ ctx.drawImage(
82
+ this.spriteSheet,
83
+ sx, 0, 32, 32,
84
+ this.x, this.y, 32 * this.scale, 32 * this.scale
85
+ );
86
+
87
+ ctx.restore();
88
+
89
+ // Draw bubble (not pixel-scaled)
90
+ this.bubbleManager.render(
91
+ ctx,
92
+ this.x + (32 * this.scale) / 2,
93
+ this.y
94
+ );
95
+ }
96
+ }
97
+
98
+ if (typeof window !== 'undefined') window.CharacterSprite = CharacterSprite;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * game.js — ClawSkin main entry point
3
+ * Initializes the pixel engine, scenes, character, and UI
4
+ */
5
+ class ClawSkinGame {
6
+ constructor(canvasId, options = {}) {
7
+ this.canvas = document.getElementById(canvasId);
8
+ this.ctx = this.canvas.getContext('2d');
9
+ this.width = options.width || 480;
10
+ this.height = options.height || 320;
11
+ this.canvas.width = this.width;
12
+ this.canvas.height = this.height;
13
+
14
+ this.spriteGen = new SpriteGenerator();
15
+ this.scenes = [];
16
+ this.currentScene = null;
17
+ this.character = null;
18
+ this.stateSync = null;
19
+ this.lastTime = 0;
20
+ this.running = false;
21
+
22
+ // Status display
23
+ this.statusEl = options.statusEl ? document.getElementById(options.statusEl) : null;
24
+ }
25
+
26
+ init(characterConfig = null) {
27
+ // Load saved config or use provided/default
28
+ const savedConfig = CharacterEditor.loadSaved();
29
+ const cfg = characterConfig || savedConfig || CharacterSprite.defaultConfig();
30
+
31
+ // Create scenes
32
+ this.scenes = [
33
+ new OfficeScene(this.canvas, this.ctx, this.spriteGen),
34
+ new HackerScene(this.canvas, this.ctx, this.spriteGen),
35
+ new CafeScene(this.canvas, this.ctx, this.spriteGen),
36
+ ];
37
+
38
+ // Init all scenes
39
+ this.scenes.forEach(s => s.init());
40
+
41
+ // Set initial scene
42
+ this.currentScene = this.scenes[0];
43
+
44
+ // Create character
45
+ const pos = this.currentScene.getCharacterPosition();
46
+ this.character = new CharacterSprite(cfg, pos.x, pos.y);
47
+
48
+ // State sync (demo mode by default)
49
+ this.stateSync = new AgentStateSync(this.character, {
50
+ gatewayUrl: null, // Will use demo mode
51
+ onStateChange: (state) => {
52
+ if (this.statusEl) {
53
+ this.statusEl.textContent = this.character.animManager.getLabel();
54
+ }
55
+ }
56
+ });
57
+ this.stateSync.connect();
58
+
59
+ return this;
60
+ }
61
+
62
+ setScene(index) {
63
+ if (index < 0 || index >= this.scenes.length) return;
64
+ this.currentScene = this.scenes[index];
65
+ this.currentScene.init();
66
+ const pos = this.currentScene.getCharacterPosition();
67
+ this.character.x = pos.x;
68
+ this.character.y = pos.y;
69
+ }
70
+
71
+ start() {
72
+ this.running = true;
73
+ this.lastTime = performance.now();
74
+ this._loop();
75
+ }
76
+
77
+ stop() {
78
+ this.running = false;
79
+ }
80
+
81
+ _loop() {
82
+ if (!this.running) return;
83
+ const now = performance.now();
84
+ const dt = now - this.lastTime;
85
+ this.lastTime = now;
86
+
87
+ this._update(dt);
88
+ this._render();
89
+
90
+ requestAnimationFrame(() => this._loop());
91
+ }
92
+
93
+ _update(dt) {
94
+ if (this.currentScene) this.currentScene.update(dt);
95
+ if (this.character) this.character.update(dt);
96
+ if (this.stateSync) this.stateSync.update(dt);
97
+
98
+ // Update status display
99
+ if (this.statusEl && this.character) {
100
+ this.statusEl.textContent = this.character.animManager.getLabel();
101
+ }
102
+ }
103
+
104
+ _render() {
105
+ this.ctx.clearRect(0, 0, this.width, this.height);
106
+ this.ctx.imageSmoothingEnabled = false;
107
+
108
+ // Render scene background + furniture
109
+ if (this.currentScene) this.currentScene.render(this.ctx);
110
+
111
+ // Render character on top
112
+ if (this.character) this.character.render(this.ctx);
113
+ }
114
+ }
115
+
116
+ if (typeof window !== 'undefined') window.ClawSkinGame = ClawSkinGame;
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Pet.js — Independent pet entity with autonomous AI behavior
3
+ * Pets move freely around the office, interact with agents and other pets
4
+ */
5
+ class Pet {
6
+ constructor(config) {
7
+ this.id = config.id || Math.random().toString(36).slice(2, 8);
8
+ this.type = config.type || 'cat'; // 'cat' | 'dog' | 'robot' | 'bird' | 'hamster'
9
+ this.moveType = config.moveType || 'walk'; // 'walk' | 'fly' | 'crawl'
10
+ this.color = config.color || null; // body color (null = random)
11
+ this.x = config.x || 100;
12
+ this.y = config.y || 200;
13
+ this.scale = config.scale || 2.5;
14
+ this.speed = config.speed || 0.04;
15
+ this.facingLeft = false;
16
+
17
+ // Pick random color if none given
18
+ if (!this.color) {
19
+ const palette = Pet.COLORS[this.type] || Pet.COLORS.cat;
20
+ this.color = palette[Math.floor(Math.random() * palette.length)];
21
+ }
22
+
23
+ // Animation
24
+ this.frame = 0;
25
+ this.frameTimer = 0;
26
+ this.frameInterval = 600; // ms per frame
27
+
28
+ // AI behavior
29
+ this.state = 'idle'; // 'idle' | 'moving' | 'sleeping' | 'interacting'
30
+ this.target = null; // { x, y }
31
+ this.actionTimer = 0;
32
+ this.nextActionTime = 3000 + Math.random() * 5000;
33
+ this.interactLabel = null; // what we're interacting with
34
+
35
+ // Flying pets: sine wave bob
36
+ this._flyTime = Math.random() * 10000;
37
+ this._flyBaseY = this.y;
38
+ this._stuckTimer = 0;
39
+
40
+ // Sprite generator (shared)
41
+ this.generator = config.generator || new SpriteGenerator();
42
+ }
43
+
44
+ /** Pet type metadata */
45
+ static TYPES = {
46
+ cat: { moveType: 'walk', speed: 0.04, scale: 2.5, sleepChance: 0.20 },
47
+ dog: { moveType: 'walk', speed: 0.05, scale: 2.5, sleepChance: 0.10 },
48
+ robot: { moveType: 'walk', speed: 0.03, scale: 2.5, sleepChance: 0.05 },
49
+ bird: { moveType: 'fly', speed: 0.06, scale: 2.0, sleepChance: 0.10 },
50
+ hamster: { moveType: 'crawl',speed: 0.02, scale: 2.0, sleepChance: 0.30 },
51
+ };
52
+
53
+ /** Color palettes per type */
54
+ static COLORS = {
55
+ cat: ['#FF9900','#333333','#FFFFFF','#C0C0C0','#D2691E','#FFD700','#8B4513'],
56
+ dog: ['#D2956A','#8B4513','#F5DEB3','#FFD700','#333333','#FFFFFF','#A0522D'],
57
+ robot: ['#95A5A6','#3498DB','#E74C3C','#2ECC71','#F39C12','#9B59B6','#1ABC9C'],
58
+ bird: ['#4FC3F7','#E74C3C','#FFD700','#2ECC71','#FF9800','#9C27B0','#F48FB1'],
59
+ hamster: ['#F5DEB3','#D2B48C','#FFFFFF','#FFD700','#C0C0C0','#DEB887','#FFDAB9'],
60
+ };
61
+
62
+ static create(type, generator, sceneBounds, color) {
63
+ const meta = Pet.TYPES[type] || Pet.TYPES.cat;
64
+ return new Pet({
65
+ type,
66
+ moveType: meta.moveType,
67
+ speed: meta.speed,
68
+ scale: meta.scale,
69
+ color: color || null, // null → random in constructor
70
+ x: 50 + Math.random() * ((sceneBounds?.w || 600) - 100),
71
+ y: (sceneBounds?.floorY || 160) + 10 + Math.random() * ((sceneBounds?.h || 400) - (sceneBounds?.floorY || 160) - 40),
72
+ generator,
73
+ });
74
+ }
75
+
76
+ update(dt, agents, otherPets, sceneBounds, obstacles) {
77
+ // Animation frame cycling
78
+ this.frameTimer += dt;
79
+ if (this.frameTimer > this.frameInterval) {
80
+ this.frameTimer = 0;
81
+ this.frame = (this.frame + 1) % 2;
82
+ }
83
+
84
+ // Flying bob
85
+ if (this.moveType === 'fly') {
86
+ this._flyTime += dt;
87
+ }
88
+
89
+ // AI decision timer
90
+ this.actionTimer += dt;
91
+ if (this.actionTimer > this.nextActionTime) {
92
+ this._decideAction(agents, otherPets, sceneBounds);
93
+ this.actionTimer = 0;
94
+ this.nextActionTime = 3000 + Math.random() * 8000;
95
+ }
96
+
97
+ // Movement
98
+ if (this.state === 'moving' && this.target) {
99
+ this._updateMovement(dt, sceneBounds, obstacles);
100
+ }
101
+ }
102
+
103
+ _decideAction(agents, otherPets, bounds) {
104
+ const roll = Math.random();
105
+ const sleepChance = (Pet.TYPES[this.type] || {}).sleepChance || 0.15;
106
+
107
+ if (roll < 0.25 && agents && agents.length > 0) {
108
+ // Go to an agent — rub against their legs
109
+ const agent = agents[Math.floor(Math.random() * agents.length)];
110
+ const charW = 32 * (agent.character?.scale || 2.5);
111
+ this.target = {
112
+ x: agent.character.x + charW * 0.3 + Math.random() * 20,
113
+ y: agent.character.y + 32 * (agent.character?.scale || 2.5) - 10,
114
+ };
115
+ this.state = 'moving';
116
+ this.interactLabel = 'agent';
117
+ } else if (roll < 0.40 && otherPets && otherPets.length > 0) {
118
+ // Go to another pet
119
+ const candidates = otherPets.filter(p => p !== this);
120
+ if (candidates.length > 0) {
121
+ const other = candidates[Math.floor(Math.random() * candidates.length)];
122
+ this.target = { x: other.x + 15, y: other.y };
123
+ this.state = 'moving';
124
+ this.interactLabel = 'pet';
125
+ } else {
126
+ this._randomWalk(bounds);
127
+ }
128
+ } else if (roll < 0.60) {
129
+ // Random walk
130
+ this._randomWalk(bounds);
131
+ } else if (roll < 0.60 + sleepChance) {
132
+ // Sleep
133
+ this.state = 'sleeping';
134
+ this.target = null;
135
+ } else {
136
+ // Idle — stay put
137
+ this.state = 'idle';
138
+ this.target = null;
139
+ }
140
+ }
141
+
142
+ _randomWalk(bounds) {
143
+ const w = bounds?.w || 640;
144
+ const h = bounds?.h || 400;
145
+ const floorY = bounds?.floorY || 160;
146
+ const floorH = h - floorY;
147
+ this.target = {
148
+ x: 30 + Math.random() * (w - 60),
149
+ y: floorY + 5 + Math.random() * (floorH - 30), // roam entire floor area
150
+ };
151
+ this.state = 'moving';
152
+ this.interactLabel = null;
153
+ }
154
+
155
+ _updateMovement(dt, bounds, obstacles) {
156
+ if (!this.target) { this.state = 'idle'; return; }
157
+
158
+ const dx = this.target.x - this.x;
159
+ const dy = this.target.y - this.y;
160
+ const dist = Math.sqrt(dx * dx + dy * dy);
161
+
162
+ if (dist <= 3) {
163
+ this.x = this.target.x;
164
+ this.y = this.target.y;
165
+ this.state = 'idle';
166
+ this.target = null;
167
+ return;
168
+ }
169
+
170
+ // Hamster: occasional speed burst
171
+ let speed = this.speed;
172
+ if (this.type === 'hamster' && Math.random() < 0.01) {
173
+ speed = 0.08; // sprint!
174
+ }
175
+
176
+ // Update facing direction
177
+ if (dx < -1) this.facingLeft = true;
178
+ else if (dx > 1) this.facingLeft = false;
179
+
180
+ let newX = this.x + (dx / dist) * speed * dt;
181
+ let newY = this.y + (dy / dist) * speed * dt;
182
+ const prevX = this.x;
183
+ const prevY = this.y;
184
+
185
+ // ── Collision detection against obstacles ──
186
+ if (obstacles && obstacles.length > 0) {
187
+ const petW = 16 * this.scale;
188
+ const petH = 16 * this.scale;
189
+ // Pet hitbox — small rect at pet center-bottom
190
+ const hw = 10, hh = 8;
191
+ const getHitbox = (px, py) => ({
192
+ x: px + petW / 2 - hw / 2,
193
+ y: py + petH - hh,
194
+ w: hw,
195
+ h: hh,
196
+ });
197
+
198
+ for (const obs of obstacles) {
199
+ const hb = getHitbox(newX, newY);
200
+ const hit = hb.x < obs.x + obs.w && hb.x + hb.w > obs.x
201
+ && hb.y < obs.y + obs.h && hb.y + hb.h > obs.y;
202
+
203
+ if (hit) {
204
+ // Try sliding: X-only or Y-only
205
+ const hbX = getHitbox(newX, this.y);
206
+ const xHit = hbX.x < obs.x + obs.w && hbX.x + hbX.w > obs.x
207
+ && hbX.y < obs.y + obs.h && hbX.y + hbX.h > obs.y;
208
+ const hbY = getHitbox(this.x, newY);
209
+ const yHit = hbY.x < obs.x + obs.w && hbY.x + hbY.w > obs.x
210
+ && hbY.y < obs.y + obs.h && hbY.y + hbY.h > obs.y;
211
+
212
+ if (xHit && yHit) {
213
+ newX = this.x; newY = this.y;
214
+ } else if (xHit) {
215
+ newX = this.x;
216
+ } else if (yHit) {
217
+ newY = this.y;
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ this.x = newX;
224
+ this.y = newY;
225
+
226
+ // Clamp within scene bounds — roam entire floor area
227
+ const w = bounds?.w || 640;
228
+ const bh = bounds?.h || 400;
229
+ const floorY = bounds?.floorY || Math.round(bh * 0.40);
230
+ this.x = Math.max(10, Math.min(w - 30, this.x));
231
+ this.y = Math.max(floorY + 5, Math.min(bh - 20, this.y));
232
+
233
+ // Anti-stuck: if barely moved, give up and pick a new target
234
+ const moved = Math.abs(this.x - prevX) + Math.abs(this.y - prevY);
235
+ if (moved < 0.05) {
236
+ this._stuckTimer += dt;
237
+ if (this._stuckTimer > 1200) {
238
+ this._stuckTimer = 0;
239
+ this.state = 'idle';
240
+ this.target = null;
241
+ }
242
+ } else {
243
+ this._stuckTimer = 0;
244
+ }
245
+ }
246
+
247
+ render(ctx) {
248
+ const petCanvas = this.generator.generatePet(this.type, this.frame, this.color);
249
+ if (!petCanvas) return;
250
+
251
+ const drawW = 16 * this.scale;
252
+ const drawH = 16 * this.scale;
253
+
254
+ // Flying pets bob up and down
255
+ let drawY = this.y;
256
+ if (this.moveType === 'fly') {
257
+ drawY = this.y - 20 + Math.sin(this._flyTime * 0.003) * 8;
258
+ }
259
+
260
+ ctx.save();
261
+ ctx.imageSmoothingEnabled = false;
262
+
263
+ // Flip horizontally if facing right (default sprite faces left)
264
+ if (!this.facingLeft) {
265
+ ctx.translate(this.x + drawW, drawY);
266
+ ctx.scale(-1, 1);
267
+ ctx.drawImage(petCanvas, 0, 0, drawW, drawH);
268
+ } else {
269
+ ctx.drawImage(petCanvas, this.x, drawY, drawW, drawH);
270
+ }
271
+
272
+ // Shadow on ground
273
+ ctx.globalAlpha = 0.15;
274
+ ctx.fillStyle = '#000';
275
+ const shadowY = this.y + drawH - 2;
276
+ const shadowW = drawW * 0.7;
277
+ ctx.beginPath();
278
+ ctx.ellipse(
279
+ this.facingLeft ? this.x + drawW * 0.35 : this.x + drawW * 0.35,
280
+ shadowY,
281
+ shadowW / 2, 3, 0, 0, Math.PI * 2
282
+ );
283
+ ctx.fill();
284
+ ctx.globalAlpha = 1;
285
+
286
+ // Sleeping ZZZ
287
+ if (this.state === 'sleeping') {
288
+ ctx.font = '6px monospace';
289
+ ctx.fillStyle = '#87CEEB';
290
+ ctx.fillText('z', this.x + drawW, drawY - 2);
291
+ ctx.fillText('Z', this.x + drawW + 4, drawY - 8);
292
+ }
293
+
294
+ ctx.restore();
295
+ }
296
+
297
+ /** Y value for depth sorting */
298
+ get sortY() {
299
+ return this.y;
300
+ }
301
+ }
302
+
303
+ if (typeof window !== 'undefined') window.Pet = Pet;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * PetManager.js — Manages all independent pets in the scene
3
+ * Handles creation, update loop, rendering, and configuration persistence
4
+ */
5
+ class PetManager {
6
+ constructor(generator) {
7
+ this.pets = [];
8
+ this.generator = generator;
9
+ this._sceneBounds = { w: 640, h: 400, floorY: 160 };
10
+ }
11
+
12
+ /** Set scene dimensions for pet movement boundaries */
13
+ setSceneBounds(w, h) {
14
+ this._sceneBounds = { w, h, floorY: Math.round(h * 0.40) };
15
+ }
16
+
17
+ /** Add a pet by type, optionally with a specific color */
18
+ addPet(type, color) {
19
+ const pet = Pet.create(type, this.generator, this._sceneBounds, color || null);
20
+ this.pets.push(pet);
21
+ this._saveConfig();
22
+ return pet;
23
+ }
24
+
25
+ /** Change a pet's color */
26
+ setPetColor(id, color) {
27
+ const pet = this.pets.find(p => p.id === id);
28
+ if (pet) {
29
+ pet.color = color;
30
+ this._saveConfig();
31
+ }
32
+ }
33
+
34
+ /** Remove a pet by id */
35
+ removePet(id) {
36
+ this.pets = this.pets.filter(p => p.id !== id);
37
+ this._saveConfig();
38
+ }
39
+
40
+ /** Remove all pets */
41
+ clearPets() {
42
+ this.pets = [];
43
+ this._saveConfig();
44
+ }
45
+
46
+ /** Update all pets */
47
+ update(dt, agents, obstacles) {
48
+ for (const pet of this.pets) {
49
+ const otherPets = this.pets.filter(p => p !== pet);
50
+ pet.update(dt, agents, otherPets, this._sceneBounds, obstacles);
51
+ }
52
+ }
53
+
54
+ /** Render all pets (caller should handle depth sorting if needed) */
55
+ render(ctx) {
56
+ // Sort pets by Y for back-to-front rendering
57
+ const sorted = [...this.pets].sort((a, b) => a.sortY - b.sortY);
58
+ for (const pet of sorted) {
59
+ pet.render(ctx);
60
+ }
61
+ }
62
+
63
+ /** Load pet configuration from localStorage */
64
+ loadConfig() {
65
+ try {
66
+ const data = JSON.parse(localStorage.getItem('clawskin_pets') || '[]');
67
+ if (Array.isArray(data) && data.length > 0) {
68
+ this.pets = [];
69
+ for (const petData of data) {
70
+ if (petData.type && Pet.TYPES[petData.type]) {
71
+ this.addPet(petData.type, petData.color || null);
72
+ }
73
+ }
74
+ }
75
+ } catch {}
76
+ }
77
+
78
+ /** Save pet configuration to localStorage */
79
+ _saveConfig() {
80
+ try {
81
+ const data = this.pets.map(p => ({ type: p.type, color: p.color }));
82
+ localStorage.setItem('clawskin_pets', JSON.stringify(data));
83
+ } catch {}
84
+ }
85
+
86
+ /** Initialize with some default pets if no saved config */
87
+ initDefaults() {
88
+ this.loadConfig();
89
+ if (this.pets.length === 0) {
90
+ // Start with a cat and a dog by default
91
+ this.addPet('cat');
92
+ this.addPet('dog');
93
+ }
94
+ }
95
+
96
+ /** Get all available pet types */
97
+ static getAvailableTypes() {
98
+ return Object.keys(Pet.TYPES);
99
+ }
100
+ }
101
+
102
+ if (typeof window !== 'undefined') window.PetManager = PetManager;