DSL 콘텐츠 JSON 레퍼런스
게임 콘텐츠(DSL)의 JSON 키·밸류 설명서. 어드민 폼에 없는 필드를 Advanced JSON으로 직접 편집할 때, 값의 의미·허용범위·함정을 확인할 때 참고한다.
- 모든 DSL은
dsl 테이블에 키별 jsonb로 저장. 서버는 loadAllDsl(60초 캐시)로 읽고 saveDslSnapshot으로 쓴다. raw jsonb SQL로 직접 쓰지 말 것(정규화·검증 우회 → 손상 위험).
- 저장 시 정규화(값 표준형 강제·폐기 필드 제거)와 검증(참조 무결성·타입, 오류면 저장 차단)이 돈다.
- 어드민 편집 2모드: 폼 에디터(구조화 입력, 단 폼이 렌더하는 필드만 보임) / Advanced JSON(상단 토글, 현재 키의 원본 JSON 전체를 Monaco로 편집 — 폼에 안 뜨는 필드도 여기선 다 보이고 삭제 가능).
규칙
op:"mul" 값은 퍼센트 포인트다. {stat:"atk",op:"mul",value:10} = 공격력 +10% (0.1이 아니라 10).
- 참조 id는 실존해야 한다. 없는 id 참조 → 검증 오류거나, 조건이 영영 충족 안 되는 유령 참조.
- 폼에 없는 필드도 저장 시 유지된다(폼은 로드된 엔트리를 패치만 함). 확인·제거는 Advanced JSON에서.
대표 함정
- 존/맵/서비스
unlockConditions의 activeQuestIds는 "진행 중"(완료하면 다시 잠김), completedQuestIds는 "완료"(영구). 헷갈리면 지역이 사라진다.
- 패시브 스탯 효과는 전투 시작 시 풀 HP로 1회만 평가 →
hp<X 조건은 스탯 보정에선 죽은 조건.
- 공격형 메카닉(흡혈·처형·방무·독·기절·둔화)은 physical 공격에서만 발동 → 마법사는 못 씀.
- 이벤트
set_flag는 세션 한정, 영속 게이팅은 set_progress_flag.
requiredCompletedAdventureZoneIds는 location_entered 트리거에서 오작동(현재 위치와 비교) → requiredFlags/requiredActiveQuestIds 사용.
| 키 | 내용 | 최상위 형태 |
classes | 직업 | {defaultClassId, classes:[]} |
skills / passives | 액티브 / 패시브 스킬 | {skills:[]} / {passiveSkills:[]} |
effects / buffs | 전투 이펙트 프리셋 / 버프 | {effects:[]} / {buffs:[]} |
monsters / monsterArchetypes | 몬스터 / 아키타입 | {monsters:[]} / {archetypes:[]} |
growth / formulas | 성장 곡선 / 전투 수식·AI | 객체 |
items + 절차/레어 8키 | 아이템·장비 생성 | {items:[]} 등 |
companionGacha / houses / npcs | 뽑기 / 가문 / NPC | 각각 |
quests | 의뢰 | {board, quests:[]} |
worldRegions / adventureZones | 지역 / 모험 존 | {regions:[]} / {zones:[]} |
gameMaps / serviceZones | 맵 / 서비스 존 | {maps:[]} / {zones:[]} |
eventScripts / eventTriggers | 이벤트 / 트리거 | {scripts:[]} / {triggers:[]} |
storyActors / sharedBackgrounds / audio | 배우·배경·오디오 자산 | 각각 |
{ "defaultClassId":"warrior", "classes":[ { /* ClassEntry */ } ] }
| Field | Type | 의미 | 허용값/비고 |
defaultClassId | string | 신규 캐릭터 기본 직업 | classes[].id 중 하나 |
id / name | string | 직업 id / 이름 | 필수, id 유니크 |
maxLevel | number | 이 직업 최대 레벨 | 정수 ≥1 |
unlock.type | enum | 해금 유형 | base(상시) | mastery_combo(마스터 필수) | event(이벤트 완료 필수) |
unlock.requiredMasteryJobIds | string[] | 승급에 마스터해야 하는 직업들 | mastery_combo인데 비면 오류. 각 id 실재 class |
unlock.completedEventIds | string[] | 이 직업의 자격 이벤트(전부 완료해야 열림) | event인데 비면 오류. 각 id 실재 eventScript |
statMultiplier | object | 6 베이스 스탯 배율(1.0=기준) | 키 str/vit/int/men/agi/luk만. 누락은 1. 그 외 키 오류 |
skillProgression[] | array | 레벨별 습득 스킬 | {skillId, learnAt≥1, type:"action"|"passive"} |
equipmentRules | object | 장비 규칙 | allowedWeaponTypes, allowedArmorTypes, allowShield, allowDualWield |
visual | object | 프로필/idle 에셋(성별 분기) | profile,idle,gender.male/female.* |
함정
unlock.type을 base로 둬도 조건 배열에 값이 있으면 정규화가 조건형으로 강제 승격한다(마스터 조건이 있으면 mastery_combo, 이벤트만 있으면 event). 기본직이면 두 배열을 모두 비울 것.
type은 「무엇이 필수인가」만 정하고, 판정은 채워진 조건 전부의 AND다. 두 배열을 같이 채우면 둘 다 요구한다 — 「자격 이벤트를 끝내고 + 전사·사제를 마스터해야 성기사」가 그 조합이다(type: mastery_combo + completedEventIds).
- ⛔자격 이벤트에
grant_job_unlock 스텝을 넣지 말 것. 그 스텝은 jobs.unlockedSources에 직접 꽂혀 unlock 조건을 통째로 건너뛴다(마스터를 안 해도 열린다). 자격 이벤트는 그냥 끝나기만 하면 되고, 완료 사실은 progress.completedEventIds에 자동 기록되어 여기 조건이 읽는다. grant_job_unlock은 「조건 없이 즉시 준다」가 진짜 의도일 때만.
- 해금 시점은 다음 프로필 읽기다(
migrateProfile → recomputeUnlockedJobs). 이벤트 종료 직후 화면 전환에서 성당 전직 목록에 오른다.
statMultiplier는 6 베이스 스탯만. atk/def 등 전투 스탯 키 넣으면 오류.
- 자동 마스터특전(
masterSkillId·인라인 masterSkill) 개념 폐기 — 저장 시 삭제. 마스터한 직업은 전직 시 스킬 1개를 영구 계승(직업당 1회).
열거값: effectType=physical|magical|heal|barrier|taunt|revive|buff · attackType=melee|ranged|magical · castType(속도 기준)=physical(민첩)|magical(정신) · outputStat(보호막·회복의 출력)=atk|matk · targetType=enemy_single|ally_single|self|enemy_all|ally_all|ally_all_except_self · targetState=alive|dead|any · magicElement=fire|water|wind|lightning|earth|light|dark
| Field | Type | 의미 | 허용값/비고 |
id/name | string | id/이름 | 필수 |
effectType | enum | 효과 유형(파이프라인·마법숙련 분류) | 미지정 시 physical |
attackType/castType | enum | 공격 방향성 / 시전 계열 | castType은 공식(캐스트·쿨다운) 선택 |
targetType | enum | 대상 | 필수. revive=ally_*만, buff=self/ally_*만 |
power | number | 위력 | ≥0 |
cooldownMs/preloadTimeMs | number | 쿨다운/최초지연(ms) | ≥0 |
airborneMs | number | 체공(용기사 도약) — 이 스킬을 쓴 직후 이 시간(ms)만큼 타겟 불가 + 피해 무효 | ≥0 |
castEffectMs | number | 시전 연출 길이(ms). 비우면 formulas.timing.castEffectMs → 기본값. 전투 계산과 무관 | ≥0 |
showCastEffect | boolean | 시전 연출 재생 여부. 궁극기는 컷인이 대신하므로 재생되지 않는다 | true/false |
hitCount | number | 타격 횟수 | 정수 ≥1 |
magicElement | enum | 속성. 속성 상성·피격음·궁극기 컷인 색의 기준이며 물리 스킬에도 저작한다. 마법숙련 분류는 magical 스킬일 때만(물리는 무기숙련이 오른다) | magical(무 포함) · physical(무 없음) |
outputStat | enum | 보호막 두께·회복량을 어느 공격력으로 계산할지(atk|matk, 기본 matk). 공격 스킬은 effectType 이 이 축을 겸한다. ⛔castType 은 속도 축이라 출력과 무관 | barrier/heal에서만 |
lifeSteal | object | 흡혈 {ratio, basis} | physical/magical에서만 |
chanceDamage | object | 확률 피해 | physical/magical에서만 |
armorPenetration | object | 방어관통 {ignoreRatio} | 물리=def, 마법=mres 무시 |
execute | object | 처형 {hpBelowRatio, bonusMultiplier} | 대상 HP비율 ≤ 시 배율 |
cover | object | 엄호 {allyHpBelow} | 아군 HP≤ 시 단일공격 대신 받음 |
tauntDurationMs/reviveHpRatio/trueStrike | num/num/bool | 도발/부활회복비율/필중 | revive는 reviveHpRatio>0 필수 |
buffEffects+buffDurationMs | array+num | 인라인 버프 | 키: atk/def/matk/mres/agi/men/crit/tauntWeight/evasionChance |
castEffectId/attackEffectId/hitEffectId/projectileEffectId | string | 연출 프리셋 id | 실재 effects id |
*SfxTrackId | string | 효과음 트랙 | 실재 audio.sfxTracks id |
hidden+houseId+usableFromLevel | bool+.. | 혈통 히든 스킬(목록 비노출, 전투 자동발동) | |
함정
lifeSteal/chanceDamage는 physical/magical에서만 허용. armorPenetration/execute는 데미지 스킬(physical/magical)에만 적용(heal/buff에 붙여도 무효).
- 무기/패시브가 주는 dot/stun/slow는 physical 공격에서만 발동.
- 클라
DslLoader.normalizeSkills는 필드 화이트리스트 복사 — 목록에 없는 신규 필드는 런타임서 조용히 사라짐(새 필드 추가 시 클라도 갱신).
{ "passiveSkills":[ { "id":"", "name":"", "condition":"", "effects":[ ... ] } ] }
effects[]는 두 형태:
(A) 스탯 보정 (기본; kind 미지정/statMod/stat)
{stat, op:"add"|"mul"|"set", value, condition?} — 스탯: str/vit/int/men/agi/luk/hp/atk/def/matk/mres/crit. op:"mul"의 value는 퍼센트 포인트(10 = +10%). 조건식 변수: actor,stats,level,hp,weaponType,random().
(B) 전투 메카닉 (kind:"mechanic", mechanic 객체)
| mechanic.kind | 필드 | 의미 |
lifeSteal | lifeStealRatio | 흡혈 비율 — 물리 스킬에만 붙는다. |
lifeCycle | healRatio | 마법 흡혈 — 마법 스킬로 가한 피해의 이 비율만큼 회복. "모든 공격 흡혈"은 lifeSteal과 이걸 둘 다 넣어야 한다. |
armorPenetration | ignoreRatio | 방어/마방 무시 |
execute | hpBelowRatio, executeBonusMultiplier | 처형 배율 |
chanceDamage | chanceDamageChance/SuccessMultiplier/FailureMultiplier | 확률 피해 |
dot | dotTickDamage, dotTickIntervalMs(기본1000), dotDurationMs | 지속 피해 |
stun / slow | stunDurationMs / slowAgiPercent,slowDurationMs | 기절 / 둔화 |
trueStrike | (없음) | 필중 |
guard | guardRatio | 피해 경감(×(1-guardRatio)) |
reflect | procChance, reflectRatio | 확률 반사 |
regen | procChance, healRatio | 피격 시 확률 회복(받은피해×healRatio) |
resist | resistChance | 디버프 확률 무효 |
counterOnHit / counterOnEvade | procChance?, counterMultiplier | 피격/회피 시 반격 |
damageShare | shareRatio | 아군이 받을 피해의 shareRatio 만큼을 보유자가 대신 받는다(엔진 상한 0.5). 지속시간 버프로 켠다. |
elementAffinity | element, damageTakenMult | 속성 상성 — 그 속성의 공격을 맞을 때 피해에 배수를 곱한다(0.1 = 90% 무효, 1.5 = 1.5배). 판정 기준은 스킬의 magicElement 다 — 물리·마법 구분 없이 그 속성이면 걸린다(속성 칸이 빈 스킬은 해당 없음). 속성 하나당 메카닉 하나 — 여러 속성은 effects 에 여러 개 넣는다. |
(C) 스킬 onHitDebuff — 명중 시 대상에게 남기는 표식
physical/magical 스킬에만 붙는다. durationMs(필수)와 아래 중 최소 1개가 있어야 성립한다.
여기 없는 필드는 저장 때 조용히 사라진다 — 서버 정규화·검증기·관리자 저장 세 곳이 각각 화이트리스트다.
| 필드 | 의미 | 상한 |
slowAgiPercent | 민첩을 이 %로 곱한다(75 = 25% 감소). 레거시 표기. | 1~99 |
damageTakenPercent | 받는 피해를 이 %로 곱한다(120 = 20% 증가). | >100 |
statDebuffs | [{stat, mulPercent}] 임의 스탯 증감. 통일 표기(-50 = ×0.5). | mulPercent > -100 |
dotPercentOfMaxHp | 틱마다 대상 최대HP의 이 %. | 틱당 6% / 절대 100 |
dotPercentOfDamage | 틱마다 그 타격 피해의 이 %(레벨·스탯에 자동 비례 — 권장). | — |
dotTickDamage | 틱당 고정 피해(레벨이 올라도 그대로). | — |
dotMaxTicks / dotPerAction | 총 틱 수. 행동 1회당 1틱으로 센다(서버가 가상시계라 "N초당"은 안 지켜진다). ⚠️ "행동"은 전투 전체의 행동이다 — 대상 본인의 행동이 아니다. 참가자가 많으면 그만큼 빨리 닳는다. | — |
dotStackable | 다시 걸면 갱신이 아니라 쌓인다. | 5스택 |
stunDurationMs | 대상의 새 행동 시작을 막는다(진행 중이던 캐스트는 마무리된다). | 상한 없음 — 궁극기 외 200ms 기준 |
charmDurationMs | 매혹 — 대상의 적/아군 판정이 뒤집혀 자기 편을 공격한다. 때릴 아군이 없으면(혼자인 편) 매혹을 무시하고 원래 적을 노린다(자해 방지). | 상한 없음 — 데이터로 관리 |
visualPresetId | 상태 표시. debuff_hunters_mark / debuff_poison / debuff_burn / debuff_shadow / debuff_bleed / debuff_charm / debuff_stun | — |
함정 (매우 중요)
- 패시브 스탯 효과는 전투 시작 시 풀 HP로 1회만 평가 →
hp<X 류 조건은 스탯 보정에선 죽은 조건.
op:"mul" value는 배율이 아니라 퍼센트 포인트(1.1 원하면 10).
- 공격형 메카닉(lifeSteal/armorPenetration/execute/chanceDamage/dot/stun/slow/trueStrike)은 시전자 physical 공격에서만 발동 → 마법사는 실질 사용 불가. 예외는
lifeCycle(마법 흡혈) 하나로, 이건 magical 에서만 돈다.
- 방어형 메카닉(guard/reflect/regen/resist/counterOnHit/counterOnEvade)은 보유자가 맞을 때 계열 무관 동작. 단
damageShare만은 아군이 맞을 때 발동하고, elementAffinity는 속성이 있는 마법 공격에만 반응한다.
lifeSteal·lifeCycle 둘 다 스킬 자체 흡혈(skill.lifeSteal)이 있으면 얹히지 않는다 — 스킬 쪽이 이긴다. 「흡혈 30% 스킬」은 「모든 공격 흡혈 50%」 패시브를 들어도 30% 그대로다.
regen의 회복 필드명은 healRatio(regenRatio 아님).
| Field | Type | 의미 | 허용값 |
id/name | string | id/이름 | 필수 |
kind | enum | 종류 | preset | sprite |
presetId/assetUrl | string | 프리셋 id / 스프라이트 URL | preset이면 presetId(또는 layers/placements) 필수 |
anchor | enum | 기준점 | actor|primary_target|all_targets|screen |
durationMs | number | 지속 | ≥1 |
blendMode | enum | 블렌드 | normal|add|multiply|screen |
actionColor/accentColor/critColor/castColor/castAccentColor | string | 팔레트 색(스킬이 색 지정) | hex |
placements/layers | obj/array | 단계별 프리셋 / 다층 레이어 | 키 cast|action|impact|finisher |
camera/reaction/travel/physicalMotion | object | 카메라·피격반응·이동·물리궤적 | |
함정
- 도형 테두리선/얇은 line 금지 — 소프트 글로우 텍스처로(프로젝트 규칙).
presetId(단일)와 layers/placements(다층)는 별개 경로.
{ "buffs":[ { "id":"", "name":"", "durationMs":5000, "effects":[ {stat,op,value} ], "visualPresetId":"" } ] }
effects 키 제한: atk/def/matk/mres/agi/men/crit/tauntWeight/evasionChance (베이스 스탯·hp 불가). op:"mul"은 퍼센트 포인트.
{ "teamSizeMin":1, "teamSizeMax":4,
"monsters":[ { "id":"", "name":"", "monsterType":"fighter", "growth":{...}, "sizeTier":"medium", "skills":[], "drops":[{"itemId":"","chance":0}] } ] }
| Field | 의미 | 허용값 |
monsterType | 아키타입(능력치·보상 기준) | tank|fighter|rogue|mage (기본 fighter) |
growth | 아키타입 오버라이드 {budgetScale>0, statRatio} | statRatio 6스탯, 최소 1개>0 |
sizeTier | 크기 | small|medium|large|boss |
skills/drops | 스킬 id / 드롭 [{itemId,chance≥0}] — 한 줄에 itemId 대신 gold(금액)를 넣으면 골드 드롭 | |
monsterArchetypes: {archetypes:[{id:tank|fighter|rogue|mage, growth{budgetScale,statRatio}, rewards{exp,jobExp,gold:{base,perLevel}}}]}
함정
- 몬스터에
level/stats/gold를 넣어도 저장 시 삭제 — 배틀존 레벨 + 아키타입으로 런타임 산출. - 배틀존 편성: 기본 풀 랜덤, 고급은
useCustomComposition/monsterComposition로 수량 고정. - 적 NPC(
enemyNpcIds)는 레벨로 강해지지 않는다(스토리 NPC 스탯은 레벨을 안 씀). 노브는 둘: enemyNpcBudgetScale(적는 값이 곧 배율 — 전 스탯), enemyNpcHp(절대 최대 HP — HP만, 비우면 vit로 계산). 동료로서의 그 NPC엔 영향 없음. - 이 둘은 존 기본값이고, NPC마다 다르게 주려면
enemyNpcOverrides = { npcId: { budgetScale?, hp? } }. 우선순위 NPC별 > 존 기본값 > NPC growth이며 배율·HP는 서로 독립(한쪽만 NPC별로 줘도 된다).
| Field | 의미 | 기본값 |
maxLevel | 캐릭터 최대 레벨 | 99 |
charExpTable | 레벨별 요구 경험치(인덱스=level-1) | 없으면 floor(100*level^1.5) |
levelDiffExpPct | 레벨차당 경험치 증감% | 10 |
statGainWeights | 레벨업 스탯 증가값(0~5)→가중치 | {"0":18,"1":16,"2":8,"3":4,"4":2,"5":1} |
initialStatPerStat | 생성 시 각 베이스 스탯 초기값 | 5 |
함정
growth는 자유 객체라 오타 키가 검증에 안 걸린다 — 키 이름 정확히.
| Field | 의미 | 비고 |
damage / magicDamage | 물리 / 마법 피해 수식(문자열) | 필수 |
evasionChance | 회피 확률 수식 | 0~1 clamp |
castTimeByType / cooldownByType | 계열별 캐스트/쿨다운 수식 | 키는 physical/magical만 |
combatAi | 전투 스킬 선택 AI 튜닝(아래) | 없으면 코드 기본값 |
useRealTimeTimeline | ⏱ 실시간 타임라인. 켜면 도트/HoT 틱과 버프 만료가 행동과 무관하게 자기 시각에 발동하고, 스킬 연출 길이만큼 액터가 다음 행동을 미룬다(연출 점유). 끄면 예전대로 "행동이 일어날 때만" 처리. | 기본 false |
playbackTimeScale | 클라 재생 속도 배율. 서버 타임라인 ms 에 곱한다(1 = 서버 시각 그대로). useRealTimeTimeline 이 켜져 있을 때만 쓰인다. | 기본 1 |
occupancyScale | 연출 점유 배율. 점유는 effects 의 durationMs(시전+공격/투사체+타격) 합에서 산출한다. 스킬에 occupancyMs 를 직접 적으면 그 값이 이긴다. | 기본 1 |
serializeActions | 전역 무대 점유. 한 번에 한 액션만 재생되도록 서버가 순서를 벌린다. 끄면 같은 시각 행동이 화면에서 겹쳐 큰 숫자가 한꺼번에 뜬다(실기로 확인). useRealTimeTimeline 이 켜져 있을 때만 의미가 있다. | 기본 true |
minScheduledStepMs | 시간 사건 최소 간격(폭주 방어선). | 기본 50 |
⏱ 실시간 타임라인 켤 때
- 서버·클라 둘 다 같은 플래그를 본다(클라 재생 간격 Clamp 가 풀린다). 하나만 켜면 화면이 서버 시각과 어긋난다.
- 도트가
dotPerAction 이면 여전히 행동 기반이다. 시간 기반으로 돌리려면 dotPerAction 을 빼고 dotTickIntervalMs 를 준다.
- 연출 점유가 GCD·쿨보다 길면 그 값들이 죽은 노브가 된다. 시간 노브를 점유 위로 올려야 순서가 의미를 가진다.
combatAi (전투 스킬 선택 룰)
| Field | 의미 | 기본 |
healThreshold | 일반 힐: 아군 최저 HP비율 이 값 미만일 때만 | 0.6 |
criticalHealThreshold | 위급 힐 승격 비율 | 0.35 |
skipActiveBuff | 이미 걸린 버프 재시전 안 함 | true |
preferLongerCooldown | 같은 우선순위면 쿨다운 긴(강한) 스킬 먼저 | true |
rolePriority | 역할 우선순위(낮을수록 먼저) | {reviveCritical:1,criticalHeal:1,hidden:2,buff:3,debuff:4,heal:5,attack:6} |
함정
damage/magicDamage 필수(없으면 런타임 예외). rolePriority는 얕은 병합(일부만 오버라이드해도 나머지 기본 유지).
worldRegions/adventureZones/gameMaps/serviceZones가 공유. 서버·클라 이중 구현(파리티).
{ "unlockConditions": { "minPlayerLevel":1, "clearedAdventureZoneIds":[], "completedEventIds":[], "requiredPartyNpcIds":[], "activeQuestIds":[], "completedQuestIds":[] } }
| Field | 의미 | 참조 |
minPlayerLevel | 최소 레벨 | 기본 1 |
clearedAdventureZoneIds | 이 존들 모두 클리어 | adventureZones[].id |
completedEventIds | 이 이벤트들 모두 완료 | eventScripts[].id |
requiredPartyNpcIds | 이 동료들 파티 보유 | npcs[].id |
activeQuestIds | 이 의뢰들 진행 중(수락) — 완료해서 active 에서 빠지면 다시 잠긴다 | quests[].id |
completedQuestIds | 이 의뢰들 완료(보상 수령) — 한번 열리면 계속 열려 있다 | quests[].id |
함정
activeQuestIds(진행 중)와 completedQuestIds(완료)는 다르다. 「의뢰 받으면 열리고 끝내면 닫히는 곳」은 앞쪽, 「의뢰를 끝내면 열리는 곳」은 뒤쪽. 둘 다 적으면 AND.
- Deprecated 별칭이 하나라도 있으면 영구 잠금(fail-closed):
clearedLocationIds, requiredCompletedEventIds, requiredStoryCompletions, partyNpcIds.
activeQuestIds/completedQuestIds에 존재하지 않는 의뢰 id를 넣으면 영구 미해금(고블린동굴 버그 원인). 저장 시 검증에서 막는다.
{regions:[{id, name, desc, unlockConditions}]}. 자식 어드벤처 존을 묶는다. 자식 존이 하나도 없거나 전부 잠기면 지역 자체가 목록에서 제외된다.
{ "zones":[ {
"id":"", "parentRegionId":"", "name":"", "backgroundId":"", "recommendedLevel":1, "entryCost":0,
"requirements":{"minPlayerLevel":1,"maxPlayerLevel":99,"minPartySize":1,"maxPartySize":4},
"flow":{ "travelSecondsPerEncounter":3, "encounterCountToClear":1,
"encounterSequence":[ {"type":"battle","battleZoneId":""},
{"type":"event","eventScriptId":"","unlockConditions":{"completedEventIds":[]}} ],
"forwardMotion":{"bgScrollPxPerTick":26,"zoomDelta":0.03,"swayPx":8,"tickMs":140} },
"unlockConditions":{} } ] }
| Field | 의미 | 비고 |
parentRegionId | 소속 지역 | worldRegions[].id |
backgroundId | 배경 | sharedBackgrounds에 있어야 적용 |
requirements.* | 입장 레벨/파티 인원 범위 | 기본 1~99 / 1~4 |
flow.encounterSequence[] | 순서대로 진행되는 조우 | type: battle|event|treasure|weighted |
| └ battle | battleZoneId | battleZones 참조 |
| └ event | eventScriptId | eventScripts 참조 |
| └ event | unlockConditions(선택) | 조건 미충족이면 그 조우를 건너뛴다(이벤트는 소모되지 않음). 필드셋은 존 해금과 동일(completedEventIds·clearedAdventureZoneIds·completedQuestIds·activeQuestIds·requiredPartyNpcIds·minPlayerLevel). 조우 단계는 트리거를 안 타므로 진행도로 게이팅하는 유일한 방법. weighted 선택지에서는 불가 |
| └ treasure | gold, itemId, title, body | |
| └ weighted | choices[](base step + weight) | weighted 중첩 불가 |
flow.encounterCountToClear | 클리어 필요 조우 수 | 정규화 시 encounterSequence.length로 덮어씀 |
| Field | 의미 | 허용값 |
kind | 맵 종류 | hub | world |
backgroundId | 배경 | 필수 |
links[].targetMapId/ServiceZoneId/WorldRegionId | 이동 대상 | 셋 중 하나만 |
links[].coordinateSpace/displayStyle | 좌표 방식 / 표시 | design|percent / invisible|marker |
links[].x/y/width/height | 클릭 영역 | coordinateSpace 따라 해석 |
함정
- 대상 맵/서비스존이 잠겨 있으면 링크 미표시. worldRegion 링크는 그 지역이 접근 가능 목록에 있어야 표시.
| Field | 의미 | 허용값 |
backgroundId/actorId/message | 배경/액터/안내문 | background·message 필수 |
menus[].actionType | 메뉴 동작 | change_job|recruit|register|quests|forge|placeholder |
menus[].itemIds/forgeMode | 취급 아이템 / forge 모드 | buy|sell |
함정
- 메뉴가 빈 서비스존은 나가기 버튼이 없어 플레이어를 가둘 수 있다 — 이벤트 후
open_game_scene으로 맵 복귀를 꼭 넣을 것. ⛔이 자리에 move_world_location을 쓰면 맵으로 돌려보내는 게 아니라 그 모험 존으로 입장시킨다.
{scripts:[{id, name, startStepId?, oncePolicy:"once_per_account"|"repeatable"|"repeat_daily", steps:[...]}]}. once_per_account 완료 시 progress.completedEventIds에 id 기록.
| Step type | 핵심 필드 | 의미 |
narration/dialogue | text, actorId?, backgroundId? | 나레이션/대사 |
choice | prompt, options[]{id,label,nextStepId?,setValue?} | 선택지 |
text_input | prompt, variable, maxLength? | 입력을 변수에 저장 |
set_flag / set_progress_flag | key,value / flag,value? | 세션 플래그 / 영속 진행 플래그 |
branch | condition{key,op,value?}, nextStepId, fallbackStepId? | op: equals|not_equals|exists|not_exists · key: progress.<플래그> / companion.<npcId>(이벤트 동료로 동행 중인지 — 파티 편성 무관) / party.<npcId>(그 이벤트 동료가 지금 파티에 편성되어 있는지) / gender·name·houseId. ⭐뽑기로 얻은 동료는 두 조건 모두에서 제외된다 |
goto_step | stepId | 점프 |
open_game_scene | sceneKey, mapId?/locationId?/serviceId? | 씬 전환 |
grant_job_unlock/change_job | jobId | 직업 해금/전직 |
grant_item/grant_gold | itemId / amount | 지급 |
party_join/party_leave | npcId | 동료 합류/이탈 |
start_battle | battleZoneId, nextStepId? | 전투 |
open_service/complete_trigger | serviceId / triggerId? | |
move_world_location | locationId | 그 모험 존으로 입장(맵 복귀 아님). 잠겼거나 파티 조건 불일치면 지역 패널만 열고 멈춘다 |
end | message?, advance?{mode:auto|pause|click} | 종료 |
함정
- draft 플래그(
set_flag)는 세션 한정 — 영속 게이팅은 set_progress_flag.
조건이 맞으면 eventScriptId 실행. triggerType: adventure_completed | location_entered | service_interacted | npc_joined | job_unlocked | item_obtained | gacha_result | manual.
| conditions.* | 의미 |
mapId/serviceId/locationId | 현재 위치 일치 |
requiredFlags | 필요 진행 플래그(모두) — progress.flags |
requiredActiveQuestIds / requiredCompletedQuestIds | 진행 중 / 완료 의뢰 |
requiredQuestObjectives | 달성한 의뢰 목표 — "의뢰id:목표id". 의뢰 완료(보상 수령) 전에 목표 하나를 채운 시점을 잡는다(예: 늑대 10마리를 채운 뒤 마을 입장 → 촌장이 보상). 그 의뢰를 진행 중이어야 한다. |
requiredStoryCompletions | 완료 이벤트 — progress.completedEventIds |
excludedStoryCompletions | 만료 지점 — 적힌 이벤트를 하나라도 완료했으면 발동하지 않는다. 곁가지 이벤트가 한참 뒤에 튀어나와 이야기가 어긋나는 걸 막는다. requiredStoryCompletions(시작 지점)와 같이 쓰면 「이 구간에서만」이 된다. |
requiredMasteredJobs/activeJobId/excluded* | 직업 조건 |
requiredHouseIds/excludedHouseIds | 가문 조건 — 플레이어가 선언한 가문(lineage.declaredHouseId) 하나로만 판정한다. 섞인 서브 혈통은 보지 않는다. 가문을 안 정했거나 평민이면 required에 걸리지 않고 excluded에도 막히지 않는다. |
min/maxPartySize | 파티 인원(1+members) |
함정
requiredCompletedAdventureZoneIds는 location_entered에서 오작동 — "완료한 존"이 아니라 "지금 들어온 로케이션"과 비교. 존 완료 게이팅은 requiredFlags(완료 이벤트가 세운 플래그)나 requiredActiveQuestIds로.
requiredMasteredJobs/requiredItems도 context 값이 있으면 의미가 바뀐다.
{ "board":{"resetHourKst":0,"resetToken":"1786...."},
"quests":[ { "id":"", "name":"", "rank":"F", "category":"story", "autoClaim":true, "giverNpcId":"",
"days":["mon","wed"], "repeatPerDay":3,
"objectives":[{"id":"o1","type":"complete_event","targetId":"","description":""}],
"reward":{"gold":0,"charExp":0,"items":[]}, "requirements":{"requiredCompletedAdventureZoneIds":[]} } ] }
| Field | 의미 | 허용값 |
rank / category | 랭크 / 분류 | F E D C B A S / daily·story |
autoAccept | story 전용. 선행조건 충족 시 길드 방문 없이 자동 수락(연쇄 스토리 의뢰) | true만 채택 |
autoClaim | story·daily 공통. 목표를 채운 그 자리에서 자동 완료·보상. ⭐unlockConditions.activeQuestIds 로 여는 존은 수령 전(ready_to_claim)에도 열려 있으므로, 이걸 꺼두면 목표를 다 채우고도 재진입된다 | true만 채택 |
days | daily 전용. 게시판이 열리는 요일(중복 선택) | mon tue wed thu fri sat sun / 비우면 매일 |
repeatPerDay | daily 전용. 열린 하루 동안 반복 수행 가능 횟수 | 1 이상 정수(기본 1) |
clearFlagsOnClaim | daily 전용. 보상 수령 시 추가로 되돌릴 진행 플래그 | set_progress_flag 가 세우는 플래그명 |
objectives[].type | 목표 유형 | kill_monster·clear_zone·collect_item·reach_level·complete_event |
objectives[].targetId | 목표 대상 id | type에 따라 monster/zone/item/event(또는 flag) id |
reward | {gold, charExp, jobExp, items:[{itemId,count}]} | 모두 선택 |
requirements | 선행조건 | minLevel, maxLevel, requiredFlags, requiredCompletedQuestIds, requiredCompletedAdventureZoneIds, completedEventIds(선행 완료 이벤트) |
함정
complete_event objective는 flags.has(targetId) 또는 completedEvents.has(targetId)로 충족된다(targetId는 이벤트 id 또는 진행 플래그명).
- 반복 의뢰 +
complete_event = 2회차부터 수락 즉시 완료 — 진행 플래그가 영속이라 그렇다. 그래서 daily 의뢰는 수령 시 목표 플래그를 자동 소거한다. 이벤트가 세운 보조 플래그는 clearFlagsOnClaim에 직접 적는다.
- 반복 의뢰의
complete_event targetId 는 이벤트 id 가 아니라 진행 플래그여야 한다. completedEventIds는 되돌리지 않는다(되돌리면 oncePolicy: once_per_account 게이팅이 깨진다). 저장 시 에러로 막는다.
- 소거하는 플래그를 다른 트리거·의뢰가 해금 조건으로 쓰면 회차마다 다시 잠긴다. 저장 시 경고로 알려준다.
board는 resetHourKst(0~23)만 손으로 저작한다. resetToken은 의뢰 화면 최상단 「지금 리셋」 버튼이 쓰는 값이라 직접 건드리지 않는다.
- 요일·리셋 판정은
resetHourKst 기준 — resetHourKst:4면 KST 새벽 3시는 아직 전날 요일로 취급된다.
dayKey/dailyCompletedCounts는 플레이어별 런타임 상태(DSL 아님). 게시판 목록은 저장하지 않고 매 조회마다 요일로 계산하므로, 일일 의뢰를 새로 만들면 즉시 반영된다.
열거값: weaponType=dagger|sword|axe|mace|bow|staff|gauntlet · armorType=cloth|light|heavy · handedness=one_hand|two_hand · rarity=common|uncommon|rare|epic|legendary · slot=mainHand|offHand|armor|accessory
| Field | 의미 | 비고 |
slot | 장착 슬롯 | 레거시 weapon→mainHand, shield→offHand 정규화 |
weaponType/armorType/handedness | 무기/방어구/손잡이 | slot과 안 맞으면 정규화 시 삭제 |
rarity/reqLevel/buyPrice | 등급/요구레벨/구매가 | rarity 무효값은 common으로 치환 |
combat/bonus | 전투 스탯(atk/def/matk/mres) / 베이스 스탯 가산 | |
usableBy/storyItem | 착용 가능 직업 / 스토리 아이템 | |
함정
- 새
weaponType은 반드시 화이트리스트에 추가해야 저장됨 — 없으면 정규화 시 조용히 삭제.
slot에 안 맞는 weaponType/armorType/handedness는 저장 시 삭제.
| 키 | 역할 |
itemStatAffixes | 레어 어픽스 풀 + 굴림 파라미터(minAffixCount, maxAffixCount, 주속성 rollVarianceMin/Max, 보조스탯 bonusRollVarianceMin/Max, affixValueScale*, affixes[]) |
itemQualityTiers | 품질 배율 티어(가중 추첨: multiplier, weight) |
itemMechanicEffects | 무기 Tier3 메카닉 풀(effects[].kind: lifeSteal/armorPenetration/execute/dot/stun/chanceDamage/slow/trueStrike) |
itemArmorMechanicEffects | 방어구 Tier3 풀(kind: reflect/guard/regen/resist/counterOnEvade/counterOnHit) |
itemRareBaseWeapons | 레어 무기 베이스(baseValue, perLevelIncrement, weaponType, handedness). 커브는 하나지만 무기는 물리공격+마법공격 둘 다 갖고, 두 값을 각각 따로 굴린다(주속성 선택 statKey는 폐지) |
itemRareArmorBases | 레어 방어구/방패 베이스(def+mres 동시, slot: armor|offHand) |
itemRareAccessoryBases | 장신구 베이스 + 공통 마법저항 커브(mresBaseValue, perLevelRatio) |
itemLegendaryBases | 손수 저작하는 전설 베이스(bases[]) + 생성 노브(subStatChances, valueRollMin/Max, pickableRatio). 주커브는 부위별 베이스의 공통 설정을 사용한다 — 무기=공격력+마력(각각 롤), 방어구=방어력, 방패=방어력+마법저항, 장신구=마법저항 |
itemRandomDrop | 전투 승리 시 추가 드랍(dropChance, rareChance) |
함정
- 모든
*Bases/*Effects 풀은 비우면 createDefault* 기본값으로 대체된다(빈 배열 저장해도 기본값 들어감).
- 메카닉이 붙는지는 등급이 정한다(magic=안 붙음, rare/legendary=반드시). 풀의
weight는 "붙는다면 무엇이"만 정한다 — 예전의 mechanicAttachChance는 아무 경로도 쓰지 않던 죽은 노브라 삭제됐다.
- 양손무기는 주속성·부가수치가 같은 무기타입 한손의 2배다. 베이스 커브에 그대로 박혀 있고 어픽스는 생성 시 2배로 굴린다 — 전투 시점의 숨은 배율(
formulas.twoHandDamageMultiplier)은 폐지됐다.
- 무기는 종류와 무관하게 물리공격·마법공격을 둘 다 갖는다. 커브가 같고 밴드도 같으므로 어느 쪽이 높은지는 롤이 정한다 — 지팡이 물리공격이 마법공격보다 높게 나올 수도 있다.
- enum/kind/stat 무효값은 오류가 아니라 기본값으로 조용히 치환.
{goldCost, companionChance(0~1), equipmentRarityChances:{common,rare,legendary}}. companionChance 확률로 동료, 나머지는 장비. 세 rarity 합=1. 동료 후보는 npcs[].gachaEligible===true인 NPC만.
{houses:[{id, name, theme, selectable, hidden, statGrowth?, auraColor?}], commonerHouseId, heroHouseId, primaryBloodMin/Max, mixChance, ...}. statGrowth: {rolls{스탯→판정횟수}, probMultiplier, fixedProb?} — 레벨업 시 혈통% 확률로 스탯 추가 상승. 비우면 기본 5가문+commoner+hero로 복원.
| Field | 의미 |
gender | male | female |
growth | {statRatio{6스탯}, budgetScale?} (레거시 stats에서 비율 파생) |
skills.activeAction / skills.activePassive | 액티브 / 패시브 스킬 id 목록 |
gachaEligible | 동료 뽑기 후보 풀 포함(=== true만) |
{tracks:[], sfxTracks:[], ui:{}, zones:{}, sfx:{}}. tracks=BGM, sfxTracks=효과음(전투 스킬 SFX가 참조). ui=상황별 BGM(intro/login/town_hub/battle/…), zones=존 id→트랙. AudioTrack: {id,name,url,mimeType,originalName,sizeBytes}.
storyActors.actors[] = 이벤트 dialogue·서비스존 actorId의 참조 대상. sharedBackgrounds.backgrounds[] = 각종 backgroundId의 참조 대상. 전체화면 배경은 cover(채우기) 원칙, contain 금지 → 배경 이미지는 마진 넉넉히 제작.
이 문서는 서버 스키마·검증기에서 추출. 코드가 바뀌면 server/public/admin/help/dsl-reference.html도 갱신할 것.