tycono 0.1.94-beta.4 → 0.1.94-beta.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tycono",
3
- "version": "0.1.94-beta.4",
3
+ "version": "0.1.94-beta.6",
4
4
  "description": "Build an AI company. Watch them work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -743,8 +743,13 @@ function buildSupervisionSection(node: OrgNode): string {
743
743
  - ✅ **Peer consult**: Unsure about business/market direction? → \`python3 "$CONSULT_CMD" cbo "question"\`
744
744
  - ⚠️ **Course correct**: Wrong direction → \`python3 "$SUPERVISION_CMD" amend <ses-id> "new instruction"\`
745
745
  - 🛑 **Abort**: Seriously wrong → \`python3 "$SUPERVISION_CMD" abort <ses-id> --reason "why"\`
746
- - ✅ **All done**: Compile results and report to your superior
746
+ - ✅ **All done?** Before reporting done, **verify deliverables** (see Quality Gate below)
747
747
  4. **Repeat** watch until all subordinates complete. Do NOT stop after one tick.
748
+ 5. **Quality Gate**: When subordinates report done, **read their actual output**:
749
+ - Check files exist and are non-trivial
750
+ - Verify key requirements from your task are met
751
+ - If gaps found → re-dispatch with specific feedback: "Missing X, Y, Z. Continue."
752
+ - There is NO time limit. Iterate until the work truly meets the requirements.
748
753
 
749
754
  ## Supervision Commands
750
755
 
@@ -371,10 +371,9 @@ elif cmd == 'amend':
371
371
  log('Usage: supervision amend <sessionId> "<instruction>"')
372
372
  sys.exit(1)
373
373
 
374
- # Amend uses continue-session with amended context
374
+ # Amend sends a message to the session with amendment instructions
375
375
  body = json.dumps({
376
- 'response': f'[SUPERVISION AMENDMENT] {instruction}',
377
- 'responderRole': os.environ.get('DISPATCH_SOURCE_ROLE', 'ceo'),
376
+ 'content': f'[SUPERVISION AMENDMENT] {instruction}',
378
377
  }).encode()
379
378
 
380
379
  try:
@@ -310,12 +310,45 @@ function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): voi
310
310
  let sessionIds = (body.sessionIds ?? body.jobIds) as string[] | undefined;
311
311
  const waveId = body.waveId as string | undefined;
312
312
 
313
- // BUG-W01 fix: auto-collect sessionIds from session-store when waveId is present
313
+ // BUG-W01 + BUG-009 fix: auto-collect sessionIds from session-store AND activity-streams
314
314
  if (waveId && (!sessionIds || sessionIds.length === 0)) {
315
- const allSessions = listSessions();
316
- sessionIds = allSessions
317
- .filter(s => s.waveId === waveId)
318
- .map(s => s.id);
315
+ const sessionIdSet = new Set(
316
+ listSessions().filter(s => s.waveId === waveId).map(s => s.id)
317
+ );
318
+
319
+ // Scan activity-streams for sessions belonging to this wave
320
+ const streamsDir = path.join(COMPANY_ROOT, 'operations', 'activity-streams');
321
+ if (fs.existsSync(streamsDir)) {
322
+ const waveTimestamp = waveId.replace('wave-', '');
323
+ for (const file of fs.readdirSync(streamsDir)) {
324
+ if (!file.endsWith('.jsonl')) continue;
325
+ const sid = file.replace('.jsonl', '');
326
+ if (sessionIdSet.has(sid)) continue;
327
+ if (sid.includes(waveTimestamp)) {
328
+ sessionIdSet.add(sid);
329
+ }
330
+ }
331
+
332
+ // Recursively find all child sessions via dispatch:start events
333
+ let foundNew = true;
334
+ while (foundNew) {
335
+ foundNew = false;
336
+ for (const sid of Array.from(sessionIdSet)) {
337
+ try {
338
+ const events = ActivityStream.readAll(sid);
339
+ for (const e of events) {
340
+ const childSessionId = e.data.childSessionId as string | undefined;
341
+ if (e.type === 'dispatch:start' && childSessionId && !sessionIdSet.has(childSessionId)) {
342
+ sessionIdSet.add(childSessionId);
343
+ foundNew = true;
344
+ }
345
+ }
346
+ } catch { /* skip */ }
347
+ }
348
+ }
349
+ }
350
+
351
+ sessionIds = Array.from(sessionIdSet);
319
352
  console.log(`[WaveSave] Auto-collected ${sessionIds.length} sessionIds for wave ${waveId}`);
320
353
  }
321
354
 
@@ -387,14 +420,45 @@ function handleSaveWave(body: Record<string, unknown>, res: ServerResponse): voi
387
420
  }
388
421
  const jsonPath = path.join(wavesDir, `${baseName}.json`);
389
422
 
423
+ // Calculate actual duration from activity stream timestamps
424
+ let startedAt = now;
425
+ let endedAt = now;
426
+ for (const role of rolesData) {
427
+ if (role.events.length > 0) {
428
+ const firstTs = new Date(role.events[0].ts);
429
+ const lastTs = new Date(role.events[role.events.length - 1].ts);
430
+ if (firstTs < startedAt) startedAt = firstTs;
431
+ if (lastTs > endedAt) endedAt = lastTs;
432
+ }
433
+ for (const child of role.childSessions) {
434
+ if (child.events.length > 0) {
435
+ const firstTs = new Date(child.events[0].ts);
436
+ const lastTs = new Date(child.events[child.events.length - 1].ts);
437
+ if (firstTs < startedAt) startedAt = firstTs;
438
+ if (lastTs > endedAt) endedAt = lastTs;
439
+ }
440
+ }
441
+ }
442
+ const duration = Math.round((endedAt.getTime() - startedAt.getTime()) / 1000);
443
+
444
+ // Collect ALL session IDs including child sessions
445
+ const allSessionIds = [...sessionIds];
446
+ for (const role of rolesData) {
447
+ for (const child of role.childSessions) {
448
+ if (!allSessionIds.includes(child.sessionId)) {
449
+ allSessionIds.push(child.sessionId);
450
+ }
451
+ }
452
+ }
453
+
390
454
  const waveJson = {
391
455
  id: baseName,
392
456
  directive,
393
- startedAt: now.toISOString(),
394
- duration: 0,
457
+ startedAt: startedAt.toISOString(),
458
+ duration,
395
459
  roles: rolesData,
396
460
  ...(waveId && { waveId }),
397
- ...(sessionIds.length > 0 && { sessionIds }),
461
+ sessionIds: allSessionIds,
398
462
  };
399
463
  fs.writeFileSync(jsonPath, JSON.stringify(waveJson, null, 2), 'utf-8');
400
464
 
@@ -56,6 +56,7 @@ sessionsRouter.patch('/:id', (req, res) => {
56
56
 
57
57
  /* DELETE /api/sessions — bulk delete (body: { ids }) or ?empty=true */
58
58
  sessionsRouter.delete('/', (req, res) => {
59
+ console.log(`[Sessions] DELETE / called (empty=${req.query.empty}, origin=${req.headers.origin ?? req.headers.referer ?? 'unknown'})`);
59
60
  if (req.query.empty === 'true') {
60
61
  const result = deleteEmpty();
61
62
  res.json(result);
@@ -72,6 +73,7 @@ sessionsRouter.delete('/', (req, res) => {
72
73
 
73
74
  /* DELETE /api/sessions/:id — delete session */
74
75
  sessionsRouter.delete('/:id', (req, res) => {
76
+ console.log(`[Sessions] DELETE /${req.params.id} called (origin=${req.headers.origin ?? req.headers.referer ?? 'unknown'})`);
75
77
  const ok = deleteSession(req.params.id);
76
78
  if (!ok) {
77
79
  res.status(404).json({ error: 'Session not found' });
@@ -101,7 +101,11 @@ function writeImmediate(session: Session): void {
101
101
  clearTimeout(timer);
102
102
  writeTimers.delete(session.id);
103
103
  }
104
- fs.writeFileSync(sessionPath(session.id), JSON.stringify(session, null, 2));
104
+ try {
105
+ fs.writeFileSync(sessionPath(session.id), JSON.stringify(session, null, 2));
106
+ } catch (err) {
107
+ console.error(`[SessionStore] WRITE FAILED for ${session.id}:`, err);
108
+ }
105
109
  }
106
110
 
107
111
  /* ─── In-memory cache ───────────────────── */
@@ -250,10 +254,17 @@ export function updateSession(id: string, updates: Partial<Pick<Session, 'title'
250
254
  return session;
251
255
  }
252
256
 
253
- export function deleteSession(id: string): boolean {
257
+ export function deleteSession(id: string, force = false): boolean {
254
258
  const session = cache.get(id);
255
259
  if (!session) return false;
256
260
 
261
+ // BUG-008 fix: protect wave sessions from accidental deletion
262
+ if (session.waveId && !force) {
263
+ console.warn(`[SessionStore] BLOCKED deletion of wave session ${id} (waveId=${session.waveId}, roleId=${session.roleId}). Use force=true to override.`);
264
+ return false;
265
+ }
266
+
267
+ console.log(`[SessionStore] Deleting session ${id} (roleId=${session.roleId}, waveId=${session.waveId ?? 'none'}, messages=${session.messages.length})`);
257
268
  cache.delete(id);
258
269
  const p = sessionPath(id);
259
270
  if (fs.existsSync(p)) fs.unlinkSync(p);
@@ -294,12 +294,32 @@ If new CEO directives arrive mid-execution, they will appear in your supervision
294
294
  marked as [CEO DIRECTIVE]. These are PRIORITY 1 — process before anything else.
295
295
  ${recoveryContext}
296
296
 
297
+ ## Quality Gate (CRITICAL — G-09)
298
+ ⛔ **"Subordinate said done" ≠ "Work is actually done."**
299
+ Before declaring yourself done, you MUST verify the deliverables meet the directive's requirements:
300
+
301
+ 1. **Read the actual output files** — don't trust status reports. Check the code, docs, or artifacts yourself.
302
+ 2. **Test if it works** — if the directive asks for a working game/app, check if it actually runs.
303
+ 3. **Count against requirements** — if the directive says "15 monsters, 7 maps", count them.
304
+ 4. **If quality is insufficient → re-dispatch** with specific feedback:
305
+ - "You implemented 4/11 systems. Still missing: NPC dialogue, inventory, capture. Continue."
306
+ - "The game doesn't load in browser. Fix the entry point and test."
307
+ 5. **Iterate until the directive is truly fulfilled.** There is NO time limit.
308
+ A half-finished deliverable is worse than taking 2-3 hours to get it right.
309
+
310
+ Re-dispatch pattern:
311
+ - dispatch same C-Level with specific gaps identified
312
+ - Each iteration should close specific gaps, not redo everything
313
+ - Maximum 5 iterations per C-Level before escalating
314
+
297
315
  ## Instructions
298
316
  1. Analyze the directive and decide which C-Level roles to dispatch (not necessarily all)
299
317
  2. Dispatch them with clear tasks
300
318
  3. Enter supervision watch loop
301
- 4. Monitor, **actively relay results between teams**, course-correct, until all done
302
- 5. Compile results and report`;
319
+ 4. Monitor, **actively relay results between teams**, course-correct
320
+ 5. When subordinates report done → **verify deliverables against requirements (G-09)**
321
+ 6. If gaps exist → re-dispatch with specific feedback. Repeat 3-5.
322
+ 7. Only when ALL requirements are met → compile results and report`;
303
323
 
304
324
  // Create supervisor session
305
325
  const session = createSession('ceo', {
@@ -168,11 +168,51 @@ export function updateFollowUpInWave(waveId: string, sessionId: string, roleId:
168
168
  */
169
169
  export function saveCompletedWave(waveId: string, directive: string): { ok: boolean; path?: string } {
170
170
  try {
171
- // Collect all sessionIds for this wave from session-store
172
- const allSessions = listSessions();
173
- const sessionIds = allSessions
174
- .filter(s => s.waveId === waveId)
175
- .map(s => s.id);
171
+ // BUG-009 fix: collect sessions from BOTH session-store AND activity-streams.
172
+ // Session-store cache may miss the CEO supervisor session (BUG-008).
173
+ // Activity-streams on disk are the source of truth for what actually ran.
174
+ const sessionIdSet = new Set(
175
+ listSessions().filter(s => s.waveId === waveId).map(s => s.id)
176
+ );
177
+
178
+ // Scan activity-streams for ALL sessions belonging to this wave.
179
+ // Wave sessions share a traceId chain: CEO → C-Level → subordinates.
180
+ // We find the CEO session (waveId timestamp embedded in its ID), then follow dispatch:start events.
181
+ const streamsDir = path.join(COMPANY_ROOT, 'operations', 'activity-streams');
182
+ if (fs.existsSync(streamsDir)) {
183
+ // Find all activity stream files and check if they belong to this wave
184
+ const waveTimestamp = waveId.replace('wave-', '');
185
+ for (const file of fs.readdirSync(streamsDir)) {
186
+ if (!file.endsWith('.jsonl')) continue;
187
+ const sid = file.replace('.jsonl', '');
188
+ if (sessionIdSet.has(sid)) continue;
189
+ // Check if session ID contains the wave timestamp (CEO session)
190
+ // or if the session was dispatched from a known wave session
191
+ if (sid.includes(waveTimestamp)) {
192
+ sessionIdSet.add(sid);
193
+ }
194
+ }
195
+
196
+ // Now recursively find all child sessions via dispatch:start events
197
+ let foundNew = true;
198
+ while (foundNew) {
199
+ foundNew = false;
200
+ for (const sid of Array.from(sessionIdSet)) {
201
+ try {
202
+ const events = ActivityStream.readAll(sid);
203
+ for (const e of events) {
204
+ const childSessionId = e.data.childSessionId as string | undefined;
205
+ if (e.type === 'dispatch:start' && childSessionId && !sessionIdSet.has(childSessionId)) {
206
+ sessionIdSet.add(childSessionId);
207
+ foundNew = true;
208
+ }
209
+ }
210
+ } catch { /* skip */ }
211
+ }
212
+ }
213
+ }
214
+
215
+ const sessionIds = Array.from(sessionIdSet);
176
216
 
177
217
  if (sessionIds.length === 0) {
178
218
  console.warn(`[WaveTracker] No sessions found for wave ${waveId}, skipping save`);
@@ -1 +1 @@
1
- import{i as $,k as T,l as R,m as F,n as S,o as A,p as P,r as i,j as a}from"./index-DyFUdV3e.js";const B=[{x:1,y:19,w:10,h:2,c:"#100A06",a:.15},{x:2,y:15,w:3,h:4,c:"$pants"},{x:7,y:15,w:3,h:4,c:"$pants"},{x:2,y:19,w:3,h:2,c:"$shoes"},{x:7,y:19,w:3,h:2,c:"$shoes"},{x:2,y:19,w:3,h:1,c:"lighten($shoes, 20)",a:.4},{x:7,y:19,w:3,h:1,c:"lighten($shoes, 20)",a:.4}],L=[{x:4,y:8,w:4,h:3,c:"$skin"},{x:1,y:1,w:10,h:8,c:"$skin"},{x:3,y:4,w:2,h:2,c:"#1A1A2E"},{x:7,y:4,w:2,h:2,c:"#1A1A2E"},{x:3,y:4,w:1,h:1,c:"#FFF",a:.35},{x:7,y:4,w:1,h:1,c:"#FFF",a:.35},{x:5,y:7,w:2,h:1,c:"darken($skin, 25)",a:.4},{x:0,y:4,w:1,h:1,c:"$skin"},{x:11,y:4,w:1,h:1,c:"$skin"}];function y(s,t,e,c,r,o){for(const l of t){const d=F(l.c,o);l.a!==void 0&&l.a!==1&&(s.globalAlpha=l.a),s.fillStyle=d,s.fillRect((l.x+c)*e,(l.y+r)*e,l.w*e,l.h*e),l.a!==void 0&&l.a!==1&&(s.globalAlpha=1)}}function O(s){var e;const t=A(s);return t?(e=t.directions)!=null&&e.down?t.directions.down.pixels:t.layer.pixels:A("short").layer.pixels}function D(s){var e;const t=S(s);return t?(e=t.directions)!=null&&e.down?t.directions.down.pixels:t.layer.pixels:S("tshirt").layer.pixels}function I(s){var e;const t=P(s);return t?(e=t.directions)!=null&&e.down?t.directions.down.pixels:t.layer.pixels:[]}function k(s,t){const e=(t==null?void 0:t.scale)??3,c=(t==null?void 0:t.padX)??2,r=(t==null?void 0:t.padY)??4,o=1,l=12+c*2,d=22+r+o,n=document.createElement("canvas");n.width=l*e,n.height=d*e,n.style.imageRendering="pixelated";const u=n.getContext("2d"),p=c,x=r;y(u,B,e,p,x,s);const h=s.outfitStyle||"tshirt";y(u,D(h),e,p,x,s),y(u,L,e,p,x,s);const f=s.hairStyle||"short";y(u,O(f),e,p,x,s);const j=s.accessory||"none";return y(u,I(j),e,p,x,s),n}const w=$().map(s=>s.id),_=T().map(s=>s.id),E=_,H=R().map(s=>s.id),C=["#FFE0BD","#F1C27D","#E0AC69","#C68642","#8D5524","#6B4423"],b=["#2C1B18","#724133","#C68642","#E6BE8A","#D4AF37","#B94E48"];function N(){return"#"+Math.floor(Math.random()*16777215).toString(16).padStart(6,"0")}function g(s){return s[Math.floor(Math.random()*s.length)]}function G({onComplete:s}){const t=i.useRef(null),[e,c]=i.useState("플레이어"),[r,o]=i.useState({skinColor:C[0],hairColor:b[0],shirtColor:"#3498db",pantsColor:"#2C3E50",shoeColor:"#34495E",hairStyle:"short",outfitStyle:"tshirt",accessory:"none"});i.useEffect(()=>{if(!t.current)return;t.current.innerHTML="";const n=k(r,{scale:6});t.current.appendChild(n)},[r]);const l=()=>{o({skinColor:g(C),hairColor:g(b),shirtColor:N(),pantsColor:N(),shoeColor:N(),hairStyle:g(w),outfitStyle:g(E),accessory:g(H)})},d=()=>{s({appearance:r,stats:{name:e,level:1,hp:100,maxHp:100,mp:80,maxMp:80,attack:20,defense:10}})};return a.jsxs("div",{className:"character-creator",children:[a.jsx("h1",{children:"캐릭터 생성"}),a.jsxs("div",{className:"creator-layout",children:[a.jsx("div",{className:"preview-section",children:a.jsx("div",{className:"preview-canvas",ref:t})}),a.jsxs("div",{className:"customization-section",children:[a.jsxs("div",{className:"form-group",children:[a.jsx("label",{children:"이름"}),a.jsx("input",{type:"text",value:e,onChange:n=>c(n.target.value),maxLength:10})]}),a.jsxs("div",{className:"form-group",children:[a.jsx("label",{children:"피부색"}),a.jsx("div",{className:"color-palette",children:C.map(n=>a.jsx("button",{className:`color-btn ${r.skinColor===n?"active":""}`,style:{backgroundColor:n},onClick:()=>o({...r,skinColor:n})},n))})]}),a.jsxs("div",{className:"form-group",children:[a.jsx("label",{children:"헤어스타일"}),a.jsx("select",{value:r.hairStyle,onChange:n=>o({...r,hairStyle:n.target.value}),children:w.map(n=>a.jsx("option",{value:n,children:n},n))})]}),a.jsxs("div",{className:"form-group",children:[a.jsx("label",{children:"헤어 색상"}),a.jsx("div",{className:"color-palette",children:b.map(n=>a.jsx("button",{className:`color-btn ${r.hairColor===n?"active":""}`,style:{backgroundColor:n},onClick:()=>o({...r,hairColor:n})},n))})]}),a.jsxs("div",{className:"form-group",children:[a.jsx("label",{children:"의상"}),a.jsx("select",{value:r.outfitStyle,onChange:n=>o({...r,outfitStyle:n.target.value}),children:E.map(n=>a.jsx("option",{value:n,children:n},n))})]}),a.jsxs("div",{className:"form-group",children:[a.jsx("label",{children:"액세서리"}),a.jsx("select",{value:r.accessory,onChange:n=>o({...r,accessory:n.target.value}),children:H.slice(0,15).map(n=>a.jsx("option",{value:n,children:n},n))})]}),a.jsxs("div",{className:"button-group",children:[a.jsx("button",{onClick:l,className:"btn-secondary",children:"🎲 랜덤 생성"}),a.jsx("button",{onClick:d,className:"btn-primary",children:"⚔️ 게임 시작!"})]})]})]})]})}const m={attack:{name:"공격",emoji:"⚔️"},defend:{name:"방어",emoji:"🛡️"},heal:{name:"회복",mpCost:20,emoji:"💚"},flee:{name:"도망",emoji:"🏃"}};function Y(){const s=[{name:"슬라임",hp:50,attack:8,defense:3},{name:"고블린",hp:70,attack:12,defense:5},{name:"해골",hp:60,attack:15,defense:2},{name:"오크",hp:100,attack:18,defense:8}],t=s[Math.floor(Math.random()*s.length)];return{name:t.name,level:1,hp:t.hp,maxHp:t.hp,mp:0,maxMp:0,attack:t.attack,defense:t.defense}}function M(s,t,e){const c=s.attack-t.defense/2,r=Math.max(1,Math.floor(c));return e?Math.floor(r*.5):r}function z(s,t){const e={...s};if(t==="heal"&&e.player.mp<m.heal.mpCost)return{newState:e,continueToEnemyTurn:!1};let c;switch(t){case"attack":{const r=M(e.player,e.enemy,!1);e.enemy.hp=Math.max(0,e.enemy.hp-r),c={actorName:e.player.name,skill:"attack",damage:r,message:`${e.player.name}의 공격! ${r} 데미지!`},e.defendActive=!1;break}case"defend":{e.defendActive=!0,c={actorName:e.player.name,skill:"defend",message:`${e.player.name}이(가) 방어 태세를 취했다!`};break}case"heal":{const o=Math.min(30,e.player.maxHp-e.player.hp);e.player.hp=Math.min(e.player.maxHp,e.player.hp+30),e.player.mp-=m.heal.mpCost,c={actorName:e.player.name,skill:"heal",heal:o,message:`${e.player.name}이(가) 회복! HP +${o}`},e.defendActive=!1;break}case"flee":{Math.random()<.5?(e.status="fled",c={actorName:e.player.name,skill:"flee",message:`${e.player.name}이(가) 도망쳤다!`}):c={actorName:e.player.name,skill:"flee",message:"도망에 실패했다!"},e.defendActive=!1;break}}return e.log=[...e.log,c],e.enemy.hp<=0?(e.status="victory",{newState:e,continueToEnemyTurn:!1}):e.status==="fled"?{newState:e,continueToEnemyTurn:!1}:{newState:e,continueToEnemyTurn:!0}}function K(s){const t={...s},e=M(t.enemy,t.player,t.defendActive);t.player.hp=Math.max(0,t.player.hp-e);const c={actorName:t.enemy.name,skill:"attack",damage:e,message:`${t.enemy.name}의 공격! ${e} 데미지!`};return t.log=[...t.log,c],t.defendActive=!1,t.player.hp<=0&&(t.status="defeat"),t}function U(s,t){return{player:s,enemy:t,turn:"player",log:[],defendActive:!1,status:"ongoing"}}function W(){return{skinColor:"#6B8E23",hairColor:"#2F4F2F",shirtColor:"#8B4513",pantsColor:"#654321",shoeColor:"#3E2723",hairStyle:"messy",outfitStyle:"vest",accessory:"horns"}}function X({player:s,onRestart:t}){const e=i.useRef(null),c=i.useRef(null),r=i.useRef(null),[o]=i.useState(W()),[l,d]=i.useState(()=>U(s.stats,Y()));i.useEffect(()=>{if(e.current){e.current.innerHTML="";const h=k(s.appearance,{scale:6});e.current.appendChild(h)}if(c.current){c.current.innerHTML="";const h=k(o,{scale:6});c.current.appendChild(h)}},[s.appearance,o]),i.useEffect(()=>{r.current&&(r.current.scrollTop=r.current.scrollHeight)},[l.log]);const n=h=>{if(l.status!=="ongoing")return;const{newState:f,continueToEnemyTurn:j}=z(l,h);d(f),j&&f.status==="ongoing"&&setTimeout(()=>{d(v=>v.status!=="ongoing"?v:K(v))},800)},u=l.player.hp/l.player.maxHp*100,p=l.enemy.hp/l.enemy.maxHp*100,x=l.player.mp/l.player.maxMp*100;return a.jsxs("div",{className:"battle-screen",children:[a.jsx("h1",{children:"⚔️ 배틀!"}),a.jsxs("div",{className:"battle-characters",children:[a.jsxs("div",{className:"character-box",children:[a.jsx("div",{className:"character-canvas",ref:e}),a.jsx("div",{className:"character-name",children:l.player.name}),a.jsxs("div",{className:"stat-bar",children:[a.jsxs("div",{className:"stat-label",children:["HP: ",l.player.hp,"/",l.player.maxHp]}),a.jsx("div",{className:"stat-bar-bg",children:a.jsx("div",{className:"stat-bar-fill hp",style:{width:`${u}%`}})})]}),a.jsxs("div",{className:"stat-bar",children:[a.jsxs("div",{className:"stat-label",children:["MP: ",l.player.mp,"/",l.player.maxMp]}),a.jsx("div",{className:"stat-bar-bg",children:a.jsx("div",{className:"stat-bar-fill mp",style:{width:`${x}%`}})})]})]}),a.jsx("div",{className:"vs-text",children:"VS"}),a.jsxs("div",{className:"character-box",children:[a.jsx("div",{className:"character-canvas",ref:c}),a.jsx("div",{className:"character-name",children:l.enemy.name}),a.jsxs("div",{className:"stat-bar",children:[a.jsxs("div",{className:"stat-label",children:["HP: ",l.enemy.hp,"/",l.enemy.maxHp]}),a.jsx("div",{className:"stat-bar-bg",children:a.jsx("div",{className:"stat-bar-fill hp",style:{width:`${p}%`}})})]})]})]}),a.jsxs("div",{className:"battle-log",ref:r,children:[l.log.length===0&&a.jsx("div",{className:"log-entry",children:"배틀 시작!"}),l.log.map((h,f)=>a.jsxs("div",{className:"log-entry",children:["> ",h.message]},f))]}),l.status!=="ongoing"&&a.jsxs("div",{className:"battle-result",children:[l.status==="victory"&&a.jsx("h2",{children:"🎉 승리!"}),l.status==="defeat"&&a.jsx("h2",{children:"💀 패배..."}),l.status==="fled"&&a.jsx("h2",{children:"🏃 도망쳤다!"}),a.jsx("button",{onClick:t,className:"btn-primary",children:"다시 시작"})]}),l.status==="ongoing"&&a.jsxs("div",{className:"skill-buttons",children:[a.jsxs("button",{onClick:()=>n("attack"),className:"skill-btn",children:[m.attack.emoji," ",m.attack.name]}),a.jsxs("button",{onClick:()=>n("defend"),className:"skill-btn",children:[m.defend.emoji," ",m.defend.name]}),a.jsxs("button",{onClick:()=>n("heal"),className:"skill-btn",disabled:l.player.mp<m.heal.mpCost,children:[m.heal.emoji," ",m.heal.name,a.jsxs("span",{className:"mp-cost",children:["(MP ",m.heal.mpCost,")"]})]}),a.jsxs("button",{onClick:()=>n("flee"),className:"skill-btn",children:[m.flee.emoji," ",m.flee.name]})]})]})}function q(){const[s,t]=i.useState("create"),[e,c]=i.useState(null),r=l=>{c(l),t("battle")},o=()=>{c(null),t("create")};return a.jsxs("div",{className:"rpg-game",children:[s==="create"&&a.jsx(G,{onComplete:r}),s==="battle"&&e&&a.jsx(X,{player:e,onRestart:o})]})}export{q as default};
1
+ import{i as $,k as T,l as R,m as F,n as S,o as A,p as P,r as i,j as a}from"./index-BkPv-nh1.js";const B=[{x:1,y:19,w:10,h:2,c:"#100A06",a:.15},{x:2,y:15,w:3,h:4,c:"$pants"},{x:7,y:15,w:3,h:4,c:"$pants"},{x:2,y:19,w:3,h:2,c:"$shoes"},{x:7,y:19,w:3,h:2,c:"$shoes"},{x:2,y:19,w:3,h:1,c:"lighten($shoes, 20)",a:.4},{x:7,y:19,w:3,h:1,c:"lighten($shoes, 20)",a:.4}],L=[{x:4,y:8,w:4,h:3,c:"$skin"},{x:1,y:1,w:10,h:8,c:"$skin"},{x:3,y:4,w:2,h:2,c:"#1A1A2E"},{x:7,y:4,w:2,h:2,c:"#1A1A2E"},{x:3,y:4,w:1,h:1,c:"#FFF",a:.35},{x:7,y:4,w:1,h:1,c:"#FFF",a:.35},{x:5,y:7,w:2,h:1,c:"darken($skin, 25)",a:.4},{x:0,y:4,w:1,h:1,c:"$skin"},{x:11,y:4,w:1,h:1,c:"$skin"}];function y(s,t,e,c,r,o){for(const l of t){const d=F(l.c,o);l.a!==void 0&&l.a!==1&&(s.globalAlpha=l.a),s.fillStyle=d,s.fillRect((l.x+c)*e,(l.y+r)*e,l.w*e,l.h*e),l.a!==void 0&&l.a!==1&&(s.globalAlpha=1)}}function O(s){var e;const t=A(s);return t?(e=t.directions)!=null&&e.down?t.directions.down.pixels:t.layer.pixels:A("short").layer.pixels}function D(s){var e;const t=S(s);return t?(e=t.directions)!=null&&e.down?t.directions.down.pixels:t.layer.pixels:S("tshirt").layer.pixels}function I(s){var e;const t=P(s);return t?(e=t.directions)!=null&&e.down?t.directions.down.pixels:t.layer.pixels:[]}function k(s,t){const e=(t==null?void 0:t.scale)??3,c=(t==null?void 0:t.padX)??2,r=(t==null?void 0:t.padY)??4,o=1,l=12+c*2,d=22+r+o,n=document.createElement("canvas");n.width=l*e,n.height=d*e,n.style.imageRendering="pixelated";const u=n.getContext("2d"),p=c,x=r;y(u,B,e,p,x,s);const h=s.outfitStyle||"tshirt";y(u,D(h),e,p,x,s),y(u,L,e,p,x,s);const f=s.hairStyle||"short";y(u,O(f),e,p,x,s);const j=s.accessory||"none";return y(u,I(j),e,p,x,s),n}const w=$().map(s=>s.id),_=T().map(s=>s.id),E=_,H=R().map(s=>s.id),C=["#FFE0BD","#F1C27D","#E0AC69","#C68642","#8D5524","#6B4423"],b=["#2C1B18","#724133","#C68642","#E6BE8A","#D4AF37","#B94E48"];function N(){return"#"+Math.floor(Math.random()*16777215).toString(16).padStart(6,"0")}function g(s){return s[Math.floor(Math.random()*s.length)]}function G({onComplete:s}){const t=i.useRef(null),[e,c]=i.useState("플레이어"),[r,o]=i.useState({skinColor:C[0],hairColor:b[0],shirtColor:"#3498db",pantsColor:"#2C3E50",shoeColor:"#34495E",hairStyle:"short",outfitStyle:"tshirt",accessory:"none"});i.useEffect(()=>{if(!t.current)return;t.current.innerHTML="";const n=k(r,{scale:6});t.current.appendChild(n)},[r]);const l=()=>{o({skinColor:g(C),hairColor:g(b),shirtColor:N(),pantsColor:N(),shoeColor:N(),hairStyle:g(w),outfitStyle:g(E),accessory:g(H)})},d=()=>{s({appearance:r,stats:{name:e,level:1,hp:100,maxHp:100,mp:80,maxMp:80,attack:20,defense:10}})};return a.jsxs("div",{className:"character-creator",children:[a.jsx("h1",{children:"캐릭터 생성"}),a.jsxs("div",{className:"creator-layout",children:[a.jsx("div",{className:"preview-section",children:a.jsx("div",{className:"preview-canvas",ref:t})}),a.jsxs("div",{className:"customization-section",children:[a.jsxs("div",{className:"form-group",children:[a.jsx("label",{children:"이름"}),a.jsx("input",{type:"text",value:e,onChange:n=>c(n.target.value),maxLength:10})]}),a.jsxs("div",{className:"form-group",children:[a.jsx("label",{children:"피부색"}),a.jsx("div",{className:"color-palette",children:C.map(n=>a.jsx("button",{className:`color-btn ${r.skinColor===n?"active":""}`,style:{backgroundColor:n},onClick:()=>o({...r,skinColor:n})},n))})]}),a.jsxs("div",{className:"form-group",children:[a.jsx("label",{children:"헤어스타일"}),a.jsx("select",{value:r.hairStyle,onChange:n=>o({...r,hairStyle:n.target.value}),children:w.map(n=>a.jsx("option",{value:n,children:n},n))})]}),a.jsxs("div",{className:"form-group",children:[a.jsx("label",{children:"헤어 색상"}),a.jsx("div",{className:"color-palette",children:b.map(n=>a.jsx("button",{className:`color-btn ${r.hairColor===n?"active":""}`,style:{backgroundColor:n},onClick:()=>o({...r,hairColor:n})},n))})]}),a.jsxs("div",{className:"form-group",children:[a.jsx("label",{children:"의상"}),a.jsx("select",{value:r.outfitStyle,onChange:n=>o({...r,outfitStyle:n.target.value}),children:E.map(n=>a.jsx("option",{value:n,children:n},n))})]}),a.jsxs("div",{className:"form-group",children:[a.jsx("label",{children:"액세서리"}),a.jsx("select",{value:r.accessory,onChange:n=>o({...r,accessory:n.target.value}),children:H.slice(0,15).map(n=>a.jsx("option",{value:n,children:n},n))})]}),a.jsxs("div",{className:"button-group",children:[a.jsx("button",{onClick:l,className:"btn-secondary",children:"🎲 랜덤 생성"}),a.jsx("button",{onClick:d,className:"btn-primary",children:"⚔️ 게임 시작!"})]})]})]})]})}const m={attack:{name:"공격",emoji:"⚔️"},defend:{name:"방어",emoji:"🛡️"},heal:{name:"회복",mpCost:20,emoji:"💚"},flee:{name:"도망",emoji:"🏃"}};function Y(){const s=[{name:"슬라임",hp:50,attack:8,defense:3},{name:"고블린",hp:70,attack:12,defense:5},{name:"해골",hp:60,attack:15,defense:2},{name:"오크",hp:100,attack:18,defense:8}],t=s[Math.floor(Math.random()*s.length)];return{name:t.name,level:1,hp:t.hp,maxHp:t.hp,mp:0,maxMp:0,attack:t.attack,defense:t.defense}}function M(s,t,e){const c=s.attack-t.defense/2,r=Math.max(1,Math.floor(c));return e?Math.floor(r*.5):r}function z(s,t){const e={...s};if(t==="heal"&&e.player.mp<m.heal.mpCost)return{newState:e,continueToEnemyTurn:!1};let c;switch(t){case"attack":{const r=M(e.player,e.enemy,!1);e.enemy.hp=Math.max(0,e.enemy.hp-r),c={actorName:e.player.name,skill:"attack",damage:r,message:`${e.player.name}의 공격! ${r} 데미지!`},e.defendActive=!1;break}case"defend":{e.defendActive=!0,c={actorName:e.player.name,skill:"defend",message:`${e.player.name}이(가) 방어 태세를 취했다!`};break}case"heal":{const o=Math.min(30,e.player.maxHp-e.player.hp);e.player.hp=Math.min(e.player.maxHp,e.player.hp+30),e.player.mp-=m.heal.mpCost,c={actorName:e.player.name,skill:"heal",heal:o,message:`${e.player.name}이(가) 회복! HP +${o}`},e.defendActive=!1;break}case"flee":{Math.random()<.5?(e.status="fled",c={actorName:e.player.name,skill:"flee",message:`${e.player.name}이(가) 도망쳤다!`}):c={actorName:e.player.name,skill:"flee",message:"도망에 실패했다!"},e.defendActive=!1;break}}return e.log=[...e.log,c],e.enemy.hp<=0?(e.status="victory",{newState:e,continueToEnemyTurn:!1}):e.status==="fled"?{newState:e,continueToEnemyTurn:!1}:{newState:e,continueToEnemyTurn:!0}}function K(s){const t={...s},e=M(t.enemy,t.player,t.defendActive);t.player.hp=Math.max(0,t.player.hp-e);const c={actorName:t.enemy.name,skill:"attack",damage:e,message:`${t.enemy.name}의 공격! ${e} 데미지!`};return t.log=[...t.log,c],t.defendActive=!1,t.player.hp<=0&&(t.status="defeat"),t}function U(s,t){return{player:s,enemy:t,turn:"player",log:[],defendActive:!1,status:"ongoing"}}function W(){return{skinColor:"#6B8E23",hairColor:"#2F4F2F",shirtColor:"#8B4513",pantsColor:"#654321",shoeColor:"#3E2723",hairStyle:"messy",outfitStyle:"vest",accessory:"horns"}}function X({player:s,onRestart:t}){const e=i.useRef(null),c=i.useRef(null),r=i.useRef(null),[o]=i.useState(W()),[l,d]=i.useState(()=>U(s.stats,Y()));i.useEffect(()=>{if(e.current){e.current.innerHTML="";const h=k(s.appearance,{scale:6});e.current.appendChild(h)}if(c.current){c.current.innerHTML="";const h=k(o,{scale:6});c.current.appendChild(h)}},[s.appearance,o]),i.useEffect(()=>{r.current&&(r.current.scrollTop=r.current.scrollHeight)},[l.log]);const n=h=>{if(l.status!=="ongoing")return;const{newState:f,continueToEnemyTurn:j}=z(l,h);d(f),j&&f.status==="ongoing"&&setTimeout(()=>{d(v=>v.status!=="ongoing"?v:K(v))},800)},u=l.player.hp/l.player.maxHp*100,p=l.enemy.hp/l.enemy.maxHp*100,x=l.player.mp/l.player.maxMp*100;return a.jsxs("div",{className:"battle-screen",children:[a.jsx("h1",{children:"⚔️ 배틀!"}),a.jsxs("div",{className:"battle-characters",children:[a.jsxs("div",{className:"character-box",children:[a.jsx("div",{className:"character-canvas",ref:e}),a.jsx("div",{className:"character-name",children:l.player.name}),a.jsxs("div",{className:"stat-bar",children:[a.jsxs("div",{className:"stat-label",children:["HP: ",l.player.hp,"/",l.player.maxHp]}),a.jsx("div",{className:"stat-bar-bg",children:a.jsx("div",{className:"stat-bar-fill hp",style:{width:`${u}%`}})})]}),a.jsxs("div",{className:"stat-bar",children:[a.jsxs("div",{className:"stat-label",children:["MP: ",l.player.mp,"/",l.player.maxMp]}),a.jsx("div",{className:"stat-bar-bg",children:a.jsx("div",{className:"stat-bar-fill mp",style:{width:`${x}%`}})})]})]}),a.jsx("div",{className:"vs-text",children:"VS"}),a.jsxs("div",{className:"character-box",children:[a.jsx("div",{className:"character-canvas",ref:c}),a.jsx("div",{className:"character-name",children:l.enemy.name}),a.jsxs("div",{className:"stat-bar",children:[a.jsxs("div",{className:"stat-label",children:["HP: ",l.enemy.hp,"/",l.enemy.maxHp]}),a.jsx("div",{className:"stat-bar-bg",children:a.jsx("div",{className:"stat-bar-fill hp",style:{width:`${p}%`}})})]})]})]}),a.jsxs("div",{className:"battle-log",ref:r,children:[l.log.length===0&&a.jsx("div",{className:"log-entry",children:"배틀 시작!"}),l.log.map((h,f)=>a.jsxs("div",{className:"log-entry",children:["> ",h.message]},f))]}),l.status!=="ongoing"&&a.jsxs("div",{className:"battle-result",children:[l.status==="victory"&&a.jsx("h2",{children:"🎉 승리!"}),l.status==="defeat"&&a.jsx("h2",{children:"💀 패배..."}),l.status==="fled"&&a.jsx("h2",{children:"🏃 도망쳤다!"}),a.jsx("button",{onClick:t,className:"btn-primary",children:"다시 시작"})]}),l.status==="ongoing"&&a.jsxs("div",{className:"skill-buttons",children:[a.jsxs("button",{onClick:()=>n("attack"),className:"skill-btn",children:[m.attack.emoji," ",m.attack.name]}),a.jsxs("button",{onClick:()=>n("defend"),className:"skill-btn",children:[m.defend.emoji," ",m.defend.name]}),a.jsxs("button",{onClick:()=>n("heal"),className:"skill-btn",disabled:l.player.mp<m.heal.mpCost,children:[m.heal.emoji," ",m.heal.name,a.jsxs("span",{className:"mp-cost",children:["(MP ",m.heal.mpCost,")"]})]}),a.jsxs("button",{onClick:()=>n("flee"),className:"skill-btn",children:[m.flee.emoji," ",m.flee.name]})]})]})}function q(){const[s,t]=i.useState("create"),[e,c]=i.useState(null),r=l=>{c(l),t("battle")},o=()=>{c(null),t("create")};return a.jsxs("div",{className:"rpg-game",children:[s==="create"&&a.jsx(G,{onComplete:r}),s==="battle"&&e&&a.jsx(X,{player:e,onRestart:o})]})}export{q as default};