clawmate 1.2.0 → 1.4.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.
@@ -55,7 +55,7 @@ const Character = (() => {
55
55
  ],
56
56
 
57
57
  walk: [
58
- // Frame 0: 왼발
58
+ // Frame 0: 왼쪽 다리 세트 앞으로 크게 벌림, 집게 열림
59
59
  [
60
60
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
61
61
  [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
@@ -70,17 +70,17 @@ const Character = (() => {
70
70
  [0,0,1,2,1,1,1,1,1,1,1,1,2,1,0,0],
71
71
  [0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0],
72
72
  [0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
73
- [0,0,3,3,3,0,3,3,3,3,0,0,3,3,3,0],
74
- [0,3,3,3,0,0,0,3,3,0,0,0,3,3,3,0],
73
+ [0,3,3,0,0,0,3,3,3,3,0,0,0,3,3,0],
74
+ [3,3,0,0,0,0,0,3,3,0,0,0,0,0,3,3],
75
75
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
76
76
  ],
77
- // Frame 1: 오른발
77
+ // Frame 1: 양쪽 다리 모임 (접촉 순간), 집게 반 닫힘
78
78
  [
79
79
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
80
80
  [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
81
- [0,6,0,0,6,0,0,0,0,0,0,6,0,0,6,0],
82
- [0,6,0,0,6,0,0,0,0,0,0,6,0,0,6,0],
83
- [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
81
+ [0,0,6,0,6,0,0,0,0,0,0,6,0,6,0,0],
82
+ [0,0,6,0,6,0,0,0,0,0,0,6,0,6,0,0],
83
+ [0,0,0,6,6,0,0,0,0,0,0,0,6,6,0,0],
84
84
  [0,0,0,6,1,1,0,0,0,0,1,1,6,0,0,0],
85
85
  [0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
86
86
  [0,0,0,1,1,4,5,1,1,4,5,1,1,0,0,0],
@@ -89,11 +89,11 @@ const Character = (() => {
89
89
  [0,0,1,2,1,1,1,1,1,1,1,1,2,1,0,0],
90
90
  [0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0],
91
91
  [0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
92
- [0,0,0,3,3,3,0,3,3,0,3,3,3,0,0,0],
93
- [0,0,0,0,3,3,3,0,0,3,3,3,0,0,0,0],
92
+ [0,0,0,3,3,0,3,3,3,3,0,3,3,0,0,0],
93
+ [0,0,0,3,3,0,0,3,3,0,0,3,3,0,0,0],
94
94
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
95
95
  ],
96
- // Frame 2: 왼발
96
+ // Frame 2: 오른쪽 다리 세트 앞으로 크게 벌림, 집게 닫힘
97
97
  [
98
98
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
99
99
  [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
@@ -108,17 +108,17 @@ const Character = (() => {
108
108
  [0,0,1,2,1,1,1,1,1,1,1,1,2,1,0,0],
109
109
  [0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0],
110
110
  [0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
111
- [0,0,3,3,3,0,3,3,3,3,0,0,3,3,3,0],
112
- [0,3,3,3,0,0,0,3,3,0,0,0,3,3,3,0],
111
+ [0,0,3,3,0,0,3,3,3,3,0,0,3,3,0,0],
112
+ [0,3,3,0,0,0,0,3,3,0,0,0,0,3,3,0],
113
113
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
114
114
  ],
115
- // Frame 3: 오른발
115
+ // Frame 3: 양쪽 다리 모임 (접촉 순간), 집게 반 열림
116
116
  [
117
117
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
118
118
  [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
119
+ [0,6,0,0,6,0,0,0,0,0,0,6,0,0,6,0],
120
+ [0,6,0,0,6,0,0,0,0,0,0,6,0,0,6,0],
119
121
  [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
120
- [0,0,6,6,0,0,0,0,0,0,0,0,6,6,0,0],
121
- [0,0,0,6,6,0,0,0,0,0,0,6,6,0,0,0],
122
122
  [0,0,0,6,1,1,0,0,0,0,1,1,6,0,0,0],
123
123
  [0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
124
124
  [0,0,0,1,1,4,5,1,1,4,5,1,1,0,0,0],
@@ -127,14 +127,14 @@ const Character = (() => {
127
127
  [0,0,1,2,1,1,1,1,1,1,1,1,2,1,0,0],
128
128
  [0,0,0,1,1,1,1,1,1,1,1,1,1,0,0,0],
129
129
  [0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0],
130
- [0,0,0,3,3,3,0,3,3,0,3,3,3,0,0,0],
131
- [0,0,0,0,3,3,3,0,0,3,3,3,0,0,0,0],
130
+ [0,0,0,3,3,0,3,3,3,3,0,3,3,0,0,0],
131
+ [0,0,0,3,3,0,0,3,3,0,0,3,3,0,0,0],
132
132
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
133
133
  ],
134
134
  ],
135
135
 
136
136
  climb: [
137
- // Frame 0: 기어오르기 포즈 1 (옆모습)
137
+ // Frame 0: 기어오르기 윗다리 세트 뻗음, 집게 열림
138
138
  [
139
139
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
140
140
  [0,0,0,0,6,6,0,0,0,0,0,0,0,0,0,0],
@@ -147,13 +147,13 @@ const Character = (() => {
147
147
  [0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0],
148
148
  [0,0,0,1,2,1,1,1,1,0,0,0,0,0,0,0],
149
149
  [0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0],
150
- [0,0,0,3,3,0,3,3,0,0,0,0,0,0,0,0],
151
150
  [0,0,3,3,0,0,0,3,3,0,0,0,0,0,0,0],
151
+ [0,3,3,0,0,0,0,0,3,3,0,0,0,0,0,0],
152
152
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
153
153
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
154
154
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
155
155
  ],
156
- // Frame 1: 기어오르기 포즈 2
156
+ // Frame 1: 기어오르기 다리 모임 (교차 순간)
157
157
  [
158
158
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
159
159
  [0,0,0,0,6,6,0,0,0,0,0,0,0,0,0,0],
@@ -166,12 +166,50 @@ const Character = (() => {
166
166
  [0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0],
167
167
  [0,0,0,1,2,1,1,1,1,0,0,0,0,0,0,0],
168
168
  [0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0],
169
- [0,0,3,3,0,0,0,3,3,0,0,0,0,0,0,0],
169
+ [0,0,0,3,3,0,3,3,0,0,0,0,0,0,0,0],
170
170
  [0,0,0,3,3,0,3,3,0,0,0,0,0,0,0,0],
171
171
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
172
172
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
173
173
  [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
174
174
  ],
175
+ // Frame 2: 기어오르기 — 아랫다리 세트 뻗음, 집게 닫힘
176
+ [
177
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
178
+ [0,0,0,0,6,6,0,0,0,0,0,0,0,0,0,0],
179
+ [0,0,0,0,6,6,0,0,0,0,0,0,0,0,0,0],
180
+ [0,0,0,0,6,6,0,0,0,0,0,0,0,0,0,0],
181
+ [0,0,0,0,6,6,1,1,0,0,0,0,0,0,0,0],
182
+ [0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0],
183
+ [0,0,0,0,1,4,5,1,1,0,0,0,0,0,0,0],
184
+ [0,0,0,0,1,4,5,1,1,1,0,0,0,0,0,0],
185
+ [0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0],
186
+ [0,0,0,1,2,1,1,1,1,0,0,0,0,0,0,0],
187
+ [0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0],
188
+ [0,0,0,3,3,0,3,3,0,0,0,0,0,0,0,0],
189
+ [0,0,3,3,0,0,0,3,3,0,0,0,0,0,0,0],
190
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
191
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
192
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
193
+ ],
194
+ // Frame 3: 기어오르기 — 다리 모임 (교차 순간, 반대쪽)
195
+ [
196
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
197
+ [0,0,0,0,6,6,0,0,0,0,0,0,0,0,0,0],
198
+ [0,0,0,0,6,6,0,0,0,0,0,0,0,0,0,0],
199
+ [0,0,0,0,6,6,0,0,0,0,0,0,0,0,0,0],
200
+ [0,0,0,0,6,6,1,1,0,0,0,0,0,0,0,0],
201
+ [0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0],
202
+ [0,0,0,0,1,4,5,1,1,0,0,0,0,0,0,0],
203
+ [0,0,0,0,1,4,5,1,1,1,0,0,0,0,0,0],
204
+ [0,0,0,1,1,1,1,1,1,1,0,0,0,0,0,0],
205
+ [0,0,0,1,2,1,1,1,1,0,0,0,0,0,0,0],
206
+ [0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0],
207
+ [0,0,3,3,0,0,0,3,3,0,0,0,0,0,0,0],
208
+ [0,0,3,3,0,0,0,3,3,0,0,0,0,0,0,0],
209
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
210
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
211
+ [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
212
+ ],
175
213
  ],
176
214
 
177
215
  sleep: [
@@ -335,10 +373,12 @@ const Character = (() => {
335
373
  jumping: 'excited', // 점프: excited 프레임셋 재활용
336
374
  rappelling: 'climb', // 레펠: climb 프레임셋 재활용
337
375
  falling: 'scared', // 낙하: scared 프레임셋 재활용
376
+ custom: 'walk', // 커스텀 이동: 기본은 walk, 패턴별로 동적 변경 가능
338
377
  };
339
378
 
340
379
  let currentCanvas = null;
341
380
  let currentColorMap = null;
381
+ let originalFrames = null; // 원본 프레임 백업 (리셋용)
342
382
 
343
383
  function createCanvas(container) {
344
384
  const canvas = document.createElement('canvas');
@@ -395,5 +435,62 @@ const Character = (() => {
395
435
  return (FRAMES[frameSet] || FRAMES.idle).length;
396
436
  }
397
437
 
398
- return { createCanvas, setColorMap, renderFrame, getFrameCount, SIZE, FRAMES, STATE_FRAMES };
438
+ /**
439
+ * 커스텀 프레임 데이터 설정 (AI 생성 캐릭터용)
440
+ * 기존 프레임을 백업하고 새 프레임으로 교체
441
+ */
442
+ function setCustomFrames(newFrames) {
443
+ if (!originalFrames) {
444
+ originalFrames = {};
445
+ for (const [key, value] of Object.entries(FRAMES)) {
446
+ originalFrames[key] = JSON.parse(JSON.stringify(value));
447
+ }
448
+ }
449
+ for (const [key, value] of Object.entries(newFrames)) {
450
+ if (Array.isArray(value) && value.length > 0) {
451
+ FRAMES[key] = value;
452
+ }
453
+ }
454
+ }
455
+
456
+ /**
457
+ * 캐릭터 데이터 일괄 설정 (색상 + 프레임)
458
+ * @param {object} data - { colorMap?, frames? }
459
+ */
460
+ function setCharacterData(data) {
461
+ if (data.colorMap) {
462
+ setColorMap(data.colorMap);
463
+ }
464
+ if (data.frames) {
465
+ setCustomFrames(data.frames);
466
+ }
467
+ }
468
+
469
+ /**
470
+ * 원래 캐릭터로 리셋
471
+ */
472
+ function resetCharacter() {
473
+ if (originalFrames) {
474
+ for (const key of Object.keys(FRAMES)) {
475
+ delete FRAMES[key];
476
+ }
477
+ Object.assign(FRAMES, originalFrames);
478
+ originalFrames = null;
479
+ }
480
+ // 기본 색상으로 리셋
481
+ setColorMap({
482
+ primary: '#ff4f40',
483
+ secondary: '#ff775f',
484
+ dark: '#8B4513',
485
+ eye: '#ffffff',
486
+ pupil: '#111111',
487
+ claw: '#ff4f40',
488
+ });
489
+ }
490
+
491
+ return {
492
+ createCanvas, setColorMap, renderFrame, getFrameCount,
493
+ setCustomFrames, setCharacterData, resetCharacter,
494
+ SIZE, FRAMES, STATE_FRAMES,
495
+ };
399
496
  })();
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 마우스/클릭/드래그 상호작용 시스템
3
3
  *
4
- * AI 연결 시: 이벤트를 OpenClaw에 전달 → AI가 반응 결정
4
+ * AI 연결 시: 이벤트를 AI에 전달 → AI가 반응 결정
5
5
  * AI 미연결 시: 자율 반응 (랜덤 FSM)
6
6
  */
7
7
  const Interactions = (() => {
@@ -53,6 +53,8 @@ const Interactions = (() => {
53
53
  clickTimer = setTimeout(() => {
54
54
  if (clickCount >= 3) {
55
55
  onTripleClick();
56
+ } else if (clickCount === 2) {
57
+ onDoubleClick();
56
58
  } else if (clickCount === 1) {
57
59
  onSingleClick();
58
60
  }
@@ -100,18 +102,24 @@ const Interactions = (() => {
100
102
  PetEngine.start();
101
103
  window.clawmate.setClickThrough(true);
102
104
 
103
- // AI에 드래그 이벤트 리포트
105
+ // AI에 드래그 이벤트 리포트 + 반응 기록
104
106
  if (dragStartPos) {
107
+ const draggedAction = StateMachine.getState();
108
+ Memory.recordReaction(draggedAction, 'drag');
105
109
  AIController.reportDrag(dragStartPos, endPos);
106
110
  }
107
111
  }
108
112
 
109
113
  function onSingleClick() {
110
114
  const pos = PetEngine.getPosition();
115
+ const currentAction = StateMachine.getState();
111
116
 
112
117
  // AI에 클릭 이벤트 리포트
113
118
  AIController.reportClick(pos);
114
119
 
120
+ // 유저 반응 기록 — 현재 행동 중 클릭 = 긍정 반응
121
+ Memory.recordReaction(currentAction, 'click');
122
+
115
123
  // AI 연결 시: AI가 반응 결정 (아무것도 안 함, AI 응답 대기)
116
124
  // AI 미연결 시: 자율 반응
117
125
  if (AIController.isAutonomous()) {
@@ -123,7 +131,38 @@ const Interactions = (() => {
123
131
  spawnHeartEffect();
124
132
  }
125
133
 
134
+ function onDoubleClick() {
135
+ const pos = PetEngine.getPosition();
136
+ const currentAction = StateMachine.getState();
137
+
138
+ // 유저 반응 기록
139
+ Memory.recordReaction(currentAction, 'double_click');
140
+
141
+ // AI에 더블클릭 리포트
142
+ if (window.clawmate.reportToAI) {
143
+ window.clawmate.reportToAI('double_click', { position: pos });
144
+ }
145
+
146
+ // 자율 모드: 더블클릭 = 특별 반응 (점프 + 기분좋음)
147
+ if (AIController.isAutonomous()) {
148
+ StateMachine.forceState('excited');
149
+ PetEngine.jumpTo(
150
+ pos.x + (Math.random() - 0.5) * 200,
151
+ Math.max(100, pos.y - 150)
152
+ );
153
+ Speech.show('우와! 더블클릭이다!');
154
+ }
155
+
156
+ Memory.recordClick();
157
+ Memory.recordClick(); // 더블클릭 = 2회 클릭
158
+ spawnHeartEffect();
159
+ spawnStarEffect();
160
+ }
161
+
126
162
  function onTripleClick() {
163
+ const currentAction = StateMachine.getState();
164
+ Memory.recordReaction(currentAction, 'triple_click');
165
+
127
166
  if (typeof ModeManager !== 'undefined') {
128
167
  ModeManager.toggle();
129
168
  }
@@ -139,6 +178,10 @@ const Interactions = (() => {
139
178
  const dist = Math.hypot(e.clientX - (pos.x + 32), e.clientY - (pos.y + 32));
140
179
 
141
180
  if (dist < 100) {
181
+ // 유저 반응 기록 — 커서 접근 = 관심 표현
182
+ const curAction = StateMachine.getState();
183
+ Memory.recordReaction(curAction, 'cursor_near');
184
+
142
185
  // AI에 커서 접근 리포트
143
186
  AIController.reportCursorNear(dist);
144
187
 
@@ -14,8 +14,22 @@ const Memory = (() => {
14
14
  milestones: [],
15
15
  evolutionStage: 0,
16
16
  interactionStreak: 0, // 연속 방문 일수
17
+
18
+ // --- 모션 히스토리 ---
19
+ motionHistory: [], // 최근 100개 상태 전환 기록 [{state, timestamp, duration}]
20
+ motionStats: {}, // 상태별 누적 시간 {idle: 12345, walking: 6789, ...}
21
+
22
+ // --- 유저 반응 저장 ---
23
+ reactionLog: [], // 최근 50개 유저 반응 [{action, reaction, timestamp}]
24
+ favoriteActions: {}, // 행동별 긍정 반응 횟수 {excited: 5, walking: 2, ...}
25
+ dislikedActions: {}, // 행동별 부정 반응 횟수 (무시/이탈)
17
26
  };
18
27
 
28
+ let lastMotionState = null;
29
+ let lastMotionTime = 0;
30
+ const MAX_MOTION_HISTORY = 100;
31
+ const MAX_REACTION_LOG = 50;
32
+
19
33
  let evolutionStages = null;
20
34
 
21
35
  async function init() {
@@ -304,6 +318,95 @@ const Memory = (() => {
304
318
  container.appendChild(acc);
305
319
  }
306
320
 
321
+ // --- 모션 히스토리 기록 ---
322
+
323
+ /**
324
+ * 상태 전환 기록
325
+ * StateMachine에서 상태 변경 시 호출
326
+ */
327
+ function recordMotion(newState) {
328
+ const now = Date.now();
329
+
330
+ // 이전 상태의 지속 시간 계산 → 통계 누적
331
+ if (lastMotionState && lastMotionTime > 0) {
332
+ const duration = now - lastMotionTime;
333
+ if (!data.motionStats[lastMotionState]) data.motionStats[lastMotionState] = 0;
334
+ data.motionStats[lastMotionState] += duration;
335
+ }
336
+
337
+ // 히스토리에 추가
338
+ data.motionHistory.push({
339
+ state: newState,
340
+ timestamp: now,
341
+ from: lastMotionState || 'init',
342
+ });
343
+
344
+ // 최대 크기 초과 시 오래된 것 제거
345
+ if (data.motionHistory.length > MAX_MOTION_HISTORY) {
346
+ data.motionHistory = data.motionHistory.slice(-MAX_MOTION_HISTORY);
347
+ }
348
+
349
+ lastMotionState = newState;
350
+ lastMotionTime = now;
351
+
352
+ // 10회 전환마다 자동 저장
353
+ if (data.motionHistory.length % 10 === 0) save();
354
+ }
355
+
356
+ /**
357
+ * 유저 반응 기록
358
+ * 특정 행동 중 사용자가 클릭/드래그 등의 반응을 보인 경우
359
+ *
360
+ * @param {string} action - 펫이 하고 있던 행동
361
+ * @param {string} reaction - 'click' | 'drag' | 'cursor_near' | 'triple_click' | 'double_click'
362
+ */
363
+ function recordReaction(action, reaction) {
364
+ const now = Date.now();
365
+
366
+ // 반응 로그 추가
367
+ data.reactionLog.push({ action, reaction, timestamp: now });
368
+ if (data.reactionLog.length > MAX_REACTION_LOG) {
369
+ data.reactionLog = data.reactionLog.slice(-MAX_REACTION_LOG);
370
+ }
371
+
372
+ // 클릭/더블클릭은 긍정 반응으로 분류
373
+ if (reaction === 'click' || reaction === 'double_click' || reaction === 'cursor_near') {
374
+ if (!data.favoriteActions[action]) data.favoriteActions[action] = 0;
375
+ data.favoriteActions[action]++;
376
+ }
377
+ // 드래그(잡아서 옮김)는 약간 부정 반응
378
+ if (reaction === 'drag') {
379
+ if (!data.dislikedActions[action]) data.dislikedActions[action] = 0;
380
+ data.dislikedActions[action]++;
381
+ }
382
+
383
+ save();
384
+ }
385
+
386
+ /**
387
+ * 사용자 선호 행동 Top N 반환
388
+ * AI가 행동 결정 시 참고
389
+ */
390
+ function getFavoriteActions(topN = 5) {
391
+ const entries = Object.entries(data.favoriteActions || {});
392
+ entries.sort((a, b) => b[1] - a[1]);
393
+ return entries.slice(0, topN).map(([action, count]) => ({ action, count }));
394
+ }
395
+
396
+ /**
397
+ * 최근 모션 히스토리 반환
398
+ */
399
+ function getMotionHistory(limit = 20) {
400
+ return (data.motionHistory || []).slice(-limit);
401
+ }
402
+
403
+ /**
404
+ * 상태별 누적 시간 반환
405
+ */
406
+ function getMotionStats() {
407
+ return { ...(data.motionStats || {}) };
408
+ }
409
+
307
410
  async function save() {
308
411
  try {
309
412
  await window.clawmate.saveMemory(data);
@@ -318,5 +421,9 @@ const Memory = (() => {
318
421
  return data.evolutionStage;
319
422
  }
320
423
 
321
- return { init, recordClick, getData, getEvolutionStage, save };
424
+ return {
425
+ init, recordClick, getData, getEvolutionStage, save,
426
+ recordMotion, recordReaction, getFavoriteActions,
427
+ getMotionHistory, getMotionStats,
428
+ };
322
429
  })();