Junhoee commited on
Commit
6e20b9b
ยท
verified ยท
1 Parent(s): f8a6c42

Upload 140 files

Browse files
content/beatcards/E06.yaml CHANGED
@@ -348,14 +348,13 @@ cards:
348
  - label: ๋‘˜๋งŒ์˜ ๋น„๋ฐ€์— ๋™์กฐํ•œ๋‹ค
349
  modifiers: &id001 []
350
  axis: trust
351
- result: ๋‚˜๋Š” ๊ณ ๊ฐœ๋ฅผ ๋„๋•์˜€๋‹ค. ๊ทธ๋…€์˜ ๋ง์ด ์˜ณ์€ ๊ฒƒ๋งŒ ๊ฐ™์•˜๋‹ค. ์šฐ๋ฆฌ ์‚ฌ์ด์˜ ์ผ์„, ๋‹ค๋ฅธ ๋ˆ„๊ตฌ๋„ ์•Œ ํ•„์š”๋Š” ์—†์—ˆ๋‹ค. ๋กœ.
352
  next: null
353
  preset_branch: E06-10C
354
  - label: ๊ทธ๋ž˜๋„ ๋ˆ„๊ตฐ๊ฐ€์—๊ฒŒ ์•Œ๋ ค์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค
355
  modifiers: *id001
356
  axis: doubt
357
- result: ๊ทธ๋Ÿฐ๋ฐ๋„, ๋งˆ์Œ ๊นŠ์€ ๊ณณ์—์„œ ๋ฌด์–ธ๊ฐ€๊ฐ€ ์ž๊พธ ๊ณ ๊ฐœ๋ฅผ ๋“ค์—ˆ๋‹ค. ์ด๋Œ€๋กœ ์‹œ๋“ค๋‹ค ์ฃฝ์„ ์ˆ˜๋Š” ์—†๋‹ค๋Š”, ๊ฐ€๋А๋‹ค๋ž€ ์™ธ์นจ์ด. (๋‘ ์  ๋ฐœ๊ฒฌ) ์ดํ›„ ์•Œ๋ฆผ ๋ถ„๊ธฐ(E06-10B)๋กœ
358
- ์ด์–ด์ง.
359
  next: E06-10
360
  r_card_refs: []
361
  note: '**[์—”์ง„D ํ•ด๊ธˆ + ์—”์ง„C ๋ถ„๊ธฐ ์ž…๋ ฅ]** ํ‘œ๋ฉด("์ฃฝ์Œ์€ ์ถ•๋ณต")์€ ์ „์› / **์‹ฌ์ธต("๊ทธ๊ฑด ๋์ด ์•„๋‹ˆ์•ผ, ์‹œ์ž‘์ด์•ผ")์€ ์‹ ๋ขฐ้ซ˜์—์„œ๋งŒ** โ€” โ‘ก๋™ํ™” ์œ ํ˜น์˜ ์ง์ ‘ ๋ˆ„์„ค(์ „ํ™˜=์˜์‹ฌ์—†๋Š”
 
348
  - label: ๋‘˜๋งŒ์˜ ๋น„๋ฐ€์— ๋™์กฐํ•œ๋‹ค
349
  modifiers: &id001 []
350
  axis: trust
351
+ result: ๋‚˜๋Š” ๊ณ ๊ฐœ๋ฅผ ๋„๋•์˜€๋‹ค. ๊ทธ๋…€์˜ ๋ง์ด ์˜ณ์€ ๊ฒƒ๋งŒ ๊ฐ™์•˜๋‹ค. ์šฐ๋ฆฌ ์‚ฌ์ด์˜ ์ผ์„, ๋‹ค๋ฅธ ๋ˆ„๊ตฌ๋„ ์•Œ ํ•„์š”๋Š” ์—†์—ˆ๋‹ค.
352
  next: null
353
  preset_branch: E06-10C
354
  - label: ๊ทธ๋ž˜๋„ ๋ˆ„๊ตฐ๊ฐ€์—๊ฒŒ ์•Œ๋ ค์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•œ๋‹ค
355
  modifiers: *id001
356
  axis: doubt
357
+ result: ๊ทธ๋Ÿฐ๋ฐ๋„, ๋งˆ์Œ ๊นŠ์€ ๊ณณ์—์„œ ๋ฌด์–ธ๊ฐ€๊ฐ€ ์ž๊พธ ๊ณ ๊ฐœ๋ฅผ ๋“ค์—ˆ๋‹ค. ์ด๋Œ€๋กœ ์‹œ๋“ค๋‹ค ์ฃฝ์„ ์ˆ˜๋Š” ์—†๋‹ค๋Š”, ๊ฐ€๋А๋‹ค๋ž€ ์™ธ์นจ์ด.
 
358
  next: E06-10
359
  r_card_refs: []
360
  note: '**[์—”์ง„D ํ•ด๊ธˆ + ์—”์ง„C ๋ถ„๊ธฐ ์ž…๋ ฅ]** ํ‘œ๋ฉด("์ฃฝ์Œ์€ ์ถ•๋ณต")์€ ์ „์› / **์‹ฌ์ธต("๊ทธ๊ฑด ๋์ด ์•„๋‹ˆ์•ผ, ์‹œ์ž‘์ด์•ผ")์€ ์‹ ๋ขฐ้ซ˜์—์„œ๋งŒ** โ€” โ‘ก๋™ํ™” ์œ ํ˜น์˜ ์ง์ ‘ ๋ˆ„์„ค(์ „ํ™˜=์˜์‹ฌ์—†๋Š”
engine/__pycache__/router.cpython-312.pyc CHANGED
Binary files a/engine/__pycache__/router.cpython-312.pyc and b/engine/__pycache__/router.cpython-312.pyc differ
 
engine/__pycache__/router.cpython-314.pyc CHANGED
Binary files a/engine/__pycache__/router.cpython-314.pyc and b/engine/__pycache__/router.cpython-314.pyc differ
 
engine/router.py CHANGED
@@ -50,8 +50,8 @@ INTRO_IMAGES = [
50
  SPEAKER_PORTRAIT = {
51
  "์นด๋ฅด๋ฐ€๋ผ": "/assets/์ธ๋ฌผ/์นด๋ฅด๋ฐ€๋ผ.png",
52
  "์•„๋ฒ„์ง€": "/assets/์ธ๋ฌผ/์•„๋ฒ„์ง€.png",
53
- "๋งˆ๋“œ๋ชจ์•„์ ค": "/assets/์ธ๋ฌผ/๊ฐ€์ •๊ต์‚ฌ.png",
54
- "๋งˆ๋‹ด": "/assets/์ธ๋ฌผ/๋ณด๋ชจ.png",
55
  }
56
 
57
  # ์žฅ์†Œ๋ณ„ ๋ฐฐ๊ฒฝ (๋น„์ฃผ์–ผ ๋…ธ๋ฒจ) โ€” ์นด๋“œ ํƒœ๊ทธ `#์žฅ์†Œ/X` ๊ธฐ์ค€. ๋งคํ•‘ ์—†๋Š” ์žฅ์†Œ(์ •์›ยทํ™”๋ž‘ยท์„œ์žฌ ๋“ฑ)๋Š”
@@ -158,10 +158,10 @@ def _step_json(ps: PlaySession) -> dict:
158
  "card": {"id": last.id, "title": first.title, "episode": first.episode_id,
159
  "episode_title": ps.episode_titles.get(first.episode_id, first.episode_id)},
160
  "narration": [{"type": b.type, "text": b.text, "speaker": b.speaker,
161
- "role": b.role,
162
  "portrait": (SPEAKER_PORTRAIT.get(b.speaker)
163
  if b.type == "character_dialogue" else None)}
164
- for b in page.narration],
165
  "choices": [{"label": c.label} for c in page.choices],
166
  "can_chat": page.can_chat,
167
  "image": IMAGE_MAP.get(first.id),
 
50
  SPEAKER_PORTRAIT = {
51
  "์นด๋ฅด๋ฐ€๋ผ": "/assets/์ธ๋ฌผ/์นด๋ฅด๋ฐ€๋ผ.png",
52
  "์•„๋ฒ„์ง€": "/assets/์ธ๋ฌผ/์•„๋ฒ„์ง€.png",
53
+ # ๋งˆ๋‹ดยท๋งˆ๋“œ๋ชจ์•„์ ค์€ ์ „๋ฉด ์Šคํ…Œ์ด์ง€ ๋Œ€์ƒ์—์„œ ์ œ์™ธ (์œ ์ € ๊ฒฐ์ • โ€” ์•™์ƒ๋ธ” ์žฅ๋ฉด์€
54
+ # ๋ณธ๋ฌธ ํ๋ฆ„์œผ๋กœ. ์—์…‹ ๋งคํ•‘๋„ ์ถ”์ •์ด์—ˆ์Œ). ํ•„์š” ์‹œ ์—ฌ๊ธฐ ํ•œ ์ค„๋กœ ๋ณต์›
55
  }
56
 
57
  # ์žฅ์†Œ๋ณ„ ๋ฐฐ๊ฒฝ (๋น„์ฃผ์–ผ ๋…ธ๋ฒจ) โ€” ์นด๋“œ ํƒœ๊ทธ `#์žฅ์†Œ/X` ๊ธฐ์ค€. ๋งคํ•‘ ์—†๋Š” ์žฅ์†Œ(์ •์›ยทํ™”๋ž‘ยท์„œ์žฌ ๋“ฑ)๋Š”
 
158
  "card": {"id": last.id, "title": first.title, "episode": first.episode_id,
159
  "episode_title": ps.episode_titles.get(first.episode_id, first.episode_id)},
160
  "narration": [{"type": b.type, "text": b.text, "speaker": b.speaker,
161
+ "role": b.role, "card": cid,
162
  "portrait": (SPEAKER_PORTRAIT.get(b.speaker)
163
  if b.type == "character_dialogue" else None)}
164
+ for b, cid in zip(page.narration, page.narration_cards)],
165
  "choices": [{"label": c.label} for c in page.choices],
166
  "can_chat": page.can_chat,
167
  "image": IMAGE_MAP.get(first.id),
engine/schemas/__pycache__/chapter.cpython-314.pyc CHANGED
Binary files a/engine/schemas/__pycache__/chapter.cpython-314.pyc and b/engine/schemas/__pycache__/chapter.cpython-314.pyc differ
 
engine/schemas/__pycache__/validate.cpython-312.pyc CHANGED
Binary files a/engine/schemas/__pycache__/validate.cpython-312.pyc and b/engine/schemas/__pycache__/validate.cpython-312.pyc differ
 
engine/schemas/__pycache__/validate.cpython-314.pyc CHANGED
Binary files a/engine/schemas/__pycache__/validate.cpython-314.pyc and b/engine/schemas/__pycache__/validate.cpython-314.pyc differ
 
engine/schemas/validate.py CHANGED
@@ -119,9 +119,10 @@ def check_links(episodes: list[EpisodeFile], chapter, r_cards) -> tuple[list[str
119
  if not has_out:
120
  warnings.append(f"{c.id}: ๋‚˜๊ฐ€๋Š” ๋งํฌ ์—†์Œ (์ฑ•ํ„ฐ ์ถœ๊ตฌ๊ฐ€ ์•„๋‹ˆ๋ฉด ๊ฒฐํ•จ)")
121
 
122
- # ์ €์ž‘ ๋ฉ”ํƒ€ ๋ˆ„์ˆ˜ โ€” ๋ณผ๋“œ ์นด๋“œID(**E##-) ๊ฐ™์€ ํŽธ์ง‘ ๋ฉ”๋ชจ๊ฐ€ ์ง€๋ฌธ/๊ฒฐ๊ณผ์— ๋‚จ์œผ๋ฉด ํ™”๋ฉด์— ๋…ธ์ถœ๋œ๋‹ค
 
123
  import re as _re
124
- leak = _re.compile(r"\*\*E\d{2}-|\*\*โ–ท")
125
  for c in cards.values():
126
  for b in c.narration:
127
  if leak.search(b.text):
 
119
  if not has_out:
120
  warnings.append(f"{c.id}: ๋‚˜๊ฐ€๋Š” ๋งํฌ ์—†์Œ (์ฑ•ํ„ฐ ์ถœ๊ตฌ๊ฐ€ ์•„๋‹ˆ๋ฉด ๊ฒฐํ•จ)")
121
 
122
+ # ์ €์ž‘ ๋ฉ”ํƒ€ ๋ˆ„์ˆ˜ โ€” ์นด๋“œID ์ฐธ์กฐ(E##-##)ยท๋ณผ๋“œ ๋งˆ์ปค ๊ฐ™์€ ํŽธ์ง‘ ๋ฉ”๋ชจ๊ฐ€
123
+ # ์ง€๋ฌธ/๊ฒฐ๊ณผ์— ๋‚จ์œผ๋ฉด ํ™”๋ฉด์— ๋…ธ์ถœ๋œ๋‹ค (๋ณธ๋ฌธ ๋Œ€์‚ฌยท์ง€๋ฌธ์— ์นด๋“œID๊ฐ€ ๋“ฑ์žฅํ•  ์ผ์€ ์—†๋‹ค)
124
  import re as _re
125
+ leak = _re.compile(r"\bE\d{2}-\d{2}[A-C]?\b|\*\*โ–ท|\*\*E\d{2}-")
126
  for c in cards.values():
127
  for b in c.narration:
128
  if leak.search(b.text):
engine/services/__pycache__/paging.cpython-314.pyc CHANGED
Binary files a/engine/services/__pycache__/paging.cpython-314.pyc and b/engine/services/__pycache__/paging.cpython-314.pyc differ
 
engine/services/paging.py CHANGED
@@ -31,6 +31,7 @@ class PageView:
31
  first: Optional[BeatCard] = None # ํŽ˜์ด์ง€ ์ œ๋ชฉยท์—ํ”ผ์†Œ๋“œยท์—ฐ์ถœ ์ปท์˜ ๊ธฐ์ค€
32
  last: Optional[BeatCard] = None # ์„ ํƒ์ง€ยท์ฑ„ํŒ…ยท์ง„ํ–‰ ์œ„์น˜(current_id)์˜ ๊ธฐ์ค€
33
  narration: list[NarrationBlock] = field(default_factory=list)
 
34
  choices: list[Choice] = field(default_factory=list)
35
  can_chat: bool = False
36
  ending: Optional[EndingRule] = None
@@ -139,13 +140,16 @@ class Pager:
139
  first = step.card
140
  part = self.partitions[self.part_of[first.id]]
141
  narration = list(step.narration)
 
142
  while engine.current_id != part[-1]:
143
  step = engine.advance()
144
  if step.ending: # ํŒŒํ‹ฐ์…˜ ๋‚ด ๋งํฌ๋Š” ์ฑ•ํ„ฐ ์•ˆ โ€” ๋ฐฉ์–ด์  ์ฒ˜๋ฆฌ
145
  return PageView(ending=step.ending)
146
  narration += step.narration
 
147
  last = step.card
148
  return PageView(first=first, last=last, narration=narration,
 
149
  choices=list(last.choices), can_chat=last.can_chat)
150
 
151
  # โ”€โ”€ ๋Ÿฐํƒ€์ž„: ๋ณต์› ๋ Œ๋” (๋ถ€์ˆ˜ํšจ๊ณผ ์—†์Œ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -155,6 +159,11 @@ class Pager:
155
  return PageView(ending=engine.ending)
156
  part = self.partitions[self.part_of[engine.current_id]]
157
  cards = [self.repo.get_card(cid) for cid in part]
158
- narration = [b for c in cards for b in engine.visible_narration(c)]
 
 
 
 
159
  return PageView(first=cards[0], last=cards[-1], narration=narration,
 
160
  choices=list(cards[-1].choices), can_chat=cards[-1].can_chat)
 
31
  first: Optional[BeatCard] = None # ํŽ˜์ด์ง€ ์ œ๋ชฉยท์—ํ”ผ์†Œ๋“œยท์—ฐ์ถœ ์ปท์˜ ๊ธฐ์ค€
32
  last: Optional[BeatCard] = None # ์„ ํƒ์ง€ยท์ฑ„ํŒ…ยท์ง„ํ–‰ ์œ„์น˜(current_id)์˜ ๊ธฐ์ค€
33
  narration: list[NarrationBlock] = field(default_factory=list)
34
+ narration_cards: list[str] = field(default_factory=list) # ๋ธ”๋ก๋ณ„ ์†Œ์† ์นด๋“œ (๋ณ‘๋ ฌ)
35
  choices: list[Choice] = field(default_factory=list)
36
  can_chat: bool = False
37
  ending: Optional[EndingRule] = None
 
140
  first = step.card
141
  part = self.partitions[self.part_of[first.id]]
142
  narration = list(step.narration)
143
+ cards = [first.id] * len(step.narration)
144
  while engine.current_id != part[-1]:
145
  step = engine.advance()
146
  if step.ending: # ํŒŒํ‹ฐ์…˜ ๋‚ด ๋งํฌ๋Š” ์ฑ•ํ„ฐ ์•ˆ โ€” ๋ฐฉ์–ด์  ์ฒ˜๋ฆฌ
147
  return PageView(ending=step.ending)
148
  narration += step.narration
149
+ cards += [step.card.id] * len(step.narration)
150
  last = step.card
151
  return PageView(first=first, last=last, narration=narration,
152
+ narration_cards=cards,
153
  choices=list(last.choices), can_chat=last.can_chat)
154
 
155
  # โ”€โ”€ ๋Ÿฐํƒ€์ž„: ๋ณต์› ๋ Œ๋” (๋ถ€์ˆ˜ํšจ๊ณผ ์—†์Œ) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
159
  return PageView(ending=engine.ending)
160
  part = self.partitions[self.part_of[engine.current_id]]
161
  cards = [self.repo.get_card(cid) for cid in part]
162
+ narration, ncards = [], []
163
+ for c in cards:
164
+ vis = engine.visible_narration(c)
165
+ narration += vis
166
+ ncards += [c.id] * len(vis)
167
  return PageView(first=cards[0], last=cards[-1], narration=narration,
168
+ narration_cards=ncards,
169
  choices=list(cards[-1].choices), can_chat=cards[-1].can_chat)
scripts/beatcard_convert.py CHANGED
@@ -208,6 +208,11 @@ class EpisodeParser:
208
  def close_result():
209
  nonlocal cur_result
210
  if cur_result:
 
 
 
 
 
211
  cur_result["text"] = cur_result["text"].strip()
212
  results.append(cur_result)
213
  cur_result = None
@@ -240,8 +245,9 @@ class EpisodeParser:
240
  flush_prose()
241
  close_result()
242
  nm = RE_RESULT_NEXT.search(rest)
 
243
  cur_result = {"axis": AXIS_MAP.get(mark), "hint": hint,
244
- "text": RE_RESULT_NEXT.sub("", rest).strip(),
245
  "next": nm.group(1) if nm else None}
246
  continue
247
 
 
208
  def close_result():
209
  nonlocal cur_result
210
  if cur_result:
211
+ # ์—ฐ์† ์ค„์— ์„ž์—ฌ ๋“  ๋ผ์šฐํŒ… ๋ฉ”๋ชจ(โ†’ E##-โ€ฆ)๋„ ์ ˆ๋‹จ
212
+ m = RE_RESULT_NEXT.search(cur_result["text"])
213
+ if m:
214
+ cur_result["next"] = cur_result["next"] or m.group(1)
215
+ cur_result["text"] = cur_result["text"][:m.start()]
216
  cur_result["text"] = cur_result["text"].strip()
217
  results.append(cur_result)
218
  cur_result = None
 
245
  flush_prose()
246
  close_result()
247
  nm = RE_RESULT_NEXT.search(rest)
248
+ # ํ™”์‚ดํ‘œ๋ถ€ํ„ฐ๋Š” ๋ผ์šฐํŒ… ๋ฉ”๋ชจ("โ†’ E06-10(๋‘ ์  ๋ฐœ๊ฒฌ) ์ดํ›„ โ€ฆ") โ€” ๋ณธ๋ฌธ์—์„œ ์ ˆ๋‹จ
249
  cur_result = {"axis": AXIS_MAP.get(mark), "hint": hint,
250
+ "text": (rest[:nm.start()] if nm else rest).strip(),
251
  "next": nm.group(1) if nm else None}
252
  continue
253
 
web/index.html CHANGED
@@ -354,19 +354,27 @@ function speechBox(name, lines, mode) { // ์‚ฌ๊ฐ ๋งํ’์„ : ์ขŒ ํ™”์ž๋ช… ยท
354
  return h + `</div></div>`;
355
  }
356
 
357
- function stageScreen(g, boxHtml, choices) { // ์ „๋ฉด ์ด๋ฏธ์ง€ + ๋งํ’์„  (+์„ ํƒ์ง€)
358
  return `<div class="vn-stage">` +
359
- `<img class="vn-stage-art" src="${encodeURI(g.portrait)}" alt="${esc(g.spk)}">` +
360
  `<div class="vn-stage-panel">${boxHtml}` +
361
  `<div id="choice-area">${choices}</div></div></div>`;
362
  }
363
 
 
 
 
 
 
 
 
 
364
  function groupNarration(blocks) {
365
- // ์—ฐ์†๋œ ๊ฐ™์€ ํ™”์ž์˜ ๋ฐœํ™”๋Š” ํ•œ ๊ทธ๋ฃน์œผ๋กœ
366
  const groups = [];
367
  for (let i = 0; i < blocks.length;) {
368
  const b = blocks[i];
369
- if (b.type !== 'character_dialogue') { groups.push({block: b}); i++; continue; }
370
  const spk = b.speaker, lines = [];
371
  let portrait = null;
372
  while (i < blocks.length && blocks[i].type === 'character_dialogue'
@@ -375,40 +383,66 @@ function groupNarration(blocks) {
375
  portrait = portrait || blocks[i].portrait;
376
  i++;
377
  }
378
- groups.push({spk, portrait, lines});
379
  }
380
  return groups;
381
  }
382
 
383
- /* ์„œ๋ฒ„ ํŽ˜์ด์ง€ โ†’ ํ™”๋ฉด๋“ค (์Šคํฌ๋กค ์—†๋Š” ๋„˜๊น€).
384
- - ํƒ€์ดํ‹€์€ ์—ํ”ผ์†Œ๋“œ ์ฒซ ํŽ˜์ด์ง€์˜ ํ™”(่ฉฑ) ํ—ค๋”๋งŒ โ€” ์นด๋“œ ์†Œ์ œ๋ชฉ์€ ํ‘œ์‹œํ•˜์ง€ ์•Š๋Š”๋‹ค (์ฑ…์˜ ํ๋ฆ„)
385
- - ์ดˆ์ƒ ์žˆ๋Š” ํ™”์ž์˜ ๋ฐœํ™”๋Š” ์ „๋ถ€ ์ „๋ฉด ์Šคํ…Œ์ด์ง€(์ด๋ฏธ์ง€ + ์‚ฌ๊ฐ ๋งํ’์„ ) ๋‹จ๋… ํ™”๋ฉด
386
- - ์ธํ„ฐ๋ž™์…˜ ํŽ˜์ด์ง€์˜ ๋งˆ์ง€๋ง‰ ์ดˆ์ƒ ๋ฐœํ™”๋ถ€ํ„ฐ๋Š” "๋งํ’์„  ๋‹จ๊ณ„ ์‹œํ€€์Šค" โ€” ๊ฐ™์€ ์ด๋ฏธ์ง€ ์œ„์—์„œ
387
- ๋Œ€์‚ฌ โ†’ (์ดํ›„ ์ง€๋ฌธยท๋Œ€์‚ฌ) โ†’ ๋งˆ์ง€๋ง‰ ๋‹จ๊ณ„์— ์„ ํƒ์ง€. ์›๋ฌธ ์ˆœ์„œ ๋ณด์กด + ๋ฐœํ™”ยท์„ ํƒ์ง€ ์ธ์ ‘
388
- - ๋‚˜๋จธ์ง€ ์ง€๋ฌธยท๋ฐœํ™”๋Š” ์„ธ๊ทธ๋จผํŠธ๋ณ„ ์‹ค์ธก ๊ท ํ˜• ๋ถ„ํ• ๋กœ ํ™”๋ฉด์„ ์ตœ๋Œ€ํ•œ ์ฑ„์šด๋‹ค */
389
- function buildScreens(step, fresh) {
390
  const c = step.card;
391
- let head = '';
392
- if (fresh || c.episode !== lastEpisode)
393
- head += `<div class="ep-header fade">${esc(c.episode_title || c.episode)}</div>`;
394
  lastEpisode = c.episode;
 
 
395
 
 
 
 
 
 
 
 
 
396
  const groups = groupNarration(step.narration);
397
  const choices = choicesHtml(step);
398
- const isPortraitG = (g) => g.lines && g.portrait && !PLAYER_SPEAKERS.has(g.spk);
399
- let seqIdx = -1; // ์ธํ„ฐ๋ž™์…˜ ํŽ˜์ด์ง€: ๋งˆ์ง€๋ง‰ ์ดˆ์ƒ ๋ฐœํ™” = ์‹œํ€€์Šค ์‹œ์ž‘
400
- if (step.can_chat || step.choices.length) {
401
- for (let k = groups.length - 1; k >= 0; k--)
402
- if (isPortraitG(groups[k])) { seqIdx = k; break; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  }
404
 
 
405
  const meas = $('page-measure');
406
- const budget = Math.max((window.innerHeight || 900) - 130, 320); // ์ตœ๋Œ€ํ•œ ๊ฐ€๋“ ์ฑ„์›€
407
  const measure = (html) => { meas.innerHTML = html; return meas.offsetHeight; };
408
-
409
  const screens = [];
410
- let seg = []; // ํ˜„์žฌ ์„ธ๊ทธ๋จผํŠธ์˜ ์œ ๋‹›๋“ค {html, h}
411
- const flushSeg = () => { // ์„ธ๊ทธ๋จผํŠธ๋ฅผ ๊ท ํ˜• ๋ถ„ํ• ํ•ด ํ™”๋ฉด์œผ๋กœ
412
  if (!seg.length) return;
413
  const total = seg.reduce((s, u) => s + u.h, 0);
414
  const k = Math.max(1, Math.ceil(total / budget));
@@ -428,50 +462,52 @@ function buildScreens(step, fresh) {
428
  }
429
  });
430
  if (cur) screens.push(cur);
431
- seg = [];
432
  };
433
 
434
- const upto = seqIdx >= 0 ? seqIdx : groups.length;
435
- for (let k = 0; k < upto; k++) {
436
- const g = groups[k];
437
- if (isPortraitG(g)) { // ์ผ๋ฐ˜ ์ดˆ์ƒ ๋ฐœํ™” โ€” ์ „๋ฉด ์Šคํ…Œ์ด์ง€ 1์žฅ (์„œ์‚ฌ ์œ„์น˜ ์œ ์ง€)
438
- flushSeg();
439
- screens.push(stageScreen(g, speechBox(g.spk, g.lines, 'speech'), ''));
 
 
440
  } else {
441
- const html = g.lines ? charLines(g) : block(g.block);
442
- seg.push({html, h: measure(html)});
443
- }
444
- }
445
-
446
- if (seqIdx < 0) { // ์Šคํ…Œ์ด์ง€ ์‹œํ€€์Šค ์—†์Œ โ€” ์„ ํƒ์ง€๋Š” ๋งˆ์ง€๋ง‰ ํ™”๋ฉด ํ•˜๋‹จ
447
- if (choices) {
448
- const area = `<div id="choice-area">${choices}</div>`;
449
- seg.push({html: area, h: measure(area)});
450
- }
451
- flushSeg();
452
- } else { // ๋งํ’์„  ๋‹จ๊ณ„ ์‹œํ€€์Šค (๊ฐ™์€ ์ด๋ฏธ์ง€, ๋ฐ•์Šค ๋‚ด์šฉ๋งŒ ์ „ํ™˜)
453
- flushSeg();
454
- const sg = groups[seqIdx];
455
- const steps = [{name: sg.spk, lines: sg.lines, mode: 'speech'}];
456
- for (let k = seqIdx + 1; k < groups.length; k++) {
457
- const g = groups[k];
458
- if (g.lines)
459
- steps.push({name: PLAYER_SPEAKERS.has(g.spk) ? '๋‚˜' : g.spk,
460
- lines: g.lines, mode: 'speech'});
461
- else if (g.block.type === 'dialogue_center')
462
- steps.push({name: null, lines: [g.block.text], mode: 'center'});
463
- else
464
- steps.push({name: null, lines: [g.block.text], mode: 'narr'});
465
  }
466
- steps.forEach((s, i) => {
467
- screens.push(stageScreen(sg, speechBox(s.name, s.lines, s.mode),
468
- i === steps.length - 1 ? choices : ''));
469
- });
470
- }
471
  meas.innerHTML = '';
472
 
473
- if (screens.length) screens[0] = head + screens[0]; // ํ™”(่ฉฑ) ํ—ค๋”๋Š” ์ฒซ ํ™”๋ฉด์—
474
  else screens.push(head);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
475
  return screens;
476
  }
477
 
@@ -493,21 +529,10 @@ function setProgress(p, epLabel) {
493
  function showStep(step, fresh) {
494
  STEP = step; pendingStep = null; pendingScreens = [];
495
  if (step.ending) return renderEnding(step.ending);
496
- if (step.image) { // ์—ฐ์ถœ ์ปท = ์ž์ฒด ๋ฒˆํ˜ธ๋ฅผ ๊ฐ€์ง„ ๋…๋ฆฝ ํŽ˜์ด์ง€, ๋ณธ๋ฌธ์€ ๋‹ค์Œ ๋„˜๊น€์—
497
- pushCutPage(step);
498
- pendingStep = {...step, image: null};
499
- return;
500
- }
501
- const screens = buildScreens(step, fresh);
502
- pushEntry(screens[0], step.progress, step.card.episode_title, step.background);
503
- pendingScreens = screens.slice(1); // ๋‚˜๋จธ์ง€๋Š” ํŽ˜์ด์ง€ ๋„˜๊น€์œผ๋กœ
504
- }
505
-
506
- function pushCutPage(step) { // ์ด๋ฏธ์ง€ ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ = ๋ณธ๋ฌธ ๋ฒˆํ˜ธ - 1 (์„œ๋ฒ„ ๊ณ„์•ฝ)
507
- const html = `<div class="page-cut"><img src="${encodeURI(step.image)}" alt="">` +
508
- `<div class="cut-caption">${esc(step.card.title)}</div></div>`;
509
- pushEntry(html, {index: step.progress.index - 1, total: step.progress.total},
510
- step.card.episode_title, step.background);
511
  }
512
 
513
  function pushEntry(html, progress, ep, bg) {
@@ -559,8 +584,8 @@ async function go(dir) {
559
  return;
560
  }
561
  if (pendingScreens.length) { // ๊ฐ™์€ ์„œ๋ฒ„ ํŽ˜์ด์ง€์˜ ๋‹ค์Œ ํ™”๋ฉด
562
- pushEntry(pendingScreens.shift(), STEP.progress,
563
- STEP.card.episode_title, STEP.background);
564
  return;
565
  }
566
  if (pendingStep) { // ์„ ํƒ ๊ฒฐ๊ณผ ํ™•์ธ ํ›„ ๋‹ค์Œ ์žฅ
 
354
  return h + `</div></div>`;
355
  }
356
 
357
+ function stageScreen(art, alt, boxHtml, choices) { // ์ „๋ฉด ์ด๋ฏธ์ง€ + ๋งํ’์„  (+์„ ํƒ์ง€)
358
  return `<div class="vn-stage">` +
359
+ `<img class="vn-stage-art" src="${encodeURI(art)}" alt="${esc(alt)}">` +
360
  `<div class="vn-stage-panel">${boxHtml}` +
361
  `<div id="choice-area">${choices}</div></div></div>`;
362
  }
363
 
364
+ function groupToStep(g) { // ์Šคํ…Œ์ด์ง€ ํƒ‘์Šน ๊ทธ๋ฃน โ†’ ๋งํ’์„  ๋‹จ๊ณ„
365
+ if (g.lines)
366
+ return {mode: 'speech', name: PLAYER_SPEAKERS.has(g.spk) ? '๋‚˜' : g.spk, lines: g.lines};
367
+ if (g.block.type === 'dialogue_center')
368
+ return {mode: 'center', name: null, lines: [g.block.text]};
369
+ return {mode: 'narr', name: null, lines: [g.block.text]};
370
+ }
371
+
372
  function groupNarration(blocks) {
373
+ // ์—ฐ์†๋œ ๊ฐ™์€ ํ™”์ž์˜ ๋ฐœํ™”๋Š” ํ•œ ๊ทธ๋ฃน์œผ๋กœ. card = ์†Œ์† ์นด๋“œ (์Šคํ…Œ์ด์ง€ ํƒ‘์Šน ๊ฒฝ๊ณ„)
374
  const groups = [];
375
  for (let i = 0; i < blocks.length;) {
376
  const b = blocks[i];
377
+ if (b.type !== 'character_dialogue') { groups.push({card: b.card, block: b}); i++; continue; }
378
  const spk = b.speaker, lines = [];
379
  let portrait = null;
380
  while (i < blocks.length && blocks[i].type === 'character_dialogue'
 
383
  portrait = portrait || blocks[i].portrait;
384
  i++;
385
  }
386
+ groups.push({card: b.card, spk, portrait, lines});
387
  }
388
  return groups;
389
  }
390
 
391
+ function epHeader(step, fresh) {
 
 
 
 
 
 
392
  const c = step.card;
393
+ const h = (fresh || c.episode !== lastEpisode)
394
+ ? `<div class="ep-header fade">${esc(c.episode_title || c.episode)}</div>` : '';
 
395
  lastEpisode = c.episode;
396
+ return h;
397
+ }
398
 
399
+ /* ์„œ๋ฒ„ ํŽ˜์ด์ง€ โ†’ ํ™”๋ฉด๋“ค {html, prog} (์Šคํฌ๋กค ์—†๋Š” ๋„˜๊น€).
400
+ - ์ดˆ์ƒ ํ™”์ž(์นด๋ฅด๋ฐ€๋ผยท์•„๋ฒ„์ง€)์˜ ๋ฐœํ™” = ์ „๋ฉด ์Šคํ…Œ์ด์ง€. ๊ฐ™์€ ์นด๋“œ ์•ˆ์˜ ํ›„์† ์ง€๋ฌธยท
401
+ ๋ฌด์ดˆ์ƒ ๋ฐœํ™”ยท์ค‘์•™ ๋Œ€์‚ฌ๋Š” ๊ทธ ์ด๋ฏธ์ง€ ์œ„ ๋งํ’์„  ๋‹จ๊ณ„๋กœ ํƒ‘์Šน (์›๋ฌธ ์ˆœ์„œ ๋ณด์กด)
402
+ - ์ง์ „์˜ ์งง์€ ๋ฆฌ๋“œ์ธ(role=lead_in, ๊ฐ™์€ ์นด๋“œ)์€ ์Šคํ…Œ์ด์ง€์˜ ์ฒซ ๋‹จ๊ณ„(์ง€๋ฌธ ๋ฐ•์Šค)๋กœ
403
+ - ๋งˆ์ง€๋ง‰ ์žฅ๋ฉด์ด ์Šคํ…Œ์ด์ง€๋ฉด ์„ ํƒ์ง€๋Š” ์ตœ์ข… ๋‹จ๊ณ„์—, ์•„๋‹ˆ๋ฉด ํ…์ŠคํŠธ ํ™”๋ฉด ํ•˜๋‹จ์—
404
+ - ํ…์ŠคํŠธ๋Š” ์„ธ๊ทธ๋จผํŠธ๋ณ„ ์‹ค์ธก ๊ท ํ˜• ๋ถ„ํ• ๋กœ ํ™”๋ฉด์„ ์ตœ๋Œ€ํ•œ ์ฑ„์šด๋‹ค */
405
+ function buildScreens(step, fresh) {
406
+ const head = epHeader(step, fresh);
407
  const groups = groupNarration(step.narration);
408
  const choices = choicesHtml(step);
409
+ const isP = (g) => g.lines && g.portrait && !PLAYER_SPEAKERS.has(g.spk);
410
+
411
+ // 1) ์žฅ๋ฉด ์กฐ๋ฆฝ: text(๊ทธ๋ฃน ๋‚˜์—ด) | stage(์ดˆ์ƒ + ๋งํ’์„  ๋‹จ๊ณ„๋“ค)
412
+ const scenes = [];
413
+ for (let i = 0; i < groups.length;) {
414
+ const g = groups[i];
415
+ if (!isP(g)) {
416
+ let last = scenes[scenes.length - 1];
417
+ if (!last || last.type !== 'text') { last = {type: 'text', groups: []}; scenes.push(last); }
418
+ last.groups.push(g); i++;
419
+ continue;
420
+ }
421
+ const steps = [];
422
+ const prev = scenes[scenes.length - 1]; // ๋ฆฌ๋“œ์ธ โ†’ ์ฒซ ๋‹จ๊ณ„๋กœ ์ด๋™
423
+ if (prev && prev.type === 'text' && prev.groups.length) {
424
+ const lg = prev.groups[prev.groups.length - 1];
425
+ if (lg.block && lg.block.role === 'lead_in' && lg.card === g.card) {
426
+ prev.groups.pop();
427
+ if (!prev.groups.length) scenes.pop();
428
+ steps.push({mode: 'narr', name: null, lines: [lg.block.text]});
429
+ }
430
+ }
431
+ steps.push({mode: 'speech', name: g.spk, lines: g.lines});
432
+ i++;
433
+ while (i < groups.length && !isP(groups[i]) && groups[i].card === g.card) {
434
+ steps.push(groupToStep(groups[i])); // ๊ฐ™์€ ์นด๋“œ โ€” ์ด๋ฏธ์ง€์— ํƒ‘์Šน
435
+ i++;
436
+ }
437
+ scenes.push({type: 'stage', g, steps});
438
  }
439
 
440
+ // 2) ๋ Œ๋”: ํ…์ŠคํŠธ ๊ท ํ˜• ๋ถ„ํ•  + ์Šคํ…Œ์ด์ง€ ๋‹จ๊ณ„ ํ™”๋ฉด
441
  const meas = $('page-measure');
442
+ const budget = Math.max((window.innerHeight || 900) - 130, 320);
443
  const measure = (html) => { meas.innerHTML = html; return meas.offsetHeight; };
 
444
  const screens = [];
445
+ const flushSeg = (seg) => {
 
446
  if (!seg.length) return;
447
  const total = seg.reduce((s, u) => s + u.h, 0);
448
  const k = Math.max(1, Math.ceil(total / budget));
 
462
  }
463
  });
464
  if (cur) screens.push(cur);
 
465
  };
466
 
467
+ const lastScene = scenes[scenes.length - 1];
468
+ scenes.forEach((sc) => {
469
+ if (sc.type === 'stage') {
470
+ sc.steps.forEach((s, si) => {
471
+ const withChoices = (sc === lastScene && si === sc.steps.length - 1) ? choices : '';
472
+ screens.push(stageScreen(sc.g.portrait, sc.g.spk,
473
+ speechBox(s.name, s.lines, s.mode), withChoices));
474
+ });
475
  } else {
476
+ const seg = sc.groups.map((g) => {
477
+ const html = g.lines ? charLines(g) : block(g.block);
478
+ return {html, h: measure(html)};
479
+ });
480
+ if (sc === lastScene && choices) {
481
+ const area = `<div id="choice-area">${choices}</div>`;
482
+ seg.push({html: area, h: measure(area)});
483
+ }
484
+ flushSeg(seg);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  }
486
+ });
 
 
 
 
487
  meas.innerHTML = '';
488
 
489
+ if (screens.length) screens[0] = head + screens[0];
490
  else screens.push(head);
491
+ return screens.map((html) => ({html, prog: step.progress}));
492
+ }
493
+
494
+ /* ์—ฐ์ถœ ์ปท(์ด๋ฏธ์ง€ ์นด๋“œ) โ†’ ์บ”๋ฒ„์Šค ํ™”๋ฉด๋“ค: ์บก์…˜ ์ปท โ†’ ์ง€๋ฌธยท๋ฐœํ™” ๋งํ’์„  ๋‹จ๊ณ„ โ†’ ์„ ํƒ์ง€.
495
+ ์ปท ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ = ๋ณธ๋ฌธ ๋ฒˆํ˜ธ - 1 (๊ธฐ์กด ๊ณ„์•ฝ ์œ ์ง€) */
496
+ function buildCutScreens(step, fresh) {
497
+ const head = epHeader(step, fresh);
498
+ const cut = `<div class="page-cut"><img src="${encodeURI(step.image)}" alt="">` +
499
+ `<div class="cut-caption">${esc(step.card.title)}</div></div>`;
500
+ const screens = [{html: head + cut,
501
+ prog: {index: step.progress.index - 1, total: step.progress.total}}];
502
+ const steps = groupNarration(step.narration).map(groupToStep);
503
+ const choices = choicesHtml(step);
504
+ if (!steps.length && choices) steps.push({mode: 'narr', name: null, lines: []});
505
+ steps.forEach((s, i) => {
506
+ const box = s.lines.length ? speechBox(s.name, s.lines, s.mode) : '';
507
+ screens.push({html: stageScreen(step.image, step.card.title, box,
508
+ i === steps.length - 1 ? choices : ''),
509
+ prog: step.progress});
510
+ });
511
  return screens;
512
  }
513
 
 
529
  function showStep(step, fresh) {
530
  STEP = step; pendingStep = null; pendingScreens = [];
531
  if (step.ending) return renderEnding(step.ending);
532
+ const screens = step.image ? buildCutScreens(step, fresh) : buildScreens(step, fresh);
533
+ const first = screens.shift();
534
+ pushEntry(first.html, first.prog, step.card.episode_title, step.background);
535
+ pendingScreens = screens; // ๋‚˜๋จธ์ง€๋Š” ํŽ˜์ด์ง€ ๋„˜๊น€์œผ๋กœ
 
 
 
 
 
 
 
 
 
 
 
536
  }
537
 
538
  function pushEntry(html, progress, ep, bg) {
 
584
  return;
585
  }
586
  if (pendingScreens.length) { // ๊ฐ™์€ ์„œ๋ฒ„ ํŽ˜์ด์ง€์˜ ๋‹ค์Œ ํ™”๋ฉด
587
+ const s = pendingScreens.shift();
588
+ pushEntry(s.html, s.prog, STEP.card.episode_title, STEP.background);
589
  return;
590
  }
591
  if (pendingStep) { // ์„ ํƒ ๊ฒฐ๊ณผ ํ™•์ธ ํ›„ ๋‹ค์Œ ์žฅ