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 +1 -1
- package/src/api/src/engine/context-assembler.ts +6 -1
- package/src/api/src/engine/runners/claude-cli.ts +2 -3
- package/src/api/src/routes/execute.ts +72 -8
- package/src/api/src/routes/sessions.ts +2 -0
- package/src/api/src/services/session-store.ts +13 -2
- package/src/api/src/services/supervisor-heartbeat.ts +22 -2
- package/src/api/src/services/wave-tracker.ts +45 -5
- package/src/web/dist/assets/{index-DlFP0kZX.js → index-BPXMqFIq.js} +1 -1
- package/src/web/dist/assets/index-BkPv-nh1.js +138 -0
- package/src/web/dist/assets/index-yklWJXTD.css +1 -0
- package/src/web/dist/assets/{preview-app-3C1r9jSY.js → preview-app-C-tDc4fA.js} +1 -1
- package/src/web/dist/index.html +2 -2
- package/src/web/dist/assets/index-BnhRwHgb.css +0 -1
- package/src/web/dist/assets/index-DyFUdV3e.js +0 -116
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
374
|
+
# Amend sends a message to the session with amendment instructions
|
|
375
375
|
body = json.dumps({
|
|
376
|
-
'
|
|
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
|
|
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
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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:
|
|
394
|
-
duration
|
|
457
|
+
startedAt: startedAt.toISOString(),
|
|
458
|
+
duration,
|
|
395
459
|
roles: rolesData,
|
|
396
460
|
...(waveId && { waveId }),
|
|
397
|
-
|
|
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
|
-
|
|
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
|
|
302
|
-
5.
|
|
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
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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};
|