← 기술 문서

스토리 MOD

원작 스테이지에 이벤트를 추가합니다. 언제 발생하는지, 무엇이 충족되어야 하는지, 무엇을 하는지, 어떤 새 대사를 말하는지 적습니다. 원작 이벤트 슬롯을 차지하거나 원작 이벤트를 건드리지 않고도 캐릭터를 합류시키거나 남게 하고 스토리 분기를 바꿀 수 있습니다.

업데이트 2026-11-03

스토리 MOD는 원작 스테이지에 이벤트를 추가합니다. 특정 턴에, 누군가 격추되었을 때, 두 캐릭터가 싸웠을 때, 설득을 골랐을 때 등에 여러분이 쓴 대사를 재생하거나, 캐릭터를 합류 또는 이탈시키거나, 스토리 변수를 설정해 이후 원작 스토리가 다른 분기로 가게 합니다. 이벤트가 언제 발생할지는 게임이 판단합니다. 여러분은 조건과 일어날 일만 쓰면 되고, 원작 이벤트 테이블을 알 필요가 없습니다. 원작 이벤트는 바뀌지도, 밀려나지도 않습니다.

패키지 설치와 관리는 MOD 패키지: 설치와 제작에 있습니다. 버전 0.5.3 이상이 필요합니다.

완전한 예제는 “마스터 아시아 생존”이며, 소스는 config/recomp/mods/master-asia에 있습니다. 독립군 루트의 “사투! 란타오섬 대결전 후편”과 OZ 루트의 “절망과 슬픔의 주먹 후편”에서, 도몬이 전투 중에 마스터 아시아와 대화한 뒤에는 그를 설득할 수 있습니다. 격추 수가 50 이상이면 마스터 아시아는 철수하고 스테이지가 끝난 뒤 합류합니다.

패키지 구성

yourname.story/
  mod.json
  story/
    <any name>.json   스토리 스크립트, 개수 제한 없음
  data/
    pilots.json       필요하면 함께 바꿀 수치 (수치 MOD 참고)

패키지를 묶을 때 (tools/mods/pack.py) story/의 스토리 스크립트는 게임이 읽는 형식으로 컴파일됩니다. 오류가 있으면 해당 이벤트와 항목을 알려 줍니다.

이벤트 쓰기

{
  "schema": "srw64.story.v1",
  "events": [
    {
      "id": "persuade",
      "scene": 129,
      "when": { "persuade": { "by": 4, "target": 18 } },
      "if": { "vars": { "131": 0 }, "kills": { "4": 50 } },
      "do": [
        { "say": 4, "text": { "ja": "「師匠!」", "zh-Hans": "“师父!”", "en": "“Master!”" } },
        { "say": 18, "mode": 2, "text": { "ja": "「……」", "zh-Hans": "“……”", "en": "“...”" } },
        { "leave": 18 },
        { "set": { "131": 3 } }
      ]
    },
    {
      "id": "joins",
      "scene": 129,
      "when": { "after": "end" },
      "if": { "fired": ["persuade"] },
      "do": [ { "join": { "pilot": 18, "unit": 27 } } ]
    }
  ]
}

각 이벤트에는 다음이 있습니다.

필드 의미
id 이벤트 이름. 패키지 안에서 고유해야 하며, 다른 이벤트가 이 이름으로 참조합니다
scene 장면 번호로 지정하는 스테이지 (0–255). 모든 스테이지의 장면 번호는 장면 표에 있습니다
when 발생 시점 (아래 참고)
if 선택: 추가로 충족되어야 하는 조건
do 하는 일, 순서대로
repeat 선택: true면 다시 발생할 수 있습니다
set_flags 선택: 이벤트가 실행되면 설정되는 플래그, 이후 스테이지용
note 선택: 자유 메모. 게임은 읽지 않습니다

기본적으로 각 이벤트는 스테이지당 한 번 발생합니다. 게임이 중단 세이브까지 포함해 이를 기록하므로, 원작 이벤트처럼 재발생을 막으려고 변수를 설정할 필요가 없습니다.

파일럿과 기체 번호는 수치 MOD의 파일럿·기체 표에 있습니다.

when

쓰는 법 의미
{"turn": {"turn": 3, "side": "player"}} 3턴부터, 해당 진영 페이즈 시작 시 (player, enemy, third)
{"defeated": 18} 그 캐릭터의 기체가 격추됨 (이탈은 해당하지 않음)
{"hp_below": {"who": 18, "percent": 30}} 그 캐릭터의 HP가 30% 아래로 떨어짐
{"battle": {"between": [4, 18], "moment": "after"}} 두 캐릭터가 싸운 뒤 (before는 전투 전, 0은 누구나)
{"enemies_gone": true} 적이 모두 사라짐. {"by_turn": 5}를 붙이면 5턴 이전으로 제한
{"remaining": {"side": "enemy", "at_most": 3}} 적 (또는 third 제3세력) 기체가 3기 이하로 남음
{"persuade": {"by": 4, "target": 18}} 4가 18 옆에 있으면 커맨드 메뉴에 설득이 나오고, 그것을 고르면 이벤트가 발생
{"after": "opening"}, "setup", "end" 스테이지의 원작 오프닝, 배치, 종료 이벤트 직후

원작 이벤트가 같은 시점에 예정되어 있으면 원작 이벤트가 먼저 실행되고, 여러분의 이벤트는 다음 확인 때 검토됩니다. 오프닝, 배치, 종료 뒤의 이벤트는 repeat할 수 없습니다.

if

쓰는 법 의미
"vars": {"131": 0} 스토리 변수 (0–199)가 이 값 (0–3)임. 원작 스토리는 분기를 이 변수들에 보관합니다. 예를 들어 장면 129에서 131은 “도몬이 전투 중에 마스터 아시아와 대화했는가” (대화했으면 0)입니다
"kills": {"4": 50} 이 아군 캐릭터의 격추 수가 이 값 이상
"kills_below": {"4": 50} 격추 수가 이 값 미만
"fired": ["persuade"] 패키지의 이 이벤트들이 이 스테이지에서 발생했음
"not_fired": [...] 이 스테이지에서 아직 발생하지 않았음
"roster": {"pilots": [18], "units": [27]} 이 캐릭터와 기체가 아군 명단에 있음 (출격 여부와 무관)
"not_roster": {...} 그중 아무도 명단에 없음
"flags": ["persuaded"] 패키지의 이 플래그들이 설정되어 있음
"not_flags": [...] 그중 아무것도 설정되어 있지 않음

캐릭터가 아군 명단에 없으면 kills와 kills_below 모두 충족되지 않습니다.

성공 대사와 실패 대사. 같은 시점에 서로 배타적인 조건을 가진 이벤트 두 개를 쓰면, 게임은 충족되는 쪽을 고릅니다. 예제에서는 격추 수가 충분하면 성공, 부족하면 거절 장면이 재생됩니다.

{ "id": "persuade", "when": { "persuade": { "by": 4, "target": 18 } },
  "if": { "vars": { "131": 0 }, "kills": { "4": 50 }, "not_fired": ["refused"] }, "do": [] },
{ "id": "refused", "when": { "persuade": { "by": 4, "target": 18 } },
  "if": { "vars": { "131": 0 }, "kills_below": { "4": 50 } }, "do": [] }

거절은 한 번만 발생하고 성공에는 not_fired: refused가 필요하므로, 거절된 뒤에는 그 스테이지에서 둘 다 충족되지 않습니다. 설득은 메뉴에서 사라지고 스토리는 원래대로 진행됩니다. 플레이어가 다시 시도할 수 있게 하려면 거절에 "repeat": true를 주고 성공 쪽의 not_fired를 빼세요.

스테이지를 넘어서: 플래그

fired는 한 스테이지 안에서만 유효하며 다음 스테이지에서는 지워집니다. 이후 스테이지에 무슨 일이 있었는지 알리려면 이벤트에 "set_flags": ["name"]을 쓰세요 (영문 소문자, 숫자, _, -, 32자 이내). 플래그는 이벤트가 실행되면 설정됩니다. 플래그는 여러분의 패키지에만 속하며 원작 스토리 변수와는 관계가 없습니다. 중단 세이브와 인터미션 세이브를 따라가며, 새 게임으로만 지워집니다.

{ "id": "persuade", "scene": 129, "set_flags": ["persuaded"], "when": { "persuade": { "by": 4, "target": 18 } }, "do": [] },
{ "id": "comes-along", "scene": 81, "when": { "after": "opening" },
  "if": { "roster": { "pilots": [18] }, "flags": ["persuaded"] },
  "do": [ { "say": 18, "text": { "ja": "「ドモン、遅れを取るでないぞ!」", "en": "“Domon, don't fall behind!”" } } ] }

명단도 함께 확인하는 이유는 원작 스토리나 다른 이벤트가 나중에 그를 명단에서 빼낼 수 있기 때문입니다. 플래그만 보면 그가 없을 때도 대사를 말하게 됩니다.

do

동작 의미
{"say": 18, "text": {...}, "mode": 0} 대사 한 줄 (아래 참고)
{"join": {"pilot": 18, "unit": 27}} 캐릭터와 기체가 아군에 합류
{"remove": {"pilot": 18, "unit": 27}} 명단에서 제외 (기체가 먼저 맵을 떠나 있어야 함)
{"leave": 18} 캐릭터의 기체가 맵에서 이탈 (격추가 아님)
{"leave_group": 11}, {"deploy": 11} 스테이지의 원작 출격 그룹 하나가 이탈하거나 등장
{"set": {"131": 3}} 스토리 변수 설정
{"if": {"var": 128, "is": 2}, "then": [...], "else": [...]} 스토리 변수에 따른 분기
{"focus": 18} 카메라를 캐릭터에게 이동
{"bgm": 11}, {"sound": 124} 음악 변경, 효과음 재생
{"fade": "out"}, {"fade": "in"}, {"wait": 30} 페이드 아웃, 페이드 인, 대기 (프레임)
{"close": "window"} 대화창 닫기
{"op": "3D35", "args": [2335, 1]} 원작 스크립트 명령을 그대로 사용. 스테이지 스크립트 레퍼런스 참고. 원작 대사는 {"op": "3D40", "args": [43215]}처럼 직접 쓸 수 있습니다

대사. text에는 ja가 반드시 있어야 합니다. 빠진 다른 언어는 일본어로 표시됩니다 (번체 중국어는 간체 중국어로 대체). 한 줄은 200자까지이며 리더가 알아서 페이지를 나누므로 <STOP>은 쓰지 마세요. 원작 대사처럼 따옴표는 직접 쓰세요. 일본어와 번체 중국어는 「」, 간체 중국어와 영어는 “ ”입니다. mode는 원작 대사가 그 줄을 표시하는 방식입니다. 맵에서는 보통 아군이 0, 적이 2이고, 월드맵에서는 0과 1이 위아래를 번갈아 씁니다.

합류 시 레벨은 원작 규칙, 즉 스테이지의 기준 레벨 (아군 상위 15명의 평균에서 산출)을 따릅니다. "level": {"offset": 3}은 거기에 더하거나 빼고, "level": {"like": 4}는 합류 시점의 캐릭터 4의 레벨을 줍니다. SP, 능력, 특기, 정신기는 레벨에 따라 원작에서 가져옵니다. 기본값을 바꾸려면 data/pilots.json을 쓰세요 (적 캐릭터는 SP나 정신기가 없는 경우가 많아, 예제에서는 SP를 추가합니다).

알아 둘 점

  • 원작 이벤트는 원작 변수만 읽습니다. 원작 이벤트를 막거나 다른 부분을 재생하게 하려면 그 이벤트가 확인하는 변수를 바꾸세요. 예제에서는 설득이 131 = 3을 설정하고, 원작 종료 이벤트는 131이 1 또는 2일 때만 마스터 아시아의 작별 장면을 재생합니다.
  • 기체 이탈 (leave)은 원작의 격추 이벤트를 발생시키지 않습니다. 원작이 격추 후에 하는 일 (예제에서는 웡의 등장과 승리 조건 개방)은 여러분의 이벤트가 직접 해야 합니다.
  • 스토리 전개를 다른 스테이지로 옮기기 전에, 그 스테이지의 원작 이벤트를 읽어 보세요: 대화가 어느 변수를 설정하는지, 격추 후 무엇이 이어지는지, 종료 이벤트가 어느 변수에 따라 어느 부분을 재생하는지. 예제의 OZ 루트 이벤트는 장면 137의 원작 이벤트를 바탕으로 고친 것입니다.
  • 플래그는 세이브 파일 자체가 아니라 세이브 옆에 보관됩니다 (saves/ 폴더의 story-state/). 세이브를 다른 에뮬레이터로 옮기거나 그 폴더를 지우면 플래그가 사라져 스테이지를 넘나드는 이벤트가 발생하지 않습니다. 한 스테이지 안의 이벤트는 영향을 받지 않습니다.
  • 스토리 MOD는 사용자 캠페인 스테이지 (campaign/)에서는 작동하지 않습니다.

검사와 묶기

python3 tools/mods/story.py compile story/master.json -o out.json
python3 tools/mods/pack.py yourname.story -o build/mods

첫 번째 명령은 스토리 스크립트 하나를 컴파일해 각 이벤트가 어떤 명령이 되는지 보여 줍니다. 두 번째 명령은 패키지를 묶고 게임의 규칙으로 검사합니다. 게임이 읽을 수 없는 패키지는 로그에 SRW64_STORY_MOD_PROBLEM으로 보고됩니다.

저장소의 docs/guide/story-mods/ 폴더에 전체 형식과 장면 표가 있습니다. 이것을 AI 어시스턴트에게 건네 여러분의 아이디어로 스토리 스크립트를 쓰게 할 수 있습니다.