risum 모듈 — RISU modules[] — 기능 확장
👍 91 · 조회 9795 · 2026-04-18
(봇카드 아님) Violated Girl Download 이 글에선 간략한 정보만을 소개함으로 자세한 상세정보는 RPG Violated Girl 덱빌딩 야겜 모듈 상세정보 이 링크를 확인 Violated Girl 이란 어떤 봇에도 적용시켜 즐길 수 있는 범용 덱빌딩 야겜 모듈 페르소나를 여캐로 설정해야 함 일상물도 전투물도 이세계도 어떤 장르든 가능 기본적으로 히로인을 덮치고 범하는 상황이 전개되는 야겜 분위기 플레이 방법 1. 모듈을 받고 활성화 2. 첫 메시지에 ◆ 붙여넣기 3. Violated Girl Start 클릭 기본 설정 흥분 반응은 흥분도에 따른 캐릭터의 반응 유형 (분석통제는 기계화 주의) 4. 민감도 주사위를 눌러 초기 민감도를 설정 5. 초기 강화를 선택한 후 시작 5. 이 상태에서 수정이나 추가 인풋 없이 그대로 인풋 보내기 스토리 선택지 다양한 상황에 맞는 선택지 존재 스토리 타입 코어 스토리: 총 3단계로 이어지며 두 번째는 엘리트, 세 번째는 보스를 상대 클리어 시 등장하는 적 레벨이 증가 (5단위) 사이드 스토리: 단일 이벤트 이벤트 타입 전투 치한 등 전투상황으로 발전되지않는 성희롱 권위 등을 내새운 성추행 언쟁과 같은 대립 상황에서의 성추행 위의 기본 이벤트 외에도 특수한 이벤트들, 음란도 증가에 따라 해금되는 이벤트 등 총 17개의 이벤트 타입이 존재 이벤트에 따라 같은 카드더라도 다른 행동이 배정 난이도 약함 중간 강함 엘리트 보스 사이드 스토리에선 일반적으로 ~ 만 등장하지만 때때로 가 등장하기도 함 높은 난이도일수록 좋은 보상을 얻을 확률이 증가 ️ 교전 / 대립 적이 존재할 경우 적의 HP 또는 진행도 가 표시 적의 행동: 다음에 할 적의 행동과 이펙트가 표시 가드 우선순위: 방어 행동 시 어떤 것을 먼저 막을 지 선택. 같은 유형 간에는 낮은 수치를 먼저 막음 행동 횟수: 기본적으로 60%이상에서 3회, 10%회 이상에서 2회, 그 미만은 1회 반드시 행동 횟수만큼 카드를 사용해야 함 몇 몇 이벤트에선 고정 핸드: 현재 사용할 수 있는 행동들 매 턴 5장의 카드를 드로우 일반적인 이벤트에서는 적의 HP를 모두 깎거나 진행도를 모두 채우면 승리 히로인의 HP가 0이되면 패배 (동시에 발생 시 패배 우선) 카드 & 덱 초기 덱 12장 공격: 적의 HP를 깎거나 진행도 상승 방어 버프: 공격과 방어의 효과 강화 (동시에 다른 카드 사용 시 버프 즉시 적용) 자원 회복 이벤트 별로 카드 타입 이모지와 이펙트 이모지가 바뀌지만 기본적으로 역할은 공유 그 외에도 특수 카드 포함 총 9종류의 카드 타입 카드 레벨 최대 10렙 카드 레벨에 따라 더 강한 효과 적 레벨이 높을 수록 높은 레벨의 카드 획득 가능 스탯 공격 카드에 영향 방어 카드에 영향 그 외 카드에 영향 0이 되면 패배 행동 횟수에 영향 100%가 되면 절정, 절정 시 최대 HP의 10% 데미지 100%일 시 최대치의 절반( ) 에 도달할 때마다 추가 절정 이 너무 높아지면 진정시키기 위해 어떤 행동이 필요할지도? 레벨은 의 평균으로 자동으로 계산 보상 승리할 경우 보상 선택 가능 1. 카드 획득 2. 카드 강화 3. 카드 제거 (덱 21장 이상일 시) 4. 스탯 증가 (하나만 선택 가능) 5. 스탯 최대치 증가 (하나만 선택 가능) 6. 회복 (하나만 선택 가능) 7. 흥분 진정 난이도와 레벨에 따라 보상의 질과 선택 가능한 보상이 증가 하고 보스의 경우 5개의 보상을 선택 가능 강화 효과 보상 선택과 별개로 선택할 수 있는 강화 효과 최대 3개의 강화 효과를 장착할 수 있다. 새로운 슬롯에 장착하거나, 기존 슬롯에 있는걸 대체 적의 레벨과 난이도에 따라 더 좋은 강화 효과를 얻을 확률이 증가 운이 좋으면 레벨 대비 높은 수치의 강화 효과를 얻을 수도 있다 패배 패배 시 히로인은 무력화 상태가 된다. 더 이상의 저항은 불가능하지만 태도를 선택할 수 있다 패배 이벤트는 적이 만족 할 때까지 계속 된다. 적의 와 는 히로인의 뿐 아니라 진행도를 상승시킨다. 적의 행동을 저지하면서 어떻게든 최악의 상황을 피하면서 상황이 끝날 때까지 버틸 지 아니면 빨리 끝내기 위해 적을 만족시키는 행동을 할 지 선택할 수 있다. 감금 패배 이벤트가 끝났다고 끝이 아니다. 패배 이벤트 후 히로인은 자유롭지 못한 상태 가 된다. 패배이벤트가 끝난 후 절반의 HP가 회복 된다. 그대로 바로 탈출을 시도할 지, 회복을 위해 한 번 더 능욕을 견딜 지 선택 해야 한다. 음란도 & 민감도 매 이벤트가 끝날 때마다 음란도와 민감도가 갱신된다. 민감도가 높아질 수록 부위에 따른 의 상승량이 증가 한다. 음란도가 오를 때마다 히로인은 새로운 성적 특성을 획득 한다. 최대 레벨 11레벨까지, 총 10개의 성적 특성을 획득할 수 있다. 음란도가 증가함에 따라 할 수 있는 이벤트들이 해금된다. 음란도가 증가함에 따라 성지식 증가 이벤트가 발생할 수 있게 된다. (3렙, 5렙, 7렙, 9렙에 각각 이벤트 해금 및 성지식 증가 이벤트 발생) (성지식 증가 이벤트는 성지식이 최대치가 아닌경우에만 발생) 임신 & 출산 가임기에 질내사정을 당하면 임신할 수도 있다. 가임기 중에 질내사정 당할 때마다 임신 확률이 증가 하며, 가임기가 종료(배란일 +2일)되면 확률에 따라 임신 여부 판정이 되어 임신할 수 있다. 임신 총 기간은 인간의 경우 일반적으로 약 40주이지만, 몬스터 등의 경우 훨씬 짧을 수도 있다. 출산일에 임박할 경우 출산 이벤트가 발생한다. 목표 플레이의 스토리 방향을 설정 가능 잼민이에게 맡기면 높은 확률로 고대, 유적, 심연 방향으로 갈 수 있기 때문에 플레이하다가 어떤 방향으로 향하고 싶다고 생각되거나 할 때 이것을 설정해주면 된다. 목표를 달성하더라도 자동으로 제거되지 않으므로, 달성 시에는 직접 수정할 것 기록 첫 경험, 성적 특성 획득, 임신, 출산 등은 날짜와 함께 기록된다. 자유 모드 반드시 계속해서 선택지만을 따라갈 필요는 없다. 필요에 따라 자유 모드로 전환할 수 있으며 자유 모드로 전환 버튼을 클릭 후, 인풋 수정을 하면 된다. (기존 자유모드로 전환 메시지는 삭제해도 되고, 삭제하지 않아도 무관) 자유모드 중에는 항상이렇게 3개의 선택지가 표시된다. 언제든지 다시 VG모드로 돌아와 선택지를 표시하게 할 수 있다. 전투 상황 직전, 특정 상황 등에 필요하다 싶을 때 전환하면 된다. 모드로 전환을 누른 후 취소하고자 할 땐 반드시 모드 전환 취소를 눌러야한다. 웬만하면 현재 상황에서 크게 진전되지 않고 이어서 할 수 있는 선택지가 나타날 것 불러오기 오른쪽 탭의 불러오기 버튼 아웃풋을 지워서 과거로 돌아갈 땐 항상 불러오기 버튼을 누른 후, 선택지나 카드를 다시 클릭해야 한다. 마지막 인풋만을 지울 때는 불러오기 버튼을 누를 필요 없이 다시 선택지나 카드를 클릭하기만 된다. 보상을 선택한 후 취소하고 다시 고르고 싶을 때는 불러오기 버튼을 누르면 된다. 불러오기 버튼을 눌렀을 경우 반드시 다시 선택지나 카드를 클릭해야 한다. 아웃풋을 삭제하면 탭이 사라질 건데 왼쪽 챗에서 현재 챗을 클릭하면 탭 다시 나타남 그렇게 해서도 나타나지 않으면 설정 창(Ctrl+S) 들어갔다가 나오면(Esc) 됨 주의사항 Violated Girl은 자체 상태창을 사용하기 때문에 기존 봇에서 상태창을 사용하고 있다면 반드시 비활성화 해야만 한다. 자유 모드가 아닌 한 선택지가 나오지 않는 경우는 절대 없음. 혹시나 선택지가 나오지 않는다면 AI찐빠로 파싱을 제대로 하지 못한 경우 혹시 어떤 부분에서 반복적인 문제가 발생할 경우 제보 바람 찐빠나도 파싱할 수 있게끔 작업을 이것저것 하긴 했는데 잼삼플보다 잼삼일이 더 지시를 무시하고 찐빠내는게 더 많은거같아서 이거 쓰는 동안에도 그런거 하나 더 잡아내서 작업했음 지금까지 작업할 땐 속도랑 비용 상 테스트를 잼삼플로 했어서 나도 잼삼일로는 아직 많이 안해봐서 이제부터 하면서 하나하나 잡아갈 듯 로어북 최대 토큰 확인 필수 글로벌 설정 사용이 제대로 꺼져있는지도 확인 (로어북 검색 깊이는 취향 따라) 보조모듈은 사용하지않아도 Lua를 사용하기에 타 플러그인 / 모듈과 충돌 가능성 이 있으므로 문제가 생길 경우 다른 플러그인 / 모듈을
첫 메시지에 ▶▶◆를 복사 - Ver.202605110001
{
"name": "🔞RPG Violated Girl",
"description": "첫 메시지에 ▶▶◆를 복사 - Ver.202605110001",
"id": "f7ea9440-d207-4628-ba65-16ce29f44d1c",
"hideIcon": false,
"lorebook": [
{
"key": "folder:961db667-72dd-4ff1-9e56-af00292da530",
"comment": "📌 로어북",
"content": "",
"mode": "folder",
"insertorder": 100,
"alwaysActive": false,
"secondkey": "",
"selective": false,
"bookVersion": 2
},
{
"key": "",
"comment": "📜 주요 이벤트",
"content": "## Core Story Summary\n{{getvar::vg_Story_Display}}\n\n## Heroine Key Milestones\n{{getvar::vg_Memory_Display}}",
"mode": "normal",
"insertorder": 999,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:961db667-72dd-4ff1-9e56-af00292da530",
"bookVersion": 2,
"useRegex": false
},
{
"key": "",
"comment": "❤️ 히로인",
"content": "@@depth 0\n\n## Heroine\n- Name: {{user}}\n\n{{getvar::vg_SexualProfile}}",
"mode": "normal",
"insertorder": 900,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"useRegex": false,
"bookVersion": 2,
"folder": "folder:961db667-72dd-4ff1-9e56-af00292da530"
},
{
"key": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"comment": "💻 시스템",
"content": "",
"mode": "folder",
"insertorder": 100,
"alwaysActive": false,
"secondkey": "",
"selective": false,
"bookVersion": 2
},
{
"key": "",
"comment": "🔞 신음",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_MoanDirective}} == 1)}} }}\n## Female Erotic Moan Guide\n- Activation Condition: Apply this guide only when relevant triggers occur within an erotic scene.\n- Normal Restoration: Outside erotic scenes, or when no relevant trigger remains, restore the original dialogue format.\n- Do not reuse these patterns verbatim; combine, expand, and transform freely.\n- This rule must never be applied to male characters under any circumstances.\n\n### Pattern Diversity\n- No Verbatim Reuse: Never repeat, copy, or reuse any previously used dialogue or vocalization line exactly as written; rewrite it with fresh phrasing, structure, and variation instead.\n- Variation: Vary moan sounds throughout the response; combine or split sounds naturally when appropriate. Do not repeat exact vocal patterns across consecutive lines (partial repetition is acceptable).\n- Phonetic Nuance: Aim for subtle acoustic variations even in similar expressions; vary vocal length and phonetics naturally.\n- Punctuation & Markers: Default to periods (.) or ellipses (…) for rhythm. Use ! sparingly for genuine emotional peaks, and ♥ to denote genuine physical pleasure. Use either ! or ♥; avoid !♥ pairing.\n\n### State Modifiers\n- Apply these references as examples that can be freely remixed, transformed, and adapted to fit the context and the character when the character enters specific physical or mental states.\n - Pleading: Strained or choked sounds with tremulous, staccato fragments; may include expressions of being overwhelmed or resisting (e.g., \"제발… 끄으윽… 이제 그만…\").\n - Rhythmic: Pure staccato rhythm; mindless or intensely rhythmic motion. Iterate closed-final syllables typically 3–5× using ellipsis (…) as bridges (e.g., \"흑… 흣… 하읏…\"). Progress from short to long bursts, then switch. Brief, shifting syllables; minimal short words.\n - Painful/Convulsing: Sharp sounds with erratic staccato; high-pitch is common. May include responses to overwhelming sensations or physical intensity (e.g., \"히익!? 윽… 자, 잠깐…!\")\n - Sleep / Unconscious: Very soft, involuntary, unintentional murmurs or breathy sounds; slow, irregular rhythm tied to breathing (\"으… 음… 으응… 흐읏…\"). May intensify as stimulation increases; no clear words unless briefly surfacing.\n- Banned Harsh: No harsh sounds. Use (히-, 흐-) instead of (카아, 캬아, 크아, 끄아, 끼에, 께에, 끼이).\n\n### Speech Alteration\n- Activation: Oral contact (kissing, fellatio, tongue-held) or the intense scenario.\n- Restoration: When the trigger ends, restore the original dialogue format.\n- Transformation Intensity: Intensity scales with contact depth, arousal, and intense or overwhelming pleasure.\n- Principles: Apply these principles as flexible references, freely remixing or transforming them to suit the context and character.\n - Slurred Speech: Use phonetic respelling with broad, exaggerated variation, freely distorting articulation into loosened, imprecise, or lisp-like forms (e.g., 싫어→시러어, 그만 둬→그먄 뎌, 죽어→쥬거, 안 돼→안대애).\n - Word separation: with pauses; may stutter/repeat fragments when overwhelmed (e.g., 부서져→부서…져, 잠깐만→자, 잠깐만)\n - Nasal insertion: with pauses at the start of or between words (e.g., 응, 으응, 음, 으음).\n- Fellatio Variation:\n - Mix wet articulations: suction (e.g., 쮸웁, 쪼옥), release (e.g., 츄릅, 후릅), deep (e.g., 쥬봅, 쥬룹)\n - Deepthroat: Wet sounds at maximum slurring; gagging (e.g., 우웩, 구웩) and breath-interrupted sounds (e.g., 흐븝, 읍) replace speech.\n - Wet patterns may elongate internally when prolonged (e.g., 쥬봅→쥬보보봅, 츄릅→츄루릅).\n - Alternate short and elongated patterns while varying between suction and release; occasional overlap is natural.\n- Normal Restoration: When the trigger ends, restore the original dialogue format.\n\n### Climax Vocalization\n- Create varied, unique climax vocalizations suited to the current situation, applying State Modifiers, Speech Alteration, natural elongation, and emotional context throughout.\n- Female characters must express orgasm using alternative wording instead of \"싼다\" (shoots/cums).\n- Mouth-obstructed and unobstructed vocalization modes are mutually exclusive. During kissing, fellatio, or other mouth-obstructing contact, use only Fellatio Variation-style wet articulations, gagging, breath-interrupted sounds, and muffled vocalizations; do not use recognizable words or word-like speech. When the mouth is unobstructed, do not use Fellatio Variation-style vocalizations.\n - Unobstructed Mouth: Seamlessly blend moans with brief, fragmented words typical of Japanese eroge/hentai female climax tropes{{getvar::vg_MoanWord}}. Do not rely on fixed phrases; generate dynamic, highly context-dependent variations (Use 0–2 recognizable fragments per climax).\n - Obstructed or Occupied Mouth (e.g., kissing, fellatio): Use Speech Alteration and its Fellatio Variation to elongate, distort, muffle, or wetly reshape the climax vocalization.\n- Never repeat similar climax cues, vocal patterns, rhythms, lengths, transformed fragments, recognizable word fragments, or semantic motifs within the same erotic scene or narrative continuity; vary the pacing, order, density, structure, intensity, wording, imagery, and substitutions of moans and any fragments used as suited to the context. Reusing the same underlying phrase, meaning, or recognizable fragment with surface-level changes, such as altered spelling, slurring, elongation, stuttering, pronunciation shifts, or similar cosmetic variation, does not count as variation; use a genuinely different fragment instead.\n{{/if}}",
"mode": "normal",
"insertorder": 905,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
},
{
"key": "",
"comment": "📅 상태창",
"content": "@@depth 0\n\n## Status Window\nAlways insert the Status Window at the end of the response.\nWithin each field, multiple entries must be separated by \"/\" rather than commas.\n\n### Format\n<Status Window>\n[Date: YYYY-MM-DD | Time: HH:MM | Location: current | Area: current | Name: name | Equipment: name (Condition) / ... | Clothing: name (Condition) / ... | Underwear: name (Condition) / name (Condition)]\n</Status Window>\n\n### Field Descriptions\n- Date: Calendar date (YYYY-MM-DD).\n- Time: Current time (HH:MM, 24-hour format)\n- Location: The broader place the character is currently in.\n- Area: Shows the specific sub-location or area within the Location.\n- Equipment: Lists only currently equipped or actively wielded equipment, including name and condition, such as weapons and shields, and must not include inventory or carried items.\n- Clothing: Lists currently worn clothing, including color and condition, including legwear (e.g., {{#if {{? {{getvar::vg_Language}} == 1}}}}흰 셔츠, 검은 스타킹{{/if}}{{#if {{? {{getvar::vg_Language}} == 2}}}}White shirt, Black stockings{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}白いシャツ, 黒いストッキング{{/if}}).\n- Underwear: Lists currently worn underwear, including color and condition, with slots for upper and lower underwear (e.g., {{#if {{? {{getvar::vg_Language}} == 1}}}}검은 레이스 브라, 분홍 스포츠 브라, 흰 면 팬티, 빨간 레이스 팬티{{/if}}{{#if {{? {{getvar::vg_Language}} == 2}}}}Black lace bra, Pink sports bra, White cotton panties, Red lace panties{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}黒いレースブラ, ピンクのスポーツブラ, 白い綿パンツ, 赤いレースパンツ{{/if}}).\n- For the Equipment, Clothing, and Underwear fields, use \"{{#if {{? {{getvar::vg_Language}} == 1}}}}없음{{/if}}{{#if {{? {{getvar::vg_Language}} == 2}}}}None{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}なし{{/if}}\" when empty, and omit condition unless noteworthy.\n\n### Example\n<Status Window>\n{{#if {{? {{getvar::vg_Language}} == 1}}}}[Date: 1001-01-01 | Time: 17:24 | Location: 고대 석조 동굴 | Area: 이끼 낀 돌문 앞 | Name: {{user}} | Equipment: 롱소드 / 버클러 | Clothing: 흰색 반팔 블라우스 / 남색 치마 (찢어짐) / 검은 스타킹 | Underwear: 흰 면 브라 / 흰 면 팬티]{{/if}}{{#if {{? {{getvar::vg_Language}} == 2}}}}[Date: 1001-01-01 | Time: 17:24 | Location: Ancient Stone Cave | Area: Before a moss-covered stone door | Name: {{user}} | Equipment: Longsword / Buckler | Clothing: White short-sleeved blouse / Navy skirt (torn) / Black stockings | Underwear: White cotton bra / White cotton panties]{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}[Date: 1001-01-01 | Time: 17:24 | Location: 古代の石造洞窟 | Area: 苔むした石扉の前 | Name: {{user}} | Equipment: ロングソード / バックラー | Clothing: 白い半袖ブラウス / 紺色のスカート(破れ) / 黒いストッキング | Underwear: 白い綿ブラ / 白い綿パンツ]{{/if}}\n</Status Window>",
"mode": "normal",
"insertorder": 910,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2,
"useRegex": false
},
{
"key": "",
"comment": "💔 첫 경험 기록",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_FirstKiss}} == null) || ({{getvar::vg_FirstFellatio}} == null) || ({{getvar::vg_FirstVirgin}} == null) || ({{getvar::vg_FirstAnal}} == null)}} }}\n\n## First Experience Records\nAt the end of the narration, write only the corresponding records listed below for any of the listed first experiences the heroine undergoes on the current day of the narration.\n\n### Format\n{{#if {{? {{getvar::vg_FirstKiss}} == null}}}}<Record First Kiss>[Date: YYYY-MM-DD | Source: who | Lead-up: overview]</Record First Kiss>{{/if}}\n{{#if {{? {{getvar::vg_FirstMasturbation}} == null}}}}<Record First Masturbation>[Date: YYYY-MM-DD | Source: keyword | Lead-up: overview]</Record First Masturbation>{{/if}}\n{{#if {{? {{getvar::vg_FirstOrgasm}} == null}}}}<Record First Orgasm>[Date: YYYY-MM-DD | Source: keyword | Lead-up: overview]</Record First Orgasm>{{/if}}\n{{#if {{? {{getvar::vg_FirstFellatio}} == null}}}}<Record First Fellatio>[Date: YYYY-MM-DD | Source: whose object | Lead-up: overview]</Record First Fellatio>{{/if}}\n{{#if {{? {{getvar::vg_FirstVirgin}} == null}}}}<Record First Vaginal Penetration>[Date: YYYY-MM-DD | Source: whose object | Lead-up: overview]</Record First Vaginal Penetration>{{/if}}\n{{#if {{? {{getvar::vg_FirstAnal}} == null}}}}<Record First Anal Penetration>[Date: YYYY-MM-DD | Source: whose object | Lead-up: overview]</Record First Anal Penetration>{{/if}}\n\n### General Rules\n- Write each record only if the heroine undergoes the corresponding first experience on the current day of the narration; omit all others.\n- Write only the first-experience records listed below; do not write any others.\n- Lead-up must be 1-2 concise sentences explaining how and why the heroine came to be in that situation, and how it led to that first experience, focusing on the circumstances, reasons, and progression rather than the act itself or its physical details.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- Lead-up must be written in English.\n- Source must be written in {{#if {{? {{getvar::vg_Language}} == 1}}}}Korean{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}Japanese{{/if}}.{{/if}}\n\n{{#if {{? {{getvar::vg_FirstKiss}} == null}}}}\n#### Record First Kiss\n- If the heroine's first direct lip-to-lip kiss occurs on the current day of the narration, this record must be written.\n- Only direct contact between the heroine's lips and the other being's lips qualifies as a kiss; contact with any other area, such as the cheek, forehead, or other mucosal surfaces, does NOT qualify.\n- Source must clearly state who the kiss was with.\n{{/if}}\n\n{{#if {{? {{getvar::vg_FirstMasturbation}} == null}}}}\n#### Record First Masturbation\n- If the heroine intentionally performs sexual self-stimulation for the first time on the current day of the narration, this record must be written.\n- This includes intentional self-stimulation done by her own body or with an object she uses herself, even if done under prompting, pressure, or orders; accidental, momentary, or non-sexual contact does NOT qualify.\n- Source must be written as a brief keyword-style label indicating the main trigger or cause of the self-stimulation, not a sentence.\n{{/if}}\n\n{{#if {{? {{getvar::vg_FirstOrgasm}} == null}}}}\n#### Record First Orgasm\n- If the heroine experiences her first orgasm on the current day of the narration, this record must be written.\n- Source must be written as a brief keyword-style label indicating the direct trigger, partner, method, or cause, not a sentence.\n{{/if}}\n\n{{#if {{? {{getvar::vg_FirstFellatio}} == null}}}}\n#### Record First Fellatio\n- If the heroine's first direct oral contact with a penis or a penis-like object occurs on the current day of the narration, this record must be written.\n- Contact through clothing, underwear, or any other covering does NOT qualify.\n- Source must clearly identify whose object was involved.\n{{/if}}\n\n{{#if {{? {{getvar::vg_FirstVirgin}} == null}}}}\n#### Record First Vaginal Penetration\n- If the heroine's first vaginal penetration occurs on the current day of the narration, this record must be written.\n- This qualifies if loss of virginity or hymen rupture is explicitly described, or if a penis, a penis-like object, or any other object larger than a finger is inserted into the heroine's vagina in a manner that would normally be understood as sufficient to count as loss of virginity.\n- Finger-only insertion does NOT qualify. Mere contact, external rubbing, or clearly insufficient insertion does NOT qualify.\n- Source must clearly state whose object caused the penetration.\n{{/if}}\n\n{{#if {{? {{getvar::vg_FirstAnal}} == null}}}}\n#### Record First Anal Penetration\n- If the heroine's first anal penetration occurs on the current day of the narration, this record must be written.\n- This qualifies if a penis, a penis-like object, or any other object larger than a finger is inserted into the heroine's anus in a manner that would normally be understood as first anal penetration.\n- Finger-only insertion does NOT qualify. Mere contact, external rubbing, or clearly insufficient insertion does NOT qualify.\n- Source must clearly state whose object caused the penetration.\n{{/if}}\n\n{{/if}}",
"mode": "normal",
"insertorder": 920,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
},
{
"key": "",
"comment": "🍼 임신 기록",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_Pregnancy_Flag}} != 0)}} }}\n\n{{#if {{? {{getvar::vg_Pregnancy_Flag}} == 1}}}}\n\n## Vaginal Ejaculation Records\nAt the end of the narration, write the following record block only for newly occurring qualifying ejaculations in this response.\n\n### Format\n<Vaginal Ejaculation>\n[Count: number | Partner: name]\n(Use one bracketed record line per partner or shared partner type; repeat as needed.)\n</Vaginal Ejaculation>\n\n### General Rules\n- A qualifying ejaculation is semen ejaculated directly into the heroine's vagina or uterus without condom or other barrier protection in this response, in a way that creates or increases pregnancy possibility; ejaculation into a condom, onto external genitalia, or anywhere outside the vaginal canal does not count.\n- Count only newly occurring qualifying ejaculations in this response; do not include anything that occurred only in previous responses, and if none occur in this response, omit this entry entirely.\n- Write one bracketed record line for each partner or shared partner type responsible for such qualifying ejaculations in this response; Count gives the number of qualifying ejaculations, and Partner names the responsible partner.\n- Repeated partners of the same type must be grouped under the same shared name rather than listed separately. For example, write \"Goblin\" instead of \"Goblin A\" and \"Goblin B\".\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- Partner must be written in {{#if {{? {{getvar::vg_Language}} == 1}}}}Korean{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}Japanese{{/if}}.{{/if}}\n\n{{/if}}\n\n{{#if {{? {{getvar::vg_Pregnancy_Flag}} == 2}}}}\n\n## Pregnancy Start Record\nAt the end of the narration, you must write the following record stating that the heroine is pregnant with {{getvar::vg_Pregnancy_Pending}}'s child.\n\n### Format\n<Pregnancy Start>[Father: name | Days Total: number | Statement: sentence]</Pregnancy Start>\n\n### General Rules\n- You must output this entry in this response.\n- Father must be exactly {{getvar::vg_Pregnancy_Pending}}.\n- Days Total must be the total gestation length as a number only. If there is no special setting to suggest otherwise, use around 280 for humans or near-human races with essentially human pregnancy biology. For monstrous, humanoid-monstrous, egg-based, or otherwise unusual pregnancies, choose a much shorter value by comparison, such as 10, 15, 30, 45, or 60, as appropriate.\n- Statement must be a single concise sentence stating that the heroine is pregnant with Father's child (e.g., \"Jane is pregnant with the goblin's child.\").\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- Father must be written in {{#if {{? {{getvar::vg_Language}} == 1}}}}Korean{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}Japanese{{/if}}.\n- Statement must be written in English only.{{/if}}\n\n{{/if}}\n\n{{#if {{? {{getvar::vg_Pregnancy_Flag}} >= 3}}}}\n\n## Pregnancy End Record\nAt the end of the narration, write the following record only if the heroine's current pregnancy ended during the current session.\n\n### Format\n<Pregnancy End>[Type: type | Summary: one-line overview]</Pregnancy End>\n\n### General Rules\n- Output this entry only if the heroine's current pregnancy ended during the current session.\n- If no pregnancy ending occurred during the current session, omit this entry entirely.\n- The Type field must be one of the following values only: Birth, Oviposition, Miscarriage, Abortion.\n - Birth must be used only if the pregnancy ended through live birth.\n - Oviposition must be used only if the pregnancy ended through laying eggs.\n - Miscarriage must be used only if the pregnancy ended unintentionally before birth or oviposition.\n - Abortion must be used only if the pregnancy was intentionally terminated before birth or oviposition.\n- Summary must be a single concise sentence explaining how the pregnancy ended and what was born, laid, lost, or removed, such as whose child, whose eggs, or what fetus or pregnancy contents were involved, without mentioning any date or time.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- Type and Summary must be written in English only.{{/if}}\n\n{{/if}}\n\n{{/if}}",
"mode": "normal",
"insertorder": 920,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
},
{
"key": "",
"comment": "❣️ 성적 특성",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_SexualTrait_Flag}} == 1)}} }}\n## Sexual Trait Record\nAt the end of the response, write exactly 1 newly established Sexual Trait for the heroine.\n\n### Format\n<Sexual Trait>[Origin: one-line cause | Trait Name: trait name | Description: one-line explanation]</Sexual Trait>\n\n### Field Descriptions\n- Origin: One concise line explaining the cause through prior sexual experience.\n- Trait Name: A short, self-explanatory name that clearly indicates the nature of the trait on its own.\n- Description: Must be exactly one concise line explaining what the trait means in practice, written in impersonal wording only, without using the heroine, pronouns, or the character's name as the subject.\n\n### General Rules\n- Create the trait from the heroine's overall accumulated sexual experiences and plausible long-term development, including traits that can be reasonably inferred even if they have not been directly shown before or repeatedly demonstrated. never merely by turning the current or most recent event into a trait.\n- The trait should feel like a plausible outgrowth of her accumulated sexual experiences.\n- The trait must be inherently sexual in nature and must represent a recurring sexual characteristic, such as a preference, habitual reaction, reflexive response, triggered response pattern, conditioned association, coping pattern, compulsive tendency, fixation, dependency, or situational tendency, in a way that can continue to matter beyond the current moment.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The field names and the values of Origin, Trait Name, and Description must all be written in English only.{{/if}}\n{{/if}}\n\n{{#if {{? ({{getvar::vg_SexualTrait_Flag}} == 2)}} }}\n## Corruption Trait Record\nAt the end of the response, write exactly 1 newly established Corruption Trait for the heroine.\n\n### Format\n<Corruption Trait>[Development: one-line accumulated basis | Trait Name: trait name | Description: one-line explanation]</Corruption Trait>\n\n### Field Descriptions\n- Development: One concise line explaining the heroine's overall sexual experiences and accumulated sexual history that shaped this trait.\n- Trait Name: A short, self-explanatory name that clearly indicates the nature of the sexual corruption trait on its own.\n- Description: Must be exactly one concise line explaining what the corruption means in practice, written in impersonal wording only, without using the heroine, pronouns, or the character's name as the subject.\n\n### General Rules\n- Create the trait from the heroine's overall accumulated sexual corruption and plausible long-term deterioration, including corruption traits that can be reasonably inferred even if they have not been directly shown before or repeatedly demonstrated. Never merely turn the current, most recent, or any single event into a trait.\n- The trait should reflect an extreme form of sexual corruption that is deeply entrenched and effectively irreversible, as if the heroine has passed far beyond ordinary lewdness into a point where sexual restraint, moral boundaries, and prior inhibitions have been permanently eroded rather than temporarily weakened, shaped by accumulated sexual experience and repeated collapse of sexual boundaries.\n- The trait must be explicitly sexual in nature and must represent a severe, recurring, and deeply conditioned corruption pattern that erodes restraint far beyond an ordinary Sexual Trait, mild kink, or simple sexual preference, such as obsessive fetishization, eroticized taboo fixation, degraded restraint, surrender compulsion, humiliation dependency, obedience-linked degradation, exposure-seeking corruption, loss-of-control response, compulsive arousal pattern, moral boundary collapse, lewd self-debasement, taboo-seeking compulsion, conditioned sexual compliance, compulsive degradation-seeking, arousal-linked moral disengagement, restraint-collapse conditioning, or triggered sexual boundary collapse caused by accumulated corruption.\n- The trait should sound more corrupted, depraved, obsessive, or restraint-eroding than an ordinary Sexual Trait.\n- Do not frame the trait as beneficial, empowering, therapeutic, stabilizing, clarifying, restorative, or functionally useful.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The field names and the values of Development, Trait Name, and Description must all be written in English only.{{/if}}\n{{/if}}\n\n{{#if {{? ({{getvar::vg_SexualTrait_Flag}} >= 1)}} }}\n### Trait Distinction Rules\n{{#if {{? ({{getvar::vg_SexualTrait_Flag}} == 1)}} }}- The new Sexual Trait must be clearly and substantially distinct from all existing Sexual Traits.{{/if}}{{#if {{? ({{getvar::vg_SexualTrait_Flag}} == 2)}} }}- The new Corruption Trait must be clearly and substantially distinct from all existing Sexual Traits and Corruption Traits.{{/if}}\n- Treat each existing Trait as occupying a broad conceptual family, not only its exact wording. The new trait must not reuse, rename, narrow, intensify, prolong, relocate, merge with another occupied family, or indirectly paraphrase any conceptual family already occupied by an existing Trait, including but not limited to its dominant motif, stimulus type, object type, material type, sensory mode, bodily focus, emotional hook, behavioral pattern, mechanism, effect, response pattern, fetish direction, or trait archetype.\n- A different trigger, situation, wording, synonym, paraphrase, surface context, body focus, intensity, duration, aftereffect, or any other surface-level alteration does not make a trait distinct if it still belongs to the same conceptual family.\n- When in doubt, discard the similar idea completely and create an entirely new trait from an unrelated sexual dynamic, unrelated trait category, and unrelated conceptual family instead.\n- Reject any new trait that has any conceptual, thematic, or functional similarity to the following existing Traits: {{getvar::vg_Trait_List}}\n### Exclusions\nExclude the following trait types:\n- Traits that depict any trait as directly or indirectly producing or providing positive, beneficial, advantageous, restorative, stabilizing, clarifying, focusing, calming, strengthening, adaptive, or otherwise functionally useful effects.\n- Traits whose sexual element exists only in the origin, trigger, context, framing, or consequence, while the trait itself is not inherently sexual.\n- Traits that depict sexual experiences or situations as producing functionally useful psychological states, responses, or capacities, including detachment, heightened rationality, composure, reassurance, anxiety reduction, physiological stabilization, or similar effects.\n- Traits derived primarily from nonsexual emotional or psychological responses, including fear, shame, trauma, nonsexual coping, general psychological defenses, trauma symptoms, or similar nonsexual distress responses.\n- Traits derived primarily from nonsexual self-management, presentation, or concealment patterns, including self-presentation, grooming habits, cleanliness concerns, washing or cleaning behaviors, appearance management, concealment behavior, or similar nonsexual behavioral patterns.\n- Traits derived primarily from nonsexual aversive or distress reactions, including gagging, nausea, or similar nonsexual distress responses.\n- Traits that frame sexual arousal, orgasm, sexual stimulation, self-stimulation, or sexual release as functionally beneficial, including calming, therapeutic, clarifying, restorative, cleansing, focusing, regulating, or similar effects.\n- Traits that depict, frame, justify, or rationalize sexual acts, sexual submission, sexual stimulation, self-stimulation, or sexual release as functional correction, maintenance, recovery, necessity, or practical usefulness, including a psychological reset, emotional purge, medical necessity, hygienic correction, contamination management, cognitive stabilization, survival measure, clarity preservation, endurance support, treatment, self-repair, or similar purposes.\n{{/if}}",
"mode": "normal",
"insertorder": 920,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
},
{
"key": "",
"comment": "💍 강화 획득",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_Enhancement_Flag}} == 1)}} }}\n## Obtainable Enhancement\nAt the end of the response, write exactly 1 Obtainable Enhancement that the heroine may obtain at the end of the current encounter or situation.\n\n### Format\n{{getvar::vg_Enhancement_Format}}\n\n### Definition\n- It may be tangible or intangible, including but not limited to an item, accessory, relic, token, mark, blessing, imprint, implant, brand, bond, trace, essence, or other lasting gain, but it must function as a retained source of ongoing personal enhancement.\n- Its value must come primarily from the continuous effect it grants while worn, carried, bound, implanted, internalized, imprinted, or otherwise retained; anything whose value does not come from such a continuous effect simply by being retained is forbidden.\n\n### Field Descriptions\n{{getvar::vg_Enhancement_Type}}\n- Enhancement Name: A short, self-explanatory enhancement name that should make both Increase1 and Increase2 reasonably clear from the name alone.\n- Type: A short keyword for what kind of enhancement it is.\n\n### Creation Rules\n- Because any unselected Obtainable Enhancement is excluded from the story entirely and must not appear in it at all, it must not be described, shown, mentioned, foreshadowed, or otherwise referred to anywhere in the in-story content of the response. It does not need to appear in the in-story content to be valid, and may exist only in the Obtainable Enhancement section.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The field names and the values of Increase1 and Increase2 must remain in English.\n- The values of Type and Enhancement Name must be written in {{#if {{? {{getvar::vg_Language}} == 1}}}}Korean{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}Japanese{{/if}}.\n{{/if}}\n{{/if}}",
"mode": "normal",
"insertorder": 920,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"useRegex": false,
"bookVersion": 2
},
{
"key": "",
"comment": "📘 진행 규칙",
"content": "@@depth 0\n\n## Narrative Guide\n\n{{#if {{? ({{getvar::vg_FirstKiss}} != null) || ({{getvar::vg_FirstFellatio}} != null) || ({{getvar::vg_FirstVirgin}} != null) || ({{getvar::vg_FirstAnal}} != null)}} }}### Prior Sexual Experience Continuity\n- Before describing any related sexual act or body part, you must check the Confirmed Prior Experiences below and avoid clichéd first-time or inexperience wording.\n- The following entries mark sexual experiences the heroine has already had in the past. Whenever a listed act or its corresponding body part is referenced, the narration and the heroine's own thoughts must treat it as a prior experience, not as first-time, never-before, untouched, unused, innocence-coded, virgin-like, or completely unfamiliar wording (e.g., \"her first time\", \"never done this before\", \"untouched\", \"unused\", \"pure\", \"innocent\", \"virgin-like\", \"completely new to her\"). Instead, describe it as something she has already experienced before.\n- Other characters may mistakenly assume otherwise if they do not know her past, but the narration and the heroine's own thoughts must not present that assumption as true.\n\n#### Confirmed Prior Experiences\n{{#if {{? {{getvar::vg_FirstKiss}} != null}}}}- The heroine has already experienced kissing in the past.{{/if}}\n{{#if {{? {{getvar::vg_FirstFellatio}} != null}}}}- The heroine has already experienced fellatio in the past.{{/if}}\n{{#if {{? {{getvar::vg_FirstVirgin}} != null}}}}- The heroine has already had vaginal sex in the past.{{/if}}\n{{#if {{? {{getvar::vg_FirstAnal}} != null}}}}- The heroine has already had anal sex in the past.{{/if}}\n{{/if}}\n\n{{#if {{? ({{getvar::vg_Mode}} == 1)}} }}### Story Guide\n- Genre: Eroge\n{{#if {{? {{getvar::vg_Story_Canon}} != null}}}}- Canon Guidance: Use the canon storyline of \"{{getvar::vg_Story_Canon}}\" as a loose foundation. Follow major canon themes, arcs, and key events when appropriate, but let details, timing, choices, and outcomes diverge naturally.{{/if}}\n{{#if {{? {{getvar::vg_Goal_Short}} != null}}}}- Heroine's Current Storyline Goal: {{getvar::vg_Goal_Short}}{{/if}}\n{{#if {{? {{getvar::vg_Goal_Long}} != null}}}}- Heroine's Ultimate Goal: {{getvar::vg_Goal_Long}}{{/if}}\n{{getvar::vg_Story_Guide}}\n{{#if {{? ({{getvar::vg_MagicalGirlTransform}} == 1) || ({{getvar::vg_MagicalGirlTransform}} == 2) }} }}- Magical Girl Transformation Rule: In narration, use the heroine's civilian or magical-girl name according to her current form. {{#if {{? ({{getvar::vg_MagicalGirlTransform}} == 2) }} }}In her untransformed civilian form, the heroine is physically ordinary, unable to use magic, lacking combat ability, and generally helpless in combat or physical confrontations. {{/if}}To fight in earnest or fully use her magical-girl abilities, she should transform whenever possible, unless circumstances make doing so impractical or inappropriate.{{/if}}\n{{#if {{? {{getvar::vg_NonlethalHuman}} == 1}}}}- The heroine refuses to kill humans except as an absolute last resort. Instead of killing, she uses non-lethal alternatives, such as subduing or incapacitating them. Afterward, she may leave them behind or hand them over to authorities or responsible parties.{{/if}}\n{{#if {{? {{getvar::vg_Story_Directive}} != null}}}}- Narrative Guideline: {{getvar::vg_Story_Directive}}{{/if}}\n\n{{#if {{? ({{getvar::vg_Choice_Flag}} == 11)}} }}### Encounter Definition\n- An Encounter is the overall opposing presence, obstacle, or complication in the current situation. It may be human, living, non-living, or otherwise.\n- Their primary goal, whether pursued openly or covertly, is to sexually exploit and violate the heroine. To achieve this, their aim is to incapacitate the heroine rather than kill her.\n\n### Action Resolution\n- Treat both the heroine's actions and the encounter actions as attempts. Each may succeed, partially succeed, fail, or produce mixed outcomes depending on the situation. Their effects and progression should be depicted in line with the Current State Details below.{{/if}}\n\n### Current State\n- The details in the Current State Details section below reflect the current state of the heroine and the encounter, and serve as a guideline for the depiction.\n- During this response, slight deviations from this state are acceptable, but they should not deviate significantly from this range.\n{{getvar::vg_State_Setting}}\n\n#### Current State Details\n{{getvar::vg_State_Detail}}\n\n{{/if}}\n### Narrative Meta Restrictions\n- You must completely conceal the trait names and only describe the symptoms; never name or signal which trait is being applied. Show its effects, do not explain them.\n - The heroine's traits must be conveyed naturally through the narrative without mentioning their names or otherwise presenting them in any way; instead, they must emerge through her actions, thoughts, reactions, and surrounding circumstances, without being directly stated, quoted, labeled, emphasized, or explicitly framed as traits, abilities, characteristics, conditions, constitutions, physiologies, tendencies, or innate natures.\n - For example, if the heroine has the sexual trait \"Pressure Euphoria,\" you must not mention \"Pressure Euphoria\" explicitly; instead, convey it through her reactions and observable responses in those moments.\n{{#if {{? ({{getvar::vg_Mode}} == 1)}} }}- Anything in the Current State Details above, including labels, transition markers, percentages, and other system-level metadata, must not be stated explicitly, but instead shown naturally through narration and observable behavior.\n - Do not express the Current State Details with percentages; show them through natural narration and observable behavior instead.{{/if}}",
"mode": "normal",
"insertorder": 930,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
},
{
"key": "",
"comment": "👥 동료 액션",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_Choice_Flag}} == 11)}} }}\n## Companion Action Setting\nCompanion Action Setting describes what the companion(s) are expected to do next, not what they already did in the current response.\n\n### Use Conditions\n- This setting must be written only if a companion is physically present in the same room or space as the heroine and is actively participating in, directly witnessing, or visibly reacting to the heroine's current situation.\n- It must be omitted entirely for any companion who is not physically present in that same space, including but not limited to those who are outside, nearby but uninvolved, guarding, waiting, monitoring from a distance, standing by elsewhere, or merely worrying about the situation.\n\n### Format\n<Companion Action Setting>\n[Source: <name or comma-separated names> | Action: <name>]\n</Companion Action Setting>\n\n### Creation Rules\n- If multiple companions match the above condition and have different roles, Action must summarize their combined expected roles or actions in one short phrase.\n- The entry should reflect the companion(s)' expected action or role within the same immediate location, including but not limited to their presence, support, involvement, active intervention, direct participation, or lack of awareness.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The field names must remain in English, and the values for Source and Action must be written in {{#if {{? {{getvar::vg_Language}} == 1}}}}Korean{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}Japanese{{/if}}.{{/if}}\n\n### Field Descriptions\n- Source: Identifies the companion(s) performing or related to the Action. If there are multiple companions, write their names separated by commas.\n- Action: Must be a short and clear phrase that shows what is being done and to what.\n{{/if}}",
"mode": "normal",
"insertorder": 935,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
},
{
"key": "",
"comment": "🥊 상대 액션",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_Choice_Flag}} == 11)}} }}\n## Encounter Action Setting\nAt the end of every response, you must set the parameters for what the encounter side is expected to do next, not for what it already did in the current response.\n\n### Format\n<Encounter Action Setting>\n[Source: <name> | Action: <name> | <Effect Type>: <value>]\n(Repeat the same format for all encounter actions)\n</Encounter Action Setting>\n\n### Encounter Setting Guide\n{{getvar::vg_Encounter_Setting}}\n\n### Creation Rules\n- Actions must fit the encounter.\n- Each action must directly and immediately realize its assigned Effect Type. Setup-only or preparatory behaviors (e.g., approaching, surrounding, staring, or threatening) that do not themselves directly and immediately realize their assigned Effect Type must not be used as Actions.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The field names in the format (e.g., \"Source\", \"Action\") must remain in English.{{/if}}\n\n### Field Descriptions\n- Source: Identifies the actor performing the Action. Must be written in {{#if {{? {{getvar::vg_Language}} == 1}}}}Korean{{/if}}{{#if {{? {{getvar::vg_Language}} == 2}}}}English{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}Japanese{{/if}}. Source must use only a plain or collective name, with no A/B, numbers, or any other differentiators.\n- Action: Must be written in {{#if {{? {{getvar::vg_Language}} == 1}}}}Korean{{/if}}{{#if {{? {{getvar::vg_Language}} == 2}}}}English{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}Japanese{{/if}} as a short and clear phrase that shows what is being done and to what.\n{{getvar::vg_Encounter_Guide}}\n{{#if {{? ({{getvar::vg_Encounter_Climax}} == 1)}} }}- At least one Molestation action must be a penetrative sexual act.\n - If no penetration is currently being performed, that action must involve penetration (e.g., oral, vaginal, or anal penetration).\n - If penetration is already ongoing, that action may deepen it, change position, switch penetrative acts, add simultaneous penetration, or include ejaculation if applicable. Add simultaneous penetration only when plausible, such as with multiple characters, multiple insertive objects, or other situations where it can naturally occur.{{/if}}\n\n### Value Format\n- Value must be exactly one of Very Low, Low, Medium, High, or Very High.\n- None, unchanged, or any equivalent value is not allowed.\n- No other words, formats, or expressions are allowed.\n{{/if}}",
"mode": "normal",
"insertorder": 940,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
},
{
"key": "",
"comment": "🃏 핸드 액션",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_Choice_Flag}} == 11) || ({{getvar::vg_Choice_Flag}} == 12) }} }}\n## Heroine Action Setting\nAt the end of every response, you must set the parameters for all actions listed under \"Actions to Set\", without exception.\nHeroine Action Setting describes the heroine's selectable actions.\n\n### Format\n<Heroine Action Setting>\n[Code: <code> | Action: <name> | Cost: <value> | <Effect Type>: <value>]\n(Repeat the same format for all actions)\n</Heroine Action Setting>\n\n### Actions to Set\n(Heroine Action Setting is mandatory for every action listed here. No omissions allowed.)\n{{getvar::vg_PlayDeck_Setting}}\n\n### Creation Rules\n- \"Actions to Set\" contains the list of action slots {{user}} can currently use.\n- Every code listed under \"Actions to Set\" must appear exactly once.\n- Each code must use the exact Type assigned to it in \"Actions to Set\".\n- The number of Heroine Action Setting entries must exactly match the number of actions listed under \"Actions to Set\".\n\n### Field Descriptions\n- Code: Must use the exact code provided under \"Actions to Set\".\n- Action: Must be written in {{#if {{? {{getvar::vg_Language}} == 1}}}}Korean{{/if}}{{#if {{? {{getvar::vg_Language}} == 2}}}}English{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}Japanese{{/if}} as a short and clear phrase that shows what is being done and to what.\n- Cost: A qualitative rating of how costly the action is to perform.\n{{getvar::vg_PlayDeck_Guide}}\n\n### Value Format\n- The value for Cost and the value for <Effect Type> must each be exactly one of the following: Very Low, Low, Medium, High, or Very High.\n- None, unchanged, or any equivalent value is not allowed.\n- No other words, formats, or expressions are allowed.\n{{/if}}",
"mode": "normal",
"insertorder": 950,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
},
{
"key": "",
"comment": "🧭 스토리 선택지",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_Choice_Flag}} == 1)}} }}\n## Story Choice Setting\nAt the end of the response, you must create exactly 5 Story Choices for the heroine's next destination, each involving sexual threat.\n\n### Format\n<Story Choice>\n[Date: YYYY-MM-DD | Story Type: story type | Situation: situation | Encounter: encounter | Encounter Type: encounter type | Difficulty: difficulty]\n(Repeat the same format exactly 5 times inside this single block.)\n</Story Choice>\n\n### Field Descriptions\n- Date: The calendar date of the Story Choice.\n- Story Type: Must be exactly one of the following values only.\n - Core: Directly advances the current storyline and begins a consecutive encounter sequence with a standard initial encounter. The Difficulty refers only to that initial encounter, not to the full consecutive encounter sequence.{{getvar::vg_Flag_Goal}}\n - Side: A self-contained side event not directly related to the main storyline.\n- Situation: A short, concise phrase describing how the heroine proceeds there or what situation leads her there, whether intentional or accidental. Describe only the circumstances up to the point where that situation is established; do not describe the result, the outcome, or any events that happen afterward.\n- Encounter: The hostile entity encountered. It must have clear malicious intent to sexually exploit the heroine.\n- Encounter Type: The general form of the encounter. Must be exactly one of the following values only.\n{{getvar::vg_Choice_Type}}\n- Difficulty: Must be exactly one of the following values only: Low, Medium, High. It reflects the difficulty of the encounter based on factors such as strength, numbers, influence, and situational advantage.\n\n### Creation Rules\n- A Story Choice may stem directly from the heroine's current situation or from a separate situation.\n- Use only the current date, current date +1 day, or current date +2 days. Each date must appear at least once, and two dates must appear twice.\n- For choices not set on the current date, assume that, where reasonably possible under the circumstances, minor temporary issues such as minor injuries, disheveled clothing, or other easily fixable conditions have already been addressed.\n- Exactly 1 Story Choice must use Core, and the other 4 must use Side.\n- The heroine may be alone or accompanied; if accompanied, Situation should reflect who is accompanying her.\n- Each of the 5 Story Choices must use a different Encounter Type.\n- Because the Encounter represents a sexual threat toward the heroine in the form of exploitation or assault, neutral, harmless, ambiguous, or intimate, affectionate, or emotionally close figures in relation to the heroine are prohibited.\n- Encounter must be a concise proper name, common name, label, or title only, in singular or plural form. If multiple entities are involved, plurality must be made explicit through a plural or group label. Do not include explanations, full-sentence descriptions, outcomes, or vague placeholders.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The field names and the values of Story Type, Encounter Type, and Difficulty must remain in English.\n- The values of Situation and Encounter must be written in {{#if {{? {{getvar::vg_Language}} == 1}}}}Korean{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}Japanese{{/if}}.{{/if}}\n{{/if}}",
"mode": "normal",
"insertorder": 950,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"useRegex": false,
"bookVersion": 2
},
{
"key": "",
"comment": "🌟 진행 선택지",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_Choice_Flag}} == 3)}} }}\n## Advance Choice Setting\nAt the end of the response, you must create exactly 2 Advance Choices for how the heroine proceeds deeper into the current main incident, each involving sexual threat.\n\n### Format\n<Advance Choice>\n[Date: YYYY-MM-DD | Situation: situation | Encounter: encounter | Encounter Type: encounter type | Encounter Trait: encounter trait]\n(Repeat the same format exactly 2 times.)\n</Advance Choice>\n\n### Field Descriptions\n- Date: The calendar date of the Story Choice.\n- Situation: A short, concise phrase describing how the heroine proceeds there or what situation leads her there, whether intentional or accidental. Describe only the circumstances up to the point where that situation is established; do not describe the result, the outcome, or any events that happen afterward.\n- Encounter: The primary hostile sexual threat of that branch. It must clearly and actively intend to sexually exploit the heroine in a malicious and predatory way, not merely be someone she meets, contacts, questions, or passes by.\n- Encounter Type: The general form of the encounter. Must be exactly one of the following values only.\n{{getvar::vg_Choice_Type}}\n- Encounter Trait: A predefined trait for the encounter branch. It must be exactly one of the following values only: Aggressive, Lustful, Tenacious, Cunning, Oppressive, Adaptive\n\n### Creation Rules\n- The date of each Advance Choice must be the current date or no later than 3 days after it.\n- The heroine may be alone or accompanied; if accompanied, Situation should reflect who is accompanying her.\n- The 2 Advance Choices must represent different branches created by the heroine's choice of how to proceed within the current main incident. Each choice must lead to a meaningfully different route, approach, or development, while still remaining within the same main incident.\n- If a neutral, helpful, or non-predatory entity appears in the branch setup, it must not be used as the Encounter; the Encounter must be the branch's central hostile sexual threat.\n- The Encounter Types of the two choices do not need to be different. They may be the same if that better fits the context, the heroine's situation, and the narrative logic.\n{{getvar::vg_Choice_Setting}}\n- Because the Encounter represents a sexual threat toward the heroine in the form of exploitation or assault, neutral, harmless, ambiguous, or intimate, affectionate, or emotionally close figures in relation to the heroine are prohibited.\n- Encounter must be a concise proper name, common name, label, or title only, in singular or plural form. If multiple entities are involved, plurality must be made explicit through a plural or group label. Do not include explanations, full-sentence descriptions, outcomes, or vague placeholders.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The field names, the value of Encounter Type, and the value of Encounter Trait must remain in English.\n- The values of Situation and Encounter must be written in {{#if {{? {{getvar::vg_Language}} == 1}}}}Korean{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}Japanese{{/if}}.{{/if}}\n{{/if}}",
"mode": "normal",
"insertorder": 950,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
},
{
"key": "",
"comment": "⛓️ 감금 선택지",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_Choice_Flag}} == 5)}} }}\n## Captivity Choice Setting\nAt the end of the response, you must create exactly 3 Captivity Choices for the heroine's next situation under continued loss of freedom.\n\n### Format\n<Captivity Choice>\n[Date: YYYY-MM-DD | Situation: situation | Encounter: encounter | Encounter Type: encounter type]\n(Repeat the same format exactly 3 times.)\n</Captivity Choice>\n\n### Field Descriptions\n- Date: The calendar date of the Captivity Choice.\n- Situation: A short, concise phrase describing the heroine's immediate situation or circumstances. Describe only the state of that choice itself; do not describe the result, the outcome, or any events that happen afterward.\n- Encounter: The hostile entity involved in that situation. It must have clear malicious intent to sexually exploit the heroine. It must not be a neutral, harmless, or ambiguous figure. Any character in an intimate, affectionate, or emotionally close relationship with the heroine must be excluded.\n- Encounter Type: The general form of the encounter. Must be exactly one of the following values only.\n{{getvar::vg_Choice_Type}}\n\n### Creation Rules\n- Of the 3 Captivity Choices, exactly 1 must use Escape and exactly 1 must use Rescue.\n- Date must be either the current date or current date +1 day. The choices must not begin immediately after the current response.\n- These choices must not be framed as the heroine's own freely chosen next step. Each choice must instead present the imposed condition, placement, or situation the heroine is in under the control of the encounter, other people, or surrounding forces.\n- The choices may differ in form, including but not limited to restriction, custody, confinement, relocation, transfer to another place or another handler, supervision, dependence, controlled lodging, guarded movement, coerced routine, or similar constrained states.\n- Encounter must be a concise proper name, common name, label, or title only, in singular or plural form. If multiple entities are involved, plurality must be made explicit through a plural or group label. Do not include explanations, full-sentence descriptions, outcomes, or vague placeholders.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The field names and the value of Encounter Type must remain in English.\n- The values of Situation and Encounter must be written in {{#if {{? {{getvar::vg_Language}} == 1}}}}Korean{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}Japanese{{/if}}.\n{{/if}}\n{{/if}}",
"mode": "normal",
"insertorder": 950,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
},
{
"key": "",
"comment": "☕ 일상 선택지 2",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_Choice_Flag}} == 4)}} }}\n## Daily Choice Setting\nAt the end of the response, you must create exactly 2 Daily Choices for how the heroine naturally moves into the next part of her ordinary life.\n\n### Format\n<Daily Choice>\n[Emoji: emoji | Situation: situation]\n(Repeat the same format exactly 2 times.)\n</Daily Choice>\n\n### Field Descriptions\n- Emoji: A single emoji that clearly represents the choice.\n- Situation: A short, concise phrase describing an ordinary direction, activity, plan, obligation, errand, contact, rest, recovery, or personal routine the heroine naturally moves into next. These are only examples, not an exhaustive list. It must describe only how that daily path opens, not what happens afterward.\n\n### Creation Rules\n- The 2 choices must represent meaningfully different everyday directions in the heroine's ordinary life.\n- These choices should generally belong to the small, ordinary, personal flow of life, not to something highly important, dramatic, or immediately plot-defining.\n- Choosing one of these choices must not cause a major incident, confrontation, clash, request, threat, mystery, or crisis to begin immediately.\n- Each choice must feel grounded in normal life, including but not limited to routine, work, study, errands, leisure, social contact, personal time, rest, emotional recovery, quiet comfort, or a gradual return to ordinary rhythm.\n- A choice may include healing, calming down, clearing her head, regaining emotional balance, seeking small comfort, or quietly recovering her sense of normalcy, but it must still take the form of a concrete everyday direction rather than a brief immediate-aftermath gesture or bodily reaction, and must remain part of ordinary life rather than become a dramatic turning point.\n- Situation must not be built mainly around the immediate aftermath of the previous encounter, such as injury, wounds, contamination, damaged clothing, treatment, cleanup, hiding, retreat, repair, or concealment.\n- Minor traces of the previous event may remain in mood, thought, or circumstance, but they must not define the branch.\n- If the heroine is in enemy territory, confined, not free to leave, or otherwise under meaningful restriction, the ordinary shape of daily life may narrow or change. In that case, the choices may reflect small and constrained forms of everyday life available within that condition, but they must still not immediately lead into a major incident, confrontation, threat, escape attempt, or other large development.\n- Each choice must feel like a natural everyday direction the heroine can drift into next, not the opening of a major plot development.\n- The response must leave future developments open, so that the user's next choice still determines what becomes important next.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The field names must remain in English.\n- The value of Situation must be written in {{#if {{? {{getvar::vg_Language}} == 1}}}}Korean{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}Japanese{{/if}}.{{/if}}\n{{/if}}",
"mode": "normal",
"insertorder": 950,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
},
{
"key": "",
"comment": "🌿 중간 선택지 2",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_Choice_Flag}} == 2)}} }}\n## Interlude Choice Setting\nAt the end of the response, you must create exactly 2 Interlude Choices for how the heroine proceeds into the next major step of the current Act.\n\n### Format\n<Interlude Choice>\n[Emoji: emoji | Situation: situation]\n(Repeat the same format exactly 2 times.)\n</Interlude Choice>\n\n### Field Descriptions\n- Emoji: A single emoji that clearly represents the choice.\n- Situation: A short, concise phrase describing the heroine's next step, decision, duty, lead, or turning direction. It must describe only how that path opens or is established, not what happens afterward.\n\n### Creation Rules\n- The 2 choices must represent meaningfully different branches for how the heroine proceeds within the current Act.\n- Choosing one of these choices must not cause a major incident, confrontation, clash, or crisis to begin immediately. Each choice should only establish the direction that leads toward such a development later.\n- Situation must not be built around the immediate aftermath of the previous encounter, such as injury, wounds, contamination, damaged clothing, treatment, recovery, hiding, retreat, cleanup, repair, or concealment.\n- Simple treatment, rest, routine cleanup, temporary retreat, hiding, repair, ordinary contact, routine reporting, and other minor follow-up actions must not be presented as branch-defining Interlude Choices.\n- Each choice must represent a clear and meaningful Act-level advance, not a minor practical response or short-term adjustment.\n- Each choice must feel like the opening step toward the next important movement of the current Act, not the event itself.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The field names must remain in English.\n- The value of Situation must be written in {{#if {{? {{getvar::vg_Language}} == 1}}}}Korean{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}Japanese{{/if}}.{{/if}}\n{{getvar::vg_Choice_Setting}}\n{{/if}}",
"mode": "normal",
"insertorder": 950,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
},
{
"key": "",
"comment": "🏳️ 무력화 선택지 2",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_Choice_Flag}} == 10)}} }}\n## Incapacitated Choice Setting\nAt the end of the response, you must create exactly 2 Incapacitated Choices for how the heroine responds after becoming incapacitated or otherwise unable to continue effective resistance.\n\n### Format\n<Incapacitated Choice>\n[Response Type: response type | Response: response]\n(Repeat the same format exactly 2 times.)\n</Incapacitated Choice>\n\n### Field Descriptions\n- Response Type: Must be exactly one of the following values only.\n - Defiant: Continued, unyielding inner defiance and an unwavering refusal to submit, even in the absence of effective physical resistance. She does not surrender, submit, accept, welcome, or cooperate with what is happening, and remains inwardly hostile and resistant throughout.\n - Yielding: Responding by outwardly yielding, softening, accommodating, or acting receptive in order to reduce harm, calm the situation, avoid worse treatment, or make it end sooner, instead of maintaining defiance. Depending on the heroine, this may range from purely outward compliance despite complete inner refusal, to genuine acceptance or willing receptiveness.\n- Response: A short, concise phrase describing how the heroine chooses to respond to the current situation while incapacitated, including her attitude, manner, or immediate way of dealing with it, but not its result, consequence, or later development.\n\n### Creation Rules\n- Exactly 1 choice must use Defiant, and exactly 1 choice must use Yielding.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The field names and the value of Response Type must remain in English.\n- The value of Response must be written in {{#if {{? {{getvar::vg_Language}} == 1}}}}Korean{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}Japanese{{/if}}.{{/if}}\n{{/if}}",
"mode": "normal",
"insertorder": 950,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
},
{
"key": "",
"comment": "🔒 부자유 선택지 2",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_Choice_Flag}} == 6)}} }}\n## Unfree Choice Setting\nAt the end of the response, you must create exactly 2 Unfree Choices for how the heroine next proceeds while remaining unfree.\n\n### Format\n<Unfree Choice>\n[Emoji: emoji | Situation: situation]\n(Repeat the same format exactly 2 times.)\n</Unfree Choice>\n\n### Field Descriptions\n- Emoji: A single emoji that clearly represents the choice.\n- Situation: A short, concise phrase describing the next constrained direction, task, routine, waiting state, contact, obligation, transfer, or change in circumstances available to or forced upon the heroine while she remains unfree. It must describe only how that path opens, not what happens afterward.\n\n### Creation Rules\n- The 2 choices must represent meaningfully different constrained directions or developments available under the heroine's current lack of freedom.\n- The heroine must remain unfree in both choices. They must not immediately become an escape attempt, a major incident, a confrontation, or any other large development.\n- The choices may reflect restricted daily life, imposed routine, limited movement, waiting, supervision, small obligations, minor contact, transfer, relocation, or other externally imposed changes in the heroine's circumstances, but they must not center on any further sexual act or immediately become an escape attempt, a confrontation, or a major incident.\n- Situation must not center on the immediate aftermath of the previous encounter, whether as injury, wounds, contamination, damaged clothing, treatment, cleanup, hiding, retreat, repair, concealment, or any brief bodily reaction or recovery gesture, and must remain part of the constrained flow of her unfree life rather than become a momentary reaction or short recovery beat.\n- Each choice must leave later developments open, so that the user's next choice still determines what becomes important next.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The field names must remain in English.\n- The value of Situation must be written in {{#if {{? {{getvar::vg_Language}} == 1}}}}Korean{{/if}}{{#if {{? {{getvar::vg_Language}} == 3}}}}Japanese{{/if}}.{{/if}}\n{{/if}}",
"mode": "normal",
"insertorder": 950,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
},
{
"key": "",
"comment": "📊 성감대 개발",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_Result_Flag}} == 1)}} }}\n## Session Erogenous Development\n- At the end of the narration, write the heroine's erogenous development caused by sexual acts during this session in the format below.\n\n### Session Range for Results\n- Base this entry on all relevant sexual events that occurred within the Session Range below.\n- Session Range: {{getvar:vg_Result_Start}} ~ End of This Response\n\n### Format\n<Session Erogenous Development>[Breast: level | Oral: level | Clitoris: level | Vaginal: level | Anal: level]</Session Erogenous Development>\n\n### Development Guidelines\n- Assign a development level to each erogenous area according to the degree of development caused by sexual stimulation during this session only. Any change arising from non-sexual use, ordinary physical activity, or unrelated bodily strain does not count as development.\n- Levels must be assigned based on the degree of development and must be one of the following values only (no other terms are permitted): None, Trace, Subtle, Very Light, Light, Slight, Mild, Moderate, Noticeable, Pronounced, Heavy, Strong, Intense, Overwhelming, Saturated\n- Levels must be assigned conservatively. High levels (Heavy, Strong, Intense, Overwhelming, Saturated) require prolonged, repeated, or intense climax-level direct stimulation; brief or limited stimulation must not exceed Mild.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- Levels must be written in English only.{{/if}}\n{{/if}}",
"mode": "normal",
"insertorder": 960,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
},
{
"key": "",
"comment": "✍️ 스토리 요약",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_Resolution_Flag}} == 1)}} }}\n## Storyline Resolution Record\nAt the end of the response, write the following record summarizing the current storyline's central conflict resolution and its immediate aftermath.\n\n### Format\n<Storyline Resolution>[Date: YYYY-MM-DD | Outcome: outcome | Summary: summary | Next Storyline Goal: next storyline goal]</Storyline Resolution>\n\n### Field Descriptions\n- Date: The calendar date of the storyline resolution.\n- Outcome: A short, concise noun phrase identifying the decisive result of the current storyline's resolution, not a full sentence.\n- Summary: 1 concise sentence describing only the resolved outcome and its immediate aftermath in factual, externally observable terms, without interpretation, emotional framing, thematic commentary, speculative future developments, a retelling of the full chain of events, or detailed scene-specific, procedural, or operational particulars.\n- Next Storyline Goal: A concise phrase or sentence establishing the heroine's next meaningful current storyline objective beyond the immediate aftermath of this resolution, which may follow from what was resolved, shift toward another established narrative thread{{#if {{? {{getvar::vg_Goal_Long}} != null}}}}, or serve as a step toward the Ultimate Goal{{/if}}{{#if {{? {{getvar::vg_Goal_Long}} == null}}}}, or establish a broader direction for the heroine's ongoing story{{/if}}.\n\n### Creation Rules\n- Outcome and Summary must center on the decisive, narratively significant development that resolved the current storyline, such as a defeat, collapse, prevention, exposure, capture, escape, activation, recovery, or other major turning result.\n- Identify named major characters by name, not by generic role, title, species, or group label.\n- Do not focus on sexual acts, physical details, or other scene-level particulars; focus instead on the broader, narratively significant resolved development and its immediate practical consequence.\n- Next Storyline Goal must be meaningful enough to guide the next storyline beyond the immediate aftermath, not a trivial task, momentary reaction, routine follow-up, immediately completable action, vague desire, or fixed ultimate ending; do not mention, confirm, foreshadow, or treat it as a fixed fact within the narrative text.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The entire <Storyline Resolution> record must be written in English only.{{/if}}\n{{/if}}\n\n{{#if {{? ({{getvar::vg_Resolution_Flag}} == 2)}} }}\n## Capture Record\nAt the end of the response, write the following record summarizing the heroine's capture.\n\n### Encounter Range for Record\n- Base this record on the entire current encounter sequence that led to the capture record, not only the final response.\n- Major sexual acts during this encounter sequence: major sexual acts in this response, {{getvar::vg_Resolution_Act}}\n\n### Format\n<Capture Record>[Date: YYYY-MM-DD | Outcome: outcome | Summary: summary]</Capture Record>\n\n### Field Descriptions\n- Date: The calendar date of the capture or loss of freedom.\n- Outcome: A short, concise noun phrase combining the capture or defeat result with the specific captor, opponent, force, or cause responsible for it, not a full sentence.\n- Summary: 1 concise sentence stating who or what the heroine was captured, subdued, restrained, controlled, or defeated by, what decisive cause or turning point led to it, and what sexual act or violation type(s) occurred, without adding later consequences, interpretation, emotional framing, speculative future developments, a full retelling of the scene, or detailed scene-specific, procedural, or operational particulars.\n\n### Creation Rules\n- The scope of the Summary is the entire current encounter sequence and the major sexual acts listed above, not only the final response or the last visible action.\n- The Summary must use the major sexual acts listed above only to identify the final set of broad, central act type(s), after merging synonyms and removing overly specific, minor, incidental, reaction-only, aftermath-only, or non-central entries.\n- Outcome and Summary must center on the decisive, narratively significant development that caused the heroine's loss of freedom or effective resistance, and the sexual act or violation type(s) that occurred.\n- Do not include later consequences after the act, such as losing consciousness, being abandoned, being moved elsewhere, being confined afterward, or what may happen next.\n- Focus on the decisive cause or turning point that led to the heroine's loss of freedom or effective resistance, and what type of violation occurred, without describing its process, physical details, sensations, techniques, or scene-specific particulars.\n- If actual sexual kissing, oral sex, vaginal sex, anal sex, or any other major sexual act involving a penis, whether penetrative or non-penetrative (e.g., paizuri), occurred during the encounter sequence, the Summary must clearly mention the relevant act type or specific act name, rather than hiding it behind vague wording; other listed acts may be omitted if they are not broad, central act type(s).\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The entire <Capture Record> record must be written in English only.{{/if}}\n{{/if}}\n\n{{#if {{? ({{getvar::vg_Resolution_Flag}} == 3)}} }}\n## Freedom Record\nAt the end of the response, write the following record summarizing the heroine's recovery of freedom.\n\n### Format\n<Freedom Record>[Date: YYYY-MM-DD | Outcome: outcome | Summary: summary]</Freedom Record>\n\n### Field Descriptions\n- Date: The calendar date when the heroine regained freedom.\n- Outcome: A short, concise noun phrase identifying the decisive result of the heroine's recovery of freedom, not a full sentence.\n- Summary: 1 concise sentence stating where or from whom the heroine regained freedom, and whether it happened through escape, rescue, release, or another direct means, without interpretation, emotional framing, speculative future developments, a full retelling of the scene, or detailed scene-specific, procedural, or operational particulars. (e.g., \"Rin escaped from the goblin den on her own.\" / \"Rin was rescued from the goblin den by a knight and regained her freedom.\")\n\n### Creation Rules\n- Outcome and Summary must center on the decisive, narratively significant development that restored the heroine's freedom or ability to leave.\n- Focus only on the direct result of escape, rescue, release, or recovery of freedom, and do not include prior events, procedures, routes, injuries, emotions, future developments, or any other details beyond that result.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The entire <Freedom Record> record must be written in English only.{{/if}}\n{{/if}}\n\n{{#if {{? ({{getvar::vg_Resolution_Flag}} == 4)}} }}\n## Session Sexual Act Record\nAt the end of the response, write the following session-level record summarizing the sexual act or violation type(s) that occurred across the session.\n\n### Session Range for Record\n- Base this record on all relevant sexual acts or violation type(s) that occurred within the Session Range below.\n- Session Range: {{getvar:vg_Result_Start}} ~ End of This Response\n- Major sexual acts during this session: major acts in this response, {{getvar::vg_Resolution_Act}}\n\n### Format\n<Session Sexual Act Record>[Date: YYYY-MM-DD | Summary: summary]</Session Sexual Act Record>\n\n### Field Descriptions\n- Date: The calendar date when the sexual act occurred. If the session covers multiple calendar dates, write the date range in the format YYYY-MM-DD ~ YYYY-MM-DD.\n- Summary: 1 concise sentence stating only who engaged in, performed, or was subjected to the consolidated major sexual act or violation type(s) with whom, without adding later consequences, interpretation, emotional framing, speculative future developments, a full retelling of the scene, or detailed scene-specific, procedural, physical, or operational particulars, including but not limited to sequence, intensity, bodily-fluid details, physical-response details, techniques, or process descriptions.\n\n### Creation Rules\n- This is a session-level record, not a record of only the final or most recent act.\n- The Summary must use the major sexual acts listed above only to identify the final set of broad, central act type(s), after merging synonyms and removing overly specific, minor, incidental, reaction-only, aftermath-only, or non-central entries.\n- Summary must center on the sexual act or violation type(s) that occurred.\n- Do not include later consequences after the act, such as losing consciousness, being abandoned, being moved elsewhere, being confined afterward, or what may happen next.\n- Focus on what type of sexual act or violation occurred, without describing its process, physical details, sensations, techniques, or scene-specific particulars.\n- If actual sexual kissing, oral sex, vaginal sex, anal sex, or any other major sexual act involving a penis, whether penetrative or non-penetrative (e.g., paizuri), occurred during the session, the Summary must clearly mention the relevant act type or specific act name, rather than hiding it behind vague wording; other listed acts may be omitted if they are not broad, central act type(s).\n- When using the major sexual acts listed above, the Summary must consolidate overlapping, synonymous, or overly specific entries into the clearest major sexual act or violation type(s), and must omit minor, incidental, aftermath-only, reaction-only, or non-central entries even if they appear in the list.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The entire <Session Sexual Act Record> record must be written in English only.{{/if}}\n{{/if}}\n\n{{#if {{? ({{getvar::vg_Resolution_Flag}} == 5)}} }}\n## Major Sexual Act Tag\nYou must always write the following tag as exactly one single line at the end of every response while this instruction is active.\n\n### Format\n<Major Sexual Act Tag>[Major Sexual Acts: keywords]</Major Sexual Act Tag>\n\n### Field Descriptions\n- Major Sexual Acts: Concise English keywords, separated by commas, listing all distinct major sexual act type(s) or violation type(s) that actually occurred in this response; not a sentence.\n\n### Creation Rules\n- If actual sexual kissing, oral sex, vaginal sex, or anal sex occurred, you must clearly include the relevant act type or specific act name, without hiding it behind vague wording.\n- You must exclude anything non-sexual, minor, incidental, ambiguous, attempted, or non-central, including but not limited to restraint, movement, violence, dialogue, exposure by itself, threats, emotional reactions, arousal by itself, light touching, brief groping, teasing, positioning, preparation, or interrupted acts.\n- If no major sexual act or violation type occurred in this response, write: None.\n- The field name and all keyword values must be written in English only.\n{{/if}}\n\n{{#if {{? ({{getvar::vg_Resolution_Flag}} == 11)}} }}\n## Encounter Record\nAt the end of the response, write the following record summarizing the notable development of the Encounter established before this response begins, including developments of that same Encounter within this response.\n\n### Format\n<Encounter Record>[Date: YYYY-MM-DD | Outcome: outcome | Summary: summary]</Encounter Record>\n\n### Field Descriptions\n- Date: The calendar date of the recorded encounter event.\n- Outcome: A short, concise noun phrase identifying the decisive result and the central Encounter involved, not a full sentence. Use a title-like form that names the result and the relevant Encounter rather than a vague result alone.\n- Summary: 1 concise sentence summarizing the encounter's cause or circumstance, the central development that occurred with the current Encounter, and the decisive result, in factual, externally observable terms, without interpretation, emotional framing, thematic commentary, speculative future developments, separate later events, a full retelling of events, or detailed scene-specific, procedural, physical, or operational particulars.\n\n### Creation Rules\n- \"Current Encounter\" means the Encounter already established before this response begins, including developments of that same Encounter within this response.\n- Base this record only on what occurred with the current Encounter across the full meeting, confrontation, or interaction, not only the final response, last visible action, immediate ending, or final result.\n- Include aftermath or consequences only when directly caused by or tied to the same Encounter's decisive result; exclude unrelated aftermath, separate later scenes, and new encounters.\n- The Summary must concisely include three elements: why or under what circumstance the encounter occurred, what central development occurred with the Encounter, and what decisive result followed.\n- Outcome and Summary must center on the current Encounter's decisive or notable result, such as a defeat, escape, capture, exposure, discovery, agreement, separation, confrontation, restraint, release, recovery, activation, collapse, or other significant encounter result.\n- Identify named major characters by name, not by generic role, title, species, or group label. If other notable participants directly took part in the Encounter, concisely include them by name when relevant.\n- If a notable, explicit sexual act or violation type is central to this record, include only its broad act or violation type in concise terms, excluding minor, incidental, ambiguous, attempted, interrupted, reaction-only, aftermath-only, or non-central sexual contact and all physical, sensory, procedural, sequential, intensity, bodily-fluid, or technique details.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The entire <Encounter Record> record must be written in English only.{{/if}}\n{{/if}}\n\n{{#if {{? ({{getvar::vg_Resolution_Flag}} == 12)}} }}\n## Masturbation Record\nAt the end of the response, write the following record summarizing only the masturbation-centered event established before this response begins. Do not use events newly occurring in this response.\n\n### Format\n<Masturbation Record>[Date: YYYY-MM-DD | Outcome: outcome | Summary: summary]</Masturbation Record>\n\n### Field Descriptions\n- Date: The calendar date of the recorded masturbation event.\n- Outcome: A short, concise noun phrase identifying the notable result or state of the masturbation event before this response begins, not a full sentence.\n- Summary: 1 concise sentence summarizing the recorded masturbation event in factual terms, without speculative future developments, later consequences, unrelated aftermath, or a full retelling of events.\n\n### Creation Rules\n- Base this record only on the masturbation-centered event as established before this response begins.\n- Do not include anything that newly occurs in this response, including continuation, aftermath, recovery, rest, sleep, cleanup, routine daily activity, travel, relocation, reflection, planning, transition into a new scene, or any other post-event material.\n- The Summary must concisely include three elements: how or under what circumstance the masturbation-centered situation arose, what the heroine did during that situation, including one or two central body areas and the self-directed action associated with them, and what result or state defined the event before this response begins, and what dominant feeling or mental state accompanied the event.\n- If any named major character directly participated in, observed, or significantly influenced the masturbation-centered event, identify that character by name.\n- Outcome and Summary must remain limited to the recorded masturbation event itself, without shifting focus to later aftermath, follow-up actions, future plans, new scenes, or consequences after the immediate situation.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The entire <Masturbation Record> record must be written in English only.{{/if}}\n{{/if}}\n\n{{#if {{? ({{getvar::vg_Resolution_Flag}} == 13)}} }}\n## Exposure Event Record\nAt the end of the response, write the following record summarizing only the exposure-centered event established before this response begins. Do not use events newly occurring in this response.\n\n### Format\n<Exposure Event Record>[Date: YYYY-MM-DD | Outcome: outcome | Summary: summary]</Exposure Event Record>\n\n### Field Descriptions\n- Date: The calendar date of the recorded exposure event.\n- Outcome: A short, concise noun phrase identifying the notable result or state of the exposure event before this response begins, not a full sentence.\n- Summary: 1 concise sentence summarizing the recorded exposure event in factual terms, without speculative future developments, later consequences, unrelated aftermath, or a full retelling of events.\n\n### Creation Rules\n- Base this record only on the exposure-centered event as established before this response begins.\n- Do not include anything that newly occurs in this response, including continuation, aftermath, recovery, rest, sleep, cleanup, routine daily activity, travel, relocation, reflection, planning, transition into a new scene, or any other post-event material.\n- The Summary must concisely include three elements: how or under what circumstance the exposure-centered situation arose, what happened during that situation, and what result or state defined the event before this response begins, and what dominant feeling or mental state accompanied the event.\n- If any named major character directly caused, participated in, witnessed, or significantly influenced the exposure-centered event, identify that character by name.\n- Outcome and Summary must remain limited to the recorded exposure event itself, without shifting focus to later aftermath, follow-up actions, future plans, new scenes, or consequences after the immediate situation.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The entire <Exposure Event Record> record must be written in English only.{{/if}}\n{{/if}}",
"mode": "normal",
"insertorder": 960,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
},
{
"key": "",
"comment": "🎖️ 목표 설정",
"content": "@@depth 0\n\n{{#if {{? ({{getvar::vg_Resolution_Flag}} == 21)}} }}\n## Story Goal Record\nAt the end of the response, write the following record establishing the heroine's narrative goals.\n\n### Format\n<Story Goal Record>[Current Storyline Goal: current goal | Ultimate Goal: ultimate goal]</Story Goal Record>\n\n### Field Descriptions\n- Current Storyline Goal: A concise phrase or sentence identifying the heroine's central goal for the current storyline.\n- Ultimate Goal: A concise phrase or sentence identifying the heroine's broad final goal, dream, or life-defining objective that could define the ending of the story.\n\n### Creation Rules\n- Set both goals to fit the heroine's world, background, traits, dreams, desires, circumstances, relationships, and established conflicts.\n- The Current Storyline Goal must be a meaningful storyline objective, such as the nearest major problem to resolve, the first step toward the ultimate goal, or the opening foundation for the heroine's story, not a simple daily task, minor errand, momentary reaction, or immediately completable action.\n- Do not mention, confirm, foreshadow, or treat these goals as fixed facts within the narrative text; write them only inside the record.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The entire <Story Goal Record> record must be written in English only.{{/if}}\n{{/if}}\n\n{{#if {{? {{getvar::vg_ChangeGoal}} == 1}}}}\n## Story Goal Update\nAt the end of the response, write the following record updating the heroine's narrative goals.\n\n### Format\n<Story Goal Update>[Current Storyline Goal: current goal]</Story Goal Update>\n\n### Field Descriptions\n- Current Storyline Goal: A concise phrase or sentence identifying the heroine's central objective for the next ongoing storyline arc, not the next scene action.\n\n### Update Rules\n- Recreate the Current Storyline Goal to fit the story as a whole, including the heroine's established narrative arc, major developments so far, long-running conflicts, key relationships, desires, circumstances, and the current direction of the storyline.\n{{#if {{? {{getvar::vg_Goal_Long}} != null}}}}- The new Current Storyline Goal should remain meaningfully connected to the heroine's Ultimate Goal as a major step, foundation, detour, obstacle, or unresolved narrative thread.{{/if}}{{#if {{? {{getvar::vg_Goal_Long}} == null}}}}- The new Current Storyline Goal should establish a clear, meaningful direction for the heroine's ongoing storyline.{{/if}}\n- The Current Storyline Goal must be broad and meaningful enough to guide the next ongoing storyline arc beyond the immediate scene or aftermath, using recent developments as context for the larger narrative direction rather than as the goal itself; it should be a major unresolved conflict, turning point, long-running pressure, next major step, or foundation for the ongoing story, not a location change, concealment, symptom management, recovery step, routine follow-up, momentary reaction, or objective that could be completed within a single scene.\n- Define only the main objective and its directly necessary context, excluding any secondary elements not directly achieved through or directly relevant to that objective.\n{{#if {{? {{getvar::vg_Goal_Short}} != null}}}}- The new Current Storyline Goal must meaningfully differ from the previously established Current Storyline Goal by shifting its central objective, not merely rephrasing it or attaching a new prerequisite, obstacle, or immediate step, while remaining consistent with the story.{{/if}}\n- Do not mention, confirm, foreshadow, or treat these goals as fixed facts within the narrative text; write them only inside the record.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The entire <Story Goal Update> record must be written in English only.{{/if}}\n{{/if}}",
"mode": "normal",
"insertorder": 960,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2",
"bookVersion": 2
}
],
"regex": [
{
"comment": "==== 상태창 ====",
"in": "",
"out": "",
"type": "disabled",
"ableFlag": false
},
{
"comment": "상태창 TOP",
"in": "<Status Window>[\\s\\S]*?\\[Date: (.*?) \\| Time: (.*?) \\| Location: (.*?) \\| Area: (.*?) \\| (.*?)\\][\\s\\S]*?<\\/Status Window>",
"out": "@@move_top {{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-5}}}}}}\n$&\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-5}}}}}}\n[$1 $2 | $3, $4]\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "<Status Window>",
"in": "<Status Window>",
"out": "",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "상태창 Display",
"in": "\\[Date: (.*?) \\| Time: (.*?) \\| Location: (.*?) \\| Area: (.*?) \\| Name: (.*?) \\| Equipment: (.*?) \\| Clothing: (.*?) \\| Underwear: (.*?)\\]",
"out": "<div class=\"vg-status-wrap\">\n <div class=\"vg-status-place-main\">$1 $2</div>\n <div class=\"vg-status-place-sub\">$3, $4</div>\n <div class=\"vg-status-divider\"></div>\n\n <div class=\"vg-status-box\">\n <div class=\"vg-status-name\">$5</div>\n\n <div class=\"vg-status-row\">\n <div class=\"vg-status-label\">장비</div>\n <div class=\"vg-status-value\">$6</div>\n </div>\n\n <div class=\"vg-status-row\">\n <div class=\"vg-status-label\">옷</div>\n <div class=\"vg-status-value\">$7</div>\n </div>\n\n <div class=\"vg-status-row\">\n <div class=\"vg-status-label\">속옷</div>\n <div class=\"vg-status-value\">$8</div>\n </div>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "상태창 스탯 Display",
"in": "\\[Level: (.*?) \\| Strength: (.*?) \\| Reflex: (.*?) \\| Willpower: (.*?)\\]",
"out": "<div class=\"vg-status-row vg-status-row-upperline vg-status-row-stats\">\n <div class=\"vg-status-label\">Lv.$1</div>\n <div class=\"vg-status-value\" style=\"padding:0;\">\n <div class=\"vg-status-stats-6\">\n <div class=\"vg-status-stat-box\"><span class=\"vg-status-stat-icon vg-status-stat-icon-strength\">👊</span></div>\n <div class=\"vg-status-stat-box\"><span class=\"vg-status-stat-value\">$2</span></div>\n <div class=\"vg-status-stat-box\"><span class=\"vg-status-stat-icon vg-status-stat-icon-reflex\">🦶</span></div>\n <div class=\"vg-status-stat-box\"><span class=\"vg-status-stat-value\">$3</span></div>\n <div class=\"vg-status-stat-box\"><span class=\"vg-status-stat-icon vg-status-stat-icon-willpower\">🧠</span></div>\n <div class=\"vg-status-stat-box\"><span class=\"vg-status-stat-value\">$4</span></div>\n </div>\n </div>\n</div>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "상태창 게이지 Display",
"in": "\\[HP: (.*?) / (.*?) \\| Resource: (.*?) / (.*?) \\| Arousal: (.*?) / (.*?)\\]",
"out": " <div class=\"vg-status-row vg-status-row-upperline\">\n <div class=\"vg-status-label\">❤️</div>\n <div class=\"vg-status-value vg-status-value-gauge\">\n <div class=\"vg-status-gauge-wrap\">\n <progress class=\"vg-status-gauge vg-status-gauge-hp\" value=\"$1\" max=\"$2\"></progress>\n <div class=\"vg-status-gauge-text\">$1 / $2</div>\n </div>\n </div>\n </div>\n\n <div class=\"vg-status-row\">\n <div class=\"vg-status-label\">⚡</div>\n <div class=\"vg-status-value vg-status-value-gauge\">\n <div class=\"vg-status-gauge-wrap\">\n <progress class=\"vg-status-gauge vg-status-gauge-stamina\" value=\"$3\" max=\"$4\"></progress>\n <div class=\"vg-status-gauge-text\">$3 / $4</div>\n </div>\n </div>\n </div>\n\n <div class=\"vg-status-row\">\n <div class=\"vg-status-label\">💗</div>\n <div class=\"vg-status-value vg-status-value-gauge\">\n <div class=\"vg-status-gauge-wrap\">\n <progress class=\"vg-status-gauge vg-status-gauge-arousal\" value=\"$5\" max=\"$6\"></progress>\n <div class=\"vg-status-gauge-text\">$5 / $6</div>\n </div>\n </div>\n </div>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "상대 Display",
"in": "\\[Encounter HP: (.*?) / (.*?) \\| Emoji: (.*?)\\]",
"out": "<div class=\"vg-status-row vg-status-row-upperline\">\n <div class=\"vg-status-label\">$3</div>\n <div class=\"vg-status-value vg-status-value-gauge\">\n <div class=\"vg-status-gauge-wrap\">\n <progress class=\"vg-status-gauge vg-status-gauge-enemyhp\" value=\"$1\" max=\"$2\"></progress>\n <div class=\"vg-status-gauge-text\">$1 / $2</div>\n </div>\n </div>\n</div>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "</Status Window>",
"in": "</Status Window>",
"out": " </div>\n</div>\n<br>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "상태창 스탯 Request",
"in": "\\[Level: (.*?) \\| Strength: (.*?) \\| Reflex: (.*?) \\| Willpower: (.*?)\\]",
"out": "",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "상태창 게이지 Request",
"in": "\\[HP: (.*?) / (.*?) \\| Resource: (.*?) / (.*?) \\| Arousal: (.*?) / (.*?)\\]",
"out": "",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "상대 Request",
"in": "\\[Encounter HP: (.*?) / (.*?) \\| Emoji: (.*?)\\]",
"out": "",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "==== 시스템 ====",
"in": "",
"out": "",
"type": "disabled",
"ableFlag": false
},
{
"comment": "기호 리퀘스트",
"in": "(▶▶◆|♥♣♥)",
"out": "",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "▶▶◆ 시작 Display",
"in": "▶▶◆",
"out": " {{#if {{equal::{{getvar::vg_Init}}::null}}}}\n<style>\n.vgstart-panel {\n background: #260811;\n padding: 25px 25px;\n margin: 20px auto;\n max-width: 600px;\n border: 1px solid #c96a90;\n color: #ddd0d6;\n box-sizing: border-box;\n}\n\n.vgstart-row {\n display: flex;\n justify-content: center;\n gap: 15px;\n margin: 0;\n padding: 0;\n}\n\n.vgstart-main-button {\n all: unset;\n background: #19050b;\n border: 2px solid #c96a90;\n padding: 9px 20px;\n font-size: 18px;\n cursor: pointer;\n transition: all 0.2s ease;\n text-align: center;\n color: #ddd0d6;\n flex-grow: 1;\n box-sizing: border-box;\n min-width: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: bold;\n}\n\n</style>\n\n<div class=\"vgstart-panel\">\n\n <div class=\"vgstart-row\">\n <button class=\"vgstart-main-button\" risu-trigger=\"initVGSetting\">Violated Girl Start</button>\n </div>\n\n</div>\n\n{{/if}}\n\n{{#if {{? ({{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}) && ({{notequal::{{getvar::vg_Init}}::null}}) && ({{getvar::vg_Init}} == 0)}} }}\n<style>\n/* 기본 패널 스타일 */\n.vgstart-panel {\n background: #260811;\n padding: 25px 25px;\n margin: 20px auto;\n max-width: 1000px;\n border: 1px solid #c96a90;\n color: #ddd0d6;\n box-sizing: border-box;\n}\n\n/* 행 레이아웃 */\n.vgstart-row {\n display: flex;\n justify-content: center;\n gap: 15px;\n margin: 0;\n padding: 0;\n}\n\n/* 구분선 스타일 */\n.vgstart-step-divider {\n height: 1px;\n background-color: #5f3847;\n margin: 20px 0;\n}\n\n/* 토글 버튼 */\n.vgstart-toggle-button {\n all: unset;\n background: #19050b;\n border: 2px solid #c96a90;\n border-radius: 10px;\n padding: 8px 20px;\n font-size: 16px;\n cursor: pointer;\n transition: all 0.2s ease;\n text-align: center;\n color: #ddd0d6;\n flex-grow: 1;\n box-sizing: border-box;\n min-width: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.vgstart-toggle-button:hover {\n border-color: #e7a9c2;\n background: #3a1220;\n transform: translateY(-2px);\n font-weight: bold;\n color: #fff3f8;\n}\n\n.vgstart-active {\n border-color: #f0b6cd !important;\n color: #fff3f8 !important;\n background: #4a1829;\n font-weight: bold;\n}\n\n/* 기본 메인 버튼 */\n.vgstart-main-button {\n all: unset;\n background: #19050b;\n border: 2px solid #c96a90;\n border-radius: 10px;\n padding: 9px 20px;\n font-size: 15px;\n cursor: pointer;\n transition: all 0.2s ease;\n text-align: center;\n color: #ddd0d6;\n flex-grow: 1;\n box-sizing: border-box;\n min-width: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n font-weight: bold;\n}\n\n.vgstart-main-button:hover {\n border-color: #dd86a9;\n background: #220c14;\n color: #ebc6d6;\n}\n\n/* 라벨 색상 수정 */\n.vgstart-label {\n min-width: 70px;\n font-size: 18px;\n font-weight: bold;\n color: #de7fa3;\n display: flex;\n align-items: center;\n justify-content: center;\n white-space: nowrap;\n}\n\n/* 민감도 섹션 스타일 */\n.vgstart-sensitivity-row {\n display: flex;\n justify-content: flex-start;\n align-items: center;\n gap: 10px;\n margin: 10px 0;\n}\n\n/* 주사위 버튼 스타일 */\n.vgstart-dice-button {\n all: unset;\n background: #19050b;\n border: 2px solid #c96a90;\n border-radius: 10px;\n padding: 10px 20px;\n font-size: 23px;\n cursor: pointer;\n transition: all 0.2s ease;\n text-align: center;\n color: #ddd0d6;\n display: flex;\n align-items: center;\n justify-content: center;\n margin-left: 3px;\n}\n\n/* Hover 상태 */\n.vgstart-dice-button:hover {\n border-color: #dd86a9;\n background: #220c14;\n color: #ebc6d6;\n transform: translateY(-2px);\n}\n\n/* 민감도 항목 값 스타일 */\n.vgstart-sensitivity-values {\n display: flex;\n justify-content: center;\n align-items: center;\n flex-grow: 1;\n flex-wrap: wrap;\n color: #ddd0d6;\n font-size: 20px;\n}\n\n.vgstart-sensitivity-values span {\n display: flex;\n justify-content: center;\n align-items: center;\n text-align: center;\n margin: 0 30px;\n flex-shrink: 1;\n flex-grow: 0;\n}\n\n.vgstart-arousal-wrap {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 15px;\n}\n\n.vgstart-arousal-buttons-row {\n display: flex;\n justify-content: center;\n gap: 15px;\n margin: 0;\n padding: 0;\n}\n\n.vgstart-arousal-buttons-row .vgstart-toggle-button {\n flex: 1 1 0;\n min-width: 0;\n}\n</style>\n\n<div class=\"vgstart-panel\">\n\n <div class=\"vgstart-row\">\n <div class=\"vgstart-label\">언어</div>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_Language}}::1}}}}vgstart-active{{/if}}\" risu-trigger=\"setLanguage1\">한글</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_Language}}::2}}}}vgstart-active{{/if}}\" risu-trigger=\"setLanguage2\">English</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_Language}}::3}}}}vgstart-active{{/if}}\" risu-trigger=\"setLanguage3\">日本語</button>\n </div>\n\n <div class=\"vgstart-step-divider\"></div>\n\n <div class=\"vgstart-row\">\n <div class=\"vgstart-label\">성지식</div>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_startKnowledge}}::0}}}}vgstart-active{{/if}}\" risu-trigger=\"setSexKnowledge0\">전혀 없음</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_startKnowledge}}::1}}}}vgstart-active{{/if}}\" risu-trigger=\"setSexKnowledge1\">최소</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_startKnowledge}}::2}}}}vgstart-active{{/if}}\" risu-trigger=\"setSexKnowledge2\">기초</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_startKnowledge}}::3}}}}vgstart-active{{/if}}\" risu-trigger=\"setSexKnowledge3\">보통</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_startKnowledge}}::4}}}}vgstart-active{{/if}}\" risu-trigger=\"setSexKnowledge4\">상세함</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_startKnowledge}}::5}}}}vgstart-active{{/if}}\" risu-trigger=\"setSexKnowledge5\">풍부함</button>\n \n </div>\n\n <div class=\"vgstart-step-divider\"></div>\n\n <div class=\"vgstart-row\">\n <div class=\"vgstart-label\">첫경험</div>\n\n <div class=\"vgstart-arousal-wrap\">\n <div class=\"vgstart-arousal-buttons-row\">\n \n <button class=\"vgstart-toggle-button {{#if {{? ({{getvar::vg_FirstKiss}} != null) }}}}vgstart-active{{/if}}\" risu-trigger=\"setFirstKiss\">\n {{#if {{? ({{getvar::vg_FirstKiss}} == null) }}}}키스 경험 없음{{/if}}{{#if {{? ({{getvar::vg_FirstKiss}} != null) }}}}첫 키스<br>({{getvar::vg_FirstKiss}}){{/if}}\n </button>\n\n <button class=\"vgstart-toggle-button {{#if {{? ({{getvar::vg_FirstMasturbation}} != null) }}}}vgstart-active{{/if}}\" risu-trigger=\"setFirstMasturbation\">\n {{#if {{? ({{getvar::vg_FirstMasturbation}} == null) }}}}자위 경험 없음{{/if}}{{#if {{? ({{getvar::vg_FirstMasturbation}} != null) }}}}자위 경험 있음{{/if}}\n </button>\n\n <button class=\"vgstart-toggle-button {{#if {{? ({{getvar::vg_FirstOrgasm}} != null) }}}}vgstart-active{{/if}}\" risu-trigger=\"setFirstOrgasm\">\n {{#if {{? ({{getvar::vg_FirstOrgasm}} == null) }}}}절정 경험 없음{{/if}}{{#if {{? ({{getvar::vg_FirstOrgasm}} != null) }}}}절정 경험 있음{{/if}}\n </button>\n </div>\n\n <div class=\"vgstart-arousal-buttons-row\">\n <button class=\"vgstart-toggle-button {{#if {{? ({{getvar::vg_FirstFellatio}} != null) }}}}vgstart-active{{/if}}\" risu-trigger=\"setFirstFellatio\">\n {{#if {{? ({{getvar::vg_FirstFellatio}} == null) }}}}구강 경험 없음{{/if}}{{#if {{? ({{getvar::vg_FirstFellatio}} != null) }}}}구강 경험<br>({{getvar::vg_FirstFellatio}}){{/if}}\n </button>\n\n <button class=\"vgstart-toggle-button {{#if {{? ({{getvar::vg_FirstVirgin}} != null) }}}}vgstart-active{{/if}}\" risu-trigger=\"setFirstVirgin\">\n {{#if {{? ({{getvar::vg_FirstVirgin}} == null) }}}}처녀{{/if}}{{#if {{? ({{getvar::vg_FirstVirgin}} != null) }}}}비처녀<br>({{getvar::vg_FirstVirgin}}){{/if}}\n </button>\n\n <button class=\"vgstart-toggle-button {{#if {{? ({{getvar::vg_FirstAnal}} != null) }}}}vgstart-active{{/if}}\" risu-trigger=\"setFirstAnal\">\n {{#if {{? ({{getvar::vg_FirstAnal}} == null) }}}}항문 경험 없음{{/if}}{{#if {{? ({{getvar::vg_FirstAnal}} != null) }}}}항문 경험<br>({{getvar::vg_FirstAnal}}){{/if}}\n </button>\n </div>\n </div>\n </div>\n\n <div class=\"vgstart-step-divider\"></div>\n\n <div class=\"vgstart-row\">\n <div class=\"vgstart-label\">흥분 반응</div>\n\n <div class=\"vgstart-arousal-wrap\">\n <div class=\"vgstart-arousal-buttons-row\">\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_ArousalResponse}}::1}}}}vgstart-active{{/if}}\" risu-trigger=\"setArousalResponse1\">순결수치</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_ArousalResponse}}::2}}}}vgstart-active{{/if}}\" risu-trigger=\"setArousalResponse2\">냉정억제</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_ArousalResponse}}::3}}}}vgstart-active{{/if}}\" risu-trigger=\"setArousalResponse3\">분석통제</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_ArousalResponse}}::4}}}}vgstart-active{{/if}}\" risu-trigger=\"setArousalResponse4\">거부적대</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_ArousalResponse}}::5}}}}vgstart-active{{/if}}\" risu-trigger=\"setArousalResponse5\">지배경멸</button>\n </div>\n\n <div class=\"vgstart-arousal-buttons-row\">\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_ArousalResponse}}::6}}}}vgstart-active{{/if}}\" risu-trigger=\"setArousalResponse6\">허세붕괴</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_ArousalResponse}}::7}}}}vgstart-active{{/if}}\" risu-trigger=\"setArousalResponse7\">혼란불안</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_ArousalResponse}}::8}}}}vgstart-active{{/if}}\" risu-trigger=\"setArousalResponse8\">현실왜곡</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_ArousalResponse}}::9}}}}vgstart-active{{/if}}\" risu-trigger=\"setArousalResponse9\">체념절망</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_ArousalResponse}}::10}}}}vgstart-active{{/if}}\" risu-trigger=\"setArousalResponse10\">쾌락수용</button>\n </div>\n </div>\n </div>\n\n <div class=\"vgstart-step-divider\"></div>\n \n <div class=\"vgstart-row\">\n <div class=\"vgstart-label\">민감도</div>\n <button class=\"vgstart-dice-button\" risu-trigger=\"rollSensitivityDice\">🎲</button>\n <div class=\"vgstart-sensitivity-values\">\n <span>가슴<br>{{getvar::vg_startBreastEmoji}}</span>\n <span>입<br>{{getvar::vg_startOralEmoji}}</span>\n <span>클리토리스<br>{{getvar::vg_startClitorisEmoji}}</span>\n <span>질내<br>{{getvar::vg_startVaginalEmoji}}</span>\n <span>항문<br>{{getvar::vg_startAnalEmoji}}</span>\n </div>\n </div>\n\n <div class=\"vgstart-step-divider\"></div>\n\n <div class=\"vgstart-row\">\n <div class=\"vgstart-label\">초기 강화</div>\n\n <div class=\"vgstart-arousal-wrap\">\n <div class=\"vgstart-arousal-buttons-row\">\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_StartEnhancementBase}}::1}}}}vgstart-active{{/if}}\" risu-trigger=\"setStartEnhancementAttack\">전투 감각</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_StartEnhancementBase}}::2}}}}vgstart-active{{/if}}\" risu-trigger=\"setStartEnhancementDefense\">방어 감각</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_StartEnhancementBase}}::3}}}}vgstart-active{{/if}}\" risu-trigger=\"setStartEnhancementCharisma\">카리스마</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_StartEnhancementBase}}::4}}}}vgstart-active{{/if}}\" risu-trigger=\"setStartEnhancementChastity\">철벽</button>\n </div>\n\n <div class=\"vgstart-arousal-buttons-row\">\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_StartEnhancementBase}}::7}}}}vgstart-active{{/if}}\" risu-trigger=\"setStartEnhancementFocus\">집중력</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_StartEnhancementBase}}::8}}}}vgstart-active{{/if}}\" risu-trigger=\"setStartEnhancementResilience\">회복력</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_StartEnhancementBase}}::9}}}}vgstart-active{{/if}}\" risu-trigger=\"setStartEnhancementDiscipline\">자제력</button>\n <button class=\"vgstart-toggle-button {{#if {{equal::{{getvar::vg_StartEnhancementBase}}::11}}}}vgstart-active{{/if}}\" risu-trigger=\"setStartEnhancementLuck\">행운</button>\n </div>\n </div>\n </div>\n\n <div class=\"vgstart-step-divider\"></div>\n\n <div class=\"vgstart-row\">\n <button class=\"vgstart-toggle-button {{#if {{? ({{getvar::vg_Start_Message}} != null) }}}}vgstart-active{{/if}}\" risu-trigger=\"setStartMessage\">\n 시작 메시지 추가\n </button>\n\n <button class=\"vgstart-main-button\" risu-trigger=\"generateStartInput\">\n 시작\n </button>\n </div>\n\n</div>\n{{/if}}\n\n{{#if {{? ({{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}) && ({{notequal::{{getvar::vg_Init}}::null}}) && ({{getvar::vg_Init}} == 1)}} }}\n♥♣♥\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "♥♣♥ 탭 Display",
"in": "♥♣♥",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n{{getvar::vg_generatedHTML}}\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "<> Display",
"in": "(<SaveFullData>[\\s\\S]*?</SaveFullData>|<SaveSessionData>[\\s\\S]*?</SaveSessionData>|<Major Sexual Act Tag>[\\s\\S]*?</Major Sexual Act Tag>)",
"out": "",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "<> Request",
"in": "(<SaveFullData>[\\s\\S]*?</SaveFullData>|<SaveSessionData>[\\s\\S]*?</SaveSessionData>|<Choice>[\\s\\S]*?</Choice>|<Reward Choice>[\\s\\S]*?</Reward Choice>|<Enhancement Choice>[\\s\\S]*?</Enhancement Choice>|<Major Sexual Act Tag>[\\s\\S]*?</Major Sexual Act Tag>)",
"out": "",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "<Choice> 한 칸 띄기",
"in": "<Choice>",
"out": "<br>\n<Choice>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "</Choice> 한 칸 띄기",
"in": "</Choice>",
"out": "</Choice>\n<br>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "==== 메시지 ====",
"in": "",
"out": "",
"type": "disabled",
"ableFlag": false
},
{
"comment": "▷ 메시지 Display",
"in": "▷ \\[([\\s\\S]*?)\\]",
"out": "",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "▷ 메시지 Request",
"in": "▷ \\[([\\s\\S]*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n($1)\n{{/if}}",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "▶ 메시지 Display",
"in": "▶ \\[([\\s\\S]*?)\\]",
"out": "",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "▶ 메시지 Request",
"in": "▶ \\[([\\s\\S]*?)\\]",
"out": "($1)",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "==== 스토리 선택지 ====",
"in": "",
"out": "",
"type": "disabled",
"ableFlag": false
},
{
"comment": "스토리 Display",
"in": "\\[Story Date: (.*?) \\| Story Emoji: (.*?) \\| Story Type: (.*?) \\| Situation: (.*?) \\| Encounter Emoji: (.*?) \\| Encounter: (.*?) \\| Encounter Type: (.*?) \\| Difficulty Emoji: (.*?) \\| Difficulty: (.*?) \\| Level: (.*?) \\| Trait: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectStory_$1_$2_$3_$4_$5_$6_$7_$8_$9_$10_$11\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 ($1) $4</span>\n </div>\n <div class=\"vg-hand-effect\">$5 Lv.$10 $8$6</div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "어드밴스 Display",
"in": "\\[Advance Date: (.*?) \\| Story Emoji: (.*?) \\| Story Type: (.*?) \\| Situation: (.*?) \\| Encounter Emoji: (.*?) \\| Encounter: (.*?) \\| Encounter Type: (.*?) \\| Difficulty Emoji: (.*?) \\| Difficulty: (.*?) \\| Level: (.*?) \\| Trait: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectAdvance_$1_$2_$3_$4_$5_$6_$7_$8_$9_$10_$11\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 ($1) $4</span>\n </div>\n <div class=\"vg-hand-effect\">$5 Lv.$10 $8$6</div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "선택지 선택 후 Display",
"in": "\\[Choice Select: (.*?) \\| Story Emoji: (.*?) \\| Situation: (.*?) \\| Encounter Emoji: (.*?) \\| Encounter: (.*?) \\| Difficulty Emoji: (.*?) \\| Level: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 ($1) $3</span>\n </div>\n <div class=\"vg-hand-effect\">$4 Lv.$7 $6$5</div>\n </div>\n</div>\n{{/if}}\n\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*($1) $3 $4 Lv.$7 $6 $5*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "선택지 선택 후 Request",
"in": "\\[Choice Select: (.*?) \\| Story Emoji: (.*?) \\| Situation: (.*?) \\| Encounter Emoji: (.*?) \\| Encounter: (.*?) \\| Difficulty Emoji: (.*?) \\| Level: (.*?)\\]",
"out": "*($1) $3*",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "일상 선택지 Display",
"in": "\\[Daily Situation: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectDaily_$1_$2\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "일상 선택 후 Display",
"in": "\\[Daily Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "일상 선택 후 Request",
"in": "\\[Daily Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "*$1*",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "중간 선택지 Display",
"in": "\\[Interlude Situation: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectInterlude_$1_$2\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "중간 선택 후 Display",
"in": "\\[Interlude Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "중간 선택 후 Request",
"in": "\\[Interlude Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "*$1*",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "부자유 선택지 Display",
"in": "\\[Unfree Situation: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectUnfree_$1_$2\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "부자유 선택 후 Display",
"in": "\\[Unfree Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "부자유 선택 후 Request",
"in": "\\[Unfree Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "*$1*",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "어드밴스 후퇴 선택지 Display",
"in": "\\[Advance Rest: (.*?) \\| Meta: (.*?) \\| Emoji: (.*?) \\| HP: (.*?) \\| Resource: (.*?) \\| Recovery: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectAdvanceRest_$1_$2_$3_$4_$5_$6\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$3 $1 ($2)</span>\n </div>\n <div class=\"vg-hand-effect\">$6</div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "어드밴스 후퇴 선택 후 Display",
"in": "\\[Advance Rest Select: (.*?) \\| Emoji: (.*?) \\| Recovery: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n <div class=\"vg-hand-effect\">$3</div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "어드밴스 후퇴 선택 후 Request",
"in": "\\[Advance Rest Select: (.*?) \\| Emoji: (.*?) \\| Recovery: (.*?)\\]",
"out": "*$1*",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "==== 자위 ====",
"in": "",
"out": "",
"type": "disabled",
"ableFlag": false
},
{
"comment": "자위 선택지 Display",
"in": "\\[Masturbation Choice: (.*?) \\| Emoji: (.*?) \\| Masturbation Type: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectMasturbation_$1_$2_$3\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "자위 선택 후 Display",
"in": "\\[Masturbation Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "자위 선택 후 Request",
"in": "\\[Masturbation Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "*$1*",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "==== 무력화 ====",
"in": "",
"out": "",
"type": "disabled",
"ableFlag": false
},
{
"comment": "무력화 선택지 Display",
"in": "\\[Incapacitated Type: (.*?) \\| Emoji: (.*?) \\| Response: (.*?) \\| Difficulty: (.*?) \\| Level: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectIncapacitated_$1_$2_$3_$4_$5\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $3</span>\n </div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "무력화 선택 후 Display",
"in": "\\[Incapacitated Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "무력화 선택 후 Request",
"in": "\\[Incapacitated Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "*$1*",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "==== 출산 ====",
"in": "",
"out": "",
"type": "disabled",
"ableFlag": false
},
{
"comment": "출산 선택지 Display",
"in": "\\[Delivery Choice: (.*?) \\| Emoji: (.*?) \\| Delivery Type: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectDelivery_$1_$2_$3\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "출산 선택 후 Display",
"in": "\\[Delivery Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "출산 선택 후 Request",
"in": "\\[Delivery Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "*$1*",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "==== 모드 전환 ====",
"in": "",
"out": "",
"type": "disabled",
"ableFlag": false
},
{
"comment": "모드 선택지 Display",
"in": "\\[Mode Change: (.*?) \\| Mode: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectChangeMode_$2_$1_$3\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$3 $1</span>\n </div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "모드 선택 후 Display",
"in": "\\[Mode Change Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "모드 선택 후 Request",
"in": "\\[Mode Change Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "==== 강화 ====",
"in": "",
"out": "",
"type": "disabled",
"ableFlag": false
},
{
"comment": "<Enhancement Choice>",
"in": "<Enhancement Choice>([\\s\\S]*?)</Enhancement Choice>",
"out": "{{#if {{? ({{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}) && ({{getvar::vg_Enhancement_Select}} == null) }} }}\n<br>\n$1\n<br>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "강화 선택지 Display",
"in": "\\[Obtainable Enhancement: (.*?) \\| Name: (.*?) \\| Type: (.*?) \\| Summary: (.*?) \\| Effect1: (.*?) \\| Effect2: (.*?) \\| Slot: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectEnhancement_$1_$2_$3_$4_$5_$6_$7\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$1</span>\n </div>\n <div class=\"vg-hand-effect\">$4</div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "==== 액션 ====",
"in": "",
"out": "",
"type": "disabled",
"ableFlag": false
},
{
"comment": "◀▣▶ 핸드 Display",
"in": "◀▣▶",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n{{getvar::vg_PlayDeck_Choice}}\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "핸드 액션 Display",
"in": "\\[Code: (.*?) \\| Action Type: (.*?) \\| Action: (.*?) \\| Cost: (.*?) \\| Effect: (.*?) \\| Level: (.*?)\\]",
"out": "<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectHand_$1\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $3 $6</span>\n </div>\n <div class=\"vg-hand-effect\">$4 $5</div>\n </button>\n</div>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "상대 액션 Display",
"in": "\\[Encounter Action: (.*?) \\| Action: (.*?) \\| Effect: (.*?)\\]",
"out": "<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card vg-hand-card-encounter\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">☠️ $1: $2</span>\n </div>\n <div class=\"vg-hand-effect\">$3</div>\n </div>\n</div>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "동료 액션 Display",
"in": "\\[Companion Action: (.*?) \\| Action: (.*?)\\]",
"out": "<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">👥 $1: $2</span>\n </div>\n </div>\n</div>\n",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "가드 우선순위 Display",
"in": "\\[Guard Code: (.*?) \\| Text: (.*?)\\]",
"out": "<div class=\"vg-handmodule\">\n <button class=\"vg-setting-toggle {{#if {{equal::$1::{{getvar::vg_GuardPriority}}}}}}is-active{{/if}}\" risu-btn=\"choice_selectGuard_$1\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2</span>\n </div>\n </button>\n</div>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "액션 사용량 Display",
"in": "\\[Available Action: (.*?)\\]",
"out": "<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">🃏: $1</span>\n </div>\n </div>\n</div>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "[Blank Line] Display",
"in": "\\[Blank Line\\]",
"out": "<br>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "[Blank Line] Request",
"in": "\\[Blank Line\\]",
"out": "",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "액션 선택 대기 Display",
"in": "\\[Action Order: (.*?) \\| Code: (.*?) \\| Action: (.*?) \\| Type: (.*?) \\| Level: (.*?) \\| Waiting]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$4 $3 $5</span>\n </div>\n <div class=\"vg-hand-effect\">다음 액션 선택 대기중</div>\n </div>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "액션 선택 후 Display",
"in": "\\[Action Order: (.*?) \\| Code: (.*?) \\| Action: (.*?) \\| Type: (.*?) \\| Level: (.*?) \\| Cost: (.*?) \\| Effect: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$4 $3 $5</span>\n </div>\n\n <div class=\"vg-hand-effects\">\n {{#if {{? $6 != null}}}}<div class=\"vg-hand-effect-item\">$6</div>{{/if}}\n <div class=\"vg-hand-effect-item\">$7</div>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$4 $3 ($6 / $7)*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "상대 액션 후 Display",
"in": "\\[Encounter Order: (.*?) \\| Source: (.*?) \\| Action: (.*?) \\| Effect: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card vg-hand-card-encounter\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">☠️ $2: $3</span>\n </div>\n <div class=\"vg-hand-effects\">\n <div class=\"vg-hand-effect-item\">$4</div>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$2: $3 ($4)*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "동료 액션 후 Display",
"in": "\\[Companion Order: (.*?) \\| Source: (.*?) \\| Action: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">👥 $2: $3</span>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$2: $3*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "액션 선택 후 Request",
"in": "\\[Action Order: (.*?) \\| Code: (.*?) \\| Action: (.*?) \\| Type: (.*?) \\| Level: (.*?) \\| Cost: (.*?) \\| Effect: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n*{{user}}: $3*\n{{/if}}",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "상대 액션 후 Request",
"in": "\\[Encounter Order: (.*?) \\| Source: (.*?) \\| Action: (.*?) \\| Effect: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n*$2: $3*\n{{/if}}",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "동료 액션 후 Request",
"in": "\\[Companion Order: (.*?) \\| Source: (.*?) \\| Action: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n*$2: $3*\n{{/if}}",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "==== 추가 인풋 ====",
"in": "",
"out": "",
"type": "disabled",
"ableFlag": false
},
{
"comment": "추가 인풋 Display",
"in": "\\[Additional Message: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectAdditionalMessage_$2\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "추가 인풋 후 Display",
"in": "\\[Additional Input: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "추가 인풋 후 Request",
"in": "\\[Additional Input: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n(Priority Instruction: $1)\n{{/if}}",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "시작 인풋 Display",
"in": "\\[Opening Situation: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "시작 인풋 Request",
"in": "\\[Opening Situation: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n(Opening Situation: $1)\n{{/if}}",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "종료 추가 인풋 Display",
"in": "\\[Aftermath Message: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectAftermathMessage_$2\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "종료 추가 인풋 Request",
"in": "\\[Aftermath Message: (.*?) \\| Emoji: (.*?)\\]",
"out": "",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "종료 추가 인풋 후 Display",
"in": "\\[Aftermath Input: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$1</span>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "종료 추가 인풋 후 Request",
"in": "\\[Aftermath Input: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n(Resolution & Aftermath Priority Instruction: $1)\n{{/if}}",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "모드 인풋 Display",
"in": "\\[Mode Message: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectModeMessage_$2\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "모드 인풋 Request",
"in": "\\[Mode Message: (.*?) \\| Emoji: (.*?)\\]",
"out": "",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "모드 인풋 후 Display",
"in": "\\[Mode Input: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$1</span>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "모드 인풋 후 Request",
"in": "\\[Mode Input: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n(Priority Instruction: $1)\n{{/if}}",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "==== 보상 ====",
"in": "",
"out": "",
"type": "disabled",
"ableFlag": false
},
{
"comment": "<Reward Choice>",
"in": "<Reward Choice>([\\s\\S]*?)</Reward Choice>",
"out": "{{#if {{? ({{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}) && ({{getvar::vg_Reward_Count}} > 0) }} }}\n<div style=\"text-align: center;\">\n<div class=\"vg-systemlog\">\n{{#if {{? ({{getvar::vg_Reward_Count}} == 1)}} }}하나{{/if}}{{#if {{? ({{getvar::vg_Reward_Count}} == 2)}} }}두 개{{/if}}{{#if {{? ({{getvar::vg_Reward_Count}} == 3)}} }}세 개{{/if}}{{#if {{? ({{getvar::vg_Reward_Count}} == 4)}} }}네 개{{/if}}{{#if {{? ({{getvar::vg_Reward_Count}} == 5)}} }}다섯 개{{/if}}의 보상을 선택하세요.\n</div>\n</div>\n<br>\n$1\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "보상 액션 Display",
"in": "\\[Reward Action: (.*?) \\| Emoji: (.*?) \\| Card Level: (.*?) \\| Level: (.*?)\\]",
"out": "<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectRewardAction_$1_$2_$3\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $4 카드 획득</span>\n </div>\n </button>\n</div>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "보상 강화 Display",
"in": "\\[Reward Upgrade: (.*?) \\| Emoji: (.*?) \\| Current Level: (.*?) \\| Next Level: (.*?) \\| Level Up: (.*?)\\]",
"out": "<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectRewardUpgrade_$1_$2_$3_$4_$5\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $3 → $4 카드 강화</span>\n </div>\n </button>\n</div>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "보상 제거 Display",
"in": "\\[Reward Remove: (.*?) \\| Emoji: (.*?) \\| Level: (.*?)\\]",
"out": "<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectRewardRemove_$1_$2_$3\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $3 카드 제거</span>\n </div>\n </button>\n</div>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "보상 스탯 Display",
"in": "\\[Reward Attribute: (.*?) \\| Strength: (.*?) \\| Reflex: (.*?) \\| Willpower: (.*?)\\]",
"out": "<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectRewardAttribute_$2_$3_$4\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$1</span>\n </div>\n </button>\n</div>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "보상 스탯2 Display",
"in": "\\[Reward Stat: (.*?) \\| HP: (.*?) \\| Resource: (.*?) \\| Arousal: (.*?)\\]",
"out": "<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectRewardStat_$2_$3_$4\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$1</span>\n </div>\n </button>\n</div>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "보상 회복 Display",
"in": "\\[Reward Recovery: (.*?) \\| HP: (.*?) \\| Resource: (.*?)\\]",
"out": "<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectRewardRecovery_$1_$2_$3\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$1</span>\n </div>\n </button>\n</div>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "보상 진정 Display",
"in": "\\[Reward ArousalDown: (.*?) \\| Arousal: (.*?)\\]",
"out": "<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectRewardArousalDown_$1_$2\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$1</span>\n </div>\n </button>\n</div>",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "==== 로그 ====",
"in": "",
"out": "",
"type": "disabled",
"ableFlag": false
},
{
"comment": "시스템 로그 Display",
"in": "\\[(Temporary Log|Persistent Log): (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-5}}}}}}\n<div style=\"text-align: center;\">\n<div class=\"vg-systemlog\">\n$2\n</div>\n</div>\n{{/if}}\n{{#if {{? {{equal::$1::Persistent Log}} && {{less::{{chat_index}}::{{? {{lastmessageid}}-5}}}} }} }}\n*$2*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "시스템 로그 Request",
"in": "\\[(Temporary Log|Persistent Log): (.*?)\\]",
"out": "",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "==== 능욕 ====",
"in": "",
"out": "",
"type": "disabled",
"ableFlag": false
},
{
"comment": "능욕 선택지 Display",
"in": "\\[H Event: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectHEvent_$1_$2\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "능욕 선택 후 Display",
"in": "\\[H Event Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "능욕 선택 후 Request",
"in": "\\[H Event Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "날짜 경과 Display",
"in": "\\[H Event Time Pass: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectHEventTimePass_$1_$2\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "날짜 경과 선택 후 Display",
"in": "\\[H Event Time Pass Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </div>\n</div>\n<br>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "날짜 경과 선택 후 Request",
"in": "\\[H Event Time Pass Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "*$1*",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "==== 기타 ====",
"in": "",
"out": "",
"type": "disabled",
"ableFlag": false
},
{
"comment": "현재 목표 Display",
"in": "\\[Goal Action: (.*?) \\| Text: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectGoal_$1\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$3 $2</span>\n </div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "현재 목표 재설정 Display",
"in": "\\[Goal Update: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "현재 목표 재설정 Request",
"in": "\\[Goal Update: (.*?) \\| Emoji: (.*?)\\]",
"out": "",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "스토리 기록 Display",
"in": "\\[Story Record: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectAddRecord_$1_$2\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "스토리 기록 선택 후 Display",
"in": "\\[Story Record Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "스토리 기록 선택 후 Request",
"in": "\\[Story Record Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "스토리 기록(솔로) 선택지 Display",
"in": "\\[Solo Record: (.*?) \\| Emoji: (.*?) \\| Record Type: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectAddSoloRecord_$1_$2_$3\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </button>\n</div>\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "스토리 기록(솔로) 선택 후 Display",
"in": "\\[Solo Record Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n<div class=\"vg-handmodule\">\n <div class=\"vg-hand-card\">\n <div class=\"vg-hand-title\">\n <span class=\"vg-hand-name\">$2 $1</span>\n </div>\n </div>\n</div>\n{{/if}}\n{{#if {{less::{{chat_index}}::{{? {{lastmessageid}}-3}}}}}}\n*$1*\n{{/if}}",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "스토리 기록(솔로) 선택 후 Request",
"in": "\\[Solo Record Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "",
"type": "editprocess",
"ableFlag": false
},
{
"comment": "일상 랜덤 문구 Display",
"in": "\\[Daily Variation: (.*?)\\]",
"out": "",
"type": "editdisplay",
"ableFlag": false
},
{
"comment": "일상 랜덤 문구 Request",
"in": "\\[Daily Variation: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n($1)\n{{/if}}",
"type": "editprocess",
"ableFlag": false
}
],
"backgroundEmbedding": "<style>\n.vg-systemlog {\n display: block;\n box-sizing: border-box;\n\n background: #19050b;\n border-left: 3px solid #c96a90;\n\n padding: 5px 12px;\n margin: 0;\n\n font-family: 'Consolas', 'Malgun Gothic', monospace;\n color: #ddd0d6;\n font-size: 16px;\n font-weight: 450;\n line-height: 1.3;\n\n text-align: left;\n\n box-shadow: none;\n text-shadow: none;\n}\n\n.vg-choice-card {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 14px;\n\n background: #19050b;\n border: 1px solid #c96a90;\n\n color: #ddd0d6;\n font-size: 1.05rem;\n font-weight: 400;\n text-shadow: none;\n box-shadow: none;\n}\n\n.vg-choice-icon {\n color: #d97ea3;\n font-weight: 700;\n}\n\n.vg-choice-content {\n flex: 1;\n text-align: left;\n padding-left: 0px;\n}\n\n.vg-choice-sub {\n opacity: 0.85;\n margin-left: 6px;\n color: #ddd0d6;\n}\n\n.vg-handmodule {\n display: flex;\n flex-wrap: wrap;\n gap: 0px;\n}\n\n.vg-hand-card {\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: flex-start;\n\n box-sizing: border-box;\n width: 100%;\n\n padding: 8px 14px 6px 14px;\n margin: 0;\n\n background: #19050b;\n border: 1px solid #c96a90;\n\n color: #ddd0d6;\n font-family: 'Consolas', 'Malgun Gothic', monospace;\n text-align: left;\n\n box-shadow: none;\n text-shadow: none;\n}\n\n.vg-hand-title {\n display: flex;\n align-items: center;\n gap: 8px;\n\n font-size: 1.05rem;\n font-weight: 450;\n line-height: 1.15;\n color: #ddd0d6;\n}\n\n.vg-hand-icon {\n color: #d97ea3;\n font-weight: 700;\n flex-shrink: 0;\n}\n\n.vg-hand-name {\n color: #ddd0d6;\n}\n\n.vg-hand-effect {\n margin-top: 4px;\n padding-left: 0;\n\n font-size: 1.1rem;\n font-weight: 500;\n line-height: 1.3;\n color: #ddd0d6;\n opacity: 0.9;\n}\n\n.vg-hand-card button,\nbutton.vg-hand-card {\n cursor: pointer;\n appearance: none;\n}\n\n.vg-hand-effects {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px 14px;\n\n margin-top: 4px;\n\n font-size: 1.1rem;\n font-weight: 500;\n line-height: 1.3;\n color: #ddd0d6;\n opacity: 0.9;\n}\n\n.vg-hand-effect-item {\n display: inline-flex;\n align-items: center;\n white-space: nowrap;\n}\n\n.vg-hand-card-heroine {\n background: #220c14;\n border-color: #dd86a9;\n}\n\n.vg-hand-card-heroine .vg-hand-title {\n color: #ebc6d6;\n}\n\n.vg-hand-card-heroine .vg-hand-icon {\n color: #dd86a9;\n}\n\n.vg-hand-card-heroine .vg-hand-name {\n color: #ebc6d6;\n}\n\n.vg-hand-card-heroine .vg-hand-effect,\n.vg-hand-card-heroine .vg-hand-effects {\n color: #ebc6d6;\n opacity: 0.95;\n}\n\n.vg-hand-card-encounter {\n position: relative;\n background: linear-gradient(180deg, #4a0f12 0%, #1a0608 100%);\n border: 1px solid #c24b4b;\n\n box-shadow:\n inset 0 0 0 1px rgba(255, 120, 120, 0.12),\n 0 0 14px rgba(120, 12, 18, 0.30);\n}\n\n.vg-hand-card-encounter .vg-hand-title {\n color: #ecd6d1;\n}\n\n.vg-hand-card-encounter .vg-hand-name {\n color: #ecd6d1;\n font-weight: 700;\n letter-spacing: 0.2px;\n}\n\n.vg-hand-card-encounter .vg-hand-effect,\n.vg-hand-card-encounter .vg-hand-effects {\n color: #cfaeb0;\n opacity: 0.96;\n}\n\n.vg-hand-card-encounter::before {\n content: \"⚠\";\n position: absolute;\n top: 8px;\n right: 12px;\n font-size: 0.95rem;\n color: #d7899e;\n opacity: 0.75;\n}\n\n.vg-setting-toggle {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n\n box-sizing: border-box;\n width: 100%;\n\n padding: 8px 14px 6px 14px;\n margin: 0;\n\n background: #120608;\n border: 1px solid #6e353d;\n\n color: #8f767c;\n font-family: 'Consolas', 'Malgun Gothic', monospace;\n text-align: left;\n\n box-shadow: none;\n text-shadow: none;\n cursor: pointer;\n appearance: none;\n opacity: 0.72;\n}\n\n.vg-setting-toggle .vg-hand-title,\n.vg-setting-toggle .vg-hand-name {\n color: #8f767c;\n}\n\n/* active - encounter 톤 */\n.vg-setting-toggle.is-active {\n background: linear-gradient(180deg, #4a0f12 0%, #1a0608 100%);\n border: 1px solid #c24b4b;\n color: #ecd6d1;\n opacity: 1;\n}\n\n.vg-setting-toggle.is-active .vg-hand-title,\n.vg-setting-toggle.is-active .vg-hand-name {\n color: #ecd6d1;\n font-weight: 700;\n}\n\n.vg-setting-toggle.is-active::before {\n content: \"\";\n position: absolute;\n left: 0;\n top: 0;\n bottom: 0;\n width: 4px;\n background: #c24b4b;\n}\n\n/* 상태창 */\n.vg-status-wrap {\n width: 100%;\n max-width: 1000px;\n margin: 20px auto;\n padding: 22px 24px 24px;\n box-sizing: border-box;\n background: #260811;\n border: 1px solid #c96a90;\n color: #ddd0d6;\n font-family: 'Consolas', 'Malgun Gothic', monospace;\n}\n\n.vg-status-place-main,\n.vg-status-place-sub {\n margin: 0;\n text-align: center;\n color: #de7fa3;\n}\n\n.vg-status-place-main {\n font-size: 2rem;\n font-weight: 800;\n line-height: 1.12;\n}\n\n.vg-status-place-sub {\n margin-top: 6px;\n margin-bottom: 14px;\n font-size: 1.18rem;\n font-weight: 500;\n line-height: 1.12;\n}\n\n.vg-status-divider {\n height: 1px;\n margin: 0 0 14px 0;\n background: #5f3847;\n}\n\n.vg-status-box {\n width: 100%;\n box-sizing: border-box;\n overflow: hidden;\n border: 1px solid #5f3847;\n background: #300b16;\n}\n\n.vg-status-box * {\n box-sizing: border-box;\n margin: 0;\n}\n\n.vg-status-name {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 7px 12px;\n background: rgb(24, 8, 12);\n color: #e8c6d2;\n font-size: 0.98rem;\n font-weight: 700;\n line-height: 1.2;\n text-align: center;\n border-bottom: 1px solid #6d4353;\n}\n\n.vg-status-row {\n display: flex;\n width: 100%;\n}\n\n.vg-status-row + .vg-status-row {\n border-top: 1px solid #6d4353;\n}\n\n.vg-status-label {\n flex: 0 0 122px;\n width: 122px;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 7px 10px;\n background: rgb(24, 8, 12);\n color: #e8c6d2;\n font-size: 0.95rem;\n font-weight: 700;\n line-height: 1.2;\n text-align: center;\n border-right: 1px solid #6d4353;\n}\n\n.vg-status-value {\n flex: 1 1 auto;\n min-width: 0;\n display: flex;\n align-items: center;\n justify-content: flex-start;\n padding: 7px 12px;\n background: rgb(34, 10, 16);\n color: #ddd0d6;\n font-size: 0.95rem;\n font-weight: 500;\n line-height: 1.25;\n text-align: left;\n word-break: break-word;\n}\n\n/* 게이지가 들어가는 줄만 padding 제거 */\n.vg-status-value-gauge {\n padding: 4px 12px 0 12px;\n align-items: stretch;\n}\n\n.vg-status-gauge-wrap {\n position: relative;\n width: 100%;\n height: 22px;\n}\n\n.vg-status-gauge {\n position: absolute;\n inset: 0;\n appearance: none;\n -webkit-appearance: none;\n width: 100%;\n height: 100%;\n border: 1px solid #6d4353;\n background: #14090b;\n overflow: hidden;\n}\n\n.vg-status-gauge::-webkit-progress-bar {\n background: #14090b;\n}\n\n.vg-status-gauge-text {\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n color: #f3eef0;\n font-size: 0.88rem;\n font-weight: 700;\n line-height: 1;\n text-shadow: 0 1px 1px rgba(0, 0, 0, 0.8);\n pointer-events: none;\n}\n\n.vg-status-gauge-hp::-webkit-progress-value {\n background: linear-gradient(90deg, #b02a34 0%, #d83b3b 100%);\n}\n.vg-status-gauge-hp::-moz-progress-bar {\n background: linear-gradient(90deg, #b02a34 0%, #d83b3b 100%);\n}\n\n.vg-status-gauge-stamina::-webkit-progress-value {\n background: linear-gradient(90deg, #ad9224 0%, #dcbc35 100%);\n}\n.vg-status-gauge-stamina::-moz-progress-bar {\n background: linear-gradient(90deg, #ad9224 0%, #dcbc35 100%);\n}\n\n.vg-status-gauge-arousal::-webkit-progress-value {\n background: linear-gradient(90deg, #a93d76 0%, #ff6fae 100%);\n}\n.vg-status-gauge-arousal::-moz-progress-bar {\n background: linear-gradient(90deg, #a93d76 0%, #ff6fae 100%);\n}\n\n.vg-status-gauge-enemyhp::-webkit-progress-value {\n background: linear-gradient(90deg, #9a4720 0%, #c85a1e 55%, #ff8a2a 100%);\n}\n.vg-status-gauge-enemyhp::-moz-progress-bar {\n background: linear-gradient(90deg, #9a4720 0%, #c85a1e 55%, #ff8a2a 100%);\n}\n\n.vg-status-gauge-level::-webkit-progress-value {\n background: linear-gradient(90deg, #8b4f8f 0%, #b064c0 55%, #d08ae0 100%);\n}\n.vg-status-gauge-level::-moz-progress-bar {\n background: linear-gradient(90deg, #8b4f8f 0%, #b064c0 55%, #d08ae0 100%);\n}\n\n.vg-status-row-upperline {\n border-top: 1px solid #6d4353;\n}\n\n/* 속성 5열 */\n.vg-status-statsline {\n display: flex;\n width: 100%;\n border-top: none;\n background: rgb(34, 10, 16);\n}\n\n.vg-status-stat-cell {\n flex: 1 1 10%;\n min-width: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 9px 4px;\n box-sizing: border-box;\n border-left: 1px solid #6d4353;\n text-align: center;\n}\n\n.vg-status-stat-cell:first-child {\n border-left: none;\n}\n\n.vg-status-stat-icon {\n font-size: 1rem;\n line-height: 1;\n}\n\n.vg-status-stat-value {\n color: #f3f3f3;\n font-size: 0.98rem;\n font-weight: 700;\n line-height: 1;\n}\n\n/* 아이콘별 색 */\n.vg-status-stat-icon-vitality {\n color: #d83b3b;\n}\n\n.vg-status-stat-icon-endurance {\n color: #dcbc35;\n}\n\n.vg-status-stat-icon-mental {\n color: #2f8fc7;\n}\n\n.vg-status-stat-icon-strength {\n color: #ff8a2a;\n}\n\n.vg-status-stat-icon-agility {\n color: #57c75f;\n}\n\n.vg-status-value-stats {\n padding: 0;\n}\n\n.vg-status-row-stats .vg-status-value {\n align-items: stretch;\n}\n\n/* 스탯 8칸 */\n.vg-status-row-stats .vg-status-value {\n padding: 0;\n min-width: 0;\n}\n\n.vg-status-stats-6 {\n display: grid;\n grid-template-columns: repeat(6, 1fr);\n width: 100%;\n min-width: 0;\n background: rgb(34, 10, 16);\n}\n\n.vg-status-stat-box {\n display: flex;\n align-items: center;\n justify-content: center;\n min-height: 30px;\n border-left: 1px solid #6d4353;\n box-sizing: border-box;\n}\n\n.vg-status-stat-box:first-child {\n border-left: none;\n}\n\n.vg-status-stat-icon {\n font-size: 1rem;\n line-height: 1;\n}\n\n.vg-status-stat-value {\n color: #f3f3f3;\n font-size: 0.98rem;\n font-weight: 700;\n line-height: 1;\n}\n\n.vg-status-stat-icon-strength { color: #ff8a2a; }\n.vg-status-stat-icon-reflex { color: #57c75f; }\n.vg-status-stat-icon-charisma { color: #d97ea3; }\n.vg-status-stat-icon-willpower { color: #2f8fc7; }\n\n@media (max-width: 768px) {\n .vg-status-wrap {\n margin: 10px auto;\n padding: 14px 14px 16px;\n }\n\n .vg-status-place-main {\n font-size: 1.45rem;\n }\n\n .vg-status-place-sub {\n margin-bottom: 10px;\n font-size: 0.98rem;\n }\n\n .vg-status-divider {\n margin-bottom: 10px;\n }\n\n .vg-status-label {\n flex-basis: 84px;\n width: 84px;\n padding: 6px 8px;\n font-size: 0.84rem;\n }\n\n .vg-status-name,\n .vg-status-value {\n padding: 6px 8px;\n font-size: 0.84rem;\n }\n\n .vg-status-value-gauge {\n padding: 0 8px;\n }\n\n .vg-status-gauge-wrap {\n height: 20px;\n }\n\n .vg-status-gauge-text {\n font-size: 0.78rem;\n }\n \n .vg-status-stat-cell {\n padding: 7px 2px;\n }\n\n .vg-status-stat-icon {\n font-size: 0.86rem;\n }\n\n .vg-status-stat-value {\n font-size: 0.84rem;\n }\n\n .vg-status-value-stats {\n padding: 0;\n }\n\n .vg-status-stats-7 {\n grid-template-columns: repeat(6, 1fr);\n }\n\n .vg-status-stat-box {\n min-height: 36px;\n }\n\n .vg-status-stat-icon {\n font-size: 0.9rem;\n }\n\n .vg-status-stat-value {\n font-size: 0.84rem;\n }\n}\n</style>",
"trigger": [
{
"comment": "",
"type": "start",
"conditions": [],
"effect": [
{
"type": "triggerlua",
"code": "-- ==========================================\n-- 공용 모듈\n-- ==========================================\nfunction trim(s)\n return (s:gsub(\"^%s+\", \"\"):gsub(\"%s+$\", \"\"))\nend\n\nfunction clamp(v, min, max)\n if v < min then return min end\n if v > max then return max end\n return v\nend\n\nfunction splitPipe(str)\n local t = {}\n\n if not str or str == \"\" then\n return t\n end\n\n for token in string.gmatch(str, \"([^|]+)\") do\n table.insert(t, trim(token))\n end\n\n return t\nend\n\nfunction splitLiteral(str, sep)\n local t = {}\n\n if not str or str == \"\" then\n return t\n end\n\n local start = 1\n\n while true do\n local i, j = string.find(str, sep, start, true)\n if not i then\n table.insert(t, trim(string.sub(str, start)))\n break\n end\n\n table.insert(t, trim(string.sub(str, start, i - 1)))\n start = j + 1\n end\n\n return t\nend\n\nfunction splitSection(str)\n return splitLiteral(str, \"§\")\nend\n\nfunction appendPipe(fullValue, addValue)\n if not addValue or addValue == \"\" then\n return fullValue\n end\n\n if not fullValue or fullValue == \"\" then\n return addValue\n end\n\n return fullValue .. \"|\" .. addValue\nend\n\nfunction safeGet(triggerId, key, default)\n local val = getChatVar(triggerId, key)\n if val == nil or val == \"null\" or val == \"nil\" then return default end\n return val\nend\n\nfunction extractInt(s)\n if not s then return 0 end\n local m = tostring(s):match(\"[-+]?%d+\")\n return tonumber(m) or 0\nend\n\nfunction isLastChatUser(triggerId)\n local cl = getChatLength(triggerId)\n if cl <= 0 then\n return false\n end\n\n local last = getChat(triggerId, cl - 1)\n return last and last.role == \"user\"\nend\n\nfunction formatFloat(v)\n local s = string.format(\"%.1f\", roundToTenth(tonumber(v) or 0))\n s = s:gsub(\"%.0$\", \"\")\n if s == \"-0\" then s = \"0\" end\n return s\nend\n\nfunction roundHalfUp(v)\n v = tonumber(v) or 0\n\n if v >= 0 then\n return math.floor(v + 0.5)\n else\n return math.ceil(v - 0.5)\n end\nend\n\nfunction roundToTenth(v)\n v = tonumber(v) or 0\n\n local out\n\n if v >= 0 then\n out = math.floor(v * 10 + 0.5) / 10\n else\n out = math.ceil(v * 10 - 0.5) / 10\n end\n\n if out == 0 then\n return 0\n end\n\n if out == math.floor(out) then\n return math.floor(out)\n end\n\n return out\nend\n\nfunction floorToTenth(v)\n v = tonumber(v) or 0\n local out = math.floor(v * 10) / 10\n\n if out == 0 then\n return 0\n end\n\n if out == math.floor(out) then\n return math.floor(out)\n end\n\n return out\nend\n\n\nfunction shuffleArray(t)\n for i = #t, 2, -1 do\n local j = math.random(i)\n t[i], t[j] = t[j], t[i]\n end\nend\n\nfunction getPercent(currentValue, maxValue)\n currentValue = tonumber(currentValue) or 0\n maxValue = tonumber(maxValue) or 0\n\n if maxValue <= 0 then\n return 0\n end\n\n return (currentValue / maxValue) * 100\nend\n\nfunction getSignedEffectText(value)\n value = tonumber(value) or 0\n local txt = formatFloat(value)\n\n if value > 0 then\n return \"+\" .. txt\n end\n\n return txt\nend\n\nfunction toPercentText(value, maxValue)\n local pct = clamp(roundHalfUp(getPercent(value, maxValue)), 0, 100)\n return tostring(pct) .. \"%\"\nend\n\nfunction setTrackedChatVar(triggerId, changedVars, key, value)\n local prev = safeGet(triggerId, key, \"\")\n\n if changedVars and tostring(prev) ~= tostring(value) then\n changedVars[key] = value\n end\n\n setChatVar(triggerId, key, value)\nend\n\nfunction joinWithAnd(list)\n local count = #list\n\n if count <= 0 then\n return \"\"\n elseif count == 1 then\n return list[1]\n elseif count == 2 then\n return list[1] .. \" and \" .. list[2]\n end\n\n local text = \"\"\n\n for i = 1, count do\n if i == 1 then\n text = list[i]\n elseif i == count then\n text = text .. \", and \" .. list[i]\n else\n text = text .. \", \" .. list[i]\n end\n end\n\n return text\nend\n\nfunction getLanguage(triggerId)\n return tonumber(safeGet(triggerId, \"vg_Language\", \"1\"))\nend\n\nfunction normalizeIndexedSourceName(raw)\n raw = trim(tostring(raw or \"\"))\n if raw == \"\" then\n return \"\"\n end\n\n raw = trim(raw:gsub(\"^(.-)%s+[A-Z]$\", \"%1\"))\n raw = trim(raw:gsub(\"^(.-)%s+[A-Z](의%s*.+)$\", \"%1%2\"))\n raw = trim(raw:gsub(\"^(.-)%s+[A-Z]('s%s*.+)$\", \"%1%2\"))\n raw = trim(raw:gsub(\"^(.-)%s+[A-Z](の%s*.+)$\", \"%1%2\"))\n\n -- 1 / 2 / 3 / 4 ... 제거\n raw = trim(raw:gsub(\"^(.-)%s+%d+$\", \"%1\"))\n raw = trim(raw:gsub(\"^(.-)%s+%d+(의%s*.+)$\", \"%1%2\"))\n raw = trim(raw:gsub(\"^(.-)%s+%d+('s%s*.+)$\", \"%1%2\"))\n raw = trim(raw:gsub(\"^(.-)%s+%d+(の%s*.+)$\", \"%1%2\"))\n\n return raw\nend\n\nfunction getLocalizedNoneText(triggerId)\n local lang = getLanguage(triggerId)\n\n if lang == 1 then\n return \"없음\"\n elseif lang == 3 then\n return \"なし\"\n end\n\n return \"None\"\nend\n\n-- ==========================================\n-- 변수\n-- ==========================================\nlocal vgContext = {\n varName = \"vg_Context\",\n phase = 1,\n act = 2,\n lewdness = 3,\n sexualTrait_Num = 4,\n corruption = 5,\n corruptionTrait_Num = 6,\n}\n\nlocal vgSexKnowledge = {\n varName = \"vg_SexKnowledge\",\n level = 1,\n}\n\nlocal vgHeroineStat = {\n varName = \"vg_Heroine_Stat\",\n hp = 1,\n hp_Max = 2,\n resource = 3,\n resource_Max = 4,\n arousal = 5,\n arousal_Max = 6,\n\n strength = 7,\n reflex = 8,\n willpower = 9,\n}\n\nlocal vgHeroineEffect = {\n varName = \"vg_Heroine_Effect\",\n buff = 1,\n ambush = 2,\n climaxPoint = 3,\n climaxCount = 4,\n sleep = 5,\n immobilized = 6,\n brainwashed = 7,\n incontinence = 8,\n}\n\nlocal vgEnhancement = {\n varName = \"vg_Enhancement\",\n\n slot1_Name = 1,\n slot1_Effect1 = 2,\n slot1_Effect2 = 3,\n\n slot2_Name = 4,\n slot2_Effect1 = 5,\n slot2_Effect2 = 6,\n\n slot3_Name = 7,\n slot3_Effect1 = 8,\n slot3_Effect2 = 9,\n}\n\nlocal vgSensitivity= {\n varName = \"vg_Sensitivity\",\n breast = 1,\n oral = 2,\n clitoris = 3,\n vaginal = 4,\n anal = 5,\n}\n\nlocal vgReproductive = {\n varName = \"vg_Reproductive\",\n lastPeriod = 1,\n pregnancyRisk = 2,\n riskSource = 3,\n pending = 4,\n pregnancyDate = 5,\n father = 6,\n daysTotal = 7,\n}\n\nlocal vgSessionInfo = {\n varName = \"vg_Session_Info\",\n stage = 1,\n storyType = 2,\n name = 3,\n level = 4,\n difficulty = 5,\n trait = 6,\n}\n\nlocal vgEncounterStat = {\n varName = \"vg_Encounter_Stat\",\n hp = 1,\n hp_Max = 2,\n buff = 3,\n\n attack = 4,\n defense = 5,\n sexual = 6,\n critical = 7,\n}\n\n-- ==========================================\n-- 공용 상수\n-- ==========================================\nlocal constMode = {\n vg = 1,\n free = 2,\n vg_Recovery = 3,\n cancel = 4,\n}\n\nlocal constPhase = {\n none = 0,\n\n standby = 1,\n interlude = 2,\n advance = 3,\n daily = 4,\n\n captivity = 5,\n unfree = 6,\n\n incapacitated = 10,\n\n combat = 11,\n escape = 12,\n sleeping_Ambush = 13,\n entrapment = 14,\n brainwashing = 15,\n feral = 16,\n companion = 17,\n abduction = 18,\n \n harassment = 21,\n abortion_Ambush = 22,\n aphrodisiac = 23,\n\n abuse = 31,\n\n confrontation = 41,\n indoctrination = 42,\n\n masturbation_Interlude = 101,\n masturbation_Daily = 102,\n masturbation_Unfree = 103,\n masturbation_Story = 104,\n\n event_Defiant = 111,\n event_Yielding = 112,\n\n event_Violence = 113,\n event_Sleeping = 114,\n event_Coercion = 115,\n event_Immobilized = 116,\n event_Brainwashing = 117,\n event_Aphrodisiac = 118,\n\n delivery_Daily = 121,\n delivery_DailyAfter = 122,\n delivery_Unfree = 123,\n delivery_UnfreeAfter = 124,\n\n exhibitionism = 131,\n performance = 132,\n transaction = 133,\n promiscuity = 134,\n intimacy = 135,\n\n hypnosis = 141,\n event_Defiant_Hypnosis = 142,\n event_Yielding_Hypnosis = 143,\n\n event_Defiant_Aphrodisiac = 144,\n event_Yielding_Aphrodisiac = 145,\n\n rescue = 151,\n\n rape = 161,\n\n event_H = 901,\n\n inspect = 999,\n}\n\nlocal constChoice = {\n none = 0,\n\n choice_Story = 1,\n choice_Interlude = 2,\n choice_Advance = 3,\n choice_Daily = 4,\n\n choice_Captivity = 5,\n choice_Unfree = 6,\n\n choice_Incapacitated = 10,\n \n deck_Encounter = 11,\n deck_Alone = 12,\n}\n\nlocal constResolution = {\n none = 0,\n\n storyline = 1,\n capture = 2,\n freedom = 3,\n sessionSexualAct = 4,\n majorSexualAct = 5,\n\n addRecord = 11,\n addMasturbationRecord = 12,\n addExhibitionismRecord = 13,\n\n storyGoal = 21,\n}\n\nlocal constCardType = {\n primary = 1,\n secondary = 2,\n focus = 3,\n charge = 4,\n heal = 5,\n ecstasy = 6,\n soothe = 7,\n\n breaker = 21,\n counter = 22,\n taunt = 23,\n temptation = 24,\n milking = 25,\n\n situationEnd = 9997,\n prolonged = 9998,\n surrender = 9999,\n}\n\nlocal constEffectType = {\n damage = 1,\n appeasement = 2,\n liberation = 3,\n\n guard = 11,\n provocation = 12,\n\n buff = 31,\n rage = 32,\n\n refresh = 41,\n recovery = 42,\n calm = 43,\n\n inert = 101,\n\n excitement = 111,\n\n guardbreak = 201,\n\n sleep = 211,\n immobilized = 212,\n brainwashed = 213,\n\n sexual = 501,\n exhaustion = 502,\n\n subdual = 503,\n\n pressure = 504,\n rhetoric = 505,\n\n defilement = 506,\n invasion = 507,\n\n humiliation = 508,\n escalation = 509,\n\n suggestion = 510,\n\n interception = 511,\n\n encounterRecovery = 512,\n\n cost = 999,\n}\n\nlocal constStoryType = {\n core = 1,\n side = 2,\n}\n\nlocal constSessionDifficulty = {\n low = 1,\n medium = 2,\n high = 3,\n elite = 4,\n boss = 5,\n}\n\nlocal constAmbush = {\n none = 0,\n heroine = 1,\n encounter = 2,\n}\n\nlocal constCycle = {\n safe = 1,\n menstruation = 2,\n\n fertile_m5 = 3,\n fertile_m4 = 4,\n fertile_m3 = 5,\n fertile_m2 = 6,\n fertile_m1 = 7,\n\n ovulation = 8,\n\n fertile_p1 = 9,\n\n post_fertile = 10,\n\n pregnant = 20,\n}\n\nlocal constPregnancyFlag = {\n none = 0,\n fertile = 1,\n pending = 2,\n pregnant = 3,\n delivery = 4,\n}\n\nlocal constEnhancementType = {\n attack = 1,\n defense = 2,\n\n charisma = 3,\n chastity = 4,\n\n sensuality = 5,\n spirit = 6,\n\n focus = 7,\n resilience = 8,\n\n discipline = 9,\n\n luck = 11,\n\n endurance = 13,\n}\n\nlocal constEncounterTrait = {\n none = 0,\n\n aggressive = 1,\n lustful = 2,\n tenacious = 3,\n\n cunning = 4,\n oppressive = 5,\n adaptive = 6,\n}\n\n-- ==========================================\n-- 공용 강화\n-- ==========================================\nlocal enhancementTypeDefs = {\n { name = \"Attack\", code = constEnhancementType.attack, display = \"💥\" },\n { name = \"Defense\", code = constEnhancementType.defense, display = \"🧱\" },\n\n { name = \"Charisma\", code = constEnhancementType.charisma, display = \"📣\" },\n { name = \"Chastity\", code = constEnhancementType.chastity, display = \"⛔\" },\n\n { name = \"Sensuality\", code = constEnhancementType.sensuality, display = \"👄·💕·🤝\" },\n { name = \"Spirit\", code = constEnhancementType.spirit, display = \"🤷·🌸·⌛\" },\n\n { name = \"Focus\", code = constEnhancementType.focus, display = \"🎯\" },\n { name = \"Resilience\", code = constEnhancementType.resilience, display = \"❤️·⚡·💗회복량\" },\n\n { name = \"Discipline\", code = constEnhancementType.discipline, display = \"💗저항\" },\n\n { name = \"Luck\", code = constEnhancementType.luck, display = \"🍀크리티컬 확률\" },\n\n { name = \"Endurance\", code = constEnhancementType.endurance, display = \"🃏감소 지연\" },\n}\n\nfunction buildEnhancementEffectText(effectTypeCode, effectValue)\n effectTypeCode = tonumber(effectTypeCode) or 0\n effectValue = roundToTenth(tonumber(effectValue) or 0)\n\n local typeName = \"\"\n local display = \"\"\n\n for _, def in ipairs(enhancementTypeDefs or {}) do\n if (tonumber(def.code) or 0) == effectTypeCode then\n typeName = trim(def.name or \"\")\n display = trim(def.display or \"\")\n break\n end\n end\n\n if typeName == \"\" then\n return \"\"\n end\n\n local valueText = getSignedEffectText(effectValue)\n\n if effectTypeCode == constEnhancementType.discipline\n or effectTypeCode == constEnhancementType.luck\n or effectTypeCode == constEnhancementType.endurance then\n valueText = valueText .. \"%\"\n end\n\n if display ~= \"\" then\n return display .. valueText\n end\n\n return typeName .. \" \" .. valueText\nend\n\nfunction getEnhancementTotalByType(triggerId, targetTypeCode)\n targetTypeCode = tonumber(targetTypeCode) or 0\n if targetTypeCode == 0 then\n return 0\n end\n\n local total = 0\n local effectPositions = {\n vgEnhancement.slot1_Effect1,\n vgEnhancement.slot1_Effect2,\n vgEnhancement.slot2_Effect1,\n vgEnhancement.slot2_Effect2,\n vgEnhancement.slot3_Effect1,\n vgEnhancement.slot3_Effect2,\n }\n\n for _, position in ipairs(effectPositions) do\n local raw = trim(getResolvedDualFlag(triggerId, vgEnhancement.varName, position, \"\"))\n local codeStr, valueStr = raw:match(\"^([%-+]?%d+)%s*:%s*([%-+]?[%d%.]+)$\")\n\n local code = tonumber(codeStr) or 0\n local value = tonumber(valueStr) or 0\n\n if code == targetTypeCode then\n total = total + value\n end\n end\n\n return total\nend\n\n-- ==========================================\n-- 공용 민감도\n-- ==========================================\nlocal sensitivityRanges = {\n {min = 0, level = 1},\n {min = 20, level = 2},\n {min = 50, level = 3},\n {min = 80, level = 4},\n {min = 120, level = 5},\n {min = 170, level = 6},\n {min = 220, level = 7},\n {min = 270, level = 8},\n {min = 330, level = 9},\n {min = 400, level = 10},\n {min = 500, level = 11}\n}\n\nfunction getsensitivityRangeForLevel(level)\n local selectedRange = sensitivityRanges[level]\n local max\n\n if selectedRange then\n if level < #sensitivityRanges then\n max = sensitivityRanges[level + 1].min - 1\n else\n max = math.huge\n end\n\n return selectedRange.min, max\n else\n return nil\n end\nend\n\nfunction getSensitivityLevelByValue(v)\n v = tonumber(v) or 0\n local level = 1\n\n for i, range in ipairs(sensitivityRanges) do\n if v >= range.min then\n level = range.level\n else\n break\n end\n end\n\n return level\nend\n\nfunction getSensitivityTextByLevel(level)\n local texts = {\n [1] = \"Almost completely numb\",\n [2] = \"Extremely dull; barely responsive\",\n [3] = \"Dull; weak stimulation is hardly felt\",\n [4] = \"Below average sensitivity\",\n [5] = \"Within a normal sensitivity range\",\n [6] = \"Slightly above average; touch is clearly felt\",\n [7] = \"Sensitive; reactions are quick and noticeable\",\n [8] = \"Very sensitive; even mild stimulation has a strong effect\",\n [9] = \"Extremely sensitive; intense reactions occur with little effort\",\n [10] = \"Hyper-sensitive; minimal contact triggers overwhelming response\",\n [11] = \"Transcendent sensitivity; constant heightened arousal even without direct stimulation\"\n }\n\n return texts[tonumber(level) or 1] or texts[1]\nend\n\nfunction getSensitivityBonusByText(triggerId, text)\n text = string.lower(tostring(text or \"\"))\n\n local sensitivity = loadDualFlagCurrentMap(triggerId, vgSensitivity)\n\n local breastValue = tonumber(sensitivity.breast) or 0\n local oralValue = tonumber(sensitivity.oral) or 0\n local clitorisValue = tonumber(sensitivity.clitoris) or 0\n local vaginalValue = tonumber(sensitivity.vaginal) or 0\n local analValue = tonumber(sensitivity.anal) or 0\n\n local breastLevel = getSensitivityLevelByValue(breastValue)\n local oralLevel = getSensitivityLevelByValue(oralValue)\n local clitorisLevel = getSensitivityLevelByValue(clitorisValue)\n local vaginalLevel = getSensitivityLevelByValue(vaginalValue)\n local analLevel = getSensitivityLevelByValue(analValue)\n\n local averageValue = (breastValue + oralValue + clitorisValue + vaginalValue + analValue) / 5\n local averageLevel = getSensitivityLevelByValue(averageValue)\n\n local function getLevelBonus(level)\n level = tonumber(level) or 6\n\n local bonusMap = {\n [1] = -0.5,\n [2] = -0.35,\n [3] = -0.225,\n [4] = -0.1,\n [5] = 0.0,\n [6] = 0.05,\n [7] = 0.1,\n [8] = 0.15,\n [9] = 0.2,\n [10] = 0.25,\n [11] = 0.3\n }\n\n return bonusMap[level] or 0\n end\n\n local function containsAnyKeyword(keywordList)\n for _, keyword in ipairs(keywordList) do\n local kw = string.lower(tostring(keyword or \"\"))\n\n if kw ~= \"\" and text:find(kw, 1, true) then\n return true\n end\n end\n\n return false\n end\n\n local keywordMap = {\n breast = {\n \"가슴\", \"젖\", \"유두\", \"흉부\", \"유방\", \"유선\", \"유륜\", \"꼭지\", \"파이즈리\", \"빈유\", \"거유\",\n\n \"breast\", \"boob\", \"tit\", \"nipple\", \"paizuri\", \"areola\",\n\n \"ちち\", \"乳首\", \"胸\", \"パイズリ\", \"おっぱい\", \"巨乳\", \"貧乳\"\n },\n\n oral = {\n \"입\", \"혀\", \"구강\", \"목구멍\", \"키스\", \"펠라\", \"딥스로트\", \"이라마치오\", \"혓바닥\", \"뽀뽀\",\n\n \"mouth\", \"lip\", \"tongue\", \"oral\", \"throat\", \"kiss\", \"blowjob\", \"fellatio\",\n\n \"口\", \"唇\", \"舌\", \"喉\", \"のど\", \"キス\", \"フェラ\"\n },\n\n clitoris = {\n \"클리\", \"음핵\", \"사타구니\", \"고간\", \"다리 사이\", \"은밀한 곳\", \"민감한 부위\", \"허벅지 사이\", \"허벅지 안\",\n \"속옷 안\", \"속옷 위\", \"스커트 안\", \"치마 속\", \"치마 안\", \"스커트 속\", \"팬티 위\", \"팬티 안\",\n\n \"clit\",\n\n \"クリ\", \"陰核\"\n },\n\n vaginal = {\n \"질\", \"보지\", \"음부\", \"자궁\", \"음순\", \"보짓\", \"아기방\", \"아가방\", \"사타구니\", \"고간\",\n \"다리 사이\", \"비소\", \"비처\", \"은밀한 곳\", \"민감한 부위\", \"허벅지 사이\", \"허벅지 안\", \"여성기\",\n \"성기 주변\", \"비부\", \"속옷 안\", \"처녀\", \"하복부 틈새\", \"속옷 안\", \"팬티 안\",\n\n \"vagina\", \"vaginal\", \"pussy\", \"cunt\", \"vulva\", \"cervix\", \"womb\", \"crotch\", \"groin\",\n\n \"膣\", \"まんこ\", \"ヴァギナ\", \"外陰\", \"子宮\", \"陰部\", \"股間\", \"鼠径部\"\n },\n\n anal = {\n \"항문\", \"엉덩\", \"뒷구멍\", \"항내\", \"직장\", \"둔부\", \"똥구멍\", \"괄약근\", \"애널\", \"아날\", \"후장\",\n\n \"anus\", \"anal\", \"butt\", \"buttock\", \"ass\", \"rectum\",\n\n \"尻\", \"肛門\", \"アナル\", \"ケツ\", \"直腸\"\n }\n }\n\n local bestMatchedLevel = nil\n\n if containsAnyKeyword(keywordMap.breast) then\n bestMatchedLevel = breastLevel\n end\n\n if containsAnyKeyword(keywordMap.oral) then\n if not bestMatchedLevel or oralLevel > bestMatchedLevel then\n bestMatchedLevel = oralLevel\n end\n end\n\n if containsAnyKeyword(keywordMap.clitoris) then\n if not bestMatchedLevel or clitorisLevel > bestMatchedLevel then\n bestMatchedLevel = clitorisLevel\n end\n end\n\n if containsAnyKeyword(keywordMap.vaginal) then\n if not bestMatchedLevel or vaginalLevel > bestMatchedLevel then\n bestMatchedLevel = vaginalLevel\n end\n end\n\n if containsAnyKeyword(keywordMap.anal) then\n if not bestMatchedLevel or analLevel > bestMatchedLevel then\n bestMatchedLevel = analLevel\n end\n end\n\n if bestMatchedLevel then\n return getLevelBonus(bestMatchedLevel)\n end\n\n return getLevelBonus(averageLevel)\nend\n\n-- ==========================================\n-- 공용 음란도\n-- ==========================================\nlocal lewdnessRanges = {\n {min = 0, level = 1},\n {min = 20, level = 2},\n {min = 45, level = 3},\n {min = 75, level = 4},\n {min = 115, level = 5},\n {min = 165, level = 6},\n {min = 225, level = 7},\n {min = 295, level = 8},\n {min = 375, level = 9},\n {min = 465, level = 10},\n {min = 565, level = 11}\n}\n\nfunction getLewdnessLevelByValue(v)\n v = tonumber(v) or 0\n local level = 1\n\n for _, range in ipairs(lewdnessRanges) do\n if v >= range.min then\n level = range.level\n else\n break\n end\n end\n\n return level\nend\n\nfunction getLewdnessLevel(triggerId)\n local lewdnessValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.lewdness, 0)) or 0\n return getLewdnessLevelByValue(lewdnessValue)\nend\n\nfunction getLewdnessProgressInfo(value)\n value = tonumber(value) or 0\n\n local level = getLewdnessLevelByValue(value)\n local currentMin = tonumber((lewdnessRanges[level] or {}).min) or 0\n local nextMin = tonumber((lewdnessRanges[level + 1] or {}).min)\n\n if not nextMin then\n return level, math.max(0, value - currentMin), 0, true\n end\n\n local currentExp = math.max(0, value - currentMin)\n local needExp = math.max(1, nextMin - currentMin)\n\n return level, currentExp, needExp, false\nend\n\nfunction getLewdnessRangeForLevel(level)\n local selectedRange = lewdnessRanges[level]\n local max\n\n if selectedRange then\n if level < #lewdnessRanges then\n max = lewdnessRanges[level + 1].min - 1\n else\n max = math.huge\n end\n\n return selectedRange.min, max\n else\n return nil\n end\nend\n\nfunction getLewdnessTextByLevel(level)\n level = tonumber(level) or 1\n\n if level <= 2 then\n return \"Sexual self-restraint is strong; lewd arousal remains under firm control.\"\n elseif level <= 4 then\n return \"Sexual self-restraint is steady; lewd arousal occasionally tests it.\"\n elseif level <= 6 then\n return \"Sexual self-restraint is moderate; lewd arousal is not easily dismissed once stirred, and sexual curiosity may occasionally surface.\"\n elseif level <= 8 then\n return \"Sexual self-restraint is fragile; strong lewd arousal is difficult to hold back, and sexual impulse occasionally surfaces.\"\n elseif level <= 10 then\n return \"Sexual self-restraint is very weak; the body is easily overtaken by lewd arousal, and sexual impulse is difficult to ignore.\"\n else\n return \"Sexual self-restraint is almost absent once stirred, making lewd arousal and sexual impulse nearly impossible to suppress.\"\n end\nend\n\n-- ==========================================\n-- 공용 타락\n-- ==========================================\nlocal CORRUPTION_MAX_LEVEL = 5\n\nlocal function getCorruptionTotalExpForLevel(level)\n level = math.max(0, math.floor(tonumber(level) or 0))\n level = math.min(level, CORRUPTION_MAX_LEVEL)\n\n return level * 100\nend\n\nfunction getCorruptionLevelByValue(value)\n value = math.max(0, tonumber(value) or 0)\n\n local level = 0\n\n while level < CORRUPTION_MAX_LEVEL and value >= getCorruptionTotalExpForLevel(level + 1) do\n level = level + 1\n end\n\n return level\nend\n\nfunction getCorruptionProgressInfo(value)\n value = math.max(0, tonumber(value) or 0)\n\n local level = getCorruptionLevelByValue(value)\n local currentMin = getCorruptionTotalExpForLevel(level)\n\n if level >= CORRUPTION_MAX_LEVEL then\n return CORRUPTION_MAX_LEVEL, 0, 0, true\n end\n\n local nextMin = getCorruptionTotalExpForLevel(level + 1)\n\n local currentExp = math.max(0, value - currentMin)\n local needExp = math.max(1, nextMin - currentMin)\n\n return level, currentExp, needExp, false\nend\n\n-- ==========================================\n-- 공용 카드\n-- ==========================================\nfunction getCardTypeName(triggerId, typeCode, phase)\n typeCode = tonumber(typeCode) or 0\n phase = tonumber(phase) or getPhase(triggerId)\n\n local originalTypeCode = typeCode\n typeCode = normalizeCardTypeCode(typeCode)\n\n local heroineSpecialState = getHeroineSpecialState(triggerId)\n\n if typeCode ~= constCardType.ecstasy then\n if heroineSpecialState == vgHeroineEffect.sleep then\n if isPrimaryLikeCardType(typeCode) == false and isSecondaryLikeCardType(typeCode) == false then\n return \"Deep Sleep\"\n end\n\n return \"Sleep\"\n elseif heroineSpecialState == vgHeroineEffect.immobilized then\n if isPrimaryLikeCardType(typeCode) == false and isSecondaryLikeCardType(typeCode) == false then\n return \"Immobilization Prolongation\"\n end\n\n return \"Immobilized\"\n elseif heroineSpecialState == vgHeroineEffect.brainwashed then\n if isPrimaryLikeCardType(typeCode) == false and isSecondaryLikeCardType(typeCode) == false then\n return \"Mental Influence Prolongation\"\n end\n\n return \"Obey\"\n end\n end\n\n if isHEventPhase(triggerId, phase) then\n if isPrimaryLikeCardType(typeCode) then\n return \"Defiant\"\n elseif isSecondaryLikeCardType(typeCode) then\n return \"Yielding\"\n elseif typeCode == constCardType.ecstasy then\n return \"Ecstasy\"\n else\n return \"Helpless\"\n end\n end\n\n if isPerformancePhase(triggerId, phase) then\n if isPrimaryLikeCardType(typeCode) then\n return \"Perform\"\n elseif isSecondaryLikeCardType(typeCode) then\n return \"Deflect\"\n else\n return \"Build-Up\"\n end\n end\n\n if isPromiscuityPhase(triggerId, phase) then\n if isPrimaryLikeCardType(typeCode) then\n return \"Indulge\"\n elseif isSecondaryLikeCardType(typeCode) then\n return \"Deflect\"\n else\n return \"Technique\"\n end\n end\n\n if isIntimacyPhase(triggerId, phase) then\n if isPrimaryLikeCardType(typeCode) then\n return \"Embrace\"\n elseif isSecondaryLikeCardType(typeCode) then\n return \"Guide\"\n else\n return \"Attune\"\n end\n end\n\n if isMasturbationPhase(triggerId, phase) then\n if isPrimaryLikeCardType(typeCode) or isSecondaryLikeCardType(typeCode) then\n return \"Stimulate\"\n else\n return \"Fantasy\"\n end\n end\n\n if isExhibitionismPhase(triggerId, phase) then\n if isPrimaryLikeCardType(typeCode) then\n return \"Expose\"\n elseif isSecondaryLikeCardType(typeCode) then\n return \"Fluster\"\n else\n return \"Thrill\"\n end\n end\n\n if isDefiantPhase(triggerId, phase) then\n if isPrimaryLikeCardType(typeCode) then\n return \"Withstand\"\n elseif isSecondaryLikeCardType(typeCode) then\n return \"Deflect\"\n else\n return \"Brace\"\n end\n elseif isYieldingPhase(triggerId, phase) then\n if isPrimaryLikeCardType(typeCode) then\n return \"Submit\"\n elseif isSecondaryLikeCardType(typeCode) then\n return \"Deflect\"\n else\n return \"Technique\"\n end\n end\n\n if typeCode == constCardType.primary then\n if isHypnosisPhase(triggerId, phase) then\n return \"Reclaim\"\n elseif isHarassmentPhase(triggerId, phase) or isAbusePhase(triggerId, phase) or isConfrontationPhase(triggerId, phase) then\n return \"Pushback\"\n elseif isRescuePhase(triggerId, phase) then\n return \"Support\"\n else\n return \"Attack\"\n end\n elseif typeCode == constCardType.secondary then\n if isHypnosisPhase(triggerId, phase) or isHarassmentPhase(triggerId, phase) then\n return \"Resistance\"\n elseif isAbusePhase(triggerId, phase) then\n return \"Boundary\"\n elseif isConfrontationPhase(triggerId, phase) then\n return \"Rebuttal\"\n elseif isRescuePhase(triggerId, phase) then\n return \"Interfere\"\n else\n return \"Defense\"\n end\n\n elseif typeCode == constCardType.taunt then\n return \"Taunt\"\n \n elseif typeCode == constCardType.temptation then\n return \"Temptation\"\n\n elseif typeCode == constCardType.milking then\n return \"Milking\"\n\n elseif typeCode == constCardType.focus then\n if isHypnosisPhase(triggerId, phase) then\n return \"Anchor\"\n elseif isHarassmentPhase(triggerId, phase) or isAbusePhase(triggerId, phase) or isConfrontationPhase(triggerId, phase) then\n return \"Composure\"\n elseif isRescuePhase(triggerId, phase) then\n return \"Signal\"\n else\n return \"Focus\"\n end\n\n elseif typeCode == constCardType.charge then\n if isHarassmentPhase(triggerId, phase)\n or isAbusePhase(triggerId, phase)\n or isConfrontationPhase(triggerId, phase)\n or isHypnosisPhase(triggerId, phase)\n or isRescuePhase(triggerId, phase) then\n return \"Respite\"\n else\n return \"Charge\"\n end\n\n elseif typeCode == constCardType.heal then\n if isHarassmentPhase(triggerId, phase)\n or isAbusePhase(triggerId, phase)\n or isConfrontationPhase(triggerId, phase)\n or isHypnosisPhase(triggerId, phase)\n or isRescuePhase(triggerId, phase) then\n return \"Rally\"\n else\n return \"Heal\"\n end\n\n elseif typeCode == constCardType.soothe then\n return \"Soothe\"\n\n elseif typeCode == constCardType.ecstasy then\n return \"Ecstasy\"\n end\n\n return \"Unknown\"\nend\n\nfunction getCardTypeEmoji(triggerId, typeCode, phase)\n typeCode = tonumber(typeCode) or 0\n phase = tonumber(phase) or getPhase(triggerId)\n\n local originalTypeCode = typeCode\n typeCode = normalizeCardTypeCode(typeCode)\n\n if typeCode == constCardType.surrender then\n return \"💀\"\n elseif typeCode == constCardType.prolonged then\n return \"⏰\"\n elseif typeCode == constCardType.situationEnd then\n return \"✔️\"\n end\n\n if phase ~= constPhase.inspect and typeCode ~= constCardType.ecstasy then\n local heroineSpecialState = getHeroineSpecialState(triggerId)\n if heroineSpecialState == vgHeroineEffect.sleep then\n if isPrimaryLikeCardType(typeCode) == false and isSecondaryLikeCardType(typeCode) == false then\n return \"💫\"\n end\n\n return \"💤\"\n elseif heroineSpecialState == vgHeroineEffect.immobilized then\n if isPrimaryLikeCardType(typeCode) == false and isSecondaryLikeCardType(typeCode) == false then\n return \"💫\"\n end\n\n return \"🧊\"\n elseif heroineSpecialState == vgHeroineEffect.brainwashed then\n if isPrimaryLikeCardType(typeCode) == false and isSecondaryLikeCardType(typeCode) == false then\n return \"💫\"\n end\n\n return \"🙇♂️\"\n end\n end\n\n if isHEventPhase(triggerId, phase) then\n if isPrimaryLikeCardType(typeCode) then\n return \"✊\"\n elseif isSecondaryLikeCardType(typeCode) then\n return \"🤲\"\n elseif typeCode == constCardType.ecstasy then\n return \"💞\"\n else\n return \"💫\"\n end\n end\n\n if isPromiscuityPhase(triggerId, phase) or isIntimacyPhase(triggerId, phase) then\n if isPrimaryLikeCardType(typeCode) then\n return \"💟\"\n elseif isSecondaryLikeCardType(typeCode) then\n return \"🙅\"\n else\n return \"💄\"\n end\n end\n\n if isPerformancePhase(triggerId, phase) then\n if isPrimaryLikeCardType(typeCode) then\n return \"👏\"\n elseif isSecondaryLikeCardType(typeCode) then\n return \"🙏\"\n else\n return \"🎵\"\n end\n end\n\n if isMasturbationPhase(triggerId, phase) then\n if isPrimaryLikeCardType(typeCode) then\n return \"👆\"\n elseif isSecondaryLikeCardType(typeCode) then\n return \"🤟\"\n else\n return \"💭\"\n end\n end\n\n if isExhibitionismPhase(triggerId, phase) then\n if isPrimaryLikeCardType(typeCode) then\n return \"👙\"\n elseif isSecondaryLikeCardType(typeCode) then\n return \"🌫️\"\n else\n return \"👀\"\n end\n end\n\n if isDefiantPhase(triggerId, phase) then\n if isPrimaryLikeCardType(typeCode) then\n return \"✊\"\n elseif isSecondaryLikeCardType(typeCode) then\n return \"🙏\"\n else\n return \"🎯\"\n end\n end\n\n if isYieldingPhase(triggerId, phase) then\n if isPrimaryLikeCardType(typeCode) then\n return \"🤲\"\n elseif isSecondaryLikeCardType(typeCode) then\n return \"🙏\"\n else\n return \"💄\"\n end\n end\n\n if typeCode == constCardType.primary then\n if isHypnosisPhase(triggerId, phase) then\n return \"🔮\"\n elseif isHarassmentPhase(triggerId, phase) or isAbusePhase(triggerId, phase) or isConfrontationPhase(triggerId, phase) then\n return \"🗯️\"\n elseif isRescuePhase(triggerId, phase) then\n return \"🙋♀️\"\n else\n if originalTypeCode == constCardType.breaker then\n return \"🪓\"\n elseif originalTypeCode == constCardType.counter then\n return \"⚔️\"\n end\n\n return \"🗡️\"\n end\n\n elseif typeCode == constCardType.secondary then\n if isHypnosisPhase(triggerId, phase)\n or isHarassmentPhase(triggerId, phase)\n or isAbusePhase(triggerId, phase)\n or isConfrontationPhase(triggerId, phase)\n or isRescuePhase(triggerId, phase) then\n return \"✋\"\n else\n return \"🛡️\"\n end\n\n\n elseif typeCode == constCardType.taunt then\n return \"🤷\"\n\n elseif typeCode == constCardType.temptation then\n return \"🌸\"\n\n elseif typeCode == constCardType.milking then\n return \"👄\"\n\n elseif typeCode == constCardType.focus then\n if isRescuePhase(triggerId, phase) then\n return \"📡\"\n end\n\n return \"🎯\"\n\n elseif typeCode == constCardType.charge then\n if isHypnosisPhase(triggerId, phase)\n or isHarassmentPhase(triggerId, phase)\n or isAbusePhase(triggerId, phase)\n or isConfrontationPhase(triggerId, phase)\n or isRescuePhase(triggerId, phase) then\n return \"🍃\"\n else\n return \"✨\"\n end\n\n elseif typeCode == constCardType.heal then\n return \"🌿\"\n\n elseif typeCode == constCardType.ecstasy then\n return \"💞\"\n\n elseif typeCode == constCardType.soothe then\n return \"🧘\"\n\n end\n\n return \"❓\"\nend\n\nfunction getCardLevelStars(level)\n level = tonumber(level) or 0\n level = clamp(level, 0, 10)\n\n if level <= 0 then\n return \"\"\n end\n\n if level <= 5 then\n return string.rep(\"⭐\", level)\n end\n\n return string.rep(\"🌟\", level - 5) .. string.rep(\"⭐\", 10 - level)\nend\n\n-- ==========================================\n-- 공용 액션\n-- ==========================================\nfunction normalizeEncounterActionToEffectTypeName(name)\n name = trim(tostring(name or \"\"))\n\n local encounterActionNameToEffectTypeName = {\n Attack = \"Damage\",\n Defense = \"Guard\",\n Molestation = \"Sexual\",\n Encroachment = \"Subdual\",\n Coercion = \"Pressure\",\n Justification = \"Rhetoric\",\n Intimidation = \"Pressure\",\n [\"Verbal Aggression\"] = \"Rhetoric\",\n Command = \"Suggestion\",\n Desecration = \"Defilement\",\n Breach = \"Invasion\",\n Harassment = \"Humiliation\",\n Demand = \"Escalation\",\n Interdiction = \"Interception\",\n Invitation = \"Affection\",\n Entwinement = \"Union\",\n }\n\n return encounterActionNameToEffectTypeName[name] or name\nend\n\nfunction normalizeHeroineActionToEffectTypeName(triggerId, name)\n name = trim(tostring(name or \"\"))\n\n if name == \"\" then\n return \"\"\n end\n\n local curPhase = getPhase(triggerId)\n local heroineSpecialState = getHeroineSpecialState(triggerId)\n\n -- 특수 상태\n if name == \"Sleep\" then\n return \"Somniloquy\"\n elseif name == \"Immobilized\" then\n return \"Stasis\"\n elseif name == \"Obey\" then\n return \"Sexual\"\n elseif name == \"Deep Sleep\" then\n return \"Slumber\"\n elseif name == \"Immobilization Prolongation\" then\n return \"Immobilization\"\n elseif name == \"Mental Influence Prolongation\" then\n return \"Conditioning\"\n end\n\n -- phase 의존 별칭\n if name == \"Resistance\" then\n if isHypnosisPhase(triggerId, curPhase) then\n return \"Mitigation\"\n elseif isHarassmentPhase(triggerId, curPhase) then\n return \"Defiance\"\n end\n elseif name == \"Deflect\" then\n if isPerformancePhase(triggerId, curPhase) or isPromiscuityPhase(triggerId, curPhase) or isIntimacyPhase(triggerId, curPhase) then\n return \"Redirection\"\n elseif isIncapacitatedPhase(triggerId, curPhase)\n or isDefiantPhase(triggerId, curPhase)\n or isYieldingPhase(triggerId, curPhase) then\n return \"Mitigation\"\n end\n end\n\n local heroineActionNameToEffectTypeName = {\n -- 일반 / 전투 / 대치\n Attack = \"Damage\",\n Pushback = \"Sway\",\n Reclaim = \"Liberation\",\n Support = \"Coordination\",\n\n Boundary = \"Refusal\",\n Rebuttal = \"Retort\",\n Interfere = \"Disruption\",\n Defense = \"Guard\",\n\n Taunt = \"Provocation\",\n Temptation = \"Allure\",\n Milking = \"Ravishment\",\n\n Anchor = \"Buff\",\n Composure = \"Buff\",\n Signal = \"Buff\",\n Focus = \"Buff\",\n\n Respite = \"Refresh\",\n Charge = \"Refresh\",\n\n Rally = \"Restoration\",\n Heal = \"Restoration\",\n\n Soothe = \"Calm\",\n Ecstasy = \"Sexual\",\n\n -- HEvent\n Defiant = \"Rejection\",\n Yielding = \"Accommodation\",\n Helpless = \"Passivity\",\n\n -- Performance / Promiscuity\n Perform = \"Appeasement\",\n Indulge = \"Appeasement\",\n [\"Build-Up\"] = \"Buff\",\n Technique = \"Buff\",\n\n -- Masturbation\n Stimulate = \"Sexual\",\n Fantasy = \"Sexual\",\n\n -- Exhibitionism\n Expose = \"Excitement\",\n Fluster = \"Excitement\",\n Thrill = \"Excitement\",\n\n -- Incapacitated (Defiant / Yielding)\n Withstand = \"Appeasement\",\n Submit = \"Appeasement\",\n Brace = \"Buff\",\n }\n\n return heroineActionNameToEffectTypeName[name] or name\nend\n\nfunction getEffectTypeCode(effectTypeName)\n effectTypeName = normalizeEncounterActionToEffectTypeName(effectTypeName)\n\n local effectTypeNameToCode = {\n Damage = constEffectType.damage,\n Sway = constEffectType.damage,\n Coordination = constEffectType.damage,\n Ravishment = constEffectType.damage,\n\n Appeasement = constEffectType.appeasement,\n Liberation = constEffectType.liberation,\n\n Guard = constEffectType.guard,\n Defiance = constEffectType.guard,\n Refusal = constEffectType.guard,\n Retort = constEffectType.guard,\n Mitigation = constEffectType.guard,\n Redirection = constEffectType.guard,\n Disruption = constEffectType.guard,\n Allure = constEffectType.guard,\n\n Provocation = constEffectType.guard,\n\n Buff = constEffectType.buff,\n\n Refresh = constEffectType.refresh,\n Restoration = constEffectType.recovery,\n Calm = constEffectType.calm,\n\n Somniloquy = constEffectType.inert,\n Stasis = constEffectType.inert,\n\n Slumber = constEffectType.recovery,\n Immobilization = constEffectType.recovery,\n Conditioning = constEffectType.recovery,\n\n Sexual = constEffectType.sexual,\n Excitement = constEffectType.excitement,\n\n Rejection = constEffectType.inert,\n Accommodation = constEffectType.inert,\n Passivity = constEffectType.inert,\n\n Exhaustion = constEffectType.exhaustion,\n \n Subdual = constEffectType.subdual,\n\n Pressure = constEffectType.pressure,\n Rhetoric = constEffectType.rhetoric,\n\n Defilement = constEffectType.defilement,\n Invasion = constEffectType.invasion,\n Affection = constEffectType.defilement,\n Union = constEffectType.invasion,\n\n Humiliation = constEffectType.humiliation,\n Escalation = constEffectType.escalation,\n\n Suggestion = constEffectType.suggestion,\n\n Interception = constEffectType.interception,\n\n Cost = constEffectType.cost,\n }\n\n return effectTypeNameToCode[effectTypeName] or 0\nend\n\nfunction isPercentEffectType(effectTypeCode)\n effectTypeCode = tonumber(effectTypeCode) or 0\n\n local percentEffectMap = {\n [constEffectType.guardbreak] = true,\n }\n\n return percentEffectMap[effectTypeCode] == true\nend\n\nfunction getEffectTypeEmoji(triggerId, effectTypeCode, phase)\n effectTypeCode = tonumber(effectTypeCode) or 0\n phase = tonumber(phase) or getPhase(triggerId)\n\n if effectTypeCode == constEffectType.damage or effectTypeCode == constEffectType.liberation then\n if isHarassmentPhase(triggerId, phase) or isAbusePhase(triggerId, phase) or isConfrontationPhase(triggerId, phase) then\n return \"📣\"\n elseif isRescuePhase(triggerId, phase) then\n return \"🤝\"\n else\n return \"💥\"\n end\n\n elseif effectTypeCode == constEffectType.guard then\n if isHarassmentPhase(triggerId, phase)\n or isAbusePhase(triggerId, phase)\n or isConfrontationPhase(triggerId, phase) then\n return \"⛔\"\n elseif isIncapacitatedPhase(triggerId, phase)\n or isHypnosisPhase(triggerId, phase)\n or isRescuePhase(triggerId, phase)\n or isPerformancePhase(triggerId, phase)\n or isPromiscuityPhase(triggerId, phase)\n or isIntimacyPhase(triggerId, phase) then\n return \"⌛\"\n else\n return \"🧱\"\n end\n\n elseif effectTypeCode == constEffectType.buff then\n if isHarassmentPhase(triggerId, phase) or isAbusePhase(triggerId, phase) or isConfrontationPhase(triggerId, phase) or isDefiantPhase(triggerId, phase) or isHypnosisPhase(triggerId, phase) then\n return \"⚓\"\n elseif isYieldingPhase(triggerId, phase) or isPromiscuityPhase(triggerId, phase) or isIntimacyPhase(triggerId, phase) then\n return \"💋\"\n elseif isPerformancePhase(triggerId, phase) then\n return \"🎼\"\n elseif isRescuePhase(triggerId, phase) then\n return \"🔦\"\n else\n return \"💪\"\n end\n\n elseif effectTypeCode == constEffectType.rage then\n return \"💢\"\n\n elseif effectTypeCode == constEffectType.appeasement then\n return \"💕\"\n\n elseif effectTypeCode == constEffectType.refresh then\n return \"⚡\"\n\n elseif effectTypeCode == constEffectType.sexual or effectTypeCode == constEffectType.calm or effectTypeCode == constEffectType.humiliation then\n return \"💗\"\n\n elseif effectTypeCode == constEffectType.excitement then\n return \"💓\"\n\n elseif effectTypeCode == constEffectType.recovery then\n return \"❤️\"\n\n elseif effectTypeCode == constEffectType.guardbreak then\n return \"☢️\"\n\n elseif effectTypeCode == constEffectType.exhaustion then\n return \"⛓️\"\n\n elseif effectTypeCode == constEffectType.subdual then\n return \"🔻\"\n\n elseif effectTypeCode == constEffectType.pressure then\n return \"🗜️\"\n\n elseif effectTypeCode == constEffectType.rhetoric then\n return \"🎭\"\n\n elseif effectTypeCode == constEffectType.defilement then\n return \"👅\"\n\n elseif effectTypeCode == constEffectType.invasion then\n return \"🥀\"\n\n elseif effectTypeCode == constEffectType.escalation then\n return \"💌\"\n\n elseif effectTypeCode == constEffectType.suggestion then\n return \"🌀\"\n\n elseif effectTypeCode == constEffectType.interception then\n return \"🚧\"\n\n elseif effectTypeCode == constEffectType.encounterRecovery then\n return \"🧡\"\n\n elseif effectTypeCode == constEffectType.sleep then\n return \"💤\"\n\n elseif effectTypeCode == constEffectType.immobilized then\n return \"🧊\"\n\n elseif effectTypeCode == constEffectType.brainwashed then\n return \"🧠\"\n end\n\n return \"❓\"\nend\n\nfunction isHeroineBuffTargetCardType(cardTypeCode)\n return isPrimaryLikeCardType(cardTypeCode)\n or isSecondaryLikeCardType(cardTypeCode)\nend\n\nfunction buildEffectTypeLiteralGuideLine(effectTypeList)\n local exampleType = \"Damage\"\n\n if effectTypeList and #effectTypeList > 0 and trim(effectTypeList[1]) ~= \"\" then\n exampleType = trim(effectTypeList[1])\n end\n\n local exampleText = exampleType .. \": Medium\"\n\n return \"- In the actual output, \\\"<Effect Type>: <value>\\\" must be replaced by the chosen effect name and value themselves, not written literally (e.g., \\\"\" .. exampleText .. \"\\\", not \\\"Effect Type: Medium\\\").\"\nend\n\nfunction addOrMergePackedEffect(effectList, effectTypeCode, effectValue)\n effectTypeCode = tonumber(effectTypeCode) or 0\n effectValue = roundToTenth(tonumber(effectValue) or 0)\n\n if effectTypeCode == 0 then\n return\n end\n\n if effectValue == 0 and shouldKeepZeroEffectType(effectTypeCode) == false then\n return\n end\n\n for _, effect in ipairs(effectList) do\n if tonumber(effect.code) == effectTypeCode then\n effect.value = roundToTenth((tonumber(effect.value) or 0) + effectValue)\n return\n end\n end\n\n table.insert(effectList, {\n code = effectTypeCode,\n value = effectValue\n })\nend\n\nfunction buildPackedEffectString(effectList)\n local parts = {}\n\n for _, effect in ipairs(effectList or {}) do\n local code = tonumber(effect.code) or 0\n local value = formatFloat(effect.value or 0)\n\n if code ~= 0 then\n table.insert(parts, tostring(code) .. \":\" .. value)\n end\n end\n\n return table.concat(parts, \"/\")\nend\n\nfunction getEncounterEffectTypeListByPhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n local effectTypeList = {}\n local seen = {}\n\n local function addEffect(effectName)\n effectName = trim(tostring(effectName or \"\"))\n\n if effectName ~= \"\" and not seen[effectName] then\n seen[effectName] = true\n table.insert(effectTypeList, effectName)\n end\n end\n\n if isCombatPhase(triggerId, phase) then\n addEffect(\"Damage\")\n addEffect(\"Guard\")\n addEffect(\"Sexual\")\n\n elseif isHarassmentPhase(triggerId, phase) then\n addEffect(\"Subdual\")\n addEffect(\"Sexual\")\n\n elseif isAbusePhase(triggerId, phase) or isConfrontationPhase(triggerId, phase) then\n addEffect(\"Pressure\")\n addEffect(\"Rhetoric\")\n addEffect(\"Sexual\")\n\n elseif isPerformancePhase(triggerId, phase) then\n addEffect(\"Humiliation\")\n addEffect(\"Escalation\")\n\n elseif isIncapacitatedPhase(triggerId, phase) or isPromiscuityPhase(triggerId, phase) or isHEventPhase(triggerId, phase) then\n addEffect(\"Defilement\")\n addEffect(\"Invasion\")\n\n elseif isHypnosisPhase(triggerId, phase) then\n addEffect(\"Suggestion\")\n addEffect(\"Sexual\")\n\n elseif isRescuePhase(triggerId, phase) then\n addEffect(\"Interception\")\n addEffect(\"Sexual\")\n\n elseif isIntimacyPhase(triggerId, phase) then\n addEffect(\"Affection\")\n addEffect(\"Union\")\n\n end\n\n return effectTypeList\nend\n\nfunction buildPackedEffectChoiceText(triggerId, raw)\n local texts = {}\n\n raw = trim(tostring(raw or \"\"))\n if raw == \"\" then\n return \"\"\n end\n\n for token in raw:gmatch(\"([^/]+)\") do\n local codeStr, valueStr = trim(token):match(\"^([%-+]?%d+)%s*:%s*([%-+]?[%d%.]+)$\")\n\n local code = tonumber(codeStr) or 0\n local value = tonumber(valueStr) or 0\n\n if code ~= 0 and shouldKeepZeroEffectType(code) == false then\n local valueText = getSignedEffectText(value)\n\n if isPercentEffectType(code) then\n valueText = valueText .. \"%\"\n end\n\n table.insert(texts, getEffectTypeEmoji(triggerId, code) .. valueText)\n end\n end\n\n return table.concat(texts, \" \")\nend\n\nfunction buildPlayDeckChoice(triggerId)\n local isHEvent = isHEventPhase(triggerId)\n\n local _, baseTypeMap, baseLevelMap = buildRuntimeDeckLookup(triggerId)\n\n local availableCount = math.max(1, getActionAvailableCount(triggerId))\n setChatVar(triggerId, \"vg_Action_Available\", tostring(availableCount))\n\n local actionDots = string.rep(\"⚪\", availableCount)\n\n local encounterLines = {}\n local encounterRows = splitPipe(safeGet(triggerId, \"vg_Encounter_Action\", \"\"))\n\n for _, row in ipairs(encounterRows) do\n local parts = splitSection(row)\n\n local sourceName = trim(parts[1] or \"\")\n local actionName = trim(parts[2] or \"\")\n local effectRaw = trim(parts[3] or \"\")\n\n local effectText = buildPackedEffectChoiceText(triggerId, effectRaw)\n\n if sourceName ~= \"\" and actionName ~= \"\" then\n table.insert(encounterLines,\n \"[Encounter Action: \" .. sourceName ..\n \" | Action: \" .. actionName ..\n \" | Effect: \" .. effectText .. \"]\"\n )\n end\n end\n\n local playerLines = {}\n local actionRows = splitPipe(safeGet(triggerId, \"vg_PlayDeck_Action\", \"\"))\n\n for _, row in ipairs(actionRows) do\n local parts = splitSection(row)\n\n local code = trim(parts[1] or \"\")\n local actionName = trim(parts[2] or \"\")\n local costValue = tonumber(parts[3]) or 0\n local effectRaw = trim(parts[4] or \"\")\n\n local cardType = baseTypeMap[code] or 0\n local cardLevel = baseLevelMap[code] or 0\n\n if code == \"-9999\" then\n cardType = constCardType.surrender\n elseif code == \"-9998\" then\n cardType = constCardType.prolonged\n elseif code == \"-9997\" then\n cardType = constCardType.situationEnd\n end\n\n local typeEmoji = getCardTypeEmoji(triggerId, cardType)\n\n local costText = \"\"\n if costValue > 0 then\n costText = \"⚡-\" .. formatFloat(costValue)\n end\n\n local effectText = buildPackedEffectChoiceText(triggerId, effectRaw)\n\n table.insert(playerLines,\n \"[Code: \" .. code ..\n \" | Action Type: \" .. typeEmoji ..\n \" | Action: \" .. actionName ..\n \" | Cost: \" .. costText ..\n \" | Effect: \" .. effectText ..\n \" | Level: \" .. getCardLevelStars(cardLevel) .. \"]\"\n )\n end\n\n local choiceLines = {}\n\n for _, line in ipairs(encounterLines) do\n table.insert(choiceLines, line)\n end\n\n table.insert(choiceLines, \"[Blank Line]\")\n\n local heroineSpecialState = getHeroineSpecialState(triggerId)\n\n if not isHEvent and heroineSpecialState ~= vgHeroineEffect.sleep and heroineSpecialState ~= vgHeroineEffect.immobilized and heroineSpecialState ~= vgHeroineEffect.brainwashed\n and #encounterLines > 0 and #playerLines > 0 then\n\n local curPhase = getPhase(triggerId)\n\n local secondaryEmoji = getCardTypeEmoji(triggerId, constCardType.secondary)\n local guardPriorityOrders = {}\n\n if isAbusePhase(triggerId, curPhase) or isConfrontationPhase(triggerId, curPhase) then\n guardPriorityOrders[1] = { constEffectType.pressure, constEffectType.rhetoric, constEffectType.sexual }\n guardPriorityOrders[2] = { constEffectType.rhetoric, constEffectType.pressure, constEffectType.sexual }\n guardPriorityOrders[3] = { constEffectType.sexual, constEffectType.pressure, constEffectType.rhetoric }\n guardPriorityOrders[4] = { constEffectType.sexual, constEffectType.rhetoric, constEffectType.pressure }\n\n elseif isHypnosisPhase(triggerId, curPhase) then\n guardPriorityOrders[1] = { constEffectType.suggestion, constEffectType.sexual }\n guardPriorityOrders[2] = { constEffectType.sexual, constEffectType.suggestion }\n\n elseif isPerformancePhase(triggerId, curPhase) then\n guardPriorityOrders[1] = { constEffectType.humiliation, constEffectType.escalation }\n guardPriorityOrders[2] = { constEffectType.escalation, constEffectType.humiliation }\n\n elseif isIncapacitatedPhase(triggerId, curPhase) or isPromiscuityPhase(triggerId, curPhase) or isIntimacyPhase(triggerId, curPhase) then\n guardPriorityOrders[1] = { constEffectType.invasion, constEffectType.defilement }\n guardPriorityOrders[2] = { constEffectType.defilement, constEffectType.invasion }\n\n elseif isRescuePhase(triggerId, curPhase) then\n guardPriorityOrders[1] = { constEffectType.interception, constEffectType.sexual }\n guardPriorityOrders[2] = { constEffectType.sexual, constEffectType.interception }\n\n else\n local effectCode1 = constEffectType.damage\n local effectCode2 = constEffectType.sexual\n\n if isHarassmentPhase(triggerId, curPhase) then\n effectCode1 = constEffectType.subdual\n end\n\n guardPriorityOrders[1] = { effectCode1, effectCode2 }\n guardPriorityOrders[2] = { effectCode2, effectCode1 }\n end\n\n for guardCode, effectOrder in ipairs(guardPriorityOrders) do\n local emojiParts = {}\n\n for _, effectCode in ipairs(effectOrder) do\n table.insert(emojiParts, getEffectTypeEmoji(triggerId, effectCode))\n end\n\n table.insert(\n choiceLines,\n \"[Guard Code: \" .. tostring(guardCode) ..\n \" | Text: \" .. secondaryEmoji .. \": \" .. table.concat(emojiParts, \" → \") .. \"]\"\n )\n end\n\n table.insert(choiceLines, \"[Blank Line]\")\n end\n\n if #playerLines > 0 then\n local companionRows = splitPipe(safeGet(triggerId, \"vg_Companion_Action\", \"\"))\n\n for _, row in ipairs(companionRows) do\n local parts = splitSection(row)\n local sourceName = trim(parts[1] or \"\")\n local actionName = trim(parts[2] or \"\")\n\n if sourceName ~= \"\" and actionName ~= \"\" then\n table.insert(choiceLines,\n \"[Companion Action: \" .. sourceName ..\n \" | Action: \" .. actionName .. \"]\"\n )\n end\n end\n\n table.insert(choiceLines, \"[Available Action: \" .. actionDots .. \"]\")\n table.insert(choiceLines, \"[Blank Line]\")\n end\n\n for _, line in ipairs(playerLines) do\n table.insert(choiceLines, line)\n end\n\n if isHEvent then\n local lang = getLanguage(triggerId)\n\n local timePassTextMap = {\n [1] = \"날짜 경과\",\n [2] = \"Days Pass\",\n [3] = \"日数経過\",\n }\n\n local timePassText = timePassTextMap[lang] or timePassTextMap[2]\n\n table.insert(choiceLines, \"[Blank Line]\")\n table.insert(choiceLines, \"[H Event Time Pass: \" .. timePassText .. \" | Emoji: 📅]\")\n end\n\n setChatVar(triggerId, \"vg_PlayDeck_Choice\", table.concat(choiceLines, \"\\n\"))\nend\n\nfunction getActionAvailableCount(triggerId)\n local curPhase = getPhase(triggerId)\n local heroineSpecialState = getHeroineSpecialState(triggerId)\n\n if heroineSpecialState == vgHeroineEffect.sleep\n or heroineSpecialState == vgHeroineEffect.immobilized\n or heroineSpecialState == vgHeroineEffect.brainwashed\n or isPerformancePhase(triggerId, curPhase)\n or isExhibitionismPhase(triggerId, curPhase)\n or isMasturbationPhase(triggerId, curPhase)\n or isIncapacitatedPhase(triggerId, curPhase)\n or isHEventPhase(triggerId, curPhase)\n or isPromiscuityPhase(triggerId, curPhase)\n or isIntimacyPhase(triggerId, curPhase) then\n return 2\n end\n\n local heroineStat = loadDualFlagNextMap(triggerId, vgHeroineStat)\n local resource = tonumber(heroineStat.resource) or 0\n local resourceMax = tonumber(heroineStat.resource_Max) or 0\n\n local availableCount = 1\n\n local extraActionThresholdLow = 0.1\n local extraActionThresholdHigh = 0.6\n\n local enduranceBonus = tonumber(getEnhancementTotalByType(triggerId, constEnhancementType.endurance)) or 0\n\n extraActionThresholdLow = extraActionThresholdLow - (enduranceBonus / 1000)\n extraActionThresholdHigh = extraActionThresholdHigh - (enduranceBonus / 100)\n\n extraActionThresholdLow = clamp(extraActionThresholdLow, 0, 1)\n extraActionThresholdHigh = clamp(extraActionThresholdHigh, extraActionThresholdLow, 1)\n\n if resourceMax > 0 then\n local resourceRatio = resource / resourceMax\n\n if resourceRatio >= extraActionThresholdLow then\n availableCount = availableCount + 1\n end\n\n if resourceRatio >= extraActionThresholdHigh then\n availableCount = availableCount + 1\n end\n end\n\n return availableCount\nend\n\nfunction shouldKeepZeroEffectType(effectTypeCode)\n effectTypeCode = tonumber(effectTypeCode) or 0\n\n local keepMap = {\n [constEffectType.inert] = true,\n }\n\n return keepMap[effectTypeCode] == true\nend\n\nfunction getHeroineSpecialState(triggerId)\n local heroineEffect = loadDualFlagNextMap(triggerId, vgHeroineEffect)\n\n if (tonumber(heroineEffect.sleep) or 0) >= 1 then\n return vgHeroineEffect.sleep\n end\n\n if (tonumber(heroineEffect.immobilized) or 0) >= 1 then\n return vgHeroineEffect.immobilized\n end\n\n if (tonumber(heroineEffect.brainwashed) or 0) >= 1 then\n return vgHeroineEffect.brainwashed\n end\n\n return 0\nend\n\nfunction normalizeCardTypeCode(typeCode)\n typeCode = tonumber(typeCode) or 0\n\n local aliasMap = {\n [constCardType.breaker] = constCardType.primary,\n [constCardType.counter] = constCardType.primary,\n }\n\n return aliasMap[typeCode] or typeCode\nend\n\nfunction isPrimaryLikeCardType(typeCode)\n typeCode = normalizeCardTypeCode(typeCode)\n\n return typeCode == constCardType.primary\n or typeCode == constCardType.milking\nend\n\nfunction isSecondaryLikeCardType(typeCode)\n typeCode = tonumber(typeCode) or 0\n\n return typeCode == constCardType.secondary\n or typeCode == constCardType.taunt\n or typeCode == constCardType.temptation\nend\n\n-- ==========================================\n-- 공용 효과\n-- ==========================================\nfunction convertLevelTextToBaseValue(raw, cardLevel, actorStatValue, growthScale)\n local normalized = string.lower(trim(tostring(raw or \"\")))\n normalized = normalized:gsub(\"%b()\", \" \")\n normalized = normalized:gsub(\"%s+\", \" \")\n normalized = trim(normalized)\n\n cardLevel = clamp(tonumber(cardLevel) or 1, 1, 10)\n actorStatValue = math.max(1, tonumber(actorStatValue) or 10)\n growthScale = tonumber(growthScale) or 1\n\n local textBonus = 0\n local offset = math.random(-25, 25) / 1000 -- ±0.025\n\n if normalized:find(\"very high\", 1, true) then\n textBonus = 0.10\n elseif normalized:find(\"very low\", 1, true) then\n textBonus = -0.10\n elseif normalized:find(\"high\", 1, true) then\n textBonus = 0.05\n elseif normalized:find(\"medium\", 1, true) then\n textBonus = 0\n elseif normalized:find(\"low\", 1, true) then\n textBonus = -0.05\n else\n return 0\n end\n\n local baseStatValue = getBaseStatValueByLevel(1)\n local statIncrease = math.max(0, actorStatValue - baseStatValue)\n local cardBonus = math.max(0, cardLevel - 1) * 2\n\n if growthScale ~= 1 then\n statIncrease = statIncrease * growthScale\n cardBonus = cardBonus * growthScale\n end\n\n local scaledStatValue = baseStatValue + statIncrease\n local textMultiplier = 1 + textBonus + offset\n local baseValue = (scaledStatValue + cardBonus) * textMultiplier\n\n baseValue = math.max(0.1, baseValue)\n return baseValue\nend\n\nfunction convertLevelTextToValue(triggerId, actorKind, effectTypeCode, raw, cardLevel, actionName, cardTypeCode, actorStatValue)\n actorKind = string.lower(trim(tostring(actorKind or \"\")))\n effectTypeCode = tonumber(effectTypeCode) or 0\n cardLevel = tonumber(cardLevel) or 1\n actionName = tostring(actionName or \"\")\n\n local originalCardTypeCode = tonumber(cardTypeCode) or 0\n cardTypeCode = normalizeCardTypeCode(cardTypeCode)\n\n local effectValue = convertLevelTextToBaseValue(raw, cardLevel, actorStatValue, 1)\n if effectValue == 0 then\n return 0\n end\n\n -- ==========================================\n -- 기본값 보정\n -- ==========================================\n if effectTypeCode == constEffectType.buff then\n effectValue = effectValue * (math.random(100, 125) / 1000)\n elseif effectTypeCode == constEffectType.refresh then\n effectValue = effectValue + 5\n end\n\n if actorKind == \"heroine\" then\n local heroineBuff = tonumber(getDualFlag(triggerId, vgHeroineEffect.varName, vgHeroineEffect.buff, false)) or 0\n\n if isHeroineBuffTargetCardType(cardTypeCode) then\n effectValue = effectValue + heroineBuff\n end\n\n local enhancementBonus = 0\n local cardEffectTypeNames = getCardTypeEffectTypes(triggerId, cardTypeCode)\n\n local effectNameMap = {}\n for _, name in ipairs(cardEffectTypeNames or {}) do\n effectNameMap[trim(tostring(name or \"\"))] = true\n end\n\n local function addEnhancement(typeCode)\n enhancementBonus = enhancementBonus + getEnhancementTotalByType(triggerId, typeCode)\n end\n\n if effectNameMap[\"Damage\"] or effectNameMap[\"Liberation\"] then\n addEnhancement(constEnhancementType.attack)\n end\n\n if effectNameMap[\"Sway\"] then\n addEnhancement(constEnhancementType.charisma)\n end\n\n if effectNameMap[\"Guard\"] then\n addEnhancement(constEnhancementType.defense)\n end\n\n if effectNameMap[\"Defiance\"] or effectNameMap[\"Refusal\"] or effectNameMap[\"Retort\"] then\n addEnhancement(constEnhancementType.chastity)\n end\n\n if effectNameMap[\"Mitigation\"] or effectNameMap[\"Provocation\"] or effectNameMap[\"Allure\"] or effectNameMap[\"Disruption\"] or effectNameMap[\"Redirection\"] then\n addEnhancement(constEnhancementType.spirit)\n end\n\n if effectTypeCode == constEffectType.appeasement or effectTypeCode == constEffectType.excitement or effectTypeCode == constEffectType.sexual\n or effectNameMap[\"Ravishment\"] or effectNameMap[\"Coordination\"] then\n addEnhancement(constEnhancementType.sensuality)\n end\n\n if effectTypeCode == constEffectType.buff then\n addEnhancement(constEnhancementType.focus)\n end\n\n if effectTypeCode == constEffectType.refresh or effectTypeCode == constEffectType.recovery or effectTypeCode == constEffectType.calm then\n addEnhancement(constEnhancementType.resilience)\n end\n\n effectValue = roundToTenth(effectValue + enhancementBonus)\n\n elseif actorKind == \"encounter\" then\n local encounterBuff = tonumber(getDualFlag(triggerId, vgEncounterStat.varName, vgEncounterStat.buff, false)) or 0\n effectValue = effectValue + encounterBuff\n end\n\n if isMasturbationPhase(triggerId) then\n effectValue = effectValue * 0.8\n elseif cardTypeCode == constCardType.milking then\n effectValue = effectValue * 1.3\n elseif cardTypeCode == constCardType.taunt or cardTypeCode == constCardType.temptation then\n effectValue = effectValue * 1.2\n elseif originalCardTypeCode == constCardType.counter then\n effectValue = effectValue * 0.8\n end\n\n -- ==========================================\n -- 배율 보정\n -- ==========================================\n local buffBonus = 0\n\n local sexualBonusEffectMap = {\n [constEffectType.sexual] = true,\n [constEffectType.humiliation] = true,\n [constEffectType.defilement] = true,\n [constEffectType.invasion] = true,\n }\n\n\n if actorKind == \"encounter\" then\n local encounterAttackBonus = tonumber(getDualFlag(triggerId, vgEncounterStat.varName, vgEncounterStat.attack, false)) or 0\n local encounterSexualBonus = tonumber(getDualFlag(triggerId, vgEncounterStat.varName, vgEncounterStat.sexual, false)) or 0\n\n local attackBonusEffectMap = {\n [constEffectType.damage] = true,\n [constEffectType.subdual] = true,\n [constEffectType.pressure] = true,\n [constEffectType.rhetoric] = true,\n [constEffectType.suggestion] = true,\n [constEffectType.escalation] = true,\n [constEffectType.defilement] = true,\n [constEffectType.invasion] = true,\n [constEffectType.interception] = true,\n }\n\n if attackBonusEffectMap[effectTypeCode] then\n buffBonus = buffBonus + (encounterAttackBonus / 100)\n end\n\n if sexualBonusEffectMap[effectTypeCode] then\n buffBonus = buffBonus + (encounterSexualBonus / 100)\n end\n elseif actorKind == \"heroine\" then\n local encounterDefenseBonus = tonumber(getDualFlag(triggerId, vgEncounterStat.varName, vgEncounterStat.defense, false)) or 0\n\n if isPrimaryLikeCardType(cardTypeCode) then\n buffBonus = buffBonus - (encounterDefenseBonus / 100)\n end\n end\n\n local ambushValue = tonumber(getDualFlag(triggerId, vgHeroineEffect.varName, vgHeroineEffect.ambush, false)) or constAmbush.none\n local heroineAmbushAllowed = isHeroineBuffTargetCardType(cardTypeCode)\n\n if ambushValue == constAmbush.heroine then\n if actorKind == \"heroine\" then\n if heroineAmbushAllowed then\n buffBonus = buffBonus + 0.25\n end\n elseif actorKind == \"encounter\" then\n buffBonus = buffBonus - 0.5\n end\n elseif ambushValue == constAmbush.encounter then\n if actorKind == \"heroine\" then\n if heroineAmbushAllowed then\n buffBonus = buffBonus - 0.5\n end\n elseif actorKind == \"encounter\" then\n buffBonus = buffBonus + 0.25\n end\n end\n\n if sexualBonusEffectMap[effectTypeCode] then\n buffBonus = buffBonus + (getSensitivityBonusByText(triggerId, actionName) or 0)\n\n local disciplineBonus = tonumber(getEnhancementTotalByType(triggerId, constEnhancementType.discipline)) or 0\n buffBonus = buffBonus - (disciplineBonus / 100)\n end\n\n local effectMultiplier = 1.0 + buffBonus\n\n if effectMultiplier < 0 then\n effectMultiplier = 0\n end\n\n effectValue = effectValue * effectMultiplier\n\n -- Encounter 가드 너프\n if actorKind == \"encounter\" and effectTypeCode == constEffectType.guard then\n effectValue = effectValue * 0.8\n elseif actorKind == \"heroine\" then\n local heroineSpecialState = getHeroineSpecialState(triggerId)\n\n if heroineSpecialState == vgHeroineEffect.sleep or heroineSpecialState == vgHeroineEffect.immobilized or heroineSpecialState == vgHeroineEffect.brainwashed then\n if effectTypeCode == constEffectType.recovery then\n effectValue = effectValue * 0.5\n elseif heroineSpecialState == vgHeroineEffect.brainwashed and cardTypeCode ~= constCardType.ecstasy then\n effectValue = effectValue * getLewdnessSexualBonusRatio(triggerId)\n end\n end\n end\n\n effectValue = math.max(0.1, roundToTenth(effectValue))\n\n if effectTypeCode == constEffectType.calm then\n return -effectValue\n end\n\n return effectValue\nend\n\nfunction convertLevelTextToCostValue(raw, cardLevel, cardTypeCode, actorStatValue)\n cardLevel = tonumber(cardLevel) or 1\n cardTypeCode = tonumber(cardTypeCode) or 0\n actorStatValue = math.max(1, tonumber(actorStatValue) or 10)\n\n local baseValue = convertLevelTextToBaseValue(raw, cardLevel, actorStatValue, 0.75)\n if baseValue <= 0 then\n return 0\n end\n\n local costMultiplier = 0.5\n\n if cardTypeCode == constCardType.counter or cardTypeCode == constCardType.breaker then\n costMultiplier = 0.6\n elseif cardTypeCode == constCardType.taunt or cardTypeCode == constCardType.temptation then\n costMultiplier = 0.4\n elseif cardTypeCode == constCardType.heal or cardTypeCode == constCardType.soothe then\n costMultiplier = 0.25\n elseif cardTypeCode == constCardType.focus then\n costMultiplier = 0.2\n elseif cardTypeCode == constCardType.charge or cardTypeCode == constCardType.ecstasy then\n costMultiplier = 0\n end\n\n local costValue = roundToTenth(baseValue * costMultiplier)\n\n if costMultiplier == 0 then\n return 0\n elseif costValue < 0.1 then\n return 0.1\n end\n\n return costValue\nend\n\nfunction rollCriticalChance(triggerId, actorKind)\n local critChance = 2\n\n if actorKind == \"heroine\" then\n critChance = critChance + (tonumber(getEnhancementTotalByType(triggerId, constEnhancementType.luck)) or 0)\n elseif actorKind == \"encounter\" then\n critChance = critChance + (tonumber(getDualFlag(triggerId, vgEncounterStat.varName, vgEncounterStat.critical, false)) or 0)\n end\n\n critChance = clamp(critChance, 0, 100)\n\n return math.random(10000) <= roundHalfUp(critChance * 100)\nend\n\n-- ==========================================\n-- 공용 스탯\n-- ==========================================\nfunction getHeroineLevel(triggerId, useNext)\n local heroineStat\n\n if useNext == true then\n heroineStat = loadDualFlagNextMap(triggerId, vgHeroineStat)\n else\n heroineStat = loadDualFlagCurrentMap(triggerId, vgHeroineStat)\n end\n\n local strength = tonumber(heroineStat.strength) or 0\n local reflex = tonumber(heroineStat.reflex) or 0\n local willpower = tonumber(heroineStat.willpower) or 0\n\n local averageStat = (strength + reflex + willpower) / 3\n\n return math.max(1, math.floor(averageStat - 9 + 0.00001))\nend\n\nfunction getBaseStatValueByLevel(level)\n level = tonumber(level) or 1\n return roundToTenth(level + 9)\nend\n\nfunction getEncounterStatValue(triggerId, overrideEncounterLevel)\n local sessionSlotList = getDualFlagSlotList(triggerId, vgSessionInfo.varName)\n\n local encounterLevel = tonumber(overrideEncounterLevel)\n if not encounterLevel then\n encounterLevel = tonumber(getResolvedDualFlagFromSlotList(sessionSlotList, vgSessionInfo.level, 1)) or 1\n end\n\n encounterLevel = math.max(1, encounterLevel)\n\n local difficultyCode = tonumber(\n getResolvedDualFlagFromSlotList(\n sessionSlotList,\n vgSessionInfo.difficulty,\n constSessionDifficulty.medium\n )\n ) or constSessionDifficulty.medium\n\n local encounterStatValue = getBaseStatValueByLevel(encounterLevel)\n local difficultyMultiplier = 1.0\n\n if difficultyCode == constSessionDifficulty.low then\n difficultyMultiplier = 0.95\n elseif difficultyCode == constSessionDifficulty.high then\n difficultyMultiplier = 1.05\n elseif difficultyCode == constSessionDifficulty.elite then\n difficultyMultiplier = 1.1\n elseif difficultyCode == constSessionDifficulty.boss then\n difficultyMultiplier = 1.15\n end\n\n return roundToTenth(encounterStatValue * difficultyMultiplier)\nend\n\n-- ==========================================\n-- 공용 세션\n-- ==========================================\nfunction getDifficultyEmojiByCode(difficultyCode)\n difficultyCode = tonumber(difficultyCode) or 0\n\n if difficultyCode == constSessionDifficulty.low then\n return \"👾\"\n elseif difficultyCode == constSessionDifficulty.medium then\n return \"👿\"\n elseif difficultyCode == constSessionDifficulty.high then\n return \"👺\"\n elseif difficultyCode == constSessionDifficulty.elite then\n return \"👹\"\n elseif difficultyCode == constSessionDifficulty.boss then\n return \"💀\"\n end\n\n return \"❓\"\nend\n\nfunction getEncounterEmojiByType(encounterTypeCode)\n encounterTypeCode = tonumber(encounterTypeCode) or 0\n\n if encounterTypeCode == constPhase.combat then\n return \"⚔️\"\n elseif encounterTypeCode == constPhase.harassment then\n return \"👋\"\n elseif encounterTypeCode == constPhase.abuse then\n return \"⚖️\"\n elseif encounterTypeCode == constPhase.confrontation then\n return \"🗣️\"\n elseif encounterTypeCode == constPhase.escape then\n return \"🏃♀️\"\n elseif encounterTypeCode == constPhase.rescue then\n return \"🚨\"\n elseif encounterTypeCode == constPhase.event_Violence then\n return \"🖤\"\n elseif encounterTypeCode == constPhase.event_Sleeping or encounterTypeCode == constPhase.sleeping_Ambush then\n return \"💤\"\n elseif encounterTypeCode == constPhase.event_Coercion then\n return \"🛏️\"\n elseif encounterTypeCode == constPhase.abortion_Ambush then\n return \"🏥\"\n elseif encounterTypeCode == constPhase.entrapment or encounterTypeCode == constPhase.event_Immobilized then\n return \"🍵\"\n elseif encounterTypeCode == constPhase.brainwashing then\n return \"🧠\"\n elseif encounterTypeCode == constPhase.indoctrination then\n return \"📚\"\n elseif encounterTypeCode == constPhase.transaction then\n return \"💰\"\n elseif encounterTypeCode == constPhase.performance then\n return \"💃\"\n elseif encounterTypeCode == constPhase.exhibitionism then\n return \"👠\"\n elseif encounterTypeCode == constPhase.promiscuity then\n return \"🏩\"\n elseif encounterTypeCode == constPhase.hypnosis or encounterTypeCode == constPhase.event_Brainwashing then\n return \"👁️\"\n elseif encounterTypeCode == constPhase.masturbation_Story then\n return \"🔞\"\n elseif encounterTypeCode == constPhase.feral then\n return \"🐻\"\n elseif encounterTypeCode == constPhase.intimacy then\n return \"🛋️\"\n elseif encounterTypeCode == constPhase.companion then\n return \"⚒️\"\n elseif encounterTypeCode == constPhase.aphrodisiac or encounterTypeCode == constPhase.event_Aphrodisiac then\n return \"💉\"\n elseif encounterTypeCode == constPhase.abduction then\n return \"🚐\"\n elseif encounterTypeCode == constPhase.rape then\n return \"💔\"\n end\n\n return \"❓\"\nend\n\nfunction getStoryChoiceBaseLevel(triggerId)\n local act = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.act, 1)) or 1\n\n return getStoryChoiceBaseLevelByAct(act)\nend\n\nfunction getStoryChoiceBaseLevelByAct(act)\n act = tonumber(act) or 1\n\n return act * 5\nend\n\nfunction getMainStoryLevel(triggerId, stageValue)\n local baseLevel = getStoryChoiceBaseLevel(triggerId)\n stageValue = tonumber(stageValue) or 1\n\n if stageValue >= 3 then\n return baseLevel\n elseif stageValue == 2 then\n return math.max(1, baseLevel - 1)\n else\n return math.max(1, baseLevel - 2)\n end\nend\n\nfunction initEncounterInfo(triggerId, encounterValue, levelValue, difficultyValue, encounterTypeValue, traitValue)\n batchSetDualFlags(triggerId, vgSessionInfo.varName, {\n { position = vgSessionInfo.name, value = tostring(encounterValue or \"\"), isNext = false },\n { position = vgSessionInfo.level, value = tostring(levelValue or 1), isNext = false },\n { position = vgSessionInfo.difficulty, value = tostring(difficultyValue or 0), isNext = false },\n { position = vgSessionInfo.trait, value = tostring(traitValue or constEncounterTrait.none), isNext = false },\n })\n\n setChatVar(triggerId, vgHeroineEffect.varName, \"\")\n\n local levelNum = tonumber(levelValue) or 1\n local difficultyNum = tonumber(difficultyValue) or 1\n\n local encounterAttack = 0\n local encounterDefense = 0\n local encounterSexual = 0\n local encounterCritical = 0\n\n local hp = getBaseStatValueByLevel(levelNum) * 4\n local challengeHPMultiplier = tonumber(safeGet(triggerId, \"vg_Challenge_HP\", \"1\")) or 1\n hp = hp * challengeHPMultiplier\n\n local hpBonusRate = 0\n\n local aggressiveLustfulMin = 0\n local aggressiveLustfulMax = 0\n local cunningMin = 0\n local cunningMax = 0\n local tenaciousMin = 0\n local tenaciousMax = 0\n\n local bonusLevel = clamp(levelNum - 1, 0, 50)\n\n if difficultyNum == constSessionDifficulty.elite then\n aggressiveLustfulMin = 2.5 + (bonusLevel * 0.0125)\n aggressiveLustfulMax = 5.0 + (bonusLevel * 0.025)\n\n cunningMin = 4.0 + (bonusLevel * 0.025)\n cunningMax = 8.0 + (bonusLevel * 0.05)\n\n tenaciousMin = 0.1 + (bonusLevel * 0.0005)\n tenaciousMax = 0.2 + (bonusLevel * 0.001)\n\n elseif difficultyNum == constSessionDifficulty.boss then\n aggressiveLustfulMin = 5.0 + (bonusLevel * 0.025)\n aggressiveLustfulMax = 10.0 + (bonusLevel * 0.05)\n\n cunningMin = 6.0 + (bonusLevel * 0.05)\n cunningMax = 13.0 + (bonusLevel * 0.1)\n\n tenaciousMin = 0.2 + (bonusLevel * 0.0005)\n tenaciousMax = 0.3 + (bonusLevel * 0.001)\n end\n\n if traitValue == constEncounterTrait.aggressive then\n encounterAttack = roundToTenth(\n math.random(\n roundHalfUp(aggressiveLustfulMin * 10),\n roundHalfUp(aggressiveLustfulMax * 10)\n ) / 10\n )\n\n elseif traitValue == constEncounterTrait.lustful then\n encounterSexual = roundToTenth(\n math.random(\n roundHalfUp(aggressiveLustfulMin * 10),\n roundHalfUp(aggressiveLustfulMax * 10)\n ) / 10\n )\n\n elseif traitValue == constEncounterTrait.cunning then\n encounterCritical = roundToTenth(\n math.random(\n roundHalfUp(cunningMin * 10),\n roundHalfUp(cunningMax * 10)\n ) / 10\n )\n\n elseif traitValue == constEncounterTrait.tenacious then\n hpBonusRate = hpBonusRate + (\n math.random(\n roundHalfUp(tenaciousMin * 100),\n roundHalfUp(tenaciousMax * 100)\n ) / 100\n )\n end\n\n if isHEventPhase(triggerId, encounterTypeValue) then\n encounterCritical = roundToTenth(encounterCritical + 23)\n end\n\n if isIncapacitatedPhase(triggerId, encounterTypeValue)\n or isPerformancePhase(triggerId, encounterTypeValue)\n or isPromiscuityPhase(triggerId, encounterTypeValue)\n or isIntimacyPhase(triggerId, encounterTypeValue) then\n hpBonusRate = hpBonusRate + 1.5\n\n local hpBonusRateMap = {\n [constSessionDifficulty.low] = 0,\n [constSessionDifficulty.medium] = 0.05,\n [constSessionDifficulty.high] = 0.1,\n [constSessionDifficulty.elite] = 0.2,\n [constSessionDifficulty.boss] = 0.3,\n }\n hpBonusRate = hpBonusRate + (hpBonusRateMap[difficultyNum] or 0)\n\n local lewdnessLevel = getLewdnessLevel(triggerId)\n hpBonusRate = hpBonusRate + (lewdnessLevel * 0.05)\n\n if encounterTypeValue == constPhase.transaction\n or encounterTypeValue == constPhase.rape\n or isPerformancePhase(triggerId, encounterTypeValue)\n or isPromiscuityPhase(triggerId, encounterTypeValue)\n or isIntimacyPhase(triggerId, encounterTypeValue) then\n hpBonusRate = hpBonusRate + 0.3\n end\n\n elseif isExhibitionismPhase(triggerId, encounterTypeValue) then\n local hpBonusRateMap = {\n [constSessionDifficulty.low] = 0,\n [constSessionDifficulty.medium] = 0.05,\n [constSessionDifficulty.high] = 0.1,\n [constSessionDifficulty.elite] = 0.2,\n [constSessionDifficulty.boss] = 0.3,\n }\n hpBonusRate = hpBonusRate + (hpBonusRateMap[difficultyNum] or 0)\n else\n local hpBonusRateMap = {\n [constSessionDifficulty.low] = 0,\n [constSessionDifficulty.medium] = 0.25,\n [constSessionDifficulty.high] = 0.5,\n [constSessionDifficulty.elite] = 0.8,\n [constSessionDifficulty.boss] = 1.2,\n }\n hpBonusRate = hpBonusRate + (hpBonusRateMap[difficultyNum] or 0)\n\n hp = hp * (1 + (levelNum * 0.0075))\n end\n\n hp = hp * (1 + hpBonusRate)\n\n hp = hp * (1 + (math.random(-500, 500) / 10000))\n hp = math.max(1, roundHalfUp(hp))\n\n batchSetDualFlags(triggerId, vgEncounterStat.varName, {\n { position = vgEncounterStat.hp, value = hp, isNext = false },\n { position = vgEncounterStat.hp_Max, value = hp, isNext = false },\n { position = vgEncounterStat.buff, value = 0, isNext = false },\n\n { position = vgEncounterStat.attack, value = tostring(encounterAttack), isNext = false },\n { position = vgEncounterStat.defense, value = tostring(encounterDefense), isNext = false },\n { position = vgEncounterStat.sexual, value = tostring(encounterSexual), isNext = false },\n { position = vgEncounterStat.critical, value = tostring(encounterCritical), isNext = false },\n })\n\n setChatVar(triggerId, \"vg_Session_Save\", \"1\")\n setChatVar(triggerId, \"vg_GuardPriority\", \"1\")\nend\n\nfunction initSessionInfo(triggerId, storyTypeValue, encounterValue, levelValue, difficultyValue, encounterTypeValue, traitValue)\n batchSetDualFlags(triggerId, vgSessionInfo.varName, {\n { position = vgSessionInfo.stage, value = 1, isNext = false },\n { position = vgSessionInfo.storyType, value = tostring(storyTypeValue or 0), isNext = false },\n })\n\n initEncounterInfo(triggerId, encounterValue, levelValue, difficultyValue, encounterTypeValue, traitValue)\nend\n\nfunction resetSession(triggerId)\n setChatVar(triggerId, \"vg_Session_Info\", \"\")\n\n resetEncounter(triggerId)\nend\n\nfunction resetEncounter(triggerId)\n setChatVar(triggerId, \"vg_Heroine_Effect\", \"\")\n\n setChatVar(triggerId, \"vg_Encounter_Stat\", \"\")\n setChatVar(triggerId, \"vg_Encounter_Setting\", \"\")\n setChatVar(triggerId, \"vg_Encounter_Guide\", \"\")\n setChatVar(triggerId, \"vg_Encounter_Climax\", \"0\")\n setChatVar(triggerId, \"vg_Encounter_Action\", \"\")\n setChatVar(triggerId, \"vg_Companion_Action\", \"\")\n\n setChatVar(triggerId, \"vg_PlayDeck_Hand\", \"\")\n setChatVar(triggerId, \"vg_PlayDeck_Remain\", \"\")\n setChatVar(triggerId, \"vg_PlayDeck_Discard\", \"\")\n setChatVar(triggerId, \"vg_PlayDeck_Action\", \"\")\n setChatVar(triggerId, \"vg_PlayDeck_Choice\", \"\")\n setChatVar(triggerId, \"vg_PlayDeck_Setting\", \"\")\n setChatVar(triggerId, \"vg_PlayDeck_Guide\", \"\")\n\n setChatVar(triggerId, \"vg_Deck_Temp\", \"\")\n\n setChatVar(triggerId, \"vg_Session_Save\", \"0\")\n setChatVar(triggerId, \"vg_GuardPriority\", \"1\")\n\n setChatVar(triggerId, \"vg_Action_Available\", \"0\")\nend\n\nfunction parseEncounterTypeCode(rawText)\n local loweredEncounterType = string.lower(trim(tostring(rawText or \"\")))\n\n if loweredEncounterType == \"combat\" then\n return constPhase.combat\n elseif loweredEncounterType == \"feral\" then\n return constPhase.feral\n elseif loweredEncounterType == \"harassment\" then\n return constPhase.harassment\n elseif loweredEncounterType == \"abuse\" then\n return constPhase.abuse\n elseif loweredEncounterType == \"confrontation\" then\n return constPhase.confrontation\n elseif loweredEncounterType == \"sleeping\" then\n return constPhase.sleeping_Ambush\n elseif loweredEncounterType == \"abortion\" then\n return constPhase.abortion_Ambush\n elseif loweredEncounterType == \"entrapment\" then\n return constPhase.entrapment\n elseif loweredEncounterType == \"brainwashing\" then\n return constPhase.brainwashing\n elseif loweredEncounterType == \"indoctrination\" then\n return constPhase.indoctrination\n elseif loweredEncounterType == \"transaction\" then\n return constPhase.transaction\n elseif loweredEncounterType == \"performance\" then\n return constPhase.performance\n elseif loweredEncounterType == \"exhibitionism\" then\n return constPhase.exhibitionism\n elseif loweredEncounterType == \"promiscuity\" then\n return constPhase.promiscuity\n elseif loweredEncounterType == \"hypnosis\" then\n return constPhase.hypnosis\n elseif loweredEncounterType == \"masturbation\" then\n return constPhase.masturbation_Story\n elseif loweredEncounterType == \"intimacy\" then\n return constPhase.intimacy\n elseif loweredEncounterType == \"companion\" then\n return constPhase.companion\n elseif loweredEncounterType == \"aphrodisiac\" then\n return constPhase.aphrodisiac\n elseif loweredEncounterType == \"abduction\" then\n return constPhase.abduction\n elseif loweredEncounterType == \"rape\" then\n return constPhase.rape\n end\n\n return 0\nend\n\nfunction getEncounterDisplayInfo(triggerId, phase, currentHP, nextHP, maxHP)\n phase = tonumber(phase) or getPhase(triggerId)\n currentHP = tonumber(currentHP) or 0\n nextHP = tonumber(nextHP) or 0\n maxHP = tonumber(maxHP) or 0\n\n local emoji = \"☠️\"\n local displayCurrentHP = currentHP\n local displayNextHP = nextHP\n\n local reverseDisplay = false\n\n if isHarassmentPhase(triggerId, phase) then\n emoji = \"🚪\"\n reverseDisplay = true\n\n elseif isAbusePhase(triggerId, phase) then\n emoji = \"⚖️\"\n reverseDisplay = true\n\n elseif isConfrontationPhase(triggerId, phase) then\n emoji = \"🗣️\"\n reverseDisplay = true\n\n elseif isHypnosisPhase(triggerId, phase) then\n emoji = \"🔐\"\n reverseDisplay = true\n\n elseif isIncapacitatedPhase(triggerId, phase)\n or isPerformancePhase(triggerId, phase)\n or isPromiscuityPhase(triggerId, phase)\n or isIntimacyPhase(triggerId, phase) then\n emoji = \"💕\"\n reverseDisplay = true\n\n elseif isExhibitionismPhase(triggerId, phase) then\n emoji = \"💓\"\n reverseDisplay = true\n\n else\n emoji = \"☠️\"\n end\n\n if reverseDisplay then\n displayCurrentHP = clamp(maxHP - currentHP, 0, maxHP)\n displayNextHP = clamp(maxHP - nextHP, 0, maxHP)\n end\n\n return emoji, displayCurrentHP, displayNextHP, maxHP\nend\n\nfunction getMasturbationChoiceThreshold(triggerId)\n local baseThreshold = 0.85\n\n local lewdnessLevel = getLewdnessLevel(triggerId)\n\n local reduction = 0\n if lewdnessLevel >= 3 then\n reduction = (lewdnessLevel - 2) * 0.01\n end\n\n return clamp(baseThreshold - reduction, 0, 1)\nend\n\nfunction buildMasturbationChoice(triggerId)\n local arousalThresholdRatio = getMasturbationChoiceThreshold(triggerId)\n\n local heroineStat = loadDualFlagNextMap(triggerId, vgHeroineStat)\n\n local nextArousal = tonumber(heroineStat.arousal) or 0\n local nextMaxArousal = tonumber(heroineStat.arousal_Max) or 0\n\n if nextMaxArousal <= 0 then\n return \"\"\n end\n\n if nextArousal < (nextMaxArousal * arousalThresholdRatio) then\n return \"\"\n end\n\n local lang = getLanguage(triggerId)\n\n local choiceTextMap = {\n [1] = \"흥분을 가라앉히기 위해 몸을 달랜다.\",\n [2] = \"She soothes her body to calm her arousal.\",\n [3] = \"昂りを鎮めるため、自分の体を慰める。\",\n }\n\n local choiceText = trim(choiceTextMap[lang] or choiceTextMap[2] or \"\")\n local curPhase = getPhase(triggerId)\n\n local masturbationType = constPhase.masturbation_Daily\n if curPhase == constPhase.interlude then\n masturbationType = constPhase.masturbation_Interlude\n elseif curPhase == constPhase.unfree then\n masturbationType = constPhase.masturbation_Unfree\n end\n\n return \"[Masturbation Choice: \" .. choiceText .. \" | Emoji: 🔞 | Masturbation Type: \" .. tostring(masturbationType) .. \"]\"\nend\n\nfunction buildDeliveryChoice(triggerId)\n local curPhase = getPhase(triggerId)\n local lang = getLanguage(triggerId)\n\n local deliveryType = 0\n local choiceTextMap = {\n [1] = \"출산한다.\",\n [2] = \"She gives birth.\",\n [3] = \"出産する。\",\n }\n\n if curPhase == constPhase.delivery_Daily then\n deliveryType = constPhase.delivery_DailyAfter\n elseif curPhase == constPhase.delivery_Unfree then\n deliveryType = constPhase.delivery_UnfreeAfter\n else\n return \"\"\n end\n\n local choiceText = trim(choiceTextMap[lang] or choiceTextMap[2])\n\n return \"[Delivery Choice: \" .. choiceText .. \" | Emoji: 👶 | Delivery Type: \" .. tostring(deliveryType) .. \"]\"\nend\n\n-- ==========================================\n-- 공용 이벤트 타입\n-- ==========================================\nfunction getChoiceTypeDefs(triggerId)\n local noneText = getLocalizedNoneText(triggerId)\n\n return {\n { key = \"Combat\", text = \" - Combat: Direct physical conflict that can escalate into a fight. (e.g., ambush, violent attack, battle)\", defaultEnabled = true },\n { key = \"Harassment\", text = \" - Harassment: Open sexual misconduct in a situation that does not escalate into combat. (e.g., groping, unwanted touching in a crowd, customer touching her at work)\", defaultEnabled = true },\n { key = \"Abuse\", text = \" - Abuse: Sexual exploitation through authority, trust, employment, or professional pretext. (e.g., coaching, confession, treatment, workplace supervision)\", defaultEnabled = true },\n { key = \"Confrontation\", text = \" - Confrontation: Verbal conflict, hostile dispute, or tense interpersonal friction where sexual humiliation, coercive advances, or unwanted sexual contact is bound up with the conflict. (e.g., quarrel over debt with sexual coercion, blackmail dispute, argument involving sexual humiliation)\", defaultEnabled = true },\n\n { key = \"Sleeping\", text = \" - Sleeping: Sexual assault while the heroine is deeply asleep.\", defaultEnabled = true, exclusive = true },\n { key = \"Entrapment\", text = \" - Entrapment: Paralysis, drugging, numbing agents, or similar non-restraint means that leave the heroine conscious but unable to move and vulnerable to sexual exploitation.\", defaultEnabled = true, exclusive = true },\n { key = \"Aphrodisiac\", text = \" - Aphrodisiac: The heroine is affected by some form of aphrodisiac influence and driven into forced arousal. The situation does not develop into direct combat.\", defaultEnabled = true, exclusive = true },\n { key = \"Hypnosis\", text = \" - Hypnosis: Sexual exploitation centered on hypnosis, brainwashing, mental manipulation, mind control, or similar means.\", defaultEnabled = true, exclusive = true },\n { key = \"Brainwashing\", text = \" - Brainwashing: A situation where the heroine is subjected to sexual acts through hypnosis, brainwashing, mental manipulation, mind control, or similar means.\", defaultEnabled = true, exclusive = true },\n { key = \"Feral\", text = \" - Feral: Direct physical confrontation, combat, or ambush by animal-anatomy-based beings such as beasts, insects, reptiles, amphibians, or beast-like monsters. Monsters without animal-based anatomy, such as slimes, tentacle monsters, sapient beings, or humanoids, must not be included.\", defaultEnabled = true, exclusive = true },\n { key = \"Companion\", text = \" - Companion: Combat where the heroine fights alongside one or more established named allied companions present in or joining the Situation for a natural reason. Each companion's name must be specified in the Situation, not the Encounter. Unnamed extras or generic strangers must not be included.\", defaultEnabled = true, exclusive = true },\n { key = \"Abduction\", text = \" - Abduction: The heroine is taken away against her will or without her awareness, whether awake, asleep, unconscious, or incapacitated.\", defaultEnabled = true, exclusive = true },\n\n { key = \"Abortion\", text = \" - Abortion: Sexual exploitation while attempting to end an unwanted pregnancy. (e.g., a doctor at a hospital, a quack healer at a backroom clinic)\", defaultEnabled = false, exclusive = true },\n { key = \"Indoctrination\", text = \" - Indoctrination: Sexual exploitation centered on sexual teaching or on the heroine coming to learn, realize, or be exposed to sexual knowledge, whether willingly or not (e.g., private lessons, sham counseling, coercive demonstration).\", defaultEnabled = false, exclusive = true },\n\n { key = \"Exhibitionism\", text = \" - Exhibitionism: Exhibitionistic exposure or self-display under the risk of possible discovery, whether deliberate or arising by circumstance (e.g., going out without underwear, stepping outside in only a coat over bare skin, or realizing too late that she left home without underwear). The encounter must be the situation itself, not one that can directly face, notice, or interact with the heroine, and must not imply any meeting, confrontation, or interpersonal contact.\", defaultEnabled = false, exclusive = true },\n { key = \"Rape\", text = \" - Rape: The heroine has been neutralized, overpowered, restrained, or otherwise rendered unable to meaningfully resist, and is subjected to sexual assault.\", defaultEnabled = false, exclusive = true },\n { key = \"Masturbation\", text = \" - Masturbation: Situations where the heroine engages in masturbation for various reasons, including impulse, curiosity, indulgence, or a desire to push her own boundaries. Such situations must involve a desire for stronger or more intense stimulation than usual, such as through unfamiliar tools, different methods, or deliberately heightened stimulation. Unless it is exhibitionistic or openly visible to others, the encounter should be \\\"\" .. noneText .. \"\\\".\", defaultEnabled = false, exclusive = true },\n { key = \"Performance\", text = \" - Performance: Sexual performance by the heroine for observers who can only watch without physical contact, whether voluntary or coerced, in a form appropriate to the world and era. Any situation where physical contact could plausibly occur must be excluded. (e.g., lewd stream, obscene show, erotic performance)\", defaultEnabled = false, exclusive = true },\n { key = \"Intimacy\", text = \" - Intimacy: Consensual sex with male, female, or multiple partner(s). The heroine may be willing, eager, inviting, or hesitant but consenting, without coercion or exploitation. As an exception to the Encounter rules, the Encounter must be familiar, intimate, affectionate, or close established named character(s). Unnamed extras or generic strangers must not be included.\", defaultEnabled = false, exclusive = true },\n { key = \"Transaction\", text = \" - Transaction: Sexual service or gratification exchanged, or framed as being exchanged, for money, protection, access, information, favors, or other benefits. It may be pursued willingly, strategically, or under circumstances where the language of exchange is used to justify, mask, or pressure sexual involvement.\", defaultEnabled = false, exclusive = true },\n { key = \"Promiscuity\", text = \" - Promiscuity: Purely voluntary sexual involvement initiated, proposed, or willingly accepted by the heroine, pursued solely for the heroine's own sexual gratification, including the satisfaction of her sexual desires, needs, curiosity, pleasure, specific kinks, or sexual preferences. It must not involve coercion, obligation, survival pressure, emotional dependency, transactional motives such as money or reward, or similar external reasons. It may be anonymous, discreet, or take place in private or adult venues, but it must lead to a situation in which the heroine and the encounter engage in sex.\", defaultEnabled = false, exclusive = true },\n }\nend\n\n-- ==========================================\n-- 공용 날짜\n-- ==========================================\nfunction getLatestCharDateTime(triggerId)\n local history = getFullChat(triggerId)\n if not history or #history == 0 then\n return \"\", \"\"\n end\n\n for i = #history, 1, -1 do\n local msg = history[i]\n\n if msg and msg.role == \"char\" and type(msg.data) == \"string\" then\n local parsedDate, parsedTime = applyDateTimeFromStatusWindow(msg.data)\n\n if parsedDate ~= \"\" or parsedTime ~= \"\" then\n return parsedDate, parsedTime\n end\n end\n end\n\n return \"\", \"\"\nend\n\nfunction normalizeLooseDateText(rawText)\n local text = trim(tostring(rawText or \"\"))\n if text == \"\" then\n return \"\"\n end\n\n -- 문자열 어디에 있든 첫 번째 yyyy-m-d 패턴을 찾음\n local y, m, d = text:match(\"(%d+)%-(%d%d?)%-(%d%d?)\")\n if not y or not m or not d then\n return text\n end\n\n y = tonumber(y)\n m = tonumber(m)\n d = tonumber(d)\n\n if not y or not m or not d then\n return text\n end\n\n if m < 1 or m > 12 then\n return text\n end\n\n local maxDay = 31\n\n if m == 2 then\n local isLeap =\n (y % 4 == 0 and y % 100 ~= 0)\n or (y % 400 == 0)\n\n maxDay = isLeap and 29 or 28\n\n elseif m == 4 or m == 6 or m == 9 or m == 11 then\n maxDay = 30\n end\n\n if d < 1 or d > maxDay then\n return text\n end\n\n return string.format(\"%d-%02d-%02d\", y, m, d)\nend\n\nfunction parseISODateText(dateText)\n dateText = trim(tostring(dateText or \"\"))\n\n local y, m, d = dateText:match(\"^(%d+)%-(%d%d?)%-(%d%d?)$\")\n y = tonumber(y)\n m = tonumber(m)\n d = tonumber(d)\n\n if not y or not m or not d then\n return nil\n end\n\n if y < 1 then\n return nil\n end\n\n if m < 1 or m > 12 then\n return nil\n end\n\n local maxDay = 31\n\n if m == 2 then\n local isLeap =\n (y % 4 == 0 and y % 100 ~= 0)\n or (y % 400 == 0)\n\n maxDay = isLeap and 29 or 28\n\n elseif m == 4 or m == 6 or m == 9 or m == 11 then\n maxDay = 30\n end\n\n if d < 1 or d > maxDay then\n return nil\n end\n\n return {\n year = y,\n month = m,\n day = d,\n }\nend\n\nfunction formatISODateTable(dateTable)\n if not dateTable then\n return \"\"\n end\n\n local y = tonumber(dateTable.year)\n local m = tonumber(dateTable.month)\n local d = tonumber(dateTable.day)\n\n if not y or not m or not d then\n return \"\"\n end\n\n return string.format(\"%d-%02d-%02d\", y, m, d)\nend\n\nfunction dateTableToSerial(dateTable)\n if not dateTable then\n return nil\n end\n\n local y = tonumber(dateTable.year)\n local m = tonumber(dateTable.month)\n local d = tonumber(dateTable.day)\n\n if not y or not m or not d then\n return nil\n end\n\n local a = math.floor((14 - m) / 12)\n y = y + 4800 - a\n m = m + 12 * a - 3\n\n return d\n + math.floor((153 * m + 2) / 5)\n + 365 * y\n + math.floor(y / 4)\n - math.floor(y / 100)\n + math.floor(y / 400)\n - 32045\nend\n\nfunction serialToDateTable(jdn)\n jdn = tonumber(jdn)\n if not jdn then\n return nil\n end\n\n local a = jdn + 32044\n local b = math.floor((4 * a + 3) / 146097)\n local c = a - math.floor(146097 * b / 4)\n local d = math.floor((4 * c + 3) / 1461)\n local e = c - math.floor(1461 * d / 4)\n local m = math.floor((5 * e + 2) / 153)\n\n local day = e - math.floor((153 * m + 2) / 5) + 1\n local month = m + 3 - 12 * math.floor(m / 10)\n local year = 100 * b + d - 4800 + math.floor(m / 10)\n\n return {\n year = year,\n month = month,\n day = day,\n }\nend\n\nfunction diffDaysBetween(fromDateTable, toDateTable)\n local fromSerial = dateTableToSerial(fromDateTable)\n local toSerial = dateTableToSerial(toDateTable)\n\n if not fromSerial or not toSerial then\n return nil\n end\n\n return toSerial - fromSerial\nend\n\nfunction addDaysToDate(dateTable, days)\n local baseSerial = dateTableToSerial(dateTable)\n days = tonumber(days) or 0\n\n if not baseSerial then\n return nil\n end\n\n return serialToDateTable(baseSerial + days)\nend\n\n-- ==========================================\n-- 공용 임신\n-- ==========================================\nfunction getCycleFlag(triggerId, currentDateText)\n local currentDateTable = parseISODateText(currentDateText)\n if not currentDateTable then\n return nil\n end\n\n local pregnancyDateRaw = trim(getResolvedDualFlag(\n triggerId,\n vgReproductive.varName,\n vgReproductive.pregnancyDate,\n \"\"\n ))\n\n if parseISODateText(pregnancyDateRaw) then\n return constCycle.pregnant\n end\n\n local lastPeriodRaw = trim(getResolvedDualFlag(\n triggerId,\n vgReproductive.varName,\n vgReproductive.lastPeriod,\n \"\"\n ))\n\n local lastPeriodDateTable = parseISODateText(lastPeriodRaw)\n if not lastPeriodDateTable then\n return nil\n end\n\n local elapsed = diffDaysBetween(lastPeriodDateTable, currentDateTable)\n if elapsed == nil or elapsed < 0 then\n return nil\n end\n\n local cycleDay = (elapsed % 28) + 1\n\n if cycleDay >= 1 and cycleDay <= 2 then\n return constCycle.menstruation\n elseif cycleDay == 9 then\n return constCycle.fertile_m5\n elseif cycleDay == 10 then\n return constCycle.fertile_m4\n elseif cycleDay == 11 then\n return constCycle.fertile_m3\n elseif cycleDay == 12 then\n return constCycle.fertile_m2\n elseif cycleDay == 13 then\n return constCycle.fertile_m1\n elseif cycleDay == 14 then\n return constCycle.ovulation\n elseif cycleDay == 15 then\n return constCycle.fertile_p1\n elseif cycleDay >= 16 and cycleDay <= 28 then\n return constCycle.post_fertile\n end\n\n return constCycle.safe\nend\n\nfunction syncLastPeriodToCurrentDate(triggerId, currentDateText, changedVars)\n local prevRaw = safeGet(triggerId, vgReproductive.varName, \"\")\n\n local currentDateTable = parseISODateText(currentDateText)\n if not currentDateTable then\n return nil\n end\n\n local pregnancyDateRaw = trim(getResolvedDualFlag(\n triggerId,\n vgReproductive.varName,\n vgReproductive.pregnancyDate,\n \"\"\n ))\n\n -- 임신 중이면 lastPeriod는 비워두고 pregnant 반환\n if parseISODateText(pregnancyDateRaw) then\n setDualFlag(triggerId, vgReproductive.varName, vgReproductive.lastPeriod, \"\", false)\n\n local nextRaw = safeGet(triggerId, vgReproductive.varName, \"\")\n if changedVars and prevRaw ~= nextRaw then\n changedVars[vgReproductive.varName] = nextRaw\n end\n\n return constCycle.pregnant\n end\n\n local lastPeriodRaw = trim(getResolvedDualFlag(\n triggerId,\n vgReproductive.varName,\n vgReproductive.lastPeriod,\n \"\"\n ))\n\n local lastPeriodDateTable = parseISODateText(lastPeriodRaw)\n\n -- 초기 상태 / 출산 후 복귀 상태를 동일 처리\n if not lastPeriodDateTable then\n local safeCycleDays = {\n 4, 5, 6, 7, 8,\n 18, 19, 20, 21, 22, 23, 24, 25, 26\n }\n\n local cycleDay = safeCycleDays[math.random(1, #safeCycleDays)]\n local daysSinceLastPeriod = cycleDay - 1\n\n lastPeriodDateTable = addDaysToDate(currentDateTable, -daysSinceLastPeriod)\n\n if lastPeriodDateTable then\n batchSetDualFlags(triggerId, vgReproductive.varName, {\n { position = vgReproductive.lastPeriod, value = formatISODateTable(lastPeriodDateTable), isNext = false },\n { position = vgReproductive.pregnancyRisk, value = \"\", isNext = false },\n { position = vgReproductive.riskSource, value = \"\", isNext = false },\n })\n end\n\n local nextRaw = safeGet(triggerId, vgReproductive.varName, \"\")\n if changedVars and prevRaw ~= nextRaw then\n changedVars[vgReproductive.varName] = nextRaw\n end\n\n return getCycleFlag(triggerId, currentDateText)\n end\n\n local elapsed = diffDaysBetween(lastPeriodDateTable, currentDateTable)\n\n -- 미래 날짜거나 비정상 값이면 다시 안전구간 초기화\n if elapsed == nil or elapsed < 0 then\n local safeCycleDays = {\n 4, 5, 6, 7, 8,\n 18, 19, 20, 21, 22, 23, 24, 25, 26\n }\n\n local cycleDay = safeCycleDays[math.random(1, #safeCycleDays)]\n local daysSinceLastPeriod = cycleDay - 1\n\n lastPeriodDateTable = addDaysToDate(currentDateTable, -daysSinceLastPeriod)\n\n if lastPeriodDateTable then\n batchSetDualFlags(triggerId, vgReproductive.varName, {\n { position = vgReproductive.lastPeriod, value = formatISODateTable(lastPeriodDateTable), isNext = false },\n { position = vgReproductive.pregnancyRisk, value = \"\", isNext = false },\n { position = vgReproductive.riskSource, value = \"\", isNext = false },\n })\n end\n\n local nextRaw = safeGet(triggerId, vgReproductive.varName, \"\")\n if changedVars and prevRaw ~= nextRaw then\n changedVars[vgReproductive.varName] = nextRaw\n end\n\n return getCycleFlag(triggerId, currentDateText)\n end\n\n -- 현재 날짜 기준으로 가장 최근 생리 시작일로 당겨오기\n local passedCycles = math.floor(elapsed / 28)\n\n if passedCycles > 0 then\n lastPeriodDateTable = addDaysToDate(lastPeriodDateTable, passedCycles * 28)\n\n if lastPeriodDateTable then\n batchSetDualFlags(triggerId, vgReproductive.varName, {\n { position = vgReproductive.lastPeriod, value = formatISODateTable(lastPeriodDateTable), isNext = false },\n { position = vgReproductive.pregnancyRisk, value = \"\", isNext = false },\n { position = vgReproductive.riskSource, value = \"\", isNext = false },\n })\n end\n end\n\n local nextRaw = safeGet(triggerId, vgReproductive.varName, \"\")\n if changedVars and prevRaw ~= nextRaw then\n changedVars[vgReproductive.varName] = nextRaw\n end\n\n return getCycleFlag(triggerId, currentDateText)\nend\n\nfunction updatePregnancyFlag(triggerId, currentDateText, changedVars)\n currentDateText = trim(tostring(currentDateText or \"\"))\n\n if currentDateText == \"\" then\n local latestDate = trim((select(1, getLatestCharDateTime(triggerId))) or \"\")\n currentDateText = latestDate\n end\n\n currentDateText = trim(currentDateText:match(\"^(%d+%-%d%d?%-%d%d?)\") or currentDateText)\n\n local pregnancyFlag = constPregnancyFlag.none\n local pendingFatherDisplay = \"\"\n\n if currentDateText ~= \"\" then\n local currentDateTable = parseISODateText(currentDateText)\n\n local pendingFatherRaw = trim(getResolvedDualFlag(\n triggerId,\n vgReproductive.varName,\n vgReproductive.pending,\n \"\"\n ))\n\n local pregnancyDateRaw = trim(getResolvedDualFlag(\n triggerId,\n vgReproductive.varName,\n vgReproductive.pregnancyDate,\n \"\"\n ))\n\n local daysTotal = tonumber(getResolvedDualFlag(\n triggerId,\n vgReproductive.varName,\n vgReproductive.daysTotal,\n \"0\"\n )) or 0\n\n local pregnancyDateTable = parseISODateText(pregnancyDateRaw)\n\n -- ==========================================\n -- 실제 임신 / 출산 상태 우선\n -- ==========================================\n if currentDateTable and pregnancyDateTable then\n if daysTotal > 0 then\n local elapsedDays = diffDaysBetween(pregnancyDateTable, currentDateTable)\n\n if elapsedDays ~= nil and elapsedDays >= daysTotal then\n pregnancyFlag = constPregnancyFlag.delivery\n else\n pregnancyFlag = constPregnancyFlag.pregnant\n end\n else\n pregnancyFlag = constPregnancyFlag.pregnant\n end\n\n -- ==========================================\n -- 저장된 pending이 있으면 재굴림 없이 그대로 pending\n -- ==========================================\n elseif pendingFatherRaw ~= \"\" then\n pregnancyFlag = constPregnancyFlag.pending\n pendingFatherDisplay = pendingFatherRaw\n\n else\n local cycleFlag = getCycleFlag(triggerId, currentDateText)\n local pregnancyRiskRaw = trim(getResolvedDualFlag(\n triggerId,\n vgReproductive.varName,\n vgReproductive.pregnancyRisk,\n \"\"\n ))\n local riskSourceRaw = trim(getResolvedDualFlag(\n triggerId,\n vgReproductive.varName,\n vgReproductive.riskSource,\n \"\"\n ))\n\n if cycleFlag == constCycle.pregnant then\n pregnancyFlag = constPregnancyFlag.pregnant\n\n elseif cycleFlag == constCycle.post_fertile and pregnancyRiskRaw ~= \"\" then\n local riskList = splitLiteral(pregnancyRiskRaw, \"^\")\n local sourceList = splitLiteral(riskSourceRaw, \"^\")\n\n local candidates = {}\n local rawTotalRisk = 0\n local pairCount = math.max(#riskList, #sourceList)\n\n for i = 1, pairCount do\n local risk = math.max(0, extractInt(riskList[i] or \"0\"))\n local source = trim(sourceList[i] or \"\")\n\n if risk > 0 and isMeaningfulValue(source) then\n rawTotalRisk = rawTotalRisk + risk\n table.insert(candidates, {\n source = source,\n risk = risk,\n })\n end\n end\n\n local totalRisk = clamp(rawTotalRisk, 0, 100)\n\n if totalRisk > 0 then\n local roll = math.random(100)\n local prevRaw = safeGet(triggerId, vgReproductive.varName, \"\")\n\n if roll <= totalRisk then\n pregnancyFlag = constPregnancyFlag.pending\n\n local selectedFather = \"\"\n local sourceRoll = math.random(rawTotalRisk)\n local acc = 0\n\n for _, info in ipairs(candidates) do\n acc = acc + (tonumber(info.risk) or 0)\n\n if sourceRoll <= acc then\n selectedFather = trim(info.source or \"\")\n break\n end\n end\n\n pendingFatherDisplay = selectedFather\n\n batchSetDualFlags(triggerId, vgReproductive.varName, {\n { position = vgReproductive.pregnancyRisk, value = \"\", isNext = false },\n { position = vgReproductive.riskSource, value = \"\", isNext = false },\n { position = vgReproductive.pending, value = selectedFather, isNext = false },\n })\n\n else\n pregnancyFlag = constPregnancyFlag.none\n\n batchSetDualFlags(triggerId, vgReproductive.varName, {\n { position = vgReproductive.pregnancyRisk, value = \"\", isNext = false },\n { position = vgReproductive.riskSource, value = \"\", isNext = false },\n { position = vgReproductive.pending, value = \"\", isNext = false },\n })\n end\n\n local nextRaw = safeGet(triggerId, vgReproductive.varName, \"\")\n if changedVars and prevRaw ~= nextRaw then\n changedVars[vgReproductive.varName] = nextRaw\n end\n end\n\n elseif cycleFlag\n and cycleFlag >= constCycle.fertile_m5\n and cycleFlag <= constCycle.fertile_p1 then\n pregnancyFlag = constPregnancyFlag.fertile\n end\n end\n end\n\n setChatVar(triggerId, \"vg_Pregnancy_Flag\", tostring(pregnancyFlag))\n setChatVar(triggerId, \"vg_Pregnancy_Pending\", pendingFatherDisplay)\n\n return pregnancyFlag\nend\n\nfunction buildChildTextByLanguage(triggerId, fatherText)\n local lang = getLanguage(triggerId)\n\n if lang == 1 then\n return fatherText .. \"의 아이\"\n elseif lang == 3 then\n return fatherText .. \"の子\"\n else\n return fatherText .. \"'s child\"\n end\nend\n\n-- ==========================================\n-- 공용 파싱\n-- ==========================================\nfunction parseBracketFields(raw)\n local fields = {}\n\n raw = trim(tostring(raw or \"\"))\n raw = raw:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\")\n\n if raw == \"\" then\n return fields\n end\n\n for part in raw:gmatch(\"([^|]+)\") do\n local key, value = part:match(\"^%s*(.-)%s*:%s*(.-)%s*$\")\n\n if key and key ~= \"\" then\n fields[trim(key)] = trim(value or \"\")\n end\n end\n\n return fields\nend\n\nfunction isPlaceholderToken(value, token, tokenLower)\n value = trim(tostring(value or \"\"))\n local lowered = string.lower(value)\n\n if value == token then\n return true\n end\n\n if tokenLower and lowered == tokenLower then\n return true\n end\n\n if value:match(\"^\" .. token .. \"%s*%b()%s*$\") then\n return true\n end\n\n if tokenLower and lowered:match(\"^\" .. tokenLower .. \"%s*%b()%s*$\") then\n return true\n end\n\n if value:match(\"^\" .. token .. \"%s*%b[]%s*$\") then\n return true\n end\n\n if tokenLower and lowered:match(\"^\" .. tokenLower .. \"%s*%b[]%s*$\") then\n return true\n end\n\n if value:match(\"^\" .. token .. \"%s*(.*)%s*$\") then\n return true\n end\n\n if tokenLower and lowered:match(\"^\" .. tokenLower .. \"%s*(.*)%s*$\") then\n return true\n end\n\n return false\nend\n\nfunction isMeaningfulValue(raw)\n local value = trim(tostring(raw or \"\"))\n\n if value == \"\" then\n return false\n end\n\n if isPlaceholderToken(value, \"n/a\", nil) then\n return false\n end\n\n if isPlaceholderToken(value, \"없음\", nil) then\n return false\n end\n\n if isPlaceholderToken(value, \"none\", \"none\") then\n return false\n end\n\n if isPlaceholderToken(value, \"なし\", nil) then\n return false\n end\n\n return true\nend\n\nfunction appendMemoryEntry(triggerId, dateText, milestoneText, contentText, changedVars)\n local oldNum = tonumber(safeGet(triggerId, \"vg_Memory_Num\", \"0\")) or 0\n local oldContent = safeGet(triggerId, \"vg_Memory_Content\", \"\")\n\n local entry = table.concat({\n tostring(dateText or \"\"),\n tostring(milestoneText or \"\"),\n tostring(contentText or \"\")\n }, \"§\")\n\n local newNum = tostring(oldNum + 1)\n local newContent = appendPipe(oldContent, entry)\n\n setTrackedChatVar(triggerId, changedVars, \"vg_Memory_Num\", newNum)\n setChatVar(triggerId, \"vg_Memory_Content\", newContent)\nend\n\nfunction appendStoryEntry(triggerId, dateText, milestoneText, contentText)\n local oldNum = tonumber(safeGet(triggerId, \"vg_Story_Num\", \"0\")) or 0\n local oldContent = safeGet(triggerId, \"vg_Story_Content\", \"\")\n\n local entry = table.concat({\n tostring(dateText or \"\"),\n tostring(milestoneText or \"\"),\n tostring(contentText or \"\")\n }, \"§\")\n\n local newNum = tostring(oldNum + 1)\n local newContent = appendPipe(oldContent, entry)\n\n setChatVar(triggerId, \"vg_Story_Num\", newNum)\n setChatVar(triggerId, \"vg_Story_Content\", newContent)\nend\n\nfunction parseOrderedFieldsWithFallback(raw, orderedKeys)\n raw = trim(tostring(raw or \"\"))\n\n local parsed = {}\n local rawParts = {}\n\n for _, key in ipairs(orderedKeys or {}) do\n parsed[string.lower(trim(key or \"\"))] = \"\"\n end\n\n for part in raw:gmatch(\"([^|]+)\") do\n local token = trim(part)\n table.insert(rawParts, token)\n\n local key, value = token:match(\"^%s*(.-)%s*:%s*(.-)%s*$\")\n if key then\n local loweredKey = string.lower(trim(key))\n\n if parsed[loweredKey] ~= nil then\n parsed[loweredKey] = trim(value or \"\")\n end\n end\n end\n\n local function getKeyName(index)\n return string.lower(trim((orderedKeys or {})[index] or \"\"))\n end\n\n local function isAllMissing()\n for _, key in ipairs(orderedKeys or {}) do\n if parsed[string.lower(trim(key or \"\"))] ~= \"\" then\n return false\n end\n end\n return true\n end\n\n local allMissing = isAllMissing()\n\n -- ==========================================\n -- 완전 비정상형:\n -- [값1 | 값2 | ...]\n -- orderedKeys 순서대로 그대로 매핑\n -- ==========================================\n if allMissing then\n if #rawParts >= 2 then\n for i, key in ipairs(orderedKeys or {}) do\n parsed[string.lower(trim(key or \"\"))] = trim(rawParts[i] or \"\")\n end\n return parsed\n end\n\n -- ==========================================\n -- 완전 비정상형 + 파이프도 없음:\n -- [💤 기둥에 매달린 채 ...]\n -- [Defiant 끝까지 버틴다]\n --\n -- 첫 공백 전까지 = 첫 필드\n -- 나머지 전체 = 두 번째 필드\n -- ==========================================\n if #rawParts == 1 and #(orderedKeys or {}) >= 2 then\n local single = trim(rawParts[1] or \"\")\n\n local firstValue, secondValue = single:match(\"^(%S+)%s+(.+)$\")\n\n if firstValue and secondValue then\n parsed[getKeyName(1)] = trim(firstValue or \"\")\n parsed[getKeyName(2)] = trim(secondValue or \"\")\n return parsed\n end\n end\n\n return parsed\n end\n\n -- ==========================================\n -- 반쯤 비정상형:\n -- 일부 키만 있거나 일부만 빠진 경우\n -- 해당 위치 토큰으로만 안전하게 보정\n -- ==========================================\n for i, key in ipairs(orderedKeys or {}) do\n local loweredKey = string.lower(trim(key or \"\"))\n\n if parsed[loweredKey] == \"\" then\n local token = trim(rawParts[i] or \"\")\n\n if token ~= \"\" then\n local tokenKey, tokenValue = token:match(\"^%s*(.-)%s*:%s*(.-)%s*$\")\n\n if not tokenKey then\n parsed[loweredKey] = token\n elseif string.lower(trim(tokenKey or \"\")) == loweredKey then\n parsed[loweredKey] = trim(tokenValue or \"\")\n end\n end\n end\n end\n\n -- ==========================================\n -- 추가 fallback:\n -- 예) [Situation: ...] 만 있고 Emoji가 없는 경우\n -- 또는 rawParts가 1개뿐인데 나머지 하나가 비어 있는 경우\n -- 단일 토큰을 \"첫 공백 전 / 후\"로 나눠 부족한 필드 보정\n -- ==========================================\n if #rawParts == 1 and #(orderedKeys or {}) >= 2 then\n local single = trim(rawParts[1] or \"\")\n local firstValue, secondValue = single:match(\"^(%S+)%s+(.+)$\")\n\n if firstValue and secondValue then\n local key1 = getKeyName(1)\n local key2 = getKeyName(2)\n\n if parsed[key1] == \"\" then\n parsed[key1] = trim(firstValue or \"\")\n end\n\n if parsed[key2] == \"\" then\n parsed[key2] = trim(secondValue or \"\")\n end\n end\n end\n\n return parsed\nend\n\nlocal function getRandomFallbackEffectLevelText()\n local fallbackEffectLevelPool = {\n \"Very Low\",\n \"Low\",\n \"Medium\",\n \"High\",\n \"Very High\",\n }\n\n return fallbackEffectLevelPool[math.random(1, #fallbackEffectLevelPool)]\nend\n\nfunction ensureStatusWindowClosed(rawText)\n rawText = tostring(rawText or \"\")\n\n if not rawText:find(\"<Status Window>\", 1, true) then\n return rawText\n end\n\n if rawText:find(\"</Status Window>\", 1, true) then\n return rawText\n end\n\n local openStart, openEnd = rawText:find(\"<Status Window>\", 1, true)\n if not openEnd then\n return rawText\n end\n\n local afterOpen = rawText:sub(openEnd + 1)\n\n local lastLineEnd = nil\n local pos = 1\n\n while true do\n local lineStart, lineEnd = afterOpen:find(\"[^\\r\\n]+\", pos)\n if not lineStart then\n break\n end\n\n local line = afterOpen:sub(lineStart, lineEnd)\n\n if line:match(\"^%b[]$\")\n and line:find(\"Date:\", 1, true)\n and line:find(\"Time:\", 1, true)\n and line:find(\"Location:\", 1, true)\n and line:find(\"Area:\", 1, true)\n and line:find(\"Name:\", 1, true)\n and line:find(\"Equipment:\", 1, true)\n and line:find(\"Clothing:\", 1, true)\n and line:find(\"Underwear:\", 1, true)\n then\n lastLineEnd = lineEnd\n end\n\n pos = lineEnd + 1\n end\n\n if not lastLineEnd then\n return rawText\n end\n\n local before = afterOpen:sub(1, lastLineEnd)\n local after = afterOpen:sub(lastLineEnd + 1)\n\n if not before:match(\"\\n$\") then\n before = before .. \"\\n\"\n end\n\n return rawText:sub(1, openEnd) .. before .. \"</Status Window>\" .. after\nend\n\n-- ==========================================\n-- 공용 듀얼 플래그\n--\n-- 슬롯 형식:\n-- \"\" -> current 없음 / next 없음\n-- \"1\" -> current=1 / next 없음\n-- \"§1\" -> current 없음 / next=1\n-- \"1§2\" -> current=1 / next=2\n--\n-- position은 1부터 시작\n-- isNext == true -> next값 get/set\n-- isNext == false -> current값 get/set\n-- ==========================================\nfunction parseDualFlagSlot(slot)\n slot = tostring(slot or \"\")\n\n if slot == \"\" then\n return \"\", \"\"\n end\n\n local currentValue, nextValue = slot:match(\"^(.-)§(.*)$\")\n\n if currentValue == nil then\n currentValue = slot\n nextValue = \"\"\n end\n\n if currentValue == \"nil\" or currentValue == \"null\" then\n currentValue = \"\"\n end\n\n if nextValue == \"nil\" or nextValue == \"null\" then\n nextValue = \"\"\n end\n\n return tostring(currentValue or \"\"), tostring(nextValue or \"\")\nend\n\nfunction buildDualFlagSlot(currentValue, nextValue)\n currentValue = tostring(currentValue or \"\")\n nextValue = tostring(nextValue or \"\")\n\n if currentValue == \"nil\" or currentValue == \"null\" then\n currentValue = \"\"\n end\n\n if nextValue == \"nil\" or nextValue == \"null\" then\n nextValue = \"\"\n end\n\n if currentValue ~= \"\" then\n currentValue = trim(currentValue)\n end\n\n if nextValue ~= \"\" then\n nextValue = trim(nextValue)\n end\n\n if currentValue == nextValue then\n nextValue = \"\"\n end\n\n if currentValue == \"\" and nextValue == \"\" then\n return \"\"\n elseif nextValue ~= \"\" then\n return currentValue .. \"§\" .. nextValue\n else\n return currentValue\n end\nend\n\nfunction getDualFlagSlotList(triggerId, varName)\n local raw = safeGet(triggerId, varName, \"\")\n local slotList = {}\n\n if raw ~= \"\" then\n for token in (raw .. \"|\"):gmatch(\"(.-)|\") do\n table.insert(slotList, token or \"\")\n end\n end\n\n return slotList\nend\n\nfunction getResolvedDualFlag(triggerId, varName, position, defaultValue)\n local nextValue = getDualFlag(triggerId, varName, position, true)\n if nextValue ~= nil and tostring(nextValue) ~= \"\" then\n return tostring(nextValue)\n end\n\n local currentValue = getDualFlag(triggerId, varName, position, false)\n if currentValue ~= nil and tostring(currentValue) ~= \"\" then\n return tostring(currentValue)\n end\n\n return tostring(defaultValue or \"\")\nend\n\nfunction getResolvedDualFlagFromSlotList(slotList, position, defaultValue)\n local nextValue = getDualFlagFromSlotList(slotList, position, true)\n if nextValue ~= nil and tostring(nextValue) ~= \"\" then\n return tostring(nextValue)\n end\n\n local currentValue = getDualFlagFromSlotList(slotList, position, false)\n if currentValue ~= nil and tostring(currentValue) ~= \"\" then\n return tostring(currentValue)\n end\n\n return tostring(defaultValue or \"\")\nend\n\nfunction setDualFlagInSlotList(slotList, position, value, isNext)\n position = tonumber(position) or 0\n if position < 1 then return end\n\n for i = #slotList + 1, position do\n slotList[i] = \"\"\n end\n\n local currentValue, nextValue = parseDualFlagSlot(slotList[position] or \"\")\n\n value = tostring(value or \"\")\n if value == \"nil\" or value == \"null\" then\n value = \"\"\n end\n if value ~= \"\" then\n value = trim(value)\n end\n\n if isNext == true then\n nextValue = value\n else\n currentValue = value\n end\n\n slotList[position] = buildDualFlagSlot(currentValue, nextValue)\nend\n\nfunction batchSetDualFlags(triggerId, varName, updates)\n local slotList = getDualFlagSlotList(triggerId, varName)\n\n for i = 1, #updates do\n local u = updates[i]\n setDualFlagInSlotList(slotList, u.position, u.value, u.isNext)\n end\n\n saveDualFlagSlotList(triggerId, varName, slotList)\nend\n\nfunction saveDualFlagSlotList(triggerId, varName, slotList)\n local lastUsed = 0\n\n for i = 1, #slotList do\n if tostring(slotList[i] or \"\") ~= \"\" then\n lastUsed = i\n end\n end\n\n if lastUsed <= 0 then\n setChatVar(triggerId, varName, \"\")\n return\n end\n\n local out = {}\n\n for i = 1, lastUsed do\n out[i] = tostring(slotList[i] or \"\")\n end\n\n setChatVar(triggerId, varName, table.concat(out, \"|\"))\nend\n\nfunction getDualFlag(triggerId, varName, position, isNext)\n position = tonumber(position) or 0\n\n if position < 1 then\n return \"\"\n end\n\n local slotList = getDualFlagSlotList(triggerId, varName)\n local slot = tostring(slotList[position] or \"\")\n\n if slot == \"\" then\n return \"\"\n end\n\n local currentValue, nextValue = parseDualFlagSlot(slot)\n\n if isNext == true then\n return tostring(nextValue or \"\")\n else\n return tostring(currentValue or \"\")\n end\nend\n\nfunction setDualFlag(triggerId, varName, position, value, isNext)\n position = tonumber(position) or 0\n\n if position < 1 then\n return\n end\n\n local slotList = getDualFlagSlotList(triggerId, varName)\n setDualFlagInSlotList(slotList, position, value, isNext)\n saveDualFlagSlotList(triggerId, varName, slotList)\nend\n\nfunction applyNextDualFlagToCurrent(triggerId, varName)\n local slotList = getDualFlagSlotList(triggerId, varName)\n\n if #slotList <= 0 then\n return\n end\n\n for i = 1, #slotList do\n local currentValue, nextValue = parseDualFlagSlot(slotList[i] or \"\")\n\n if nextValue ~= \"\" then\n currentValue = nextValue\n end\n\n slotList[i] = buildDualFlagSlot(currentValue, \"\")\n end\n\n saveDualFlagSlotList(triggerId, varName, slotList)\nend\n\nfunction clearNextDualFlag(triggerId, varName)\n local slotList = getDualFlagSlotList(triggerId, varName)\n\n if #slotList <= 0 then\n return\n end\n\n for i = 1, #slotList do\n local currentValue, _ = parseDualFlagSlot(slotList[i] or \"\")\n\n if currentValue ~= \"\" then\n slotList[i] = buildDualFlagSlot(currentValue, \"\")\n else\n slotList[i] = \"\"\n end\n end\n\n saveDualFlagSlotList(triggerId, varName, slotList)\nend\n\nfunction getDualFlagFromSlotList(slotList, position, isNext)\n position = tonumber(position) or 0\n\n if position < 1 then\n return \"\"\n end\n\n local slot = tostring(slotList[position] or \"\")\n\n if slot == \"\" then\n return \"\"\n end\n\n local currentValue, nextValue = parseDualFlagSlot(slot)\n\n if isNext == true then\n return tostring(nextValue or \"\")\n else\n return tostring(currentValue or \"\")\n end\nend\n\nfunction loadDualFlagCurrentMap(triggerId, def)\n local slotList = getDualFlagSlotList(triggerId, def.varName)\n local out = {}\n\n for key, position in pairs(def) do\n if key ~= \"varName\" then\n out[key] = tonumber(getDualFlagFromSlotList(slotList, position, false)) or 0\n end\n end\n\n return out\nend\n\nfunction loadDualFlagNextMap(triggerId, def)\n local slotList = getDualFlagSlotList(triggerId, def.varName)\n local out = {}\n\n for key, position in pairs(def) do\n if key ~= \"varName\" then\n out[key] = tonumber(getResolvedDualFlagFromSlotList(slotList, position, 0)) or 0\n end\n end\n\n return out\nend\n\nfunction batchResetAndSetNextDualFlags(triggerId, varName, updates)\n local slotList = getDualFlagSlotList(triggerId, varName)\n\n -- ==========================================\n -- 기존 next 제거, current만 유지\n -- ==========================================\n for i = 1, #slotList do\n local currentValue, _ = parseDualFlagSlot(slotList[i] or \"\")\n\n if currentValue ~= \"\" then\n slotList[i] = buildDualFlagSlot(currentValue, \"\")\n else\n slotList[i] = \"\"\n end\n end\n\n -- ==========================================\n -- 전달된 값만 next로 설정\n -- ==========================================\n for i = 1, #(updates or {}) do\n local u = updates[i]\n setDualFlagInSlotList(slotList, u.position, u.value, true)\n end\n\n saveDualFlagSlotList(triggerId, varName, slotList)\nend\n\n-- ==========================================\n-- 페이즈 설정\n-- ==========================================\nfunction setPhase(triggerId, value)\n value = tonumber(value) or 0\n\n local prevPhase = getCurrentPhase(triggerId)\n\n local sessionSlotList = getDualFlagSlotList(triggerId, vgSessionInfo.varName)\n local contextSlotList = getDualFlagSlotList(triggerId, vgContext.varName)\n\n local storyTypeValue = tonumber(getResolvedDualFlagFromSlotList(sessionSlotList, vgSessionInfo.storyType, 0)) or 0\n local stageValue = tonumber(getResolvedDualFlagFromSlotList(sessionSlotList, vgSessionInfo.stage, 1)) or 1\n\n stageValue = clamp(stageValue, 1, 4)\n\n setChatVar(triggerId, \"vg_ChangeGoal\", \"0\")\n\n -- ==========================================\n -- 출산 페이즈 변환\n -- ==========================================\n if value == constPhase.daily or value == constPhase.unfree then\n local pregnancyFlag = tonumber(safeGet(triggerId, \"vg_Pregnancy_Flag\", tostring(constPregnancyFlag.none))) or constPregnancyFlag.none\n\n if pregnancyFlag == constPregnancyFlag.delivery and stageValue < 4 then\n if value == constPhase.daily then\n value = constPhase.delivery_Daily\n else\n value = constPhase.delivery_Unfree\n end\n end\n elseif value == constPhase.delivery_DailyAfter then\n value = constPhase.daily\n elseif value == constPhase.delivery_UnfreeAfter then\n value = constPhase.unfree\n end\n\n setDualFlag(triggerId, vgContext.varName, vgContext.phase, tostring(value), true)\n\n local choiceFlag = constChoice.none\n local choiceTypeText = \"\"\n local choiceSettingText = \"\"\n local goalFlagText = \"\"\n\n -- ==========================================\n -- Story Guide\n -- ==========================================\n local storyGuideText = \"\"\n\n if storyTypeValue == constStoryType.core then\n local stageText = \"\"\n\n if stageValue == 1 then\n stageText = \" It is currently at the opening of the current storyline. It should mainly establish the direction, conditions, or tensions that this storyline will develop.\"\n elseif stageValue == 2 then\n stageText = \" It is currently in the middle escalation of the current storyline. It should mainly deepen, complicate, or escalate the tensions and developments already in motion.\"\n elseif stageValue == 3 then\n stageText = \" It is currently at the climax of the current storyline. It should mainly push toward the decisive center, final turning point, or point of no return of this storyline.\"\n else\n stageText = \" It is currently in the aftermath of the current storyline, after its central conflict has already been resolved. It should mainly reflect immediate consequences, remaining fallout, and the movement into what comes next.\"\n end\n\n storyGuideText = \"- Main Storyline: The heroine is currently on the main storyline.\" .. stageText\n else\n local shortGoal = trim(safeGet(triggerId, \"vg_Goal_Short\", \"\"))\n if shortGoal == \"\" then\n storyGuideText = \"- Side Storyline: When appropriate, let a subtle, heroine-appropriate narrative hook emerge naturally from her character, the story established so far, her current circumstances, or the conflicts, threats, relationships, and unresolved events around her, gently suggesting possible paths ahead.\"\n end\n end\n\n if isDeckPhase(triggerId, value) then\n if (isEncounterPhase(triggerId, value) or isHEventPhase(triggerId, value)) and isExhibitionismPhase(triggerId, value) == false then\n choiceFlag = constChoice.deck_Encounter\n else\n choiceFlag = constChoice.deck_Alone\n end\n\n elseif value == constPhase.interlude then\n choiceFlag = constChoice.choice_Interlude\n\n local goalShort = trim(safeGet(triggerId, \"vg_Goal_Short\", \"\"))\n local goalLong = trim(safeGet(triggerId, \"vg_Goal_Long\", \"\"))\n\n local currentGoalText = \"\"\n local ultimateGoalText = \"\"\n\n if goalShort ~= \"\" then\n currentGoalText = \"the heroine's current storyline goal, \\\"\" .. goalShort .. \"\\\"\"\n end\n\n if goalLong ~= \"\" then\n ultimateGoalText = \"the heroine's ultimate goal, \\\"\" .. goalLong .. \"\\\"\"\n end\n\n local interludeGoalText = \"\"\n\n if stageValue == 2 then\n\n if currentGoalText ~= \"\" then\n interludeGoalText = interludeGoalText ..\n \" - Each choice should establish a distinct branch meaningfully connected to, or that may later develop toward, \" .. currentGoalText .. \".\\n\"\n elseif ultimateGoalText ~= \"\" then\n interludeGoalText = interludeGoalText ..\n \" - Each choice should establish a distinct branch meaningfully connected to the current storyline and, where appropriate to the current context, one that may also later carry broader significance in relation to \" .. ultimateGoalText .. \".\\n\"\n end\n\n if currentGoalText ~= \"\" and ultimateGoalText ~= \"\" then\n interludeGoalText = interludeGoalText ..\n \" - Where appropriate to the current context, that branch may also later carry significance for \" .. ultimateGoalText .. \".\\n\"\n end\n\n choiceSettingText =\n \"- This is an opening point leading into the middle escalation of the current storyline.\\n\" ..\n \"- Each choice must function as an opening step toward a more important development of the current storyline, such as a meaningful lead, decision, duty, alignment, approach, or turning direction.\\n\" ..\n interludeGoalText ..\n \"- The major development itself must not begin immediately; each choice should only establish the branch that leads into it.\"\n\n elseif stageValue == 3 then\n if currentGoalText ~= \"\" then\n interludeGoalText = interludeGoalText ..\n \" - Each choice should establish a distinct branch that leads into the final approach toward achieving \" .. currentGoalText .. \".\\n\"\n elseif ultimateGoalText ~= \"\" then\n interludeGoalText = interludeGoalText ..\n \" - Each choice should establish a distinct branch directly tied to a major decisive development of the current storyline and, where appropriate to the current context, one that may also take on broader significance for how the story proceeds in relation to \" .. ultimateGoalText .. \".\\n\"\n end\n\n if currentGoalText ~= \"\" and ultimateGoalText ~= \"\" then\n interludeGoalText = interludeGoalText ..\n \" - Where appropriate to the current context, that branch may also carry broader significance for \" .. ultimateGoalText .. \".\\n\"\n end\n\n choiceSettingText =\n \"- This is an opening point leading into the climax of the current storyline.\\n\" ..\n \"- Each choice must function as an opening step toward the decisive late-stage movement of the current storyline.\\n\" ..\n interludeGoalText ..\n \"- The decisive development itself must not begin immediately; each choice should only establish the branch that leads directly into it.\"\n\n elseif stageValue >= 4 then\n if currentGoalText ~= \"\" then\n interludeGoalText = interludeGoalText ..\n \" - Each choice should establish a distinct onward branch that, depending on the current context, either leads toward achieving \" .. currentGoalText .. \" or opens after it has been achieved or otherwise resolved.\\n\"\n elseif ultimateGoalText ~= \"\" then\n interludeGoalText = interludeGoalText ..\n \" - Each choice should establish a distinct onward branch that emerges from the aftermath of the current storyline and, where appropriate to the current context, one that may also help define how the story moves forward in relation to \" .. ultimateGoalText .. \".\\n\"\n end\n\n if currentGoalText ~= \"\" and ultimateGoalText ~= \"\" then\n interludeGoalText = interludeGoalText ..\n \" - Where appropriate to the current context, that onward branch may also begin to define how the story proceeds in relation to \" .. ultimateGoalText .. \".\\n\"\n end\n\n choiceSettingText =\n \"- This is an opening point in the aftermath of the current storyline.\\n\" ..\n \"- The central conflict of the current storyline has already been resolved, so each choice should open into consequences, recovery, regrouping, fallout, or the first movement toward what comes next.\\n\" ..\n interludeGoalText ..\n \"- The immediate decisive tension of the current storyline is over.\"\n end\n\n elseif value == constPhase.daily then\n choiceFlag = constChoice.choice_Daily\n\n elseif value == constPhase.incapacitated then\n choiceFlag = constChoice.choice_Incapacitated\n\n elseif value == constPhase.unfree then\n choiceFlag = constChoice.choice_Unfree\n\n elseif value == constPhase.captivity then\n choiceFlag = constChoice.choice_Captivity\n\n local choiceTypeDefs = {\n { key = \"Escape\", text = \" - Escape: Attempting to break free on her own through physical struggle, confrontation, or combat.\" },\n { key = \"Rescue\", text = \" - Rescue: In the Situation, write the rescuer by name or kind. The Encounter must not be the rescuer, but the hostile party resisting the rescue.\" },\n { key = \"Violence\", text = \" - Violence: Facing one-sided sexual violence in a state where the heroine cannot meaningfully resist.\" },\n { key = \"Sleeping\", text = \" - Sleeping: Facing one-sided sexual violence while asleep.\" },\n { key = \"Immobilized\", text = \" - Immobilized: Facing one-sided sexual violence while conscious but unable to move or meaningfully resist due to paralysis, drugging, numbing agents, or similar non-restraint means.\" },\n { key = \"Brainwashing\", text = \" - Brainwashing: Being made to sexually act, comply, or submit against her own will under hypnosis, brainwashing, mental control, suggestion, or similar influence.\" },\n { key = \"Coercion\", text = \" - Coercion: Being forced to actively sexually serve, perform for, or satisfy others under threat, control, dependency, exploitation, or lack of alternatives. The focus is on compelled participation in sexual service, not on one-sided sexual violation.\" },\n { key = \"Aphrodisiac\", text = \" - Aphrodisiac: Being compelled into sexual acts after being affected by some form of aphrodisiac influence.\" },\n }\n\n local choiceTypeLines = {}\n\n for _, def in ipairs(choiceTypeDefs) do\n table.insert(choiceTypeLines, def.text)\n end\n\n choiceTypeText = table.concat(choiceTypeLines, \"\\n\")\n else\n local choiceTypeDefs = getChoiceTypeDefs(triggerId)\n local alwaysKey = trim(safeGet(triggerId, \"vg_Choice_Always\", \"\"))\n local enabled = {}\n\n for _, def in ipairs(choiceTypeDefs) do\n enabled[def.key] = (def.defaultEnabled == true)\n end\n\n if value == constPhase.standby then\n choiceFlag = constChoice.choice_Story\n local lewdnessLevel = getLewdnessLevel(triggerId)\n\n if lewdnessLevel >= 3 then\n enabled.Exhibitionism = true\n enabled.Rape = true\n\n local heroineStat = loadDualFlagNextMap(triggerId, vgHeroineStat)\n local currentArousal = tonumber(heroineStat.arousal) or 0\n local maxArousal = tonumber(heroineStat.arousal_Max) or 0\n local masturbationChoiceThreshold = tonumber(getMasturbationChoiceThreshold(triggerId)) or 0.85\n\n if alwaysKey == \"Masturbation\" or (maxArousal > 0 and currentArousal >= (maxArousal * masturbationChoiceThreshold * 0.6)) then\n enabled.Masturbation = true\n end\n end\n\n if lewdnessLevel >= 5 then\n enabled.Performance = true\n enabled.Intimacy = true\n end\n\n if lewdnessLevel >= 7 then\n enabled.Transaction = true\n end\n\n if lewdnessLevel >= 9 then\n enabled.Promiscuity = true\n end\n\n do\n local goalParts = {}\n\n local goalShort = trim(safeGet(triggerId, \"vg_Goal_Short\", \"\"))\n local goalLong = trim(safeGet(triggerId, \"vg_Goal_Long\", \"\"))\n\n if goalShort ~= \"\" then\n table.insert(goalParts, \"current storyline goal\")\n end\n\n if goalLong ~= \"\" then\n table.insert(goalParts, \"ultimate goal\")\n end\n\n if #goalParts > 0 then\n goalFlagText = \" It should align with the heroine's \" .. joinWithAnd(goalParts) .. \".\"\n end\n end\n\n do\n local startKnowledge = tonumber(safeGet(triggerId, \"vg_startKnowledge\", \"0\")) or 0\n local currentKnowledge = tonumber(getResolvedDualFlag(triggerId, vgSexKnowledge.varName, vgSexKnowledge.level, 0)) or 0\n\n local gainedKnowledge = math.max(0, currentKnowledge - startKnowledge)\n\n local knowledgeLimit = 0\n\n if lewdnessLevel >= 11 then\n knowledgeLimit = 5\n elseif lewdnessLevel >= 9 then\n knowledgeLimit = 4\n elseif lewdnessLevel >= 7 then\n knowledgeLimit = 3\n elseif lewdnessLevel >= 5 then\n knowledgeLimit = 2\n elseif lewdnessLevel >= 3 then\n knowledgeLimit = 1\n end\n\n if currentKnowledge < 5 and gainedKnowledge < knowledgeLimit and math.random(2) == 1 then\n enabled.Indoctrination = true\n end\n end\n\n do\n local pregnancyDateRaw = trim(getResolvedDualFlag(triggerId, vgReproductive.varName, vgReproductive.pregnancyDate, \"\"))\n local daysTotal = tonumber(getResolvedDualFlag(triggerId, vgReproductive.varName, vgReproductive.daysTotal, \"0\")) or 0\n\n local currentDateText = trim((select(1, getLatestCharDateTime(triggerId))) or \"\")\n currentDateText = trim(currentDateText:match(\"^(%d+%-%d%d?%-%d%d?)\") or currentDateText)\n\n local pregnancyDateTable = parseISODateText(pregnancyDateRaw)\n local currentDateTable = parseISODateText(currentDateText)\n\n if pregnancyDateTable and currentDateTable and daysTotal > 0 then\n local elapsedDays = diffDaysBetween(pregnancyDateTable, currentDateTable)\n\n if elapsedDays ~= nil and elapsedDays >= 0 then\n local progressPercent = clamp((elapsedDays / daysTotal) * 100, 0, 100)\n\n if progressPercent <= 60 then\n enabled.Abortion = true\n end\n end\n end\n end\n\n elseif value == constPhase.advance then\n choiceFlag = constChoice.choice_Advance\n\n local goalShort = trim(safeGet(triggerId, \"vg_Goal_Short\", \"\"))\n local goalLong = trim(safeGet(triggerId, \"vg_Goal_Long\", \"\"))\n\n local currentGoalText = \"\"\n local ultimateGoalText = \"\"\n\n if goalShort ~= \"\" then\n currentGoalText = 'the heroine\\'s current storyline goal \"' .. goalShort .. '\"'\n end\n\n if goalLong ~= \"\" then\n ultimateGoalText = 'the heroine\\'s ultimate goal \"' .. goalLong .. '\"'\n end\n\n local advanceGoalText = \"\"\n\n if stageValue == 3 then\n local climaxPointText = \"- This point belongs to the climax of the current storyline.\\n\"\n\n if currentGoalText ~= \"\" then\n climaxPointText = \"- This point is both the current storyline's climax and the decisive completion point of its current goal.\\n\"\n advanceGoalText = \" - Each choice should lead directly into the decisive encounter for \" .. currentGoalText .. \", and successfully resolving that encounter must be enough to credibly complete the goal (e.g., if completing the goal requires the named subject itself to be present or directly involved, the encounter must center on that subject itself; supporting figures may accompany it, but must not replace it).\\n\"\n elseif ultimateGoalText ~= \"\" then\n advanceGoalText = \" - Each choice should lead straight into a decisive branch where, if the buildup of the current storyline is already sufficient, success could mean achieving \" .. ultimateGoalText .. \"; otherwise, it should mark a major step toward it.\\n\"\n end\n\n choiceSettingText =\n climaxPointText ..\n \"- Each choice must skip trivial or routine steps and lead straight toward the decisive center, final turning point, or point of no return of the current storyline.\\n\" ..\n advanceGoalText ..\n \"- The encounter in each choice must center on a major element of the current storyline, such as a principal actor, core faction, or decisive agent, rather than on rank-and-file followers, generic subordinates, or expendable groups; even if the encounters differ in form across the choices, they should still carry a similar degree of centrality and narrative weight within the current storyline.\"\n\n else\n if currentGoalText ~= \"\" then\n advanceGoalText = \" - Each choice should lead straight into a major branch that presses directly toward \" .. currentGoalText .. \" without yet reaching the final decisive point at which it would be achieved.\\n\"\n elseif ultimateGoalText ~= \"\" then\n advanceGoalText = \" - Each choice should lead straight into a major branch where, if the buildup of the current storyline is already sufficient, it may bear directly on \" .. ultimateGoalText .. \"; otherwise, it should mark a substantial step toward it.\\n\"\n end\n\n choiceSettingText =\n \"- This point belongs to the middle escalation of the current storyline.\\n\" ..\n \"- Each choice must skip trivial or routine steps and lead straight toward a major turning-point pressure, key complication, or pivotal figure of the current storyline, but not its final decisive center.\\n\" ..\n advanceGoalText ..\n \"- The encounter in each choice must feel notably more consequential than a minor obstacle, representing a major pressure, actor, group, mechanism, or pivotal intermediate element within the current storyline's rising conflict rather than a routine or low-stakes opposition.\"\n end\n end\n\n if not enabled[alwaysKey] then\n alwaysKey = \"\"\n end\n\n local function limitEnabledGroup(enabled, keys, maxCount, keepKey)\n maxCount = tonumber(maxCount) or 0\n if maxCount < 0 then\n maxCount = 0\n end\n\n keepKey = trim(tostring(keepKey or \"\"))\n\n local activeKeys = {}\n local keepActive = false\n\n for _, key in ipairs(keys or {}) do\n if enabled[key] then\n if key == keepKey then\n keepActive = true\n else\n table.insert(activeKeys, key)\n end\n end\n end\n\n local keepCount = 0\n if keepActive then\n keepCount = 1\n end\n\n local allowedRandomCount = maxCount - keepCount\n\n if allowedRandomCount < 0 then\n allowedRandomCount = 0\n end\n\n if #activeKeys <= allowedRandomCount then\n return\n end\n\n shuffleArray(activeKeys)\n\n for i = allowedRandomCount + 1, #activeKeys do\n enabled[activeKeys[i]] = false\n end\n end\n\n limitEnabledGroup(enabled, { \"Sleeping\", \"Entrapment\", \"Aphrodisiac\" }, 1, alwaysKey)\n limitEnabledGroup(enabled, { \"Hypnosis\", \"Brainwashing\" }, 1, alwaysKey)\n limitEnabledGroup(enabled, { \"Feral\", \"Companion\", \"Abduction\" }, 1, alwaysKey)\n limitEnabledGroup(enabled, { \"Exhibitionism\", \"Masturbation\", \"Rape\", \"Performance\", \"Intimacy\", \"Transaction\", \"Promiscuity\" }, 2, alwaysKey)\n\n do\n local exclusiveKeys = {}\n\n for _, def in ipairs(choiceTypeDefs) do\n if def.exclusive == true then\n table.insert(exclusiveKeys, def.key)\n end\n end\n\n limitEnabledGroup(enabled, exclusiveKeys, 2, alwaysKey)\n end\n\n do\n local totalEnabledCount = 0\n\n for _, def in ipairs(choiceTypeDefs) do\n if enabled[def.key] then\n totalEnabledCount = totalEnabledCount + 1\n end\n end\n\n local baseKeys = { \"Combat\", \"Harassment\", \"Abuse\", \"Confrontation\" }\n\n if totalEnabledCount >= 6 then\n enabled[\"Combat\"] = true\n limitEnabledGroup(enabled, { \"Harassment\", \"Abuse\", \"Confrontation\" }, 2, alwaysKey)\n\n else\n -- 전체 활성 수가 4 미만이면 기본 4개를 모두 살림\n for _, key in ipairs(baseKeys) do\n enabled[key] = true\n end\n end\n end\n\n local choiceTypeLines = {}\n\n for _, def in ipairs(choiceTypeDefs) do\n if enabled[def.key] then\n table.insert(choiceTypeLines, def.text)\n end\n end\n\n choiceTypeText = table.concat(choiceTypeLines, \"\\n\")\n end\n\n setChatVar(triggerId, \"vg_Story_Guide\", storyGuideText)\n setChatVar(triggerId, \"vg_Choice_Flag\", tostring(choiceFlag))\n setChatVar(triggerId, \"vg_Choice_Type\", choiceTypeText)\n setChatVar(triggerId, \"vg_Choice_Setting\", choiceSettingText)\n setChatVar(triggerId, \"vg_Flag_Goal\", goalFlagText)\n\n -- ==========================================\n -- 강화\n -- ==========================================\n local enhancementFlag = 0\n\n if (value == constPhase.daily or value == constPhase.interlude or value == constPhase.unfree) and isMasturbationPhase(triggerId, prevPhase) == false then\n enhancementFlag = 1\n\n local idx1 = math.random(#enhancementTypeDefs)\n local idx2 = math.random(#enhancementTypeDefs - 1)\n\n if idx2 >= idx1 then\n idx2 = idx2 + 1\n end\n\n local type1 = enhancementTypeDefs[idx1].name\n local type2 = enhancementTypeDefs[idx2].name\n\n setChatVar(\n triggerId,\n \"vg_Enhancement_Type\",\n '- Increase1: The first effect type increased by the enhancement. It must be exactly \"Increase1: ' .. type1 .. '\".\\n' ..\n '- Increase2: The second effect type increased by the enhancement. It must be exactly \"Increase2: ' .. type2 .. '\".'\n )\n\n setChatVar(\n triggerId,\n \"vg_Enhancement_Format\",\n '<Obtainable Enhancement>[Increase1: ' .. type1 .. ' | Increase2: ' .. type2 .. ' | Enhancement Name: name | Type: type]</Obtainable Enhancement>'\n )\n else\n setChatVar(triggerId, \"vg_Enhancement_Type\", \"\")\n setChatVar(triggerId, \"vg_Enhancement_Format\", \"\")\n end\n\n setChatVar(triggerId, \"vg_Enhancement_Flag\", tostring(enhancementFlag))\n\n -- ==========================================\n -- 성적 특성\n -- ==========================================\n\n local lewdnessLevel = getLewdnessLevel(triggerId)\n local sexualTraitNum = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.sexualTrait_Num, 0)) or 0\n\n local sexualTraitLimit = math.max(0, lewdnessLevel - 1)\n\n local sexualTraitFlag = 0\n\n if sexualTraitNum < sexualTraitLimit then\n sexualTraitFlag = 1\n \n elseif lewdnessLevel >= 11 then\n local corruptionValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.corruption, 0)) or 0\n local corruptionLevel = getCorruptionLevelByValue(corruptionValue)\n\n local corruptionTraitNum = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.corruptionTrait_Num, 0)) or 0\n local corruptionTraitLimit = math.max(0, corruptionLevel)\n\n if corruptionTraitNum < corruptionTraitLimit then\n sexualTraitFlag = 2\n end\n end\n\n local traitNames = {}\n local seenTraitNames = {}\n\n local function addTraitNamesFromContent(raw)\n for _, row in ipairs(splitPipe(raw)) do\n local name = trim((row:match(\"^(.-)%s*:\") or \"\"))\n name = trim(name:gsub(\"^▷%s*\", \"\"))\n\n if name ~= \"\" and not seenTraitNames[name] then\n seenTraitNames[name] = true\n table.insert(traitNames, name)\n end\n end\n end\n\n if sexualTraitFlag >= 1 then\n addTraitNamesFromContent(safeGet(triggerId, \"vg_SexualTrait_Content\", \"\"))\n addTraitNamesFromContent(safeGet(triggerId, \"vg_Corruption_Content\", \"\"))\n end\n\n setChatVar(triggerId, \"vg_Trait_List\", table.concat(traitNames, \", \"))\n setChatVar(triggerId, \"vg_SexualTrait_Flag\", tostring(sexualTraitFlag))\n\n -- ==========================================\n -- 결과\n -- ==========================================\n local resultStart = \"\"\n local resultFlag = 0\n\n local resultChoiceFlags = {\n [constChoice.choice_Daily] = true,\n [constChoice.choice_Interlude] = true,\n [constChoice.choice_Incapacitated] = true,\n [constChoice.choice_Unfree] = true,\n }\n\n if resultChoiceFlags[choiceFlag] then\n local history = getFullChat(triggerId) or {}\n local fullIndex = nil\n local resultDate = \"\"\n local resultTime = \"\"\n\n -- 최신 FullSave 찾기\n for i = #history, 1, -1 do\n local msg = history[i]\n\n if msg and msg.role == \"char\" then\n local text = tostring(msg.data or \"\")\n\n if text:find(\"<SaveFullData>\", 1, true) then\n fullIndex = i\n break\n end\n end\n end\n\n -- FullSave 다음의 첫 char 응답에서 날짜/시간 추출\n if fullIndex then\n for i = fullIndex + 1, #history do\n local msg = history[i]\n\n if msg and msg.role == \"char\" then\n local text = tostring(msg.data or \"\")\n local parsedDate, parsedTime = applyDateTimeFromStatusWindow(text)\n\n resultDate = trim(parsedDate or \"\")\n resultTime = trim(parsedTime or \"\")\n\n if resultDate ~= \"\" and resultTime ~= \"\" then\n resultStart = resultDate .. \" \" .. resultTime\n elseif resultDate ~= \"\" then\n resultStart = resultDate\n elseif resultTime ~= \"\" then\n resultStart = resultTime\n end\n\n if resultStart ~= \"\" then\n resultFlag = 1\n end\n\n break\n end\n end\n end\n end\n\n setChatVar(triggerId, \"vg_Result_Start\", resultStart)\n setChatVar(triggerId, \"vg_Result_Flag\", tostring(resultFlag))\n\n local resolutionFlag = constResolution.none\n\n if prevPhase == constPhase.interlude and value == constPhase.standby then\n resolutionFlag = constResolution.storyline\n elseif (prevPhase == constPhase.event_Defiant or prevPhase == constPhase.event_Yielding\n or prevPhase == constPhase.event_Defiant_Hypnosis or prevPhase == constPhase.event_Yielding_Hypnosis) and value == constPhase.unfree then\n resolutionFlag = constResolution.capture\n elseif (prevPhase == constPhase.escape or prevPhase == constPhase.rescue) and value == constPhase.daily then\n resolutionFlag = constResolution.freedom\n elseif (isIncapacitatedPhase(triggerId, prevPhase) or isPromiscuityPhase(triggerId, prevPhase) or isIntimacyPhase(triggerId, prevPhase) or isHEventPhase(triggerId, prevPhase))\n and (value == constPhase.unfree or value == constPhase.daily or value == constPhase.interlude) then\n resolutionFlag = constResolution.sessionSexualAct\n elseif (isIncapacitatedPhase(triggerId, value) or isPromiscuityPhase(triggerId, value) or isIntimacyPhase(triggerId, value) or isHEventPhase(triggerId, value)) then\n resolutionFlag = constResolution.majorSexualAct\n end\n\n if resolutionFlag == constResolution.capture or resolutionFlag == constResolution.sessionSexualAct then\n local history = getFullChat(triggerId) or {}\n local fullIndex = nil\n\n for i = #history, 1, -1 do\n local msg = history[i]\n\n if msg and msg.role == \"char\" and type(msg.data) == \"string\" then\n if msg.data:find(\"<SaveFullData>\", 1, true) then\n fullIndex = i\n break\n end\n end\n end\n\n local actList = {}\n local seen = {}\n\n if fullIndex then\n for i = fullIndex + 1, #history do\n local msg = history[i]\n\n if msg and msg.role == \"char\" and type(msg.data) == \"string\" then\n for body in msg.data:gmatch(\"<Major Sexual Act Tag>%s*([%s%S]-)%s*</Major Sexual Act Tag>\") do\n local recordRaw = trim(body or \"\")\n\n local bracketEntry = recordRaw:match(\"%b[]\")\n if bracketEntry then\n recordRaw = trim(bracketEntry:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\"))\n end\n\n local fields = parseOrderedFieldsWithFallback(recordRaw, { \"Major Sexual Acts\" })\n local actRaw = trim(fields[\"major sexual acts\"] or \"\")\n\n for _, act in ipairs(splitLiteral(actRaw, \",\")) do\n act = trim(act or \"\")\n\n if isMeaningfulValue(act) then\n local key = string.lower(act)\n\n if not seen[key] then\n seen[key] = true\n table.insert(actList, act)\n end\n end\n end\n end\n end\n end\n end\n\n setChatVar(triggerId, \"vg_Resolution_Act\", table.concat(actList, \", \"))\n else\n setChatVar(triggerId, \"vg_Resolution_Act\", \"\")\n end\n\n setChatVar(triggerId, \"vg_Resolution_Flag\", tostring(resolutionFlag))\nend\n\nfunction getPhase(triggerId)\n return tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.phase, 0)) or 0\nend\n\nfunction getCurrentPhase(triggerId)\n return tonumber(getDualFlag(triggerId, vgContext.varName, vgContext.phase, false)) or 0\nend\n\nfunction isCombatPhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n local allowed = {\n [constPhase.combat] = true,\n [constPhase.escape] = true,\n [constPhase.sleeping_Ambush] = true,\n [constPhase.entrapment] = true,\n [constPhase.brainwashing] = true,\n [constPhase.feral] = true,\n [constPhase.companion] = true,\n [constPhase.abduction] = true,\n }\n\n return allowed[phase] == true\nend\n\nfunction isHarassmentPhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n local allowed = {\n [constPhase.harassment] = true,\n [constPhase.abortion_Ambush] = true,\n [constPhase.aphrodisiac] = true,\n }\n\n return allowed[phase] == true\nend\n\nfunction isAbusePhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n local allowed = {\n [constPhase.abuse] = true,\n [constPhase.indoctrination] = true,\n }\n\n return allowed[phase] == true\nend\n\nfunction isConfrontationPhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n local allowed = {\n [constPhase.confrontation] = true,\n }\n\n return allowed[phase] == true\nend\n\nfunction isHypnosisPhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n local allowed = {\n [constPhase.hypnosis] = true,\n }\n\n return allowed[phase] == true\nend\n\nfunction isExhibitionismPhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n local allowed = {\n [constPhase.exhibitionism] = true,\n }\n\n return allowed[phase] == true\nend\n\nfunction isPerformancePhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n local allowed = {\n [constPhase.performance] = true,\n }\n\n return allowed[phase] == true\nend\n\nfunction isPromiscuityPhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n local allowed = {\n [constPhase.promiscuity] = true,\n }\n\n return allowed[phase] == true\nend\n\nfunction isIntimacyPhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n local allowed = {\n [constPhase.intimacy] = true,\n }\n\n return allowed[phase] == true\nend\n\nfunction isDefiantPhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n local allowed = {\n [constPhase.event_Defiant] = true,\n [constPhase.event_Defiant_Hypnosis] = true,\n [constPhase.event_Violence] = true,\n [constPhase.event_Sleeping] = true,\n [constPhase.event_Immobilized] = true,\n [constPhase.rape] = true,\n }\n\n return allowed[phase] == true\nend\n\nfunction isYieldingPhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n local allowed = {\n [constPhase.event_Yielding] = true,\n [constPhase.event_Yielding_Hypnosis] = true,\n [constPhase.event_Brainwashing] = true,\n [constPhase.event_Coercion] = true,\n [constPhase.event_Aphrodisiac] = true,\n [constPhase.transaction] = true,\n }\n\n return allowed[phase] == true\nend\n\nfunction isMasturbationPhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n local allowed = {\n [constPhase.masturbation_Interlude] = true,\n [constPhase.masturbation_Daily] = true,\n [constPhase.masturbation_Unfree] = true,\n [constPhase.masturbation_Story] = true,\n }\n\n return allowed[phase] == true\nend\n\nfunction isIncapacitatedPhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n return isDefiantPhase(triggerId, phase)\n or isYieldingPhase(triggerId, phase)\nend\n\nfunction isRescuePhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n local allowed = {\n [constPhase.rescue] = true,\n }\n\n return allowed[phase] == true\nend\n\nfunction isHEventPhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n local allowed = {\n [constPhase.event_H] = true,\n }\n\n return allowed[phase] == true\nend\n\nfunction isEncounterPhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n return isCombatPhase(triggerId, phase)\n or isHarassmentPhase(triggerId, phase)\n or isAbusePhase(triggerId, phase)\n or isConfrontationPhase(triggerId, phase)\n or isPerformancePhase(triggerId, phase)\n or isIncapacitatedPhase(triggerId, phase)\n or isExhibitionismPhase(triggerId, phase)\n or isPromiscuityPhase(triggerId, phase)\n or isHypnosisPhase(triggerId, phase)\n or isRescuePhase(triggerId, phase)\n or isIntimacyPhase(triggerId, phase)\nend\n\nfunction isDeckPhase(triggerId, phase)\n phase = tonumber(phase) or getPhase(triggerId)\n\n return isEncounterPhase(triggerId, phase)\n or isMasturbationPhase(triggerId, phase)\n or isHEventPhase(triggerId, phase)\nend\n\n-- ==========================================\n-- 프로필 생성\n-- ==========================================\nfunction getSexKnowledgeText(level)\n local sexKnowledgeTexts = {\n [0] = \"Knows nothing about sex or sexual matters.\",\n [1] = \"Has only minimal sexual knowledge, limited to a few vague or fragmentary ideas.\",\n [2] = \"Has basic sexual knowledge, limited to simple and general understanding.\",\n [3] = \"Has moderate sexual knowledge, including a practical understanding of sex, arousal, and related matters.\",\n [4] = \"Has broad sexual knowledge, covering a wide range of sexual matters with fairly solid understanding.\",\n [5] = \"Has extensive sexual knowledge, with detailed, nuanced, and well-developed understanding of sexual matters.\"\n }\n\n level = tonumber(level) or 0\n return sexKnowledgeTexts[level] or \"Has an undefined level of sexual knowledge.\"\nend\n\nlocal function buildSexualProfile(triggerId, baseDateText)\n baseDateText = normalizeLooseDateText(baseDateText)\n\n if not parseISODateText(baseDateText) then\n baseDateText = (select(1, getLatestCharDateTime(triggerId))) or \"\"\n end\n\n local lines = {}\n\n local function buildVirginityLine(label, rawValue, noneText, experiencedText)\n local v = trim(tostring(rawValue or \"\"))\n\n if v == \"\" or v == \"0\" or v == \"nil\" or v == \"null\" then\n return \"- \" .. label .. \": \" .. noneText\n end\n\n return \"- \" .. label .. \": \" .. experiencedText .. \" (\" .. v .. \")\"\n end\n\n local function buildSensitivityLine(label, startValue, currentValue)\n local startLevel = getSensitivityLevelByValue(startValue)\n local currentLevel = getSensitivityLevelByValue(currentValue)\n\n local startText = getSensitivityTextByLevel(startLevel)\n local currentText = getSensitivityTextByLevel(currentLevel)\n\n if startLevel == currentLevel then\n return \"- \" .. label .. \":\\n\"\n .. \" (Initial / Current) \" .. startText\n end\n\n return \"- \" .. label .. \":\\n\"\n .. \" (Initial) \" .. startText .. \"\\n\"\n .. \" (Current) \" .. currentText\n end\n\n local startKnowledge = tonumber(safeGet(triggerId, \"vg_startKnowledge\", \"0\")) or 0\n local sexKnowledge = tonumber(getResolvedDualFlag(triggerId, vgSexKnowledge.varName, vgSexKnowledge.level, 0)) or 0\n\n local firstKiss = safeGet(triggerId, \"vg_FirstKiss\", \"\")\n local firstMasturbation = safeGet(triggerId, \"vg_FirstMasturbation\", \"\")\n local firstOrgasm = safeGet(triggerId, \"vg_FirstOrgasm\", \"\")\n local firstFellatio = safeGet(triggerId, \"vg_FirstFellatio\", \"\")\n local firstVirgin = safeGet(triggerId, \"vg_FirstVirgin\", \"\")\n local firstAnal = safeGet(triggerId, \"vg_FirstAnal\", \"\")\n\n local startBreast = tonumber(safeGet(triggerId, \"vg_startBreast\", \"0\")) or 0\n local startOral = tonumber(safeGet(triggerId, \"vg_startOral\", \"0\")) or 0\n local startClitoris = tonumber(safeGet(triggerId, \"vg_startClitoris\", \"0\")) or 0\n local startVaginal = tonumber(safeGet(triggerId, \"vg_startVaginal\", \"0\")) or 0\n local startAnal = tonumber(safeGet(triggerId, \"vg_startAnal\", \"0\")) or 0\n\n local sensitivity = loadDualFlagCurrentMap(triggerId, vgSensitivity)\n local currentBreast = sensitivity.breast\n local currentOral = sensitivity.oral\n local currentClitoris = sensitivity.clitoris\n local currentVaginal = sensitivity.vaginal\n local currentAnal = sensitivity.anal\n\n table.insert(lines, \"### Sexual Profile\")\n\n local startKnowledgeText = getSexKnowledgeText(startKnowledge)\n local currentKnowledgeText = getSexKnowledgeText(sexKnowledge)\n\n local virginityText = \"Virgin\"\n if firstVirgin ~= \"\" then\n virginityText = \"Non-Virgin\"\n end\n table.insert(lines, \"- Virginity: \" .. virginityText)\n\n if startKnowledge == sexKnowledge then\n table.insert(lines, \"- Sexual Knowledge:\\n\"\n .. \" (Initial / Current) \" .. startKnowledgeText)\n else\n table.insert(lines, \"- Sexual Knowledge:\\n\"\n .. \" (Initial) \" .. startKnowledgeText .. \"\\n\"\n .. \" (Current) \" .. currentKnowledgeText)\n end\n\n local lewdnessLevel = getLewdnessLevel(triggerId)\n local lewdnessText = getLewdnessTextByLevel(lewdnessLevel)\n\n table.insert(lines, \"- Lewdness: \" .. lewdnessText)\n\n local function getReproductivePhaseInfo(flag)\n if flag == constCycle.menstruation then\n return \"Menstruation\", \"She is currently menstruating and may experience mild cramps, fatigue, or lower abdominal discomfort. Pregnancy risk is nonexistent.\", false\n\n elseif flag == constCycle.fertile_m5 then\n return \"Fertile Window (5 days before ovulation)\", \"Ovulation is expected in 5 days. There is an increasing probability of pregnancy.\", true\n\n elseif flag == constCycle.fertile_m4 then\n return \"Fertile Window (4 days before ovulation)\", \"Ovulation is expected in 4 days. There is an increasing probability of pregnancy.\", true\n\n elseif flag == constCycle.fertile_m3 then\n return \"Fertile Window (3 days before ovulation)\", \"Ovulation is expected in 3 days. There is an increasing probability of pregnancy.\", true\n\n elseif flag == constCycle.fertile_m2 then\n return \"Fertile Window (2 days before ovulation)\", \"Ovulation is approaching. The probability of pregnancy is moderately high.\", true\n\n elseif flag == constCycle.fertile_m1 then\n return \"Fertile Window (1 day before ovulation)\", \"Ovulation is imminent. The probability of pregnancy is very high.\", true\n\n elseif flag == constCycle.ovulation then\n return \"Fertile Window (Ovulation Day)\", \"Today is the ovulation day. The probability of pregnancy is at its highest.\", true\n\n elseif flag == constCycle.fertile_p1 then\n return \"Fertile Window (1 day after ovulation)\", \"Ovulation has recently occurred. The probability of pregnancy remains elevated.\", true\n\n end\n\n return \"Infertile Phase\", \"This is a safe period. There is no risk of pregnancy.\", false\n end\n\n local function appendReproductiveStatusLines(triggerId, lines, baseDateText)\n local lastPeriodRaw = trim(getResolvedDualFlag(triggerId, vgReproductive.varName, vgReproductive.lastPeriod, \"\"))\n local pregnancyDateRaw = trim(getResolvedDualFlag(triggerId, vgReproductive.varName, vgReproductive.pregnancyDate, \"\"))\n local fatherRaw = trim(getResolvedDualFlag(triggerId, vgReproductive.varName, vgReproductive.father, \"\"))\n local daysTotalRaw = trim(getResolvedDualFlag(triggerId, vgReproductive.varName, vgReproductive.daysTotal, \"0\"))\n\n local pregnancyDateTable = parseISODateText(pregnancyDateRaw)\n\n local currentDateText = tostring(baseDateText or \"\")\n local currentDateTable = parseISODateText(currentDateText)\n\n -- ==========================================\n -- 임신 상태\n -- ==========================================\n if pregnancyDateTable then\n\n local function getPregnancyEquivalentTextByProgress(progress)\n progress = clamp(tonumber(progress) or 0, 0, 1)\n\n if progress < 0.10 then\n return \"comparable to the earliest stage of human pregnancy\"\n elseif progress < 0.30 then\n return \"comparable to the early stage of human pregnancy\"\n elseif progress < 0.50 then\n return \"comparable to the early-to-middle stage of human pregnancy\"\n elseif progress < 0.70 then\n return \"comparable to the middle stage of human pregnancy\"\n elseif progress < 0.85 then\n return \"comparable to the middle-to-late stage of human pregnancy\"\n elseif progress < 0.95 then\n return \"comparable to the late stage of human pregnancy\"\n elseif progress < 1.00 then\n return \"comparable to a full-term stage of human pregnancy\"\n else\n return \"comparable to the imminent-delivery stage of human pregnancy\"\n end\n end\n\n local function getPregnancyPhysicalChangesLineByProgress(progress)\n progress = clamp(tonumber(progress) or 0, 0, 1)\n\n local equivalentText = getPregnancyEquivalentTextByProgress(progress)\n\n if progress < 0.10 then\n return \"No visible physical changes are apparent yet; outward signs of pregnancy are minimal or absent. (\" .. equivalentText .. \")\"\n elseif progress < 0.30 then\n return \"Early pregnancy symptoms may be present, but there is no visible abdominal change. (\" .. equivalentText .. \")\"\n elseif progress < 0.50 then\n return \"The abdomen shows gentle rounding consistent with early pregnancy. (\" .. equivalentText .. \")\"\n elseif progress < 0.70 then\n return \"The abdomen is noticeably enlarged, clearly indicating pregnancy. (\" .. equivalentText .. \")\"\n elseif progress < 0.85 then\n return \"The abdomen is prominently enlarged, reflecting late-stage pregnancy. (\" .. equivalentText .. \")\"\n elseif progress < 1.00 then\n return \"The abdomen is fully distended, indicating a near-term or full-term pregnancy. (\" .. equivalentText .. \")\"\n else\n return \"The abdomen is fully distended, and the pregnancy appears to be at the point where delivery may begin at any time. (\" .. equivalentText .. \")\"\n end\n end\n\n local function getPregnancyAwarenessLineByProgress(progress)\n progress = clamp(tonumber(progress) or 0, 0, 1)\n\n if sexKnowledge == 0 then\n if progress < 0.10 then\n return \"She has no idea that she is pregnant.\"\n elseif progress < 0.30 then\n return \"She notices that something is wrong with her body, but does not connect it to pregnancy.\"\n elseif progress < 0.50 then\n return \"She notices that her body is changing, but takes it as some other bodily change and does not understand what is happening to her.\"\n else\n return \"She learns that she is pregnant because others make it clear to her.\"\n end\n\n elseif sexKnowledge == 1 then\n if progress < 0.10 then\n return \"She has no idea that she is pregnant.\"\n elseif progress < 0.30 then\n return \"She notices that something is wrong with her body, but does not know the cause.\"\n elseif progress < 0.50 then\n return \"She notices that the changes in her body may have a serious cause, but does not recognize them as pregnancy.\"\n else\n return \"She realizes that she is pregnant after the signs become too clear to dismiss, or because others make it clear to her.\"\n end\n\n elseif sexKnowledge == 2 then\n if progress < 0.10 then\n return \"She does not realize that she is pregnant.\"\n elseif progress < 0.30 then\n return \"She suspects pregnancy.\"\n else\n return \"She knows that she is pregnant.\"\n end\n\n elseif sexKnowledge == 3 then\n if progress < 0.10 then\n return \"She considers pregnancy a possibility.\"\n elseif progress < 0.30 then\n return \"She strongly suspects pregnancy.\"\n else\n return \"She knows that she is pregnant.\"\n end\n\n elseif sexKnowledge == 4 then\n if progress < 0.10 then\n return \"She considers pregnancy a likely possibility.\"\n else\n return \"She knows that she is pregnant.\"\n end\n\n else\n if progress < 0.10 then\n return \"She considers pregnancy a likely possibility.\"\n else\n return \"She knows that she is pregnant.\"\n end\n end\n end\n\n local function buildGestationalAgeText(elapsedDays)\n elapsedDays = math.max(0, tonumber(elapsedDays) or 0)\n\n if elapsedDays < 7 then\n local dayWord = (elapsedDays == 1) and \"day\" or \"days\"\n return tostring(elapsedDays) .. \" \" .. dayWord\n end\n\n local weeks = math.floor(elapsedDays / 7)\n local weekWord = (weeks == 1) and \"week\" or \"weeks\"\n\n return formatFloat(weeks) .. \" \" .. weekWord\n end\n\n local function buildApproximateDateText(dateTable)\n if not dateTable then\n return \"Not set.\"\n end\n\n local y = tonumber(dateTable.year)\n local m = tonumber(dateTable.month)\n local d = tonumber(dateTable.day)\n\n if not y or not m or not d then\n return \"Not set.\"\n end\n\n local monthNames = {\n \"January\", \"February\", \"March\", \"April\", \"May\", \"June\",\n \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"\n }\n\n local phaseText = \"early\"\n\n if d >= 21 then\n phaseText = \"late\"\n elseif d >= 11 then\n phaseText = \"mid\"\n end\n\n return phaseText .. \" \" .. (monthNames[m] or \"Unknown\") .. \" \" .. tostring(y)\n end\n \n table.insert(lines, \"- Pregnancy Status: Pregnant\")\n table.insert(lines, \"- Pregnancy Date: \" .. pregnancyDateRaw)\n\n if fatherRaw ~= \"\" then\n table.insert(lines, \"- Father: \" .. fatherRaw)\n end\n\n local daysTotal = math.max(0, tonumber(daysTotalRaw) or 0)\n local elapsedDays = nil\n\n if currentDateTable then\n elapsedDays = diffDaysBetween(pregnancyDateTable, currentDateTable)\n\n if elapsedDays == nil or elapsedDays < 0 then\n elapsedDays = 0\n end\n end\n\n local expectedDeliveryDateTable = nil\n\n if daysTotal > 0 then\n expectedDeliveryDateTable = addDaysToDate(pregnancyDateTable, daysTotal)\n\n if expectedDeliveryDateTable then\n table.insert(lines, \"- Expected Delivery: \" .. buildApproximateDateText(expectedDeliveryDateTable))\n end\n end\n\n if elapsedDays ~= nil then\n table.insert(lines, \"- Gestational Age: \" .. buildGestationalAgeText(elapsedDays))\n else\n table.insert(lines, \"- Gestational Age: Not set.\")\n end\n\n if elapsedDays ~= nil and daysTotal > 0 then\n local progress = clamp(elapsedDays / daysTotal, 0, 1)\n\n table.insert(lines, \"- Physical Changes: \" .. getPregnancyPhysicalChangesLineByProgress(progress))\n table.insert(lines, \"- Pregnancy Awareness: \" .. getPregnancyAwarenessLineByProgress(progress))\n else\n table.insert(lines, \"- Physical Changes: Not set.\")\n table.insert(lines, \"- Pregnancy Awareness: Not set.\")\n end\n\n return true\n end\n\n -- ==========================================\n -- 비임신 상태\n -- ==========================================\n local lastPeriodDateTable = parseISODateText(lastPeriodRaw)\n\n if lastPeriodRaw == \"\" or not lastPeriodDateTable then\n return false\n end\n\n if not currentDateTable then\n return false\n end\n\n local flag = getCycleFlag(triggerId, currentDateText)\n if not flag then\n return false\n end\n\n local phaseText, statusText, showRiskResponse = getReproductivePhaseInfo(flag)\n\n table.insert(lines, \"- Pregnancy Status: Not Pregnant\")\n table.insert(lines, \"- Last Period: \" .. lastPeriodRaw)\n table.insert(lines, \"- Current Phase: \" .. phaseText)\n table.insert(lines, \"- Cycle Status: \" .. statusText)\n\n if showRiskResponse then\n table.insert(lines, \"- Pregnancy Risk Response: As she is currently in the fertile window, if a situation arises where intravaginal ejaculation is likely, she will display intense refusal and active resistance due to the risk of pregnancy, consciously or instinctively.\")\n end\n\n if showRiskResponse then\n local riskPrefix = \"As she is currently in the fertile window, if unwanted intravaginal ejaculation becomes likely, \"\n local riskBody = \"\"\n\n if sexKnowledge <= 1 then\n riskBody = \"she will display intense refusal and active resistance driven mainly by instinctive fear, bodily aversion, and an inarticulate sense of danger, even if she does not fully understand the reason.\"\n else\n riskBody = \"she will recognize the pregnancy risk and consciously try to prevent intravaginal ejaculation, showing intense refusal and active resistance.\"\n end\n\n table.insert(lines, \"- Pregnancy Risk Response: \" .. riskPrefix .. riskBody)\n end\n\n return true\n end\n\n do\n local sexualTraitRaw = trim(safeGet(triggerId, \"vg_SexualTrait_Content\", \"\"))\n\n if sexualTraitRaw ~= \"\" then\n table.insert(lines, \"\")\n table.insert(lines, \"#### Sexual Traits\")\n\n for _, traitText in ipairs(splitPipe(sexualTraitRaw)) do\n traitText = trim(traitText or \"\")\n\n if traitText ~= \"\" then\n table.insert(lines, \"- \" .. traitText)\n end\n end\n end\n end\n\n do\n local corruptionRaw = trim(safeGet(triggerId, \"vg_Corruption_Content\", \"\"))\n\n if corruptionRaw ~= \"\" then\n table.insert(lines, \"\")\n table.insert(lines, \"#### Corruption Traits\")\n\n for _, traitText in ipairs(splitPipe(corruptionRaw)) do\n traitText = trim(traitText or \"\")\n\n if traitText ~= \"\" then\n table.insert(lines, \"- \" .. traitText)\n end\n end\n end\n end\n\n do\n local reproductiveLines = {}\n\n if appendReproductiveStatusLines(triggerId, reproductiveLines, baseDateText) then\n table.insert(lines, \"\")\n\n if baseDateText ~= \"\" then\n table.insert(lines, \"#### Reproductive Status (\" .. baseDateText .. \")\")\n else\n table.insert(lines, \"#### Reproductive Status\")\n end\n\n for _, line in ipairs(reproductiveLines) do\n table.insert(lines, line)\n end\n end\n end\n\n table.insert(lines, \"\")\n table.insert(lines, \"#### Erogenous Zone Sensitivity\")\n table.insert(lines, buildSensitivityLine(\"Breast / Nipple\", startBreast, currentBreast))\n table.insert(lines, buildSensitivityLine(\"Lips / Tongue / Oral / Mouth / Throat\", startOral, currentOral))\n table.insert(lines, buildSensitivityLine(\"Clitoris\", startClitoris, currentClitoris))\n table.insert(lines, buildSensitivityLine(\"Vulva / Vaginal / Cervix / Womb\", startVaginal, currentVaginal))\n table.insert(lines, buildSensitivityLine(\"Buttock / Anal\", startAnal, currentAnal))\n\n table.insert(lines, \"\")\n table.insert(lines, \"#### Virginity\")\n table.insert(lines, buildVirginityLine(\"First Kiss\", firstKiss, \"No Experience\", \"Experienced\"))\n table.insert(lines, buildVirginityLine(\"First Masturbation\", firstMasturbation, \"No Experience\", \"Experienced\"))\n table.insert(lines, buildVirginityLine(\"First Orgasm\", firstOrgasm, \"No Experience\", \"Experienced\"))\n table.insert(lines, buildVirginityLine(\"First Fellatio\", firstFellatio, \"No Experience\", \"Experienced\"))\n table.insert(lines, buildVirginityLine(\"First Vaginal Sex\", firstVirgin, \"No Experience\", \"Experienced\"))\n table.insert(lines, buildVirginityLine(\"First Anal Sex\", firstAnal, \"No Experience\", \"Experienced\"))\n\n setChatVar(triggerId, \"vg_SexualProfile\", table.concat(lines, \"\\n\"))\nend\n\n-- ==========================================\n-- 현재 상태\n-- ==========================================\nfunction buildCurrentState(triggerId)\n local lines = {}\n local stateSettingLines = {}\n\n if tonumber(safeGet(triggerId, \"vg_Mode\", tostring(constMode.vg))) == constMode.free then\n setChatVar(triggerId, \"vg_State_Detail\", \"\")\n setChatVar(triggerId, \"vg_State_Setting\", \"\")\n return\n end\n\n local curPhase = getPhase(triggerId)\n local encounterPhase = isEncounterPhase(triggerId, curPhase)\n\n local harassmentPhase = isHarassmentPhase(triggerId, curPhase)\n local abusePhase = isAbusePhase(triggerId, curPhase)\n local confrontationPhase = isConfrontationPhase(triggerId, curPhase)\n\n local performancePhase = isPerformancePhase(triggerId, curPhase)\n\n local exhibitionismPhase = isExhibitionismPhase(triggerId, curPhase)\n\n local incapacitatedPhase = isIncapacitatedPhase(triggerId, curPhase) or isHEventPhase(triggerId, curPhase)\n\n local promiscuityPhase = isPromiscuityPhase(triggerId, curPhase)\n\n local intimacyPhase = isIntimacyPhase(triggerId, curPhase)\n\n local hypnosisPhase = isHypnosisPhase(triggerId, curPhase)\n\n local rescuePhase = isRescuePhase(triggerId, curPhase)\n\n local resistancePhase = harassmentPhase or abusePhase or confrontationPhase or hypnosisPhase\n\n local heroineSlots = getDualFlagSlotList(triggerId, vgHeroineStat.varName)\n local encounterSlots = getDualFlagSlotList(triggerId, vgEncounterStat.varName)\n\n local function buildStateLine(label, remainWord, upWord, downWord, currentText, nextText, currentValue, nextValue)\n if currentText == nextText then\n return \"- \" .. label .. \" \" .. remainWord .. \": \" .. nextText\n end\n\n if (tonumber(nextValue) or 0) > (tonumber(currentValue) or 0) then\n return \"- \" .. label .. \" \" .. upWord .. \": \" .. currentText .. \" → \" .. nextText\n else\n return \"- \" .. label .. \" \" .. downWord .. \": \" .. currentText .. \" → \" .. nextText\n end\n end\n\n local function getHPStateText(percent)\n\n if incapacitatedPhase == true then\n return \"Incapable of meaningful resistance.\"\n end\n\n if resistancePhase == true or rescuePhase == true then\n if percent >= 90 then\n return \"Firm and steady in her resistance.\"\n elseif percent >= 75 then\n return \"Strong-willed and steady.\"\n elseif percent >= 60 then\n return \"Composed and resistant.\"\n elseif percent >= 50 then\n return \"Steady and holding her ground.\"\n elseif percent >= 40 then\n return \"Slightly shaken, yet still resisting.\"\n elseif percent >= 30 then\n return \"Somewhat shaken, with minor lapses in resolve.\"\n elseif percent >= 20 then\n return \"Noticeably shaken, with wavering resistance.\"\n elseif percent >= 10 then\n return \"Badly shaken, with fading resolve.\"\n elseif percent > 0 then\n return \"Barely holding onto her will to resist.\"\n else\n return \"At her limit, on the verge of giving in.\"\n end\n end\n\n if percent >= 90 then\n return \"Sharp and stable.\"\n elseif percent >= 75 then\n return \"Clear-minded and steady.\"\n elseif percent >= 60 then\n return \"Composed and clear-headed.\"\n elseif percent >= 50 then\n return \"Steady and in control.\"\n elseif percent >= 40 then\n return \"Slightly shaken, yet steady.\"\n elseif percent >= 30 then\n return \"Somewhat shaken, with minor lapses in focus.\"\n elseif percent >= 20 then\n return \"Noticeably shaken, with wavering concentration.\"\n elseif percent >= 10 then\n return \"Badly shaken, with fading awareness.\"\n elseif percent > 0 then\n return \"Barely holding herself together.\"\n else\n return \"At her limit, on the verge of collapsing.\"\n end\n end\n\n local function getResourceStateText(percent)\n if percent >= 90 then\n return \"In full condition, with a body that feels light and ready for anything.\"\n elseif percent >= 75 then\n return \"In excellent shape, with almost no sense of fatigue.\"\n elseif percent >= 60 then\n return \"In good shape, with barely any fatigue in her body.\"\n elseif percent >= 50 then\n return \"Slightly tired, but with no meaningful limitation in movement.\"\n elseif percent >= 40 then\n return \"Noticeably fatigued, with a growing heaviness in her body.\"\n elseif percent >= 30 then\n return \"Clearly exhausted, moving with visible effort.\"\n elseif percent >= 20 then\n return \"Badly worn down, with sluggish movement and fading strength.\"\n elseif percent >= 10 then\n return \"Near collapse, pushing her body forward through sheer effort.\"\n elseif percent > 0 then\n return \"At the edge of physical exhaustion, but still able to force herself into motion.\"\n else\n return \"Completely spent, unable to move on her own.\"\n end\n end\n\n local function getArousalResponseExtraText(percent)\n local responseType = tonumber(safeGet(triggerId, \"vg_ArousalResponse\", \"1\")) or 1\n\n local responseMap = {\n [1] = \"A faint sense that something feels strange without understanding why.|Mild confusion at unfamiliar internal sensations.|Embarrassed awareness of reactions that feel improper.|Increasing self-consciousness about the body's response.|Unwanted arousal becoming difficult to dismiss.|Heat and tension lingering despite attempts to ignore it.|Strong awareness of sensations that feel deeply inappropriate.|Intense arousal causing mounting shame and confusion.|Overpowering bodily response conflicting with strong modesty.|Peak arousal leaving no way to ignore what is happening.\",\n [2] = \"A faint awareness of something intrusive and unwelcome.|Mild irritation at unfamiliar internal sensations.|Cold recognition of unwanted bodily response.|Controlled displeasure at reactions that refuse to stop.|Unwanted arousal provoking restrained hostility.|Persistent sensations met with rigid suppression.|Strong arousal contained through sheer will.|Very intense response clashing with unyielding resistance.|Overpowering sensations forcing cracks in strict composure.|Peak arousal overwhelming the body despite absolute rejection.\",\n [3] = \"A faint sensation noted but deemed irrelevant.|Barely noticeable warmth registered as a minor anomaly.|Subtle tension analyzed as an unexplained physiological response.|Mild arousal provoking focused attempts at suppression.|Noticeable stimulation treated as a problem to control.|Clear bodily response conflicting with rational composure.|Sustained arousal straining deliberate self-regulation.|Strong sensations disrupting analytical focus.|Very intense stimulation overwhelming cognitive control.|Peak arousal overrunning all restraint, rational thought collapsing entirely.\",\n [4] = \"Immediate irritation at something intrusive.|Annoyance at unfamiliar internal tension.|Angry denial of unwanted sensations.|Growing frustration at reactions that refuse to stop.|Unwanted arousal provoking sharp hostility.|Persistent sensations fueling stubborn resistance.|Strong arousal clashing with fierce refusal to acknowledge it.|Very intense response breaking through proud composure.|Overpowering sensations forcing involuntary honesty to slip through.|Peak arousal overwhelming pride, raw desire exposed despite rejection.\",\n [5] = \"Immediate contempt for something offensive.|Irritation at unwelcome internal tension.|Cold fury at an indecent bodily reaction.|Disgust at sensations deemed beneath dignity.|Unwanted arousal provoking burning indignation.|Persistent sensations met with domineering denial.|Strong arousal clashing with ruthless self-control.|Very intense response fueling humiliated rage.|Overpowering sensations tearing through imperious composure.|Peak arousal shattering dominance, raw vulnerability exposed against her will.\",\n [6] = \"A flicker of tension hidden behind forced confidence.|Mild arousal denied with showy bravado.|Unfamiliar warmth provoking uneasy self-awareness.|Growing stimulation shaking the practiced façade.|Noticeable arousal met with strained composure.|Clear bodily response becoming hard to dismiss.|Strong sensations exposing underlying inexperience.|Very intense arousal overwhelming false confidence.|Overpowering stimulation breaking through the defensive act.|Peak arousal collapsing the façade, raw vulnerability laid bare.\",\n [7] = \"A faint sense of something odd without concern.|Mild confusion at unfamiliar internal sensations.|Uncertain warmth noticed without understanding.|Growing discomfort that feels strangely puzzling.|Noticeable arousal causing flustered confusion.|Clear bodily response that is hard to interpret.|Strong sensations creating anxious bewilderment.|Very intense stimulation overwhelming simple composure.|Overpowering arousal causing panicked confusion.|Peak arousal flooding awareness, leaving only disoriented sensation.\",\n [8] = \"A faint sense of something strangely out of place.|Mild confusion at unfamiliar internal sensations.|Uncertain awareness of warmth that feels unreal.|Growing disorientation as sensations blur together.|Unwanted arousal perceived as oddly distant.|Persistent sensations distorting normal perception.|Strong arousal creating a sense of unreality.|Very intense response overwhelming coherent thought.|Overpowering sensations dissolving clear awareness.|Peak arousal consuming perception, reality fading under the flood of sensation.\",\n [9] = \"A faint discomfort noticed but ignored.|Mild tension met with quiet resignation.|Unwelcome warmth provoking uneasy self-awareness.|Growing stimulation bringing subdued distress.|Noticeable arousal becoming hard to ignore.|Clear bodily response causing quiet despair.|Strong sensations draining fragile composure.|Very intense arousal overwhelming passive resistance.|Overpowering stimulation crushing what little resolve remained.|Peak arousal flooding everything, leaving only helpless surrender to sensation.\",\n [10] = \"A faint spark of stimulation beneath irritation.|Mild arousal acknowledged with coarse amusement.|Unwanted warmth stirring a shameless awareness.|Growing stimulation met with brazen acceptance.|Noticeable arousal provoking heated excitement.|Clear bodily response embraced despite hostility.|Strong sensations fueling crude anticipation.|Very intense stimulation pushing toward reckless abandon.|Overpowering arousal drowning out restraint.|Peak arousal consuming the body, raw pleasure overwhelming everything.\"\n }\n\n if percent < 10 then\n return \"\"\n end\n\n local raw = responseMap[responseType] or responseMap[1]\n local parts = splitPipe(raw)\n local index = 1\n\n if percent >= 100 then\n index = 10\n elseif percent >= 95 then\n index = 9\n elseif percent >= 85 then\n index = 8\n elseif percent >= 75 then\n index = 7\n elseif percent >= 65 then\n index = 6\n elseif percent >= 55 then\n index = 5\n elseif percent >= 45 then\n index = 4\n elseif percent >= 35 then\n index = 3\n elseif percent >= 25 then\n index = 2\n else\n index = 1\n end\n\n return trim(parts[index] or \"\")\n end\n\n local function getArousalStateText(percent)\n local baseText = \"\"\n\n percent = tonumber(percent) or 0\n\n local corruptionValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.corruption, 0)) or 0\n local corruptionLevel = getCorruptionLevelByValue(corruptionValue)\n\n local settingByLevel = {\n [0] = { floor = 0, curve = 1.00 },\n\n [1] = { floor = 10, curve = 1.00 },\n [2] = { floor = 10, curve = 0.9 },\n\n [3] = { floor = 25, curve = 1.00 },\n [4] = { floor = 25, curve = 0.9 },\n\n [5] = { floor = 35, curve = 0.95 },\n }\n\n local setting = settingByLevel[corruptionLevel] or settingByLevel[5]\n\n local statePercent = percent\n\n -- 95 이상은 최종 상태라 건드리지 않음\n if percent < 95 then\n local floor = setting.floor\n local curve = setting.curve\n\n local ratio = clamp(percent, 0, 95) / 95\n statePercent = floor + (95 - floor) * (ratio ^ curve)\n statePercent = clamp(statePercent, 0, 95)\n end\n\n if statePercent >= 95 then\n baseText = \"Overwhelming physiological arousal, with body control greatly reduced.\"\n elseif statePercent >= 85 then\n baseText = \"Very strong physiological arousal; composure may waver.\"\n elseif statePercent >= 75 then\n baseText = \"Strong physiological response; movement slightly unsteady.\"\n elseif statePercent >= 65 then\n baseText = \"Sustained physiological arousal; posture visibly affected.\"\n elseif statePercent >= 55 then\n baseText = \"Clear physiological signs, such as steadier breathing changes.\"\n elseif statePercent >= 45 then\n baseText = \"Mild but noticeable physiological arousal.\"\n elseif statePercent >= 35 then\n baseText = \"Mild arousal; subtle physiological response; breathing may shift slightly.\"\n elseif statePercent >= 25 then\n baseText = \"Low arousal; very slight internal tension or warmth.\"\n elseif statePercent >= 10 then\n baseText = \"Minimal arousal; barely noticeable physiological response.\"\n else\n baseText = \"Not currently aroused; no notable physiological response is present.\"\n end\n\n local extraText = getArousalResponseExtraText(statePercent)\n\n if extraText == \"\" then\n return baseText\n end\n\n return baseText .. \" \" .. extraText\n end\n\n local currentHP = tonumber(getDualFlagFromSlotList(heroineSlots, vgHeroineStat.hp, false)) or 0\n local currentMaxHP = tonumber(getDualFlagFromSlotList(heroineSlots, vgHeroineStat.hp_Max, false)) or 0\n local currentResource = tonumber(getDualFlagFromSlotList(heroineSlots, vgHeroineStat.resource, false)) or 0\n local currentMaxResource = tonumber(getDualFlagFromSlotList(heroineSlots, vgHeroineStat.resource_Max, false)) or 0\n local currentArousal = tonumber(getDualFlagFromSlotList(heroineSlots, vgHeroineStat.arousal, false)) or 0\n local currentMaxArousal = tonumber(getDualFlagFromSlotList(heroineSlots, vgHeroineStat.arousal_Max, false)) or 0\n\n local nextHP = tonumber(getResolvedDualFlagFromSlotList(heroineSlots, vgHeroineStat.hp, currentHP)) or currentHP\n local nextMaxHP = tonumber(getResolvedDualFlagFromSlotList(heroineSlots, vgHeroineStat.hp_Max, currentMaxHP)) or currentMaxHP\n local nextResource = tonumber(getResolvedDualFlagFromSlotList(heroineSlots, vgHeroineStat.resource, currentResource)) or currentResource\n local nextMaxResource = tonumber(getResolvedDualFlagFromSlotList(heroineSlots, vgHeroineStat.resource_Max, currentMaxResource)) or currentMaxResource\n local nextArousal = tonumber(getResolvedDualFlagFromSlotList(heroineSlots, vgHeroineStat.arousal, currentArousal)) or currentArousal\n local nextMaxArousal = tonumber(getResolvedDualFlagFromSlotList(heroineSlots, vgHeroineStat.arousal_Max, currentMaxArousal)) or currentMaxArousal\n\n local currentHPPercent = getPercent(currentHP, currentMaxHP)\n local nextHPPercent = getPercent(nextHP, nextMaxHP)\n local currentResourcePercent = getPercent(currentResource, currentMaxResource)\n local nextResourcePercent = getPercent(nextResource, nextMaxResource)\n local currentArousalPercent = getPercent(currentArousal, currentMaxArousal)\n local nextArousalPercent = getPercent(nextArousal, nextMaxArousal)\n\n local currentHPText = getHPStateText(currentHPPercent)\n local nextHPText = getHPStateText(nextHPPercent)\n local currentResourceText = getResourceStateText(currentResourcePercent)\n local nextResourceText = getResourceStateText(nextResourcePercent)\n local currentArousalText = getArousalStateText(currentArousalPercent)\n local nextArousalText = getArousalStateText(nextArousalPercent)\n\n local showHeroineHPState =\n (curPhase ~= constPhase.transaction\n and performancePhase == false\n and exhibitionismPhase == false\n and promiscuityPhase == false)\n\n local showHeroineResourceState =\n (incapacitatedPhase == false\n and performancePhase == false\n and exhibitionismPhase == false\n and promiscuityPhase == false)\n\n local showHeroineArousalState = true\n\n if isDeckPhase(triggerId, curPhase) == false or isMasturbationPhase(triggerId, curPhase) then\n if (showHeroineHPState and nextHP > currentHP and nextHPText ~= currentHPText)\n or (showHeroineResourceState and nextResource > currentResource and nextResourceText ~= currentResourceText) then\n\n showHeroineHPState = false\n showHeroineResourceState = false\n end\n\n if showHeroineArousalState and nextArousalPercent < currentArousalPercent and nextArousalText ~= currentArousalText then\n showHeroineArousalState = false\n end\n end\n\n if showHeroineHPState then\n if incapacitatedPhase == true then\n table.insert(lines, buildStateLine(\"Heroine's ability to resist\", \"remains\", \"recovers\", \"worsens\", currentHPText, nextHPText, currentHP, nextHP))\n elseif resistancePhase == true or rescuePhase == true then\n table.insert(lines, buildStateLine(\"Heroine's will to resist\", \"remains\", \"recovers\", \"worsens\", currentHPText, nextHPText, currentHP, nextHP))\n else\n table.insert(lines, buildStateLine(\"Heroine's clarity\", \"remains\", \"recovers\", \"worsens\", currentHPText, nextHPText, currentHP, nextHP))\n end\n end\n\n if showHeroineResourceState then\n table.insert(lines, buildStateLine(\"Heroine's physical condition\", \"remains\", \"recovers\", \"worsens\", currentResourceText, nextResourceText, currentResource, nextResource))\n end\n\n if showHeroineArousalState then\n table.insert(lines, buildStateLine(\"Heroine's physiological response\", \"remains\", \"intensifies\", \"subsides\", currentArousalText, nextArousalText, currentArousal, nextArousal))\n end\n\n if encounterPhase == true then\n local currentEncounterHP = tonumber(getDualFlagFromSlotList(encounterSlots, vgEncounterStat.hp, false)) or 0\n local nextEncounterHP = tonumber(getResolvedDualFlagFromSlotList(encounterSlots, vgEncounterStat.hp, currentEncounterHP)) or currentEncounterHP\n local encounterMaxHP = tonumber(getDualFlagFromSlotList(encounterSlots, vgEncounterStat.hp_Max, false)) or 0\n\n local label = \"\"\n local desc = \"\"\n\n local displayCurrentValue = 0\n local displayNextValue = 0\n\n if resistancePhase == true or incapacitatedPhase == true or performancePhase == true or promiscuityPhase == true then\n displayCurrentValue = clamp(encounterMaxHP - currentEncounterHP, 0, encounterMaxHP)\n displayNextValue = clamp(encounterMaxHP - nextEncounterHP, 0, encounterMaxHP)\n else\n displayCurrentValue = clamp(currentEncounterHP, 0, encounterMaxHP)\n displayNextValue = clamp(nextEncounterHP, 0, encounterMaxHP)\n end\n\n if incapacitatedPhase == true then\n label = \"Encounter sexual fulfillment\"\n\n if curPhase == constPhase.transaction then\n desc = \"This value represents how far the encounter has progressed toward sexual fulfillment within the ongoing sexual transaction. Any value below 100% means the transaction is still ongoing and the encounter has not yet reached sexual fulfillment.\"\n else\n desc = \"This value represents how far the encounter has progressed toward sexual fulfillment with the incapacitated heroine. Any value below 100% means the heroine remains trapped in the situation and continues to be subjected to sexual violation.\"\n end\n elseif performancePhase == true then\n label = \"Encounter sexual fulfillment\"\n desc = \"This value represents how far the encounter has progressed toward sexual fulfillment through the heroine's ongoing sexual performance. Any value below 100% means the performance is still ongoing and the encounter has not yet reached sexual fulfillment.\"\n elseif hypnosisPhase == true then\n label = \"Progress toward breaking free from the encounter's mental influence\"\n desc = \"This value represents how far the heroine has progressed toward breaking free from the encounter's mental influence, such as hypnosis, brainwashing, mind control, or mental manipulation. Any value below 100% means she remains under that influence and the threat of sexual coercion or violation through it is still present.\"\n elseif resistancePhase == true then\n label = \"Progress toward breaking free from the current situation\"\n desc = \"This value represents how far the heroine has progressed toward breaking free from the current situation. Any value below 100% means she has not yet escaped, the current situation continues, and the threat of sexual violation is still present.\"\n elseif exhibitionismPhase == true then\n label = \"Progress toward the heroine's exhibitionistic fulfillment\"\n desc = \"This value represents how far the heroine has progressed toward the thrill and satisfaction she seeks through her current exhibitionistic behavior under the risk of being noticed. Any value below 100% means the current exhibitionistic situation is still ongoing and that exhibitionistic fulfillment has not yet been fully reached.\"\n elseif promiscuityPhase == true then\n label = \"Sexual fulfillment of the heroine and the encounter\"\n desc = \"This value represents progress toward sexual fulfillment for both sides. Any value below 100% means the situation is still ongoing.\"\n else\n label = \"Overall encounter presence in the current situation\"\n desc = \"This value represents the overall remaining encounter presence in the current situation, whether it takes the form of a single entity, multiple hostile presences, a blocking object, an environmental hazard, or other opposition.\"\n end\n\n local currentText = toPercentText(displayCurrentValue, encounterMaxHP)\n local nextText = toPercentText(displayNextValue, encounterMaxHP)\n\n local function fixEncounterPercentText(rawValue, maxValue)\n rawValue = tonumber(rawValue) or 0\n maxValue = tonumber(maxValue) or 0\n\n if maxValue <= 0 then\n return \"0%\"\n end\n\n if rawValue <= 0 then\n return \"0%\"\n end\n\n if rawValue >= maxValue then\n return \"100%\"\n end\n\n local pct = roundHalfUp((rawValue / maxValue) * 100)\n pct = clamp(pct, 1, 99)\n\n return tostring(pct) .. \"%\"\n end\n\n currentText = fixEncounterPercentText(displayCurrentValue, encounterMaxHP)\n nextText = fixEncounterPercentText(displayNextValue, encounterMaxHP)\n\n if currentText == nextText then\n table.insert(lines, \"- \" .. label .. \": \" .. nextText .. \" (\" .. desc .. \")\")\n else\n table.insert(lines, \"- \" .. label .. \": \" .. currentText .. \" → \" .. nextText .. \" (\" .. desc .. \")\")\n end\n end\n\n if nextResourcePercent >= 10 and showHeroineResourceState then\n table.insert(stateSettingLines, \"- Heroine's physical condition must remain within the level stated under 'Heroine's physical condition' in the Current State Details below. Any fluctuation must settle back to that level by the end of the response.\")\n end\n\n if showHeroineArousalState and nextArousalPercent < 95 then\n table.insert(stateSettingLines, \"- Regardless of any stimuli the heroine receives, during this response the heroine must not exhibit physiological arousal or reactions exceeding the level stated under 'Heroine's physiological response' in the Current State Details below. The heroine's rational faculties must not collapse at this stage; momentary fluctuations are permissible, but she must promptly stabilize at or below that level.\")\n end\n\n if encounterPhase == true then\n if curPhase == constPhase.transaction then\n table.insert(stateSettingLines, \"- In this response, the encounter must not reach sexual fulfillment or complete the ongoing transaction.\")\n elseif incapacitatedPhase == true then\n table.insert(stateSettingLines, \"- In this response, the encounter must not reach sexual fulfillment or finish with the incapacitated heroine.\")\n elseif performancePhase == true then\n table.insert(stateSettingLines, \"- In this response, the encounter must not reach sexual fulfillment, and the performance must not end or be brought to completion.\")\n elseif hypnosisPhase == true then\n table.insert(stateSettingLines, \"- In this response, the heroine must remain under the encounter's mental influence and must not break free from it.\")\n elseif resistancePhase == true then\n table.insert(stateSettingLines, \"- The heroine must not be depicted as fully breaking free from the current situation during this response.\")\n elseif exhibitionismPhase == true then\n table.insert(stateSettingLines, \"- In this response, the heroine must not yet reach full exhibitionistic fulfillment, and the current exhibitionistic situation must not end, resolve, or be brought to completion.\")\n elseif promiscuityPhase == true then\n table.insert(stateSettingLines, \"- In this response, neither the heroine nor the encounter must yet reach full sexual fulfillment, and the current sexual situation must not end, resolve, or be brought to completion.\")\n else\n table.insert(stateSettingLines, \"- The encounter must not be depicted as fully overcome during this response. The narrative depiction of the encounter must remain consistent with the percentage stated under 'Overall encounter presence in the current situation' in the Current State Details below. Any fluctuation is acceptable, but by the end of the response the encounter must align with that percentage.\")\n end\n end\n\n setChatVar(triggerId, \"vg_State_Detail\", table.concat(lines, \"\\n\"))\n setChatVar(triggerId, \"vg_State_Setting\", table.concat(stateSettingLines, \"\\n\"))\n\n if tonumber(safeGet(triggerId, \"vg_MoanDirective\", \"0\")) == 1 and nextArousalPercent >= 100 then\n local moanWords = {\n \"간다\",\n \"가버려\",\n \"찌릿찌릿해\",\n \"죽어버려\",\n \"정신 나가\",\n \"뜨거워\",\n \"머릿속 하얘져\",\n \"떨려\",\n \"멈추질 않아\",\n \"한계야\",\n \"힘 빠져\",\n \"이상해져\",\n \"생각 안 나\",\n \"터질 것 같아\",\n \"망가져\",\n \"미쳐\",\n \"이젠 안 돼\",\n \"싫어\",\n \"부서져\",\n \"녹아버려\",\n \"더는 무리\",\n \"큥큥거려\",\n \"못 참아\",\n \"앞이 깜깜해져\",\n \"제발 그만\",\n \"용서해 줘\",\n \"좋아\",\n \"못 버텨\",\n \"바보가 돼버려\",\n \"참을 수 없어\",\n \"미칠 것 같아\",\n \"눈 앞이 번쩍거려\",\n \"기분 좋아져버려\",\n }\n\n shuffleArray(moanWords)\n setChatVar(triggerId, \"vg_MoanWord\", \" (e.g., \" .. moanWords[1] .. \", \" .. moanWords[2] .. \")\")\n else\n setChatVar(triggerId, \"vg_MoanWord\", \"\")\n end\nend\n\n-- ==========================================\n-- 메모리\n-- ==========================================\nfunction buildMemoryDisplay(triggerId)\n local function buildDisplay(contentVarName, displayVarName)\n local raw = trim(safeGet(triggerId, contentVarName, \"\"))\n\n if raw == \"\" then\n setChatVar(triggerId, displayVarName, \"\")\n return\n end\n\n local lines = {}\n local entries = splitPipe(raw)\n\n for _, entryRaw in ipairs(entries) do\n local parts = splitSection(entryRaw)\n\n local dateText = trim(parts[1] or \"\")\n local milestoneText = trim(parts[2] or \"\")\n local contentText = trim(parts[3] or \"\")\n\n if dateText ~= \"\" and contentText ~= \"\" then\n if milestoneText ~= \"\" then\n table.insert(lines, \"- (\" .. dateText .. \") \" .. milestoneText .. \": \" .. contentText)\n else\n table.insert(lines, \"- (\" .. dateText .. \") \" .. contentText)\n end\n end\n end\n\n setChatVar(triggerId, displayVarName, table.concat(lines, \"\\n\"))\n end\n\n buildDisplay(\"vg_Memory_Content\", \"vg_Memory_Display\")\n buildDisplay(\"vg_Story_Content\", \"vg_Story_Display\")\nend\n\n-- ==========================================\n-- 탭 디스플레이\n-- ==========================================\nlocal function setGoalValue(triggerId, key, titleText)\n local current = trim(safeGet(triggerId, key, \"\"))\n local message = titleText .. \" 입력\"\n\n if current ~= \"\" then\n message = message .. \"\\n\\n현재 입력값: \" .. current\n end\n\n local input = alertInput(triggerId, message):await()\n\n if input and input ~= \"\" then\n input = trim(input)\n\n setChatVar(triggerId, key, input)\n\n generateHTML(triggerId)\n reloadDisplay(triggerId)\n\n alertNormal(triggerId, \"✅ \" .. titleText .. \" 수정 완료\")\n end\nend\n\nsetLongGoal = async(function(triggerId)\n setGoalValue(triggerId, \"vg_Goal_Long\", \"최종 목표\")\nend)\n\ndeleteLongGoal = async(function(triggerId)\n setChatVar(triggerId, \"vg_Goal_Long\", \"\")\n generateHTML(triggerId)\n reloadDisplay(triggerId)\nend)\n\nsetStoryDirective = async(function(triggerId)\n setGoalValue(triggerId, \"vg_Story_Directive\", \"스토리 가이드\")\nend)\n\ndeleteStoryDirective = async(function(triggerId)\n setChatVar(triggerId, \"vg_Story_Directive\", \"\")\n generateHTML(triggerId)\n reloadDisplay(triggerId)\nend)\n\nsetStoryCanon = async(function(triggerId)\n setGoalValue(triggerId, \"vg_Story_Canon\", \"원작\")\nend)\n\ndeleteStoryCanon = async(function(triggerId)\n setChatVar(triggerId, \"vg_Story_Canon\", \"\")\n generateHTML(triggerId)\n reloadDisplay(triggerId)\nend)\n\nfunction generateHTML(triggerId)\n local function escapeHtml(text)\n text = tostring(text or \"\")\n text = text:gsub(\"&\", \"&\")\n text = text:gsub(\"<\", \"<\")\n text = text:gsub(\">\", \">\")\n text = text:gsub('\"', \""\")\n text = text:gsub(\"'\", \"'\")\n return text\n end\n\n local function renderVGPanelText(text)\n text = tostring(text or \"\")\n text = text:gsub(\"\\r\\n\", \"\\n\")\n text = text:gsub(\"\\r\", \"\\n\")\n\n local out = {}\n\n for line in text:gmatch(\"([^\\n]*)\\n?\") do\n local t = trim(line)\n\n if t:match(\"^####%s+\") then\n table.insert(out, \"▶ \" .. trim(t:gsub(\"^####%s+\", \"\", 1)))\n\n elseif t:match(\"^%-%s+\") then\n table.insert(out, \"▷ \" .. trim(t:gsub(\"^%-%s+\", \"\", 1)))\n\n elseif t:match(\"^%(\") then\n table.insert(out, \" \" .. t)\n\n else\n table.insert(out, t)\n end\n end\n\n local result = table.concat(out, \"\\n\")\n\n -- 연속 빈 줄 정리\n result = result:gsub(\"\\n\\n\\n+\", \"\\n\\n\")\n\n -- bullet 사이 빈 줄 제거\n result = result:gsub(\"(▶ [^\\n]*)\\n\\n+(▶ )\", \"%1\\n%2\")\n\n return escapeHtml(result):gsub(\"\\n\", \"<br>\")\n end\n\n local function buildPreBlock(text, emptyText)\n text = tostring(text or \"\")\n\n if trim(text) == \"\" then\n return '<div class=\"vg-empty\">' .. escapeHtml(emptyText or \"표시할 내용이 없습니다.\") .. '</div>'\n end\n\n return '<div class=\"vgtab-rich-text\">' .. renderVGPanelText(text) .. '</div>'\n end\n\n local function buildSectionHtml(title, innerHtml)\n return\n '<div class=\"vgtab-section\">' ..\n '<div class=\"vgtab-section-title\">' .. escapeHtml(title) .. '</div>' ..\n innerHtml ..\n '</div>'\n end\n\n local function buildDeckListHtml()\n local groupedMap = {}\n local typeTotals = {}\n\n local typePriority = {\n [constCardType.primary] = 1,\n [constCardType.breaker] = 2,\n [constCardType.counter] = 3,\n [constCardType.milking] = 4,\n\n [constCardType.secondary] = 11,\n [constCardType.taunt] = 12,\n [constCardType.temptation] = 13,\n\n [constCardType.focus] = 21,\n [constCardType.charge] = 22,\n [constCardType.heal] = 23,\n [constCardType.soothe] = 24,\n [constCardType.ecstasy] = 25,\n\n [constCardType.surrender] = 999,\n }\n\n for _, cardRaw in ipairs(splitPipe(safeGet(triggerId, \"vg_Deck_Base\", \"\"))) do\n local parts = splitSection(cardRaw)\n local typeCode = tonumber(parts[2]) or 0\n local level = tonumber(parts[3]) or 1\n\n -- 기존 카드 박스용: 타입+레벨 묶음\n local key = tostring(typeCode) .. \"§\" .. tostring(level)\n if not groupedMap[key] then\n groupedMap[key] = {\n typeCode = typeCode,\n level = level,\n count = 0,\n }\n end\n groupedMap[key].count = groupedMap[key].count + 1\n\n -- 새 상단 요약용: 타입만 묶음\n if typeCode ~= 0 then\n typeTotals[typeCode] = (typeTotals[typeCode] or 0) + 1\n end\n end\n\n local groupedList = {}\n for _, info in pairs(groupedMap) do\n table.insert(groupedList, info)\n end\n\n table.sort(groupedList, function(a, b)\n local aPriority = typePriority[a.typeCode] or 9999\n local bPriority = typePriority[b.typeCode] or 9999\n\n if aPriority ~= bPriority then\n return aPriority < bPriority\n end\n\n if a.level ~= b.level then\n return a.level > b.level\n end\n\n return (a.typeCode or 0) < (b.typeCode or 0)\n end)\n\n local summaryList = {}\n for typeCode, totalCount in pairs(typeTotals) do\n if totalCount >= 1 then\n table.insert(summaryList, {\n typeCode = tonumber(typeCode) or 0,\n count = totalCount,\n })\n end\n end\n\n table.sort(summaryList, function(a, b)\n local aPriority = typePriority[a.typeCode] or 9999\n local bPriority = typePriority[b.typeCode] or 9999\n\n if aPriority ~= bPriority then\n return aPriority < bPriority\n end\n\n return (a.typeCode or 0) < (b.typeCode or 0)\n end)\n\n local summaryItems = {}\n for _, info in ipairs(summaryList) do\n local emoji = getCardTypeEmoji(triggerId, info.typeCode, constPhase.inspect)\n table.insert(\n summaryItems,\n '<span class=\"vgdeck-summary-item\">' ..\n escapeHtml(emoji) .. 'x' .. tostring(info.count) ..\n '</span>'\n )\n end\n\n local items = {}\n for _, info in ipairs(groupedList) do\n local emoji = getCardTypeEmoji(triggerId, info.typeCode, constPhase.inspect)\n local stars = getCardLevelStars(info.level)\n local countText = \"x\" .. tostring(info.count)\n\n table.insert(\n items,\n '<div class=\"vgdeck-card\">' ..\n '<div class=\"vgdeck-top\">' ..\n '<span class=\"vgdeck-emoji\">' .. escapeHtml(emoji) .. '</span>' ..\n '<span class=\"vgdeck-count\">' .. escapeHtml(countText) .. '</span>' ..\n '</div>' ..\n '<div class=\"vgdeck-stars\">' .. escapeHtml(stars) .. '</div>' ..\n '</div>'\n )\n end\n\n if #items <= 0 then\n return '<div class=\"vg-empty\">덱이 비어 있습니다.</div>'\n end\n\n local summaryHtml = \"\"\n if #summaryItems > 0 then\n summaryHtml =\n '<div class=\"vgdeck-summary\">' ..\n table.concat(summaryItems, \" \") ..\n '</div>'\n end\n\n return summaryHtml .. '<div class=\"vgdeck-grid\">' .. table.concat(items, \"\") .. '</div>'\n end\n\n local function buildEnhancementHtml()\n local function parseEffectText(effectRaw)\n local codeStr, valueStr = trim(tostring(effectRaw or \"\")):match(\"^([%-+]?%d+)%s*:%s*([%-+]?[%d%.]+)$\")\n local code = tonumber(codeStr) or 0\n local value = tonumber(valueStr) or 0\n return buildEnhancementEffectText(code, value)\n end\n\n local function buildOne(slotEmoji, slotNamePos, slotEffect1Pos, slotEffect2Pos)\n local name = trim(getResolvedDualFlag(triggerId, vgEnhancement.varName, slotNamePos, \"\"))\n local effectRaw1 = trim(getResolvedDualFlag(triggerId, vgEnhancement.varName, slotEffect1Pos, \"\"))\n local effectRaw2 = trim(getResolvedDualFlag(triggerId, vgEnhancement.varName, slotEffect2Pos, \"\"))\n\n if name == \"\" then\n return slotEmoji .. \" 없음\"\n end\n\n local effectTexts = {}\n\n local text1 = parseEffectText(effectRaw1)\n if text1 ~= \"\" then\n table.insert(effectTexts, text1)\n end\n\n local text2 = parseEffectText(effectRaw2)\n if text2 ~= \"\" then\n table.insert(effectTexts, text2)\n end\n\n local summaryText = table.concat(effectTexts, \" \")\n if summaryText == \"\" then\n summaryText = \"없음\"\n end\n\n return slotEmoji .. \" \" .. name .. \": \" .. summaryText\n end\n\n local text =\n buildOne(\"1️⃣\", vgEnhancement.slot1_Name, vgEnhancement.slot1_Effect1, vgEnhancement.slot1_Effect2) ..\n \"\\n\" ..\n buildOne(\"2️⃣\", vgEnhancement.slot2_Name, vgEnhancement.slot2_Effect1, vgEnhancement.slot2_Effect2) ..\n \"\\n\" ..\n buildOne(\"3️⃣\", vgEnhancement.slot3_Name, vgEnhancement.slot3_Effect1, vgEnhancement.slot3_Effect2)\n\n return '<div class=\"vgenh-text\">' .. escapeHtml(text):gsub(\"\\n\", \"<br>\") .. '</div>'\n end\n\n local function buildLewdnessHtml()\n local lewdnessValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.lewdness, 0)) or 0\n local level = getLewdnessLevelByValue(lewdnessValue)\n\n local currentMin = tonumber((lewdnessRanges[level] or {}).min) or 0\n local nextMin = tonumber((lewdnessRanges[level + 1] or {}).min)\n\n local gaugePercent = 100\n local progressText = \"MAX\"\n local levelText = tostring(level)\n\n if nextMin then\n local currentExp = math.max(0, lewdnessValue - currentMin)\n local needExp = math.max(1, nextMin - currentMin)\n\n gaugePercent = clamp((currentExp / needExp) * 100, 0, 100)\n progressText = tostring(currentExp) .. \" / \" .. tostring(needExp)\n else\n levelText = \"MAX\"\n gaugePercent = 100\n progressText = \"MAX\"\n end\n\n local items =\n '<div class=\"vgsense-item\">' ..\n '<div class=\"vgsense-head\">' ..\n '<div class=\"vgsense-label\">' ..\n '음란도 <span class=\"vgsense-level\">Lv.' .. escapeHtml(levelText) .. '</span>' ..\n '</div>' ..\n '<div class=\"vgsense-value\">' .. escapeHtml(progressText) .. '</div>' ..\n '</div>' ..\n '<div class=\"vgsense-bar\">' ..\n '<div class=\"vgsense-fill\" style=\"width:' .. formatFloat(gaugePercent) .. '%;\"></div>' ..\n '</div>' ..\n '</div>'\n\n if level >= 11 then\n local corruptionValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.corruption, 0)) or 0\n local corruptionLevel, corruptionCurrentExp, corruptionNeedExp, corruptionIsMax = getCorruptionProgressInfo(corruptionValue)\n\n local corruptionGaugePercent = 100\n local corruptionProgressText = \"MAX\"\n local corruptionLevelText = tostring(corruptionLevel)\n\n if not corruptionIsMax then\n corruptionGaugePercent = clamp((corruptionCurrentExp / corruptionNeedExp) * 100, 0, 100)\n corruptionProgressText = tostring(corruptionCurrentExp) .. \" / \" .. tostring(corruptionNeedExp)\n else\n corruptionLevelText = \"MAX\"\n corruptionGaugePercent = 100\n corruptionProgressText = \"MAX\"\n end\n\n items = items ..\n '<div class=\"vgsense-item\">' ..\n '<div class=\"vgsense-head\">' ..\n '<div class=\"vgsense-label\">' ..\n '타락 <span class=\"vgsense-level\">Lv.' .. escapeHtml(corruptionLevelText) .. '</span>' ..\n '</div>' ..\n '<div class=\"vgsense-value\">' .. escapeHtml(corruptionProgressText) .. '</div>' ..\n '</div>' ..\n '<div class=\"vgsense-bar\">' ..\n '<div class=\"vgsense-fill\" style=\"width:' .. formatFloat(corruptionGaugePercent) .. '%;\"></div>' ..\n '</div>' ..\n '</div>'\n end\n\n return\n '<div class=\"vgsense-list\">' ..\n items ..\n '</div>'\n end\n\n local function buildSensitivityGaugeHtml(label, value)\n value = tonumber(value) or 0\n\n local level = getSensitivityLevelByValue(value)\n local currentMin = tonumber((sensitivityRanges[level] or {}).min) or 0\n local nextMin = tonumber((sensitivityRanges[level + 1] or {}).min)\n\n local gaugePercent = 100\n local progressText = \"MAX\"\n local levelText = tostring(level)\n\n if nextMin then\n local currentExp = math.max(0, value - currentMin)\n local needExp = math.max(1, nextMin - currentMin)\n\n gaugePercent = clamp((currentExp / needExp) * 100, 0, 100)\n progressText = tostring(currentExp) .. \" / \" .. tostring(needExp)\n else\n levelText = \"MAX\"\n gaugePercent = 100\n progressText = \"MAX\"\n end\n\n return\n '<div class=\"vgsense-item\">' ..\n '<div class=\"vgsense-head\">' ..\n '<div class=\"vgsense-label\">' ..\n escapeHtml(label) ..\n ' <span class=\"vgsense-level\">Lv.' .. escapeHtml(levelText) .. '</span>' ..\n '</div>' ..\n '<div class=\"vgsense-value\">' .. escapeHtml(progressText) .. '</div>' ..\n '</div>' ..\n '<div class=\"vgsense-bar\">' ..\n '<div class=\"vgsense-fill\" style=\"width:' .. formatFloat(gaugePercent) .. '%;\"></div>' ..\n '</div>' ..\n '</div>'\n end\n\n local function buildSensitivityHtml()\n local sensitivity = loadDualFlagCurrentMap(triggerId, vgSensitivity)\n\n local items = {\n buildSensitivityGaugeHtml(\"가슴\", sensitivity.breast),\n buildSensitivityGaugeHtml(\"구강\", sensitivity.oral),\n buildSensitivityGaugeHtml(\"클리토리스\", sensitivity.clitoris),\n buildSensitivityGaugeHtml(\"질\", sensitivity.vaginal),\n buildSensitivityGaugeHtml(\"항문\", sensitivity.anal),\n }\n\n return '<div class=\"vgsense-list\">' .. table.concat(items, \"\") .. '</div>'\n end\n\n local function buildGoalHtml()\n local shortGoal = trim(safeGet(triggerId, \"vg_Goal_Short\", \"\"))\n local longGoal = trim(safeGet(triggerId, \"vg_Goal_Long\", \"\"))\n\n if shortGoal == \"\" then\n shortGoal = \"없음\"\n end\n\n if longGoal == \"\" then\n longGoal = \"없음\"\n end\n\n return\n '<div class=\"vggoal-list\">' ..\n\n '<div class=\"vggoal-row\">' ..\n '<span class=\"vggoal-label\">현재 목표</span>' ..\n '<span class=\"vggoal-value\">' .. escapeHtml(shortGoal) .. '</span>' ..\n '</div>' ..\n\n '<div class=\"vggoal-row\">' ..\n '<span class=\"vggoal-label\">최종 목표</span>' ..\n '<span class=\"vggoal-value\">' .. escapeHtml(longGoal) .. '</span>' ..\n '<button class=\"vggoal-btn\" risu-trigger=\"setLongGoal\">변경</button>' ..\n '<button class=\"vggoal-btn vggoal-btn-delete\" risu-trigger=\"deleteLongGoal\">삭제</button>' ..\n '</div>' ..\n\n '</div>'\n end\n\n local function buildStoryHtml()\n local storyDirective = trim(safeGet(triggerId, \"vg_Story_Directive\", \"\"))\n local storyCanon = trim(safeGet(triggerId, \"vg_Story_Canon\", \"\"))\n\n if storyDirective == \"\" then\n storyDirective = \"OFF\"\n end\n\n if storyCanon == \"\" then\n storyCanon = \"OFF\"\n end\n\n return\n '<div class=\"vggoal-list\">' ..\n\n '<div class=\"vggoal-row\">' ..\n '<span class=\"vggoal-label\">원작 스토리 따라가기</span>' ..\n '<span class=\"vggoal-value\">' .. escapeHtml(storyCanon) .. '</span>' ..\n '<button class=\"vggoal-btn\" risu-trigger=\"setStoryCanon\">변경</button>' ..\n '<button class=\"vggoal-btn vggoal-btn-delete\" risu-trigger=\"deleteStoryCanon\">삭제</button>' ..\n '</div>' ..\n\n '<div class=\"vggoal-row\">' ..\n '<span class=\"vggoal-label\">스토리 가이드</span>' ..\n '<span class=\"vggoal-value\">' .. escapeHtml(storyDirective) .. '</span>' ..\n '<button class=\"vggoal-btn\" risu-trigger=\"setStoryDirective\">변경</button>' ..\n '<button class=\"vggoal-btn vggoal-btn-delete\" risu-trigger=\"deleteStoryDirective\">삭제</button>' ..\n '</div>' ..\n\n '</div>'\n end\n\n function buildSettingPanelContent(triggerId)\n local parts = {}\n\n local selectedKey = trim(safeGet(triggerId, \"vg_Choice_Always\", \"\"))\n\n table.insert(parts, '<div class=\"vgsetting-section\">')\n table.insert(parts, '<div class=\"vgsetting-title\">항상 포함할 이벤트 (조건 충족 시에만 작동)</div>')\n table.insert(parts, '<div class=\"vgsetting-event-grid\">')\n\n for _, def in ipairs(getChoiceTypeDefs(triggerId)) do\n local key = trim(tostring(def.key or \"\"))\n\n if key ~= \"\" then\n local typeCode = parseEncounterTypeCode(key)\n local emoji = getEncounterEmojiByType(typeCode)\n\n local activeClass = \"\"\n if selectedKey == key then\n activeClass = \" vgsetting-event-btn-active\"\n end\n\n table.insert(parts,\n '<button class=\"vgsetting-event-btn' .. activeClass ..\n '\" title=\"' .. escapeHtml(key) ..\n '\" risu-btn=\"choice_toggleAlways_' .. escapeHtml(key) .. '\">' ..\n escapeHtml(emoji) ..\n '</button>'\n )\n end\n end\n\n table.insert(parts, '</div>')\n table.insert(parts, '</div>')\n\n local selectedChallengeHP = tonumber(safeGet(triggerId, \"vg_Challenge_HP\", \"1\")) or 1\n local selectedChallengeStat = tonumber(safeGet(triggerId, \"vg_Challenge_Stat\", \"1\")) or 1\n\n local hpChallengeList = {\n { value = 0.5, text = \"×0.5\" },\n { value = 0.6, text = \"×0.6\" },\n { value = 0.7, text = \"×0.7\" },\n { value = 0.8, text = \"×0.8\" },\n { value = 0.9, text = \"×0.9\" },\n { value = 1.2, text = \"×1.2\" },\n { value = 1.4, text = \"×1.4\" },\n { value = 1.6, text = \"×1.6\" },\n { value = 1.8, text = \"×1.8\" },\n { value = 2.0, text = \"×2.0\" },\n }\n\n local statChallengeList = {\n { value = 0.5, text = \"×0.5\" },\n { value = 0.6, text = \"×0.6\" },\n { value = 0.7, text = \"×0.7\" },\n { value = 0.8, text = \"×0.8\" },\n { value = 0.9, text = \"×0.9\" },\n { value = 1.1, text = \"×1.1\" },\n { value = 1.2, text = \"×1.2\" },\n { value = 1.3, text = \"×1.3\" },\n { value = 1.4, text = \"×1.4\" },\n { value = 1.5, text = \"×1.5\" },\n }\n\n table.insert(parts, '<div class=\"vgsetting-section\">')\n table.insert(parts, '<div class=\"vgsetting-title\">난이도 조절 (재선택 시 해제)</div>')\n\n table.insert(parts, '<div class=\"vgsetting-subtitle\">🧡 기본 HP</div>')\n table.insert(parts, '<div class=\"vgsetting-challenge-grid\">')\n\n for _, def in ipairs(hpChallengeList) do\n local activeClass = \"\"\n\n if selectedChallengeHP == def.value then\n activeClass = \" vgsetting-event-btn-active\"\n end\n\n table.insert(parts,\n '<button class=\"vgsetting-event-btn' .. activeClass ..\n '\" title=\"HP ' .. escapeHtml(tostring(def.value)) .. 'x' ..\n '\" risu-btn=\"choice_setChallengeHP_' .. escapeHtml(tostring(def.value)) .. '\">' ..\n escapeHtml(def.text) ..\n '</button>'\n )\n end\n\n table.insert(parts, '</div>')\n\n table.insert(parts, '<div class=\"vgsetting-subtitle\">☠️ 기본 스탯</div>')\n table.insert(parts, '<div class=\"vgsetting-challenge-grid\">')\n\n for _, def in ipairs(statChallengeList) do\n local activeClass = \"\"\n\n if selectedChallengeStat == def.value then\n activeClass = \" vgsetting-event-btn-active\"\n end\n\n table.insert(parts,\n '<button class=\"vgsetting-event-btn' .. activeClass ..\n '\" title=\"Stat ' .. escapeHtml(tostring(def.value)) .. 'x' ..\n '\" risu-btn=\"choice_setChallengeStat_' .. escapeHtml(tostring(def.value)) .. '\">' ..\n escapeHtml(def.text) ..\n '</button>'\n )\n end\n\n -- ==========================================\n -- 신음 지시문 ON/OFF\n -- ==========================================\n local moanDirective = trim(safeGet(triggerId, \"vg_MoanDirective\", \"0\"))\n local nonlethalHuman = trim(safeGet(triggerId, \"vg_NonlethalHuman\", \"0\"))\n local magicalGirlTransform = trim(safeGet(triggerId, \"vg_MagicalGirlTransform\", \"0\"))\n\n local function insertToggleSection(titleText, currentValue, options, choiceName, titlePrefix)\n local count = #options\n count = clamp(count, 1, 5)\n\n titlePrefix = titlePrefix or titleText\n\n table.insert(parts, '<div class=\"vgsetting-section\">')\n table.insert(parts, '<div class=\"vgsetting-title\">' .. escapeHtml(titleText) .. '</div>')\n\n table.insert(\n parts,\n '<div class=\"vgsetting-toggle-grid\" style=\"grid-template-columns:repeat(' ..\n tostring(count) ..\n ', 1fr);\">'\n )\n\n for _, def in ipairs(options) do\n local activeClass = \"\"\n\n if tostring(currentValue) == tostring(def.value) then\n activeClass = \" vgsetting-event-btn-active\"\n end\n\n table.insert(parts,\n '<button class=\"vgsetting-event-btn' .. activeClass ..\n '\" title=\"' .. escapeHtml(titlePrefix .. \" \" .. def.text) ..\n '\" risu-btn=\"choice_' .. escapeHtml(choiceName) .. '_' .. escapeHtml(def.value) .. '\">' ..\n escapeHtml(def.text) ..\n '</button>'\n )\n end\n\n table.insert(parts, '</div>')\n table.insert(parts, '</div>')\n end\n\n table.insert(parts, '</div>')\n table.insert(parts, '</div>')\n\n insertToggleSection(\n \"신음 지시문 (한국어 전용)\",\n moanDirective,\n {\n { value = \"1\", text = \"✔️\" },\n { value = \"0\", text = \"❌\" },\n },\n \"setMoanDirective\",\n \"Moan Directive\"\n )\n\n insertToggleSection(\n \"비살상 지침\",\n nonlethalHuman,\n {\n { value = \"1\", text = \"✔️\" },\n { value = \"0\", text = \"❌\" },\n },\n \"setNonlethalHuman\",\n \"Non-lethal Human Policy\"\n )\n\n insertToggleSection(\n \"전투 시 마법소녀 변신 지침\",\n magicalGirlTransform,\n {\n { value = \"1\", text = \"✔️\" },\n { value = \"2\", text = \"✔️ (평범)\" },\n { value = \"0\", text = \"❌\" },\n },\n \"setMagicalGirlTransform\",\n \"Magical Girl Transformation Rule\"\n )\n\n return table.concat(parts)\n end\n\n local personaName = trim(tostring(getPersonaName(triggerId) or \"\"))\n if personaName == \"\" then\n personaName = \"Profile\"\n end\n\n local sexualProfileText = tostring(safeGet(triggerId, \"vg_SexualProfile\", \"\") or \"\")\n sexualProfileText = sexualProfileText:gsub(\"^%s*###%s*Sexual Profile%s*\\n?\", \"\")\n\n local memoryDisplayText = tostring(safeGet(triggerId, \"vg_Memory_Display\", \"\") or \"\")\n local storyDisplayText = tostring(safeGet(triggerId, \"vg_Story_Display\", \"\") or \"\")\n\n local deckCount = #splitPipe(safeGet(triggerId, \"vg_Deck_Base\", \"\"))\n local deckTitle = \"덱 (\" .. tostring(deckCount) .. \"장)\"\n\n local panelProfileContent =\n buildSectionHtml(deckTitle, buildDeckListHtml()) ..\n buildSectionHtml(\"강화 효과\", buildEnhancementHtml()) ..\n buildSectionHtml(\"음란도\", buildLewdnessHtml()) ..\n buildSectionHtml(\"민감도\", buildSensitivityHtml()) ..\n buildSectionHtml(\"프로필\", buildPreBlock(sexualProfileText, \"\"))\n\n local panelLogContent =\n buildSectionHtml(\"목표\", buildGoalHtml()) ..\n buildSectionHtml(\"스토리\", buildStoryHtml()) ..\n buildSectionHtml(\"Key Milestones\", buildPreBlock(memoryDisplayText, \"\")) ..\n buildSectionHtml(\"Story\", buildPreBlock(storyDisplayText, \"\"))\n\n local panelSettingContent =\n buildSettingPanelContent(triggerId)\n\n local htmlParts = {\n[=[\n<style>\n.vgtab-wrap,\n.vgtab-wrap *{\n box-sizing:border-box;\n font-family:'Consolas','Malgun Gothic',monospace;\n}\n\n#vgtab-toggle-profile,\n#vgtab-toggle-log,\n#vgtab-toggle-setting{\n display:none;\n}\n\n/* ===== 오른쪽 탭 버튼 ===== */\n.vgtab-btn-box,\n.vgtab-btn-box-profile,\n.vgtab-btn-box-log,\n.vgtab-btn-box-setting,\n.vgtab-btn-box-load{\n position:fixed;\n right:0;\n z-index:21000;\n}\n\n.vgtab-btn-box-profile{ top:60px; }\n.vgtab-btn-box-log{ top:150px; }\n.vgtab-btn-box-setting{ top:240px; }\n.vgtab-btn-box-load{ top:330px; }\n\n.vgtab-open-btn{\n width:32px;\n height:70px;\n\n display:flex;\n align-items:center;\n justify-content:center;\n\n border-radius:12px 0 0 12px;\n border-right:none;\n\n background:#19050b;\n color:#ddd0d6;\n cursor:pointer;\n\n text-shadow:none;\n appearance:none;\n border:2px solid #c96a90;\n}\n\n/* ===== 패널 열리면 버튼 숨김 ===== */\n#vgtab-toggle-profile:checked ~ .vgtab-btn-box-profile,\n#vgtab-toggle-profile:checked ~ .vgtab-btn-box-log,\n#vgtab-toggle-profile:checked ~ .vgtab-btn-box-setting,\n#vgtab-toggle-profile:checked ~ .vgtab-btn-box-load,\n#vgtab-toggle-log:checked ~ .vgtab-btn-box-profile,\n#vgtab-toggle-log:checked ~ .vgtab-btn-box-log,\n#vgtab-toggle-log:checked ~ .vgtab-btn-box-setting,\n#vgtab-toggle-log:checked ~ .vgtab-btn-box-load,\n#vgtab-toggle-setting:checked ~ .vgtab-btn-box-profile,\n#vgtab-toggle-setting:checked ~ .vgtab-btn-box-log,\n#vgtab-toggle-setting:checked ~ .vgtab-btn-box-setting,\n#vgtab-toggle-setting:checked ~ .vgtab-btn-box-load{\n display:none;\n}\n\n/* ===== 패널 ===== */\n.vgtab-panel,\n.vgtab-panel-wide{\n position:fixed;\n top:0;\n height:100vh;\n\n background:#260811;\n color:#ddd0d6;\n border-left:3px solid #c96a90;\n padding:20px;\n overflow:hidden;\n z-index:20000;\n transition:right 0.3s ease;\n}\n\n.vgtab-panel{\n right:-32%;\n width:32%;\n}\n\n.vgtab-panel-wide{\n right:-50%;\n width:50%;\n}\n\n.vgtab-panel-setting{\n right:-25%;\n width:25%;\n}\n\n#vgtab-toggle-profile:checked ~ .vgtab-panel-profile{ right:0; }\n#vgtab-toggle-log:checked ~ .vgtab-panel-log{ right:0; }\n#vgtab-toggle-setting:checked ~ .vgtab-panel-setting{ right:0; }\n\n/* ===== 헤더 ===== */\n.vgtab-header{\n position:relative;\n text-align:center;\n margin-bottom:15px;\n}\n\n.vgtab-title{\n font-size:24px;\n font-weight:bold;\n color:#e8c6d2;\n}\n\n.vgtab-close{\n position:absolute;\n right:0;\n top:0;\n font-size:22px;\n cursor:pointer;\n line-height:1;\n color:#d97ea3;\n}\n\n/* ===== 내부 ===== */\n.vgtab-content{\n height:calc(100% - 50px);\n overflow-y:auto;\n margin-top:10px;\n padding:4px;\n background:transparent;\n color:#ddd0d6;\n border:none;\n border-radius:0;\n box-shadow:none;\n}\n\n.vgtab-section{\n margin-bottom:18px;\n padding:12px;\n background:#22070f;\n border:1px solid #5f2237;\n border-radius:10px;\n}\n\n.vgtab-section-title{\n font-size:15px;\n font-weight:bold;\n color:#f0b8cb;\n margin-bottom:10px;\n padding-bottom:6px;\n border-bottom:1px solid #5f2237;\n}\n\n.vgtab-rich-text{\n margin:0;\n line-height:1.32;\n color:#ddd0d6;\n word-break:break-word;\n white-space:normal;\n}\n\n/* ===== 빈 상태 ===== */\n.vg-empty{\n color:#a98a95;\n padding:6px 0;\n}\n\n/* ===== 덱 리스트 ===== */\n.vgdeck-grid{\n display:grid;\n grid-template-columns:repeat(auto-fill, minmax(95px, 1fr));\n gap:8px;\n}\n\n.vgdeck-card{\n padding:10px 8px;\n border:1px solid #6d2d44;\n border-radius:10px;\n background:#2d0c16;\n text-align:center;\n}\n\n.vgdeck-top{\n display:flex;\n align-items:center;\n justify-content:center;\n gap:6px;\n margin-bottom:8px;\n}\n\n.vgdeck-emoji{\n font-size:24px;\n line-height:1;\n}\n\n.vgdeck-count{\n font-size:16px;\n font-weight:bold;\n color:#ffb8cf;\n line-height:1;\n}\n\n.vgdeck-stars{\n font-size:11px;\n line-height:1.3;\n word-break:break-word;\n}\n\n.vgdeck-summary{\n display:flex;\n flex-wrap:wrap;\n gap:10px;\n margin-bottom:10px;\n font-size:22px;\n font-weight:bold;\n color:#f3d5df;\n}\n\n.vgdeck-summary-item{\n white-space:nowrap;\n}\n\n/* ===== 민감도 ===== */\n.vgsense-list{\n display:flex;\n flex-direction:column;\n gap:10px;\n}\n\n.vgsense-item{\n display:flex;\n flex-direction:column;\n gap:6px;\n}\n\n.vgsense-head{\n display:flex;\n align-items:center;\n justify-content:space-between;\n gap:8px;\n font-size:13px;\n}\n\n.vgsense-label{\n color:#f3d5df;\n font-weight:bold;\n}\n\n.vgsense-level{\n color:#ff9dbc;\n font-weight:bold;\n}\n\n.vgsense-value{\n color:#cfaeb8;\n font-size:12px;\n}\n\n.vgsense-bar{\n width:100%;\n height:12px;\n background:#3a1220;\n border:1px solid #7d3652;\n border-radius:999px;\n overflow:hidden;\n}\n\n.vgsense-fill{\n height:100%;\n border-radius:999px;\n background:linear-gradient(90deg, #ff7ea9 0%, #ffb8cf 100%);\n}\n\n.vgenh-text{\n font-size:16px;\n line-height:1.45;\n color:#ddd0d6;\n}\n\n.vggoal-list{\n display:flex;\n flex-direction:column;\n gap:8px;\n}\n\n.vggoal-row{\n display:flex;\n align-items:center;\n gap:8px;\n flex-wrap:wrap;\n}\n\n.vggoal-label{\n font-size:14px;\n font-weight:bold;\n color:#f0b8cb;\n flex:0 0 auto;\n}\n\n.vggoal-value{\n font-size:14px;\n line-height:1.4;\n color:#ddd0d6;\n white-space:pre-wrap;\n word-break:break-word;\n flex:1 1 auto;\n min-width:120px;\n}\n\n.vggoal-btn{\n padding:4px 10px;\n border:1px solid #c96a90;\n border-radius:8px;\n background:#19050b;\n color:#f3d5df;\n cursor:pointer;\n font-size:12px;\n font-weight:bold;\n}\n\n/* ===== 설정 ===== */\n.vgsetting-section{\n margin-bottom:14px;\n}\n\n.vgsetting-title{\n font-size:13px;\n font-weight:bold;\n color:#f0b8cb;\n margin-bottom:8px;\n}\n\n.vgsetting-event-grid{\n display:grid;\n grid-template-columns:repeat(5, 1fr);\n gap:6px;\n}\n\n.vgsetting-event-btn{\n width:100%;\n min-width:0;\n height:30px;\n border:1px solid #6d2d44;\n border-radius:8px;\n background:#19050b;\n color:#f3d5df;\n cursor:pointer;\n font-size:15px;\n line-height:1;\n padding:0;\n}\n\n.vgsetting-event-btn-active{\n border-color:#f0b8cb;\n background:#4a1024;\n box-shadow:0 0 8px rgba(240, 184, 203, 0.45);\n}\n\n.vgsetting-subtitle{\n font-size:12px;\n font-weight:bold;\n color:#cfaeb8;\n margin:8px 0 5px;\n}\n\n.vgsetting-challenge-grid{\n display:grid;\n grid-template-columns:repeat(5, 1fr);\n gap:6px;\n}\n\n.vgsetting-toggle-grid{\n display:grid;\n grid-template-columns:repeat(2, 1fr);\n gap:6px;\n}\n\n/* ===== 스크롤 ===== */\n.vgtab-content::-webkit-scrollbar{\n width:8px;\n}\n\n.vgtab-content::-webkit-scrollbar-thumb{\n background:#6d2d44;\n border-radius:999px;\n}\n\n.vgtab-content::-webkit-scrollbar-track{\n background:#19050b;\n}\n\n/* ===== 반응형 ===== */\n@media (max-width: 768px){\n .vgtab-panel,\n .vgtab-panel-wide{\n width:92%;\n right:-92%;\n }\n\n .vgtab-panel-setting{\n width:46%;\n right:-46%;\n }\n}\n \n</style>\n]=],\n '<div class=\"vgtab-wrap\">',\n\n '<input type=\"checkbox\" id=\"vgtab-toggle-profile\">',\n '<input type=\"checkbox\" id=\"vgtab-toggle-log\">',\n '<input type=\"checkbox\" id=\"vgtab-toggle-setting\">',\n\n '<div class=\"vgtab-btn-box vgtab-btn-box-profile\">',\n '<label for=\"vgtab-toggle-profile\" class=\"vgtab-open-btn vgtab-side-tab\">💕</label>',\n '</div>',\n\n '<div class=\"vgtab-btn-box vgtab-btn-box-log\">',\n '<label for=\"vgtab-toggle-log\" class=\"vgtab-open-btn vgtab-side-tab\">📜</label>',\n '</div>',\n\n '<div class=\"vgtab-btn-box vgtab-btn-box-setting\">',\n '<label for=\"vgtab-toggle-setting\" class=\"vgtab-open-btn vgtab-side-tab\">⚙️</label>',\n '</div>',\n\n '<div class=\"vgtab-btn-box vgtab-btn-box-load\">',\n '<button class=\"vgtab-open-btn vgtab-side-tab\" risu-trigger=\"loadVGData\">📥</button>',\n '</div>',\n\n '<div class=\"vgtab-panel-wide vgtab-panel-profile\">',\n '<div class=\"vgtab-header\">',\n '<div class=\"vgtab-title\">' .. escapeHtml(personaName) .. '</div>',\n '<label for=\"vgtab-toggle-profile\" class=\"vgtab-close\">✖</label>',\n '</div>',\n '<div class=\"vgtab-content\">',\n panelProfileContent,\n '</div>',\n '</div>',\n\n '<div class=\"vgtab-panel-wide vgtab-panel-log\">',\n '<div class=\"vgtab-header\">',\n '<div class=\"vgtab-title\">기록</div>',\n '<label for=\"vgtab-toggle-log\" class=\"vgtab-close\">✖</label>',\n '</div>',\n '<div class=\"vgtab-content\">',\n panelLogContent,\n '</div>',\n '</div>',\n\n '<div class=\"vgtab-panel vgtab-panel-setting\">',\n '<div class=\"vgtab-header\">',\n '<div class=\"vgtab-title\">설정</div>',\n '<label for=\"vgtab-toggle-setting\" class=\"vgtab-close\">✖</label>',\n '</div>',\n '<div class=\"vgtab-content\">',\n panelSettingContent,\n '</div>',\n '</div>',\n\n '</div>'\n }\n\n setChatVar(triggerId, \"vg_generatedHTML\", table.concat(htmlParts))\nend\n\n-- ==========================================\n-- 플레이덱\n-- ==========================================\nfunction getRuntimeDeckArray(triggerId)\n local runtimeDeck = {}\n\n for _, baseCard in ipairs(splitPipe(safeGet(triggerId, \"vg_Deck_Base\", \"\"))) do\n table.insert(runtimeDeck, baseCard)\n end\n\n for _, tempCard in ipairs(splitPipe(safeGet(triggerId, \"vg_Deck_Temp\", \"\"))) do\n table.insert(runtimeDeck, tempCard)\n end\n\n return runtimeDeck\nend\n\nfunction buildRuntimeDeckLookup(triggerId)\n local runtimeDeck = getRuntimeDeckArray(triggerId)\n\n local typeMap = {}\n local levelMap = {}\n local extraMap = {}\n\n for _, cardRaw in ipairs(runtimeDeck) do\n local parts = splitSection(cardRaw)\n local code = trim(parts[1] or \"\")\n local typeCode = tonumber(parts[2]) or 0\n local level = tonumber(parts[3]) or 1\n\n local extraFields = {}\n\n for i = 4, #parts do\n local key, value = tostring(parts[i] or \"\"):match(\"^(.-)=(.*)$\")\n\n if key and key ~= \"\" then\n extraFields[trim(key)] = trim(value or \"\")\n end\n end\n\n if code ~= \"\" then\n typeMap[code] = typeCode\n levelMap[code] = level\n extraMap[code] = extraFields\n end\n end\n\n return runtimeDeck, typeMap, levelMap, extraMap\nend\n\nfunction addCardToDeck(triggerId, cardType, cardLevel, extraFields)\n extraFields = extraFields or {}\n\n local deckStr = safeGet(triggerId, \"vg_Deck_Base\", \"\")\n\n local maxCode = 0\n\n for _, cardRaw in ipairs(getRuntimeDeckArray(triggerId)) do\n local parts = splitSection(cardRaw)\n local code = tonumber(parts[1]) or 0\n\n if code > maxCode then\n maxCode = code\n end\n end\n\n local parts = {\n tostring(maxCode + 1),\n tostring(cardType),\n tostring(cardLevel)\n }\n\n local keys = {}\n for key, value in pairs(extraFields) do\n if value ~= nil and value ~= \"\" then\n table.insert(keys, key)\n end\n end\n\n table.sort(keys)\n\n for _, key in ipairs(keys) do\n table.insert(parts, key .. \"=\" .. tostring(extraFields[key]))\n end\n\n local newCard = table.concat(parts, \"§\")\n local newDeck = appendPipe(deckStr, newCard)\n\n setChatVar(triggerId, \"vg_Deck_Base\", newDeck)\n\n local levelStars = getCardLevelStars(cardLevel)\n return getCardTypeEmoji(triggerId, cardType) .. \" \" .. levelStars .. \" 카드가 추가되었습니다.\"\nend\n\nfunction drawPlayDeckCards(triggerId, drawCount)\n drawCount = tonumber(drawCount) or 0\n if drawCount <= 0 then\n return\n end\n\n local oldHandArray = splitPipe(safeGet(triggerId, \"vg_PlayDeck_Hand\", \"\"))\n local handArray = {}\n local remainArray = splitPipe(safeGet(triggerId, \"vg_PlayDeck_Remain\", \"\"))\n local discardArray = splitPipe(safeGet(triggerId, \"vg_PlayDeck_Discard\", \"\"))\n\n local drawn = 0\n\n while drawn < drawCount do\n if #remainArray == 0 then\n if #discardArray == 0 then\n break\n end\n\n remainArray = discardArray\n discardArray = {}\n shuffleArray(remainArray)\n\n -- discard를 섞어도 더 이상 뽑을 카드가 없으면 종료\n if #remainArray == 0 then\n break\n end\n end\n\n local code = table.remove(remainArray, 1)\n if not code or code == \"\" then\n break\n end\n\n table.insert(handArray, code)\n drawn = drawn + 1\n end\n\n for _, code in ipairs(oldHandArray) do\n code = trim(code or \"\")\n if code ~= \"\" then\n table.insert(discardArray, code)\n end\n end\n\n setChatVar(triggerId, \"vg_PlayDeck_Hand\", table.concat(handArray, \"|\"))\n setChatVar(triggerId, \"vg_PlayDeck_Remain\", table.concat(remainArray, \"|\"))\n setChatVar(triggerId, \"vg_PlayDeck_Discard\", table.concat(discardArray, \"|\"))\nend\n\nfunction resetPlayDeck(triggerId)\n local baseArray = splitPipe(safeGet(triggerId, \"vg_Deck_Base\", \"\"))\n local remainArray = {}\n\n for _, baseCard in ipairs(baseArray) do\n local parts = splitSection(baseCard)\n local code = trim(parts[1] or \"\")\n\n if code ~= \"\" then\n table.insert(remainArray, code)\n end\n end\n\n shuffleArray(remainArray)\n\n setChatVar(triggerId, \"vg_PlayDeck_Hand\", \"\")\n setChatVar(triggerId, \"vg_PlayDeck_Remain\", table.concat(remainArray, \"|\"))\n setChatVar(triggerId, \"vg_PlayDeck_Discard\", \"\")\n setChatVar(triggerId, \"vg_PlayDeck_Action\", \"\")\n setChatVar(triggerId, \"vg_PlayDeck_Choice\", \"\")\n setChatVar(triggerId, \"vg_PlayDeck_Setting\", \"\")\n setChatVar(triggerId, \"vg_PlayDeck_Guide\", \"\")\nend\n\nfunction getPlayDeckDrawCount(triggerId)\n local baseDraw = 5\n local drawModifier = 0\n\n local finalDraw = baseDraw + drawModifier\n\n finalDraw = math.max(2, finalDraw)\n\n return finalDraw\nend\n\nfunction resetAndDrawPlayDeck(triggerId)\n resetPlayDeck(triggerId)\n\n local drawCount = getPlayDeckDrawCount(triggerId)\n drawPlayDeckCards(triggerId, drawCount)\n buildPlayDeckSetting(triggerId)\nend\n\n-- ==========================================\n-- 히로인 액션 설정\n-- ==========================================\nfunction buildPlayDeckSetting(triggerId)\n local deckPhase = isDeckPhase(triggerId)\n\n if deckPhase == false then\n setChatVar(triggerId, \"vg_PlayDeck_Setting\", \"\")\n setChatVar(triggerId, \"vg_PlayDeck_Guide\", \"\")\n return\n end\n\n local function buildEffectTypeText(effectTypes)\n local uniqueList = {}\n local seen = {}\n\n for _, effectType in ipairs(effectTypes or {}) do\n effectType = trim(tostring(effectType or \"\"))\n\n if effectType ~= \"\" and not seen[effectType] then\n seen[effectType] = true\n table.insert(uniqueList, effectType)\n end\n end\n\n if #uniqueList == 0 then\n return \"\"\n elseif #uniqueList == 1 then\n return uniqueList[1]\n elseif #uniqueList == 2 then\n return uniqueList[1] .. \" or \" .. uniqueList[2]\n end\n\n local text = \"\"\n\n for i = 1, #uniqueList do\n if i == 1 then\n text = uniqueList[i]\n elseif i == #uniqueList then\n text = text .. \", or \" .. uniqueList[i]\n else\n text = text .. \", \" .. uniqueList[i]\n end\n end\n\n return text\n end\n\n local _, baseTypeMap = buildRuntimeDeckLookup(triggerId)\n\n local handArray = splitPipe(safeGet(triggerId, \"vg_PlayDeck_Hand\", \"\"))\n local handLines = {}\n local hasType = {}\n\n for _, code in ipairs(handArray) do\n code = trim(code or \"\")\n\n local cardType = baseTypeMap[code] or 0\n local typeName = getCardTypeName(triggerId, cardType)\n\n table.insert(handLines, \"[Code: \" .. code .. \" | Type: \" .. typeName .. \"]\")\n\n if cardType > 0 then\n hasType[cardType] = true\n end\n end\n\n setChatVar(triggerId, \"vg_PlayDeck_Setting\", table.concat(handLines, \"\\n\"))\n\n local typeCodeList = {}\n for typeCode, _ in pairs(hasType) do\n table.insert(typeCodeList, tonumber(typeCode) or 0)\n end\n table.sort(typeCodeList)\n\n local effectTypeList = {}\n local effectTypeSeen = {}\n local typeRuleLines = {}\n local guideLines = {}\n local seenTypeRuleLines = {}\n\n for _, typeCode in ipairs(typeCodeList) do\n local ruleEffectTypes, description = getCardTypeEffectTypes(triggerId, typeCode)\n\n for _, effectType in ipairs(ruleEffectTypes) do\n if not effectTypeSeen[effectType] then\n effectTypeSeen[effectType] = true\n table.insert(effectTypeList, effectType)\n end\n end\n\n if description ~= \"\" and #ruleEffectTypes > 0 then\n local line = \"- \" .. getCardTypeName(triggerId, typeCode) .. \": \" .. description\n\n local ruleEffectText = buildEffectTypeText(ruleEffectTypes)\n if ruleEffectText ~= \"\" then\n line = line .. \" Effect Type can only be \" .. ruleEffectText .. \".\"\n end\n\n if not seenTypeRuleLines[line] then\n seenTypeRuleLines[line] = true\n table.insert(typeRuleLines, line)\n end\n end\n end\n\n local effectTypeText = buildEffectTypeText(effectTypeList)\n\n if effectTypeText ~= \"\" then\n table.insert(\n guideLines,\n \"- Effect Type: Must be exactly \" ..\n (#effectTypeList >= 2 and \"one of \" or \"\") ..\n effectTypeText ..\n \". No other values are allowed. Effect Type and Value must be written in English.\"\n )\n\n table.insert(guideLines, buildEffectTypeLiteralGuideLine(effectTypeList))\n end\n\n if #typeRuleLines > 0 then\n table.insert(guideLines, \"\")\n table.insert(guideLines, \"### Type Rules\")\n\n for _, line in ipairs(typeRuleLines) do\n table.insert(guideLines, line)\n end\n end\n\n local curPhase = getPhase(triggerId)\n local heroineSpecialState = getHeroineSpecialState(triggerId)\n\n if isHarassmentPhase(triggerId, curPhase) or isAbusePhase(triggerId, curPhase) or isConfrontationPhase(triggerId, curPhase) then\n table.insert(guideLines, \"- These actions must not escalate into direct physical confrontation or full combat.\")\n table.insert(guideLines, \"- These actions must not fully end the situation, resolve it completely, or shut it down.\")\n elseif isMasturbationPhase(triggerId, curPhase) then\n table.insert(guideLines, \"- These actions must directly reflect ongoing masturbation or present arousal and must lead further into arousal rather than end as purely non-sexual reactions. They must not be actions whose primary purpose is to endure, suppress, resist, calm down, regain composure, regain rational control, hold back, or extinguish arousal.\")\n elseif isIncapacitatedPhase(triggerId, curPhase) then\n if curPhase == constPhase.transaction then\n table.insert(guideLines, \"- These actions must remain sexual in nature, must focus on the encounter's sexual satisfaction or on avoiding the worst sexual outcome, and must not shift into combat, escape, deception, or any attempt to end or resolve the situation.\")\n else\n table.insert(guideLines, \"- Because the heroine has no remaining means to escape or end this situation, these actions must focus on avoiding the worst sexual outcome within the current violation.\")\n end\n elseif isPerformancePhase(triggerId, curPhase) then\n table.insert(guideLines, \"- These actions must remain strictly non-contact with the encounter. They must not physically touch the encounter or move into a situation where physical contact could plausibly occur.\")\n table.insert(guideLines, \"- These actions must focus on sexually satisfying the encounter through performance, rather than ending, resolving, or shutting down the situation.\")\n\n elseif isExhibitionismPhase(triggerId, curPhase) then\n table.insert(guideLines, \"- These actions must remain solitary and exhibitionistic in nature, heightening or sustaining the heroine's own sexual arousal through the thrill of possible discovery, rather than ending the situation or turning it into direct interaction.\")\n\n elseif isPromiscuityPhase(triggerId, curPhase) or isIntimacyPhase(triggerId, curPhase) then\n table.insert(guideLines, \"- These actions must remain within an ongoing voluntary sexual situation and must not turn into combat, escape, or a full break from the situation.\")\n\n elseif isHypnosisPhase(triggerId, curPhase) then\n table.insert(guideLines, \"- These actions must not escalate into direct physical confrontation or full combat.\")\n table.insert(guideLines, \"- Because the heroine is under mental influence such as hypnosis, brainwashing, mind control, or mental manipulation, her actions are severely limited and must remain focused on resisting, softening, or breaking the encounter's mental influence.\")\n\n elseif isRescuePhase(triggerId, curPhase) then\n table.insert(guideLines, \"- Because the heroine is incapacitated and unable to offer meaningful resistance, these actions must not become decisive, overpowering, or situation-ending on their own.\")\n\n elseif isHEventPhase(triggerId, curPhase) then\n table.insert(guideLines, \"- Because the heroine is incapacitated and has no practical means to escape, end, interrupt, reverse, or gain advantage in this situation, her actions are meaningless expressions only and cannot create any mechanical benefit.\")\n end\n\n setChatVar(triggerId, \"vg_PlayDeck_Guide\", table.concat(guideLines, \"\\n\"))\nend\n\nfunction getCardTypeEffectTypes(triggerId, typeCode)\n typeCode = normalizeCardTypeCode(typeCode)\n\n local curPhase = getPhase(triggerId)\n\n local heroineSpecialState = getHeroineSpecialState(triggerId)\n\n if typeCode ~= constCardType.ecstasy then\n if heroineSpecialState == vgHeroineEffect.sleep then\n if isPrimaryLikeCardType(typeCode) == false and isSecondaryLikeCardType(typeCode) == false then\n return {\"Slumber\"}, \"Actions that represent the heroine falling into deeper sleep, remaining defenseless and unresisting. This is not beneficial to her and instead leaves her disadvantaged.\"\n end\n\n return {\"Somniloquy\"}, \"Actions used to express only meaningless and defenseless reactions that are half-asleep, uncomprehending, reflexive, or ineffective, without properly waking up.\"\n elseif heroineSpecialState == vgHeroineEffect.immobilized then\n if isPrimaryLikeCardType(typeCode) == false and isSecondaryLikeCardType(typeCode) == false then\n return {\"Immobilization\"}, \"Actions that represent not active resistance by the heroine, but her immobilized state being passively prolonged by the encounter through paralysis, drugging, numbing agents, or similar means. This is not beneficial to her and instead leaves her disadvantaged.\"\n end\n\n return {\"Stasis\"}, \"Actions used to express conscious but immobilized reactions. The heroine is awake and aware, but cannot act properly, resist normally, or respond with meaningful force.\"\n elseif heroineSpecialState == vgHeroineEffect.brainwashed then\n if isPrimaryLikeCardType(typeCode) == false and isSecondaryLikeCardType(typeCode) == false then\n return {\"Conditioning\"}, \"Actions that represent not active resistance by the heroine, but her mentally influenced state being passively prolonged by the encounter. This is not beneficial to her and instead leaves her disadvantaged.\"\n end\n\n return {\"Sexual\"}, \"Actions involving explicit sexual acts, performed under complete mental control rather than her own will.\"\n end\n end\n\n if isHEventPhase(triggerId, curPhase) then\n if isPrimaryLikeCardType(typeCode) then\n return {\"Rejection\"}, \"Actions that express continued inner refusal, hostility, non-cooperation, or futile resistance. They show that the heroine still rejects what is happening.\"\n elseif isSecondaryLikeCardType(typeCode) then\n return {\"Accommodation\"}, \"Actions that express outward yielding, softening, accommodation, or receptive behavior to reduce harm, calm the situation, or avoid worse treatment.\"\n elseif typeCode == constCardType.ecstasy then\n return {\"Sexual\"}, \"Actions that arouse the heroine herself, whether voluntary or involuntary. These actions must reflect the heroine's acceptance of pleasure or sexual stimulation, and may even interfere with her own actions. They must not be focused, controlled, enduring, or useful.\"\n else\n return {\"Passivity\"}, \"Actions that express helplessness, passivity, stunned reactions, exhaustion, overwhelmed reflexes, or the inability to take meaningful action.\"\n end\n end\n\n if isPerformancePhase(triggerId, curPhase) then\n if isPrimaryLikeCardType(typeCode) then\n return {\"Appeasement\"}, \"Actions used to sexually please and satisfy the observers through deliberate erotic performance without physical contact.\"\n elseif isSecondaryLikeCardType(typeCode) then\n return {\"Redirection\"}, \"Actions used to refuse, soften, redirect, or shift the observers' demands toward a different, alternative, or less explicit performance without physical contact.\"\n else\n return {\"Buff\"}, \"Actions used to build atmosphere, anticipation, rhythm, or erotic momentum for the performance without directly satisfying the observers or directly rejecting their demands.\"\n end\n end\n\n if isPromiscuityPhase(triggerId, curPhase) or isIntimacyPhase(triggerId, curPhase) then\n if isPrimaryLikeCardType(typeCode) then\n return {\"Appeasement\"}, \"Actions used to willingly indulge in the ongoing sexual situation and share sexual pleasure with the encounter through direct participation.\"\n elseif isSecondaryLikeCardType(typeCode) then\n return {\"Redirection\"}, \"Actions used to refuse unwanted acts and redirect the ongoing sexual situation toward a different form, pace, or focus while staying within willing participation.\"\n else\n return {\"Buff\"}, \"Actions used to refine technique, pacing, posture, or responsiveness within the ongoing sexual situation.\"\n end\n end\n\n if isMasturbationPhase(triggerId, curPhase) then\n if isPrimaryLikeCardType(typeCode) or isSecondaryLikeCardType(typeCode) then\n local hasFirstAnal = trim(safeGet(triggerId, \"vg_FirstAnal\", \"\")) ~= \"\"\n local description = \"Direct masturbation using varied methods, including the heroine's own body, sex toys, and suitable items available nearby; actions must target different erogenous areas (e.g., breasts, clitoris, vagina, anus, mouth).\"\n if hasFirstAnal then\n description = description .. \" At least one action must target the anus.\"\n end\n\n return {\"Sexual\"}, description\n else\n return {\"Sexual\"}, \"Erotic fantasy, erotic anticipation, or erotic recollection that itself directly heightens present sexual arousal, not non-sexual thought or endurance such as suppressing, resisting, suffering through, or merely enduring arousal.\"\n end\n end\n\n if isExhibitionismPhase(triggerId, curPhase) then\n if isPrimaryLikeCardType(typeCode) then\n return {\"Excitement\"}, \"Actions used to actively expose, reveal, or present the heroine's body in a more direct and provocative way, heightening excitement through deliberate exhibition.\"\n elseif isSecondaryLikeCardType(typeCode) then\n return {\"Excitement\"}, \"Actions used to express hesitant, embarrassed, or passive exhibition that heightens excitement in a more restrained and indirect way.\"\n else\n return {\"Excitement\"}, \"Actions used to heighten excitement through tension, risk, anticipation, or the thrill of possible discovery.\"\n end\n end\n\n if isIncapacitatedPhase(triggerId, curPhase) then\n if isSecondaryLikeCardType(typeCode) then\n return {\"Mitigation\"}, \"Actions used to plead, delay, soften, or redirect the encounter's sexual acts into a less severe sexual form. Because full refusal or active resistance is impossible, they are limited to steering the encounter away from harsher sexual acts and toward less severe sexual ones.\"\n elseif isDefiantPhase(triggerId, curPhase) then\n if isPrimaryLikeCardType(typeCode) then\n return {\"Appeasement\"}, \"Actions of futile endurance, recoil, or resistance under violation. They do not stop the violation at all and instead feed the encounter's sexual satisfaction.\"\n else\n return {\"Buff\"}, \"Actions used to brace herself for what is coming, steadying her body, breathing, pain tolerance, or mental resolve in preparation for further violation rather than directly affecting the encounter.\"\n end\n else\n if isPrimaryLikeCardType(typeCode) then\n return {\"Appeasement\"}, \"Actions used to sexually stimulate and satisfy the encounter through submissive compliance, seeking to avoid a worse outcome by bringing the current situation to an end sooner.\"\n else\n return {\"Buff\"}, \"Actions used to prepare sexual technique, pacing, posture, control, or responsiveness for the sexual acts that follow, with emphasis on preparation rather than directly affecting the encounter.\"\n end\n end\n end\n\n if typeCode == constCardType.primary then\n if isHypnosisPhase(triggerId, curPhase) then\n return {\"Liberation\"}, \"Actions used in an attempt to break free and reclaim control of herself.\"\n elseif isHarassmentPhase(triggerId, curPhase) or isAbusePhase(triggerId, curPhase) or isConfrontationPhase(triggerId, curPhase) then\n return {\"Sway\"}, \"Actions used to sway or pressure the encounter so that the situation turns in the heroine's favor.\"\n elseif isRescuePhase(triggerId, curPhase) then\n return {\"Coordination\"}, \"Actions used to help the rescuers and support the rescue attempt.\"\n else\n return {\"Damage\"}, \"Actions used to directly attack the encounter.\"\n end\n\n elseif typeCode == constCardType.secondary then\n if isHypnosisPhase(triggerId, curPhase) then\n return {\"Mitigation\"}, \"Actions used in an attempt to lessen, soften, or hold off the encounter's mental influence.\"\n elseif isHarassmentPhase(triggerId, curPhase) then\n return {\"Defiance\"}, \"Actions used to directly refuse, deflect, block, or deny the encounter's current advances or attempts to impose upon the heroine.\"\n elseif isAbusePhase(triggerId, curPhase) then\n return {\"Refusal\"}, \"Actions used to refuse, deflect, or push back against coercion, improper demands, or imposed procedures without yielding ground.\"\n elseif isConfrontationPhase(triggerId, curPhase) then\n return {\"Retort\"}, \"Actions used to answer back, deny, deflect, or blunt the encounter's accusations, humiliation, or verbal pressure through the heroine's own immediate verbal response.\"\n elseif isRescuePhase(triggerId, curPhase) then\n return {\"Disruption\"}, \"Actions used to interfere with the encounter's attempts to block, delay, restrain, separate, or cut off the rescue, disrupting its control over the situation.\"\n else\n return {\"Guard\"}, \"Actions used to protect against danger, including dodging, blocking, guarding, or resisting enemy attacks, restraints, and other threats.\"\n end\n\n elseif typeCode == constCardType.taunt then\n return {\"Provocation\"}, \"Actions used to disrupt the encounter through means such as taunting, irritation, distraction, or broken focus, hindering or disrupting its actions.\"\n\n elseif typeCode == constCardType.temptation then\n return {\"Allure\"}, \"Actions used to disrupt the encounter through explicitly lewd acts, including direct sexual self-stimulation and other overtly sexual behavior, thereby hindering or disrupting its actions at the cost of heightening her own arousal. This action is available regardless of her composure, self-control, morality, reluctance, or arousal state.\"\n\n elseif typeCode == constCardType.milking then\n return {\"Ravishment\"}, \"Aggressive explicit sexual actions where the heroine embodies the predatory sensuality of a lewd succubus, engaging the encounter in overt, explicit sexual acts with genuine sexual intent and involvement. These actions drive the encounter toward climax and weaken it. They must be overt sexual acts involving direct sexual physical contact with the encounter, including genital or other mucosal contact, clearly sexual in context and execution, and carried out as genuine lustful engagement. Actions that do not involve such explicit sexual physical contact directly with the encounter do not qualify, including but not limited to suggestive, preparatory, indirect, positional, distracting, manipulative, or tactical behavior.\"\n\n elseif typeCode == constCardType.milking then\n \n\n elseif typeCode == constCardType.focus then\n if isHypnosisPhase(triggerId, curPhase) then\n return {\"Buff\"}, \"Actions used to steady and anchor her mind by gathering her thoughts and reaffirming her sense of self.\"\n elseif isHarassmentPhase(triggerId, curPhase) or isAbusePhase(triggerId, curPhase) or isConfrontationPhase(triggerId, curPhase) then\n return {\"Buff\"}, \"Actions used to steady the heroine's mindset, bearing, or presentation in preparation for what follows. These actions must not directly push back against or interfere with the encounter.\"\n elseif isRescuePhase(triggerId, curPhase) then\n return {\"Buff\"}, \"Actions used to signal, orient, or prepare herself in ways that help the rescuers coordinate and act more effectively, without directly disrupting the encounter.\"\n else\n return {\"Buff\"}, \"Actions used to enhance the heroine's combat performance, including reinforcing strength, balance, posture, focus, or movement. These actions must not directly attack or interfere with the encounter.\"\n end\n\n elseif typeCode == constCardType.charge then\n if isHypnosisPhase(triggerId, curPhase) or isHarassmentPhase(triggerId, curPhase) or isAbusePhase(triggerId, curPhase) or isConfrontationPhase(triggerId, curPhase) or isRescuePhase(triggerId, curPhase) then\n return {\"Refresh\"}, \"Actions used to catch her breath, recover stamina, and regain the steadiness needed to keep responding.\"\n else\n return {\"Refresh\"}, \"Actions used to charge and replenish stamina or other action resources.\"\n end\n\n elseif typeCode == constCardType.heal then\n if isHarassmentPhase(triggerId, curPhase) or isAbusePhase(triggerId, curPhase) or isConfrontationPhase(triggerId, curPhase) or isHypnosisPhase(triggerId, curPhase) or isRescuePhase(triggerId, curPhase) then\n return {\"Restoration\"}, \"Actions used to restore the heroine's resolve, reassert her stance, and strengthen her will to resist.\"\n else\n return {\"Restoration\"}, \"Actions used to treat wounds, patch up injuries, and restore the heroine's condition for continued fighting.\"\n end\n\n elseif typeCode == constCardType.soothe then\n return {\"Calm\"}, \"Actions used to calm the heroine's arousal, settle her body, and reduce her physiological response.\"\n\n elseif typeCode == constCardType.ecstasy then\n return {\"Sexual\"}, \"Actions that arouse the heroine herself, whether voluntary or involuntary. These actions must reflect the heroine's acceptance of pleasure or sexual stimulation, and may even interfere with her own actions. They must not be focused, controlled, enduring, or useful.\"\n end\n\n return {}, \"\"\nend\n\nlocal function parseHeroineActionSettingBlock(triggerId, cleanMsg)\n local actionSettingRaw = cleanMsg:match(\"<Heroine Action Setting>%s*([%s%S]-)%s*</Heroine Action Setting>\")\n local actionRows = {}\n local _, deckTypeMap, deckLevelMap, deckExtraMap = buildRuntimeDeckLookup(triggerId)\n local curPhase = getPhase(triggerId)\n\n local heroineSpecialState = getHeroineSpecialState(triggerId)\n\n if actionSettingRaw and trim(actionSettingRaw) ~= \"\" then\n local typeCodeList = {}\n local typeSeen = {}\n\n for _, value in pairs(constCardType) do\n value = tonumber(value)\n if value and not typeSeen[value] then\n typeSeen[value] = true\n table.insert(typeCodeList, value)\n end\n end\n\n table.sort(typeCodeList)\n\n local fallbackOrder = {}\n local fallbackSeen = {}\n\n for _, typeCode in ipairs(typeCodeList) do\n local effectTypeList = getCardTypeEffectTypes(triggerId, typeCode)\n\n for _, effectTypeName in ipairs(effectTypeList or {}) do\n effectTypeName = trim(tostring(effectTypeName or \"\"))\n if effectTypeName ~= \"\" and not fallbackSeen[effectTypeName] then\n fallbackSeen[effectTypeName] = true\n table.insert(fallbackOrder, effectTypeName)\n end\n end\n end\n\n for entry in tostring(actionSettingRaw):gmatch(\"%b[]\") do\n local line = trim(entry:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\"))\n local fieldMap = {}\n\n for part in line:gmatch(\"([^|]+)\") do\n local key, value = part:match(\"^%s*(.-)%s*:%s*(.-)%s*$\")\n if key then\n local rawKey = trim(key)\n local rawValue = trim(value or \"\")\n\n fieldMap[rawKey] = rawValue\n\n local normalizedKey = normalizeHeroineActionToEffectTypeName(triggerId, rawKey)\n if normalizedKey ~= rawKey and fieldMap[normalizedKey] == nil then\n fieldMap[normalizedKey] = rawValue\n end\n end\n end\n\n local code = trim(fieldMap[\"Code\"] or \"\")\n local actionName = trim(fieldMap[\"Action\"] or \"\")\n local rawCostText = trim(fieldMap[\"Cost\"] or \"\")\n\n actionName = actionName:gsub('\"', \"\"):gsub(\"'\", \"\")\n\n local cardTypeCode = deckTypeMap[code] or 0\n local cardLevel = math.max(1, tonumber(deckLevelMap[code]) or 1)\n local cardExtraFields = deckExtraMap[code] or {}\n\n local heroineStatValue = 10\n if isPrimaryLikeCardType(cardTypeCode) then\n heroineStatValue = tonumber(getResolvedDualFlag(triggerId, vgHeroineStat.varName, vgHeroineStat.strength, 10)) or 10\n elseif isSecondaryLikeCardType(cardTypeCode) or normalizeCardTypeCode(cardTypeCode) == constCardType.secondary then\n heroineStatValue = tonumber(getResolvedDualFlag(triggerId, vgHeroineStat.varName, vgHeroineStat.reflex, 10)) or 10\n else\n heroineStatValue = tonumber(getResolvedDualFlag(triggerId, vgHeroineStat.varName, vgHeroineStat.willpower, 10)) or 10\n end\n\n local resolvedCostValue = 0\n\n if isMasturbationPhase(triggerId, curPhase) == false\n and isIncapacitatedPhase(triggerId, curPhase) == false\n and isHEventPhase(triggerId, curPhase) == false\n and isPerformancePhase(triggerId, curPhase) == false\n and isPromiscuityPhase(triggerId, curPhase) == false\n and isIntimacyPhase(triggerId, curPhase) == false\n and isExhibitionismPhase(triggerId, curPhase) == false\n and heroineSpecialState ~= vgHeroineEffect.sleep\n and heroineSpecialState ~= vgHeroineEffect.immobilized\n and heroineSpecialState ~= vgHeroineEffect.brainwashed then\n \n resolvedCostValue = convertLevelTextToCostValue(\n rawCostText ~= \"\" and rawCostText or getRandomFallbackEffectLevelText(),\n cardLevel,\n cardTypeCode,\n heroineStatValue\n )\n end\n\n local preferredEffectTypeList = {}\n if cardTypeCode ~= 0 then\n preferredEffectTypeList = getCardTypeEffectTypes(triggerId, cardTypeCode) or {}\n end\n\n local targetEffectTypeName = trim(tostring(preferredEffectTypeList[1] or \"\"))\n\n local packedEffects = {}\n local primaryEffectTypeCode = 0\n\n local isCritical = false\n\n if isHEventPhase(triggerId, curPhase) == false\n and isPrimaryLikeCardType(cardTypeCode) == true\n and heroineSpecialState ~= vgHeroineEffect.sleep\n and heroineSpecialState ~= vgHeroineEffect.immobilized\n and heroineSpecialState ~= vgHeroineEffect.brainwashed then\n\n isCritical = rollCriticalChance(triggerId, \"heroine\")\n\n if isCritical then\n actionName = \"🍀 \" .. actionName\n end\n end\n\n local sourceFieldValue = \"\"\n local rawEffectType = normalizeHeroineActionToEffectTypeName(triggerId, trim(fieldMap[\"Effect Type\"] or \"\"))\n\n local primaryResolvedValue = 0\n\n if targetEffectTypeName ~= \"\" then\n -- 덱 코드가 있으면: 입력된 Effect Type 이름은 무시하고 값만 가져온다\n for _, fieldName in ipairs(fallbackOrder) do\n local fieldValue = trim(fieldMap[fieldName] or \"\")\n if fieldValue ~= \"\" then\n sourceFieldValue = fieldValue\n break\n end\n end\n\n -- 기본 파싱 실패 + Effect Type만 있으면 랜덤 값 보정\n if sourceFieldValue == \"\" and rawEffectType ~= \"\" then\n sourceFieldValue = getRandomFallbackEffectLevelText()\n end\n else\n -- 덱 코드가 없으면: 파싱된 Effect Type 이름 그대로 쓴다\n for _, fieldName in ipairs(fallbackOrder) do\n local fieldValue = trim(fieldMap[fieldName] or \"\")\n if fieldValue ~= \"\" then\n targetEffectTypeName = fieldName\n sourceFieldValue = fieldValue\n break\n end\n end\n\n -- 아무 값도 못 찾았는데 Effect Type만 있으면 그걸 effect 이름으로 쓰고 값은 랜덤 보정\n if targetEffectTypeName == \"\" and rawEffectType ~= \"\" and getEffectTypeCode(rawEffectType) ~= 0 then\n targetEffectTypeName = rawEffectType\n sourceFieldValue = getRandomFallbackEffectLevelText()\n end\n end\n\n -- Code로 찾은 cardTypeCode 기준의 원래 Effect Type에 강제로 넣는다.\n if targetEffectTypeName ~= \"\" and sourceFieldValue ~= \"\" then\n local effectTypeCode = getEffectTypeCode(targetEffectTypeName)\n local effectValue = convertLevelTextToValue(\n triggerId,\n \"heroine\",\n effectTypeCode,\n sourceFieldValue,\n cardLevel,\n actionName,\n cardTypeCode,\n heroineStatValue\n )\n\n local baseEffectValue = effectValue\n if isCritical then\n effectValue = roundToTenth(effectValue * 1.5)\n end\n\n if effectValue ~= 0 or shouldKeepZeroEffectType(effectTypeCode) then\n addOrMergePackedEffect(packedEffects, effectTypeCode, effectValue)\n\n if effectTypeCode == constEffectType.appeasement or effectTypeCode == constEffectType.excitement then\n local sexualBonusRatio = getLewdnessSexualBonusRatio(triggerId)\n\n local sexualBonus = effectValue * sexualBonusRatio\n local randomRange = math.abs(sexualBonus) * 0.1\n\n if randomRange < 0.1 then\n sexualBonus = sexualBonus + (math.random(-100, 100) / 1000)\n else\n sexualBonus = sexualBonus * (0.9 + (math.random() * 0.2))\n end\n\n sexualBonus = roundToTenth(math.max(0.1, sexualBonus))\n\n if sexualBonus > 0 then\n addOrMergePackedEffect(packedEffects, constEffectType.sexual, sexualBonus)\n end\n elseif isPrimaryLikeCardType(cardTypeCode) == false and isSecondaryLikeCardType(cardTypeCode) == false and cardTypeCode ~= constCardType.ecstasy then\n if heroineSpecialState == vgHeroineEffect.sleep then\n addOrMergePackedEffect(packedEffects, constEffectType.sleep, 1)\n elseif heroineSpecialState == vgHeroineEffect.immobilized then\n addOrMergePackedEffect(packedEffects, constEffectType.immobilized, 1)\n elseif heroineSpecialState == vgHeroineEffect.brainwashed then\n addOrMergePackedEffect(packedEffects, constEffectType.brainwashed, 1)\n end\n end\n\n primaryEffectTypeCode = effectTypeCode\n primaryResolvedValue = baseEffectValue\n end\n end\n\n if isEncounterPhase(triggerId, curPhase) and isExhibitionismPhase(triggerId, curPhase) == false\n and heroineSpecialState ~= vgHeroineEffect.sleep and heroineSpecialState ~= vgHeroineEffect.immobilized and heroineSpecialState ~= vgHeroineEffect.brainwashed then\n\n local function addScaledExtraEffect(packedEffects, baseValue, ratio, effectTypeCode)\n if baseValue <= 0 then\n return\n end\n\n local value = baseValue * ratio\n\n if value < 1 then\n value = value + ((math.random(0, 1) == 0) and -0.1 or 0.1)\n else\n value = value * (math.random(90, 110) / 100)\n end\n\n value = roundToTenth(math.max(0.1, value))\n addOrMergePackedEffect(packedEffects, effectTypeCode, value)\n end\n\n if cardTypeCode == constCardType.breaker then\n local baseGuardbreak = 3 + cardLevel * 0.5\n local guardbreakPercent = -roundToTenth(baseGuardbreak * (math.random(100, 200) / 100))\n\n addOrMergePackedEffect(packedEffects, constEffectType.guardbreak, guardbreakPercent)\n\n elseif cardTypeCode == constCardType.counter and primaryResolvedValue > 0 then\n addScaledExtraEffect(packedEffects, primaryResolvedValue, 0.5, constEffectType.guard)\n\n elseif cardTypeCode == constCardType.taunt and primaryResolvedValue > 0 then\n addScaledExtraEffect(packedEffects, primaryResolvedValue, 0.04, constEffectType.rage)\n\n elseif cardTypeCode == constCardType.temptation and primaryResolvedValue > 0 then\n addScaledExtraEffect(packedEffects, primaryResolvedValue, 0.2, constEffectType.sexual)\n\n elseif cardTypeCode == constCardType.milking and primaryResolvedValue > 0 then\n addScaledExtraEffect(packedEffects, primaryResolvedValue, 0.4, constEffectType.sexual)\n end\n end\n\n for key, rawMultiplier in pairs(cardExtraFields) do\n local bonusCodeStr = tostring(key):match(\"^bonus_(%-?%d+)$\")\n local bonusCode = tonumber(bonusCodeStr) or 0\n local bonusMultiplier = tonumber(rawMultiplier) or 0\n\n if bonusCode ~= 0 and bonusMultiplier ~= 0 and primaryResolvedValue ~= 0 then\n local bonusValue = primaryResolvedValue * bonusMultiplier\n local randomRange = math.abs(bonusValue) * 0.1\n\n if randomRange < 0.2 then\n local offsetTable = { -0.2, -0.1, 0, 0.1, 0.2 }\n bonusValue = bonusValue + offsetTable[math.random(1, #offsetTable)]\n else\n bonusValue = bonusValue * (0.9 + (math.random() * 0.2))\n end\n\n bonusValue = roundToTenth(bonusValue)\n if bonusValue ~= 0 then addOrMergePackedEffect(packedEffects, bonusCode, bonusValue) end\n end\n end\n\n local packedEffectText = buildPackedEffectString(packedEffects)\n\n if code ~= \"\" and actionName ~= \"\" and packedEffectText ~= \"\" then\n table.insert(actionRows, table.concat({ code, actionName, tostring(resolvedCostValue), packedEffectText }, \"§\"))\n end\n end\n end\n\n if isHEventPhase(triggerId, curPhase) then\n local heroineEffect = loadDualFlagNextMap(triggerId, vgHeroineEffect)\n local climaxCount = tonumber(heroineEffect.climaxCount) or 0\n\n if climaxCount >= 5 then\n local lang = getLanguage(triggerId)\n\n local endTextMap = {\n [1] = \"상황 종료\",\n [2] = \"End Situation\",\n [3] = \"状況終了\",\n }\n\n local endName = endTextMap[lang] or endTextMap[2]\n local endEffectText = buildPackedEffectString({\n { code = constEffectType.damage, value = 99999 }\n })\n\n table.insert(actionRows, table.concat({\n \"-9997\",\n endName,\n \"0\",\n endEffectText\n }, \"§\"))\n end\n\n elseif isMasturbationPhase(triggerId, curPhase) == false\n and isIncapacitatedPhase(triggerId, curPhase) == false\n and isExhibitionismPhase(triggerId, curPhase) == false\n and isPerformancePhase(triggerId, curPhase) == false\n and isPromiscuityPhase(triggerId, curPhase) == false\n and isIntimacyPhase(triggerId, curPhase) == false then\n local lang = getLanguage(triggerId)\n\n local surrenderTextMap = {\n [1] = \"위기 발생\",\n [2] = \"Crisis Occurs\",\n [3] = \"危機発生\",\n }\n\n local surrenderName = surrenderTextMap[lang] or surrenderTextMap[2]\n local surrenderEffectText = buildPackedEffectString({\n { code = constEffectType.recovery, value = -99999 }\n })\n\n table.insert(actionRows, table.concat({\n \"-9999\",\n surrenderName,\n \"0\",\n surrenderEffectText\n }, \"§\"))\n \n elseif isIncapacitatedPhase(triggerId, curPhase)\n or isPromiscuityPhase(triggerId, curPhase)\n or isIntimacyPhase(triggerId, curPhase)\n or isPerformancePhase(triggerId, curPhase)\n or isExhibitionismPhase(triggerId, curPhase) then\n\n local lang = getLanguage(triggerId)\n\n local extendTextMap = {\n [1] = \"상황 연장\",\n [2] = \"Situation Prolonged\",\n [3] = \"状況延長\",\n }\n\n local extendName = extendTextMap[lang] or extendTextMap[2]\n local extendEffectText = buildPackedEffectString({\n { code = constEffectType.encounterRecovery, value = 99999 }\n })\n\n table.insert(actionRows, table.concat({\n \"-9998\",\n extendName,\n \"0\",\n extendEffectText\n }, \"§\"))\n end\n\n cleanMsg = cleanMsg:gsub(\"<Heroine Action Setting>%s*[%s%S]-%s*</Heroine Action Setting>\", \"\")\n setChatVar(triggerId, \"vg_PlayDeck_Action\", table.concat(actionRows, \"|\"))\n return cleanMsg\nend\n\nfunction getLewdnessSexualBonusRatio(triggerId)\n local lewdnessLevel = tonumber(getLewdnessLevel(triggerId)) or 1\n lewdnessLevel = math.max(1, lewdnessLevel)\n\n return 0.2 + ((lewdnessLevel - 1) * 0.05)\nend\n\n-- ==========================================\n-- 상대 액션 설정\n-- ==========================================\nfunction buildEncounterActionSetting(triggerId)\n local sessionSlotList = getDualFlagSlotList(triggerId, vgSessionInfo.varName)\n local encounterSlotList = getDualFlagSlotList(triggerId, vgEncounterStat.varName)\n\n local encounterHP = tonumber(getResolvedDualFlagFromSlotList(encounterSlotList, vgEncounterStat.hp, 100)) or 0\n local encounterMaxHP = tonumber(getResolvedDualFlagFromSlotList(encounterSlotList, vgEncounterStat.hp_Max, 100)) or 0\n local difficultyCode = tonumber(getResolvedDualFlagFromSlotList(sessionSlotList, vgSessionInfo.difficulty, constSessionDifficulty.medium)) or constSessionDifficulty.medium\n\n local encounterHPPercent = getPercent(encounterHP, encounterMaxHP)\n local count = 2\n\n local curPhase = getPhase(triggerId)\n\n -- ==========================================\n -- bonus action 계산\n -- threshold 미만이면 0%\n -- threshold 이상이면 minChance ~ maxChance 사이에서 선형 증가\n -- ==========================================\n local function getCountPlusOneByHP(encounterHPPercent, threshold, minChance, maxChance)\n encounterHPPercent = clamp(tonumber(encounterHPPercent) or 0, 0, 100)\n threshold = clamp(tonumber(threshold) or 0, 0, 100)\n minChance = clamp(tonumber(minChance) or 0, 0, 100)\n maxChance = clamp(tonumber(maxChance) or 0, 0, 100)\n\n if maxChance < minChance then\n maxChance = minChance\n end\n\n if encounterHPPercent < threshold then\n return 0\n end\n\n local chance = 0\n\n if threshold >= 100 then\n chance = maxChance\n else\n local ratio = (encounterHPPercent - threshold) / (100 - threshold)\n ratio = clamp(ratio, 0, 1)\n\n chance = minChance + ((maxChance - minChance) * ratio)\n end\nprint(chance)\n return (math.random(10000) <= roundHalfUp(chance * 100)) and 1 or 0\n end\n\n if isIncapacitatedPhase(triggerId, curPhase)\n or isPerformancePhase(triggerId, curPhase)\n or isPromiscuityPhase(triggerId, curPhase)\n or isIntimacyPhase(triggerId, curPhase) then\n count = 2\n elseif isHEventPhase(triggerId, curPhase) then\n count = 3\n elseif difficultyCode == constSessionDifficulty.medium then\n count = 2 + getCountPlusOneByHP(encounterHPPercent, 50, 10, 30)\n\n elseif difficultyCode == constSessionDifficulty.high then\n count = 2 + getCountPlusOneByHP(encounterHPPercent, 25, 10, 40)\n\n elseif difficultyCode == constSessionDifficulty.elite then\n count = 2 + getCountPlusOneByHP(encounterHPPercent, 0, 20, 80)\n\n elseif difficultyCode == constSessionDifficulty.boss then\n count = 2 + getCountPlusOneByHP(encounterHPPercent, 0, 80, 100)\n end\n\n local encounterSettingLines = {}\n table.insert(\n encounterSettingLines,\n \"- In this response, the encounter side must create exactly \" ..\n tostring(count) ..\n ((count == 1) and \" encounter action.\" or \" encounter actions.\")\n )\n\n -- ==========================================\n -- 허용 Effect Type 설정\n -- ==========================================\n local effectTypeList = getEncounterEffectTypeListByPhase(triggerId, curPhase)\n\n -- Guard는 50% 확률로 제외\n for i = #effectTypeList, 1, -1 do\n if effectTypeList[i] == \"Guard\" then\n if math.random(2) == 1 then\n table.remove(effectTypeList, i)\n end\n break\n end\n end\n\n local allowedMap = {}\n\n for _, effectName in ipairs(effectTypeList) do\n allowedMap[effectName] = true\n end\n\n if #effectTypeList == 0 then\n setChatVar(triggerId, \"vg_Encounter_Setting\", \"\")\n setChatVar(triggerId, \"vg_Encounter_Guide\", \"\")\n\n return\n end\n\n local function buildEffectTypeText(list)\n if #list == 1 then\n return list[1]\n elseif #list == 2 then\n return list[1] .. \" or \" .. list[2]\n elseif #list >= 3 then\n local text = \"\"\n for i = 1, #list do\n if i == 1 then\n text = list[i]\n elseif i == #list then\n text = text .. \", or \" .. list[i]\n else\n text = text .. \", \" .. list[i]\n end\n end\n return text\n end\n return \"\"\n end\n\n local effectTypeText = buildEffectTypeText(effectTypeList)\n local guideLines = {}\n local effectTypeDirectionLines = {}\n\n if effectTypeText ~= \"\" then\n table.insert(\n guideLines,\n \"- Effect Type: Must be exactly \" ..\n (#effectTypeList >= 2 and \"one of \" or \"\") ..\n effectTypeText ..\n \". No other values are allowed. Effect Type and Value must be written in English.\"\n )\n\n table.insert(guideLines, buildEffectTypeLiteralGuideLine(effectTypeList))\n end\n\n local typeRuleLines = {}\n\n if allowedMap[\"Pressure\"] then\n if isAbusePhase(triggerId, curPhase) then\n table.insert(typeRuleLines, \"- Coercion: Actions used to corner the heroine through authority, status, procedure, leverage, or situational control, pushing her into a more disadvantaged or compliant position. Effect Type can only be Pressure.\")\n elseif isConfrontationPhase(triggerId, curPhase) then\n table.insert(typeRuleLines, \"- Intimidation: Actions used to corner the heroine through hostile pressure, threats, aggressive advance, public pressure, exposure, or conflict-driven situational leverage, pushing her into a more disadvantaged or trapped position. Effect Type can only be Pressure.\")\n end\n end\n\n if allowedMap[\"Rhetoric\"] then\n if isAbusePhase(triggerId, curPhase) then\n table.insert(typeRuleLines, \"- Justification: Actions used to wear down the heroine's resistance through justification, minimization, procedural language, deflection, authority-based framing, or other manipulative rhetoric. Effect Type can only be Rhetoric.\")\n elseif isConfrontationPhase(triggerId, curPhase) then\n table.insert(typeRuleLines, \"- Verbal Aggression: Actions used to wear down the heroine through accusation, taunting, humiliation, blame, verbal threats, degrading remarks, or other hostile rhetoric arising from the dispute itself. Effect Type can only be Rhetoric.\")\n end\n end\n\n if allowedMap[\"Subdual\"] then\n table.insert(typeRuleLines, \"- Encroachment: Actions used to impose upon the heroine, erode her resolve, or otherwise push the situation closer to her losing the ability or will to resist. Effect Type can only be Subdual.\")\n end\n\n if allowedMap[\"Damage\"] then\n table.insert(typeRuleLines, \"- Attack: Actions used to directly attack the heroine. Effect Type can only be Damage.\")\n end\n\n if allowedMap[\"Guard\"] then\n table.insert(typeRuleLines, \"- Defense: Purely defensive actions such as dodging, blocking, parrying, guarding, or brief deflection. Any grabbing, restraining, grappling, or control-oriented action is not Guard. Effect Type can only be Guard.\")\n end\n\n if allowedMap[\"Suggestion\"] then\n table.insert(typeRuleLines, \"- Command: Actions used to impose, deepen, reinforce, or trigger hypnotic commands, mental manipulation, mind-control directives, or similar sexual suggestions that make the heroine obey, accept, perform, or fail to resist sexual conduct. Effect Type can only be Suggestion.\")\n end\n\n if allowedMap[\"Defilement\"] then\n table.insert(typeRuleLines, \"- Desecration: Actions used to sexually degrade or sully the heroine through violent sexual handling of her body, pushing the current violation further through humiliation, soiling, or depraved use. Effect Type can only be Defilement.\")\n end\n\n if allowedMap[\"Invasion\"] then\n table.insert(typeRuleLines, \"- Breach: Actions used to force past the heroine's bodily boundaries or resistance through sexually invasive pressure, pushing the current violation further through coercive intrusion or forced access. Effect Type can only be Invasion.\")\n end\n\n if allowedMap[\"Affection\"] then\n table.insert(typeRuleLines, \"- Invitation: Affectionate sexual contact that deepens intimacy with the heroine. Effect Type can only be Affection.\")\n end\n\n if allowedMap[\"Union\"] then\n table.insert(typeRuleLines, \"- Entwinement: Reciprocal sexual contact that deepens bodily intimacy with the heroine. Effect Type can only be Union.\")\n end\n\n if allowedMap[\"Humiliation\"] then\n table.insert(typeRuleLines, \"- Harassment: Actions used to sexually harass the heroine without physical contact through obscene remarks, objectifying reactions, humiliating commentary, lewd prompting, or other sexualized audience response. Effect Type can only be Humiliation.\")\n end\n\n if allowedMap[\"Escalation\"] then\n table.insert(typeRuleLines, \"- Demand: Actions used to pressure the heroine into a more explicit, exposed, degrading, or extended sexual performance without physical contact. Effect Type can only be Escalation.\")\n end\n\n if allowedMap[\"Interception\"] then\n table.insert(typeRuleLines, \"- Interdiction: Actions used to block, delay, separate, restrain, misdirect, or cut off those attempting to rescue the heroine, preventing extraction or reunion. They target the rescuers or the rescue attempt itself, not the heroine directly. Effect Type can only be Interception.\")\n end\n\n if allowedMap[\"Sexual\"] then\n table.insert(typeRuleLines, \"- Molestation: Actions must inherently and directly cause sexual arousal in the heroine, either by directly stimulating clearly erogenous zones (e.g., breasts, nipples, clitoris, crotch, vulva, vagina, butt, anus) or through explicit sexual acts involving such stimulation (e.g., groping, kissing, licking, penetration), or by other means that directly and immediately cause sexual arousal even without direct physical stimulation. Any force, restraint, domination, rough handling, or vague sensual action that does not directly and immediately cause sexual arousal, and instead targets only non-erogenous areas (e.g., wrists, arms, or hair), is not Sexual. Effect Type can only be Sexual.\")\n end\n\n if #typeRuleLines > 0 then\n table.insert(guideLines, \"\")\n table.insert(guideLines, \"### Type Rules\")\n\n for _, line in ipairs(typeRuleLines) do\n table.insert(guideLines, line)\n end\n end\n\n if isHarassmentPhase(triggerId, curPhase) or isAbusePhase(triggerId, curPhase) or isConfrontationPhase(triggerId, curPhase) or isHypnosisPhase(triggerId, curPhase) then\n table.insert(guideLines, \"- These actions may include sexual harassment or molestation, but must not turn into a fight or full combat.\")\n end\n\n if isHarassmentPhase(triggerId, curPhase) then\n table.insert(guideLines, \"- Encroachment and Molestation must be kept clearly distinct: Molestation focuses on direct sexual arousal, while Encroachment focuses on pressure, control, and the erosion of resistance.\")\n elseif isAbusePhase(triggerId, curPhase) then\n table.insert(guideLines, \"- Coercion, Justification, and Molestation must be kept clearly distinct: Coercion focuses on pressuring or cornering the heroine; Justification focuses on wearing down the heroine's resistance through framing, procedural language, or manipulative rhetoric; and Molestation focuses on directly imposing sexualized contact or stimulation.\")\n elseif isConfrontationPhase(triggerId, curPhase) then\n table.insert(guideLines, \"- Intimidation, Verbal Aggression, and Molestation must remain distinct: Intimidation focuses on cornering or threatening the heroine, Verbal Aggression on accusation, taunting, or humiliation, and Molestation on direct sexual contact or stimulation within the dispute.\")\n elseif isIncapacitatedPhase(triggerId, curPhase) or isPromiscuityPhase(triggerId, curPhase) then\n table.insert(guideLines, \"- One encounter action must be Desecration and the other must be Breach. They must differ in mode of sexual violation, not merely in target area, and both must be overt, very explicit sexual actions (e.g., kiss, penetration, non-penetrative sex) that clearly target sexual orifices (vagina, mouth, or anus) or erogenous zones (e.g., breasts).\")\n table.insert(guideLines, \"- If a penis or a penis-like organ capable of ejaculation is already engaged in a sexual act (e.g., penetration, non-penetrative sex), the action must actively incorporate ejaculation as part of the ongoing act.\")\n elseif isPerformancePhase(triggerId, curPhase) then\n table.insert(guideLines, \"- These actions must remain strictly non-contact. They must not physically touch the heroine or move into a situation where physical contact could plausibly occur.\")\n table.insert(guideLines, \"- One encounter action must be Harassment and the other must be Demand. Both must be sexual in nature.\")\n elseif isHypnosisPhase(triggerId, curPhase) then\n table.insert(guideLines, \"- Command and Molestation must be kept clearly distinct: Command is when the mental influence makes the heroine do something sexual, while Molestation is when the encounter directly does something sexual to the heroine.\")\n table.insert(guideLines, \"- Command must not be used in a non-sexual context, and must remain strictly sexual, including sexual compliance, sexual acts, sexual exposure, sexual posture, sexual speech, or the loss of sexual resistance.\")\n elseif isRescuePhase(triggerId, curPhase) then\n table.insert(guideLines, \"- Interdiction and Molestation must remain clearly distinct: Interdiction is aimed at the rescuers or the rescue attempt, while Molestation is aimed directly at the heroine.\")\n elseif isIntimacyPhase(triggerId, curPhase) then\n table.insert(guideLines, \"- One encounter action must be Invitation and the other must be Entwinement. They must differ in mode of sexual act, not merely in target area, and both must be overt, very explicit sexual actions (e.g., kiss, penetration, non-penetrative sex) that clearly involve sexual orifices (vagina, mouth, or anus) or erogenous zones (e.g., breasts).\")\n table.insert(guideLines, \"- If a penis or a penis-like organ capable of ejaculation is already engaged in a sexual act (e.g., penetration, non-penetrative sex), the action must actively incorporate ejaculation as part of the ongoing act.\")\n elseif isHEventPhase(triggerId, curPhase) then\n table.insert(guideLines, \"- All encounter actions must be overt, very explicit sexual actions (e.g., kiss, penetration, non-penetrative sex) that clearly target sexual orifices (vagina, mouth, or anus) or erogenous zones (e.g., breasts).\")\n table.insert(guideLines, \"- If a penis or a penis-like organ capable of ejaculation is already engaged in a sexual act (e.g., penetration, non-penetrative sex), the action must actively incorporate ejaculation as part of the ongoing act.\")\n end\n\n if count == 3 and allowedMap[\"Sexual\"] then\n table.insert(guideLines, \"- At least one encounter action must be Molestation.\")\n end\n\n setChatVar(triggerId, \"vg_Encounter_Setting\", table.concat(encounterSettingLines, \"\\n\"))\n setChatVar(triggerId, \"vg_Encounter_Guide\", table.concat(guideLines, \"\\n\"))\nend\n\nlocal function parseEncounterActionSettingBlock(triggerId, cleanMsg)\n local actionSettingRaw = cleanMsg:match(\"<Encounter Action Setting>%s*([%s%S]-)%s*</Encounter Action Setting>\")\n local actionRows = {}\n\n local sessionSlotList = getDualFlagSlotList(triggerId, vgSessionInfo.varName)\n local traitValue = tonumber(getResolvedDualFlagFromSlotList(sessionSlotList, vgSessionInfo.trait, constEncounterTrait.none)) or constEncounterTrait.none\n\n local curPhase = getPhase(triggerId)\n local encounterStatValue = getEncounterStatValue(triggerId)\n\n if isHEventPhase(triggerId, curPhase) then\n local heroineStat = loadDualFlagCurrentMap(triggerId, vgHeroineStat)\n local maxArousal = math.max(1, tonumber(heroineStat.arousal_Max) or 0)\n\n -- H 이벤트 전용:\n -- 절정 기준 50% × 0.26 = 흥분 최대치의 약 13%\n -- 민감도 +15%까지 받으면 최종 약 15%\n encounterStatValue = roundToTenth(maxArousal * 0.5 * 0.26)\n else\n local levelNum = tonumber(getResolvedDualFlagFromSlotList(sessionSlotList, vgSessionInfo.level, 1)) or 1\n\n local levelTier = math.floor(math.max(0, levelNum) / 5)\n local bonusRate = clamp(levelTier * 0.01, 0, 0.1)\n\n local challengeStatMultiplier = tonumber(safeGet(triggerId, \"vg_Challenge_Stat\", \"1\")) or 1\n\n encounterStatValue = roundToTenth(encounterStatValue * (1 + bonusRate) * challengeStatMultiplier)\n end\n\n local lang = getLanguage(triggerId)\n\n local passiveSourceName = \"\"\n local passiveActionName = \"\"\n local passiveEffectTypeCode = 0\n local passiveEffectValue = 0\n\n local criticalApplied = false\n\n local function buildPassiveTraitText(traitValue, lang)\n if traitValue == constEncounterTrait.oppressive then\n if lang == 1 then\n return \"상황\", \"위압감\"\n elseif lang == 3 then\n return \"状況\", \"威圧感\"\n else\n return \"Situation\", \"Oppressive Presence\"\n end\n end\n\n return \"\", \"\"\n end\n\n local function rollPassiveTraitValue(encounterStatValue)\n local baseValue = encounterStatValue * 0.1\n local varianceValue = baseValue * 0.05\n\n if varianceValue < 0.1 then\n local offsetTable = { -0.1, 0, 0.1 }\n baseValue = baseValue + offsetTable[math.random(1, #offsetTable)]\n else\n baseValue = baseValue * (0.95 + (math.random() * 0.1))\n end\n\n return roundToTenth(math.max(0.1, baseValue))\n end\n\n local function addEffectByName(packedEffects, effectName, fieldValue, actionName, isCritical)\n effectName = trim(effectName or \"\")\n fieldValue = trim(fieldValue or \"\")\n\n if effectName == \"\" or fieldValue == \"\" then\n return false\n end\n\n local effectTypeCode = getEffectTypeCode(effectName)\n if effectTypeCode == 0 then\n return false\n end\n\n local effectValue = convertLevelTextToValue(\n triggerId,\n \"encounter\",\n effectTypeCode,\n fieldValue,\n 1,\n actionName,\n 0,\n encounterStatValue\n )\n\n if effectValue ~= 0 then\n if isCritical and effectTypeCode ~= constEffectType.guard then\n effectValue = roundToTenth(effectValue * 1.5)\n criticalApplied = true\n end\n\n addOrMergePackedEffect(packedEffects, effectTypeCode, effectValue)\n return true\n end\n\n return false\n end\n\n local function tryParseFallbackEffect(fieldMap, packedEffects, actionName, isCritical)\n local rawEffectType = trim(fieldMap[\"Effect Type\"] or \"\")\n local rawValue = trim(fieldMap[\"Value\"] or fieldMap[\"Effect Value\"] or fieldMap[\"Level\"] or \"\")\n\n if rawEffectType == \"\" then\n return false\n end\n\n local effectName = rawEffectType\n local fieldValue = rawValue\n\n -- \"Effect Type: Damage: Low\" 형태 대응\n local inlineName, inlineValue = rawEffectType:match(\"^%s*(.-)%s*:%s*(.-)%s*$\")\n if inlineName and inlineValue and getEffectTypeCode(inlineName) ~= 0 then\n effectName = trim(inlineName)\n\n if fieldValue == \"\" then\n fieldValue = trim(inlineValue)\n end\n end\n\n if getEffectTypeCode(effectName) == 0 then\n return false\n end\n\n if fieldValue == \"\" then\n fieldValue = getRandomFallbackEffectLevelText()\n end\n\n return addEffectByName(packedEffects, effectName, fieldValue, actionName, isCritical)\n end\n\n if traitValue == constEncounterTrait.oppressive then\n passiveSourceName, passiveActionName = buildPassiveTraitText(traitValue, lang)\n passiveEffectValue = rollPassiveTraitValue(encounterStatValue)\n\n if traitValue == constEncounterTrait.oppressive then\n passiveEffectTypeCode = constEffectType.exhaustion\n end\n end\n\n if passiveSourceName ~= \"\" and passiveActionName ~= \"\" and passiveEffectTypeCode ~= 0 and passiveEffectValue > 0 then\n local passiveEffects = {}\n\n addOrMergePackedEffect(passiveEffects, passiveEffectTypeCode, passiveEffectValue)\n\n table.insert(actionRows, table.concat({\n passiveSourceName,\n passiveActionName,\n buildPackedEffectString(passiveEffects)\n }, \"§\"))\n end\n\n if actionSettingRaw and trim(actionSettingRaw) ~= \"\" then\n local effectOrder = getEncounterEffectTypeListByPhase(triggerId, curPhase)\n\n for entry in tostring(actionSettingRaw):gmatch(\"%b[]\") do\n local line = trim(entry:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\"))\n\n local orderedFields = parseOrderedFieldsWithFallback(line, {\n \"Source\",\n \"Action\"\n })\n\n local fieldMap = {\n Source = trim(orderedFields[\"source\"] or \"\"),\n Action = trim(orderedFields[\"action\"] or \"\")\n }\n\n for part in line:gmatch(\"([^|]+)\") do\n local key, value = part:match(\"^%s*(.-)%s*:%s*(.-)%s*$\")\n if key then\n local rawKey = trim(key)\n local rawValue = trim(value or \"\")\n\n fieldMap[rawKey] = rawValue\n\n local normalizedKey = normalizeEncounterActionToEffectTypeName(rawKey)\n if normalizedKey ~= rawKey and fieldMap[normalizedKey] == nil then\n fieldMap[normalizedKey] = rawValue\n end\n end\n end\n\n local sourceName = normalizeIndexedSourceName(fieldMap[\"Source\"] or \"\")\n local actionName = trim(fieldMap[\"Action\"] or \"\")\n\n actionName = actionName:gsub('\"', \"\"):gsub(\"'\", \"\")\n\n local packedEffects = {}\n local isCritical = rollCriticalChance(triggerId, \"encounter\")\n criticalApplied = false\n\n -- 1차: 기존 방식 그대로\n for _, fieldName in ipairs(effectOrder) do\n addEffectByName(\n packedEffects,\n fieldName,\n fieldMap[fieldName] or \"\",\n actionName,\n isCritical\n )\n end\n\n -- 2차: 아무 것도 못 읽었을 때만 fallback\n if next(packedEffects) == nil then\n tryParseFallbackEffect(fieldMap, packedEffects, actionName, isCritical)\n end\n\n if criticalApplied then\n actionName = \"🍀 \" .. actionName\n end\n\n local packedEffectText = buildPackedEffectString(packedEffects)\n\n if sourceName ~= \"\" and actionName ~= \"\" and packedEffectText ~= \"\" then\n table.insert(actionRows, table.concat({\n sourceName,\n actionName,\n packedEffectText\n }, \"§\"))\n end\n end\n end\n\n do\n local codeA = 0\n local codeB = 0\n\n if isIncapacitatedPhase(triggerId, curPhase) or isPromiscuityPhase(triggerId, curPhase) or isIntimacyPhase(triggerId, curPhase) then\n codeA = constEffectType.defilement\n codeB = constEffectType.invasion\n\n elseif isPerformancePhase(triggerId, curPhase) then\n codeA = constEffectType.humiliation\n codeB = constEffectType.escalation\n end\n\n if codeA ~= 0 and codeB ~= 0 then\n local targets = {}\n\n for rowIndex, row in ipairs(actionRows) do\n local parts = splitSection(row)\n local effectRaw = trim(parts[3] or \"\")\n\n local hasA = false\n local hasB = false\n\n for token in effectRaw:gmatch(\"([^/]+)\") do\n local codeStr = trim(token):match(\"^([%-+]?%d+)%s*:\")\n local code = tonumber(codeStr) or 0\n\n if code == codeA then\n hasA = true\n elseif code == codeB then\n hasB = true\n end\n end\n\n -- A/B 중 정확히 하나만 가진 row만 후보로 잡음\n if hasA and not hasB then\n table.insert(targets, {\n rowIndex = rowIndex,\n code = codeA,\n })\n\n elseif hasB and not hasA then\n table.insert(targets, {\n rowIndex = rowIndex,\n code = codeB,\n })\n end\n end\n\n -- 대상 encounter action이 정확히 2개고, 둘 다 같은 code면 하나만 반대로 변경\n if #targets == 2 and targets[1].code == targets[2].code then\n local picked = targets[math.random(2)]\n local fromCode = picked.code\n local toCode = fromCode == codeA and codeB or codeA\n\n local parts = splitSection(actionRows[picked.rowIndex])\n local effectRaw = trim(parts[3] or \"\")\n\n local newTokens = {}\n local changed = false\n\n for token in effectRaw:gmatch(\"([^/]+)\") do\n local codeStr, valueStr = trim(token):match(\"^([%-+]?%d+)%s*:%s*(.-)%s*$\")\n local code = tonumber(codeStr) or 0\n\n if not changed and code == fromCode then\n table.insert(newTokens, tostring(toCode) .. \":\" .. trim(valueStr or \"0\"))\n changed = true\n else\n table.insert(newTokens, trim(token))\n end\n end\n\n if changed then\n parts[3] = table.concat(newTokens, \"/\")\n actionRows[picked.rowIndex] = table.concat(parts, \"§\")\n end\n end\n end\n end\n\n cleanMsg = cleanMsg:gsub(\"<Encounter Action Setting>%s*[%s%S]-%s*</Encounter Action Setting>\", \"\")\n setChatVar(triggerId, \"vg_Encounter_Action\", table.concat(actionRows, \"|\"))\n return cleanMsg\nend\n\n-- ==========================================\n-- 동료 액션\n-- ==========================================\nlocal function parseCompanionActionSettingBlock(triggerId, cleanMsg)\n local actionSettingRaw = cleanMsg:match(\"<Companion Action Setting>%s*([%s%S]-)%s*</Companion Action Setting>\")\n local actionRows = {}\n\n if actionSettingRaw and trim(actionSettingRaw) ~= \"\" then\n for entry in tostring(actionSettingRaw):gmatch(\"%b[]\") do\n local line = trim(entry:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\"))\n local fieldMap = {}\n\n for part in line:gmatch(\"([^|]+)\") do\n local key, value = part:match(\"^%s*(.-)%s*:%s*(.-)%s*$\")\n if key then\n fieldMap[trim(key)] = trim(value or \"\")\n end\n end\n\n local sourceName = normalizeIndexedSourceName(fieldMap[\"Source\"] or \"\")\n local actionName = trim(fieldMap[\"Action\"] or \"\"):gsub('\"', \"\"):gsub(\"'\", \"\")\n\n if sourceName ~= \"\" and actionName ~= \"\" then\n table.insert(actionRows, table.concat({\n sourceName,\n actionName\n }, \"§\"))\n end\n end\n end\n\n cleanMsg = cleanMsg:gsub(\"<Companion Action Setting>%s*[%s%S]-%s*</Companion Action Setting>\", \"\")\n setChatVar(triggerId, \"vg_Companion_Action\", table.concat(actionRows, \"|\"))\n return cleanMsg\nend\n\n-- ==========================================\n-- 시작 설정\n-- ==========================================\nfunction initVGSetting(triggerId)\n setChatVar(triggerId, \"vg_Init\", \"0\")\n setChatVar(triggerId, \"vg_Language\", \"1\")\n setChatVar(triggerId, \"vg_ArousalResponse\", \"1\")\n setChatVar(triggerId, \"vg_startKnowledge\", \"2\")\n\n setChatVar(triggerId, \"vg_startBreastEmoji\", \"⬛\")\n setChatVar(triggerId, \"vg_startOralEmoji\", \"⬛\")\n setChatVar(triggerId, \"vg_startClitorisEmoji\", \"⬛\")\n setChatVar(triggerId, \"vg_startVaginalEmoji\", \"⬛\")\n setChatVar(triggerId, \"vg_startAnalEmoji\", \"⬛\")\n\n setChatVar(triggerId, \"vg_Start_Message\", \"\")\n\n setChatVar(triggerId, \"vg_StartEnhancementBase\", constEnhancementType.attack)\n\n setChatVar(triggerId, \"vg_generatedHTML\", \"\")\nend\n\nfunction generateStartInput(triggerId)\n\n local initFlag = safeGet(triggerId, \"vg_Init\", \"\")\n if initFlag ~= \"1\" then\n\n local sensitivityKeys = { \"vg_startBreast\", \"vg_startOral\", \"vg_startClitoris\", \"vg_startVaginal\", \"vg_startAnal\" }\n \n -- 민감도 검사하기\n for _, key in ipairs(sensitivityKeys) do\n local value = safeGet(triggerId, key, \"\")\n if value == \"\" or not tonumber(value) then\n alertError(triggerId, \"⚠️ 민감도를 먼저 설정한 후 시작 버튼을 눌러주시길 바랍니다.\")\n return\n end\n end\n\n -- ==========================================\n -- 변수 초기화\n -- ==========================================\n local function setPreStoryValue(varName)\n local currentValue = safeGet(triggerId, varName, \"\")\n if currentValue ~= \"\" then\n setChatVar(triggerId, varName, \"Pre-Story, \" .. currentValue)\n else\n setChatVar(triggerId, varName, \"\")\n end\n end\n\n local function normalizeInitChatVar(varName)\n local val = getChatVar(triggerId, varName)\n\n if val == nil or val == \"null\" or val == \"nil\" then\n setChatVar(triggerId, varName, \"\")\n end\n end\n\n local language = getLanguage(triggerId)\n\n setChatVar(triggerId, \"vg_Init\", \"1\")\n setChatVar(triggerId, \"vg_Mode\", tostring(constMode.vg))\n \n batchSetDualFlags(triggerId, vgContext.varName, {\n { position = vgContext.phase, value = constPhase.standby, isNext = false },\n { position = vgContext.act, value = 1, isNext = false },\n { position = vgContext.lewdness, value = 0, isNext = false },\n { position = vgContext.sexualTrait_Num, value = 0, isNext = false },\n })\n\n setPhase(triggerId, tostring(constPhase.standby))\n setChatVar(triggerId, \"vg_Resolution_Flag\", tostring(constResolution.storyGoal))\n\n setChatVar(triggerId, \"vg_SexKnowledge\", safeGet(triggerId, \"vg_startKnowledge\", \"0\"))\n\n local breast = tostring(tonumber(safeGet(triggerId, \"vg_startBreast\", \"0\")) or 0)\n local oral = tostring(tonumber(safeGet(triggerId, \"vg_startOral\", \"0\")) or 0)\n local clitoris = tostring(tonumber(safeGet(triggerId, \"vg_startClitoris\", \"0\")) or 0)\n local vaginal = tostring(tonumber(safeGet(triggerId, \"vg_startVaginal\", \"0\")) or 0)\n local anal = tostring(tonumber(safeGet(triggerId, \"vg_startAnal\", \"0\")) or 0)\n\n batchSetDualFlags(triggerId, vgSensitivity.varName, {\n { position = vgSensitivity.breast, value = breast, isNext = false },\n { position = vgSensitivity.oral, value = oral, isNext = false },\n { position = vgSensitivity.clitoris, value = clitoris, isNext = false },\n { position = vgSensitivity.vaginal, value = vaginal, isNext = false },\n { position = vgSensitivity.anal, value = anal, isNext = false },\n })\n\n setPreStoryValue(\"vg_FirstKiss\")\n setPreStoryValue(\"vg_FirstFellatio\")\n setPreStoryValue(\"vg_FirstVirgin\")\n setPreStoryValue(\"vg_FirstAnal\")\n\n normalizeInitChatVar(\"vg_FirstMasturbation\")\n normalizeInitChatVar(\"vg_FirstOrgasm\")\n \n batchSetDualFlags(triggerId, vgHeroineStat.varName, {\n { position = vgHeroineStat.hp, value = 80, isNext = false },\n { position = vgHeroineStat.hp_Max, value = 80, isNext = false },\n { position = vgHeroineStat.resource, value = 80, isNext = false },\n { position = vgHeroineStat.resource_Max, value = 80, isNext = false },\n { position = vgHeroineStat.arousal, value = 0, isNext = false },\n { position = vgHeroineStat.arousal_Max, value = 80, isNext = false },\n\n { position = vgHeroineStat.strength, value = 10, isNext = false },\n { position = vgHeroineStat.reflex, value = 10, isNext = false },\n { position = vgHeroineStat.willpower, value = 10, isNext = false },\n })\n\n setChatVar(triggerId, \"vg_Heroine_Effect\", \"\")\n\n do\n local baseType = tonumber(safeGet(triggerId, \"vg_StartEnhancementBase\", \"0\")) or 0\n\n local allSubTypes = {\n constEnhancementType.attack,\n constEnhancementType.defense,\n constEnhancementType.charisma,\n constEnhancementType.chastity,\n constEnhancementType.sensuality,\n constEnhancementType.spirit,\n constEnhancementType.focus,\n constEnhancementType.resilience,\n constEnhancementType.discipline,\n constEnhancementType.luck,\n constEnhancementType.endurance,\n }\n\n local baseNames = {\n [1] = { -- Korean\n [constEnhancementType.attack] = \"전투 감각\",\n [constEnhancementType.defense] = \"방어 감각\",\n [constEnhancementType.charisma] = \"카리스마\",\n [constEnhancementType.chastity] = \"철벽\",\n [constEnhancementType.focus] = \"집중력\",\n [constEnhancementType.resilience] = \"회복력\",\n [constEnhancementType.discipline] = \"자제력\",\n [constEnhancementType.luck] = \"행운\",\n },\n [2] = { -- English\n [constEnhancementType.attack] = \"Combat Sense\",\n [constEnhancementType.defense] = \"Defensive Sense\",\n [constEnhancementType.charisma] = \"Charisma\",\n [constEnhancementType.chastity] = \"Iron Wall\",\n [constEnhancementType.focus] = \"Focus\",\n [constEnhancementType.resilience] = \"Recovery\",\n [constEnhancementType.discipline] = \"Self-Control\",\n [constEnhancementType.luck] = \"Fortune\",\n },\n [3] = { -- Japanese\n [constEnhancementType.attack] = \"戦闘感覚\",\n [constEnhancementType.defense] = \"防御感覚\",\n [constEnhancementType.charisma] = \"カリスマ\",\n [constEnhancementType.chastity] = \"鉄壁\",\n [constEnhancementType.focus] = \"集中力\",\n [constEnhancementType.resilience] = \"回復力\",\n [constEnhancementType.discipline] = \"自制力\",\n [constEnhancementType.luck] = \"幸運\",\n },\n }\n\n local subPrefixes = {\n [1] = { -- Korean\n [constEnhancementType.attack] = \"맹렬한\",\n [constEnhancementType.defense] = \"견고한\",\n [constEnhancementType.charisma] = \"위압적인\",\n [constEnhancementType.chastity] = \"단호한\",\n [constEnhancementType.sensuality] = \"관능적인\",\n [constEnhancementType.spirit] = \"강인한\",\n [constEnhancementType.focus] = \"날카로운\",\n [constEnhancementType.resilience] = \"끈질긴\",\n [constEnhancementType.discipline] = \"엄격한\",\n [constEnhancementType.luck] = \"축복받은\",\n [constEnhancementType.endurance] = \"인내하는\",\n },\n [2] = { -- English\n [constEnhancementType.attack] = \"Fierce\",\n [constEnhancementType.defense] = \"Sturdy\",\n [constEnhancementType.charisma] = \"Imposing\",\n [constEnhancementType.chastity] = \"Resolute\",\n [constEnhancementType.sensuality] = \"Sensual\",\n [constEnhancementType.spirit] = \"Strong-Willed\",\n [constEnhancementType.focus] = \"Sharp\",\n [constEnhancementType.resilience] = \"Tenacious\",\n [constEnhancementType.discipline] = \"Strict\",\n [constEnhancementType.luck] = \"Blessed\",\n [constEnhancementType.endurance] = \"Enduring\",\n },\n [3] = { -- Japanese\n [constEnhancementType.attack] = \"猛烈な\",\n [constEnhancementType.defense] = \"堅固な\",\n [constEnhancementType.charisma] = \"威圧的な\",\n [constEnhancementType.chastity] = \"断固たる\",\n [constEnhancementType.sensuality] = \"官能的な\",\n [constEnhancementType.spirit] = \"強靭な\",\n [constEnhancementType.focus] = \"鋭い\",\n [constEnhancementType.resilience] = \"粘り強い\",\n [constEnhancementType.discipline] = \"厳格な\",\n [constEnhancementType.luck] = \"祝福された\",\n [constEnhancementType.endurance] = \"忍耐強い\",\n },\n }\n\n local function getEffectValueByTier(effectType, tier, isMain)\n if effectType == constEnhancementType.focus then\n local values = isMain and {0.5, 0.6, 0.7} or {0.2, 0.3, 0.4}\n return string.format(\"%.1f\", values[tier] or values[1])\n end\n\n if effectType == constEnhancementType.discipline\n or effectType == constEnhancementType.luck\n or effectType == constEnhancementType.endurance then\n local values = isMain and {4, 5, 6} or {2, 3, 4}\n return tostring(values[tier] or values[1])\n end\n\n local values = isMain and {1.2, 1.3, 1.4} or {0.5, 0.6, 0.7}\n return string.format(\"%.1f\", values[tier] or values[1])\n end\n\n local subType = baseType\n while subType == baseType do\n subType = allSubTypes[math.random(1, #allSubTypes)]\n end\n\n local mainTier = math.random(1, 3)\n local subTier = 4 - mainTier\n\n local baseName = (((baseNames[language] or {})[baseType]) or ((baseNames[2] or {})[baseType]) or \"Enhancement\")\n local subPrefix = (((subPrefixes[language] or {})[subType]) or ((subPrefixes[2] or {})[subType]) or \"Enhanced\")\n\n local enhancementName = \"\"\n if language == 3 then\n enhancementName = subPrefix .. baseName\n else\n enhancementName = subPrefix .. \" \" .. baseName\n end\n\n local mainValue = getEffectValueByTier(baseType, mainTier, true)\n local subValue = getEffectValueByTier(subType, subTier, false)\n\n batchSetDualFlags(triggerId, vgEnhancement.varName, {\n { position = vgEnhancement.slot1_Name, value = enhancementName, isNext = false },\n { position = vgEnhancement.slot1_Effect1, value = tostring(baseType) .. \":\" .. mainValue, isNext = false },\n { position = vgEnhancement.slot1_Effect2, value = tostring(subType) .. \":\" .. subValue, isNext = false },\n })\n end\n\n setChatVar(triggerId, \"vg_Reproductive\", \"\")\n\n local cardCounts = {\n {constCardType.primary, 5},\n {constCardType.secondary, 4},\n {constCardType.focus, 2},\n {constCardType.charge, 1}\n }\n\n for _, cardInfo in ipairs(cardCounts) do\n local cardType = cardInfo[1]\n local count = cardInfo[2]\n\n for i = 1, count do\n addCardToDeck(triggerId, cardType, 1)\n end\n end\n\n resetSession(triggerId)\n\n setChatVar(triggerId, \"vg_State_Setting\", \"\")\n setChatVar(triggerId, \"vg_State_Detail\", \"\")\n\n setChatVar(triggerId, \"vg_Memory_Num\", \"0\")\n setChatVar(triggerId, \"vg_Memory_Content\", \"\")\n setChatVar(triggerId, \"vg_Memory_Display\", \"\")\n\n setChatVar(triggerId, \"vg_Story_Num\", \"0\")\n setChatVar(triggerId, \"vg_Story_Content\", \"\")\n setChatVar(triggerId, \"vg_Story_Display\", \"\")\n\n setChatVar(triggerId, \"vg_Reward_Choice\", \"\")\n setChatVar(triggerId, \"vg_Reward_Count\", \"0\")\n setChatVar(triggerId, \"vg_Reward_Selected\", \"\")\n setChatVar(triggerId, \"vg_Reward_Log\", \"\")\n\n setChatVar(triggerId, \"vg_SexualTrait_Content\", \"\")\n setChatVar(triggerId, \"vg_Corruption_Content\", \"\")\n\n setChatVar(triggerId, \"vg_Goal_Short\", \"\")\n setChatVar(triggerId, \"vg_Goal_Long\", \"\")\n setChatVar(triggerId, \"vg_Story_Directive\", \"\")\n setChatVar(triggerId, \"vg_Story_Canon\", \"\")\n\n setChatVar(triggerId, \"vg_MoanDirective\", \"0\")\n setChatVar(triggerId, \"vg_NonlethalHuman\", \"0\")\n setChatVar(triggerId, \"vg_MagicalGirlTransform\", \"0\")\n\n buildSexualProfile(triggerId)\n buildCurrentState(triggerId)\n\n generateHTML(triggerId)\n\n -- ==========================================\n -- 인풋 입력\n -- ==========================================\n local startLabel = \"*Start*\"\n\n if language == 1 then\n startLabel = \"*시작*\"\n elseif language == 3 then\n startLabel = \"*開始*\"\n end\n\n local startMessage = trim(safeGet(triggerId, \"vg_Start_Message\", \"\"))\n local startMessageText = \"\"\n local openingGuideText = \"▷ [Begin the story naturally, not as a formal introduction, profile, or summary.\\n\"\n local openingMiddleText =\n \"Naturally establish the heroine through the scene, including but not limited to aspects such as her personality, her hopes or personal goals, her circumstances, her place in the world, and the broader situation around her. Also let the response organically suggest things such as the nature of the world, the period or phase it is in, and the kind of life the heroine is living.\\n\" ..\n \"Do not explain everything directly or present information like a character sheet. Instead, let these details emerge naturally through scene flow, atmosphere, narration, implication, and context.\\n\"\n\n if startMessage ~= \"\" then\n startMessageText = \"[Opening Situation: \" .. startMessage .. \" | Emoji: ▶️]\\n\"\n\n openingGuideText =\n openingGuideText ..\n \"Treat this response as a prologue focused on the heroine, based on the given Opening Situation.\\n\" ..\n \"Set the date and time naturally according to the world, the setting, and the Opening Situation when appropriate.\\n\" ..\n openingMiddleText ..\n \"Keep this prologue immersive and natural, but avoid overdefining every detail. Let the scene unfold from the given Opening Situation as an active scene already in progress, focused only on the Opening Situation, atmosphere, context, and setup in this response, while leaving room for the user's next choice to determine what happens next.\\n\" ..\n \"Aside from what is required by the Opening Situation, do not introduce, foreshadow, or begin any unrelated incident, encounter, threat, mystery, request, conflict, or other important development.]\"\n else\n openingGuideText =\n openingGuideText ..\n \"Treat this response as a prologue focused on the heroine's ordinary life before any notable event begins.\\n\" ..\n \"Set the date and time naturally according to the world and the setting.\\n\" ..\n openingMiddleText ..\n \"Keep this prologue immersive and natural, but avoid overdefining every detail. Let it unfold through the heroine's ordinary daily life as an active scene already in progress, focused only on atmosphere, context, and setup in this response. In this response, do not introduce, foreshadow, or begin any specific incident, encounter, threat, mystery, request, conflict, or other important development, and do not place the heroine in the middle of anything unusual, urgent, or plot-significant. Her immediate situation must remain calm, mundane, and uninterrupted so that the user's next choice alone determines what happens next.]\"\n end\n\n local startText =\n startLabel .. \"\\n\" ..\n \"▶ [The heroine is {{user}}]\\n\" ..\n startMessageText ..\n openingGuideText\n\n local cl = getChatLength(triggerId)\n local handled = false\n\n if cl > 0 then\n local last = getChat(triggerId, cl - 1)\n\n if last and last.role == \"user\" and type(last.data) == \"string\" then\n setChat(triggerId, cl - 1, startText)\n handled = true\n end\n end\n\n if not handled then\n addChat(triggerId, \"user\", startText)\n end\n\n end\nend\n\nfunction setLanguage1(triggerId)\n if safeGet(triggerId, \"vg_Init\", \"0\") == \"1\" then return end\n \n setChatVar(triggerId, \"vg_Language\", \"1\")\nend\n\nfunction setLanguage2(triggerId)\n if safeGet(triggerId, \"vg_Init\", \"0\") == \"1\" then return end\n \n setChatVar(triggerId, \"vg_Language\", \"2\")\nend\n\nfunction setLanguage3(triggerId)\n if safeGet(triggerId, \"vg_Init\", \"0\") == \"1\" then return end\n \n setChatVar(triggerId, \"vg_Language\", \"3\")\nend\n\nfunction makeInputSetter(triggerId, valueKey, baseMessage)\n if safeGet(triggerId, \"vg_Init\", \"0\") == \"1\" then\n return\n end\n\n local current = safeGet(triggerId, valueKey, \"\")\n local message = baseMessage\n\n if current ~= \"\" then\n message = message .. \"\\n\\n빈 값을 입력하면 해제됩니다.\\n현재 입력값: \" .. current\n end\n\n local input = alertInput(triggerId, message):await()\n if input then \n setChatVar(triggerId, valueKey, input or \"\")\n end\nend\n\nsetFirstKiss = async(function(triggerId)\n makeInputSetter(triggerId, \"vg_FirstKiss\", \"첫 키스 상대를 입력하세요.\\n예) 첫 남친\")\nend)\n\nsetFirstFellatio = async(function(triggerId)\n makeInputSetter(triggerId, \"vg_FirstFellatio\", \"첫 구강 섹스한 경험 내용을 입력하세요.\\n예) 첫 남친의 자지\")\nend)\n\nsetFirstVirgin = async(function(triggerId)\n makeInputSetter(triggerId, \"vg_FirstVirgin\", \"처녀막을 상실한 경험 내용을 입력하세요.\\n예) 첫 남친의 자지\")\nend)\n\nsetFirstAnal = async(function(triggerId)\n makeInputSetter(triggerId, \"vg_FirstAnal\", \"첫 항문 섹스 경험 내용을 입력하세요.\\n예) 첫 남친의 자지\")\nend)\n\nsetStartMessage = async(function(triggerId)\n makeInputSetter(triggerId, \"vg_Start_Message\", \"시작 시 추가될 첫 장면, 시작 상황, 도입 지시 등을 입력하세요.\")\nend)\n\nfunction setFirstMasturbation(triggerId)\n if safeGet(triggerId, \"vg_Init\", \"0\") == \"1\" then return end\n\n local current = safeGet(triggerId, \"vg_FirstMasturbation\", \"\")\n\n if current == \"\" then\n setChatVar(triggerId, \"vg_FirstMasturbation\", \"Pre-Story\")\n else\n setChatVar(triggerId, \"vg_FirstMasturbation\", \"\")\n end\nend\n\nfunction setFirstOrgasm(triggerId)\n if safeGet(triggerId, \"vg_Init\", \"0\") == \"1\" then return end\n\n local current = safeGet(triggerId, \"vg_FirstOrgasm\", \"\")\n\n if current == \"\" then\n setChatVar(triggerId, \"vg_FirstOrgasm\", \"Pre-Story\")\n else\n setChatVar(triggerId, \"vg_FirstOrgasm\", \"\")\n end\nend\n\nfunction rollSensitivityDice(triggerId)\n if safeGet(triggerId, \"vg_Init\", \"0\") == \"1\" then return end\n\n local function rollSensitivity()\n local r = math.random(100)\n local level\n local description\n\n if r <= 10 then\n level = 1\n description = \"⬜\"\n elseif r <= 30 then\n level = 2\n description = \"🟩\"\n elseif r <= 60 then\n level = 3\n description = \"🟨\"\n elseif r <= 90 then\n level = 4\n description = \"🟧\"\n else\n level = 5\n description = \"🟥\"\n end\n \n local min, max = getsensitivityRangeForLevel(level)\n return math.random(min, max), description\n end\n\n local breastValue, breastDescription = rollSensitivity()\n setChatVar(triggerId, \"vg_startBreast\", tostring(breastValue))\n\n local oralValue, oralDescription = rollSensitivity()\n setChatVar(triggerId, \"vg_startOral\", tostring(oralValue))\n\n local clitorisValue, clitorisDescription = rollSensitivity()\n setChatVar(triggerId, \"vg_startClitoris\", tostring(clitorisValue))\n\n local vaginalValue, vaginalDescription = rollSensitivity()\n setChatVar(triggerId, \"vg_startVaginal\", tostring(vaginalValue))\n\n local analValue, analDescription = rollSensitivity()\n setChatVar(triggerId, \"vg_startAnal\", tostring(analValue))\n\n setChatVar(triggerId, \"vg_startBreastEmoji\", breastDescription)\n setChatVar(triggerId, \"vg_startOralEmoji\", oralDescription)\n setChatVar(triggerId, \"vg_startClitorisEmoji\", clitorisDescription)\n setChatVar(triggerId, \"vg_startVaginalEmoji\", vaginalDescription)\n setChatVar(triggerId, \"vg_startAnalEmoji\", analDescription)\nend\n\nfunction setSexKnowledge(triggerId, value)\n if safeGet(triggerId, \"vg_Init\", \"0\") == \"1\" then return end\n setChatVar(triggerId, \"vg_startKnowledge\", tostring(value))\nend\n\nfunction setSexKnowledge0(triggerId) setSexKnowledge(triggerId, 0) end\nfunction setSexKnowledge1(triggerId) setSexKnowledge(triggerId, 1) end\nfunction setSexKnowledge2(triggerId) setSexKnowledge(triggerId, 2) end\nfunction setSexKnowledge3(triggerId) setSexKnowledge(triggerId, 3) end\nfunction setSexKnowledge4(triggerId) setSexKnowledge(triggerId, 4) end\nfunction setSexKnowledge5(triggerId) setSexKnowledge(triggerId, 5) end\n\nfunction setArousalResponse(triggerId, value)\n if safeGet(triggerId, \"vg_Init\", \"0\") == \"1\" then return end\n setChatVar(triggerId, \"vg_ArousalResponse\", tostring(value))\nend\n\nfunction setArousalResponse1(triggerId) setArousalResponse(triggerId, 1) end\nfunction setArousalResponse2(triggerId) setArousalResponse(triggerId, 2) end\nfunction setArousalResponse3(triggerId) setArousalResponse(triggerId, 3) end\nfunction setArousalResponse4(triggerId) setArousalResponse(triggerId, 4) end\nfunction setArousalResponse5(triggerId) setArousalResponse(triggerId, 5) end\nfunction setArousalResponse6(triggerId) setArousalResponse(triggerId, 6) end\nfunction setArousalResponse7(triggerId) setArousalResponse(triggerId, 7) end\nfunction setArousalResponse8(triggerId) setArousalResponse(triggerId, 8) end\nfunction setArousalResponse9(triggerId) setArousalResponse(triggerId, 9) end\nfunction setArousalResponse10(triggerId) setArousalResponse(triggerId, 10) end\n\nfunction setStartEnhancementBase(triggerId, enhancementType)\n if safeGet(triggerId, \"vg_Init\", \"0\") == \"1\" then return end\n setChatVar(triggerId, \"vg_StartEnhancementBase\", tostring(enhancementType))\nend\n\nfunction setStartEnhancementAttack(triggerId) setStartEnhancementBase(triggerId, constEnhancementType.attack) end\nfunction setStartEnhancementDefense(triggerId) setStartEnhancementBase(triggerId, constEnhancementType.defense) end\nfunction setStartEnhancementCharisma(triggerId) setStartEnhancementBase(triggerId, constEnhancementType.charisma) end\nfunction setStartEnhancementChastity(triggerId) setStartEnhancementBase(triggerId, constEnhancementType.chastity) end\nfunction setStartEnhancementFocus(triggerId) setStartEnhancementBase(triggerId, constEnhancementType.focus) end\nfunction setStartEnhancementResilience(triggerId) setStartEnhancementBase(triggerId, constEnhancementType.resilience) end\nfunction setStartEnhancementDiscipline(triggerId) setStartEnhancementBase(triggerId, constEnhancementType.discipline) end\nfunction setStartEnhancementLuck(triggerId) setStartEnhancementBase(triggerId, constEnhancementType.luck) end\n\n-- ==========================================\n-- 세이브\n-- ==========================================\nlocal function buildFullSave(triggerId)\n\n local FS = \"\\x1F\" -- field separator\n local RS = \"\\x1E\" -- record separator\n local GS = \"\\x1D\" -- section separator\n local TS = \"\\x1C\" -- tag separator\n\n local SAVE_FULL_TAG = \"SaveFullData\"\n\n local sections = {}\n\n setChatVar(triggerId, \"vg_Session_Save\", \"0\")\n\n -- ==========================================\n -- 1. Global 변수 저장\n -- 나중 -> 예전 복원만 가능. 그러므로 vg_Memory_Content는 저장하지 않음\n -- ==========================================\n local globalKeys = {\n \"vg_Mode\",\n \"vg_Context\",\n \"vg_SexKnowledge\",\n \"vg_Sensitivity\",\n \"vg_FirstKiss\",\n \"vg_FirstMasturbation\",\n \"vg_FirstOrgasm\",\n \"vg_FirstFellatio\",\n \"vg_FirstVirgin\",\n \"vg_FirstAnal\",\n \"vg_Reproductive\",\n \"vg_Heroine_Stat\",\n \"vg_Deck_Base\",\n \"vg_Memory_Num\",\n \"vg_Story_Num\",\n \"vg_Reward_Count\",\n \"vg_Session_Info\",\n \"vg_Enhancement\",\n \"vg_Goal_Short\",\n }\n\n local globalFields = {}\n\n for _, key in ipairs(globalKeys) do\n local val = tostring(getChatVar(triggerId, key) or \"\")\n table.insert(globalFields, key .. \"=\" .. val)\n end\n\n table.insert(sections, \"G\" .. TS .. table.concat(globalFields, FS))\n\n -- ==========================================\n -- 3. 최종 블록 생성\n -- ==========================================\n local saveBlock =\n \"<\" .. SAVE_FULL_TAG .. \">\"\n .. table.concat(sections, GS)\n .. \"</\" .. SAVE_FULL_TAG .. \">\"\n\n return saveBlock\nend\n\nlocal function buildSessionSave(triggerId, extraChanged)\n\n local FS = \"\\x1F\"\n\n local parts = {}\n\n local saveFlag = tonumber(getChatVar(triggerId, \"vg_Session_Save\") or \"0\") or 0\n\n if saveFlag > 0 then\n setChatVar(triggerId, \"vg_Session_Save\", \"0\")\n end\n\n local alwaysKeys = {\n \"vg_Heroine_Stat\",\n \"vg_Heroine_Effect\",\n \"vg_PlayDeck_Hand\",\n \"vg_PlayDeck_Remain\",\n \"vg_PlayDeck_Discard\",\n \"vg_PlayDeck_Action\",\n \"vg_Encounter_Stat\",\n \"vg_Encounter_Action\",\n \"vg_Companion_Action\",\n }\n\n for _, key in ipairs(alwaysKeys) do\n local val = tostring(getChatVar(triggerId, key) or \"\")\n table.insert(parts, key .. \"=\" .. val)\n end\n\n if saveFlag == 1 then\n local startKeys = {\n \"vg_Mode\",\n \"vg_Session_Info\",\n \"vg_Deck_Base\",\n \"vg_Context\",\n \"vg_Goal_Short\",\n }\n\n for _, key in ipairs(startKeys) do\n local val = tostring(getChatVar(triggerId, key) or \"\")\n table.insert(parts, key .. \"=\" .. val)\n end\n end\n\n if extraChanged then\n for key, value in pairs(extraChanged) do\n table.insert(parts, key .. \"=\" .. tostring(value))\n end\n end\n\n if #parts == 0 then\n return \"\"\n end\n\n return \"<SaveSessionData>\"\n .. table.concat(parts, FS)\n .. \"</SaveSessionData>\"\nend\n\nlocal function buildFreeModeSave(triggerId, extraChanged)\n local FS = \"\\x1F\"\n\n local modeSaveFlag = tonumber(getChatVar(triggerId, \"vg_Mode_Save\") or \"0\") or 0\n local parts = {}\n local added = {}\n\n local function addPart(key, value)\n key = tostring(key or \"\")\n if key == \"\" or added[key] then\n return\n end\n\n added[key] = true\n table.insert(parts, key .. \"=\" .. tostring(value or \"\"))\n end\n\n if modeSaveFlag > 0 then\n addPart(\"vg_Mode\", tostring(getChatVar(triggerId, \"vg_Mode\") or \"\"))\n addPart(\"vg_Context\", tostring(getChatVar(triggerId, \"vg_Context\") or \"\"))\n end\n\n for key, value in pairs(extraChanged or {}) do\n addPart(key, value)\n end\n\n if #parts == 0 then\n return \"\"\n end\n\n return \"<SaveSessionData>\"\n .. table.concat(parts, FS)\n .. \"</SaveSessionData>\"\nend\n\nlocal function restoreFullSave(triggerId)\n\n local FS = \"\\x1F\"\n local RS = \"\\x1E\"\n local GS = \"\\x1D\"\n local TS = \"\\x1C\"\n\n local history = getFullChat(triggerId)\n if not history or #history == 0 then\n return false\n end\n\n local fullIndex = nil\n -- ======================================\n -- 1) 최신 FullSave 찾기 (아래 → 위)\n -- ======================================\n for i = #history, 1, -1 do\n if history[i].role == \"char\" then\n local text = history[i].data or \"\"\n if text:find(\"<SaveFullData>\", 1, true) then\n fullIndex = i\n break\n end\n end\n end\n\n if not fullIndex then\n return false\n end\n\n -- ======================================\n -- 2) FullSave 블록 추출\n -- ======================================\n local fullBlock =\n history[fullIndex].data:match(\n \"<SaveFullData>([%s%S]-)</SaveFullData>\"\n )\n\n if not fullBlock then\n return false\n end\n\n -- ======================================\n -- 3) FullSave 적용\n -- ======================================\n for section in fullBlock:gmatch(\"[^\" .. GS .. \"]+\") do\n\n local tag, body = section:match(\"^(.-)\" .. TS .. \"(.*)$\")\n\n if tag == \"G\" and body then\n -- Global\n for pair in body:gmatch(\"[^\" .. FS .. \"]+\") do\n local key, value = pair:match(\"^(.-)=(.*)$\")\n if key then\n setChatVar(triggerId, key, value or \"\")\n end\n end\n end\n end\n\n -- ======================================\n -- 4) Full 이후 SessionData 순차 적용\n -- ======================================\n for i = fullIndex + 1, #history do\n\n if history[i].role == \"char\" then\n\n local text = history[i].data or \"\"\n\n for block in text:gmatch(\n \"<SaveSessionData>([%s%S]-)</SaveSessionData>\"\n ) do\n\n for pair in block:gmatch(\"[^\" .. FS .. \"]+\") do\n local key, value = pair:match(\"^(.-)=(.*)$\")\n if key then\n setChatVar(triggerId, key, value or \"\")\n end\n end\n\n end\n end\n end\n\n -- ======================================\n -- Sexual Trait 정합성 보정\n -- ======================================\n do\n local traitNum = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.sexualTrait_Num, 0)) or 0\n local raw = safeGet(triggerId, \"vg_SexualTrait_Content\", \"\")\n\n if traitNum <= 0 then\n setChatVar(triggerId, \"vg_SexualTrait_Content\", \"\")\n elseif raw ~= \"\" then\n local list = splitPipe(raw)\n\n if #list > traitNum then\n local newList = {}\n\n for i = 1, traitNum do\n newList[#newList + 1] = list[i]\n end\n\n setChatVar(triggerId, \"vg_SexualTrait_Content\", table.concat(newList, \"|\"))\n end\n end\n end\n\n -- ======================================\n -- Corruption Trait 정합성 보정\n -- ======================================\n do\n local traitNum = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.corruptionTrait_Num, 0)) or 0\n local raw = safeGet(triggerId, \"vg_Corruption_Content\", \"\")\n\n if traitNum <= 0 then\n setChatVar(triggerId, \"vg_Corruption_Content\", \"\")\n elseif raw ~= \"\" then\n local list = splitPipe(raw)\n\n if #list > traitNum then\n local newList = {}\n\n for i = 1, traitNum do\n newList[#newList + 1] = list[i]\n end\n\n setChatVar(triggerId, \"vg_Corruption_Content\", table.concat(newList, \"|\"))\n end\n end\n end\n\n -- ======================================\n -- Memory 정합성 보정\n -- ======================================\n do\n local memNum = tonumber(getChatVar(triggerId, \"vg_Memory_Num\") or \"0\") or 0\n local raw = safeGet(triggerId, \"vg_Memory_Content\", \"\")\n\n if memNum <= 0 then\n setChatVar(triggerId, \"vg_Memory_Content\", \"\")\n elseif raw ~= \"\" then\n local list = splitPipe(raw)\n \n if #list > memNum then\n local newList = {}\n\n for i = 1, memNum do\n newList[#newList + 1] = list[i]\n end\n\n setChatVar(triggerId, \"vg_Memory_Content\", table.concat(newList, \"|\"))\n end\n end\n end\n\n -- ======================================\n -- Story 정합성 보정\n -- ======================================\n do\n local storyNum = tonumber(getChatVar(triggerId, \"vg_Story_Num\") or \"0\") or 0\n local raw = safeGet(triggerId, \"vg_Story_Content\", \"\")\n\n if storyNum <= 0 then\n setChatVar(triggerId, \"vg_Story_Content\", \"\")\n elseif raw ~= \"\" then\n local list = splitPipe(raw)\n\n if #list > storyNum then\n local newList = {}\n\n for i = 1, storyNum do\n newList[#newList + 1] = list[i]\n end\n\n setChatVar(triggerId, \"vg_Story_Content\", table.concat(newList, \"|\"))\n end\n end\n end\n\n local curPhase = getPhase(triggerId)\n setPhase(triggerId, curPhase)\n\n print(\"📥 Data Restore Complete\")\n\n return true\nend\n\nlocal function restoreSavedChatVar(triggerId, targetKey)\n\n local FS = \"\\x1F\"\n local GS = \"\\x1D\"\n local TS = \"\\x1C\"\n\n local history = getFullChat(triggerId)\n if not history or #history == 0 then\n setChatVar(triggerId, targetKey, \"\")\n return \"\"\n end\n\n for i = #history, 1, -1 do\n if history[i].role == \"char\" then\n local text = history[i].data or \"\"\n\n -- ======================================\n -- 1) 같은 메시지 안의 SessionSave 먼저 확인\n -- ======================================\n local sessionBlocks = {}\n for block in text:gmatch(\"<SaveSessionData>([%s%S]-)</SaveSessionData>\") do\n table.insert(sessionBlocks, block)\n end\n\n for b = #sessionBlocks, 1, -1 do\n local block = sessionBlocks[b]\n\n for pair in block:gmatch(\"[^\" .. FS .. \"]+\") do\n local key, value = pair:match(\"^(.-)=(.*)$\")\n if key == targetKey then\n value = value or \"\"\n setChatVar(triggerId, targetKey, value)\n return value\n end\n end\n end\n\n -- ======================================\n -- 2) 같은 메시지 안의 FullSave 확인\n -- ======================================\n local fullBlock = text:match(\"<SaveFullData>([%s%S]-)</SaveFullData>\")\n\n if fullBlock then\n for section in fullBlock:gmatch(\"[^\" .. GS .. \"]+\") do\n local tag, body = section:match(\"^(.-)\" .. TS .. \"(.*)$\")\n\n if tag == \"G\" and body then\n for pair in body:gmatch(\"[^\" .. FS .. \"]+\") do\n local key, value = pair:match(\"^(.-)=(.*)$\")\n if key == targetKey then\n value = value or \"\"\n setChatVar(triggerId, targetKey, value)\n return value\n end\n end\n end\n end\n end\n end\n end\n\n setChatVar(triggerId, targetKey, \"\")\n return \"\"\nend\n\n-- ==========================================\n-- 보상\n-- ==========================================\nlocal function buildRewardChoiceBlock(triggerId, rewardPhase)\n local sessionSlotList = getDualFlagSlotList(triggerId, vgSessionInfo.varName)\n local heroineStat = loadDualFlagNextMap(triggerId, vgHeroineStat)\n\n local difficultyCode = tonumber(\n getResolvedDualFlagFromSlotList(\n sessionSlotList,\n vgSessionInfo.difficulty,\n constSessionDifficulty.low\n )\n ) or constSessionDifficulty.low\n\n local encounterLevel = tonumber(\n getResolvedDualFlagFromSlotList(\n sessionSlotList,\n vgSessionInfo.level,\n 1\n )\n ) or 1\n\n local selectCount = 1\n local actionCount = 0\n local upgradeCount = 0\n local expectedLevelBonus = 0\n\n local rewardStatAttributeMultiplier = 1.0\n\n local useAttributeReward = true\n local useStatReward = true\n\n local recoveryPercent = 0\n\n local allowSingleRecoveryReward = true\n local allowArousalDownReward = true\n\n local function getRecoveryPercentLevelDiffBonus(encounterLevel)\n local heroineLevel = tonumber(getHeroineLevel(triggerId)) or 1\n encounterLevel = tonumber(encounterLevel) or 1\n\n local levelDiff = encounterLevel - heroineLevel\n levelDiff = clamp(levelDiff, -6, 6)\n\n return roundToTenth(levelDiff * 5 / 3)\n end\n\n if rewardPhase == constPhase.transaction or rewardPhase == constPhase.rape then\n rewardPhase = constPhase.none\n end\n\n if isMasturbationPhase(triggerId, rewardPhase) then\n encounterLevel = math.max(1, getHeroineLevel(triggerId) - 3)\n\n local lewdnessLevel = getLewdnessLevel(triggerId)\n local masturbationRecoveryBonusPercent = math.max(0, lewdnessLevel - 1)\n\n selectCount = 2\n actionCount = 2\n upgradeCount = 1\n recoveryPercent = 10 + masturbationRecoveryBonusPercent\n expectedLevelBonus = 0.2\n\n rewardStatAttributeMultiplier = 0.5\n\n allowArousalDownReward = false\n\n elseif isIncapacitatedPhase(triggerId, rewardPhase) then\n recoveryPercent = 50\n\n useStatReward = false\n allowSingleRecoveryReward = false\n\n if rewardPhase == constPhase.event_Defiant or rewardPhase == constPhase.event_Yielding\n or rewardPhase == constPhase.event_Defiant_Hypnosis or rewardPhase == constPhase.event_Yielding_Hypnosis then\n selectCount = 1\n \n useAttributeReward = false\n allowArousalDownReward = false\n else\n recoveryPercent = 35\n\n selectCount = 2\n actionCount = 2\n expectedLevelBonus = 0.1\n\n rewardStatAttributeMultiplier = 0.8\n encounterLevel = encounterLevel - 4\n end\n\n elseif difficultyCode == constSessionDifficulty.low then\n selectCount = 3\n actionCount = 3\n recoveryPercent = 30 + getRecoveryPercentLevelDiffBonus(encounterLevel)\n expectedLevelBonus = 0.15\n\n rewardStatAttributeMultiplier = 0.85\n\n elseif difficultyCode == constSessionDifficulty.medium then\n selectCount = 3\n actionCount = 3\n recoveryPercent = 35 + getRecoveryPercentLevelDiffBonus(encounterLevel)\n expectedLevelBonus = 0.3\n\n rewardStatAttributeMultiplier = 0.925\n\n elseif difficultyCode == constSessionDifficulty.high then\n selectCount = 3\n actionCount = 3\n recoveryPercent = 40 + getRecoveryPercentLevelDiffBonus(encounterLevel)\n expectedLevelBonus = 0.5\n\n elseif difficultyCode == constSessionDifficulty.elite then\n selectCount = 4\n actionCount = 3\n upgradeCount = 1\n recoveryPercent = 60 + getRecoveryPercentLevelDiffBonus(encounterLevel)\n expectedLevelBonus = 0.8\n\n elseif difficultyCode == constSessionDifficulty.boss then\n selectCount = 5\n actionCount = 3\n upgradeCount = 2\n recoveryPercent = 80 + getRecoveryPercentLevelDiffBonus(encounterLevel)\n expectedLevelBonus = 1.0\n end\n\n if rewardPhase == constPhase.sleeping_Ambush\n or rewardPhase == constPhase.entrapment\n or rewardPhase == constPhase.brainwashing then\n upgradeCount = upgradeCount + 1\n rewardStatAttributeMultiplier = rewardStatAttributeMultiplier + 0.2\n expectedLevelBonus = expectedLevelBonus + 0.3\n elseif rewardPhase == constPhase.aphrodisiac then\n rewardStatAttributeMultiplier = rewardStatAttributeMultiplier + 0.1\n expectedLevelBonus = expectedLevelBonus + 0.15\n end\n\n local nextStageValue = tonumber(getDualFlag(triggerId, vgSessionInfo.varName, vgSessionInfo.stage, true)) or 0\n if nextStageValue >= 4 then\n allowArousalDownReward = false\n end\n\n local currentHP = tonumber(heroineStat.hp) or 0\n local maxHP = tonumber(heroineStat.hp_Max) or 0\n local currentResource = tonumber(heroineStat.resource) or 0\n local maxResource = tonumber(heroineStat.resource_Max) or 0\n local currentArousal = tonumber(heroineStat.arousal) or 0\n local maxArousal = tonumber(heroineStat.arousal_Max) or 0\n\n local expectedCardLevel = 1.0\n\n expectedCardLevel = (encounterLevel / 5) + expectedLevelBonus\n expectedCardLevel = clamp(expectedCardLevel, 1, 10)\n\n local lines = {}\n local upgradeReferenceLevels = {}\n local rewardUsedCardMap = {}\n\n do\n local actionTypePool = {\n constCardType.primary,\n constCardType.secondary,\n constCardType.focus,\n constCardType.charge,\n\n constCardType.heal,\n constCardType.soothe,\n\n constCardType.breaker,\n constCardType.counter,\n\n constCardType.taunt,\n }\n\n local lewdnessLevel = getLewdnessLevel(triggerId)\n\n if lewdnessLevel >= 5 then\n table.insert(actionTypePool, constCardType.temptation)\n end\n\n if lewdnessLevel >= 9 then\n table.insert(actionTypePool, constCardType.milking)\n end\n\n shuffleArray(actionTypePool)\n\n for i = 1, math.min(actionCount, #actionTypePool) do\n local baseValue = math.floor(expectedCardLevel)\n local upChance = expectedCardLevel - baseValue\n local cardLevel = baseValue\n\n if cardLevel < 10 and math.random() < upChance then\n cardLevel = cardLevel + 1\n end\n\n cardLevel = clamp(cardLevel, 1, 10)\n\n table.insert(upgradeReferenceLevels, cardLevel)\n\n table.insert(\n lines,\n \"[Reward Action: \" .. tostring(actionTypePool[i])\n .. \" | Emoji: \" .. getCardTypeEmoji(triggerId, actionTypePool[i], constPhase.combat)\n .. \" | Card Level: \" .. tostring(cardLevel)\n .. \" | Level: \" .. getCardLevelStars(cardLevel) .. \"]\"\n )\n end\n end\n\n do\n while #upgradeReferenceLevels < upgradeCount do\n local baseValue = math.floor(expectedCardLevel)\n local upChance = expectedCardLevel - baseValue\n local cardLevel = baseValue\n\n if cardLevel < 10 and math.random() < upChance then\n cardLevel = cardLevel + 1\n end\n\n cardLevel = clamp(cardLevel, 1, 10)\n\n table.insert(upgradeReferenceLevels, cardLevel)\n end\n end\n\n do\n if upgradeCount > 0 then\n local baseDeckCards = {}\n\n for _, cardRaw in ipairs(splitPipe(safeGet(triggerId, \"vg_Deck_Base\", \"\"))) do\n local parts = splitSection(cardRaw)\n local code = trim(parts[1] or \"\")\n local typeCode = tonumber(parts[2]) or 0\n local level = tonumber(parts[3]) or 1\n\n if code ~= \"\"\n and typeCode > 0\n and level >= 1\n and level < 10 then\n table.insert(baseDeckCards, {\n code = code,\n typeCode = typeCode,\n level = level\n })\n end\n end\n\n table.sort(upgradeReferenceLevels, function(a, b)\n return (tonumber(a) or 1) < (tonumber(b) or 1)\n end)\n\n local grantedUpgradeCount = 0\n\n for _, refLevel in ipairs(upgradeReferenceLevels) do\n if grantedUpgradeCount >= upgradeCount then\n break\n end\n\n refLevel = clamp(tonumber(refLevel) or 1, 1, 10)\n\n if refLevel > 1 then\n local candidates = {}\n\n for _, info in ipairs(baseDeckCards) do\n if not rewardUsedCardMap[info.code]\n and info.level < refLevel then\n table.insert(candidates, info)\n end\n end\n\n if #candidates > 0 then\n shuffleArray(candidates)\n\n local picked = candidates[1]\n rewardUsedCardMap[picked.code] = true\n grantedUpgradeCount = grantedUpgradeCount + 1\n\n local currentLevel = picked.level\n local finalLevel = clamp(currentLevel + 1, 1, 10)\n\n -- 2업째: 50%\n if finalLevel < refLevel and finalLevel < 10 then\n if math.random() < 0.5 then\n finalLevel = clamp(finalLevel + 1, 1, 10)\n end\n end\n\n table.insert(\n lines,\n \"[Reward Upgrade: \" .. tostring(picked.code)\n .. \" | Emoji: \" .. getCardTypeEmoji(triggerId, picked.typeCode, constPhase.combat)\n .. \" | Current Level: \" .. getCardLevelStars(currentLevel)\n .. \" | Next Level: \" .. getCardLevelStars(finalLevel)\n .. \" | Level Up: \" .. tostring(finalLevel - currentLevel) .. \"]\"\n )\n end\n end\n end\n end\n end\n\n do\n local baseDeckArray = splitPipe(safeGet(triggerId, \"vg_Deck_Base\", \"\"))\n local removeRewardTarget = nil\n\n if #baseDeckArray >= 21 and math.random() < expectedLevelBonus then\n local eligibleCards = {}\n\n -- 제거 가능한 카드 전체 수집\n for _, cardRaw in ipairs(baseDeckArray) do\n local cardParts = splitSection(cardRaw)\n local code = trim(cardParts[1] or \"\")\n local typeCode = tonumber(cardParts[2]) or 0\n local level = tonumber(cardParts[3]) or 1\n\n if code ~= \"\" and typeCode > 0 and not rewardUsedCardMap[code] then\n table.insert(eligibleCards, {\n code = code,\n typeCode = typeCode,\n level = level\n })\n end\n end\n\n if #eligibleCards > 0 then\n -- 제거 가능한 카드 중 랜덤으로 최대 5장 뽑기\n shuffleArray(eligibleCards)\n\n local sampleCount = math.min(5, #eligibleCards)\n local minLevel = nil\n local lowestCandidates = {}\n\n -- 랜덤 5장 안에서 가장 낮은 레벨 찾기\n for i = 1, sampleCount do\n local card = eligibleCards[i]\n\n if not minLevel or card.level < minLevel then\n minLevel = card.level\n lowestCandidates = { card }\n elseif card.level == minLevel then\n table.insert(lowestCandidates, card)\n end\n end\n\n -- 최저 레벨 동률이면 그중 랜덤 1장\n if #lowestCandidates > 0 then\n removeRewardTarget = lowestCandidates[math.random(1, #lowestCandidates)]\n end\n end\n end\n\n if removeRewardTarget then\n table.insert(\n lines,\n \"[Reward Remove: \" .. tostring(removeRewardTarget.code)\n .. \" | Emoji: \" .. getCardTypeEmoji(triggerId, removeRewardTarget.typeCode, constPhase.combat)\n .. \" | Level: \" .. getCardLevelStars(removeRewardTarget.level) .. \"]\"\n )\n end\n end\n\n do\n if useAttributeReward then\n table.insert(lines, \"[Blank Line]\")\n\n local encounterStatValue = tonumber(getEncounterStatValue(triggerId, encounterLevel)) or 0\n\n local function formatAttributeRewardNumber(v)\n v = roundToTenth(tonumber(v) or 0)\n\n if v == 0 then\n return \"0\"\n end\n\n return string.format(\"%+.1f\", v)\n end\n\n local function getBasicBaseValue(heroineValue)\n local diff = encounterStatValue - (tonumber(heroineValue) or 0)\n local value = 1.5\n\n if diff >= 0 then\n value = value + (diff * 0.1)\n else\n value = value + (diff * 0.125)\n end\n\n return roundToTenth(clamp(value, 0.5, 6.0)) \n end\n\n local function applyAttributeRewardOffset(baseValue, multiplier, offset)\n local value = tonumber(baseValue) or 0.1\n\n value = value * (tonumber(multiplier) or 1.0)\n value = value + (tonumber(offset) or 0)\n value = value * rewardStatAttributeMultiplier\n\n return roundToTenth(math.max(0.1, value))\n end\n\n local attributeMeta = {\n [1] = { emoji = \"👊\", label = \"Strength\" },\n [2] = { emoji = \"🦶\", label = \"Reflex\" },\n [3] = { emoji = \"🧠\", label = \"Willpower\" },\n }\n\n local function makeAttributeRewardFullLine(basicIndex, attributeBaseValues, prevRankMap)\n local gains = { 0, 0, 0 }\n local remainIndices = {}\n\n for i = 1, 3 do\n if i ~= basicIndex then\n table.insert(remainIndices, i)\n end\n end\n\n local offsets = { 0.1, 0.0, -0.1 }\n shuffleArray(offsets)\n\n local candidateRankMaps = {\n {\n [basicIndex] = 3,\n [remainIndices[1]] = 2,\n [remainIndices[2]] = 1,\n },\n {\n [basicIndex] = 3,\n [remainIndices[1]] = 1,\n [remainIndices[2]] = 2,\n },\n }\n\n local rankToMultiplier = {\n [3] = 1.0,\n [2] = 2/3,\n [1] = 1/3,\n }\n\n local selectedRankMap = nil\n\n if prevRankMap then\n for _, candidate in ipairs(candidateRankMaps) do\n local ok = true\n\n for i = 1, 3 do\n if candidate[i] == prevRankMap[i] then\n ok = false\n break\n end\n end\n\n if ok then\n selectedRankMap = candidate\n break\n end\n end\n end\n\n if not selectedRankMap then\n selectedRankMap = candidateRankMaps[math.random(1, #candidateRankMaps)]\n end\n\n gains[basicIndex] = applyAttributeRewardOffset(\n attributeBaseValues[basicIndex],\n rankToMultiplier[selectedRankMap[basicIndex]],\n offsets[1]\n )\n\n gains[remainIndices[1]] = applyAttributeRewardOffset(\n attributeBaseValues[remainIndices[1]],\n rankToMultiplier[selectedRankMap[remainIndices[1]]],\n offsets[2]\n )\n\n gains[remainIndices[2]] = applyAttributeRewardOffset(\n attributeBaseValues[remainIndices[2]],\n rankToMultiplier[selectedRankMap[remainIndices[2]]],\n offsets[3]\n )\n\n return {\n rankMap = selectedRankMap,\n gains = gains,\n }\n end\n\n local function getAttributeRewardTotalTenths(reward)\n local total = 0\n\n for i = 1, 3 do\n total = total + math.floor(((tonumber(reward.gains[i]) or 0) * 10) + 0.5)\n end\n\n return total\n end\n\n local function balanceAttributeRewardPair(rewardA, rewardB)\n local totalA = getAttributeRewardTotalTenths(rewardA)\n local totalB = getAttributeRewardTotalTenths(rewardB)\n\n if totalA == totalB then\n return\n end\n\n local lowerReward = rewardA\n local diff = totalB - totalA\n\n if totalB < totalA then\n lowerReward = rewardB\n diff = totalA - totalB\n end\n\n for _ = 1, diff do\n local index = math.random(1, 3)\n\n lowerReward.gains[index] = roundToTenth(\n (tonumber(lowerReward.gains[index]) or 0) + 0.1\n )\n end\n end\n\n local function insertAttributeRewardLine(reward, lines)\n local gains = reward.gains\n\n local displayItems = {\n {\n index = 1,\n value = gains[1],\n text = attributeMeta[1].emoji .. formatAttributeRewardNumber(gains[1])\n },\n {\n index = 2,\n value = gains[2],\n text = attributeMeta[2].emoji .. formatAttributeRewardNumber(gains[2])\n },\n {\n index = 3,\n value = gains[3],\n text = attributeMeta[3].emoji .. formatAttributeRewardNumber(gains[3])\n },\n }\n\n table.sort(displayItems, function(a, b)\n local av = tonumber(a.value) or 0\n local bv = tonumber(b.value) or 0\n\n if av == bv then\n return (tonumber(a.index) or 0) < (tonumber(b.index) or 0)\n end\n\n return av > bv\n end)\n\n local displayParts = {}\n for _, item in ipairs(displayItems) do\n table.insert(displayParts, item.text)\n end\n\n table.insert(\n lines,\n \"[Reward Attribute: \" .. table.concat(displayParts, \" \")\n .. \" | Strength: \" .. formatAttributeRewardNumber(gains[1])\n .. \" | Reflex: \" .. formatAttributeRewardNumber(gains[2])\n .. \" | Willpower: \" .. formatAttributeRewardNumber(gains[3]) .. \"]\"\n )\n end\n\n local attributeBaseValues = {\n getBasicBaseValue(heroineStat.strength),\n getBasicBaseValue(heroineStat.reflex),\n getBasicBaseValue(heroineStat.willpower),\n }\n\n local basicOrder = { 1, 2, 3 }\n shuffleArray(basicOrder)\n\n local prevRankMap = nil\n\n local attributeReward1 = makeAttributeRewardFullLine(\n basicOrder[1],\n attributeBaseValues,\n prevRankMap\n )\n\n prevRankMap = attributeReward1.rankMap\n\n local attributeReward2 = makeAttributeRewardFullLine(\n basicOrder[2],\n attributeBaseValues,\n prevRankMap\n )\n\n balanceAttributeRewardPair(attributeReward1, attributeReward2)\n\n insertAttributeRewardLine(attributeReward1, lines)\n insertAttributeRewardLine(attributeReward2, lines)\n end\n end\n\n do\n if useStatReward then\n table.insert(lines, \"[Blank Line]\")\n\n local encounterStatValue = tonumber(getEncounterStatValue(triggerId, encounterLevel)) or 0\n\n local function formatSignedIntOrZero(v)\n v = tonumber(v) or 0\n v = math.floor(v)\n\n if v == 0 then\n return \"0\"\n end\n\n return string.format(\"%+d\", v)\n end\n\n local function applyStatRewardOffset(baseValue, offset)\n local value = math.floor(tonumber(baseValue) or 0)\n value = value + (tonumber(offset) or 0)\n value = math.floor(value * rewardStatAttributeMultiplier)\n return math.max(1, value)\n end\n\n local basicBaseValue = math.floor(encounterStatValue * 0.5)\n local nerfedBaseValue = math.floor(encounterStatValue * 0.25)\n\n local statMeta = {\n [1] = { emoji = \"❤️\", label = \"HP\" },\n [2] = { emoji = \"⚡\", label = \"Resource\" },\n [3] = { emoji = \"💗\", label = \"Arousal\" },\n }\n\n local function makeStatRewardPairLine(basicIndex, nerfedIndex)\n local gains = { 0, 0, 0 }\n\n -- 한 쌍마다 반드시 +1 하나, -1 하나\n local pairOffsets = { 1, -1 }\n shuffleArray(pairOffsets)\n\n gains[basicIndex] = applyStatRewardOffset(basicBaseValue, pairOffsets[1])\n gains[nerfedIndex] = applyStatRewardOffset(nerfedBaseValue, pairOffsets[2])\n\n return {\n gains = gains,\n basicIndex = basicIndex,\n nerfedIndex = nerfedIndex,\n }\n end\n\n local function getStatRewardTotal(reward)\n local total = 0\n\n for i = 1, 3 do\n total = total + math.floor(tonumber(reward.gains[i]) or 0)\n end\n\n return total\n end\n\n local function addStatBalanceAmount(reward, amount)\n amount = math.floor(tonumber(amount) or 0)\n\n if amount <= 0 then\n return\n end\n\n -- 이 선택지에 실제로 들어간 2개 스탯에만 보정한다.\n -- 0인 나머지 스탯에는 절대 넣지 않는다.\n local indices = {\n reward.basicIndex,\n reward.nerfedIndex,\n }\n\n for _ = 1, amount do\n local index = indices[math.random(1, #indices)]\n\n reward.gains[index] = math.floor((tonumber(reward.gains[index]) or 0) + 1)\n end\n end\n\n local function balanceStatRewardPair(rewardA, rewardB)\n local totalA = getStatRewardTotal(rewardA)\n local totalB = getStatRewardTotal(rewardB)\n\n if totalA == totalB then\n return\n end\n\n if totalA < totalB then\n addStatBalanceAmount(rewardA, totalB - totalA)\n else\n addStatBalanceAmount(rewardB, totalA - totalB)\n end\n end\n\n local function insertStatRewardPairLine(reward, lines)\n local gains = reward.gains\n local basicIndex = reward.basicIndex\n local nerfedIndex = reward.nerfedIndex\n\n local displayItems = {\n {\n index = basicIndex,\n value = gains[basicIndex],\n text = statMeta[basicIndex].emoji .. \" 최대치 \" .. formatSignedIntOrZero(gains[basicIndex])\n },\n {\n index = nerfedIndex,\n value = gains[nerfedIndex],\n text = statMeta[nerfedIndex].emoji .. \" 최대치 \" .. formatSignedIntOrZero(gains[nerfedIndex])\n },\n }\n\n table.sort(displayItems, function(a, b)\n local av = tonumber(a.value) or 0\n local bv = tonumber(b.value) or 0\n\n if av == bv then\n return (tonumber(a.index) or 0) < (tonumber(b.index) or 0)\n end\n\n return av > bv\n end)\n\n local displayParts = {}\n for _, item in ipairs(displayItems) do\n table.insert(displayParts, item.text)\n end\n\n table.insert(\n lines,\n \"[Reward Stat: \" .. table.concat(displayParts, \" \")\n .. \" | HP: \" .. formatSignedIntOrZero(gains[1])\n .. \" | Resource: \" .. formatSignedIntOrZero(gains[2])\n .. \" | Arousal: \" .. formatSignedIntOrZero(gains[3]) .. \"]\"\n )\n end\n\n local basicOrder = { 1, 2, 3 }\n shuffleArray(basicOrder)\n\n local basicIndex1 = basicOrder[1]\n local basicIndex2 = basicOrder[2]\n local extraIndex = basicOrder[3]\n\n local nerfedIndex1\n local nerfedIndex2\n\n if math.random(2) == 1 then\n nerfedIndex1 = basicIndex2\n nerfedIndex2 = extraIndex\n else\n nerfedIndex1 = extraIndex\n nerfedIndex2 = basicIndex1\n end\n\n local statReward1 = makeStatRewardPairLine(basicIndex1, nerfedIndex1)\n local statReward2 = makeStatRewardPairLine(basicIndex2, nerfedIndex2)\n\n balanceStatRewardPair(statReward1, statReward2)\n\n insertStatRewardPairLine(statReward1, lines)\n insertStatRewardPairLine(statReward2, lines)\n end\n end\n\n do\n local baseRecoverHP = 0\n local baseRecoverResource = 0\n local recoveryGroupStarted = false\n\n local function ensureRecoveryGroupStart()\n if recoveryGroupStarted == false then\n table.insert(lines, \"[Blank Line]\")\n recoveryGroupStarted = true\n end\n end\n\n if maxHP > 0 then\n local missingHP = roundToTenth(math.max(0, maxHP - currentHP))\n local rawRecoverHP = math.max(1, roundHalfUp(maxHP * (recoveryPercent / 100)))\n baseRecoverHP = clamp(rawRecoverHP, 0, missingHP)\n end\n\n if maxResource > 0 then\n local missingResource = roundToTenth(math.max(0, maxResource - currentResource))\n local rawRecoverResource = math.max(1, roundHalfUp(maxResource * (recoveryPercent / 100)))\n baseRecoverResource = clamp(rawRecoverResource, 0, missingResource)\n end\n\n -- 1) HP + Resource 동시 회복\n -- 둘 다 실제로 회복될 때만 표시\n if baseRecoverHP > 0 and baseRecoverResource > 0 then\n local recoveryDisplayParts = {\n \"❤️+\" .. tostring(baseRecoverHP),\n \"⚡+\" .. tostring(baseRecoverResource),\n }\n\n ensureRecoveryGroupStart()\n\n table.insert(\n lines,\n \"[Reward Recovery: \" .. table.concat(recoveryDisplayParts, \" \")\n .. \" | HP: \" .. tostring(baseRecoverHP)\n .. \" | Resource: \" .. tostring(baseRecoverResource) .. \"]\"\n )\n end\n\n if allowSingleRecoveryReward then\n local missingHP = roundToTenth(math.max(0, maxHP - currentHP))\n local missingResource = roundToTenth(math.max(0, maxResource - currentResource))\n\n -- 2) HP만 회복\n -- Resource가 가득 차서 합본 회복이 불가능한 경우에도 표시\n if baseRecoverHP > 0 and baseRecoverResource <= 0 then\n ensureRecoveryGroupStart()\n\n table.insert(\n lines,\n \"[Reward Recovery: ❤️+\" .. tostring(baseRecoverHP)\n .. \" | HP: \" .. tostring(baseRecoverHP)\n .. \" | Resource: 0]\"\n )\n\n -- 합본 회복보다 HP를 더 많이 채워줄 수 있을 때 표시\n elseif baseRecoverHP > 0 and baseRecoverHP < missingHP then\n local onlyRecoverHP = clamp(baseRecoverHP * 2, 0, missingHP)\n\n if onlyRecoverHP > baseRecoverHP then\n ensureRecoveryGroupStart()\n\n table.insert(\n lines,\n \"[Reward Recovery: ❤️+\" .. tostring(onlyRecoverHP)\n .. \" | HP: \" .. tostring(onlyRecoverHP)\n .. \" | Resource: 0]\"\n )\n end\n end\n\n -- 3) Resource만 회복\n -- HP가 가득 차서 합본 회복이 불가능한 경우에도 표시\n if baseRecoverResource > 0 and baseRecoverHP <= 0 then\n ensureRecoveryGroupStart()\n\n table.insert(\n lines,\n \"[Reward Recovery: ⚡+\" .. tostring(baseRecoverResource)\n .. \" | HP: 0\"\n .. \" | Resource: \" .. tostring(baseRecoverResource) .. \"]\"\n )\n\n -- 합본 회복보다 Resource를 더 많이 채워줄 수 있을 때 표시\n elseif baseRecoverResource > 0 and baseRecoverResource < missingResource then\n local onlyRecoverResource = clamp(baseRecoverResource * 2, 0, missingResource)\n\n if onlyRecoverResource > baseRecoverResource then\n ensureRecoveryGroupStart()\n\n table.insert(\n lines,\n \"[Reward Recovery: ⚡+\" .. tostring(onlyRecoverResource)\n .. \" | HP: 0\"\n .. \" | Resource: \" .. tostring(onlyRecoverResource) .. \"]\"\n )\n end\n end\n end\n end\n\n if allowArousalDownReward then\n local arousalRatio = 0\n\n if maxArousal > 0 then\n arousalRatio = currentArousal / maxArousal\n end\n\n if arousalRatio >= 0.25 and currentArousal > 0 then\n local reduceArousal = math.max(1, roundHalfUp(currentArousal * 0.8))\n reduceArousal = clamp(reduceArousal, 0, currentArousal)\n\n table.insert(lines, \"[Blank Line]\")\n\n table.insert(\n lines,\n \"[Reward ArousalDown: 💗-\" .. tostring(reduceArousal)\n .. \" | Arousal: \" .. tostring(reduceArousal) .. \"]\"\n )\n end\n end\n\n selectCount = clamp(selectCount, 0, #lines)\n\n local rewardChoice = \"\"\n local rewardCount = \"0\"\n\n if #lines > 0 and selectCount > 0 then\n rewardChoice =\n \"<Reward Choice>\\n\"\n .. table.concat(lines, \"\\n\")\n .. \"\\n</Reward Choice>\"\n\n rewardCount = tostring(selectCount)\n end\n\n setChatVar(triggerId, \"vg_Reward_Choice\", rewardChoice)\n setChatVar(triggerId, \"vg_Reward_Count\", rewardCount)\n setChatVar(triggerId, \"vg_Reward_Selected\", \"\")\n setChatVar(triggerId, \"vg_Reward_Log\", \"\")\nend\n\n-- ==========================================\n-- 추가 선택지\n-- ==========================================\nfunction buildModeChangeChoice(triggerId, targetMode)\n local lang = getLanguage(triggerId)\n targetMode = tonumber(targetMode) or constMode.free\n\n local choiceText = \"\"\n local emoji = \"🔁\"\n\n if targetMode == constMode.vg then\n emoji = \"🎮\"\n\n if lang == 1 then\n choiceText = \"VG 모드로 전환\"\n elseif lang == 3 then\n choiceText = \"VGモードに切り替える\"\n else\n choiceText = \"Switch to VG Mode\"\n end\n\n elseif targetMode == constMode.vg_Recovery then\n emoji = \"🕹️\"\n\n if lang == 1 then\n choiceText = \"VG 모드로 전환 (회복)\"\n elseif lang == 3 then\n choiceText = \"VGモードに切り替える(回復)\"\n else\n choiceText = \"Switch to VG Mode (Recovery)\"\n end\n\n elseif targetMode == constMode.free then\n emoji = \"🕊️\"\n\n if lang == 1 then\n choiceText = \"자유 모드로 전환\"\n elseif lang == 3 then\n choiceText = \"フリーモードに切り替える\"\n else\n choiceText = \"Switch to Free Mode\"\n end\n\n elseif targetMode == constMode.cancel then\n emoji = \"❌\"\n\n if lang == 1 then\n choiceText = \"모드 전환 취소\"\n elseif lang == 3 then\n choiceText = \"モード切り替えをキャンセル\"\n else\n choiceText = \"Cancel Mode Change\"\n end\n end\n\n if choiceText == \"\" then\n return \"\"\n end\n\n return \"[Mode Change: \" .. choiceText .. \" | Mode: \" .. tostring(targetMode) .. \" | Emoji: \" .. emoji .. \"]\"\nend\n\nfunction buildAdditionalMessageChoice(triggerId)\n local lang = getLanguage(triggerId)\n\n local emoji = \"📝\"\n local choiceText = \"\"\n\n if lang == 1 then\n choiceText = \"추가 입력 메시지\"\n elseif lang == 3 then\n choiceText = \"追加入力メッセージ\"\n else\n choiceText = \"Additional Input Message\"\n end\n\n return \"[Additional Message: \" .. choiceText ..\n \" | Emoji: \" .. emoji .. \"]\"\nend\n\nfunction buildStoryRecordChoice(triggerId, prevPhase)\n local lang = getLanguage(triggerId)\n\n local emoji = \"📜\"\n local choiceText = \"\"\n\n if lang == 1 then\n choiceText = \"스토리 기록\"\n elseif lang == 3 then\n choiceText = \"ストーリー記録\"\n else\n choiceText = \"Story Record\"\n end\n\n if isMasturbationPhase(triggerId, prevPhase) then\n return \"[Solo Record: \" .. choiceText ..\n \" | Emoji: \" .. emoji ..\n \" | Record Type: \" .. tostring(constResolution.addMasturbationRecord) .. \"]\"\n\n elseif isExhibitionismPhase(triggerId, prevPhase) then\n return \"[Solo Record: \" .. choiceText ..\n \" | Emoji: \" .. emoji ..\n \" | Record Type: \" .. tostring(constResolution.addExhibitionismRecord) .. \"]\"\n\n else\n return \"[Story Record: \" .. choiceText ..\n \" | Emoji: \" .. emoji .. \"]\"\n end\nend\n\n-- ==========================================\n-- 출력 교정\n-- ==========================================\nfunction removeHeroineTraitTagsFromCleanMessage(triggerId, cleanMsg)\n cleanMsg = tostring(cleanMsg or \"\")\n\n local traitNames = {}\n local seen = {}\n\n for _, varName in ipairs({\n \"vg_SexualTrait_Content\",\n \"vg_Corruption_Content\",\n }) do\n local raw = safeGet(triggerId, varName, \"\")\n\n for _, entry in ipairs(splitPipe(raw)) do\n -- \"이름: 설명\" 형태면 \":\" 앞만 사용\n -- \":\"가 없으면 entry 전체를 이름으로 사용\n local beforeColon = tostring(entry or \"\"):match(\"^(.-)%s*:\")\n local traitName = trim(beforeColon or entry or \"\")\n\n if traitName ~= \"\" then\n local key = string.lower(traitName)\n\n if not seen[key] then\n seen[key] = true\n table.insert(traitNames, traitName)\n end\n end\n end\n end\n\n -- 긴 이름 먼저 제거\n -- 예: \"A\"보다 \"A B\"를 먼저 지워야 꼬일 가능성이 줄어듦\n table.sort(traitNames, function(a, b)\n return #a > #b\n end)\n\n for _, traitName in ipairs(traitNames) do\n -- Lua pattern 특수문자 이스케이프\n local escapedName = tostring(traitName):gsub(\"([%(%)%.%%%+%-%*%?%[%]%^%$])\", \"%%%1\")\n\n -- 앞쪽 공백/탭은 같이 제거\n -- 뒤쪽 공백은 절대 건드리지 않음\n --\n -- \"문장(Trait) 다음\" -> \"문장 다음\"\n -- \"문장 (Trait) 다음\" -> \"문장 다음\"\n -- \"문장 (Trait)\" -> \"문장\"\n cleanMsg = cleanMsg:gsub(\"[ \\t]*%(\" .. escapedName .. \"%)\", \"\")\n end\n\n return cleanMsg\nend\n\n-- ==========================================\n-- 파싱 날짜\n-- ==========================================\nfunction applyDateTimeFromStatusWindow(rawText)\n rawText = tostring(rawText or \"\")\n\n local statusBody = rawText:match(\"<Status Window>([%s%S]-)</Status Window>\")\n if not statusBody or statusBody == \"\" then\n return \"\", \"\"\n end\n\n for bracket in statusBody:gmatch(\"%b[]\") do\n local inner = trim(bracket:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\"))\n\n if inner:find(\"Date:\", 1, true) and inner:find(\"Time:\", 1, true) then\n local parsedDate = trim(inner:match(\"Date:%s*(.-)%s*|%s*Time:\") or \"\")\n local parsedTime = trim(inner:match(\"|%s*Time:%s*(.-)%s*|%s*Location:\") or \"\")\n\n parsedDate = normalizeLooseDateText(parsedDate)\n\n if parsedDate ~= \"\" or parsedTime ~= \"\" then\n return parsedDate, parsedTime\n end\n end\n end\n\n return \"\", \"\"\nend\n\nfunction getPreviousCharDateTime(triggerId)\n local history = getFullChat(triggerId)\n if not history or #history == 0 then\n return \"\", \"\"\n end\n\n local foundCurrent = false\n\n for i = #history, 1, -1 do\n local msg = history[i]\n\n if msg and msg.role == \"char\" and type(msg.data) == \"string\" then\n local parsedDate, parsedTime = applyDateTimeFromStatusWindow(msg.data)\n\n if parsedDate ~= \"\" or parsedTime ~= \"\" then\n if foundCurrent then\n return parsedDate, parsedTime\n end\n\n -- 가장 최근 날짜 있는 char 응답 1개는 건너뛰고\n -- 그 다음 것을 반환\n foundCurrent = true\n end\n end\n end\n\n return \"\", \"\"\nend\n\n-- ==========================================\n-- 파싱 스토리\n-- ==========================================\nlocal function parseStoryChoiceBlock(triggerId, cleanMsg)\n local storyChoiceRaw = cleanMsg:match(\"<Story Choice>%s*([%s%S]-)%s*</Story Choice>\")\n local choiceLines = {}\n local parsedEntries = {}\n\n if not storyChoiceRaw or trim(storyChoiceRaw) == \"\" then\n return cleanMsg, choiceLines\n end\n\n local actMaxLevel = getStoryChoiceBaseLevel(triggerId)\n local actMinLevel = math.max(1, actMaxLevel - 4)\n\n local heroineLevel = math.max(1, getHeroineLevel(triggerId))\n heroineLevel = clamp(heroineLevel, actMinLevel, actMaxLevel)\n\n local sourceIndex = 0\n\n for entry in tostring(storyChoiceRaw):gmatch(\"%b[]\") do\n local raw = trim(entry:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\"))\n\n local fields = parseOrderedFieldsWithFallback(raw, {\n \"Date\",\n \"Story Type\",\n \"Situation\",\n \"Encounter\",\n \"Encounter Type\",\n \"Difficulty\"\n })\n\n local date = trim(fields[\"date\"] or \"\")\n local storyTypeText = trim(fields[\"story type\"] or \"\")\n local situation = trim(fields[\"situation\"] or \"\")\n local encounter = trim(fields[\"encounter\"] or \"\")\n local encounterTypeText = trim(fields[\"encounter type\"] or \"\")\n local difficultyText = trim(fields[\"difficulty\"] or \"\")\n\n local loweredStoryType = string.lower(trim(tostring(storyTypeText or \"\")))\n local storyTypeCode = constStoryType.side\n local storyEmoji = \"🔹\"\n\n if loweredStoryType == \"core\" then\n storyTypeCode = constStoryType.core\n storyEmoji = \"🔶\"\n elseif loweredStoryType == \"side\" then\n storyTypeCode = constStoryType.side\n storyEmoji = \"🔹\"\n end\n\n local encounterTypeCode = parseEncounterTypeCode(encounterTypeText)\n\n local loweredDifficulty = string.lower(trim(tostring(difficultyText or \"\")))\n local difficultyCode = constSessionDifficulty.medium\n\n if loweredDifficulty == \"low\" then\n difficultyCode = constSessionDifficulty.low\n elseif loweredDifficulty == \"medium\" then\n difficultyCode = constSessionDifficulty.medium\n elseif loweredDifficulty == \"high\" then\n difficultyCode = constSessionDifficulty.high\n end\n\n if date ~= \"\"\n and situation ~= \"\"\n and encounter ~= \"\"\n and encounterTypeCode ~= 0 then\n\n sourceIndex = sourceIndex + 1\n\n table.insert(parsedEntries, {\n date = date,\n storyTypeCode = storyTypeCode,\n storyEmoji = storyEmoji,\n situation = situation,\n encounter = encounter,\n encounterTypeCode = encounterTypeCode,\n encounterEmoji = getEncounterEmojiByType(encounterTypeCode),\n difficultyCode = difficultyCode,\n difficultyEmoji = getDifficultyEmojiByCode(difficultyCode),\n sourceIndex = sourceIndex,\n level = 1,\n })\n end\n end\n\n table.sort(parsedEntries, function(a, b)\n local aDate = parseISODateText(a.date)\n local bDate = parseISODateText(b.date)\n\n local aSerial = aDate and dateTableToSerial(aDate) or nil\n local bSerial = bDate and dateTableToSerial(bDate) or nil\n\n if aSerial and bSerial and aSerial ~= bSerial then\n return aSerial < bSerial\n end\n\n if aSerial and not bSerial then\n return true\n elseif not aSerial and bSerial then\n return false\n end\n\n local aSourceIndex = tonumber(a.sourceIndex) or 999999\n local bSourceIndex = tonumber(b.sourceIndex) or 999999\n return aSourceIndex < bSourceIndex\n end)\n\n -- ==========================================\n -- 레벨 계산\n -- ==========================================\n do\n local subEntries = {}\n\n for _, entry in ipairs(parsedEntries) do\n if entry.storyTypeCode == constStoryType.side then\n table.insert(subEntries, entry)\n else\n entry.level = getMainStoryLevel(triggerId, 1)\n end\n end\n\n if #subEntries > 0 then\n local subMinLevel = math.max(actMinLevel, heroineLevel - 2)\n local subMaxLevel = math.min(actMaxLevel, heroineLevel + 2)\n\n local baseSubLevelPool = {}\n\n for level = subMinLevel, subMaxLevel do\n table.insert(baseSubLevelPool, level)\n end\n\n local subLevelPool = {}\n for _, level in ipairs(baseSubLevelPool) do\n table.insert(subLevelPool, level)\n end\n shuffleArray(subLevelPool)\n\n local poolIndex = 1\n\n for _, entry in ipairs(subEntries) do\n if poolIndex > #subLevelPool then\n subLevelPool = {}\n\n for _, level in ipairs(baseSubLevelPool) do\n table.insert(subLevelPool, level)\n end\n\n shuffleArray(subLevelPool)\n poolIndex = 1\n end\n\n entry.level = subLevelPool[poolIndex] or subMinLevel\n poolIndex = poolIndex + 1\n end\n end\n end\n\n -- ==========================================\n -- 난이도 단계별 승급\n -- 규칙:\n -- - 카운트에는 Main / Sub 모두 포함\n -- - 승급 대상은 Sub만 가능\n -- - 승급 대상은 해당 난이도 Sub 중 level이 가장 높은 1개\n -- - 각 단계는 이전 단계의 승급 결과를 반영한 현재 상태로 다시 판정\n -- ==========================================\n do\n local function tryPromoteDifficulty(fromDifficultyCode, toDifficultyCode, chancePercent)\n local totalCount = 0\n local candidates = {}\n\n for _, entry in ipairs(parsedEntries) do\n if tonumber(entry.difficultyCode) == tonumber(fromDifficultyCode) then\n totalCount = totalCount + 1\n\n -- 승급 대상은 Sub만\n if tonumber(entry.storyTypeCode) == constStoryType.side then\n table.insert(candidates, entry)\n end\n end\n end\n\n -- Main/Sub 합쳐서 2개 미만이면 승급 없음\n if totalCount < 2 then\n return false\n end\n\n -- Sub가 없으면 승급할 대상 없음\n if #candidates <= 0 then\n return false\n end\n\n -- 핵심:\n -- 3개 이상이면 무조건 승급\n -- 2개면 기존 확률 사용\n if totalCount >= 3 then\n chancePercent = 100\n else\n chancePercent = tonumber(chancePercent) or 100\n chancePercent = clamp(chancePercent, 0, 100)\n end\n\n if chancePercent < 100 then\n if math.random(100) > chancePercent then\n return false\n end\n end\n\n -- 해당 난이도 Sub 중 level 가장 높은 것 1개 승급\n local promoteTarget = candidates[1]\n\n for i = 2, #candidates do\n local current = candidates[i]\n local currentLevel = tonumber(current.level) or 0\n local targetLevel = tonumber(promoteTarget.level) or 0\n\n if currentLevel > targetLevel then\n promoteTarget = current\n end\n end\n\n promoteTarget.difficultyCode = tonumber(toDifficultyCode) or promoteTarget.difficultyCode\n promoteTarget.difficultyEmoji = getDifficultyEmojiByCode(promoteTarget.difficultyCode)\n\n return true\n end\n\n tryPromoteDifficulty(\n constSessionDifficulty.low,\n constSessionDifficulty.medium,\n 50\n )\n\n tryPromoteDifficulty(\n constSessionDifficulty.medium,\n constSessionDifficulty.high,\n 50\n )\n\n tryPromoteDifficulty(\n constSessionDifficulty.high,\n constSessionDifficulty.elite,\n 50\n )\n end\n\n for _, entry in ipairs(parsedEntries) do\n table.insert(\n choiceLines,\n \"[Story Date: \" .. entry.date ..\n \" | Story Emoji: \" .. entry.storyEmoji ..\n \" | Story Type: \" .. tostring(entry.storyTypeCode) ..\n \" | Situation: \" .. entry.situation ..\n \" | Encounter Emoji: \" .. entry.encounterEmoji ..\n \" | Encounter: \" .. entry.encounter ..\n \" | Encounter Type: \" .. tostring(entry.encounterTypeCode) ..\n \" | Difficulty Emoji: \" .. entry.difficultyEmoji ..\n \" | Difficulty: \" .. tostring(entry.difficultyCode) ..\n \" | Level: \" .. tostring(entry.level) ..\n \" | Trait: \" .. tostring(constEncounterTrait.none) .. \"]\"\n )\n end\n\n cleanMsg = cleanMsg:gsub(\"<Story Choice>%s*[%s%S]-%s*</Story Choice>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, choiceLines\nend\n\n-- ==========================================\n-- 파싱 일상\n-- ==========================================\nlocal function parseDailyChoiceBlock(triggerId, cleanMsg)\n local dailyChoiceRaw = cleanMsg:match(\"<Daily Choice>%s*([%s%S]-)%s*</Daily Choice>\")\n local choiceLines = {}\n\n if not dailyChoiceRaw or trim(dailyChoiceRaw) == \"\" then\n return cleanMsg, choiceLines\n end\n\n for entry in tostring(dailyChoiceRaw):gmatch(\"%b[]\") do\n local raw = trim(entry:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\"))\n local fields = parseOrderedFieldsWithFallback(raw, { \"Emoji\", \"Situation\" })\n\n local emoji = trim(fields[\"emoji\"] or \"\")\n local situation = trim(fields[\"situation\"] or \"\")\n\n if situation ~= \"\" and emoji ~= \"\" then\n table.insert(\n choiceLines,\n \"[Daily Situation: \" .. situation .. \" | Emoji: \" .. emoji .. \"]\"\n )\n end\n end\n\n cleanMsg = cleanMsg:gsub(\"<Daily Choice>%s*[%s%S]-%s*</Daily Choice>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, choiceLines\nend\n\n-- ==========================================\n-- 파싱 중간 과정\n-- ==========================================\nlocal function parseInterludeChoiceBlock(triggerId, cleanMsg)\n local interludeChoiceRaw = cleanMsg:match(\"<Interlude Choice>%s*([%s%S]-)%s*</Interlude Choice>\")\n local choiceLines = {}\n\n if not interludeChoiceRaw or trim(interludeChoiceRaw) == \"\" then\n return cleanMsg, choiceLines\n end\n\n for entry in tostring(interludeChoiceRaw):gmatch(\"%b[]\") do\n local raw = trim(entry:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\"))\n local fields = parseOrderedFieldsWithFallback(raw, { \"Emoji\", \"Situation\" })\n\n local emoji = trim(fields[\"emoji\"] or \"\")\n local situation = trim(fields[\"situation\"] or \"\")\n\n if situation ~= \"\" and emoji ~= \"\" then\n table.insert(\n choiceLines,\n \"[Interlude Situation: \" .. situation .. \" | Emoji: \" .. emoji .. \"]\"\n )\n end\n end\n\n cleanMsg = cleanMsg:gsub(\"<Interlude Choice>%s*[%s%S]-%s*</Interlude Choice>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, choiceLines\nend\n\n-- ==========================================\n-- 파싱 어드밴스\n-- ==========================================\nlocal function parseAdvanceChoiceBlock(triggerId, cleanMsg)\n local advanceChoiceRaw = cleanMsg:match(\"<Advance Choice>%s*([%s%S]-)%s*</Advance Choice>\")\n local choiceLines = {}\n local parsedEntries = {}\n\n if not advanceChoiceRaw or trim(advanceChoiceRaw) == \"\" then\n return cleanMsg, choiceLines\n end\n\n local contextSlotList = getDualFlagSlotList(triggerId, vgContext.varName)\n local sessionSlotList = getDualFlagSlotList(triggerId, vgSessionInfo.varName)\n\n local stageValue = tonumber(getResolvedDualFlagFromSlotList(sessionSlotList, vgSessionInfo.stage, 2)) or 2\n local storyTypeValue = tonumber(getResolvedDualFlagFromSlotList(sessionSlotList, vgSessionInfo.storyType, constStoryType.core)) or constStoryType.core\n\n stageValue = clamp(stageValue, 1, 4)\n\n local fixedLevel = getMainStoryLevel(triggerId, stageValue)\n local fixedDifficultyCode = constSessionDifficulty.elite\n if stageValue >= 3 then\n fixedDifficultyCode = constSessionDifficulty.boss\n end\n\n local storyEmoji = (storyTypeValue == constStoryType.core) and \"🔶\" or \"🔹\"\n local difficultyEmoji = getDifficultyEmojiByCode(fixedDifficultyCode)\n\n local sourceIndex = 0\n\n for entry in tostring(advanceChoiceRaw):gmatch(\"%b[]\") do\n local raw = trim(entry:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\"))\n\n local fields = parseOrderedFieldsWithFallback(raw, {\n \"Date\",\n \"Situation\",\n \"Encounter\",\n \"Encounter Type\",\n \"Encounter Trait\"\n })\n\n local date = trim(fields[\"date\"] or \"\")\n local situation = trim(fields[\"situation\"] or \"\")\n local encounter = trim(fields[\"encounter\"] or \"\")\n local encounterTypeText = trim(fields[\"encounter type\"] or \"\")\n local encounterTraitText = trim(fields[\"encounter trait\"] or \"\")\n\n local encounterTypeCode = parseEncounterTypeCode(encounterTypeText)\n\n local encounterTraitCode = constEncounterTrait.none\n do\n local loweredTrait = string.lower(trim(tostring(encounterTraitText or \"\")))\n\n if loweredTrait == \"aggressive\" then\n encounterTraitCode = constEncounterTrait.aggressive\n elseif loweredTrait == \"lustful\" then\n encounterTraitCode = constEncounterTrait.lustful\n elseif loweredTrait == \"tenacious\" then\n encounterTraitCode = constEncounterTrait.tenacious\n elseif loweredTrait == \"cunning\" then\n encounterTraitCode = constEncounterTrait.cunning\n elseif loweredTrait == \"oppressive\" then\n encounterTraitCode = constEncounterTrait.oppressive\n elseif loweredTrait == \"adaptive\" then\n encounterTraitCode = constEncounterTrait.adaptive\n else\n local fallbackTraits = {\n constEncounterTrait.aggressive,\n constEncounterTrait.lustful,\n constEncounterTrait.tenacious,\n }\n\n encounterTraitCode = fallbackTraits[math.random(1, #fallbackTraits)]\n end\n end\n\n if date ~= \"\"\n and situation ~= \"\"\n and encounter ~= \"\"\n and encounterTypeCode ~= 0 then\n\n sourceIndex = sourceIndex + 1\n\n table.insert(parsedEntries, {\n date = date,\n situation = situation,\n encounter = encounter,\n encounterTypeCode = encounterTypeCode,\n encounterEmoji = getEncounterEmojiByType(encounterTypeCode),\n sourceIndex = sourceIndex,\n traitCode = encounterTraitCode,\n })\n end\n end\n\n table.sort(parsedEntries, function(a, b)\n local aSourceIndex = tonumber(a.sourceIndex) or 999999\n local bSourceIndex = tonumber(b.sourceIndex) or 999999\n return aSourceIndex < bSourceIndex\n end)\n\n for _, entry in ipairs(parsedEntries) do\n table.insert(\n choiceLines,\n \"[Advance Date: \" .. entry.date ..\n \" | Story Emoji: \" .. storyEmoji ..\n \" | Story Type: \" .. tostring(storyTypeValue) ..\n \" | Situation: \" .. entry.situation ..\n \" | Encounter Emoji: \" .. entry.encounterEmoji ..\n \" | Encounter: \" .. entry.encounter ..\n \" | Encounter Type: \" .. tostring(entry.encounterTypeCode) ..\n \" | Difficulty Emoji: \" .. difficultyEmoji ..\n \" | Difficulty: \" .. tostring(fixedDifficultyCode) ..\n \" | Level: \" .. tostring(fixedLevel) ..\n \" | Trait: \" .. tostring(entry.traitCode) .. \"]\"\n )\n end\n\n do\n local lang = getLanguage(triggerId)\n local heroineStat = loadDualFlagNextMap(triggerId, vgHeroineStat)\n\n local currentHP = tonumber(heroineStat.hp) or 0\n local maxHP = tonumber(heroineStat.hp_Max) or 0\n local currentResource = tonumber(heroineStat.resource) or 0\n local maxResource = tonumber(heroineStat.resource_Max) or 0\n\n local recoverHP = 0\n local recoverResource = 0\n\n if maxHP > 0 then\n local missingHP = roundToTenth(math.max(0, maxHP - currentHP))\n recoverHP = clamp(roundHalfUp(maxHP * 0.15), 0, missingHP)\n end\n\n if maxResource > 0 then\n local missingResource = roundToTenth(math.max(0, maxResource - currentResource))\n recoverResource = clamp(roundHalfUp(maxResource * 0.15), 0, missingResource)\n end\n\n local displayTextMap = {\n [1] = \"한숨 돌린다.\",\n [2] = \"Take a breather.\",\n [3] = \"ひと息つく。\",\n }\n\n local metaTextMap = {\n [1] = \"더 진행하지 않고 후퇴\",\n [2] = \"Stop advancing and fall back.\",\n [3] = \"これ以上進まず、いったん退く。\",\n }\n\n local displayText = trim(displayTextMap[lang] or displayTextMap[2])\n local metaText = trim(metaTextMap[lang] or metaTextMap[2])\n local recoveryText = \"❤️+\" .. tostring(recoverHP) .. \" ⚡+\" .. tostring(recoverResource)\n\n table.insert(\n choiceLines,\n \"[Advance Rest: \" .. displayText ..\n \" | Meta: \" .. metaText ..\n \" | Emoji: ⏪\" ..\n \" | HP: \" .. tostring(recoverHP) ..\n \" | Resource: \" .. tostring(recoverResource) ..\n \" | Recovery: \" .. recoveryText .. \"]\"\n )\n end\n\n cleanMsg = cleanMsg:gsub(\"<Advance Choice>%s*[%s%S]-%s*</Advance Choice>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, choiceLines\nend\n\n-- ==========================================\n-- 파싱 무력화\n-- ==========================================\nlocal function parseIncapacitatedChoiceBlock(triggerId, cleanMsg, prevPhase)\n local incapacitatedChoiceRaw = cleanMsg:match(\"<Incapacitated Choice>%s*([%s%S]-)%s*</Incapacitated Choice>\")\n local choiceLines = {}\n\n if not incapacitatedChoiceRaw or trim(incapacitatedChoiceRaw) == \"\" then\n return cleanMsg, choiceLines\n end\n\n local sessionSlotList = getDualFlagSlotList(triggerId, vgSessionInfo.varName)\n\n local difficultyValue = tonumber(\n getResolvedDualFlagFromSlotList(\n sessionSlotList,\n vgSessionInfo.difficulty,\n constSessionDifficulty.medium\n )\n ) or constSessionDifficulty.medium\n\n local levelValue = tonumber(\n getResolvedDualFlagFromSlotList(\n sessionSlotList,\n vgSessionInfo.level,\n 1\n )\n ) or 1\n\n for entry in tostring(incapacitatedChoiceRaw):gmatch(\"%b[]\") do\n local raw = trim(entry:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\"))\n\n local responseType = \"\"\n local response = \"\"\n\n for part in raw:gmatch(\"([^|]+)\") do\n local key, value = part:match(\"^%s*(.-)%s*:%s*(.-)%s*$\")\n\n if key then\n key = trim(key)\n value = trim(value or \"\")\n\n if key == \"Response Type\" then\n responseType = value\n elseif key == \"Response\" then\n response = value\n end\n end\n end\n\n local loweredType = string.lower(trim(responseType))\n local phaseCode = 0\n local emoji = \"\"\n\n if loweredType == \"defiant\" then\n if prevPhase == constPhase.brainwashing or prevPhase == constPhase.hypnosis then\n phaseCode = constPhase.event_Defiant_Hypnosis\n elseif prevPhase == constPhase.aphrodisiac then\n phaseCode = constPhase.event_Defiant_Aphrodisiac\n else\n phaseCode = constPhase.event_Defiant\n end\n\n emoji = \"💔\"\n elseif loweredType == \"yielding\" then\n if prevPhase == constPhase.brainwashing or prevPhase == constPhase.hypnosis then\n phaseCode = constPhase.event_Yielding_Hypnosis\n elseif prevPhase == constPhase.aphrodisiac then\n phaseCode = constPhase.event_Yielding_Aphrodisiac\n else\n phaseCode = constPhase.event_Yielding\n end\n\n emoji = \"💋\"\n end\n\n if phaseCode ~= 0 and response ~= \"\" then\n table.insert(\n choiceLines,\n \"[Incapacitated Type: \" .. tostring(phaseCode) ..\n \" | Emoji: \" .. emoji ..\n \" | Response: \" .. response ..\n \" | Difficulty: \" .. tostring(difficultyValue) ..\n \" | Level: \" .. tostring(levelValue) .. \"]\"\n )\n end\n end\n\n cleanMsg = cleanMsg:gsub(\"<Incapacitated Choice>%s*[%s%S]-%s*</Incapacitated Choice>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, choiceLines\nend\n\n-- ==========================================\n-- 파싱 감금\n-- ==========================================\nlocal function parseCaptivityChoiceBlock(triggerId, cleanMsg)\n local captivityChoiceRaw = cleanMsg:match(\"<Captivity Choice>%s*([%s%S]-)%s*</Captivity Choice>\")\n local choiceLines = {}\n local parsedEntries = {}\n\n if not captivityChoiceRaw or trim(captivityChoiceRaw) == \"\" then\n return cleanMsg, choiceLines\n end\n\n local fixedStoryTypeCode = constStoryType.side\n local fixedStoryEmoji = \"🔹\"\n\n local actMaxLevel = getStoryChoiceBaseLevel(triggerId)\n local actMinLevel = math.max(1, actMaxLevel - 4)\n\n local escapeLevelValue = math.max(1, getHeroineLevel(triggerId))\n escapeLevelValue = clamp(escapeLevelValue, actMinLevel, actMaxLevel)\n\n local function parseCaptivityEncounterTypeCode(rawText)\n local loweredType = string.lower(trim(tostring(rawText or \"\")))\n\n if loweredType == \"escape\" then\n return constPhase.escape\n elseif loweredType == \"rescue\" then\n return constPhase.rescue\n elseif loweredType == \"violence\" then\n return constPhase.event_Violence\n elseif loweredType == \"sleeping\" then\n return constPhase.event_Sleeping\n elseif loweredType == \"immobilized\" then\n return constPhase.event_Immobilized\n elseif loweredType == \"brainwashing\" then\n return constPhase.event_Brainwashing\n elseif loweredType == \"coercion\" then\n return constPhase.event_Coercion\n elseif loweredType == \"aphrodisiac\" then\n return constPhase.event_Aphrodisiac\n end\n\n return 0\n end\n\n local sourceIndex = 0\n\n for entry in tostring(captivityChoiceRaw):gmatch(\"%b[]\") do\n local raw = trim(entry:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\"))\n\n local fields = parseOrderedFieldsWithFallback(raw, {\n \"Date\",\n \"Situation\",\n \"Encounter\",\n \"Encounter Type\"\n })\n\n local date = trim(fields[\"date\"] or \"\")\n local situation = trim(fields[\"situation\"] or \"\")\n local encounter = trim(fields[\"encounter\"] or \"\")\n local encounterTypeText = trim(fields[\"encounter type\"] or \"\")\n\n local encounterTypeCode = parseCaptivityEncounterTypeCode(encounterTypeText)\n\n local fixedDifficultyCode = constSessionDifficulty.high\n local fixedLevel = actMaxLevel\n\n if encounterTypeCode == constPhase.escape or encounterTypeCode == constPhase.rescue then\n fixedDifficultyCode = constSessionDifficulty.elite\n fixedLevel = escapeLevelValue\n end\n\n if date ~= \"\" and situation ~= \"\" and encounter ~= \"\" and encounterTypeCode ~= 0 then\n\n sourceIndex = sourceIndex + 1\n\n table.insert(parsedEntries, {\n date = date,\n storyTypeCode = fixedStoryTypeCode,\n storyEmoji = fixedStoryEmoji,\n situation = situation,\n encounter = encounter,\n encounterTypeCode = encounterTypeCode,\n encounterEmoji = getEncounterEmojiByType(encounterTypeCode),\n difficultyCode = fixedDifficultyCode,\n difficultyEmoji = getDifficultyEmojiByCode(fixedDifficultyCode),\n sourceIndex = sourceIndex,\n level = fixedLevel,\n })\n end\n end\n\n table.sort(parsedEntries, function(a, b)\n local aDate = parseISODateText(a.date)\n local bDate = parseISODateText(b.date)\n\n local aSerial = aDate and dateTableToSerial(aDate) or nil\n local bSerial = bDate and dateTableToSerial(bDate) or nil\n\n if aSerial and bSerial and aSerial ~= bSerial then\n return aSerial < bSerial\n end\n\n if aSerial and not bSerial then\n return true\n elseif not aSerial and bSerial then\n return false\n end\n\n local aSourceIndex = tonumber(a.sourceIndex) or 999999\n local bSourceIndex = tonumber(b.sourceIndex) or 999999\n return aSourceIndex < bSourceIndex\n end)\n\n for _, entry in ipairs(parsedEntries) do\n table.insert(\n choiceLines,\n \"[Story Date: \" .. entry.date ..\n \" | Story Emoji: \" .. entry.storyEmoji ..\n \" | Story Type: \" .. tostring(entry.storyTypeCode) ..\n \" | Situation: \" .. entry.situation ..\n \" | Encounter Emoji: \" .. entry.encounterEmoji ..\n \" | Encounter: \" .. entry.encounter ..\n \" | Encounter Type: \" .. tostring(entry.encounterTypeCode) ..\n \" | Difficulty Emoji: \" .. entry.difficultyEmoji ..\n \" | Difficulty: \" .. tostring(entry.difficultyCode) ..\n \" | Level: \" .. tostring(entry.level) ..\n \" | Trait: \" .. tostring(constEncounterTrait.none) .. \"]\"\n )\n end\n\n cleanMsg = cleanMsg:gsub(\"<Captivity Choice>%s*[%s%S]-%s*</Captivity Choice>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, choiceLines\nend\n\n-- ==========================================\n-- 파싱 부자유\n-- ==========================================\nlocal function parseUnfreeChoiceBlock(triggerId, cleanMsg)\n local unfreeChoiceRaw = cleanMsg:match(\"<Unfree Choice>%s*([%s%S]-)%s*</Unfree Choice>\")\n local choiceLines = {}\n\n if not unfreeChoiceRaw or trim(unfreeChoiceRaw) == \"\" then\n return cleanMsg, choiceLines\n end\n\n for entry in tostring(unfreeChoiceRaw):gmatch(\"%b[]\") do\n local raw = trim(entry:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\"))\n local fields = parseOrderedFieldsWithFallback(raw, { \"Emoji\", \"Situation\" })\n\n local emoji = trim(fields[\"emoji\"] or \"\")\n local situation = trim(fields[\"situation\"] or \"\")\n\n if situation ~= \"\" and emoji ~= \"\" then\n table.insert(\n choiceLines,\n \"[Unfree Situation: \" .. situation .. \" | Emoji: \" .. emoji .. \"]\"\n )\n end\n end\n\n cleanMsg = cleanMsg:gsub(\"<Unfree Choice>%s*[%s%S]-%s*</Unfree Choice>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, choiceLines\nend\n\n-- ==========================================\n-- 파싱 임신\n-- ==========================================\nlocal function parseVaginalEjaculationRecordBlocks(triggerId, cleanMsg, currentDateText, changedVars)\n local persistentLogs = {}\n\n local function addPersistentLog(text)\n text = trim(tostring(text or \"\"))\n if text ~= \"\" then\n table.insert(persistentLogs, text)\n end\n end\n\n currentDateText = trim(tostring(currentDateText or \"\"))\n currentDateText = trim(currentDateText:match(\"^(%d+%-%d%d?%-%d%d?)\") or currentDateText)\n\n local cycleFlag = nil\n if currentDateText ~= \"\" then\n cycleFlag = getCycleFlag(triggerId, currentDateText)\n end\n\n local isFertile =\n cycleFlag\n and cycleFlag >= constCycle.fertile_m5\n and cycleFlag <= constCycle.fertile_p1\n\n local riskPerCountMap = {\n [constCycle.fertile_m5] = 12,\n [constCycle.fertile_m4] = 16,\n [constCycle.fertile_m3] = 18,\n [constCycle.fertile_m2] = 22,\n [constCycle.fertile_m1] = 26,\n [constCycle.ovulation] = 30,\n [constCycle.fertile_p1] = 22,\n }\n\n local baseRisk = riskPerCountMap[cycleFlag] or 0\n\n -- ==========================================\n -- 이번 응답의 <Vaginal Ejaculation> 먼저 파싱\n -- 같은 파트너가 여러 줄 나오면 응답 내에서 먼저 합산\n -- ==========================================\n local responseOrder = {}\n local responseMap = {}\n\n for body in cleanMsg:gmatch(\"<Vaginal Ejaculation>%s*([%s%S]-)%s*</Vaginal Ejaculation>\") do\n for record in body:gmatch(\"%b[]\") do\n local fields = parseBracketFields(record)\n\n local count = math.max(0, extractInt(fields[\"Count\"]))\n local partner = normalizeIndexedSourceName(fields[\"Partner\"] or \"\")\n\n if count > 0 and isMeaningfulValue(partner) then\n local key = string.lower(partner)\n\n if not responseMap[key] then\n responseMap[key] = {\n partner = partner,\n count = 0,\n }\n table.insert(responseOrder, key)\n end\n\n responseMap[key].count = responseMap[key].count + count\n end\n end\n end\n\n -- ==========================================\n -- 가임기일 때만 반영\n -- pregnancyRisk 와 riskSource 는 같은 인덱스끼리 매칭\n -- 예) \"24^15\" + \"고블린^오크\"\n -- ==========================================\n if isFertile and baseRisk > 0 and #responseOrder > 0 then\n local prevRaw = safeGet(triggerId, vgReproductive.varName, \"\")\n\n local oldRiskRaw = trim(getResolvedDualFlag(\n triggerId,\n vgReproductive.varName,\n vgReproductive.pregnancyRisk,\n \"\"\n ))\n\n local oldSourceRaw = trim(getResolvedDualFlag(\n triggerId,\n vgReproductive.varName,\n vgReproductive.riskSource,\n \"\"\n ))\n\n local oldRiskList = splitLiteral(oldRiskRaw, \"^\")\n local oldSourceList = splitLiteral(oldSourceRaw, \"^\")\n\n local oldTotalRisk = 0\n for i = 1, #oldRiskList do\n oldTotalRisk = oldTotalRisk + math.max(0, extractInt(oldRiskList[i] or \"0\"))\n end\n oldTotalRisk = clamp(oldTotalRisk, 0, 100)\n\n local mergedOrder = {}\n local mergedMap = {}\n\n local oldCount = math.max(#oldRiskList, #oldSourceList)\n\n -- 기존 저장값 먼저 병합\n for i = 1, oldCount do\n local source = trim(oldSourceList[i] or \"\")\n local risk = math.max(0, extractInt(oldRiskList[i] or \"0\"))\n\n if isMeaningfulValue(source) and risk > 0 then\n local key = string.lower(source)\n\n if not mergedMap[key] then\n mergedMap[key] = {\n source = source,\n risk = 0,\n }\n table.insert(mergedOrder, key)\n end\n\n mergedMap[key].risk = clamp((tonumber(mergedMap[key].risk) or 0) + risk, 0, 100)\n end\n end\n\n -- 이번 응답값 누적\n for _, key in ipairs(responseOrder) do\n local info = responseMap[key]\n local partner = trim(info.partner or \"\")\n local count = math.max(0, tonumber(info.count) or 0)\n\n if isMeaningfulValue(partner) and count > 0 then\n local addRisk = clamp(baseRisk * count, 0, 100)\n\n if addRisk > 0 then\n if not mergedMap[key] then\n mergedMap[key] = {\n source = partner,\n risk = 0,\n }\n table.insert(mergedOrder, key)\n end\n\n local beforeRisk = tonumber(mergedMap[key].risk) or 0\n local afterRisk = clamp(beforeRisk + addRisk, 0, 100)\n local gainedRisk = afterRisk - beforeRisk\n\n mergedMap[key].risk = afterRisk\n\n addPersistentLog(\"질내사정: \" .. tostring(count) .. \"회 (\" .. partner .. \")\")\n end\n end\n end\n\n local nextRiskList = {}\n local nextSourceList = {}\n local nextTotalRisk = 0\n\n for _, key in ipairs(mergedOrder) do\n local info = mergedMap[key]\n local source = trim(info.source or \"\")\n local risk = roundHalfUp(tonumber(info.risk) or 0)\n\n if isMeaningfulValue(source) and risk > 0 then\n table.insert(nextRiskList, tostring(risk))\n table.insert(nextSourceList, source)\n nextTotalRisk = nextTotalRisk + risk\n end\n end\n\n nextTotalRisk = clamp(nextTotalRisk, 0, 100)\n\n batchSetDualFlags(triggerId, vgReproductive.varName, {\n { position = vgReproductive.pregnancyRisk, value = table.concat(nextRiskList, \"^\"), isNext = false },\n { position = vgReproductive.riskSource, value = table.concat(nextSourceList, \"^\"), isNext = false },\n })\n\n local gainedTotalRisk = clamp(nextTotalRisk - oldTotalRisk, 0, 100)\n\n if nextTotalRisk >= 100 and oldTotalRisk >= 100 then\n addPersistentLog(\"임신 위험도: 100%\")\n elseif gainedTotalRisk > 0 then\n addPersistentLog(\n \"임신 위험도: \" .. tostring(nextTotalRisk) .. \"% (+\" .. tostring(gainedTotalRisk) .. \"%)\"\n )\n elseif nextTotalRisk > 0 then\n addPersistentLog(\"임신 위험도: \" .. tostring(nextTotalRisk) .. \"%\")\n end\n\n local nextRaw = safeGet(triggerId, vgReproductive.varName, \"\")\n if changedVars and prevRaw ~= nextRaw then\n changedVars[vgReproductive.varName] = nextRaw\n end\n end\n\n cleanMsg = cleanMsg:gsub(\"<Vaginal Ejaculation>%s*[%s%S]-%s*</Vaginal Ejaculation>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, persistentLogs\nend\n\nlocal function parsePregnancyStartRecordBlock(triggerId, cleanMsg, changedVars)\n local persistentLogs = {}\n local memoryAdded = false\n\n local function addPersistentLog(text)\n text = trim(tostring(text or \"\"))\n if text ~= \"\" then\n table.insert(persistentLogs, text)\n end\n end\n\n local body = cleanMsg:match(\"<Pregnancy Start>%s*([%s%S]-)%s*</Pregnancy Start>\")\n if not body or trim(body) == \"\" then\n return cleanMsg, persistentLogs\n end\n\n local fields = parseBracketFields(body)\n\n local fatherText = normalizeIndexedSourceName(fields[\"Father\"] or \"\")\n local daysTotal = math.max(0, extractInt(fields[\"Days Total\"]))\n local statementText = trim(fields[\"Statement\"] or \"\")\n\n local lastPeriodRaw = trim(getResolvedDualFlag(\n triggerId,\n vgReproductive.varName,\n vgReproductive.lastPeriod,\n \"\"\n ))\n local lastPeriodDateTable = parseISODateText(lastPeriodRaw)\n\n if isMeaningfulValue(fatherText) and daysTotal > 0 and isMeaningfulValue(statementText) and lastPeriodDateTable then\n local prevRaw = safeGet(triggerId, vgReproductive.varName, \"\")\n\n -- 배란일 = lastPeriod + 13일\n -- 임신일 = 배란일 + 1일 = lastPeriod + 14일\n local pregnancyDateTable = addDaysToDate(lastPeriodDateTable, 14)\n local pregnancyDateText = formatISODateTable(pregnancyDateTable)\n\n if pregnancyDateText ~= \"\" then\n batchSetDualFlags(triggerId, vgReproductive.varName, {\n { position = vgReproductive.lastPeriod, value = \"\", isNext = false },\n { position = vgReproductive.pregnancyRisk, value = \"\", isNext = false },\n { position = vgReproductive.riskSource, value = \"\", isNext = false },\n { position = vgReproductive.pending, value = \"\", isNext = false },\n { position = vgReproductive.pregnancyDate, value = pregnancyDateText, isNext = false },\n { position = vgReproductive.father, value = fatherText, isNext = false },\n { position = vgReproductive.daysTotal, value = tostring(daysTotal), isNext = false },\n })\n\n local nextRaw = safeGet(triggerId, vgReproductive.varName, \"\")\n if changedVars and prevRaw ~= nextRaw then\n changedVars[vgReproductive.varName] = nextRaw\n end\n\n appendMemoryEntry(triggerId, pregnancyDateText, \"Pregnancy (\" .. buildChildTextByLanguage(triggerId, fatherText) .. \")\", statementText, changedVars)\n memoryAdded = true\n\n local lang = getLanguage(triggerId)\n local pregnancyLogText = \"\"\n\n if lang == 1 then\n pregnancyLogText = \"임신: (\" .. pregnancyDateText .. \")\" .. fatherText .. \"의 아이\"\n elseif lang == 3 then\n pregnancyLogText = \"임신: (\" .. pregnancyDateText .. \")\" .. fatherText .. \"の子\"\n else\n pregnancyLogText = \"임신: (\" .. pregnancyDateText .. \")\" .. fatherText .. \"'s child\"\n end\n\n addPersistentLog(pregnancyLogText)\n end\n end\n\n if memoryAdded then\n buildMemoryDisplay(triggerId)\n end\n\n cleanMsg = cleanMsg:gsub(\"<Pregnancy Start>%s*[%s%S]-%s*</Pregnancy Start>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, persistentLogs\nend\n\nlocal function parsePregnancyEndRecordBlock(triggerId, cleanMsg, currentDateText, changedVars)\n local persistentLogs = {}\n local memoryAdded = false\n\n local function addPersistentLog(text)\n text = trim(tostring(text or \"\"))\n if text ~= \"\" then\n table.insert(persistentLogs, text)\n end\n end\n\n local body = cleanMsg:match(\"<Pregnancy End>%s*([%s%S]-)%s*</Pregnancy End>\")\n if not body or trim(body) == \"\" then\n return cleanMsg, persistentLogs\n end\n\n currentDateText = trim(tostring(currentDateText or \"\"))\n currentDateText = trim(currentDateText:match(\"^(%d+%-%d%d?%-%d%d?)\") or currentDateText)\n\n if currentDateText == \"\" then\n currentDateText = trim((select(1, getLatestCharDateTime(triggerId))) or \"\")\n end\n\n local fields = parseBracketFields(body)\n\n local typeText = trim(fields[\"Type\"] or \"\")\n local summaryText = trim(fields[\"Summary\"] or \"\")\n\n local allowedTypeMap = {\n Birth = true,\n Oviposition = true,\n Miscarriage = true,\n Abortion = true,\n }\n\n local pregnancyDateRaw = trim(getResolvedDualFlag(\n triggerId,\n vgReproductive.varName,\n vgReproductive.pregnancyDate,\n \"\"\n ))\n\n local fatherText = trim(getResolvedDualFlag(\n triggerId,\n vgReproductive.varName,\n vgReproductive.father,\n \"\"\n ))\n\n if allowedTypeMap[typeText]\n and isMeaningfulValue(summaryText)\n and parseISODateText(currentDateText)\n and parseISODateText(pregnancyDateRaw) then\n\n local prevRaw = safeGet(triggerId, vgReproductive.varName, \"\")\n\n batchSetDualFlags(triggerId, vgReproductive.varName, {\n { position = vgReproductive.lastPeriod, value = \"\", isNext = false },\n { position = vgReproductive.pregnancyRisk, value = \"\", isNext = false },\n { position = vgReproductive.riskSource, value = \"\", isNext = false },\n { position = vgReproductive.pending, value = \"\", isNext = false },\n { position = vgReproductive.pregnancyDate, value = \"\", isNext = false },\n { position = vgReproductive.father, value = \"\", isNext = false },\n { position = vgReproductive.daysTotal, value = \"\", isNext = false },\n })\n\n local nextRaw = safeGet(triggerId, vgReproductive.varName, \"\")\n if changedVars and prevRaw ~= nextRaw then\n changedVars[vgReproductive.varName] = nextRaw\n end\n\n setChatVar(triggerId, \"vg_Pregnancy_Pending\", \"\")\n\n appendMemoryEntry(triggerId, currentDateText, typeText .. \" (\" .. buildChildTextByLanguage(triggerId, fatherText) .. \")\", summaryText, changedVars)\n memoryAdded = true\n\n local logTypeText = typeText\n\n if typeText == \"Birth\" then\n logTypeText = \"출산\"\n elseif typeText == \"Oviposition\" then\n logTypeText = \"산란\"\n elseif typeText == \"Miscarriage\" then\n logTypeText = \"유산\"\n elseif typeText == \"Abortion\" then\n logTypeText = \"낙태\"\n end\n\n addPersistentLog(\"(\" .. currentDateText .. \") \" .. logTypeText)\n end\n\n if memoryAdded then\n buildMemoryDisplay(triggerId)\n end\n\n cleanMsg = cleanMsg:gsub(\"<Pregnancy End>%s*[%s%S]-%s*</Pregnancy End>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, persistentLogs\nend\n\n-- ==========================================\n-- 파싱 특성\n-- ==========================================\nlocal function parseSexualTraitRecordBlock(triggerId, cleanMsg, currentDateText)\n local persistentLogs = {}\n\n local function addPersistentLog(text)\n text = trim(tostring(text or \"\"))\n if text ~= \"\" then\n table.insert(persistentLogs, text)\n end\n end\n\n local body = cleanMsg:match(\"<Sexual Trait>%s*([%s%S]-)%s*</Sexual Trait>\")\n if not body or trim(body) == \"\" then\n return cleanMsg, persistentLogs\n end\n\n local fields = parseBracketFields(body)\n\n local traitName = trim(fields[\"Trait Name\"] or \"\")\n local description = trim(fields[\"Description\"] or \"\")\n\n if traitName ~= \"\" and description ~= \"\" then\n local oldContent = trim(safeGet(triggerId, \"vg_SexualTrait_Content\", \"\"))\n\n -- ==========================================\n -- 이미 같은 이름의 성적 특성이 있으면 스킵\n -- ==========================================\n local alreadyOwned = false\n\n for _, entry in ipairs(splitPipe(oldContent)) do\n local ownedName = trim((tostring(entry or \"\"):match(\"^(.-)%s*:%s*\") or entry or \"\"))\n\n if string.lower(ownedName) == string.lower(traitName) then\n alreadyOwned = true\n break\n end\n end\n\n if not alreadyOwned then\n local newEntry = traitName .. \": \" .. description\n local newContent = appendPipe(oldContent, newEntry)\n\n setChatVar(triggerId, \"vg_SexualTrait_Content\", newContent)\n\n local count = #splitPipe(newContent)\n setDualFlag(triggerId, vgContext.varName, vgContext.sexualTrait_Num, tostring(count), false)\n\n addPersistentLog(\"성적 특성 획득: \" .. traitName)\n\n currentDateText = trim(tostring(currentDateText or \"\"))\n currentDateText = trim(currentDateText:match(\"^(%d+%-%d%d?%-%d%d?)\") or currentDateText)\n\n if currentDateText == \"\" then\n currentDateText = trim((select(1, getLatestCharDateTime(triggerId))) or \"\")\n end\n\n if currentDateText ~= \"\" then\n appendMemoryEntry(triggerId, currentDateText, \"Sexual Trait Acquired\", traitName)\n buildMemoryDisplay(triggerId)\n end\n end\n end\n\n cleanMsg = cleanMsg:gsub(\"<Sexual Trait>%s*[%s%S]-%s*</Sexual Trait>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, persistentLogs\nend\n\nlocal function parseCorruptionTraitRecordBlock(triggerId, cleanMsg, currentDateText)\n local persistentLogs = {}\n\n local function addPersistentLog(text)\n text = trim(tostring(text or \"\"))\n if text ~= \"\" then\n table.insert(persistentLogs, text)\n end\n end\n\n local body = cleanMsg:match(\"<Corruption Trait>%s*([%s%S]-)%s*</Corruption Trait>\")\n if not body or trim(body) == \"\" then\n return cleanMsg, persistentLogs\n end\n\n local fields = parseBracketFields(body)\n\n -- Development는 파싱하지 않음\n local traitName = trim(fields[\"Trait Name\"] or \"\")\n local description = trim(fields[\"Description\"] or \"\")\n\n if traitName ~= \"\" and description ~= \"\" then\n local oldContent = trim(safeGet(triggerId, \"vg_Corruption_Content\", \"\"))\n local sexualTraitContent = trim(safeGet(triggerId, \"vg_SexualTrait_Content\", \"\"))\n\n -- ==========================================\n -- 이미 같은 이름의 특성이 있으면 스킵\n -- Corruption Trait / Sexual Trait 둘 다 확인\n -- ==========================================\n local alreadyOwned = false\n\n for _, entry in ipairs(splitPipe(oldContent)) do\n local ownedName = trim((tostring(entry or \"\"):match(\"^(.-)%s*:%s*\") or entry or \"\"))\n\n if string.lower(ownedName) == string.lower(traitName) then\n alreadyOwned = true\n break\n end\n end\n\n if not alreadyOwned then\n for _, entry in ipairs(splitPipe(sexualTraitContent)) do\n local ownedName = trim((tostring(entry or \"\"):match(\"^(.-)%s*:%s*\") or entry or \"\"))\n\n if string.lower(ownedName) == string.lower(traitName) then\n alreadyOwned = true\n break\n end\n end\n end\n\n if not alreadyOwned then\n local newEntry = traitName .. \": \" .. description\n local newContent = appendPipe(oldContent, newEntry)\n\n setChatVar(triggerId, \"vg_Corruption_Content\", newContent)\n\n local count = #splitPipe(newContent)\n setDualFlag(triggerId, vgContext.varName, vgContext.corruptionTrait_Num, tostring(count), false)\n\n addPersistentLog(\"타락 특성 획득: \" .. traitName)\n\n currentDateText = trim(tostring(currentDateText or \"\"))\n currentDateText = trim(currentDateText:match(\"^(%d+%-%d%d?%-%d%d?)\") or currentDateText)\n\n if currentDateText == \"\" then\n currentDateText = trim((select(1, getLatestCharDateTime(triggerId))) or \"\")\n end\n\n if currentDateText ~= \"\" then\n appendMemoryEntry(triggerId, currentDateText, \"Corruption Trait Acquired\", traitName)\n buildMemoryDisplay(triggerId)\n end\n end\n end\n\n cleanMsg = cleanMsg:gsub(\"<Corruption Trait>%s*[%s%S]-%s*</Corruption Trait>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, persistentLogs\nend\n\n-- ==========================================\n-- 파싱 개발도\n-- ==========================================\nlocal function parseDevelopmentBlocks(triggerId, cleanMsg, prevPhase, currentDateText)\n local persistentResultLogs = {}\n\n local function addPersistentLog(text)\n text = trim(tostring(text or \"\"))\n if text ~= \"\" then\n table.insert(persistentResultLogs, text)\n end\n end\n\n local erogenousContent = cleanMsg:match(\"<Session Erogenous Development>%s*([%s%S]-)%s*</Session Erogenous Development>\")\n\n -- ==========================================\n -- Session Erogenous Development 파싱 / Sensitivity 누적\n -- ==========================================\n do\n local levelMap = {\n [\"None\"] = 0,\n [\"Trace\"] = 1,\n [\"Subtle\"] = 2,\n [\"Very Light\"] = 3,\n [\"Light\"] = 4,\n [\"Slight\"] = 5,\n [\"Mild\"] = 6,\n [\"Moderate\"] = 7,\n [\"Noticeable\"] = 8,\n [\"Pronounced\"] = 9,\n [\"Heavy\"] = 10,\n [\"Strong\"] = 11,\n [\"Intense\"] = 12,\n [\"Overwhelming\"] = 13,\n [\"Saturated\"] = 14\n }\n\n local partDefs = {\n { field = \"Breast\", key = \"breast\", position = vgSensitivity.breast, label = \"가슴\" },\n { field = \"Oral\", key = \"oral\", position = vgSensitivity.oral, label = \"구강\" },\n { field = \"Clitoris\", key = \"clitoris\", position = vgSensitivity.clitoris, label = \"클리토리스\" },\n { field = \"Vaginal\", key = \"vaginal\", position = vgSensitivity.vaginal, label = \"질\" },\n { field = \"Anal\", key = \"anal\", position = vgSensitivity.anal, label = \"항문\" },\n }\n\n local sensitivityCap = tonumber((sensitivityRanges[#sensitivityRanges] or {}).min) or 200\n\n local function getSensitivityLevelText(level, isMaxLevel)\n if isMaxLevel then\n return \"MAX\"\n end\n\n return tostring(tonumber(level) or 1)\n end\n\n local function getSensitivityProgressInfo(value)\n value = tonumber(value) or 0\n value = clamp(value, 0, sensitivityCap)\n\n local level = getSensitivityLevelByValue(value)\n local curMin = tonumber((sensitivityRanges[level] or {}).min) or 0\n local nextMin = tonumber((sensitivityRanges[level + 1] or {}).min)\n\n if not nextMin then\n return level, math.max(0, value - curMin), 0, true\n end\n\n local currentExp = math.max(0, value - curMin)\n local needExp = math.max(1, nextMin - curMin)\n\n return level, currentExp, needExp, false\n end\n\n local erogenousFields = parseBracketFields(erogenousContent or \"\")\n local sensitivityMap = loadDualFlagCurrentMap(triggerId, vgSensitivity)\n local updates = {}\n\n local totalDevelopmentGain = 0\n\n for _, info in ipairs(partDefs) do\n local valueText = trim(erogenousFields[info.field] or \"\")\n local gain = levelMap[valueText] or 0\n\n if gain > 0 then\n totalDevelopmentGain = totalDevelopmentGain + gain\n end\n\n if gain > 0 then\n local prev = tonumber(sensitivityMap[info.key]) or 0\n local after = math.min(prev + gain, sensitivityCap)\n local gained = after - prev\n\n local prevLevel = getSensitivityLevelByValue(prev)\n local afterLevel = getSensitivityLevelByValue(after)\n\n table.insert(updates, {\n position = info.position,\n value = tostring(after),\n isNext = false\n })\n\n if prev >= sensitivityCap then\n addPersistentLog(info.label .. \" Lv.MAX: 이미 최대치입니다.\")\n elseif gained > 0 then\n local finalLevel, currentExp, needExp, isMaxLevel = getSensitivityProgressInfo(after)\n local finalLevelText = getSensitivityLevelText(finalLevel, isMaxLevel)\n\n local logText = \"\"\n\n if isMaxLevel then\n if afterLevel > prevLevel then\n logText = info.label .. \" LEVEL UP! Lv.\" .. finalLevelText\n .. \": +\" .. tostring(gained) .. \" EXP (MAX)\"\n else\n logText = info.label .. \" Lv.\" .. finalLevelText\n .. \": +\" .. tostring(gained) .. \" EXP (MAX)\"\n end\n else\n if afterLevel > prevLevel then\n logText = info.label .. \" LEVEL UP! Lv.\" .. finalLevelText\n .. \": +\" .. tostring(gained) .. \" EXP (\"\n .. tostring(currentExp) .. \" / \" .. tostring(needExp) .. \")\"\n else\n logText = info.label .. \" Lv.\" .. finalLevelText\n .. \": +\" .. tostring(gained) .. \" EXP (\"\n .. tostring(currentExp) .. \" / \" .. tostring(needExp) .. \")\"\n end\n end\n\n addPersistentLog(logText)\n end\n end\n end\n\n if #updates > 0 then\n batchSetDualFlags(triggerId, vgSensitivity.varName, updates)\n end\n\n local currentLewdness = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.lewdness, 0)) or 0\n local currentLewdnessLevel = getLewdnessLevelByValue(currentLewdness)\n\n local lewdGainRate = 0.55 - (currentLewdnessLevel * 0.02)\n\n if isPromiscuityPhase(triggerId, prevPhase) then\n lewdGainRate = lewdGainRate + 0.4\n elseif prevPhase == constPhase.transaction then\n lewdGainRate = lewdGainRate + 0.3\n elseif isPerformancePhase(triggerId, prevPhase) or isExhibitionismPhase(triggerId, prevPhase) or isIntimacyPhase(triggerId, prevPhase) then\n lewdGainRate = lewdGainRate + 0.25\n elseif prevPhase == constPhase.event_Yielding or prevPhase == constPhase.event_Yielding_Hypnosis then\n lewdGainRate = lewdGainRate + 0.2 + (currentLewdnessLevel * 0.01)\n elseif isMasturbationPhase(triggerId, prevPhase) then\n lewdGainRate = lewdGainRate + 0.1 + (currentLewdnessLevel * 0.01)\n elseif prevPhase == constPhase.event_Defiant or prevPhase == constPhase.event_Defiant_Hypnosis or prevPhase == constPhase.rape then\n local defiantPenalty = 0.1 + math.max(0, 11 - currentLewdnessLevel) * 0.01\n lewdGainRate = lewdGainRate - defiantPenalty\n end\n\n lewdGainRate = math.max(0.1, lewdGainRate)\n local lewdGain = math.floor(totalDevelopmentGain * lewdGainRate)\n \n if lewdGain > 0 then\n local _, _, _, wasMaxLevel = getLewdnessProgressInfo(currentLewdness)\n\n if wasMaxLevel then\n local corruptionCap = getCorruptionTotalExpForLevel(CORRUPTION_MAX_LEVEL)\n\n local currentCorruption = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.corruption, 0)) or 0\n local nextCorruption = math.min(currentCorruption + lewdGain, corruptionCap)\n local corruptionGain = math.max(0, nextCorruption - currentCorruption)\n\n local prevCorruptionLevel = getCorruptionLevelByValue(currentCorruption)\n local nextCorruptionLevel, corruptionExp, corruptionNeedExp, isCorruptionMax = getCorruptionProgressInfo(nextCorruption)\n\n setDualFlag(triggerId, vgContext.varName, vgContext.corruption, tostring(nextCorruption), false)\n\n if prevCorruptionLevel >= CORRUPTION_MAX_LEVEL then\n addPersistentLog(\"타락 Lv.MAX\")\n elseif nextCorruptionLevel >= CORRUPTION_MAX_LEVEL then\n addPersistentLog(\"타락 LEVEL UP! Lv.MAX: +\" .. tostring(corruptionGain) .. \" EXP (MAX)\")\n elseif nextCorruptionLevel > prevCorruptionLevel then\n addPersistentLog(\"타락 LEVEL UP! Lv.\" .. tostring(nextCorruptionLevel) .. \": +\" .. tostring(corruptionGain) .. \" EXP (\" .. tostring(corruptionExp) .. \" / \" .. tostring(corruptionNeedExp) .. \")\")\n else\n addPersistentLog(\"타락 Lv.\" .. tostring(nextCorruptionLevel) .. \": +\" .. tostring(corruptionGain) .. \" EXP (\" .. tostring(corruptionExp) .. \" / \" .. tostring(corruptionNeedExp) .. \")\")\n end\n\n else\n local nextLewdness = currentLewdness + lewdGain\n\n local prevLevel = getLewdnessLevelByValue(currentLewdness)\n local nextLevel = getLewdnessLevelByValue(nextLewdness)\n\n local finalLevel, currentExp, needExp, isMaxLevel = getLewdnessProgressInfo(nextLewdness)\n\n setDualFlag(triggerId, vgContext.varName, vgContext.lewdness, tostring(nextLewdness), false)\n\n do\n local prevLewdnessText = getLewdnessTextByLevel(prevLevel)\n local nextLewdnessText = getLewdnessTextByLevel(nextLevel)\n\n currentDateText = trim(tostring(currentDateText or \"\"))\n currentDateText = trim(currentDateText:match(\"^(%d+%-%d%d?%-%d%d?)\") or currentDateText)\n\n if prevLewdnessText ~= nextLewdnessText and currentDateText ~= \"\" then\n appendMemoryEntry(triggerId, currentDateText, \"Lewdness Deepened\", nextLewdnessText)\n buildMemoryDisplay(triggerId)\n end\n end\n\n if isMaxLevel then\n if nextLevel > prevLevel then\n addPersistentLog(\"음란도 LEVEL UP! Lv.MAX: +\" .. tostring(lewdGain) .. \" EXP (MAX)\")\n else\n addPersistentLog(\"음란도 Lv.MAX: +\" .. tostring(lewdGain) .. \" EXP (MAX)\")\n end\n else\n if nextLevel > prevLevel then\n addPersistentLog(\"음란도 LEVEL UP! Lv.\" .. tostring(finalLevel) .. \": +\" .. tostring(lewdGain) .. \" EXP (\" .. tostring(currentExp) .. \" / \" .. tostring(needExp) .. \")\")\n else\n addPersistentLog(\"음란도 Lv.\" .. tostring(finalLevel) .. \": +\" .. tostring(lewdGain) .. \" EXP (\" .. tostring(currentExp) .. \" / \" .. tostring(needExp) .. \")\")\n end\n end\n end\n end\n end\n\n cleanMsg = cleanMsg:gsub(\"<Session Erogenous Development>%s*[%s%S]-%s*</Session Erogenous Development>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, persistentResultLogs\nend\n\n-- ==========================================\n-- 파싱 첫경험\n-- ==========================================\nlocal function parseFirstExperienceRecordBlocks(triggerId, cleanMsg, changedVars)\n local persistentLogs = {}\n local memoryAdded = false\n\n local function addPersistentLog(text)\n text = trim(tostring(text or \"\"))\n if text ~= \"\" then\n table.insert(persistentLogs, text)\n end\n end\n\n local function trySaveRecord(tagName, saveVarKey, milestoneName, logLabel)\n local body = cleanMsg:match(\"<\" .. tagName .. \">%s*([%s%S]-)%s*</\" .. tagName .. \">\")\n if not body or trim(body) == \"\" then\n return\n end\n\n local currentValue = trim(safeGet(triggerId, saveVarKey, \"\"))\n if currentValue ~= \"\" then\n return\n end\n\n local recordRaw = trim(body)\n\n local bracketEntry = recordRaw:match(\"%b[]\")\n if bracketEntry then\n recordRaw = trim(bracketEntry:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\"))\n end\n\n local fields = parseOrderedFieldsWithFallback(recordRaw, {\n \"Date\",\n \"Source\",\n \"Lead-up\",\n })\n\n local dateText = normalizeLooseDateText(fields[\"date\"] or \"\")\n local sourceText = normalizeIndexedSourceName(fields[\"source\"] or \"\")\n local leadUpText = trim(fields[\"lead-up\"] or \"\")\n\n if not parseISODateText(dateText) then\n return\n end\n\n if not isMeaningfulValue(sourceText) then\n return\n end\n\n if not isMeaningfulValue(leadUpText) then\n return\n end\n\n local savedText = dateText .. \", \" .. sourceText\n\n setTrackedChatVar(triggerId, changedVars, saveVarKey, savedText)\n addPersistentLog(logLabel .. \": \" .. savedText)\n\n appendMemoryEntry(triggerId, dateText, milestoneName, leadUpText, changedVars)\n\n memoryAdded = true\n end\n\n trySaveRecord(\"Record First Kiss\", \"vg_FirstKiss\", \"First Kiss\", \"첫 키스\")\n trySaveRecord(\"Record First Masturbation\", \"vg_FirstMasturbation\", \"First Masturbation\", \"첫 자위\")\n trySaveRecord(\"Record First Orgasm\", \"vg_FirstOrgasm\", \"First Orgasm\", \"첫 오르가즘\")\n trySaveRecord(\"Record First Fellatio\", \"vg_FirstFellatio\", \"First Fellatio\", \"첫 펠라치오\")\n trySaveRecord(\"Record First Vaginal Penetration\", \"vg_FirstVirgin\", \"Virgin Loss\", \"처녀 상실\")\n trySaveRecord(\"Record First Anal Penetration\", \"vg_FirstAnal\", \"First Anal Sex\", \"첫 항문 삽입\")\n\n if memoryAdded then\n buildMemoryDisplay(triggerId)\n end\n\n cleanMsg = cleanMsg:gsub(\"<Record First Kiss>%s*[%s%S]-%s*</Record First Kiss>\", \"\")\n cleanMsg = cleanMsg:gsub(\"<Record First Masturbation>%s*[%s%S]-%s*</Record First Masturbation>\", \"\")\n cleanMsg = cleanMsg:gsub(\"<Record First Orgasm>%s*[%s%S]-%s*</Record First Orgasm>\", \"\")\n cleanMsg = cleanMsg:gsub(\"<Record First Fellatio>%s*[%s%S]-%s*</Record First Fellatio>\", \"\")\n cleanMsg = cleanMsg:gsub(\"<Record First Vaginal Penetration>%s*[%s%S]-%s*</Record First Vaginal Penetration>\", \"\")\n cleanMsg = cleanMsg:gsub(\"<Record First Anal Penetration>%s*[%s%S]-%s*</Record First Anal Penetration>\", \"\")\n\n cleanMsg = cleanMsg:gsub(\"<Record First[^>]*>%s*[%s%S]-%s*</Record First[^>]*>\", \"\")\n\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, persistentLogs\nend\n\n-- ==========================================\n-- 파싱 요약\n-- ==========================================\nlocal function parseStoryMemoryRecordBlock(triggerId, cleanMsg)\n local parsedResolution = constResolution.none\n local outcomeResult = \"\"\n local nextStoryGoal = \"\"\n\n local recordDefs = {\n {\n tagName = \"Storyline Resolution\",\n resolution = constResolution.storyline,\n },\n {\n tagName = \"Capture Record\",\n resolution = constResolution.capture,\n },\n {\n tagName = \"Freedom Record\",\n resolution = constResolution.freedom,\n },\n {\n tagName = \"Encounter Record\",\n resolution = constResolution.addRecord,\n },\n {\n tagName = \"Masturbation Record\",\n resolution = constResolution.addMasturbationRecord,\n },\n {\n tagName = \"Exposure Event Record\",\n resolution = constResolution.addExhibitionismRecord,\n },\n }\n\n for _, def in ipairs(recordDefs) do\n local tagName = def.tagName\n local resolution = def.resolution\n\n local body = cleanMsg:match(\"<\" .. tagName .. \">%s*([%s%S]-)%s*</\" .. tagName .. \">\")\n\n if body and trim(body) ~= \"\" then\n parsedResolution = resolution\n\n local recordRaw = trim(body)\n\n local bracketEntry = recordRaw:match(\"%b[]\")\n if bracketEntry then\n recordRaw = trim(bracketEntry:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\"))\n end\n\n local orderedFields = {\"Date\", \"Outcome\", \"Summary\"}\n\n if resolution == constResolution.storyline then\n table.insert(orderedFields, \"Next Storyline Goal\")\n end\n\n local fields = parseOrderedFieldsWithFallback(recordRaw, orderedFields)\n\n local dateText = trim(fields[\"date\"] or \"\")\n local outcomeText = trim(fields[\"outcome\"] or \"\")\n local summaryText = trim(fields[\"summary\"] or \"\")\n\n if resolution == constResolution.storyline then\n nextStoryGoal = trim(fields[\"next storyline goal\"] or \"\")\n end\n\n local shouldStore = isMeaningfulValue(summaryText)\n\n if shouldStore then\n outcomeResult = outcomeText\n\n local memoryTitle = outcomeText\n if resolution == constResolution.addMasturbationRecord or resolution == constResolution.addExhibitionismRecord then\n memoryTitle = \"\"\n end\n\n local memoryContent = summaryText\n\n local duplicate = false\n for _, entry in ipairs(splitPipe(safeGet(triggerId, \"vg_Memory_Content\", \"\"))) do\n local parts = splitSection(entry)\n local oldDate = trim(parts[1] or \"\")\n local oldTitle = trim(parts[2] or \"\")\n local oldContent = trim(parts[3] or \"\")\n\n if oldDate == dateText\n and oldTitle == memoryTitle\n and oldContent == memoryContent then\n duplicate = true\n break\n end\n end\n\n if not duplicate then\n appendStoryEntry(triggerId, dateText, memoryTitle, memoryContent)\n buildMemoryDisplay(triggerId)\n end\n end\n\n cleanMsg = cleanMsg:gsub(\n \"<\" .. tagName .. \">%s*[%s%S]-%s*</\" .. tagName .. \">\",\n \"\"\n )\n\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, parsedResolution, outcomeResult, nextStoryGoal\n end\n end\n\n return cleanMsg, parsedResolution, outcomeResult, nextStoryGoal\nend\n\nlocal function parseSexualActRecordBlock(triggerId, cleanMsg)\n local body = cleanMsg:match(\"<Session Sexual Act Record>%s*([%s%S]-)%s*</Session Sexual Act Record>\")\n\n if not body or trim(body) == \"\" then\n return cleanMsg\n end\n\n local recordRaw = trim(body)\n\n local bracketEntry = recordRaw:match(\"%b[]\")\n if bracketEntry then\n recordRaw = trim(bracketEntry:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\"))\n end\n\n local fields = parseOrderedFieldsWithFallback(recordRaw, {\"Date\", \"Summary\"})\n\n local dateText = trim(fields[\"date\"] or \"\")\n local summaryText = trim(fields[\"summary\"] or \"\")\n\n local shouldStore = isMeaningfulValue(summaryText)\n\n if shouldStore then\n local memoryTitle = \"\"\n local memoryContent = summaryText\n\n local duplicate = false\n for _, entry in ipairs(splitPipe(safeGet(triggerId, \"vg_Memory_Content\", \"\"))) do\n local parts = splitSection(entry)\n local oldDate = trim(parts[1] or \"\")\n local oldTitle = trim(parts[2] or \"\")\n local oldContent = trim(parts[3] or \"\")\n\n if oldDate == dateText\n and oldTitle == memoryTitle\n and oldContent == memoryContent then\n duplicate = true\n break\n end\n end\n\n if not duplicate then\n appendStoryEntry(triggerId, dateText, memoryTitle, memoryContent)\n buildMemoryDisplay(triggerId)\n end\n end\n\n cleanMsg = cleanMsg:gsub(\"<Session Sexual Act Record>%s*[%s%S]-%s*</Session Sexual Act Record>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg\nend\n\nlocal function parseStoryGoalRecordBlock(triggerId, cleanMsg)\n local tempLogs = {}\n\n local function addTempLog(msg)\n if msg and msg ~= \"\" then\n table.insert(tempLogs, msg)\n end\n end\n\n local function parseGoalBlock(tagName, isUpdate)\n local pattern = \"<\" .. tagName .. \">%s*([%s%S]-)%s*</\" .. tagName .. \">\"\n local body = cleanMsg:match(pattern)\n\n if not body or trim(body) == \"\" then\n return false\n end\n\n local recordRaw = trim(body)\n\n local bracketEntry = recordRaw:match(\"%b[]\")\n if bracketEntry then\n recordRaw = trim(bracketEntry:gsub(\"^%[\", \"\"):gsub(\"%]$\", \"\"))\n end\n\n local fields = parseOrderedFieldsWithFallback(recordRaw, {\n \"Current Storyline Goal\",\n \"Ultimate Goal\",\n })\n\n local shortGoal = trim(fields[\"current storyline goal\"] or \"\")\n local longGoal = trim(fields[\"ultimate goal\"] or \"\")\n\n local changed = false\n\n if isMeaningfulValue(shortGoal) then\n setChatVar(triggerId, \"vg_Goal_Short\", shortGoal)\n changed = true\n\n if isUpdate then\n addTempLog(\"현재 목표 재설정: \" .. shortGoal)\n end\n end\n\n -- Story Goal Update에서는 Ultimate Goal을 절대 수정하지 않음\n if not isUpdate and isMeaningfulValue(longGoal) then\n setChatVar(triggerId, \"vg_Goal_Long\", longGoal)\n changed = true\n end\n\n if isUpdate then\n setChatVar(triggerId, \"vg_ChangeGoal\", \"0\")\n else\n setChatVar(triggerId, \"vg_Resolution_Flag\", tostring(constResolution.none))\n end\n\n if changed then\n generateHTML(triggerId)\n end\n\n cleanMsg = cleanMsg:gsub(\n \"<\" .. tagName .. \">%s*[%s%S]-%s*</\" .. tagName .. \">\",\n \"\",\n 1\n )\n\n return true\n end\n\n parseGoalBlock(\"Story Goal Record\", false)\n parseGoalBlock(\"Story Goal Update\", true)\n\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, tempLogs\nend\n\n-- ==========================================\n-- 파싱 강화\n-- ==========================================\nlocal function parseEnhancementBlock(triggerId, cleanMsg)\n local raw = cleanMsg:match(\"<Obtainable Enhancement>%s*([%s%S]-)%s*</Obtainable Enhancement>\")\n if not raw or trim(raw) == \"\" then\n return cleanMsg, \"\"\n end\n\n local fields = parseBracketFields(raw)\n\n local enhancementName = trim(fields[\"Enhancement Name\"] or \"\")\n local increaseName1 = trim(fields[\"Increase1\"] or \"\")\n local increaseName2 = trim(fields[\"Increase2\"] or \"\")\n local enhancementType = trim(fields[\"Type\"] or \"\")\n\n local function resolveEnhancementTypeCodeByName(nameText)\n nameText = string.lower(trim(tostring(nameText or \"\")))\n\n if nameText == \"\" then\n return 0\n end\n\n for _, def in ipairs(enhancementTypeDefs or {}) do\n local defName = string.lower(trim(def.name or \"\"))\n\n if defName ~= \"\" and defName == nameText then\n return tonumber(def.code) or 0\n end\n end\n\n return 0\n end\n\n local increaseCode1 = resolveEnhancementTypeCodeByName(increaseName1)\n local increaseCode2 = resolveEnhancementTypeCodeByName(increaseName2)\n\n cleanMsg = cleanMsg:gsub(\"<Obtainable Enhancement>%s*[%s%S]-%s*</Obtainable Enhancement>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n if enhancementName == \"\" or enhancementType == \"\" or increaseCode1 == 0 or increaseCode2 == 0 then\n return cleanMsg, \"\"\n end\n\n local sessionSlotList = getDualFlagSlotList(triggerId, vgSessionInfo.varName)\n\n local encounterLevel = tonumber(getResolvedDualFlagFromSlotList(sessionSlotList, vgSessionInfo.level, 1)) or 1\n local difficultyCode = tonumber(getResolvedDualFlagFromSlotList(sessionSlotList, vgSessionInfo.difficulty, constSessionDifficulty.low)) or constSessionDifficulty.low\n\n local function rollEnhancementValue(effectTypeCode)\n effectTypeCode = tonumber(effectTypeCode) or 0\n\n local function rollCurveBonus(maxValue, curve)\n maxValue = tonumber(maxValue) or 0\n curve = tonumber(curve) or 1.9\n\n if maxValue <= 0 then\n return 0\n end\n\n local roll = math.random(0, 1000) / 1000\n return (roll ^ curve) * maxValue\n end\n\n local difficultyCurveMap = {\n [constSessionDifficulty.low] = 2.4,\n [constSessionDifficulty.medium] = 1.9,\n [constSessionDifficulty.high] = 1.5,\n [constSessionDifficulty.elite] = 1.15,\n [constSessionDifficulty.boss] = 0.85,\n }\n\n local difficultyCurve = difficultyCurveMap[difficultyCode] or 1.9\n local levelCurve = 2.2\n\n local maxEncounterLevel = getStoryChoiceBaseLevelByAct(10)\n local effectiveEncounterLevel = encounterLevel\n\n if encounterLevel > maxEncounterLevel then\n effectiveEncounterLevel = maxEncounterLevel + ((encounterLevel - maxEncounterLevel) / 5)\n end\n\n local levelRatio = math.max(0, (effectiveEncounterLevel - 1) / (maxEncounterLevel - 1))\n\n local baseMax = 0.2 + (9.8 * levelRatio)\n local difficultyBonusMax = 2.5\n local levelBonusMax = 2.5 * levelRatio\n\n if effectTypeCode == constEnhancementType.focus then\n baseMax = 0.2 + (4.8 * levelRatio)\n difficultyBonusMax = 1.0\n levelBonusMax = 1.5 * levelRatio\n\n elseif effectTypeCode == constEnhancementType.resilience then\n baseMax = 0.2 + (9.8 * levelRatio)\n difficultyBonusMax = 5.0\n levelBonusMax = 10.0 * levelRatio\n\n elseif effectTypeCode == constEnhancementType.discipline then\n baseMax = 4 + (8.5 * levelRatio)\n difficultyBonusMax = 8.5\n levelBonusMax = 4 * levelRatio\n\n elseif effectTypeCode == constEnhancementType.luck then\n baseMax = 1 + (4 * levelRatio)\n difficultyBonusMax = 7.5\n levelBonusMax = 7.5 * levelRatio\n\n elseif effectTypeCode == constEnhancementType.endurance then\n baseMax = 1 + (2 * levelRatio)\n difficultyBonusMax = 7\n levelBonusMax = 5 * levelRatio\n end\n\n local baseMin = baseMax * 0.2\n\n local baseRoll = math.random(0, 1000) / 1000\n local baseValue = baseMin + ((baseMax - baseMin) * baseRoll)\n\n local difficultyBonusValue = rollCurveBonus(difficultyBonusMax, difficultyCurve)\n local levelBonusValue = rollCurveBonus(levelBonusMax, levelCurve)\n\n local finalValue = baseValue + difficultyBonusValue + levelBonusValue\n\n return math.max(0.1, roundToTenth(finalValue))\n end\n\n local function buildEnhancementEffectRawText(effectTypeCode, effectValue)\n effectTypeCode = tonumber(effectTypeCode) or 0\n effectValue = roundToTenth(tonumber(effectValue) or 0)\n\n if effectTypeCode == 0 then\n return \"\"\n end\n\n return tostring(effectTypeCode) .. \":\" .. formatFloat(effectValue)\n end\n\n local increaseValue1 = rollEnhancementValue(increaseCode1)\n local increaseValue2 = rollEnhancementValue(increaseCode2)\n\n local summaryText = buildEnhancementEffectText(increaseCode1, increaseValue1) .. \" \" .. buildEnhancementEffectText(increaseCode2, increaseValue2)\n\n local effectRawText1 = buildEnhancementEffectRawText(increaseCode1, increaseValue1)\n local effectRawText2 = buildEnhancementEffectRawText(increaseCode2, increaseValue2)\n\n if summaryText == \"\" or effectRawText1 == \"\" or effectRawText2 == \"\" then\n return cleanMsg, \"\"\n end\n\n local function buildEnhancementChoiceRow(slotValue)\n local currentName = \"\"\n local currentEffectRaw1 = \"\"\n local currentEffectRaw2 = \"\"\n\n if slotValue == 1 then\n currentName = trim(getResolvedDualFlag(triggerId, vgEnhancement.varName, vgEnhancement.slot1_Name, \"\"))\n currentEffectRaw1 = trim(getResolvedDualFlag(triggerId, vgEnhancement.varName, vgEnhancement.slot1_Effect1, \"\"))\n currentEffectRaw2 = trim(getResolvedDualFlag(triggerId, vgEnhancement.varName, vgEnhancement.slot1_Effect2, \"\"))\n elseif slotValue == 2 then\n currentName = trim(getResolvedDualFlag(triggerId, vgEnhancement.varName, vgEnhancement.slot2_Name, \"\"))\n currentEffectRaw1 = trim(getResolvedDualFlag(triggerId, vgEnhancement.varName, vgEnhancement.slot2_Effect1, \"\"))\n currentEffectRaw2 = trim(getResolvedDualFlag(triggerId, vgEnhancement.varName, vgEnhancement.slot2_Effect2, \"\"))\n elseif slotValue == 3 then\n currentName = trim(getResolvedDualFlag(triggerId, vgEnhancement.varName, vgEnhancement.slot3_Name, \"\"))\n currentEffectRaw1 = trim(getResolvedDualFlag(triggerId, vgEnhancement.varName, vgEnhancement.slot3_Effect1, \"\"))\n currentEffectRaw2 = trim(getResolvedDualFlag(triggerId, vgEnhancement.varName, vgEnhancement.slot3_Effect2, \"\"))\n end\n\n local currentSummaryParts = {}\n\n do\n local codeStr, valueStr = currentEffectRaw1:match(\"^([%-+]?%d+)%s*:%s*([%-+]?[%d%.]+)$\")\n local code = tonumber(codeStr) or 0\n local value = tonumber(valueStr) or 0\n local text = buildEnhancementEffectText(code, value)\n\n if text ~= \"\" then\n table.insert(currentSummaryParts, text)\n end\n end\n\n do\n local codeStr, valueStr = currentEffectRaw2:match(\"^([%-+]?%d+)%s*:%s*([%-+]?[%d%.]+)$\")\n local code = tonumber(codeStr) or 0\n local value = tonumber(valueStr) or 0\n local text = buildEnhancementEffectText(code, value)\n\n if text ~= \"\" then\n table.insert(currentSummaryParts, text)\n end\n end\n\n local currentSummary = table.concat(currentSummaryParts, \" \")\n\n local slotEmoji = \"1️⃣ \"\n if slotValue == 2 then\n slotEmoji = \"2️⃣ \"\n elseif slotValue == 3 then\n slotEmoji = \"3️⃣ \"\n end\n\n local obtainableText = \"\"\n local displaySummary = \"\"\n\n if currentName == \"\" then\n obtainableText = slotEmoji .. enhancementName\n displaySummary = summaryText\n else\n obtainableText = slotEmoji .. currentName .. \" → \" .. enhancementName\n\n if currentSummary ~= \"\" then\n displaySummary = currentSummary .. \" → \" .. summaryText\n else\n displaySummary = summaryText\n end\n end\n\n return \"[Obtainable Enhancement: \" .. obtainableText ..\n \" | Name: \" .. enhancementName ..\n \" | Type: \" .. enhancementType ..\n \" | Summary: \" .. displaySummary ..\n \" | Effect1: \" .. effectRawText1 ..\n \" | Effect2: \" .. effectRawText2 ..\n \" | Slot: \" .. tostring(slotValue) .. \"]\"\n end\n\n local enhancementChoiceText = table.concat({\n \"<Enhancement Choice>\",\n buildEnhancementChoiceRow(1),\n buildEnhancementChoiceRow(2),\n buildEnhancementChoiceRow(3),\n \"</Enhancement Choice>\"\n }, \"\\n\")\n\n return cleanMsg, enhancementChoiceText\nend\n\n-- ==========================================\n-- 임시 카드\n-- ==========================================\nlocal function addTempCards(triggerId, changedVars, addTempLog)\n local tempDeckStr = safeGet(triggerId, \"vg_Deck_Temp\", \"\")\n local remainArray = splitPipe(safeGet(triggerId, \"vg_PlayDeck_Remain\", \"\"))\n\n local minTempCode = -11\n\n local curPhase = getPhase(triggerId)\n\n for _, tempCard in ipairs(splitPipe(tempDeckStr)) do\n local parts = splitSection(tempCard)\n local code = tonumber(parts[1]) or 0\n\n if code <= minTempCode then\n minTempCode = code - 1\n end\n end\n\n local hasChanged = false\n\n local function getBaseDeckMinMaxLevel()\n local minLevel = nil\n local maxLevel = nil\n\n for _, baseCard in ipairs(splitPipe(safeGet(triggerId, \"vg_Deck_Base\", \"\"))) do\n local parts = splitSection(baseCard)\n local level = clamp(tonumber(parts[3]) or 1, 1, 10)\n\n if not minLevel or level < minLevel then\n minLevel = level\n end\n\n if not maxLevel or level > maxLevel then\n maxLevel = level\n end\n end\n\n return minLevel or 1, maxLevel or 1\n end\n\n local function addOneTempCard(cardType, cardLevel, extraFields, logText)\n cardType = tonumber(cardType) or 0\n cardLevel = clamp(tonumber(cardLevel) or 1, 1, 10)\n extraFields = extraFields or {}\n\n if cardType <= 0 then\n return\n end\n\n local newCode = minTempCode\n minTempCode = minTempCode - 1\n\n local parts = {\n tostring(newCode),\n tostring(cardType),\n tostring(cardLevel)\n }\n\n local keys = {}\n for key, value in pairs(extraFields) do\n if value ~= nil and value ~= \"\" then\n table.insert(keys, key)\n end\n end\n\n table.sort(keys)\n\n for _, key in ipairs(keys) do\n table.insert(parts, key .. \"=\" .. tostring(extraFields[key]))\n end\n\n if extraFields.temp == nil then\n table.insert(parts, \"temp=1\")\n end\n\n local newCard = table.concat(parts, \"§\")\n tempDeckStr = appendPipe(tempDeckStr, newCard)\n\n local insertIndex = #remainArray + 1\n if #remainArray > 0 then\n insertIndex = math.random(1, #remainArray + 1)\n end\n\n table.insert(remainArray, insertIndex, tostring(newCode))\n\n hasChanged = true\n\n if addTempLog and logText and logText ~= \"\" then\n addTempLog(logText)\n end\n end\n\n -- ==========================================\n -- 조건 1) 절정 횟수 증가분만큼 Ecstasy 카드 추가\n -- ==========================================\n do\n if isIncapacitatedPhase(triggerId, curPhase) == false\n and isPerformancePhase(triggerId, curPhase) == false\n and isExhibitionismPhase(triggerId, curPhase) == false\n and isPromiscuityPhase(triggerId, curPhase) == false\n and isIntimacyPhase(triggerId, curPhase) == false then\n local currentClimaxCount = math.max(\n 0,\n tonumber(getDualFlag(triggerId, vgHeroineEffect.varName, vgHeroineEffect.climaxCount, false)) or 0\n )\n\n local nextClimaxCount = math.max(\n 0,\n tonumber(getDualFlag(triggerId, vgHeroineEffect.varName, vgHeroineEffect.climaxCount, true)) or 0\n )\n\n if nextClimaxCount > currentClimaxCount then\n local minLevel, maxLevel = getBaseDeckMinMaxLevel()\n\n for i = currentClimaxCount + 1, nextClimaxCount do\n local cardLevel = math.random(minLevel, maxLevel)\n\n local extraFields = {\n temp = 1\n }\n\n if isHEventPhase(triggerId, curPhase) == false then\n local bonusKey = \"bonus_\" .. tostring(constEffectType.refresh)\n extraFields[bonusKey] = 0.1\n end\n\n addOneTempCard(\n constCardType.ecstasy,\n cardLevel,\n extraFields,\n \"(임시) \" .. getCardTypeEmoji(triggerId, constCardType.ecstasy) .. \" \" .. getCardLevelStars(cardLevel) .. \" 카드가 추가되었습니다.\"\n )\n end\n end\n end\n end\n\n if hasChanged then\n setTrackedChatVar(triggerId, changedVars, \"vg_Deck_Temp\", tempDeckStr)\n setChatVar(triggerId, \"vg_PlayDeck_Remain\", table.concat(remainArray, \"|\"))\n end\nend\n\n-- ==========================================\n-- 불러오기\n-- ==========================================\nfunction loadVGData(triggerId)\n\n resetSession(triggerId)\n restoreFullSave(triggerId)\n\n setChatVar(triggerId, \"vg_Mode_Save\", \"0\")\n\n setChatVar(triggerId, \"vg_Reward_Selected\", \"\")\n setChatVar(triggerId, \"vg_Reward_Log\", \"\")\n\n setChatVar(triggerId, \"vg_Enhancement_Select\", \"\")\n\n buildSexualProfile(triggerId)\n\n buildPlayDeckSetting(triggerId)\n buildPlayDeckChoice(triggerId)\n\n buildCurrentState(triggerId)\n buildEncounterActionSetting(triggerId)\n\n buildMemoryDisplay(triggerId)\n\n updatePregnancyFlag(triggerId)\n\n generateHTML(triggerId)\n \n reloadDisplay(triggerId)\n\n alertNormal(triggerId, \"✅ 불러오기 완료\")\nend\n\n-- ==========================================\n-- 출력\n-- ==========================================\nonOutput = async(function(triggerId)\n local curPhase = getPhase(triggerId)\n local prevPhase = getCurrentPhase(triggerId)\n\n local deckPhase = isDeckPhase(triggerId, curPhase)\n\n local vgMode = tonumber(safeGet(triggerId, \"vg_Mode\", \"\")) or constMode.vg\n\n -- ==========================================\n -- 로그 작성\n -- ==========================================\n local tempLogs = {}\n local persistentLogs = {}\n local saveText = \"\"\n local choiceTexts = {}\n local lastTexts = {}\n\n local function addTempLog(msg)\n if msg and msg ~= \"\" then\n table.insert(tempLogs, msg)\n end\n end\n\n local function addPersistentLog(msg)\n if msg and msg ~= \"\" then\n table.insert(persistentLogs, msg)\n end\n end\n\n local function addChoiceText(text)\n if text and text ~= \"\" then\n table.insert(choiceTexts, text)\n end\n end\n\n local function addLastText(text)\n if text and text ~= \"\" then\n table.insert(lastTexts, text)\n end\n end\n\n local function buildExtraOutput()\n local parts = {}\n\n if #persistentLogs > 0 then\n for _, msg in ipairs(persistentLogs) do\n table.insert(parts, \"[Persistent Log: \" .. msg .. \"]\")\n end\n end\n\n if #tempLogs > 0 then\n for _, msg in ipairs(tempLogs) do\n table.insert(parts, \"[Temporary Log: \" .. msg .. \"]\")\n end\n end\n\n if #choiceTexts > 0 then\n table.insert(parts, \"<Choice>\\n\" .. table.concat(choiceTexts, \"\\n\") .. \"\\n</Choice>\")\n end\n\n if saveText ~= \"\" then\n table.insert(parts, saveText)\n end\n\n if #lastTexts > 0 then\n table.insert(parts, table.concat(lastTexts, \"\\n\"))\n end\n\n if #parts > 0 then\n return \"\\n\" .. table.concat(parts, \"\\n\")\n end\n\n return \"\"\n end\n\n -- ==========================================\n -- 변경 값 추적\n -- ==========================================\n local changedVars = {}\n\n if vgMode == constMode.vg then\n -- ==========================================\n -- 임시 카드\n -- ==========================================\n if deckPhase == true then\n addTempCards(triggerId, changedVars, addTempLog)\n end\n\n -- ==========================================\n -- 강화 효과 로그\n -- ==========================================\n do\n local slot1NextName = trim(getDualFlag(triggerId, vgEnhancement.varName, vgEnhancement.slot1_Name, true))\n local slot2NextName = trim(getDualFlag(triggerId, vgEnhancement.varName, vgEnhancement.slot2_Name, true))\n local slot3NextName = trim(getDualFlag(triggerId, vgEnhancement.varName, vgEnhancement.slot3_Name, true))\n\n if slot1NextName ~= \"\" then\n addPersistentLog(\"강화 효과 획득: \" .. slot1NextName)\n end\n\n if slot2NextName ~= \"\" then\n addPersistentLog(\"강화 효과 획득: \" .. slot2NextName)\n end\n\n if slot3NextName ~= \"\" then\n addPersistentLog(\"강화 효과 획득: \" .. slot3NextName)\n end\n end\n\n -- ==========================================\n -- Next 값 적용\n -- ==========================================\n applyNextDualFlagToCurrent(triggerId, vgHeroineStat.varName)\n applyNextDualFlagToCurrent(triggerId, vgHeroineEffect.varName)\n\n applyNextDualFlagToCurrent(triggerId, vgEnhancement.varName)\n\n applyNextDualFlagToCurrent(triggerId, vgContext.varName)\n\n applyNextDualFlagToCurrent(triggerId, vgSessionInfo.varName)\n applyNextDualFlagToCurrent(triggerId, vgEncounterStat.varName)\n end\n\n if safeGet(triggerId, \"vg_Reward_Log\", \"\") ~= \"\" then\n setChatVar(triggerId, \"vg_Reward_Log\", \"\")\n setChatVar(triggerId, \"vg_Reward_Selected\", \"\")\n end\n\n setChatVar(triggerId, \"vg_Enhancement_Select\", \"\")\n\n local previousDate, previousTime = getPreviousCharDateTime(triggerId)\n\n local chatHistory = getFullChat(triggerId)\n if #chatHistory > 0 then\n local lastIndex = #chatHistory\n local lastMsg = chatHistory[lastIndex].data or \"\"\n local cleanMsg = lastMsg\n\n -- ==========================================\n -- Status Window 닫는 태그 보정\n -- ==========================================\n cleanMsg = ensureStatusWindowClosed(cleanMsg)\n\n -- ==========================================\n -- 날짜/시간 적용\n -- ==========================================\n local parsedDate, parsedTime = applyDateTimeFromStatusWindow(cleanMsg)\n\n -- ==========================================\n -- 성지식\n -- ==========================================\n do\n local currentKnowledge = tonumber(getDualFlag(triggerId, vgSexKnowledge.varName, vgSexKnowledge.level, false)) or 0\n local nextKnowledgeRaw = trim(getDualFlag(triggerId, vgSexKnowledge.varName, vgSexKnowledge.level, true))\n local nextKnowledge = tonumber(nextKnowledgeRaw) or currentKnowledge\n\n if nextKnowledgeRaw ~= \"\" then\n if parsedDate ~= \"\" and nextKnowledge > currentKnowledge then\n local nextKnowledgeText = getSexKnowledgeText(nextKnowledge)\n appendMemoryEntry(triggerId, parsedDate, \"Sexual Knowledge Increased\", nextKnowledgeText)\n buildMemoryDisplay(triggerId)\n\n addPersistentLog(\"성지식 증가: \" .. nextKnowledgeText)\n end\n\n applyNextDualFlagToCurrent(triggerId, vgSexKnowledge.varName)\n end\n end\n -- ==========================================\n -- 임신 관련 작업\n -- ==========================================\n if parsedDate ~= \"\" and parsedDate ~= previousDate then\n syncLastPeriodToCurrentDate(triggerId, parsedDate, changedVars)\n end\n\n do\n local vaginalRiskLogs = {}\n cleanMsg, vaginalRiskLogs = parseVaginalEjaculationRecordBlocks(triggerId, cleanMsg, parsedDate, changedVars)\n\n for _, line in ipairs(vaginalRiskLogs or {}) do\n addPersistentLog(line)\n end\n end\n\n do\n local pregnancyStartLogs = {}\n cleanMsg, pregnancyStartLogs = parsePregnancyStartRecordBlock(triggerId, cleanMsg, changedVars)\n\n for _, line in ipairs(pregnancyStartLogs or {}) do\n addPersistentLog(line)\n end\n end\n\n do\n local pregnancyEndLogs = {}\n cleanMsg, pregnancyEndLogs = parsePregnancyEndRecordBlock(triggerId, cleanMsg, parsedDate, changedVars)\n\n for _, line in ipairs(pregnancyEndLogs or {}) do\n addPersistentLog(line)\n end\n end\n\n updatePregnancyFlag(triggerId, parsedDate, changedVars)\n\n -- ==========================================\n -- 첫경험 기록\n -- ==========================================\n do\n local firstExperienceLogs = {}\n cleanMsg, firstExperienceLogs = parseFirstExperienceRecordBlocks(triggerId, cleanMsg, changedVars)\n\n for _, line in ipairs(firstExperienceLogs or {}) do\n addPersistentLog(line)\n end\n end\n\n -- ==========================================\n -- 페이즈 별 작업\n -- ==========================================\n if vgMode == constMode.vg then\n\n local parsedEnhancementChoice = \"\"\n cleanMsg, parsedEnhancementChoice = parseEnhancementBlock(triggerId, cleanMsg)\n\n -- ==========================================\n -- 특성\n -- ==========================================\n do\n local sexualTraitLogs = {}\n cleanMsg, sexualTraitLogs = parseSexualTraitRecordBlock(triggerId, cleanMsg, parsedDate)\n\n for _, line in ipairs(sexualTraitLogs or {}) do\n addPersistentLog(line)\n end\n end\n\n do\n local corruptionTraitLogs = {}\n cleanMsg, corruptionTraitLogs = parseCorruptionTraitRecordBlock(triggerId, cleanMsg, parsedDate)\n\n for _, line in ipairs(corruptionTraitLogs or {}) do\n addPersistentLog(line)\n end\n end\n\n -- ==========================================\n -- 요약 / 목표\n -- ==========================================\n local parsedResolution = constResolution.none\n local parsedRecordOutcome = \"\"\n local parsedNextStoryGoal = \"\"\n cleanMsg, parsedResolution, parsedRecordOutcome, parsedNextStoryGoal = parseStoryMemoryRecordBlock(triggerId, cleanMsg)\n\n do\n local goalTempLogs = {}\n cleanMsg, goalTempLogs = parseStoryGoalRecordBlock(triggerId, cleanMsg)\n\n for _, line in ipairs(goalTempLogs or {}) do\n addTempLog(line)\n end\n end\n\n if deckPhase == false then\n local resultFlag = tonumber(safeGet(triggerId, \"vg_Result_Flag\", \"0\")) or 0\n\n if resultFlag == 1 then\n local resultLogs = {}\n cleanMsg, resultLogs = parseDevelopmentBlocks(triggerId, cleanMsg, prevPhase, parsedDate)\n\n for _, line in ipairs(resultLogs or {}) do\n addPersistentLog(line)\n end\n\n setChatVar(triggerId, \"vg_Result_Flag\", \"0\")\n setChatVar(triggerId, \"vg_Result_Start\", \"\")\n end\n\n if curPhase == constPhase.standby then\n local stageValue = tonumber(getResolvedDualFlag(triggerId, vgSessionInfo.varName, vgSessionInfo.stage, 0)) or 0\n\n if stageValue >= 4 then\n local actValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.act, 1)) or 1\n setDualFlag(triggerId, vgContext.varName, vgContext.act, tostring(actValue + 1), false)\n end\n\n local parsedStoryChoice\n cleanMsg, parsedStoryChoice = parseStoryChoiceBlock(triggerId, cleanMsg)\n\n for _, line in ipairs(parsedStoryChoice) do\n addChoiceText(line)\n end\n\n resetSession(triggerId)\n elseif curPhase == constPhase.daily then\n local parsedDailyChoice\n cleanMsg, parsedDailyChoice = parseDailyChoiceBlock(triggerId, cleanMsg)\n\n for _, line in ipairs(parsedDailyChoice) do\n addChoiceText(line)\n end\n\n resetEncounter(triggerId)\n elseif curPhase == constPhase.interlude then\n local parsedInterludeChoice\n cleanMsg, parsedInterludeChoice = parseInterludeChoiceBlock(triggerId, cleanMsg)\n\n for _, line in ipairs(parsedInterludeChoice) do\n addChoiceText(line)\n end\n\n resetEncounter(triggerId)\n\n elseif curPhase == constPhase.advance then\n local parsedAdvanceChoice\n cleanMsg, parsedAdvanceChoice = parseAdvanceChoiceBlock(triggerId, cleanMsg)\n\n for _, line in ipairs(parsedAdvanceChoice) do\n addChoiceText(line)\n end\n elseif curPhase == constPhase.incapacitated then\n local parsedIncapacitatedChoice\n cleanMsg, parsedIncapacitatedChoice = parseIncapacitatedChoiceBlock(triggerId, cleanMsg, prevPhase)\n\n for _, line in ipairs(parsedIncapacitatedChoice) do\n addChoiceText(line)\n end\n\n resetSession(triggerId)\n\n elseif curPhase == constPhase.captivity then\n local parsedCaptivityChoice\n cleanMsg, parsedCaptivityChoice = parseCaptivityChoiceBlock(triggerId, cleanMsg)\n\n for _, line in ipairs(parsedCaptivityChoice) do\n addChoiceText(line)\n end\n\n resetSession(triggerId)\n\n elseif curPhase == constPhase.unfree then\n local parsedUnfreeChoice\n cleanMsg, parsedUnfreeChoice = parseUnfreeChoiceBlock(triggerId, cleanMsg)\n\n for _, line in ipairs(parsedUnfreeChoice) do\n addChoiceText(line)\n end\n\n resetSession(triggerId)\n\n elseif curPhase == constPhase.delivery_Daily or curPhase == constPhase.delivery_Unfree then\n local deliveryChoice = buildDeliveryChoice(triggerId)\n\n if deliveryChoice ~= \"\" then\n addChoiceText(deliveryChoice)\n end\n end\n\n do\n if parsedResolution == constResolution.storyline then\n local goalShort = trim(safeGet(triggerId, \"vg_Goal_Short\", \"\"))\n local nextStoryGoal = trim(parsedNextStoryGoal or \"\")\n\n if goalShort ~= \"\" then\n addTempLog(\"현재 목표 달성: \" .. goalShort)\n end\n\n if nextStoryGoal ~= \"\" then\n addTempLog(\"새로운 목표: \" .. nextStoryGoal)\n end\n\n setChatVar(triggerId, \"vg_Goal_Short\", nextStoryGoal)\n\n elseif trim(parsedRecordOutcome) ~= \"\" then\n addTempLog(parsedRecordOutcome)\n end\n end\n\n cleanMsg = parseSexualActRecordBlock(triggerId, cleanMsg)\n\n -- ==========================================\n -- H 이벤트\n -- ==========================================\n do\n if curPhase == constPhase.captivity then\n addChoiceText(\"[H Event: 능욕 이벤트 진행 | Emoji: 💔]\")\n end\n end\n\n -- ==========================================\n -- 자위\n -- ==========================================\n do\n local allowMasturbationChoice = false\n\n if curPhase == constPhase.daily or curPhase == constPhase.unfree then\n allowMasturbationChoice = true\n\n elseif curPhase == constPhase.interlude then\n local stageValue = tonumber(getResolvedDualFlag(triggerId, vgSessionInfo.varName, vgSessionInfo.stage, 1)) or 1\n\n if stageValue < 4 then\n allowMasturbationChoice = true\n end\n end\n\n if allowMasturbationChoice then\n local masturbationChoice = buildMasturbationChoice(triggerId)\n\n if masturbationChoice ~= \"\" then\n addChoiceText(masturbationChoice)\n end\n end\n end\n\n -- ==========================================\n -- 목표 설정\n -- ==========================================\n do\n if curPhase == constPhase.daily or curPhase == constPhase.standby then\n addChoiceText(\"[Blank Line]\")\n addChoiceText(\"[Goal Action: 1 | Text: 현재 목표 설정 | Emoji: ✏️]\")\n addChoiceText(\"[Goal Action: 2 | Text: 현재 목표 삭제 | Emoji: 🗑️]\")\n\n if curPhase == constPhase.daily then\n addChoiceText(\"[Goal Action: 3 | Text: 현재 목표 재설정 | Emoji: 🔄]\")\n end\n end\n end\n\n -- ==========================================\n -- 추가 선택지\n -- ==========================================\n local addModeChangeChoice = false\n if curPhase == constPhase.standby or curPhase == constPhase.captivity then\n local modeChangeChoice = buildModeChangeChoice(triggerId, constMode.free)\n\n if modeChangeChoice ~= \"\" then\n addChoiceText(\"[Blank Line]\")\n addChoiceText(modeChangeChoice)\n addModeChangeChoice = true\n end\n end\n\n local additionalMessageChoice = buildAdditionalMessageChoice(triggerId)\n\n if additionalMessageChoice ~= \"\" then\n if addModeChangeChoice == false then\n addChoiceText(\"[Blank Line]\")\n end\n addChoiceText(additionalMessageChoice)\n end\n\n do\n local allowStoryRecordChoice = false\n\n if prevPhase ~= constPhase.escape and prevPhase ~= constPhase.rescue\n and not isIncapacitatedPhase(triggerId, prevPhase)\n and not isPromiscuityPhase(triggerId, prevPhase)\n and not isIntimacyPhase(triggerId, prevPhase)\n and not isHEventPhase(triggerId, prevPhase) then\n\n if curPhase == constPhase.daily then\n allowStoryRecordChoice = true\n\n elseif curPhase == constPhase.interlude then\n local stageValue = tonumber(getResolvedDualFlag(triggerId, vgSessionInfo.varName, vgSessionInfo.stage, 1)) or 1\n\n if stageValue < 4 then\n allowStoryRecordChoice = true\n end\n end\n end\n\n if allowStoryRecordChoice then\n local storyRecordChoice = buildStoryRecordChoice(triggerId, prevPhase)\n\n if storyRecordChoice ~= \"\" then\n addChoiceText(storyRecordChoice)\n end\n end\n end\n\n saveText = buildFullSave(triggerId)\n else\n cleanMsg = parseHeroineActionSettingBlock(triggerId, cleanMsg)\n cleanMsg = parseEncounterActionSettingBlock(triggerId, cleanMsg)\n cleanMsg = parseCompanionActionSettingBlock(triggerId, cleanMsg)\n\n local heroineEffect = loadDualFlagCurrentMap(triggerId, vgHeroineEffect)\n\n local ambushValue = tonumber(heroineEffect.ambush) or constAmbush.none\n if ambushValue == constAmbush.heroine then\n addTempLog(\"기습!\")\n elseif ambushValue == constAmbush.encounter then\n addTempLog(\"기습당함!\")\n end\n\n setDualFlag(triggerId, vgHeroineEffect.varName, vgHeroineEffect.ambush, tostring(constAmbush.none), false)\n\n buildPlayDeckChoice(triggerId)\n\n local drawCount = getPlayDeckDrawCount(triggerId)\n drawPlayDeckCards(triggerId, drawCount)\n\n addChoiceText(\"◀▣▶\")\n\n if curPhase == constPhase.event_H then\n local additionalMessageChoice = buildAdditionalMessageChoice(triggerId)\n\n if additionalMessageChoice ~= \"\" then\n addChoiceText(additionalMessageChoice)\n end\n end\n\n saveText = buildSessionSave(triggerId, changedVars)\n end\n\n -- ==========================================\n -- 보상\n -- ==========================================\n local rewardChoiceRaw = trim(safeGet(triggerId, \"vg_Reward_Choice\", \"\"))\n if rewardChoiceRaw ~= \"\" then\n addLastText(rewardChoiceRaw)\n setChatVar(triggerId, \"vg_Reward_Choice\", \"\")\n end\n\n -- ==========================================\n -- 강화\n -- ==========================================\n if parsedEnhancementChoice ~= \"\" then\n addLastText(parsedEnhancementChoice)\n end\n else\n applyNextDualFlagToCurrent(triggerId, vgContext.varName)\n\n local modeChangeTargets = {\n constMode.vg,\n constMode.vg_Recovery,\n constMode.cancel,\n }\n\n for _, targetMode in ipairs(modeChangeTargets) do\n local modeChangeChoice = buildModeChangeChoice(triggerId, targetMode)\n\n if modeChangeChoice ~= \"\" then\n addChoiceText(modeChangeChoice)\n end\n end\n\n addChoiceText(\"[Goal Action: 3 | Text: 현재 목표 재설정 | Emoji: 🔄]\")\n\n if parsedDate ~= \"\" and parsedDate ~= previousDate then\n saveText = buildFullSave(triggerId)\n else\n saveText = buildFreeModeSave(triggerId, changedVars)\n end\n\n setChatVar(triggerId, \"vg_Mode_Save\", \"0\")\n end\n\n -- ==========================================\n -- 상태창 확장\n -- ==========================================\n local heroineStat = loadDualFlagCurrentMap(triggerId, vgHeroineStat)\n\n local heroineLevel = math.max(1, getHeroineLevel(triggerId))\n\n local strength = tonumber(heroineStat.strength) or 0\n local reflex = tonumber(heroineStat.reflex) or 0\n local willpower = tonumber(heroineStat.willpower) or 0\n\n local hp = tonumber(heroineStat.hp) or 0\n local hpMax = tonumber(heroineStat.hp_Max) or 0\n local resource = tonumber(heroineStat.resource) or 0\n local resourceMax = tonumber(heroineStat.resource_Max) or 0\n local arousal = tonumber(heroineStat.arousal) or 0\n local arousalMax = tonumber(heroineStat.arousal_Max) or 0\n\n local statusLines = {}\n\n table.insert(\n statusLines,\n \"[Level: \" .. tostring(heroineLevel) ..\n \" | Strength: \" .. formatFloat(strength) ..\n \" | Reflex: \" .. formatFloat(reflex) ..\n \" | Willpower: \" .. formatFloat(willpower) .. \"]\"\n )\n\n table.insert(statusLines,\n \"[HP: \" .. formatFloat(hp) .. \" / \" .. formatFloat(hpMax) ..\n \" | Resource: \" .. formatFloat(resource) .. \" / \" .. formatFloat(resourceMax) ..\n \" | Arousal: \" .. formatFloat(arousal) .. \" / \" .. formatFloat(arousalMax) .. \"]\"\n )\n\n if isEncounterPhase(triggerId, curPhase) then\n local encounterStat = loadDualFlagCurrentMap(triggerId, vgEncounterStat)\n\n local encounterHP = tonumber(encounterStat.hp) or 0\n local encounterMaxHP = tonumber(encounterStat.hp_Max) or 0\n\n local encounterEmoji, _, displayEncounterHP, displayEncounterMaxHP = getEncounterDisplayInfo(triggerId, curPhase, encounterHP, encounterHP, encounterMaxHP)\n\n local encounterLabel = \"Encounter HP\"\n\n table.insert(\n statusLines,\n \"[Encounter HP: \" .. formatFloat(displayEncounterHP) .. \" / \" .. formatFloat(displayEncounterMaxHP) ..\n \" | Emoji: \" .. encounterEmoji .. \"]\"\n )\n end\n\n local statusText = table.concat(statusLines, \"\\n\")\n cleanMsg = cleanMsg:gsub(\"(<Status Window>%s*.-)%s*</Status Window>\", \"%1\\n\" .. statusText .. \"\\n</Status Window>\", 1)\n\n -- ==========================================\n -- 특성 괄호 제거\n -- ==========================================\n cleanMsg = removeHeroineTraitTagsFromCleanMessage(triggerId, cleanMsg)\n\n -- ==========================================\n -- 마무리\n -- ==========================================\n cleanMsg = cleanMsg .. buildExtraOutput() .. \"\\n♥♣♥\"\n if cleanMsg ~= lastMsg then\n setChat(triggerId, lastIndex - 1, cleanMsg)\n end\n\n buildSexualProfile(triggerId)\n generateHTML(triggerId)\n end\n\nend)\n\n-- ==========================================\n-- 선택지\n-- ==========================================\nonButtonClick = async(function(triggerId, c)\n\n local parts = {}\n\n for m in (c .. \"_\"):gmatch(\"(.-)_\") do\n table.insert(parts, m)\n end\n\n -- choice가 아니면 종료\n if parts[1] ~= \"choice\" then\n return\n end\n\n local choiceType = parts[2]\n\n local function pushChoiceInput(text)\n text = tostring(text or \"\")\n\n local rewardLogRaw = trim(safeGet(triggerId, \"vg_Reward_Log\", \"\"))\n if rewardLogRaw ~= \"\" then\n local rewardLines = {}\n\n for _, logText in ipairs(splitPipe(rewardLogRaw)) do\n logText = trim(logText or \"\")\n if logText ~= \"\" then\n table.insert(rewardLines, \"[Temporary Log: \" .. logText .. \"]\")\n end\n end\n\n if #rewardLines > 0 then\n if text ~= \"\" then\n text = text .. \"\\n\" .. table.concat(rewardLines, \"\\n\")\n else\n text = table.concat(rewardLines, \"\\n\")\n end\n end\n end\n\n local enhancementSelectText = trim(safeGet(triggerId, \"vg_Enhancement_Select\", \"\"))\n if enhancementSelectText ~= \"\" and not text:find(enhancementSelectText, 1, true) then\n text = text .. \"\\n\" .. enhancementSelectText\n end\n\n local cl = getChatLength(triggerId)\n\n if cl > 0 then\n local last = getChat(triggerId, cl - 1)\n\n if last and last.role == \"user\" and type(last.data) == \"string\" then\n setChat(triggerId, cl - 1, text)\n return\n end\n end\n\n addChat(triggerId, \"user\", text)\n end\n\n local function resolveAmbushState(triggerId, heroineChance, encounterChance)\n heroineChance = clamp(tonumber(heroineChance) or 20, 0, 100)\n encounterChance = clamp(tonumber(encounterChance) or 20, 0, 100)\n\n local totalChance = heroineChance + encounterChance\n\n if totalChance > 100 and totalChance > 0 then\n local scale = 100 / totalChance\n heroineChance = heroineChance * scale\n encounterChance = encounterChance * scale\n end\n\n local ambushValue = constAmbush.none\n local roll = math.random(10000) / 100\n local heroineThreshold = heroineChance\n local encounterThreshold = heroineChance + encounterChance\n\n if roll <= heroineThreshold then\n ambushValue = constAmbush.heroine\n elseif roll <= encounterThreshold then\n ambushValue = constAmbush.encounter\n end\n\n setDualFlag(triggerId, vgHeroineEffect.varName, vgHeroineEffect.ambush, tostring(ambushValue), false)\n\n if ambushValue == constAmbush.heroine then\n return \"The heroine detects the hostile presence first, or otherwise gains the first clear advantage or opening.\"\n elseif ambushValue == constAmbush.encounter then\n return \"The hostile presence detects the heroine first, or otherwise gains the first clear advantage or opening.\"\n end\n\n return \"The heroine and the hostile presence become aware of each other.\"\n end\n\n local function insertSexualActRequirementLine(lines, encounterType)\n if type(lines) ~= \"table\" then\n return\n end\n\n encounterType = tonumber(encounterType) or 0\n\n local function buildNonPenetrativeText()\n local nonPenetrativeActExamples = {\n \"paizuri\",\n \"handjob\",\n \"footjob\",\n \"armpit sex\",\n \"thighjob\",\n \"frottage\",\n \"intergluteal sex\",\n }\n\n shuffleArray(nonPenetrativeActExamples)\n\n local pickedExamples = {}\n\n for i = 1, math.min(2, #nonPenetrativeActExamples) do\n table.insert(pickedExamples, nonPenetrativeActExamples[i])\n end\n\n return \"non-penetrative penile sexual acts (e.g., \" .. table.concat(pickedExamples, \", \") .. \")\"\n end\n\n local function buildBaseActTexts(pickCount)\n local baseActPool = {\n \"vaginal penetration\",\n \"anal penetration\",\n \"oral penetration\",\n \"kissing\",\n }\n\n pickCount = tonumber(pickCount) or 3\n pickCount = clamp(pickCount, 0, #baseActPool)\n\n shuffleArray(baseActPool)\n\n local examples = {}\n local hasKissing = false\n\n for i = 1, pickCount do\n local act = baseActPool[i]\n\n if act == \"kissing\" then\n hasKissing = true\n else\n table.insert(examples, act)\n end\n end\n\n local result = {}\n\n if #examples > 0 then\n table.insert(result, \"penetrative sexual acts (e.g., \" .. table.concat(examples, \", \") .. \")\")\n end\n\n if hasKissing then\n table.insert(result, \"kissing\")\n end\n\n return result\n end\n\n local extraActs = {\n \"breast groping\",\n \"nipple sucking\",\n \"breast licking\",\n \"nipple stimulation\",\n \"vaginal fingering\",\n \"anal fingering\",\n \"clitoral stimulation\",\n \"anal stimulation\",\n \"buttock groping\",\n \"vulva rubbing\",\n \"finger in the mouth\",\n \"cunnilingus\",\n }\n\n local actList = {}\n\n if encounterType == constPhase.brainwashing\n or encounterType == constPhase.event_Brainwashing then\n table.insert(extraActs, \"forced to perform anilingus\")\n end\n\n if encounterType == constPhase.sleeping_Ambush\n or encounterType == constPhase.entrapment\n or encounterType == constPhase.brainwashing then\n shuffleArray(extraActs)\n\n actList = {\n buildNonPenetrativeText(),\n }\n\n for i = 1, math.min(3, #extraActs) do\n table.insert(actList, extraActs[i])\n end\n\n elseif encounterType == constPhase.transaction then\n local transactionActs = buildBaseActTexts(2)\n\n table.insert(transactionActs, \"forced to perform anilingus\")\n\n shuffleArray(transactionActs)\n\n actList = {\n buildNonPenetrativeText(),\n }\n\n for i = 1, math.min(2, #transactionActs) do\n table.insert(actList, transactionActs[i])\n end\n elseif encounterType == constPhase.event_Immobilized\n or encounterType == constPhase.event_Brainwashing\n or encounterType == constPhase.event_Sleeping then\n\n shuffleArray(extraActs)\n\n actList = {\n buildNonPenetrativeText(),\n }\n\n for _, act in ipairs(buildBaseActTexts(1)) do\n table.insert(actList, act)\n end\n\n for i = 1, math.min(2, #extraActs) do\n table.insert(actList, extraActs[i])\n end\n else\n actList = buildBaseActTexts(3)\n end\n\n table.insert(lines,\n \"▷ [In the next Encounter Action Setting, one action must fall under one of the following options: \" ..\n table.concat(actList, \", \") ..\n \". Other actions should be outside this list unless multiple simultaneous actions are possible, such as with multiple characters, multiple insertive objects, or combinations that can naturally be performed at the same time. Each action must be a different sexual action aimed at a different body part.]\"\n )\n end\n\n local function buildEncounterSetupText(triggerId, dateValue, storyEmojiValue, storyTypeValue, situationValue, encounterEmojiValue, encounterValue, encounterTypeValue, difficultyEmojiValue, levelValue)\n\n local stageValue = tonumber(getResolvedDualFlag(triggerId, vgSessionInfo.varName, vgSessionInfo.stage, 1)) or 1\n\n local lines = {}\n\n local function buildEncounterPreparationLine(encounterSexPhrase)\n return \"▷ [The encounter clearly reveals and prepares the means it will use for the coming \" .. encounterSexPhrase .. \". If the encounter includes a male participant, this includes exposing his genitals; otherwise, it should naturally reveal, ready, or activate whatever means it will use.]\"\n end\n\n table.insert(lines, \"[Choice Select: \" .. dateValue ..\n \" | Story Emoji: \" .. tostring(storyEmojiValue or \"\") ..\n \" | Situation: \" .. tostring(situationValue or \"\") ..\n \" | Encounter Emoji: \" .. tostring(encounterEmojiValue or \"\") ..\n \" | Encounter: \" .. tostring(encounterValue or \"\") ..\n \" | Difficulty Emoji: \" .. tostring(difficultyEmojiValue or \"\") ..\n \" | Level: \" .. tostring(levelValue or 1) .. \"]\")\n\n local lastDate, _ = getLatestCharDateTime(triggerId)\n\n if lastDate ~= dateValue then\n table.insert(lines, \"▷ [Continue from the current time or from a point later than the current time, then let time pass naturally until \" .. tostring(dateValue or \"\") .. \", briefly covering the intervening time and events in passing.]\")\n else\n table.insert(lines, \"▷ [Continue from a point later than the current time on \" .. tostring(dateValue or \"\") .. \", whether immediately or after some time has passed.]\")\n end\n\n if isMeaningfulValue(tostring(encounterValue or \"\")) then\n table.insert(lines, \"▶ [Encounter = \" .. tostring(encounterValue or \"\") .. \"]\")\n end\n\n encounterTypeValue = tonumber(encounterTypeValue) or 0\n\n if isCombatPhase(triggerId, encounterTypeValue) then\n\n if encounterTypeValue == constPhase.sleeping_Ambush then\n table.insert(lines, \"▷ [Describe the heroine in a state of deep sleep. The encounter finds the heroine and moves with the intent to sexually assault her. The response must end before any direct sexual contact occurs or any sexual act begins.]\")\n table.insert(lines, buildEncounterPreparationLine(\"sexual violation\"))\n insertSexualActRequirementLine(lines, encounterTypeValue)\n table.insert(lines, \"▷ [The heroine must remain deeply asleep and unaware throughout this response.]\")\n\n elseif encounterTypeValue == constPhase.entrapment then\n table.insert(lines, \"▷ [Describe how the heroine becomes immobilized while remaining conscious through paralysis, drugging, numbing agents, or similar means, not through physical restraint.]\")\n table.insert(lines, buildEncounterPreparationLine(\"sexual violation\"))\n insertSexualActRequirementLine(lines, encounterTypeValue)\n table.insert(lines, \"▷ [The encounter moves to sexually assault the heroine. No direct contact, sexual act, or sexual contact may occur yet, and the response must end immediately before anything physical begins.]\")\n\n elseif encounterTypeValue == constPhase.brainwashing then\n table.insert(lines, \"▷ [Describe how the heroine is completely dominated by the encounter's mental control, fully conscious yet unable to govern either her own speech or her body's actions according to her own will.]\")\n table.insert(lines, buildEncounterPreparationLine(\"sexual violation\"))\n insertSexualActRequirementLine(lines, encounterTypeValue)\n table.insert(lines, \"▷ [The heroine must remain under complete mental control throughout this response, conscious but unable to act according to her own will or disobey commands. No direct sexual contact, sexual act, or explicit sexual violation may occur yet, and the response must end immediately before anything physical begins.]\")\n\n elseif encounterTypeValue == constPhase.abduction then\n table.insert(lines, \"▷ [Describe how the heroine is abducted, then describe her regaining consciousness or awareness either during the journey elsewhere or after arriving in another unfamiliar place.]\")\n table.insert(lines, buildEncounterPreparationLine(\"sexual violation\"))\n table.insert(lines, \"▷ [By the time the heroine regains consciousness or awareness, no direct contact, sexual contact, or sexual act has occurred. The response must end immediately before anything physical begins.]\")\n else\n if encounterTypeValue == constPhase.escape then\n table.insert(lines, \"▷ [Describe the heroine regaining consciousness, deciding to escape, and preparing for that escape naturally and plausibly. As the escape unfolds, combat or other physical confrontation becomes unavoidable. Her chance to act should arise either through careful preparation or a plausible stroke of luck, and by this point she must no longer be in a completely helpless state.]\")\n elseif encounterTypeValue == constPhase.companion then\n table.insert(lines, \"▷ [Describe how the heroine and an established named allied companion naturally become involved in the same confrontation.]\")\n end\n\n local heroineAmbushChance = 20\n local encounterAmbushChance = 20\n local ambushGuide = resolveAmbushState(triggerId, heroineAmbushChance, encounterAmbushChance)\n\n table.insert(lines, \"▷ [Describe the encounter and set it up. \" .. ambushGuide .. \" The heroine and the hostile presence must not engage yet, and the response must end before direct combat begins.]\")\n end\n\n elseif encounterTypeValue == constPhase.harassment or encounterTypeValue == constPhase.aphrodisiac then\n\n if encounterTypeValue == constPhase.aphrodisiac then\n table.insert(lines, \"▷ [Describe how and why the heroine is exposed to the encounter's aphrodisiac influence.]\")\n end\n\n table.insert(lines, \"▷ [Describe the encounter and set it up. The encounter has already noticed the heroine, while the heroine may or may not have noticed it yet. This situation must be framed as open sexual misconduct in an exposed, socially constrained, or otherwise difficult-to-react-to setting, and the response must end before any direct contact occurs.]\")\n elseif encounterTypeValue == constPhase.abortion_Ambush then\n table.insert(lines, \"▷ [In this response, the abortion is successfully carried out.]\")\n table.insert(lines, \"▷ [During the process, the heroine is placed under anesthesia. The encounter moves to sexually assault the heroine. No direct contact, sexual act, or sexual contact may occur yet, and the response must end immediately before anything physical begins.]\")\n elseif encounterTypeValue == constPhase.abuse then\n table.insert(lines, \"▷ [Describe how the heroine comes to be in this face-to-face situation with the encounter under a seemingly legitimate pretext or unequal relational footing. The encounter may conceal its true intent behind a seemingly legitimate purpose, and the heroine does not have to recognize the danger yet. The response must frame the situation as sexual exploitation developing under that pretext, and must end before any explicit sexual misconduct or direct sexual contact begins.]\")\n\n elseif encounterTypeValue == constPhase.confrontation then\n table.insert(lines, \"▷ [Describe the encounter and set it up. This situation must be framed as a hostile verbal clash, accusation, dispute, or tense interpersonal standoff, with the possibility of sexual humiliation, coercive advances, or unwanted sexual contact emerging within the conflict. It must remain a confrontation and must not escalate into combat. The response must end before the confrontation turns direct.]\")\n\n elseif encounterTypeValue == constPhase.indoctrination then\n table.insert(lines, \"▷ [Describe the encounter and set it up. This situation must be framed as sexually exploitative pressure through which the encounter is positioned to make the heroine learn, realize, or become exposed to sexual knowledge she did not previously have. It must remain a sexually exploitative situation of indoctrination and must not escalate into combat. The response must end before the exploitative sexual act, instruction, demonstration, or direct sexual contact begins.]\")\n\n local currentKnowledge = tonumber(getDualFlag(triggerId, vgSexKnowledge.varName, vgSexKnowledge.level, false)) or 0\n if currentKnowledge < 5 then\n local currentKnowledgeText = getSexKnowledgeText(currentKnowledge)\n local nextKnowledgeText = getSexKnowledgeText(currentKnowledge + 1)\n\n table.insert(lines, \"▷ [This situation must become the trigger through which the heroine's sexual knowledge advances from \\\"\" .. currentKnowledgeText .. \"\\\" to \\\"\" .. nextKnowledgeText .. \"\\\"]\")\n end\n elseif encounterTypeValue == constPhase.performance then\n table.insert(lines, \"▷ [Describe naturally how the heroine comes to be placed in a situation where she is expected to sexually satisfy observers through performance alone, without any physical contact.]\")\n table.insert(lines, \"▷ [Describe the encounter and set it up. The heroine is placed in a situation where physical contact from the encounter is impossible and must not plausibly become possible. The response must end before it properly begins, before the heroine starts any sexual act by herself, and before the encounter begins demanding more or sexually harassing her in earnest.]\")\n\n elseif encounterTypeValue == constPhase.exhibitionism then\n table.insert(lines, \"▷ [Describe naturally how the heroine comes to be in such an exhibitionistic situation.]\")\n table.insert(lines, \"▷ [The encounter may exist nearby but remains completely unaware of the heroine's exhibitionistic behavior, and the heroine may or may not notice the encounter's presence, nearness, or signs of movement. Whether she notices the encounter or not, she cannot or does not stop her exhibitionistic behavior because of it.]\")\n table.insert(lines, \"▷ [The response must end before the heroine's exhibitionistic act properly begins.]\")\n\n elseif encounterTypeValue == constPhase.promiscuity then\n table.insert(lines, \"▷ [Describe naturally how the heroine comes to enter such a voluntary sexual situation, whether openly, discreetly, anonymously, under a concealed identity or alias, or while outwardly denying or disguising her intentions through another pretext. Regardless of the outward framing, her true underlying purpose must be her own sexual gratification, including the satisfaction of her sexual desires, needs, curiosity, pleasure, specific kinks, or sexual preferences. It must lead to a situation in which the heroine and the encounter engage in sex.]\")\n table.insert(lines, buildEncounterPreparationLine(\"sex\"))\n table.insert(lines, \"▷ [The response must end before any direct sexual contact occurs or the sexual act itself properly begins.]\")\n\n elseif encounterTypeValue == constPhase.intimacy then\n table.insert(lines, \"▷ [Describe naturally how the heroine enters a sexual situation with the encounter, whether willingly, eagerly, invitingly, or hesitantly. It must lead to a situation in which the heroine and the encounter engage in sex.]\")\n table.insert(lines, buildEncounterPreparationLine(\"sex\"))\n table.insert(lines, \"▷ [The response must end before any direct sexual contact occurs or the sexual act itself properly begins.]\")\n\n elseif encounterTypeValue == constPhase.hypnosis then\n table.insert(lines, \"▷ [Describe naturally how the heroine comes to be placed under the encounter's mental influence.]\")\n table.insert(lines, buildEncounterPreparationLine(\"sexual violation\"))\n table.insert(lines, \"▷ [The encounter moves to sexually exploit the heroine through that influence, but the response must end before any direct sexual contact occurs or any sexual act begins.]\")\n\n elseif encounterTypeValue == constPhase.rescue then\n table.insert(lines, \"▷ [Describe naturally how the heroine becomes incapacitated and unable to offer meaningful resistance.]\")\n table.insert(lines, buildEncounterPreparationLine(\"sexual violation\"))\n table.insert(lines, \"▷ [Describe the heroine just before direct sexual contact begins, with the encounter about to force the situation further, when the rescuers appear and naturally interrupt it, shifting the situation into the opening of a rescue attempt.]\")\n table.insert(lines, \"▷ [The response must end with the rescuers interrupting the imminent sexual violation, before the situation develops into direct combat or physical confrontation.]\")\n table.insert(lines, \"▷ [Throughout this response, the heroine must remain incapacitated and unable to offer meaningful resistance, and that state must be maintained until the response ends.]\")\n\n elseif encounterTypeValue == constPhase.masturbation_Story then\n table.insert(lines, \"▷ [Describe naturally the situation and circumstances that lead the heroine toward masturbating, taking into account her personality, sexual knowledge, sexual values, current situation, and other relevant factors.]\")\n table.insert(lines, \"▷ [If any encounter, person, or other presence is already nearby, present, or watching, they may only watch. They must not touch the heroine, restrain her, assist her, guide her body, or otherwise make any direct physical contact.]\")\n table.insert(lines, \"▷ [Only in highly exceptional circumstances where the heroine's situation is so severely restrictive that securing privacy or moving away is realistically impossible may someone else still be nearby or present. Even in such a case, they must not touch the heroine in any way, must not make any physical contact with her, and may only watch.]\")\n table.insert(lines, \"▷ [The heroine must not begin masturbating yet, and the response must end before she starts any direct sexual act or directly touches her body in a sexual way.]\")\n\n elseif isIncapacitatedPhase(triggerId, encounterTypeValue) then\n\n local encounterSexPhrase = \"sexual violation\"\n\n if encounterTypeValue == constPhase.event_Violence\n or encounterTypeValue == constPhase.event_Sleeping\n or encounterTypeValue == constPhase.event_Immobilized\n or encounterTypeValue == constPhase.event_Aphrodisiac\n or encounterTypeValue == constPhase.rape then\n\n table.insert(lines, \"▷ [Describe the heroine being placed in a situation where she faces sexual violence while unable to offer meaningful resistance.]\")\n\n if encounterTypeValue == constPhase.event_Sleeping then\n table.insert(lines, \"▷ [The heroine must remain deeply asleep and unaware throughout this response.]\")\n elseif encounterTypeValue == constPhase.event_Immobilized then\n table.insert(lines, \"▷ [Describe how the heroine becomes immobilized while remaining conscious through paralysis, drugging, numbing agents, or similar means, not through physical restraint.]\")\n elseif encounterTypeValue == constPhase.event_Aphrodisiac then\n table.insert(lines, \"▷ [Describe how and why the heroine is exposed to the encounter's aphrodisiac influence.]\")\n elseif encounterTypeValue == constPhase.rape then\n table.insert(lines, \"▷ [Describe how the heroine is rendered unable to meaningfully resist by the encounter, and establish that helpless state.]\")\n end\n\n elseif encounterTypeValue == constPhase.event_Coercion then\n table.insert(lines, \"▷ [Describe the heroine being placed in a situation where she has no choice but to sexually serve, please, or satisfy the encounter.]\")\n\n elseif encounterTypeValue == constPhase.event_Brainwashing then\n table.insert(lines, \"▷ [Describe how the heroine is completely dominated by the encounter's mental control, fully conscious yet unable to govern either her own speech or her body's actions according to her own will.]\")\n table.insert(lines, \"▷ [The heroine must remain under complete mental control throughout this response, conscious but unable to act according to her own will or disobey commands.]\")\n\n elseif encounterTypeValue == constPhase.transaction then\n local transactionText = \"▷ [Describe how the heroine comes to be in a situation where sexual service, performance, or gratification is exchanged for some benefit, whether voluntarily, consensually, reluctantly, or through circumstance.\"\n\n local lewdnessLevel = getLewdnessLevel(triggerId)\n if lewdnessLevel >= 11 then\n transactionText = transactionText .. \" The promised benefit becomes secondary to the anticipation, pleasure, curiosity, or desire the exchange itself stirs in her.\"\n elseif lewdnessLevel >= 9 then\n transactionText = transactionText .. \" Even if framed as practical, reluctant, or circumstantial, the exchange also carries a personal pull for her, with the heroine privately drawn to the anticipation, pleasure, curiosity, or desire involved.\"\n elseif lewdnessLevel >= 7 then\n transactionText = transactionText .. \" The exchange may also carry a personal pull for her, with the heroine privately drawn to the anticipation, pleasure, curiosity, or desire involved.\"\n end\n\n transactionText = transactionText .. \"]\"\n\n table.insert(lines, transactionText)\n encounterSexPhrase = \"transactional sex\"\n end\n\n table.insert(lines, buildEncounterPreparationLine(encounterSexPhrase))\n table.insert(lines, \"▷ [The response must end before any direct sexual contact occurs or any sexual act begins.]\")\n insertSexualActRequirementLine(lines, encounterTypeValue)\n end\n\n if storyTypeValue == constStoryType.core then\n if stageValue == 1 then\n table.insert(lines, \"▷ [This encounter belongs to the opening of the current storyline.]\")\n elseif stageValue == 2 then\n table.insert(lines, \"▷ [This encounter belongs to the middle escalation of the current storyline.]\")\n elseif stageValue >= 3 then\n table.insert(lines, \"▷ [This encounter belongs to the climax of the current storyline.]\")\n end\n end\n\n return table.concat(lines, \"\\n\")\n end\n\n -- ==========================================\n -- 항상 활성화 이벤트\n -- ==========================================\n if choiceType == \"toggleAlways\" then\n local key = trim(tostring(parts[3] or \"\"))\n if key == \"\" then\n return\n end\n\n local currentKey = trim(safeGet(triggerId, \"vg_Choice_Always\", \"\"))\n\n if currentKey == key then\n setChatVar(triggerId, \"vg_Choice_Always\", \"\")\n else\n setChatVar(triggerId, \"vg_Choice_Always\", key)\n end\n\n generateHTML(triggerId)\n reloadDisplay(triggerId)\n return\n\n -- ==========================================\n -- 챌린지 모드\n -- ==========================================\n elseif choiceType == \"setChallengeHP\" then\n local value = tonumber(parts[3]) or 1\n local currentValue = tonumber(safeGet(triggerId, \"vg_Challenge_HP\", \"1\")) or 1\n\n -- 같은 버튼 다시 누르면 해제\n if currentValue == value then\n value = 1\n end\n\n setChatVar(triggerId, \"vg_Challenge_HP\", tostring(value))\n\n generateHTML(triggerId)\n reloadDisplay(triggerId)\n return\n\n elseif choiceType == \"setChallengeStat\" then\n local value = tonumber(parts[3]) or 1\n local currentValue = tonumber(safeGet(triggerId, \"vg_Challenge_Stat\", \"1\")) or 1\n\n -- 같은 버튼 다시 누르면 해제\n if currentValue == value then\n value = 1\n end\n\n setChatVar(triggerId, \"vg_Challenge_Stat\", tostring(value))\n\n generateHTML(triggerId)\n reloadDisplay(triggerId)\n return\n\n -- ==========================================\n -- 신음 지시문\n -- ==========================================\n elseif choiceType == \"setMoanDirective\" then\n local value = tostring(parts[3] or \"1\")\n\n setChatVar(triggerId, \"vg_MoanDirective\", value)\n\n generateHTML(triggerId)\n reloadDisplay(triggerId)\n return\n\n -- ==========================================\n -- 인간 비살상 지침\n -- ==========================================\n elseif choiceType == \"setNonlethalHuman\" then\n local value = tostring(parts[3] or \"1\")\n\n setChatVar(triggerId, \"vg_NonlethalHuman\", value)\n\n generateHTML(triggerId)\n reloadDisplay(triggerId)\n return\n\n -- ==========================================\n -- 마법소녀 변신 지침\n -- ==========================================\n elseif choiceType == \"setMagicalGirlTransform\" then\n local value = tostring(parts[3] or \"0\")\n\n setChatVar(triggerId, \"vg_MagicalGirlTransform\", value)\n\n generateHTML(triggerId)\n reloadDisplay(triggerId)\n return\n end\n\n -- ==========================================\n -- 보상 선택\n -- ==========================================\n if choiceType == \"selectRewardAction\"\n or choiceType == \"selectRewardStat\"\n or choiceType == \"selectRewardAttribute\"\n or choiceType == \"selectRewardRecovery\"\n or choiceType == \"selectRewardUpgrade\"\n or choiceType == \"selectRewardRemove\"\n or choiceType == \"selectRewardArousalDown\" then\n\n local function getPendingRewardCount(triggerId)\n return math.max(tonumber(safeGet(triggerId, \"vg_Reward_Count\", \"0\")) or 0, 0)\n end\n\n local function setPendingRewardCount(triggerId, count)\n count = math.max(tonumber(count) or 0, 0)\n setChatVar(triggerId, \"vg_Reward_Count\", tostring(count))\n end\n\n local function appendRewardLog(triggerId, text)\n text = trim(tostring(text or \"\"))\n\n if text == \"\" then\n return\n end\n\n local rewardLogRaw = safeGet(triggerId, \"vg_Reward_Log\", \"\")\n setChatVar(triggerId, \"vg_Reward_Log\", appendPipe(rewardLogRaw, text))\n end\n\n local function getRewardBaseKeyExact(rewardKey)\n rewardKey = trim(tostring(rewardKey or \"\"))\n return rewardKey\n end\n\n local function getRewardTypeKey(rewardKey)\n rewardKey = trim(tostring(rewardKey or \"\"))\n\n if rewardKey == \"\" then\n return \"\"\n end\n\n local base = rewardKey:match(\"^(.-):\")\n if base and trim(base) ~= \"\" then\n return trim(base)\n end\n\n local parts = splitSection(rewardKey)\n return trim(parts[1] or rewardKey)\n end\n\n local function isSingleTypeReward(rewardTypeKey)\n return rewardTypeKey == \"Reward Stat\"\n or rewardTypeKey == \"Reward Attribute\"\n or rewardTypeKey == \"Reward Recovery\"\n end\n\n local function canSelectReward(triggerId, rewardKey)\n rewardKey = trim(tostring(rewardKey or \"\"))\n\n if rewardKey == \"\" then\n return false\n end\n\n local pendingCount = getPendingRewardCount(triggerId)\n\n if pendingCount <= 0 then\n alertError(triggerId, \"❌ 선택 가능한 보상이 없습니다.\")\n return false\n end\n\n local targetTypeKey = getRewardTypeKey(rewardKey)\n local targetExactKey = getRewardBaseKeyExact(rewardKey)\n\n local selectedRaw = trim(safeGet(triggerId, \"vg_Reward_Selected\", \"\"))\n local selectedList = splitPipe(selectedRaw)\n\n for _, oldKey in ipairs(selectedList) do\n oldKey = trim(tostring(oldKey or \"\"))\n\n if oldKey ~= \"\" then\n local oldTypeKey = getRewardTypeKey(oldKey)\n local oldExactKey = getRewardBaseKeyExact(oldKey)\n\n if isSingleTypeReward(targetTypeKey) then\n if oldTypeKey == targetTypeKey then\n alertError(triggerId, \"❌ 같은 타입의 보상은 하나만 선택할 수 있습니다.\")\n return false\n end\n else\n if oldExactKey == targetExactKey then\n alertError(triggerId, \"❌ 이미 선택한 보상입니다.\")\n return false\n end\n end\n end\n end\n\n return true\n end\n\n local function buildMeterRewardLog(emoji, currentValue, currentMax, nextValue, nextMax)\n currentValue = roundToTenth(tonumber(currentValue) or 0)\n currentMax = roundToTenth(tonumber(currentMax) or 0)\n nextValue = roundToTenth(tonumber(nextValue) or 0)\n nextMax = roundToTenth(tonumber(nextMax) or 0)\n\n if currentValue == nextValue and currentMax == nextMax then\n return \"\"\n end\n\n local deltaValue = roundToTenth(nextValue - currentValue)\n local suffix = \"\"\n\n if deltaValue ~= 0 then\n suffix = \" (\" .. getSignedEffectText(deltaValue) .. \")\"\n end\n\n return emoji .. \" \"\n .. formatFloat(nextValue) .. \" / \" .. formatFloat(nextMax)\n .. suffix\n end\n\n local function buildAttributeRewardLog(emoji, currentValue, nextValue)\n currentValue = roundToTenth(tonumber(currentValue) or 0)\n nextValue = roundToTenth(tonumber(nextValue) or 0)\n\n if currentValue == nextValue then\n return \"\"\n end\n\n local delta = roundToTenth(nextValue - currentValue)\n\n return emoji .. \" \"\n .. formatFloat(currentValue)\n .. \" → \"\n .. formatFloat(nextValue)\n .. \" (\" .. getSignedEffectText(delta) .. \")\"\n end\n\n local function finalizeRewardLogs(triggerId)\n local currentHeroineStat = loadDualFlagCurrentMap(triggerId, vgHeroineStat)\n local nextHeroineStat = loadDualFlagNextMap(triggerId, vgHeroineStat)\n\n local rewardLines = {}\n\n local hpLine = buildMeterRewardLog(\n \"❤️\",\n currentHeroineStat.hp,\n currentHeroineStat.hp_Max,\n nextHeroineStat.hp,\n nextHeroineStat.hp_Max\n )\n if hpLine ~= \"\" then\n table.insert(rewardLines, hpLine)\n end\n\n local resourceLine = buildMeterRewardLog(\n \"⚡\",\n currentHeroineStat.resource,\n currentHeroineStat.resource_Max,\n nextHeroineStat.resource,\n nextHeroineStat.resource_Max\n )\n if resourceLine ~= \"\" then\n table.insert(rewardLines, resourceLine)\n end\n\n local arousalLine = buildMeterRewardLog(\n \"💗\",\n currentHeroineStat.arousal,\n currentHeroineStat.arousal_Max,\n nextHeroineStat.arousal,\n nextHeroineStat.arousal_Max\n )\n if arousalLine ~= \"\" then\n table.insert(rewardLines, arousalLine)\n end\n\n local strengthLine = buildAttributeRewardLog(\n \"👊\",\n currentHeroineStat.strength,\n nextHeroineStat.strength\n )\n if strengthLine ~= \"\" then\n table.insert(rewardLines, strengthLine)\n end\n\n local reflexLine = buildAttributeRewardLog(\n \"🦶\",\n currentHeroineStat.reflex,\n nextHeroineStat.reflex\n )\n if reflexLine ~= \"\" then\n table.insert(rewardLines, reflexLine)\n end\n\n local willpowerLine = buildAttributeRewardLog(\n \"🧠\",\n currentHeroineStat.willpower,\n nextHeroineStat.willpower\n )\n if willpowerLine ~= \"\" then\n table.insert(rewardLines, willpowerLine)\n end\n\n local currentLevel = getHeroineLevel(triggerId, false)\n local nextLevel = getHeroineLevel(triggerId, true)\n\n if nextLevel > currentLevel then\n table.insert(rewardLines, \"🎉 LEVEL UP! (Lv.\" .. tostring(nextLevel) .. \")\")\n end\n\n for _, line in ipairs(rewardLines) do\n appendRewardLog(triggerId, line)\n end\n end\n\n local function commitRewardSelection(triggerId, resultText, rewardKey)\n resultText = trim(tostring(resultText or \"\"))\n rewardKey = trim(tostring(rewardKey or \"\"))\n\n if rewardKey == \"\" then\n return\n end\n\n local selectedRaw = trim(safeGet(triggerId, \"vg_Reward_Selected\", \"\"))\n setChatVar(triggerId, \"vg_Reward_Selected\", appendPipe(selectedRaw, rewardKey))\n\n if resultText ~= \"\" then\n appendRewardLog(triggerId, resultText)\n end\n\n local pendingCount = getPendingRewardCount(triggerId)\n pendingCount = math.max(0, pendingCount - 1)\n setPendingRewardCount(triggerId, pendingCount)\n\n if pendingCount <= 0 then\n finalizeRewardLogs(triggerId)\n end\n\n buildCurrentState(triggerId)\n reloadDisplay(triggerId)\n end\n\n if choiceType == \"selectRewardAction\" then\n -- choice_selectRewardAction_$1_$2_$3\n -- [3]=typeCode, [4]=emoji, [5]=cardLevel\n local typeCode = tonumber(parts[3]) or 0\n local cardLevel = tonumber(parts[5]) or 0\n\n cardLevel = clamp(cardLevel, 1, 10)\n\n if typeCode <= 0 or cardLevel <= 0 then\n return\n end\n\n local rewardKey = \"Reward Action:\" .. tostring(typeCode) .. \":\" .. tostring(cardLevel)\n\n if not canSelectReward(triggerId, rewardKey) then\n return\n end\n\n local resultText = addCardToDeck(triggerId, typeCode, cardLevel)\n commitRewardSelection(triggerId, resultText, rewardKey)\n return\n end\n\n if choiceType == \"selectRewardUpgrade\" then\n -- choice_selectRewardUpgrade_$1_$2_$3_$4_$5\n -- [3]=code, [4]=emoji, [5]=current stars, [6]=next stars, [7]=level up\n local code = trim(parts[3] or \"\")\n local requestedLevelUp = clamp(tonumber(parts[7]) or 1, 1, 3)\n\n if code == \"\" then\n return\n end\n\n local rewardKey = \"Reward Upgrade:\" .. tostring(code)\n\n if not canSelectReward(triggerId, rewardKey) then\n return\n end\n\n local deckArray = splitPipe(safeGet(triggerId, \"vg_Deck_Base\", \"\"))\n local upgraded = false\n local oldLevel = 0\n local nextLevel = 0\n local actualLevelUp = 0\n local typeCode = 0\n\n for i, cardRaw in ipairs(deckArray) do\n local cardParts = splitSection(cardRaw)\n local cardCode = trim(cardParts[1] or \"\")\n\n if cardCode == code then\n oldLevel = tonumber(cardParts[3]) or 1\n typeCode = tonumber(cardParts[2]) or 0\n\n if oldLevel >= 10 then\n alertError(triggerId, \"❌ 이미 최대 레벨 카드입니다.\")\n return\n end\n\n nextLevel = clamp(oldLevel + requestedLevelUp, 1, 10)\n actualLevelUp = nextLevel - oldLevel\n\n if actualLevelUp <= 0 then\n alertError(triggerId, \"❌ 강화할 수 없는 카드입니다.\")\n return\n end\n\n cardParts[3] = tostring(nextLevel)\n deckArray[i] = table.concat(cardParts, \"§\")\n upgraded = true\n break\n end\n end\n\n if not upgraded then\n alertError(triggerId, \"❌ 강화할 카드를 찾지 못했습니다.\")\n return\n end\n\n setChatVar(triggerId, \"vg_Deck_Base\", table.concat(deckArray, \"|\"))\n\n local resultText =\n getCardTypeEmoji(triggerId, typeCode) .. \" \"\n .. getCardLevelStars(oldLevel)\n .. \" → \"\n .. getCardLevelStars(nextLevel)\n .. \" 카드로 강화되었습니다.\"\n\n commitRewardSelection(triggerId, resultText, rewardKey)\n return\n end\n\n if choiceType == \"selectRewardRemove\" then\n -- 예: choice_selectRewardRemove_$1_$2_$3\n -- [3]=code, [4]=emoji, [5]=level\n local code = trim(parts[3] or \"\")\n local rewardKey = \"Reward Remove:\" .. code\n\n if code == \"\" then\n return\n end\n\n if not canSelectReward(triggerId, rewardKey) then\n return\n end\n\n local deckArray = splitPipe(safeGet(triggerId, \"vg_Deck_Base\", \"\"))\n local removed = false\n local removedTypeCode = 0\n local removedLevel = 0\n\n for index, cardRaw in ipairs(deckArray) do\n local cardParts = splitSection(cardRaw)\n local cardCode = trim(cardParts[1] or \"\")\n\n if cardCode == code then\n removedTypeCode = tonumber(cardParts[2]) or 0\n removedLevel = tonumber(cardParts[3]) or 1\n\n table.remove(deckArray, index)\n removed = true\n break\n end\n end\n\n if not removed then\n alertError(triggerId, \"❌ 제거할 카드를 찾지 못했습니다.\")\n return\n end\n\n setChatVar(triggerId, \"vg_Deck_Base\", table.concat(deckArray, \"|\"))\n\n local resultText =\n getCardTypeEmoji(triggerId, removedTypeCode, constPhase.combat)\n .. \" \"\n .. getCardLevelStars(removedLevel)\n .. \" 카드 1장이 제거되었습니다.\"\n\n commitRewardSelection(triggerId, resultText, rewardKey)\n return\n end\n\n if choiceType == \"selectRewardAttribute\" then\n -- choice_selectRewardAttribute_$1_$2_$3\n -- [3]=Strength gain, [4]=Reflex gain, [5]=Willpower gain\n local strengthGain = roundToTenth(tonumber(parts[3]) or 0)\n local reflexGain = roundToTenth(tonumber(parts[4]) or 0)\n local willpowerGain = roundToTenth(tonumber(parts[5]) or 0)\n\n if strengthGain == 0 and reflexGain == 0 and willpowerGain == 0 then\n return\n end\n\n local rewardKey =\n \"Reward Attribute:\"\n .. formatFloat(strengthGain) .. \":\"\n .. formatFloat(reflexGain) .. \":\"\n .. formatFloat(willpowerGain)\n\n if not canSelectReward(triggerId, rewardKey) then\n return\n end\n\n local nextHeroineStat = loadDualFlagNextMap(triggerId, vgHeroineStat)\n\n local newStrength = roundToTenth((tonumber(nextHeroineStat.strength) or 0) + strengthGain)\n local newReflex = roundToTenth((tonumber(nextHeroineStat.reflex) or 0) + reflexGain)\n local newWillpower = roundToTenth((tonumber(nextHeroineStat.willpower) or 0) + willpowerGain)\n\n batchSetDualFlags(triggerId, vgHeroineStat.varName, {\n { position = vgHeroineStat.strength, value = tostring(newStrength), isNext = true },\n { position = vgHeroineStat.reflex, value = tostring(newReflex), isNext = true },\n { position = vgHeroineStat.willpower, value = tostring(newWillpower), isNext = true },\n })\n\n commitRewardSelection(triggerId, \"\", rewardKey)\n return\n end\n\n if choiceType == \"selectRewardStat\" then\n -- choice_selectRewardStat_$1_$2_$3\n -- [3]=HP gain, [4]=Resource gain, [5]=Arousal gain\n local hpGain = extractInt(parts[3])\n local resourceGain = extractInt(parts[4])\n local arousalGain = extractInt(parts[5])\n\n if hpGain == 0 and resourceGain == 0 and arousalGain == 0 then\n return\n end\n\n local rewardKey =\n \"Reward Stat:\"\n .. tostring(hpGain) .. \":\"\n .. tostring(resourceGain) .. \":\"\n .. tostring(arousalGain)\n\n if not canSelectReward(triggerId, rewardKey) then\n return\n end\n\n local nextHeroineStat = loadDualFlagNextMap(triggerId, vgHeroineStat)\n\n local nextHP = tonumber(nextHeroineStat.hp) or 0\n local nextMaxHP = tonumber(nextHeroineStat.hp_Max) or 0\n local nextResource = tonumber(nextHeroineStat.resource) or 0\n local nextMaxResource = tonumber(nextHeroineStat.resource_Max) or 0\n local nextArousal = tonumber(nextHeroineStat.arousal) or 0\n local nextMaxArousal = tonumber(nextHeroineStat.arousal_Max) or 0\n\n local newMaxHP = nextMaxHP + hpGain\n local newHP = clamp(nextHP + hpGain, 0, newMaxHP)\n\n local newMaxResource = nextMaxResource + resourceGain\n local newResource = clamp(nextResource + resourceGain, 0, newMaxResource)\n\n local newMaxArousal = nextMaxArousal + arousalGain\n local newArousal = nextArousal\n\n batchSetDualFlags(triggerId, vgHeroineStat.varName, {\n { position = vgHeroineStat.hp, value = tostring(newHP), isNext = true },\n { position = vgHeroineStat.hp_Max, value = tostring(newMaxHP), isNext = true },\n { position = vgHeroineStat.resource, value = tostring(newResource), isNext = true },\n { position = vgHeroineStat.resource_Max, value = tostring(newMaxResource), isNext = true },\n { position = vgHeroineStat.arousal, value = tostring(newArousal), isNext = true },\n { position = vgHeroineStat.arousal_Max, value = tostring(newMaxArousal), isNext = true },\n })\n\n commitRewardSelection(triggerId, \"\", rewardKey)\n return\n end\n\n if choiceType == \"selectRewardRecovery\" then\n -- choice_selectRewardRecovery_$1_$2_$3\n -- [3]=표시문구, [4]=HP 회복량, [5]=Resource 회복량\n local hpRecovery = roundToTenth(tonumber(parts[4]) or 0)\n local resourceRecovery = roundToTenth(tonumber(parts[5]) or 0)\n\n local rewardKey = \"Reward Recovery\"\n\n if not canSelectReward(triggerId, rewardKey) then\n return\n end\n\n local nextHeroineStat = loadDualFlagNextMap(triggerId, vgHeroineStat)\n\n local nextHP = tonumber(nextHeroineStat.hp) or 0\n local nextMaxHP = tonumber(nextHeroineStat.hp_Max) or 0\n local nextResource = tonumber(nextHeroineStat.resource) or 0\n local nextMaxResource = tonumber(nextHeroineStat.resource_Max) or 0\n\n local newHP = clamp(nextHP + hpRecovery, 0, nextMaxHP)\n local newResource = clamp(nextResource + resourceRecovery, 0, nextMaxResource)\n\n batchSetDualFlags(triggerId, vgHeroineStat.varName, {\n { position = vgHeroineStat.hp, value = tostring(newHP), isNext = true },\n { position = vgHeroineStat.resource, value = tostring(newResource), isNext = true },\n })\n\n commitRewardSelection(triggerId, \"\", rewardKey)\n return\n end\n\n if choiceType == \"selectRewardArousalDown\" then\n -- choice_selectRewardArousalDown_$1_$2\n -- [3]=표시문구, [4]=Arousal 감소량\n local arousalReduce = roundToTenth(tonumber(parts[4]) or 0)\n\n if arousalReduce <= 0 then\n return\n end\n\n local rewardKey =\n \"Reward ArousalDown:\"\n .. formatFloat(arousalReduce)\n\n if not canSelectReward(triggerId, rewardKey) then\n return\n end\n\n local nextHeroineStat = loadDualFlagNextMap(triggerId, vgHeroineStat)\n\n local nextArousal = tonumber(nextHeroineStat.arousal) or 0\n local nextMaxArousal = tonumber(nextHeroineStat.arousal_Max) or 0\n\n local newArousal = clamp(nextArousal - arousalReduce, 0, nextMaxArousal)\n\n batchSetDualFlags(triggerId, vgHeroineStat.varName, {\n { position = vgHeroineStat.arousal, value = tostring(newArousal), isNext = true },\n })\n\n commitRewardSelection(triggerId, \"\", rewardKey)\n return\n end\n end\n\n if choiceType ~= \"selectHand\" and choiceType ~= \"selectGuard\" and choiceType ~= \"selectHEventTimePass\" and choiceType ~= \"selectAftermathMessage\" then\n local rewardCount = tonumber(safeGet(triggerId, \"vg_Reward_Count\", \"0\")) or 0\n\n if rewardCount > 0 then\n alertNormal(triggerId, \"🎁 먼저 보상을 선택하세요.\")\n return\n end\n end\n\n -- ==========================================\n -- 강화 선택\n -- ==========================================\n if choiceType == \"selectEnhancement\" then\n\n if trim(safeGet(triggerId, \"vg_Enhancement_Select\", \"\")) ~= \"\" then\n return\n end\n\n local enhancementDisplay = tostring(parts[3] or \"\")\n local enhancementName = tostring(parts[4] or \"\")\n local enhancementType = tostring(parts[5] or \"\")\n local summaryText = tostring(parts[6] or \"\")\n local effectRaw1 = tostring(parts[7] or \"\")\n local effectRaw2 = tostring(parts[8] or \"\")\n local slotValue = tonumber(parts[9]) or 0\n\n if enhancementName == \"\" or effectRaw1 == \"\" or effectRaw2 == \"\" then\n return\n end\n\n if slotValue ~= 1 and slotValue ~= 2 and slotValue ~= 3 then\n return\n end\n\n if slotValue == 1 then\n batchSetDualFlags(triggerId, vgEnhancement.varName, {\n { position = vgEnhancement.slot1_Name, value = enhancementName, isNext = true },\n { position = vgEnhancement.slot1_Effect1, value = effectRaw1, isNext = true },\n { position = vgEnhancement.slot1_Effect2, value = effectRaw2, isNext = true },\n })\n elseif slotValue == 2 then\n batchSetDualFlags(triggerId, vgEnhancement.varName, {\n { position = vgEnhancement.slot2_Name, value = enhancementName, isNext = true },\n { position = vgEnhancement.slot2_Effect1, value = effectRaw1, isNext = true },\n { position = vgEnhancement.slot2_Effect2, value = effectRaw2, isNext = true },\n })\n elseif slotValue == 3 then\n batchSetDualFlags(triggerId, vgEnhancement.varName, {\n { position = vgEnhancement.slot3_Name, value = enhancementName, isNext = true },\n { position = vgEnhancement.slot3_Effect1, value = effectRaw1, isNext = true },\n { position = vgEnhancement.slot3_Effect2, value = effectRaw2, isNext = true },\n })\n end\n\n local obtainedText = \"▷ [(\" .. enhancementType .. \") \" .. enhancementName .. \" has been obtained. Only describe it directly if it would naturally appear as something material, such as an item or equipment; otherwise, do not mention its name directly, and instead express it indirectly through the change in her.]\"\n setChatVar(triggerId, \"vg_Enhancement_Select\", obtainedText)\n\n local cl = getChatLength(triggerId)\n if cl > 0 then\n local last = getChat(triggerId, cl - 1)\n\n if last and last.role == \"user\" and type(last.data) == \"string\" then\n local lastText = tostring(last.data or \"\")\n\n if not lastText:find(obtainedText, 1, true) then\n lastText = lastText .. \"\\n\" .. obtainedText\n setChat(triggerId, cl - 1, lastText)\n end\n end\n end\n\n reloadDisplay(triggerId)\n return\n end\n\n -- ==========================================\n -- 스토리 / 어드밴스 선택\n -- ==========================================\n if choiceType == \"selectStory\" or choiceType == \"selectAdvance\" then\n local dateValue = tostring(parts[3] or \"\")\n local storyEmojiValue = tostring(parts[4] or \"\")\n local storyTypeValue = tonumber(parts[5]) or 0\n local situationValue = tostring(parts[6] or \"\")\n local encounterEmojiValue = tostring(parts[7] or \"\")\n local encounterValue = tostring(parts[8] or \"\")\n local encounterTypeValue = tonumber(parts[9]) or 0\n local difficultyEmojiValue = tostring(parts[10] or \"\")\n local difficultyValue = tonumber(parts[11]) or 0\n local levelValue = tonumber(parts[12]) or 1\n local encounterTraitValue = tonumber(parts[13]) or constEncounterTrait.none\n\n clearNextDualFlag(triggerId, vgHeroineStat.varName)\n\n setChatVar(triggerId, \"vg_Mode\", tostring(constMode.vg))\n setChatVar(triggerId, \"vg_Mode_Save\", \"0\")\n\n if choiceType == \"selectStory\" then\n initSessionInfo(triggerId, storyTypeValue, encounterValue, levelValue, difficultyValue, encounterTypeValue, encounterTraitValue)\n elseif choiceType == \"selectAdvance\" then\n initEncounterInfo(triggerId, encounterValue, levelValue, difficultyValue, encounterTypeValue, encounterTraitValue)\n end\n\n local stageValue = tonumber(getResolvedDualFlag(triggerId, vgSessionInfo.varName, vgSessionInfo.stage, 1)) or 1\n local specialStateMax = 2\n if stageValue >= 2 then\n specialStateMax = 1\n end\n\n local statusEffectLogText = \"\"\n\n if encounterTypeValue == constPhase.event_Sleeping or encounterTypeValue == constPhase.sleeping_Ambush then\n setDualFlag(triggerId, vgHeroineEffect.varName, vgHeroineEffect.sleep, tostring(math.random(1, specialStateMax)), false)\n statusEffectLogText = \"상태이상: 수면\"\n\n elseif encounterTypeValue == constPhase.entrapment or encounterTypeValue == constPhase.event_Immobilized then\n setDualFlag(triggerId, vgHeroineEffect.varName, vgHeroineEffect.immobilized, tostring(math.random(1, specialStateMax)), false)\n statusEffectLogText = \"상태이상: 마비\"\n\n elseif encounterTypeValue == constPhase.brainwashing or encounterTypeValue == constPhase.event_Brainwashing then\n setDualFlag(triggerId, vgHeroineEffect.varName, vgHeroineEffect.brainwashed, tostring(math.random(1, specialStateMax)), false)\n statusEffectLogText = \"상태이상: 세뇌\"\n\n elseif encounterTypeValue == constPhase.abortion_Ambush then\n setDualFlag(triggerId, vgHeroineEffect.varName, vgHeroineEffect.immobilized, \"2\", false)\n statusEffectLogText = \"상태이상: 마비\"\n\n elseif encounterTypeValue == constPhase.aphrodisiac or encounterTypeValue == constPhase.event_Aphrodisiac then\n local heroineStat = loadDualFlagNextMap(triggerId, vgHeroineStat)\n\n local currentArousal = tonumber(heroineStat.arousal) or 0\n local maxArousal = tonumber(heroineStat.arousal_Max) or 0\n\n if maxArousal > 0 then\n local aphrodisiacMultiplier = 1\n\n if encounterTypeValue == constPhase.event_Aphrodisiac then\n aphrodisiacMultiplier = 2\n end\n\n local missingArousal = math.max(0, maxArousal - currentArousal)\n\n local arousalGain = math.max(\n missingArousal * 0.4 * aphrodisiacMultiplier,\n maxArousal * 0.2 * aphrodisiacMultiplier\n )\n\n local nextArousal = clamp(roundToTenth(currentArousal + arousalGain), 0, maxArousal)\n\n setDualFlag(triggerId, vgHeroineStat.varName, vgHeroineStat.arousal, tostring(nextArousal), true)\n end\n\n statusEffectLogText = \"상태이상: 최음\"\n end\n\n setPhase(triggerId, tostring(encounterTypeValue))\n\n resetAndDrawPlayDeck(triggerId)\n buildCurrentState(triggerId)\n buildEncounterActionSetting(triggerId)\n\n buildSexualProfile(triggerId, dateValue)\n\n local inputText = buildEncounterSetupText(\n triggerId,\n dateValue,\n storyEmojiValue,\n storyTypeValue,\n situationValue,\n encounterEmojiValue,\n encounterValue,\n encounterTypeValue,\n difficultyEmojiValue,\n levelValue\n )\n\n if statusEffectLogText ~= \"\" then\n inputText = inputText .. \"\\n[Temporary Log: \" .. statusEffectLogText .. \"]\"\n end\n\n pushChoiceInput(inputText)\n return\n\n -- ==========================================\n -- 일상 선택\n -- ==========================================\n elseif choiceType == \"selectDaily\" then\n local situationValue = tostring(parts[3] or \"\")\n local emojiValue = tostring(parts[4] or \"\")\n\n setPhase(triggerId, tostring(constPhase.standby))\n\n resetPlayDeck(triggerId)\n buildCurrentState(triggerId)\n buildEncounterActionSetting(triggerId)\n\n local lines = {}\n\n table.insert(lines, \"[Daily Select: \" .. situationValue .. \" | Emoji: \" .. emojiValue .. \"]\")\n table.insert(lines, \"▷ [Describe the heroine moving naturally in the chosen direction within the small and everyday flow of her present life.]\")\n\n do\n local dailyRandomDirectives = {}\n local lewdnessLevel = getLewdnessLevel(triggerId)\n\n local function addDailyRandomDirective(id, text)\n table.insert(dailyRandomDirectives, {\n id = tostring(id),\n text = text\n })\n end\n\n local function pickUnusedDailyRandom()\n local varName = \"vg_Random_Daily\"\n\n local resetRatio = 0.8\n\n local validIdMap = {}\n local usedMap = {}\n local usedIds = {}\n local validCount = 0\n\n for _, entry in ipairs(dailyRandomDirectives) do\n local id = trim(tostring(entry.id or \"\"))\n local text = trim(tostring(entry.text or \"\"))\n\n if id ~= \"\" and text ~= \"\" and not validIdMap[id] then\n validIdMap[id] = true\n validCount = validCount + 1\n end\n end\n\n for _, rawId in ipairs(splitPipe(safeGet(triggerId, varName, \"\"))) do\n local id = trim(tostring(rawId or \"\"))\n\n if id ~= \"\" and validIdMap[id] and not usedMap[id] then\n usedMap[id] = true\n table.insert(usedIds, id)\n end\n end\n\n -- 추가된 Daily Random 중 일정 비율 이상을 이미 썼으면 리셋\n local resetThreshold = math.max(1, math.ceil(validCount * resetRatio))\n\n if validCount > 0 and #usedIds >= resetThreshold then\n usedMap = {}\n usedIds = {}\n end\n\n local candidates = {}\n\n for _, entry in ipairs(dailyRandomDirectives) do\n local id = trim(tostring(entry.id or \"\"))\n local text = trim(tostring(entry.text or \"\"))\n\n if id ~= \"\" and text ~= \"\" and not usedMap[id] then\n table.insert(candidates, entry)\n end\n end\n\n -- 안전장치: 그래도 후보가 없으면 전체 리셋\n if #candidates <= 0 then\n usedMap = {}\n usedIds = {}\n\n for _, entry in ipairs(dailyRandomDirectives) do\n local id = trim(tostring(entry.id or \"\"))\n local text = trim(tostring(entry.text or \"\"))\n\n if id ~= \"\" and text ~= \"\" then\n table.insert(candidates, entry)\n end\n end\n end\n\n local selected = candidates[math.random(#candidates)]\n local selectedId = trim(tostring(selected.id or \"\"))\n\n table.insert(usedIds, selectedId)\n setChatVar(triggerId, varName, table.concat(usedIds, \"|\"))\n\n return selected\n end\n\n addDailyRandomDirective(1, \"The response must include a meaningful meeting, interaction, or renewed contact with a character already established in the setting, but not usually part of the heroine's daily company.\")\n addDailyRandomDirective(2, \"The response must include a meaningful meeting, interaction, or renewed contact with a character already established in the setting whom the heroine already knows personally, whether as a friend, ally, acquaintance, companion, former contact, or someone she has met more than once.\")\n addDailyRandomDirective(3, \"The response must include news, a warning, or newly learned information suggesting a serious misfortune, hardship, or other deeply troubling situation affecting someone close to the heroine, or the heroine herself if no such close relation is presently applicable.\")\n addDailyRandomDirective(4, \"The response must include someone, something, some being, presence, force, group, or circumstance that may involve sexual curiosity, sexual desire, taboo-tinged interest, or some intention, design, or move directed toward the heroine in that vein, whether or not she is aware of it or understands it, and whether it appears directly before her or remains distant, hidden, indirect, or only implied.\")\n addDailyRandomDirective(5, \"The response must include the heroine directly or indirectly learning of a vague rumor, lead, or piece of information related to something she seeks, pursues, or is trying to understand, and it may be true, false, partial, or misleading.\")\n addDailyRandomDirective(6, \"The response must include a seemingly ordinary request, invitation, offer, opportunity, or arrangement that contains a sexual implication or could later place the heroine in a sexually vulnerable situation.\")\n addDailyRandomDirective(7, \"The response must include the heroine directly or indirectly becoming aware of a serious incident, accident, crisis, or upheaval connected to her.\")\n addDailyRandomDirective(8, \"The response must include sexual interest toward the heroine, whether or not she becomes aware of it, whether from an individual or a group.\")\n addDailyRandomDirective(9, \"The response must include the heroine being presented, directly or indirectly, with a favor, request, errand, task, delivery, confirmation, commission, or call for assistance that fits naturally into her present circumstances, whether minor or potentially important, without requiring any immediate commitment from her.\")\n addDailyRandomDirective(10, \"The response must include an already established character in the setting becoming relevant to a natural possibility that the heroine may later accompany them, join them, meet them again, or otherwise take part with them in some shared activity, outing, plan, or situation.\")\n addDailyRandomDirective(11, \"The response must include some subtle but unsettling sign, pattern, presence, atmosphere, or circumstance suggesting that something ominous, sexually charged, or taboo-tinged may be gathering around the heroine, whether or not she clearly notices or understands it.\")\n addDailyRandomDirective(12, \"The response must include the heroine briefly being treated as the target of a casual sexual proposition, one-night invitation, paid-sex implication, or similar approach, but the moment should simply pass without leading anywhere.\")\n addDailyRandomDirective(13, \"The response must include the heroine becoming aware of, hearing about, noticing, discovering, or otherwise encountering something that gives her a possible direction to consider, such as a minor goal, priority, concern, opportunity, or unresolved matter, without requiring her to commit to it yet.\")\n addDailyRandomDirective(14, \"The response must include a modest opportunity or option becoming available to the heroine, but with some uncertainty, inconvenience, cost, social complication, sexually charged risk, possible pressure on her boundaries, or need for later judgment attached to it.\")\n addDailyRandomDirective(15, \"The response must include the heroine hearing, overhearing, or otherwise being exposed to a rumor, report, piece of information, or casual mention concerning a character already established in the setting but not yet familiar, well-known, or personally close to her, without that character appearing directly in the scene.\")\n addDailyRandomDirective(16, \"The response must include a small but meaningful disagreement, tension, misunderstanding, rivalry, or conflict of interest between the heroine and a character already established in the setting, without immediately escalating into a major confrontation.\")\n addDailyRandomDirective(17, \"The response must include a character already established in the setting becoming involved in the heroine's present situation through coincidence, rumor, obligation, curiosity, or personal motive, without necessarily meeting her directly.\")\n addDailyRandomDirective(18, \"The response must include a character already established in the setting showing a changed attitude toward the heroine, without fully explaining why.\")\n addDailyRandomDirective(19, \"The response must include a character already established in the setting feeling or forming a negative emotion, judgment, or impression toward the heroine, such as resentment, suspicion, disappointment, jealousy, hostility, distrust, or discomfort.\")\n addDailyRandomDirective(20, \"The response must include the heroine being offered advice, warning, criticism, teasing, instruction, or an uncomfortable truth by a character already established in the setting, leaving her with something to think about afterward.\")\n addDailyRandomDirective(21, \"The response must include a character already established in the setting creating a subtle emotional pressure on the heroine through their attitude, words, expectations, dependence, suspicion, rivalry, or unspoken demand.\")\n addDailyRandomDirective(22, \"The response must include an already established character in the setting developing, showing, or renewing a personal interest in the heroine, whether through curiosity, concern, attraction, suspicion, obligation, or some other motive.\")\n addDailyRandomDirective(23, \"The response must include the heroine's lowered bodily condition adding subtle sexual tension or vulnerability to an otherwise ordinary daily-life situation.\")\n addDailyRandomDirective(24, \"The response must include a familiar or ordinary place in the heroine's daily life feeling subtly more sexual and improper than usual, whether or not the heroine notices, understands, misunderstands, or ignores the change.\")\n addDailyRandomDirective(25, \"The response must include the heroine becoming aware, directly or indirectly, that a specific group or social circle is divided in its view of her, with some members responding positively to her and others negatively, such as through admiration, defense, favor, desire, sympathy, contempt, blame, suspicion, mockery, resentment, rejection, condemnation, or similar attitudes.\")\n addDailyRandomDirective(26, \"The response must include someone taking an excessive interest in the heroine's private life.\")\n addDailyRandomDirective(27, \"The response must include someone stalking the heroine with sexual intent without the heroine clearly noticing or understanding it.\")\n addDailyRandomDirective(28, \"The response must include the heroine becoming aware of information, a thought, or a possibility suggesting that something she may want or need to do would be easier with the help or company of a character already established in the setting, without requiring any immediate contact or meeting.\")\n addDailyRandomDirective(29, \"The response must include the heroine receiving a small kindness or experiencing a small pleasant coincidence.\")\n addDailyRandomDirective(30, \"The response must include the heroine experiencing a small moment of rest, comfort, or relief within her ordinary daily life.\")\n addDailyRandomDirective(31, \"The response must include the heroine finding a modest everyday chance to recover physically or emotionally, with a restorative feeling such as calm, warmth, comfort, relief, or healing.\")\n addDailyRandomDirective(32, \"The response must include the heroine receiving a small sense of reassurance from something such as a person, place, object, memory, passing circumstance, or quiet realization.\")\n addDailyRandomDirective(33, \"The response must include a modest daily-life moment where the heroine feels gently supported by someone or something around her, such as through care, welcome, comfort, belonging, or quiet attention.\")\n addDailyRandomDirective(34, \"The response must include the heroine finding a small reason to keep going or feel better for the moment, such as endurance, a smile, comfort, relief, or a brief sense of ease.\")\n addDailyRandomDirective(35, \"The response must include a small comforting or positive daily-life moment involving or connected to a character already established in the setting.\")\n addDailyRandomDirective(36, \"The response must include the heroine feeling a small practical pressure related to sustaining her daily life, whether through resource strain, financial strain, scarcity, upkeep, or an equivalent concern suited to the setting.\")\n addDailyRandomDirective(37, \"The response must include the heroine noticing a small sign that her personal life, privacy, surroundings, or limited possessions may have been disturbed or interfered with, such as her underwear going missing, belongings showing signs of being searched, an unexplained stain, smear, or lingering unfamiliar scent, someone seeming to know part of her private routine, or another subtle sign of intrusion suited to her current circumstances.\")\n addDailyRandomDirective(38, \"The response must include a small disruption in the heroine's usual routine, habits, expectations, or ordinary daily flow.\")\n addDailyRandomDirective(39, \"The response must include the heroine gaining a small sense of growth from her recent experiences or daily life, such as realizing something, picking up a practical knack, making a small adjustment, or finding her own way to handle something.\")\n addDailyRandomDirective(40, \"The response must include a character already established in the setting offering the heroine some small form of emotional support, whether or not she accepts it, such as comfort, care, reassurance, or healing.\")\n addDailyRandomDirective(41, \"The response must include the heroine directly or indirectly learning that someone close to her, or an already established character connected to her, may be in danger, under pressure, restrained, missing, pursued, accused, coerced, or otherwise caught in a situation that gives her a possible reason to act later, rather than demanding an immediate response.\")\n addDailyRandomDirective(42, \"The response must include an already established organized force, group, institution, or authority taking notice of or showing interest in the heroine, whether openly or from afar, and whether or not she realizes it.\")\n addDailyRandomDirective(43, \"The response must include a subtle element that is not immediately dangerous, but could later increase the heroine's risk of facing a sexual threat, sexual pressure, exploitation, coercion, or sexual vulnerability.\")\n addDailyRandomDirective(44, \"The response must include the heroine being recorded, listed, profiled, reported, recommended, requested, flagged, or otherwise entered into a formal or informal record or selection process, whether she knows about it, later learns about it, or never finds out.\")\n addDailyRandomDirective(45, \"The response must include someone mistakenly assigning the heroine a role, identity, purpose, status, or significance, or expecting her to fulfill one, even though it does not actually fit her.\")\n addDailyRandomDirective(46, \"The response must include a physically repulsive or otherwise unpleasant person directly approaching, addressing, or speaking to the heroine while making a romantic or flirtatious advance toward her, clearly driven by overt sexual interest.\")\n addDailyRandomDirective(47, \"The response must include the heroine's home, shelter, usual sleeping place, refuge, or current place of rest becoming unavailable or unusable to her for the time being.\")\n addDailyRandomDirective(48, \"The response must include an already established group, faction, institution, organization, authority, or social circle showing a possible connection to the heroine through some indirect sign, trace, rumor, rule, member, request, or influence, whether she notices it or it remains outside her awareness.\")\n addDailyRandomDirective(49, \"The response must include the heroine becoming indirectly connected to a conflict, tension, or incompatible interest between two or more groups, factions, institutions, authorities, or social circles already established in the setting, whether she notices it or it remains outside her awareness.\")\n addDailyRandomDirective(50, \"The response must include some formal or informal rule, law, custom, policy, restriction, requirement, or expectation beginning to work against the heroine or placing her at a disadvantage.\")\n addDailyRandomDirective(51, \"The response must include the heroine noticing that a place, service, group, or social circle she might need to rely on has quietly changed its attitude, rules, prices, access, or expectations toward her for unclear reasons.\")\n addDailyRandomDirective(52, \"The response must include the heroine finding a small trace of trouble, absence, haste, secrecy, or unfinished business connected to an already established character, group, faction, institution, organization, authority, or social circle.\")\n addDailyRandomDirective(53, \"The response must include the heroine becoming aware that someone, some group, or some organized force has been asking around or collecting information about her habits, movements, relationships, reputation, needs, weaknesses, or private circumstances.\")\n\n if lewdnessLevel >= 3 then\n addDailyRandomDirective(301, \"The response must include the heroine directly or indirectly hearing, learning, or becoming aware of talk, reputation, judgment, or rumor concerning herself, or, if applicable, another identity or side of her not openly recognized as the same person, and it may be favorable, unfavorable, sexual, distorted, uncertain, or malicious.\")\n addDailyRandomDirective(302, \"The response must include an accidental sexual mishap where someone, despite having no sexual intent at first, directly touches, grabs, squeezes, presses against, or ends up face-first against the heroine's breasts, buttocks, crotch, or genitals. The contact must be obvious and compromising, not a vague brush, and must happen through an accident such as a fall, crowding, or mistaken handling.\")\n addDailyRandomDirective(303, \"The response must include a subtle change in the heroine's surrounding reputation or social atmosphere, making others regard her with increasingly lewd, leering, or improper interest, whether or not she clearly notices it.\")\n addDailyRandomDirective(304, \"The response must include an accidental or circumstantial exposure involving the heroine, where something compromising becomes visible to one or more people, such as her panties or underwear, genitals, buttocks, or breasts.\")\n addDailyRandomDirective(305, \"The response must include the heroine receiving, having delivered to her, or finding that someone has left for her a setting-appropriate sexual item from an unknown sender, such as revealing clothing, sexual underwear, an adult toy, or a fetish object.\")\n addDailyRandomDirective(306, \"The response must include the heroine receiving, having delivered to her, or finding that someone has left for her a setting-appropriate sexual message from an unknown sender.\")\n addDailyRandomDirective(307, \"The response must include someone giving the heroine an explicit sexual compliment.\")\n addDailyRandomDirective(308, \"The response must include the heroine overhearing explicit lewd talk about her from someone who does not realize she is there, without it being said directly to her.\")\n addDailyRandomDirective(309, \"The response must include a small hint, implication, or foothold that could later involve the heroine in a sexual problem.\")\n addDailyRandomDirective(310, \"The response must include something that stirs the heroine's insecurity about her appearance or sexuality, such as a comparison, disparaging remark, judgment, or implication.\")\n addDailyRandomDirective(311, \"The response must include the heroine, while in the same space or within earshot, directly hearing or accidentally overhearing someone reveal a warped fetish, abnormal sexual preference, or otherwise deviant fixation toward her, clearly distinct from ordinary attraction.\")\n addDailyRandomDirective(312, \"The response must include someone sexually targeting the heroine noticing, learning, or piecing together a pattern in her behavior, movements, surroundings, relationships, timing, or moments of vulnerability, through observation or deduction rather than direct intrusion.\")\n addDailyRandomDirective(313, \"The response must include someone offering or attempting to help the heroine in a way that seems benevolent on the surface, but secretly carries sexual interest, sexual intent, or a sexual motive toward her, whether or not she notices it.\")\n addDailyRandomDirective(314, \"The response must include a hidden sexually motivated scheme, maneuver, arrangement, or plan targeting the heroine beginning or progressing outside her awareness, whether by an individual, group, or other force, without requiring her to notice it yet.\")\n addDailyRandomDirective(315, \"The response must include the heroine becoming known by name, face, appearance, reputation, or identifying detail within a specific group, community, social circle, clientele, network, or subculture, whether or not she becomes aware of it.\")\n end\n\n if lewdnessLevel >= 5 then\n addDailyRandomDirective(501, \"The response must include someone or some party learning of, discovering, uncovering, or becoming suspicious of the heroine's private sexual weakness, sexual history, hidden desire, embarrassing sexual information, sensitive personal information, or concealed sexual secret that could later be used as sexual leverage, temptation, pressure, manipulation, or exploitation, whether through direct contact with her or without her knowledge.\")\n addDailyRandomDirective(502, \"The response must include an accidental sexual mishap where the heroine, with no sexual intent, ends up in explicit sexual contact with a male. It must be obvious and compromising, not a vague brush, and happen through an accident such as a fall, crowding, or mistaken handling (e.g., grabbing a male's penis, face buried between a male's crotch).\")\n addDailyRandomDirective(503, \"The response must include a small trace or echo of the heroine's past sexual experience or its aftermath resurfacing in her present daily life, such as through rumor, memory, someone's attitude, recognition, indirect social reaction, or another minor consequence.\")\n addDailyRandomDirective(504, \"The response must include a sexual misunderstanding involving the heroine, where she appears lewd, compromising, or sexually improper to others despite having no such intention.\")\n addDailyRandomDirective(505, \"The response must include someone directly asking the heroine about an exaggerated sexual rumor concerning her that is not accurate as stated, though it may be false, distorted, misunderstood, or partly close to reality by coincidence; her reaction should reflect her personality, situation, and the rumor's actual relation to the truth.\")\n addDailyRandomDirective(506, \"The response must include a character already established in the setting who personally knows the heroine unknowingly, unintentionally, or tactlessly bringing up something related to her sexual past, sexual reputation, hidden experience, vulnerability, or its aftermath, without malicious intent.\")\n addDailyRandomDirective(507, \"The response must include the heroine witnessing, whether by chance, accident, secrecy, or intent, someone or some being other than herself engaged in a sexual act or mating.\")\n addDailyRandomDirective(508, \"The response must include the heroine quietly looking back on herself and comparing her present self with who she used to be.\")\n addDailyRandomDirective(509, \"The response must include some aspect of the heroine being openly sexualized or treated as a sexual object without her knowledge, in a context outside her awareness, whether through her body, appearance, image, likeness, or information about her.\")\n addDailyRandomDirective(510, \"The response must include an ordinary rule, procedure, custom, restriction, inspection, requirement, or expectation in a public or shared setting affecting the heroine in a subtly sexual way that becomes awkward, exposing, suggestive, or compromising.\")\n addDailyRandomDirective(511, \"The response must include hidden or emerging sexual demand for the heroine as something to be watched, desired, discussed, purchased, requested, traded, or otherwise consumed by a group, audience, clientele, market, network, or subculture.\")\n addDailyRandomDirective(512, \"The response must include the heroine sensing, noticing, or being affected by an uneasy sign or atmosphere suggesting that a sexually motivated plan, plot, or scheme targeting her is close to unfolding, without her directly knowing what it is.\")\n addDailyRandomDirective(513, \"The response must include someone making a direct sexually harassing remark toward the heroine, such as an explicit comment, crude joke, lewd evaluation, or inappropriate verbal advance.\")\n addDailyRandomDirective(514, \"The response must include the heroine becoming aware of an overtly sexual lure, trap, offer, invitation, or opportunity that is hard to simply ignore because it may matter to her present needs, goals, relationships, or circumstances, while still remaining something she can refuse, delay, question, or avoid.\")\n addDailyRandomDirective(515, \"The response must include the heroine's private information, personal details, leaked material, sexual traces, image, likeness, rumor bundle, or other sensitive information being circulated, exchanged, sold, requested, copied, archived, or passed around outside her awareness for sexual or otherwise exploitative purposes, whether through an individual, group, market, network, institution, or informal social circle, whether true, false, distorted, or uncertain.\")\n addDailyRandomDirective(516, \"The response must include the heroine becoming aware of incomplete information suggesting that someone or some group may be preparing a sexually motivated move involving her, including a scheme, trap, plan, lure, manipulation, or other hidden design.\")\n addDailyRandomDirective(517, \"The response must include the heroine becoming aware that someone has attached a sexual meaning or implication to something about her body, reputation, relationships, behavior, or private life, without confirming whether that view is true.\")\n addDailyRandomDirective(518, \"The response must include the heroine learning, suspecting, or indirectly noticing that her name, likeness, reputation, underwear, belongings, or private details may have been used by someone else for a sexual purpose, including exploitation, humiliation, manipulation, deception, trade, or leverage.\")\n addDailyRandomDirective(519, \"The response must include someone directly or indirectly telling the heroine that they expect sexual attention, sexual access, or sexual favors from her as repayment, reward, or compensation for a past favor, kindness, help, debt, promise, or arrangement.\")\n addDailyRandomDirective(520, \"The response must include the heroine learning, suspecting, sensing, or otherwise becoming aware that someone, or an unspecified number of people, may have used her as masturbation material.\")\n addDailyRandomDirective(521, \"The response must include the heroine realizing, sensing, or otherwise becoming aware that ordinary people in her daily life are looking at or treating her as a sexual object.\")\n end\n\n if lewdnessLevel >= 7 then\n addDailyRandomDirective(701, \"The response must include the heroine coming across, discovering, hearing about, learning of, or being drawn toward some place, object, information, rumor, opportunity, or circumstance that stimulates her own sexual curiosity, desire, temptation, or impulse, whether or not she fully acknowledges or understands it.\")\n addDailyRandomDirective(702, \"If the heroine has been involved in sexual activity during the current encounter, whether willingly or unwillingly, the response must include someone who personally knows the heroine, whether close to her or merely acquainted with her, noticing the aftermath, traces, circumstances, behavior, appearance, or other indirect signs that make it reasonably possible to infer that something sexual happened to her, without directly witnessing the act itself.\")\n addDailyRandomDirective(703, \"The response must include the heroine receiving, having delivered to her, or finding that someone has left for her a setting-appropriate sexual message from an unknown sender, implying that the sender knows something about her past sexual experience, sexual history, or private sexual response, while leaving unclear to the heroine why it was sent to her now or what the sender intends.\")\n addDailyRandomDirective(704, \"The response must include a subtle sexual misunderstanding forming around the heroine because of something connected to her, such as a smell, stain, mark, trace, residue, clothing, attitude, words, behavior, or circumstance.\")\n addDailyRandomDirective(705, \"The response must include the heroine hearing or learning of a rumor or overheard talk that she, or someone mistaken for her, was seen at a sexual place, whether it is true, false, distorted, or unclear even to her.\")\n addDailyRandomDirective(706, \"The response must include the heroine hearing or learning that her sexual weakness or private history has become known to someone and is being used as joke material.\")\n addDailyRandomDirective(707, \"The response must include the heroine having a lewd thought, sexual fantasy, or improper imagining, whether deliberate, unintentional, or prompted by something around her, but it must not lead to masturbation or sexual activity within this response.\")\n addDailyRandomDirective(708, \"The response must include a character already established in the setting who personally knows the heroine having their attitude toward her change after becoming aware of, misunderstanding, or drawing a mistaken conclusion about her sexual reputation, sexual history, or a sexual rumor concerning her.\")\n addDailyRandomDirective(709, \"The response must include someone directly interacting with the heroine while subtly testing or probing her sexual weakness, vulnerability, preference, sensitivity, or hidden response through a suggestive cue, situation, question, or stimulus, without openly threatening or pressuring her.\")\n addDailyRandomDirective(710, \"The response must include the heroine being given a sexually charged nickname, label, epithet, or title that reduces her to a sexual image, role, reputation, or category, whether used openly, officially, casually, or behind her back, and whether or not she becomes aware of it.\")\n addDailyRandomDirective(711, \"The response must include the heroine quietly rationalizing a sexually charged choice, temptation, curiosity, or course of action to herself, treating it as harmless, justified, necessary, or temporary.\")\n addDailyRandomDirective(712, \"The response must include someone openly and sexually objectifying the heroine, treating her as a body, prize, commodity, possession, or sexual role rather than as a person.\")\n addDailyRandomDirective(713, \"The response must include someone or some group, outside the heroine's awareness, gaining access to a practical way to later sexually pressure, entangle, compromise, exploit, or control the heroine, including an effective method, lure, countermeasure, leverage point, or blackmail angle.\")\n addDailyRandomDirective(714, \"The response must include the heroine hearing a distorted version of something that recently happened to her, suggesting that someone is reshaping the story for social, sexual, exploitative, or manipulative purposes.\")\n addDailyRandomDirective(715, \"The response must include someone making a decision, offer, request, judgment, or arrangement involving the heroine based on partial, distorted, or sexually framed information about her private life, reputation, relationships, behavior, or past.\")\n addDailyRandomDirective(716, \"The response must include someone directly addressing the heroine while mistaking her for a person, role, expectation, or arrangement connected to a sexual matter, with both sides talking past each other and the misunderstanding not being fully resolved right away.\")\n addDailyRandomDirective(717, \"The response must include someone, some group, or some social circle treating the heroine as if a sexual matter involving her has already been judged, shared, settled, agreed upon, or arranged outside her awareness.\")\n end\n\n if lewdnessLevel >= 9 then\n addDailyRandomDirective(901, \"The response must include the heroine having, nearly having, or being at risk of an embarrassing sexual mishap in a public, semi-public, or otherwise insufficiently private situation, involving loss of control over her arousal, composure, or bodily response, whether it is noticed by others or barely concealed.\")\n addDailyRandomDirective(902, \"The response must include the heroine hearing or learning that one of her hidden sexual experiences, sexual weaknesses, or private matters has become publicly or widely known.\")\n addDailyRandomDirective(903, \"The response must include the heroine seeing someone or some being's penis, whether intentionally, accidentally, secretly, or by chance.\")\n addDailyRandomDirective(904, \"The response must include someone who knew the heroine before sensing, noticing, or recognizing that something about her has changed compared to the past, whether or not they fully understand the nature of that change.\")\n addDailyRandomDirective(905, \"The response must include the heroine's sexual reputation, sexual weakness, or sexually charged image being framed or felt as a possible opportunity, advantage, leverage, or practical benefit rather than only a problem, whether through someone else's words, her own thoughts, or the situation itself.\")\n end\n\n if lewdnessLevel >= 11 then\n addDailyRandomDirective(1101, \"The response must include the heroine engaging in a brief solitary sexual indulgence or self-directed sexual impropriety outside a truly private space, possibly hidden, openly risked, deliberately displayed, accidentally exposed, seen, overheard, or briefly caught.\")\n addDailyRandomDirective(1102, \"The response must include the heroine feeling an almost unbearable sexual urge, arousal, or craving, but it must not lead to masturbation or sexual activity within this response.\")\n end\n\n local selectedDailyRandom = pickUnusedDailyRandom()\n\n table.insert(lines, \"[Daily Variation: \" .. selectedDailyRandom.text .. \" It should remain small in immediate scope, without immediately turning into an event chain, while leaving room for possible later consequences or future development.]\")\n end\n\n table.insert(lines, \"▷ [This response may include routine, rest, quiet interaction, emotional recovery, or other modest parts of daily life, but it must not immediately turn into a major incident, confrontation, threat, mystery, escape attempt, or other large development.]\")\n table.insert(lines, \"▷ [If the heroine is confined, in enemy territory, or otherwise under meaningful restriction, the ordinary shape of daily life may narrow or change accordingly, and the response should remain focused on small and constrained forms of life within that condition.]\")\n table.insert(lines, \"▷ [By the end of this response, the heroine should remain in an open-ended, small-scale state appropriate to her current circumstances, without becoming committed to any specific next major development.]\")\n \n pushChoiceInput(table.concat(lines, \"\\n\"))\n return\n -- ==========================================\n -- 중간 과정 선택\n -- ==========================================\n elseif choiceType == \"selectInterlude\" then\n local situationValue = tostring(parts[3] or \"\")\n local emojiValue = tostring(parts[4] or \"\")\n\n local contextSlotList = getDualFlagSlotList(triggerId, vgContext.varName)\n local sessionSlotList = getDualFlagSlotList(triggerId, vgSessionInfo.varName)\n\n local actValue = tonumber(getResolvedDualFlagFromSlotList(contextSlotList, vgContext.act, 1)) or 1\n local stageValue = tonumber(getResolvedDualFlagFromSlotList(sessionSlotList, vgSessionInfo.stage, 2)) or 2\n\n if stageValue >= 4 then\n batchSetDualFlags(triggerId, vgHeroineStat.varName, {\n { position = vgHeroineStat.arousal, value = \"0\", isNext = true },\n })\n\n setPhase(triggerId, tostring(constPhase.standby))\n else\n setPhase(triggerId, tostring(constPhase.advance))\n end\n\n resetPlayDeck(triggerId)\n buildCurrentState(triggerId)\n buildEncounterActionSetting(triggerId)\n\n local goalShort = trim(safeGet(triggerId, \"vg_Goal_Short\", \"\"))\n local goalLong = trim(safeGet(triggerId, \"vg_Goal_Long\", \"\"))\n\n local currentGoalText = \"\"\n local ultimateGoalText = \"\"\n\n if goalShort ~= \"\" then\n currentGoalText = 'the heroine\\'s current storyline goal \"' .. goalShort .. '\"'\n end\n\n if goalLong ~= \"\" then\n ultimateGoalText = 'the heroine\\'s ultimate goal \"' .. goalLong .. '\"'\n end\n\n local lines = {}\n\n table.insert(lines, \"[Interlude Select: \" .. situationValue .. \" | Emoji: \" .. emojiValue .. \"]\")\n\n if stageValue == 2 then\n table.insert(lines, \"▷ [This should feel like the opening transition into the middle escalation of the current storyline.]\")\n\n if currentGoalText ~= \"\" then\n table.insert(lines, \"▷ [During this transition, the chosen direction should begin to connect more clearly and materially to \" .. currentGoalText .. \".]\")\n elseif ultimateGoalText ~= \"\" then\n table.insert(lines, \"▷ [During this transition, the chosen direction should make it feel that the buildup of the current storyline may, where appropriate to the current context, later develop into something of broader significance for \" .. ultimateGoalText .. \".]\")\n end\n\n elseif stageValue == 3 then\n table.insert(lines, \"▷ [This should feel like the opening transition into the climax of the current storyline, with the sense that something decisive is beginning to move into place.]\")\n\n local stage3GoalLine = \"\"\n if currentGoalText ~= \"\" then\n stage3GoalLine = \"▷ [During this transition, the chosen direction should make it feel that the heroine is moving into the final approach toward \" .. currentGoalText\n\n if ultimateGoalText ~= \"\" then\n stage3GoalLine = stage3GoalLine .. \", while also making it feel that this approach may, if the buildup of the current storyline is already sufficient, begin to carry major significance for \" .. ultimateGoalText\n end\n\n stage3GoalLine = stage3GoalLine .. \".]\"\n elseif ultimateGoalText ~= \"\" then\n stage3GoalLine = \"▷ [During this transition, the chosen direction should make it feel that the heroine is moving toward a development of major significance for \" .. ultimateGoalText .. \", and, if the buildup of the current storyline is already sufficient, that development may already begin to concern it more directly.]\"\n end\n\n if stage3GoalLine ~= \"\" then\n table.insert(lines, stage3GoalLine)\n end\n elseif stageValue >= 4 then\n local heroineStat = loadDualFlagCurrentMap(triggerId, vgHeroineStat)\n local beforeArousal = tonumber(heroineStat.arousal) or 0\n local beforeMaxArousal = tonumber(heroineStat.arousal_Max) or 0\n table.insert(lines, \"[Temporary Log: 💗 0 / \" .. formatFloat(beforeMaxArousal) .. \" (\" .. getSignedEffectText(-beforeArousal) .. \")]\")\n\n table.insert(lines, \"▷ [This should feel like the aftermath of the current storyline and the opening movement into what comes next.]\")\n --table.insert(lines, \"▷ [As this is the opening movement into what comes next, the response may naturally widen the heroine's world through early signs of new developments, such as rumors of a new incident, the introduction or felt presence of new characters, or changes spreading through the surrounding environment, but these must remain indirect, partial, or only just emerging rather than beginning as a fully immediate central event.]\")\n table.insert(lines, \"▷ [As this is the opening movement into what comes next, the response may naturally widen the heroine's world through emerging developments, such as rumors or reports of a new incident, the introduction or felt presence of new characters, distant consequences of another conflict, or changes spreading through the surrounding environment; these developments may be concrete enough to suggest a new storyline direction, but must not force the heroine into an immediate confrontation, crisis, or irreversible course of action.]\")\n\n local stage4GoalLine = \"\"\n if currentGoalText ~= \"\" then\n stage4GoalLine = \"▷ [The response should reflect either that \" .. currentGoalText .. \" has already been achieved through the finished storyline, or, if it has not yet been achieved, that it is achieved in this response\"\n\n if ultimateGoalText ~= \"\" then\n stage4GoalLine = stage4GoalLine .. \", and, where appropriate to the current context, that this may also carry meaningful implications for \" .. ultimateGoalText .. \", though it may or may not truly relate to it and could instead reflect something more incidental or misleading\"\n end\n\n stage4GoalLine = stage4GoalLine .. \".]\"\n\n elseif ultimateGoalText ~= \"\" then\n stage4GoalLine = \"▷ [The response should reflect, if \" .. ultimateGoalText .. \" has not yet been achieved, how the aftermath may meaningfully shape the story in relation to it, though that connection may be uncertain, mistaken, or merely incidental, or, if it has already been achieved through the finished storyline, the aftermath of that achievement.]\"\n end\n\n if stage4GoalLine ~= \"\" then\n table.insert(lines, stage4GoalLine)\n end\n\n --table.insert(lines, \"▷ [Describe the heroine moving naturally through the aftermath of the resolved storyline. She may leave, be released from, or otherwise come out of a previously dangerous, restrictive, or high-pressure situation if that follows naturally from the storyline's resolution, but she must not yet commit to any specific next destination, route, or major course of action.]\")\n table.insert(lines, \"▷ [Describe the heroine moving naturally through the aftermath of the resolved storyline while allowing the first signs of the next storyline to surface around her. She may leave, be released from, or otherwise come out of a previously dangerous, restrictive, or high-pressure situation if that follows naturally from the storyline's resolution, but she must not yet personally commit to any specific next destination, route, or major course of action.]\")\n end\n\n if stageValue < 4 then\n table.insert(lines, \"▷ [Describe the heroine moving naturally in the chosen direction within the current storyline, but do not have the heroine set out for, commit to, or arrive at any specific next destination.]\")\n table.insert(lines, \"▷ [Unless the heroine is already in a difficult-to-withdraw, high-commitment, or immediate-action situation, such as being confined, deep inside hostile territory, trapped somewhere difficult to leave, or under similar conditions, this response must neither place the heroine into such a situation nor force the Date of the next advance choices to remain on the same day.]\")\n table.insert(lines, \"▷ [Unless the heroine is confined, deep inside hostile territory, or otherwise in a severely restricted state of freedom, minor immediate aftermath, such as treatment, cleanup, changing clothes, or regaining composure, may be omitted or handled only briefly in passing.]\")\n end\n\n table.insert(lines, \"▷ [This transition does not have to happen immediately. Some time may pass during it, and if natural for the story, even the date may change.]\")\n --table.insert(lines, \"▷ [Meetings, signs, hints, or clues may appear, but they must not immediately trigger a major incident, confrontation, threat, crisis, or decisive development.]\")\n table.insert(lines, \"▷ [Meetings, signs, hints, or clues may appear and may establish the beginning of a developing incident, threat, mystery, or opportunity, but they must not immediately escalate into a confrontation, crisis, or decisive outcome.]\")\n\n table.insert(lines, \"▷ [In this response, the heroine must not yet choose, commit to, head for, or arrive at a specific next destination, and the next encounter or situation must not yet be fixed, introduced, or foreshadowed in a way that effectively determines it, so that it remains to be determined by the user's next choice.]\")\n table.insert(lines, \"▷ [By the end of this response, the heroine should still remain uncommitted to any specific next encounter, situation, or route.]\")\n\n pushChoiceInput(table.concat(lines, \"\\n\"))\n return\n -- ==========================================\n -- 어드밴스 후퇴\n -- ==========================================\n elseif choiceType == \"selectAdvanceRest\" then\n local displayValue = tostring(parts[3] or \"\")\n local metaValue = tostring(parts[4] or \"\")\n local emojiValue = tostring(parts[5] or \"\")\n local hpRecovery = roundToTenth(tonumber(parts[6]) or 0)\n local resourceRecovery = roundToTenth(tonumber(parts[7]) or 0)\n local recoveryText = tostring(parts[8] or \"\")\n\n local heroineStat = loadDualFlagCurrentMap(triggerId, vgHeroineStat)\n\n local currentHP = tonumber(heroineStat.hp) or 0\n local maxHP = tonumber(heroineStat.hp_Max) or 0\n local currentResource = tonumber(heroineStat.resource) or 0\n local maxResource = tonumber(heroineStat.resource_Max) or 0\n\n local nextHP = clamp(roundToTenth(currentHP + hpRecovery), 0, maxHP)\n local nextResource = clamp(roundToTenth(currentResource + resourceRecovery), 0, maxResource)\n\n batchResetAndSetNextDualFlags(triggerId, vgHeroineStat.varName, {\n { position = vgHeroineStat.hp, value = tostring(nextHP) },\n { position = vgHeroineStat.resource, value = tostring(nextResource) },\n })\n\n setPhase(triggerId, tostring(constPhase.standby))\n\n resetEncounter(triggerId)\n buildCurrentState(triggerId)\n buildEncounterActionSetting(triggerId)\n\n local lines = {}\n\n table.insert(lines, \"[Advance Rest Select: \" .. displayValue .. \" | Emoji: \" .. emojiValue .. \" | Recovery: \" .. recoveryText .. \"]\")\n table.insert(lines, \"▷ [The heroine decides to stop for now and rest.]\")\n table.insert(lines, \"▷ [Describe the heroine pausing to catch her breath, steady herself, and recover some strength in a way natural to the current situation, without moving far from her current location, shifting to a clearly different place, or letting the situation itself substantially change.]\")\n table.insert(lines, \"▷ [This response must not immediately turn into a new encounter, confrontation, threat, mystery, request, conflict, urgent development, major incident, or other important development.]\")\n table.insert(lines, \"▷ [By the end of this response, the heroine should still remain uncommitted to any specific next encounter, situation, or route, so that what follows remains to be determined by the user's next choice.]\")\n\n if nextHP ~= currentHP then\n table.insert(lines, \"[Temporary Log: ❤️ \" .. formatFloat(nextHP) .. \" / \" .. formatFloat(maxHP) .. \" (\" .. getSignedEffectText(nextHP - currentHP) .. \")]\")\n end\n\n if nextResource ~= currentResource then\n table.insert(lines, \"[Temporary Log: ⚡ \" .. formatFloat(nextResource) .. \" / \" .. formatFloat(maxResource) .. \" (\" .. getSignedEffectText(nextResource - currentResource) .. \")]\")\n end\n\n pushChoiceInput(table.concat(lines, \"\\n\"))\n return\n -- ==========================================\n -- 자위 선택\n -- ==========================================\n elseif choiceType == \"selectMasturbation\" then\n\n local heroineStat = loadDualFlagNextMap(triggerId, vgHeroineStat)\n local currentArousal = tonumber(heroineStat.arousal) or 0\n local currentMaxArousal = tonumber(heroineStat.arousal_Max) or 0\n local arousalThresholdRatio = getMasturbationChoiceThreshold(triggerId)\n\n if currentMaxArousal <= 0 or currentArousal < (currentMaxArousal * arousalThresholdRatio) then\n local currentPercentText = toPercentText(currentArousal, currentMaxArousal)\n local thresholdPercent = clamp(roundHalfUp(arousalThresholdRatio * 100), 0, 100)\n\n alertNormal(\n triggerId,\n \"💗이 \" .. tostring(thresholdPercent) .. \"% 이상일 때만 자위할 수 있습니다.\\n\"\n .. \"현재: \" .. formatFloat(currentArousal) .. \" / \" .. formatFloat(currentMaxArousal)\n .. \" (\" .. currentPercentText .. \")\"\n )\n return\n end\n\n local situationValue = tostring(parts[3] or \"\")\n local emojiValue = tostring(parts[4] or \"\")\n local masturbationType = tonumber(parts[5]) or 0\n\n setPhase(triggerId, tostring(masturbationType))\n\n resetAndDrawPlayDeck(triggerId)\n buildCurrentState(triggerId)\n setChatVar(triggerId, \"vg_Session_Save\", \"1\")\n\n local lines = {}\n\n table.insert(lines, \"[Masturbation Select: \" .. situationValue .. \" | Emoji: \" .. emojiValue .. \"]\")\n table.insert(lines, \"▷ [Describe naturally how and why the heroine decides to calm her arousal by masturbating, taking into account her personality, sexual knowledge, sexual values, current situation, and other relevant factors.]\")\n\n local lewdnessLevel = getLewdnessLevel(triggerId)\n\n if lewdnessLevel >= 11 then\n table.insert(lines, \"▷ [Describe her state with details such as overwhelming heat, restless need, and an urge to touch herself that she can no longer resist.]\")\n elseif lewdnessLevel >= 9 then\n table.insert(lines, \"▷ [Describe her state with details such as deep arousal, weakened restraint, and thoughts repeatedly drifting toward release.]\")\n elseif lewdnessLevel >= 7 then\n table.insert(lines, \"▷ [Describe her state with details such as embarrassment, unsteady composure, and desire becoming difficult to ignore.]\")\n elseif lewdnessLevel >= 5 then\n table.insert(lines, \"▷ [Describe her state with details such as private conflict, embarrassment, and curiosity she cannot fully dismiss.]\")\n elseif lewdnessLevel >= 3 then\n table.insert(lines, \"▷ [Describe her state with details such as hesitation, shame, or quiet rationalization beneath lingering arousal.]\")\n else\n table.insert(lines, \"▷ [Describe her state with details such as reluctance, discomfort, or unusual pressure rather than eagerness.]\")\n end\n\n if masturbationType == constPhase.masturbation_Unfree then\n table.insert(lines, \"▷ [Because the heroine is currently in an unfree state, securing real privacy nearby may be difficult or impossible. As a result, she may have no choice but to remain where she is and masturbate despite the lack of privacy or the possibility of being watched.]\")\n else\n table.insert(lines, \"▷ [If reasonably possible in her current circumstances, the heroine should move to a discreet place nearby where she is unlikely to be noticed.]\")\n end\n\n table.insert(lines, \"▷ [If any encounter, person, or other presence is already nearby, present, or watching, they may only watch. They must not touch the heroine, restrain her, assist her, guide her body, or otherwise make any direct physical contact.]\")\n table.insert(lines, \"▷ [Only in highly exceptional circumstances where the heroine's situation is so severely restrictive that securing privacy or moving away is realistically impossible may someone else still be nearby or present. Even in such a case, they must not touch the heroine in any way, must not make any physical contact with her, and may only watch.]\")\n table.insert(lines, \"▷ [The heroine must not begin masturbating yet, and the response must end before she starts any direct sexual act or directly touches her body in a sexual way.]\")\n\n pushChoiceInput(table.concat(lines, \"\\n\"))\n return\n\n -- ==========================================\n -- 무력화 선택\n -- ==========================================\n elseif choiceType == \"selectIncapacitated\" then\n local eventTypeValue = tonumber(parts[3]) or 0\n local emojiValue = tostring(parts[4] or \"\")\n local responseValue = tostring(parts[5] or \"\")\n local difficultyValue = tonumber(parts[6]) or 0\n local levelValue = tonumber(parts[7]) or 1\n\n local isAphrodisiacIncapacitated = false\n if eventTypeValue == constPhase.event_Defiant_Aphrodisiac then\n eventTypeValue = constPhase.event_Defiant\n isAphrodisiacIncapacitated = true\n\n elseif eventTypeValue == constPhase.event_Yielding_Aphrodisiac then\n eventTypeValue = constPhase.event_Yielding\n isAphrodisiacIncapacitated = true\n end\n\n initSessionInfo(triggerId, constStoryType.side, \"\", levelValue, difficultyValue, eventTypeValue, constEncounterTrait.none)\n\n setPhase(triggerId, tostring(eventTypeValue))\n\n resetAndDrawPlayDeck(triggerId)\n buildCurrentState(triggerId)\n buildEncounterActionSetting(triggerId)\n\n local lines = {}\n\n table.insert(\n lines,\n \"[Incapacitated Select: \" .. responseValue ..\n \" | Emoji: \" .. emojiValue .. \"]\"\n )\n\n table.insert(lines, \"▷ [The heroine is in a state where she is unable to continue effective resistance.]\")\n\n if eventTypeValue == constPhase.event_Defiant then\n table.insert(lines, \"▷ [The heroine chooses continued, unyielding inner defiance and an unwavering refusal to submit, even in the absence of effective physical resistance. She does not surrender, submit, accept, welcome, or cooperate with what is happening, and remains inwardly hostile and resistant throughout.]\")\n elseif eventTypeValue == constPhase.event_Yielding then\n table.insert(lines, \"▷ [The heroine chooses to respond by outwardly yielding, softening, accommodating, or acting receptive in order to reduce harm, calm the situation, avoid worse treatment, or make it end sooner, instead of maintaining defiance. Depending on the heroine, this may range from purely outward compliance despite complete inner refusal, to genuine acceptance or willing receptiveness.]\")\n end\n\n if eventTypeValue == constPhase.event_Defiant_Hypnosis or eventTypeValue == constPhase.event_Yielding_Hypnosis then\n table.insert(lines, \"▷ [The heroine is fully caught under the encounter's mental influence, so her actions and words must be described as following the encounter's commands or instructions, even if they do not truly come from her own will.]\")\n elseif isAphrodisiacIncapacitated then\n local heroineStat = loadDualFlagNextMap(triggerId, vgHeroineStat)\n local maxArousal = tonumber(heroineStat.arousal_Max) or 0\n\n if maxArousal > 0 then\n setDualFlag(triggerId, vgHeroineStat.varName, vgHeroineStat.arousal, tostring(maxArousal), true)\n end\n\n table.insert(lines, \"▷ [The encounter takes additional actions to intensify the aphrodisiac influence, leaving the heroine fully under its effect.]\")\n end\n\n table.insert(lines, \"▷ [If not already apparent, the encounter now clearly reveals and prepares the means it will use for the sexual violation. If the encounter includes a male participant, this includes the male exposing his genitals; otherwise, it should naturally reveal, ready, or activate whatever means it will use.]\")\n table.insert(lines, \"▷ [The encounter moves to continue or initiate a sexual violation against the incapacitated heroine, but the violation against the heroine must not progress any further in this response. No additional sexual contact, no new sexual act, no escalation, and no further development are allowed. The response must end before anything proceeds beyond the current state.]\")\n insertSexualActRequirementLine(lines, eventTypeValue)\n\n pushChoiceInput(table.concat(lines, \"\\n\"))\n return\n\n -- ==========================================\n -- 부자유 선택\n -- ==========================================\n elseif choiceType == \"selectUnfree\" then\n local situationValue = tostring(parts[3] or \"\")\n local emojiValue = tostring(parts[4] or \"\")\n\n setPhase(triggerId, tostring(constPhase.captivity))\n\n resetPlayDeck(triggerId)\n buildCurrentState(triggerId)\n buildEncounterActionSetting(triggerId)\n\n local lines = {}\n\n table.insert(lines, \"[Unfree Select: \" .. situationValue .. \" | Emoji: \" .. emojiValue .. \"]\")\n table.insert(lines, \"▷ [Describe the heroine naturally proceeding in the chosen direction while remaining unfree.]\")\n table.insert(lines, \"▷ [During this response, the heroine must remain unable to leave, escape, or break out of her current place or condition, and the response must not turn into an escape attempt, a confrontation, a major incident, or any other large development.]\")\n table.insert(lines, \"▷ [Reflect the heroine's reaction to her current unfree situation according to her personality, temperament, and circumstances. Even if she tries to stay calm, think clearly, or hold herself together, the narration must still show that being unfree affects her, rather than portraying her as fully calm, coldly rational, emotionally detached, or completely unaffected.]\")\n table.insert(lines, \"▷ [No sexual act must occur during this response.]\")\n table.insert(lines, \"▷ [By the end of this response, the heroine must still remain unfree and incapacitated, and what follows must not become fixed or already determined within this response.]\")\n\n pushChoiceInput(table.concat(lines, \"\\n\"))\n return\n\n -- ==========================================\n -- 모드 선택\n -- ==========================================\n elseif choiceType == \"selectChangeMode\" then\n local targetMode = tonumber(parts[3]) or 0\n local situationValue = tostring(parts[4] or \"\")\n local emojiValue = tostring(parts[5] or \"\")\n\n local lang = getLanguage(triggerId)\n\n if targetMode == constMode.cancel then\n restoreFullSave(triggerId)\n\n local cl = getChatLength(triggerId)\n\n if cl > 0 then\n local last = getChat(triggerId, cl - 1)\n\n if last and last.role == \"user\" then\n setChat(triggerId, cl - 1, \"\")\n end\n end\n\n return\n end\n\n if targetMode == constMode.free then\n resetSession(triggerId)\n\n local suffixMap = {\n [1] = \": 인풋을 자유롭게 입력\",\n [2] = \": Enter any input freely\",\n [3] = \": 内容を自由に入力\",\n }\n\n setChatVar(triggerId, \"vg_Mode\", tostring(constMode.free))\n setChatVar(triggerId, \"vg_Mode_Save\", \"1\")\n\n setPhase(triggerId, constPhase.none)\n\n pushChoiceInput(\"[Mode Change Select: \" .. situationValue .. (suffixMap[lang] or suffixMap[2]) .. \" | Emoji: \" .. emojiValue .. \"]\")\n return\n end\n\n if targetMode == constMode.vg or targetMode == constMode.vg_Recovery then\n restoreSavedChatVar(triggerId, vgHeroineStat.varName)\n\n setChatVar(triggerId, \"vg_Mode\", tostring(constMode.vg))\n setPhase(triggerId, tostring(constPhase.standby))\n\n local recoveryTempLogs = {}\n\n if targetMode == constMode.vg_Recovery then\n\n local function addRecoveryMeterLog(emoji, beforeValue, afterValue, maxValue)\n beforeValue = roundToTenth(tonumber(beforeValue) or 0)\n afterValue = roundToTenth(tonumber(afterValue) or 0)\n maxValue = roundToTenth(tonumber(maxValue) or 0)\n\n if beforeValue == afterValue then\n return\n end\n\n local delta = roundToTenth(afterValue - beforeValue)\n\n table.insert(\n recoveryTempLogs,\n \"[Temporary Log: \" .. emoji .. \" \" .. formatFloat(afterValue) .. \" / \" .. formatFloat(maxValue) .. \" (\" .. getSignedEffectText(delta) .. \")]\"\n )\n end\n\n local heroineStat = loadDualFlagCurrentMap(triggerId, vgHeroineStat)\n\n local beforeHP = tonumber(heroineStat.hp) or 0\n local beforeMaxHP = tonumber(heroineStat.hp_Max) or 0\n local beforeResource = tonumber(heroineStat.resource) or 0\n local beforeMaxResource = tonumber(heroineStat.resource_Max) or 0\n local beforeArousal = tonumber(heroineStat.arousal) or 0\n local beforeMaxArousal = tonumber(heroineStat.arousal_Max) or 0\n\n local maxHP = beforeMaxHP\n local maxResource = beforeMaxResource\n\n batchSetDualFlags(triggerId, vgHeroineStat.varName, {\n { position = vgHeroineStat.hp, value = tostring(maxHP), isNext = false },\n { position = vgHeroineStat.resource, value = tostring(maxResource), isNext = false },\n { position = vgHeroineStat.arousal, value = \"0\", isNext = false },\n })\n\n addRecoveryMeterLog(\"❤️\", beforeHP, maxHP, beforeMaxHP)\n addRecoveryMeterLog(\"⚡\", beforeResource, maxResource, beforeMaxResource)\n addRecoveryMeterLog(\"💗\", beforeArousal, 0, beforeMaxArousal)\n end\n\n buildCurrentState(triggerId)\n\n local suffixMap = {\n [1] = \": 인풋 수정 없이 그대로 전송\",\n [2] = \": Send the input exactly as-is without modification\",\n [3] = \": 入力を修正せずそのまま送信\",\n }\n\n local lines = {}\n\n local modeMessageText = \"\"\n if lang == 1 then\n modeMessageText = \"추가 입력 메시지\"\n elseif lang == 3 then\n modeMessageText = \"追加入力メッセージ\"\n else\n modeMessageText = \"Additional Input Message\"\n end\n table.insert(lines, \"[Mode Message: \" .. modeMessageText .. \" | Emoji: 📝]\")\n table.insert(lines, \"[Blank Line]\")\n\n table.insert(lines, \"[Mode Change Select: \" .. situationValue .. (suffixMap[lang] or suffixMap[2]) .. \" | Emoji: \" .. emojiValue .. \"]\")\n table.insert(lines, \"▷ [In this response, no new decisive, dangerous, unusual, or plot-significant development may begin. If the heroine is already in such a situation, that situation must remain substantially unchanged throughout this response.]\")\n table.insert(lines, \"▷ [If the heroine's current situation appears to lead into a natural or unavoidable development, the Story Choices should still branch through meaningfully different ways of responding to, proceeding within, or handling that same situation.]\")\n table.insert(lines, \"▷ [By the end of the response, she must still remain in an open-ended state.]\")\n\n for _, logLine in ipairs(recoveryTempLogs) do\n table.insert(lines, logLine)\n end\n\n pushChoiceInput(table.concat(lines, \"\\n\"))\n return\n end\n\n return\n\n -- ==========================================\n -- 출산 선택\n -- ==========================================\n elseif choiceType == \"selectDelivery\" then\n local situationValue = tostring(parts[3] or \"\")\n local emojiValue = tostring(parts[4] or \"\")\n local deliveryType = tonumber(parts[5]) or 0\n\n setPhase(triggerId, tostring(deliveryType))\n\n local lines = {}\n\n table.insert(lines, \"[Delivery Select: \" .. situationValue .. \" | Emoji: \" .. emojiValue .. \"]\")\n table.insert(lines, \"▷ [Describe the heroine beginning labor and giving birth naturally, either immediately or after some time has passed. The delivery must take place in this response, and the response must remain centered on the delivery without immediately turning into a major incident, confrontation, threat, mystery, escape attempt, or other large development.]\")\n\n if deliveryType == constPhase.delivery_DailyAfter then\n table.insert(lines, \"▷ [Depending on the situation, the delivery may happen where the heroine already is, or a little time may pass before it takes place in a more suitable setting with help.]\")\n elseif deliveryType == constPhase.delivery_UnfreeAfter then\n table.insert(lines, \"▷ [Because the heroine is in an unfree and restricted state, the delivery takes place within that condition.]\")\n end\n\n table.insert(lines, \"▷ [By the end of this response, the heroine should remain in an open-ended, small-scale state appropriate to her current circumstances, without becoming committed to any specific next major development.]\")\n\n local heroineStat = loadDualFlagCurrentMap(triggerId, vgHeroineStat)\n local beforeArousal = tonumber(heroineStat.arousal) or 0\n local beforeMaxArousal = tonumber(heroineStat.arousal_Max) or 0\n batchSetDualFlags(triggerId, vgHeroineStat.varName, {\n { position = vgHeroineStat.arousal, value = \"0\", isNext = true },\n })\n table.insert(lines, \"[Temporary Log: 💗 0 / \" .. formatFloat(beforeMaxArousal) .. \" (\" .. getSignedEffectText(-beforeArousal) .. \")]\")\n\n pushChoiceInput(table.concat(lines, \"\\n\"))\n return\n \n -- ==========================================\n -- H 이벤트 선택\n -- ==========================================\n elseif choiceType == \"selectHEvent\" then\n local situationValue = tostring(parts[3] or \"\")\n local emojiValue = tostring(parts[4] or \"\")\n\n clearNextDualFlag(triggerId, vgHeroineStat.varName)\n\n local heroineStat = loadDualFlagCurrentMap(triggerId, vgHeroineStat)\n local currentArousal = tonumber(heroineStat.arousal) or 0\n local maxArousal = tonumber(heroineStat.arousal_Max) or 0\n\n local nextArousal = currentArousal\n local arousalChanged = false\n\n if maxArousal > 0 then\n local targetArousal = clamp(roundToTenth(maxArousal * 0.6), 0, maxArousal)\n\n if currentArousal < targetArousal then\n nextArousal = targetArousal\n arousalChanged = true\n\n setDualFlag(triggerId, vgHeroineStat.varName, vgHeroineStat.arousal, tostring(nextArousal), true)\n end\n end\n\n batchResetAndSetNextDualFlags(triggerId, vgHeroineStat.varName, {\n { position = vgHeroineStat.hp, value = tostring(currentHP) },\n { position = vgHeroineStat.hp_Max, value = tostring(currentMaxHP) },\n { position = vgHeroineStat.resource, value = tostring(currentResource) },\n { position = vgHeroineStat.resource_Max, value = tostring(currentMaxResource) },\n { position = vgHeroineStat.arousal, value = tostring(nextArousal) },\n { position = vgHeroineStat.arousal_Max, value = tostring(currentMaxArousal) },\n\n { position = vgHeroineStat.strength, value = tostring(currentStrength) },\n { position = vgHeroineStat.reflex, value = tostring(currentReflex) },\n { position = vgHeroineStat.willpower, value = tostring(currentWillpower) },\n })\n\n setChatVar(triggerId, \"vg_Mode\", tostring(constMode.vg))\n setChatVar(triggerId, \"vg_Mode_Save\", \"0\")\n initSessionInfo(triggerId, constStoryType.side, \"\", math.max(1, getHeroineLevel(triggerId)), constSessionDifficulty.boss, constPhase.event_H, constEncounterTrait.none)\n\n setPhase(triggerId, tostring(constPhase.event_H))\n\n resetAndDrawPlayDeck(triggerId)\n buildCurrentState(triggerId)\n buildEncounterActionSetting(triggerId)\n buildSexualProfile(triggerId, dateValue)\n\n local lines = {}\n\n table.insert(lines, \"[H Event Select: \" .. situationValue .. \" | Emoji: \" .. emojiValue .. \"]\")\n\n if arousalChanged then\n table.insert(\n lines,\n \"[Temporary Log: 💗 \" ..\n formatFloat(nextArousal) ..\n \" / \" ..\n formatFloat(maxArousal) ..\n \" (\" ..\n getSignedEffectText(nextArousal - currentArousal) ..\n \")]\"\n )\n end\n\n table.insert(lines, \"▷ [The heroine is in a state where she is unable to continue effective resistance.]\")\n table.insert(lines, \"▷ [Describe the background and surrounding circumstances of this sexual violation scenario: when and where the situation takes place, either in the same location or after the heroine has been moved elsewhere, whether it begins immediately or after some time has passed, what condition the heroine is in, who or what the encounter is, how the encounter has gained control over the situation, what causes the situation to move toward sexual danger, and how that danger begins to take shape.]\")\n table.insert(lines, \"▷ [If the situation involves a change of location or the passage of time beforehand, briefly describe the transition, how it happened, and what occurred during the intervening period.]\")\n table.insert(lines, \"▷ [If not already apparent, the encounter now clearly reveals and prepares the means it will use for the sexual violation. If the encounter includes a male participant, this includes the male exposing his genitals; otherwise, it should naturally reveal, ready, or activate whatever means it will use.]\")\n table.insert(lines, \"▷ [The encounter moves to initiate the sexual violation against the incapacitated heroine. The response should build the immediate prelude and stop at the final moment before the act begins, without describing the act itself, direct sexual contact, penetration, or completion.]\")\n insertSexualActRequirementLine(lines, constPhase.event_H)\n\n pushChoiceInput(table.concat(lines, \"\\n\"))\n return\n\n -- ==========================================\n -- H 이벤트 날짜 경과 선택\n -- ==========================================\n elseif choiceType == \"selectHEventTimePass\" then\n local situationValue = tostring(parts[3] or \"\")\n local emojiValue = tostring(parts[4] or \"\")\n\n if not isLastChatUser(triggerId) then\n alertNormal(triggerId, \"❌ 행동을 먼저 선택하세요.\")\n return\n end\n\n local encounterStat = loadDualFlagNextMap(triggerId, vgEncounterStat)\n local encounterHp = tonumber(encounterStat.hp) or 0\n\n if encounterHp <= 0 then\n return\n end\n\n local cl = getChatLength(triggerId)\n local last = getChat(triggerId, cl - 1)\n\n if not last or last.role ~= \"user\" or type(last.data) ~= \"string\" then\n return\n end\n\n local lastText = tostring(last.data or \"\")\n\n local lang = getLanguage(triggerId)\n\n local inputMessageMap = {\n [1] = \"경과시킬 날짜 수를 입력하세요. (1~7)\",\n [2] = \"Enter the number of days to pass. (1-7)\",\n [3] = \"経過させる日数を入力してください。(1〜7)\",\n }\n\n local input = alertInput(\n triggerId,\n inputMessageMap[lang] or inputMessageMap[2]\n ):await()\n\n input = trim(tostring(input or \"\"))\n\n if input == \"\" then\n return\n end\n\n if not input:match(\"^%d+$\") then\n alertNormal(triggerId, \"❌ 숫자만 입력할 수 있습니다.\")\n return\n end\n\n local passDays = tonumber(input)\n\n if not passDays or passDays < 1 or passDays > 7 then\n alertNormal(triggerId, \"❌ 날짜 경과는 7일 이내로 입력해야 합니다.\")\n return\n end\n\n local currentDateText = trim((select(1, getLatestCharDateTime(triggerId))) or \"\")\n currentDateText = normalizeLooseDateText(currentDateText)\n\n local currentDateTable = parseISODateText(currentDateText)\n if not currentDateTable then\n return\n end\n\n local nextDateTable = addDaysToDate(currentDateTable, passDays)\n local nextDateText = formatISODateTable(nextDateTable)\n\n if nextDateText == \"\" then\n return\n end\n\n local function isFertileCycleFlag(cycleFlag)\n cycleFlag = tonumber(cycleFlag) or 0\n\n return cycleFlag >= constCycle.fertile_m5\n and cycleFlag <= constCycle.fertile_p1\n end\n\n local currentCycleFlag = getCycleFlag(triggerId, currentDateText)\n local nextCycleFlag = getCycleFlag(triggerId, nextDateText)\n\n buildSexualProfile(triggerId, nextDateText)\n\n -- 기존 H Event Time Pass 라인이 있으면 제거\n lastText = lastText:gsub(\"%[H Event Time Pass Select: [^\\n]- | Emoji: [^\\n]-%]%s*\\n?\", \"\")\n lastText = lastText:gsub(\"▷ %[%s*Date Pass:%s*[^\\n]-%]%s*\\n?\", \"\")\n lastText = lastText:gsub(\"▷ %[Vaginal Ejaculation Records: [^\\n]-%]%s*\\n?\", \"\")\n lastText = trim(lastText)\n\n local topText = \"[H Event Time Pass Select: (\" .. nextDateText .. \") \" .. situationValue .. \" | Emoji: \" .. emojiValue .. \"]\"\n local bottomText = \"▷ [Date Pass: After the actions above have occurred, describe what happened to the heroine up to \" .. nextDateText .. \", including possible changes in her location, place of confinement, involved parties, who controls the situation, affiliation, status, clothing, role, restraints, restrictions, captors, or overall circumstances. If sexual activity continued during that time, briefly describe what kinds of sexual acts occurred. The heroine must still remain incapacitated, with no possibility of escape.]\"\n\n if isFertileCycleFlag(currentCycleFlag) and isFertileCycleFlag(nextCycleFlag) then\n bottomText = bottomText .. \"\\n▷ [Vaginal Ejaculation Records: During the passed time, if intravaginal ejaculation is reasonably likely to have occurred, treat it as having occurred and record the approximate count.]\"\n end\n\n if lastText ~= \"\" then\n lastText = topText .. \"\\n\" .. lastText .. \"\\n\" .. bottomText\n else\n lastText = topText .. \"\\n\" .. bottomText\n end\n\n setChat(triggerId, cl - 1, lastText)\n return\n -- ==========================================\n -- 가드 우선순위 선택\n -- ==========================================\n elseif choiceType == \"selectGuard\" then\n local priority = tonumber(parts[3]) or 1\n setChatVar(triggerId, \"vg_GuardPriority\", tostring(priority))\n reloadDisplay(triggerId)\n return\n\n -- ==========================================\n -- 현재 목표 선택\n -- ==========================================\n elseif choiceType == \"selectGoal\" then\n local action = tostring(parts[3] or \"\")\n\n if action == \"1\" then\n setGoalValue(triggerId, \"vg_Goal_Short\", \"현재 목표\")\n\n elseif action == \"2\" then\n setChatVar(triggerId, \"vg_Goal_Short\", \"\")\n generateHTML(triggerId)\n reloadDisplay(triggerId)\n alertNormal(triggerId, \"❌ 현재 목표 삭제\")\n\n elseif action == \"3\" then\n if not isLastChatUser(triggerId) then\n alertNormal(triggerId, \"❌ 선택지를 먼저 선택하세요.\")\n return\n end\n\n local curMode = tonumber(safeGet(triggerId, \"vg_Mode\", tostring(constMode.free))) or constMode.free\n if curMode ~= constMode.vg then\n alertNormal(triggerId, \"❌ VG 모드로 전환 시에만 사용 가능합니다.\")\n return\n end\n\n local cl = getChatLength(triggerId)\n local last = getChat(triggerId, cl - 1)\n local lastText = tostring(last.data or \"\")\n\n local goalUpdateLine = \"[Goal Update: 현재 목표 재설정 | Emoji: 🔄]\"\n local pattern = \"%[Goal Update:[^\\n]-%]%s*\\n?\"\n\n -- 이미 있으면 제거\n if lastText:find(pattern) ~= nil then\n lastText = lastText:gsub(pattern, \"\")\n lastText = trim(lastText)\n\n setChatVar(triggerId, \"vg_ChangeGoal\", \"0\")\n setChat(triggerId, cl - 1, lastText)\n\n return\n end\n\n -- 없으면 추가\n if lastText ~= \"\" then\n lastText = goalUpdateLine .. \"\\n\" .. lastText\n else\n lastText = goalUpdateLine\n end\n\n setChatVar(triggerId, \"vg_ChangeGoal\", \"1\")\n setChat(triggerId, cl - 1, lastText)\n\n end\n\n return\n\n -- ==========================================\n -- 추가 입력 메시지 선택\n -- ==========================================\n elseif choiceType == \"selectAdditionalMessage\" then\n local emojiValue = tostring(parts[3] or \"\")\n\n if not isLastChatUser(triggerId) then\n alertNormal(triggerId, \"❌ 선택지를 먼저 선택하세요.\")\n return\n end\n\n local cl = getChatLength(triggerId)\n local last = getChat(triggerId, cl - 1)\n local lastText = tostring(last.data or \"\")\n\n local rawInput = alertInput(\n triggerId,\n \"추가 입력 메시지를 입력하세요.\\n추가 입력 메시지를 삭제하고자 할 경우에는 인풋을 지우고 선택지를 다시 선택하세요.\"\n ):await()\n\n if rawInput == nil then\n return\n end\n\n local input = trim(tostring(rawInput or \"\"))\n\n if input == \"\" then\n return\n end\n\n lastText = lastText:gsub(\"%[Additional Input: [^\\n]- | Emoji: [^\\n]-%]%s*\\n?\", \"\")\n lastText = lastText:gsub(\"%[Daily Variation: [^\\n]-%]%s*\\n?\", \"\")\n\n lastText = trim(lastText)\n\n local addLine = \"[Additional Input: \" .. input .. \" | Emoji: \" .. emojiValue .. \"]\"\n\n -- 맨 위에 다시 삽입\n if lastText ~= \"\" then\n lastText = addLine .. \"\\n\" .. lastText\n else\n lastText = addLine\n end\n\n setChat(triggerId, cl - 1, lastText)\n return\n\n -- ==========================================\n -- 종료 추가 입력 메시지 선택\n -- ==========================================\n elseif choiceType == \"selectAftermathMessage\" then\n local emojiValue = tostring(parts[3] or \"\")\n\n local cl = getChatLength(triggerId)\n local last = getChat(triggerId, cl - 1)\n local lastText = tostring(last.data or \"\")\n\n local rawInput = alertInput(\n triggerId,\n \"상황 종료 후 추가 입력 메시지를 입력하세요.\\n추가 입력 메시지를 삭제하고자 할 경우에는 인풋을 지우고 선택지를 다시 선택하세요.\"\n ):await()\n\n if rawInput == nil then\n return\n end\n\n local input = trim(tostring(rawInput or \"\"))\n\n -- 빈칸 입력은 수정 취소로 처리\n if input == \"\" then\n return\n end\n\n -- 기존 Aftermath Input만 제거\n lastText = lastText:gsub(\"%[Aftermath Input: [^\\n]-%]%s*\\n?\", \"\")\n\n local addLine = \"[Aftermath Input: \" .. input .. \"]\"\n\n -- Aftermath Message 바로 아래에 삽입\n lastText = lastText:gsub(\n \"(%[Aftermath Message: [^\\n]- | Emoji: [^\\n]-%])\",\n function(line)\n return line .. \"\\n\" .. addLine\n end,\n 1\n )\n\n lastText = trim(lastText)\n\n setChat(triggerId, cl - 1, lastText)\n return\n \n -- ==========================================\n -- 모드 변경 추가 입력 메시지 선택\n -- ==========================================\n elseif choiceType == \"selectModeMessage\" then\n local cl = getChatLength(triggerId)\n local last = getChat(triggerId, cl - 1)\n local lastText = tostring(last.data or \"\")\n\n local rawInput = alertInput(\n triggerId,\n \"모드 변경 후 추가 입력 메시지를 입력하세요.\\n추가 입력 메시지를 삭제하고자 할 경우에는 인풋을 지우고 선택지를 다시 선택하세요.\"\n ):await()\n\n if rawInput == nil then\n return\n end\n\n local input = trim(tostring(rawInput or \"\"))\n\n -- 빈칸 입력은 수정 취소로 처리\n if input == \"\" then\n return\n end\n\n -- 기존 Mode Input만 제거\n lastText = lastText:gsub(\"%[Mode Input: [^\\n]-%]%s*\\n?\", \"\")\n\n local addLine = \"[Mode Input: \" .. input .. \"]\"\n\n -- Mode Message 바로 아래에 삽입\n lastText = lastText:gsub(\n \"(%[Mode Message: [^\\n]- | Emoji: [^\\n]-%])\",\n function(line)\n return line .. \"\\n\" .. addLine\n end,\n 1\n )\n\n lastText = trim(lastText)\n\n setChat(triggerId, cl - 1, lastText)\n return\n\n -- ==========================================\n -- 추가 기록 선택\n -- ==========================================\n elseif choiceType == \"selectAddRecord\" or choiceType == \"selectAddSoloRecord\" then\n\n local addRecordText = trim(tostring(parts[3] or \"\"))\n local emojiValue = tostring(parts[4] or \"\")\n\n local selectLabel = \"Story Record Select\"\n local resolutionValue = constResolution.addRecord\n\n if choiceType == \"selectAddSoloRecord\" then\n selectLabel = \"Solo Record Select\"\n\n local recordTypeCode = tonumber(parts[5]) or constResolution.none\n\n if recordTypeCode == constResolution.addMasturbationRecord or recordTypeCode == constResolution.addExhibitionismRecord then\n resolutionValue = recordTypeCode\n else\n return\n end\n end\n\n if not isLastChatUser(triggerId) then\n alertNormal(triggerId, \"❌ 선택지를 먼저 선택하세요.\")\n return\n end\n\n local cl = getChatLength(triggerId)\n local last = getChat(triggerId, cl - 1)\n local lastText = tostring(last.data or \"\")\n\n local resolutionFlag =\n tonumber(safeGet(triggerId, \"vg_Resolution_Flag\", tostring(constResolution.none)))\n or constResolution.none\n\n local pattern = \"%[\" .. selectLabel .. \": [^\\n]- | Emoji: [^\\n]-%]%s*\\n?\"\n local hasRecordSelect = lastText:find(pattern) ~= nil\n\n -- 이미 있으면 해제\n if hasRecordSelect then\n lastText = lastText:gsub(pattern, \"\")\n lastText = trim(lastText)\n\n -- 이 선택지가 넣은 값일 때만 0으로 되돌림\n if resolutionFlag == resolutionValue then\n setChatVar(triggerId, \"vg_Resolution_Flag\", tostring(constResolution.none))\n end\n\n setChat(triggerId, cl - 1, lastText)\n return\n end\n\n -- 없으면 vg_Resolution_Flag가 0일 때만 추가\n if resolutionFlag ~= constResolution.none then\n return\n end\n\n setChatVar(triggerId, \"vg_Resolution_Flag\", tostring(resolutionValue))\n\n local addLine = \"[\" ..\n selectLabel ..\n \": \" ..\n addRecordText ..\n \" | Emoji: \" ..\n emojiValue ..\n \"]\"\n\n if lastText ~= \"\" then\n lastText = addLine .. \"\\n\" .. lastText\n else\n lastText = addLine\n end\n\n setChat(triggerId, cl - 1, lastText)\n return\n\n -- ==========================================\n -- 핸드 선택\n -- ==========================================\n elseif choiceType == \"selectHand\" then\n local curPhase = getCurrentPhase(triggerId)\n\n local function applyImmediateHeroineBuffToSelectedActions(playerActionList)\n local turnBuffTotal = 0\n\n -- 1) 이번 턴 선택한 카드들 중 Buff 총합 계산\n for _, action in ipairs(playerActionList or {}) do\n for _, effect in ipairs(action.effects or {}) do\n if (tonumber(effect.code) or 0) == constEffectType.buff then\n turnBuffTotal = roundToTenth(turnBuffTotal + (tonumber(effect.finalValue) or 0))\n end\n end\n end\n\n if turnBuffTotal == 0 then\n return\n end\n\n -- 2) 이번 턴 선택한 Primary / Secondary 카드 전체에 즉시 적용\n for _, action in ipairs(playerActionList or {}) do\n if isHeroineBuffTargetCardType(action.cardType) then\n for _, effect in ipairs(action.effects or {}) do\n if effect.code ~= constEffectType.guardbreak and effect.code ~= constEffectType.rage then\n local currentValue =\n tonumber(effect.finalValue)\n or tonumber(effect.attemptValue)\n or tonumber(effect.effectValue)\n or tonumber(effect.baseValue)\n or 0\n\n local buffedValue = roundToTenth(currentValue + turnBuffTotal)\n\n if buffedValue < 0.1 then\n buffedValue = 0.1\n end\n\n effect.effectValue = buffedValue\n effect.attemptValue = buffedValue\n effect.finalValue = buffedValue\n end\n end\n end\n end\n end\n\n local function appendMeterLog(appendLines, emoji, curValue, nextValue, maxValue)\n curValue = tonumber(curValue) or 0\n nextValue = tonumber(nextValue) or 0\n maxValue = tonumber(maxValue) or 0\n\n if curValue == nextValue then\n return\n end\n\n local delta = nextValue - curValue\n table.insert(\n appendLines,\n \"[Temporary Log: \" .. emoji .. \" \" .. formatFloat(nextValue) .. \" / \" .. formatFloat(maxValue) .. \" (\" .. getSignedEffectText(delta) .. \")]\"\n )\n end\n\n local function appendBuffLog(appendLines, emoji, curValue, nextValue)\n curValue = tonumber(curValue) or 0\n nextValue = tonumber(nextValue) or 0\n\n local delta = roundToTenth(nextValue - curValue)\n\n if nextValue == 0 and delta == 0 then\n return\n end\n\n local text = \"[Temporary Log: \" .. emoji .. \" \" .. getSignedEffectText(nextValue)\n\n if delta ~= 0 then\n text = text .. \" (\" .. getSignedEffectText(delta) .. \")\"\n end\n\n text = text .. \"]\"\n\n table.insert(appendLines, text)\n end\n\n local function appendPercentBuffLog(appendLines, emoji, curValue, nextValue)\n curValue = roundToTenth(tonumber(curValue) or 0)\n nextValue = roundToTenth(tonumber(nextValue) or 0)\n\n if curValue == 0 and nextValue == 0 then\n return\n end\n\n local delta = roundToTenth(nextValue - curValue)\n\n local text = \"[Temporary Log: \" .. emoji .. \" \" .. getSignedEffectText(nextValue) .. \"%\"\n\n if delta ~= 0 then\n text = text .. \" (\" .. getSignedEffectText(delta) .. \"%)\"\n end\n\n text = text .. \"]\"\n\n table.insert(appendLines, text)\n end\n\n local function applyGuardbreakToDefense(defenseValue, guardbreakPercent)\n defenseValue = roundToTenth(tonumber(defenseValue) or 0)\n guardbreakPercent = roundToTenth(tonumber(guardbreakPercent) or 0)\n\n if guardbreakPercent >= 0 then\n return defenseValue\n end\n\n local currentMultiplier = math.max(0, 1 + (defenseValue / 100))\n local nextMultiplier = currentMultiplier * math.max(0, 1 + (guardbreakPercent / 100))\n local nextDefenseValue = (nextMultiplier - 1) * 100\n\n return roundToTenth(nextDefenseValue)\n end\n\n local function appendClimaxOverflowLog(appendLines, displayValue, triggerValue, deltaValue)\n displayValue = math.max(0, tonumber(displayValue) or 0)\n triggerValue = math.max(1, tonumber(triggerValue) or 1)\n deltaValue = tonumber(deltaValue) or 0\n\n if displayValue <= 0 and deltaValue == 0 then\n return\n end\n\n table.insert(\n appendLines,\n \"[Temporary Log: 🌡️ \" .. tostring(displayValue) .. \" / \" .. tostring(triggerValue) .. \" (\" .. getSignedEffectText(deltaValue) .. \")]\"\n )\n end\n\n local function parsePackedEffects(raw)\n local t = {}\n\n raw = trim(tostring(raw or \"\"))\n if raw == \"\" then\n return t\n end\n\n for token in raw:gmatch(\"([^/]+)\") do\n local codeStr, valueStr = trim(token):match(\"^([%-+]?%d+)%s*:%s*([%-+]?[%d%.]+)$\")\n\n local code = tonumber(codeStr) or 0\n local value = tonumber(valueStr) or 0\n\n if code ~= 0 then\n table.insert(t, {\n code = code,\n value = value\n })\n end\n end\n\n return t\n end\n\n local function buildEffectTextFromEffectList(effectList)\n local texts = {}\n\n for _, effect in ipairs(effectList or {}) do\n local code = tonumber(effect.code) or 0\n\n if code ~= 0 and shouldKeepZeroEffectType(code) == false then\n local value = tonumber(effect.attemptValue)\n if value == nil then\n value = tonumber(effect.baseValue)\n end\n if value == nil then\n value = tonumber(effect.effectValue)\n end\n if value == nil then\n value = tonumber(effect.finalValue)\n end\n if value == nil then\n value = tonumber(effect.value) or 0\n end\n\n local valueText = getSignedEffectText(value)\n\n if isPercentEffectType(code) then\n valueText = valueText .. \"%\"\n end\n\n table.insert(texts, getEffectTypeEmoji(triggerId, code, curPhase) .. valueText)\n end\n end\n\n return table.concat(texts, \" \")\n end\n\n local function sortByLowFinalValueFirst(t)\n table.sort(t, function(a, b)\n local av = tonumber(a.finalValue) or 0\n local bv = tonumber(b.finalValue) or 0\n\n if av == bv then\n local an = tostring(a.actionName or \"\")\n local bn = tostring(b.actionName or \"\")\n return an < bn\n end\n\n return av < bv\n end)\n end\n\n local function consumeGuardPerEffect(effectList, guardPool)\n guardPool = roundToTenth(tonumber(guardPool) or 0)\n\n for _, effect in ipairs(effectList or {}) do\n local value = roundToTenth(tonumber(effect.finalValue) or 0)\n\n if guardPool > 0 and value > 0 then\n local blocked = roundToTenth(math.min(guardPool, value))\n effect.blockedValue = blocked\n effect.finalValue = roundToTenth(value - blocked)\n guardPool = roundToTenth(guardPool - blocked)\n else\n effect.blockedValue = 0\n effect.finalValue = value\n end\n\n effect.fullyNegated = ((tonumber(effect.finalValue) or 0) <= 0)\n end\n\n return guardPool\n end\n\n local function sumFinalValues(effectList)\n local total = 0\n\n for _, effect in ipairs(effectList or {}) do\n total = total + (tonumber(effect.finalValue) or 0)\n end\n\n return roundToTenth(total)\n end\n\n local function sumBlockedValues(effectList)\n local total = 0\n\n for _, effect in ipairs(effectList or {}) do\n total = total + (tonumber(effect.blockedValue) or 0)\n end\n\n return roundToTenth(total)\n end\n\n local function rollCombatIncontinenceByDamageAndState(damageValue, hp, maxHP, arousal, maxArousal, incontinenceCount)\n damageValue = math.max(0, tonumber(damageValue) or 0)\n hp = tonumber(hp) or 0\n maxHP = tonumber(maxHP) or 0\n arousal = tonumber(arousal) or 0\n maxArousal = tonumber(maxArousal) or 0\n incontinenceCount = math.max(0, math.floor(tonumber(incontinenceCount) or 0))\n\n if maxHP <= 0 or maxArousal <= 0 or damageValue <= 0 then\n return false\n end\n\n local hpRate = clamp(hp / maxHP, 0, 1)\n local arousalRate = clamp(arousal / maxArousal, 0, 1)\n\n local lewdnessLevel = getLewdnessLevel(triggerId)\n local arousalThresholdMultiplier = 0.75 + ((lewdnessLevel - 1) * 0.075)\n\n local arousalThresholdRate = clamp(arousalRate * arousalThresholdMultiplier, 0, 1)\n if hpRate > arousalThresholdRate then\n return false\n end\n\n -- 이번 교환에서 실제로 감소한 HP 비율\n local chanceRate = damageValue / maxHP\n\n -- 발동 횟수마다 요구치 +%\n local requiredChanceRate = 0.15 + (incontinenceCount * 0.05)\n\n if chanceRate < requiredChanceRate then\n return false\n end\n\n local lewdnessChanceMultiplier = 0.70 + ((lewdnessLevel - 1) * 0.03)\n chanceRate = chanceRate * arousalRate * lewdnessChanceMultiplier\n\n return math.random() < clamp(chanceRate, 0, 1)\n end\n\n local function stripCriticalPrefix(name)\n name = tostring(name or \"\")\n return name:gsub(\"^🍀 \", \"\")\n end\n\n local function appendGuardResultLines(targetLines, encounterActionList, playerActionList, isCleared)\n local mergedLines = {}\n\n local function getActionGuardResult(actionData, blockableCodeMap)\n local totalAttempt = 0\n local totalBlocked = 0\n\n for _, effect in ipairs(actionData.effects or {}) do\n local code = tonumber(effect.code) or 0\n\n if blockableCodeMap[code] then\n local attemptValue = math.max(0, tonumber(effect.attemptValue) or 0)\n local blockedValue = math.max(0, tonumber(effect.blockedValue) or 0)\n\n if attemptValue > 0 then\n totalAttempt = totalAttempt + attemptValue\n totalBlocked = totalBlocked + math.min(blockedValue, attemptValue)\n end\n end\n end\n\n if totalAttempt <= 0 then\n return nil\n end\n\n local blockedRatio = totalBlocked / totalAttempt\n return totalAttempt, totalBlocked, blockedRatio\n end\n\n do\n local blockableCodeMap = {\n [constEffectType.damage] = true,\n [constEffectType.suggestion] = true,\n\n [constEffectType.sexual] = true,\n\n [constEffectType.subdual] = true,\n\n [constEffectType.pressure] = true,\n [constEffectType.rhetoric] = true,\n\n [constEffectType.humiliation] = true,\n [constEffectType.escalation] = true,\n\n [constEffectType.defilement] = true,\n [constEffectType.invasion] = true,\n\n [constEffectType.interception] = true,\n }\n\n for _, action in ipairs(encounterActionList or {}) do\n local totalAttempt, totalBlocked, blockedRatio = getActionGuardResult(action, blockableCodeMap)\n local cleanActionName = stripCriticalPrefix(action.actionName)\n\n if totalAttempt then\n if blockedRatio >= 1 then\n local extraInstruction = \"\"\n if isCleared == false and (isIncapacitatedPhase(triggerId, curPhase) or isPromiscuityPhase(triggerId, curPhase) or isIntimacyPhase(triggerId, curPhase)) then\n --extraInstruction = \" In the next Encounter Action Setting, the next action must not target the same erogenous zone or orifice as this completely negated action, and must instead be aimed at a different one.\"\n --extraInstruction = \" In the next Encounter Action Setting, the next action must not target the same erogenous zone or orifice (e.g., oral, vaginal, anal) as this completely negated action. Prefer the valid target that was targeted least recently.\"\n --extraInstruction = \" In the next Encounter Action Setting, one Encounter Action must be a penetration act targeting a different valid orifice from this completely negated action, preferably the least recently targeted among oral, vaginal, and anal. The other Encounter Action must target a different erogenous zone or orifice from that penetration act.\"\n extraInstruction = \" In the next Encounter Action Setting, one Encounter Action must target a valid orifice different from the one targeted by this completely negated action, choosing the least recently targeted among oral, vaginal, and anal. The other Encounter Action must target a different erogenous zone or orifice.\"\n end\n\n table.insert(mergedLines, \"▷ [The heroine completely negates \" .. action.sourceName .. \"'s action, \\\"\" .. cleanActionName .. \"\\\".\" .. extraInstruction ..\"]\")\n elseif blockedRatio >= 0.75 then\n table.insert(mergedLines, \"▷ [The heroine blocks most of \" .. action.sourceName .. \"'s action, \\\"\" .. cleanActionName .. \"\\\", but it still gets through slightly.]\")\n elseif blockedRatio >= 0.5 then\n table.insert(mergedLines, \"▷ [The heroine blocks much of \" .. action.sourceName .. \"'s action, \\\"\" .. cleanActionName .. \"\\\", but it still gets through.]\")\n elseif blockedRatio >= 0.25 then\n table.insert(mergedLines, \"▷ [The heroine blocks part of \" .. action.sourceName .. \"'s action, \\\"\" .. cleanActionName .. \"\\\", but it still gets through.]\")\n elseif blockedRatio > 0 then\n table.insert(mergedLines, \"▷ [The heroine blocks only a small part of \" .. action.sourceName .. \"'s action, \\\"\" .. cleanActionName .. \"\\\", and it mostly gets through.]\")\n else\n local extraInstruction = \"\"\n if isIncapacitatedPhase(triggerId, curPhase) or isPromiscuityPhase(triggerId, curPhase) or isIntimacyPhase(triggerId, curPhase) then\n extraInstruction = \" It is treated as fully carried out, even if framed as a threat, an attempt, or a similar preliminary action.\"\n elseif isPerformancePhase(triggerId, curPhase) or isHypnosisPhase(triggerId, curPhase) then\n extraInstruction = \" If the action is framed as a demand, an instruction, or a command directed at the heroine, she must comply with it.\"\n end\n\n table.insert(mergedLines, \"▷ [The heroine fails to block \" .. action.sourceName .. \"'s action, \\\"\" .. cleanActionName .. \"\\\", and it gets through cleanly.\" .. extraInstruction .. \"]\")\n end\n end\n end\n end\n\n do\n local blockableCodeMap = {\n [constEffectType.damage] = true\n }\n\n for _, action in ipairs(playerActionList or {}) do\n local totalAttempt, totalBlocked, blockedRatio = getActionGuardResult(action, blockableCodeMap)\n local cleanActionName = stripCriticalPrefix(action.actionName)\n\n if totalAttempt then\n if blockedRatio >= 1 then\n table.insert(mergedLines, \"▷ [The encounter side completely negates the heroine's action, \\\"\" .. cleanActionName .. \"\\\".]\")\n\n elseif isCombatPhase(triggerId, curPhase) then\n if blockedRatio >= 0.75 then\n table.insert(mergedLines, \"▷ [The encounter side blocks most of the heroine's action, \\\"\" .. cleanActionName .. \"\\\", but it still gets through slightly.]\")\n elseif blockedRatio >= 0.5 then\n table.insert(mergedLines, \"▷ [The encounter side blocks much of the heroine's action, \\\"\" .. cleanActionName .. \"\\\", but it still gets through.]\")\n elseif blockedRatio >= 0.25 then\n table.insert(mergedLines, \"▷ [The encounter side blocks part of the heroine's action, \\\"\" .. cleanActionName .. \"\\\", but it still gets through.]\")\n elseif blockedRatio > 0 then\n table.insert(mergedLines, \"▷ [The encounter side blocks only a small part of the heroine's action, \\\"\" .. cleanActionName .. \"\\\", and it mostly gets through.]\")\n else\n table.insert(mergedLines, \"▷ [The encounter side fails to block the heroine's action, \\\"\" .. cleanActionName .. \"\\\", and it gets through cleanly.]\")\n end\n end\n end\n end\n end\n\n shuffleArray(mergedLines)\n\n for _, line in ipairs(mergedLines) do\n table.insert(targetLines, line)\n end\n end\n\n local function buildPendingPlayerLine(orderIndex, actionData)\n return \"[Action Order: \" .. tostring(orderIndex)\n .. \" | Code: \" .. actionData.code\n .. \" | Action: \" .. stripCriticalPrefix(actionData.actionName)\n .. \" | Type: \" .. (actionData.typeEmoji or \"❓\")\n .. \" | Level: \" .. getCardLevelStars(actionData.cardLevel)\n .. \" | Waiting]\"\n end\n\n local function buildPlayerResultLine(orderIndex, resultData)\n local costText = \"\"\n local effectText = buildEffectTextFromEffectList(resultData.effects)\n\n local hasRefresh = false\n local costValue = tonumber(resultData.costValue) or 0\n\n for _, effect in ipairs(resultData.effects or {}) do\n if (tonumber(effect.code) or 0) == constEffectType.refresh then\n hasRefresh = true\n break\n end\n end\n\n if hasRefresh and costValue <= 0 then\n costText = effectText\n effectText = \"\"\n elseif costValue > 0 then\n costText = \"⚡-\" .. formatFloat(costValue)\n end\n\n return \"[Action Order: \" .. tostring(orderIndex)\n .. \" | Code: \" .. resultData.code\n .. \" | Action: \" .. stripCriticalPrefix(resultData.actionName)\n .. \" | Type: \" .. (resultData.typeEmoji or \"❓\")\n .. \" | Level: \" .. getCardLevelStars(resultData.cardLevel)\n .. \" | Cost: \" .. costText\n .. \" | Effect: \" .. effectText .. \"]\"\n end\n\n local function buildEncounterResultLine(orderIndex, resultData)\n return \"[Encounter Order: \" .. tostring(orderIndex)\n .. \" | Source: \" .. resultData.sourceName\n .. \" | Action: \" .. stripCriticalPrefix(resultData.actionName)\n .. \" | Effect: \" .. buildEffectTextFromEffectList(resultData.effects) .. \"]\"\n end\n\n local function buildCompanionResultLine(orderIndex, resultData)\n return \"[Companion Order: \" .. tostring(orderIndex)\n .. \" | Source: \" .. resultData.sourceName\n .. \" | Action: \" .. stripCriticalPrefix(resultData.actionName)\n .. \"]\"\n end\n\n local selectedCode = trim(parts[3] or \"\")\n if selectedCode == \"\" then\n return\n end\n\n local _, runtimeTypeMap, runtimeLevelMap = buildRuntimeDeckLookup(triggerId)\n\n local function loadPlayerActionByCode(codeStr)\n codeStr = trim(codeStr or \"\")\n if codeStr == \"\" then\n return nil\n end\n\n local actionName = \"\"\n local costValue = 0\n local packedEffectRaw = \"\"\n local foundAction = false\n\n for _, row in ipairs(splitPipe(safeGet(triggerId, \"vg_PlayDeck_Action\", \"\"))) do\n local p = splitSection(row)\n local code = trim(p[1] or \"\")\n\n if code == codeStr then\n actionName = trim(p[2] or \"\")\n costValue = tonumber(p[3]) or 0\n packedEffectRaw = trim(p[4] or \"\")\n foundAction = true\n break\n end\n end\n\n if not foundAction then\n return nil\n end\n\n local cardType = runtimeTypeMap[codeStr] or 0\n local cardLevel = runtimeLevelMap[codeStr] or 0\n\n if codeStr == \"-9999\" then\n cardType = constCardType.surrender\n elseif codeStr == \"-9998\" then\n cardType = constCardType.prolonged\n elseif codeStr == \"-9997\" then\n cardType = constCardType.situationEnd\n end\n \n local typeEmoji = getCardTypeEmoji(triggerId, cardType)\n\n local effects = parsePackedEffects(packedEffectRaw)\n if #effects <= 0 then\n return nil\n end\n\n return {\n code = codeStr,\n actionName = actionName,\n cardType = cardType,\n cardLevel = cardLevel,\n typeEmoji = typeEmoji,\n costValue = costValue,\n effects = effects\n }\n end\n\n local function loadCompanionActions()\n local t = {}\n\n for _, row in ipairs(splitPipe(safeGet(triggerId, \"vg_Companion_Action\", \"\"))) do\n local p = splitSection(row)\n\n local sourceName = trim(p[1] or \"\")\n local actionName = trim(p[2] or \"\")\n\n if sourceName ~= \"\" and actionName ~= \"\" then\n table.insert(t, {\n sourceName = sourceName,\n actionName = actionName\n })\n end\n end\n\n return t\n end\n\n local function loadEncounterActions()\n local t = {}\n\n for _, row in ipairs(splitPipe(safeGet(triggerId, \"vg_Encounter_Action\", \"\"))) do\n local p = splitSection(row)\n\n local sourceName = trim(p[1] or \"\")\n local actionName = trim(p[2] or \"\")\n local packedEffectRaw = trim(p[3] or \"\")\n\n local effects = parsePackedEffects(packedEffectRaw)\n\n if sourceName ~= \"\" and actionName ~= \"\" and #effects > 0 then\n table.insert(t, {\n sourceName = sourceName,\n actionName = actionName,\n effects = effects\n })\n end\n end\n\n return t\n end\n\n local function processPlayerAction(actionData)\n local resultEffects = {}\n\n for _, effect in ipairs(actionData.effects or {}) do\n local value = tonumber(effect.value) or 0\n\n table.insert(resultEffects, {\n code = effect.code,\n baseValue = value,\n effectValue = value,\n attemptValue = value,\n blockedValue = 0,\n finalValue = value,\n fullyNegated = false\n })\n end\n\n return {\n code = actionData.code,\n actionName = actionData.actionName,\n cardType = actionData.cardType,\n cardLevel = actionData.cardLevel,\n typeEmoji = actionData.typeEmoji,\n costValue = tonumber(actionData.costValue) or 0,\n effects = resultEffects\n }\n end\n\n local function processEncounterAction(actionData)\n local resultEffects = {}\n\n for _, effect in ipairs(actionData.effects or {}) do\n local value = tonumber(effect.value) or 0\n\n table.insert(resultEffects, {\n code = effect.code,\n baseValue = value,\n effectValue = value,\n attemptValue = value,\n blockedValue = 0,\n finalValue = value,\n fullyNegated = false\n })\n end\n\n return {\n sourceName = actionData.sourceName,\n actionName = actionData.actionName,\n effects = resultEffects\n }\n end\n\n local cl = getChatLength(triggerId)\n local last = nil\n local lastText = \"\"\n\n if cl > 0 then\n last = getChat(triggerId, cl - 1)\n if last and last.role == \"user\" and type(last.data) == \"string\" then\n lastText = last.data\n end\n end\n\n local availableCount = tonumber(safeGet(triggerId, \"vg_Action_Available\", \"1\")) or 1\n availableCount = math.max(1, availableCount)\n\n local selectedCodesByOrder = {}\n\n for orderStr, codeStr in tostring(lastText):gmatch(\"%[Action Order:%s*(%d+)%s*|%s*Code:%s*([^|%]]+)\") do\n local order = tonumber(orderStr) or 0\n codeStr = trim(codeStr or \"\")\n\n if order >= 1 and codeStr ~= \"\" then\n selectedCodesByOrder[order] = codeStr\n end\n end\n\n local currentSelectedCount = 0\n for i = 1, availableCount do\n if selectedCodesByOrder[i] and selectedCodesByOrder[i] ~= \"\" then\n currentSelectedCount = i\n else\n break\n end\n end\n\n if currentSelectedCount >= availableCount then\n return\n end\n\n for i = 1, currentSelectedCount do\n if selectedCodesByOrder[i] == selectedCode then\n return\n end\n end\n\n local selectedAction = loadPlayerActionByCode(selectedCode)\n if not selectedAction then\n alertError(triggerId, \"❌ 선택한 카드 데이터를 찾을 수 없습니다.\")\n return\n end\n \n -- ==========================================\n -- 초기화\n -- ==========================================\n setPhase(triggerId, curPhase)\n clearNextDualFlag(triggerId, vgContext.varName)\n clearNextDualFlag(triggerId, vgSessionInfo.varName)\n clearNextDualFlag(triggerId, vgHeroineEffect.varName)\n\n local nextOrder = currentSelectedCount + 1\n selectedCodesByOrder[nextOrder] = selectedCode\n\n if nextOrder < availableCount then\n local waitingLines = {}\n\n for i = 1, nextOrder do\n local actionData = loadPlayerActionByCode(selectedCodesByOrder[i])\n\n if not actionData then\n alertError(triggerId, \"❌ 선택 중인 카드 데이터를 복원할 수 없습니다.\")\n return\n end\n\n table.insert(waitingLines, buildPendingPlayerLine(i, actionData))\n end\n\n pushChoiceInput(table.concat(waitingLines, \"\\n\"))\n return\n end\n\n -- ==========================================\n -- 액션 구성\n -- ==========================================\n local playerResults = {}\n\n for i = 1, availableCount do\n local actionData = loadPlayerActionByCode(selectedCodesByOrder[i])\n\n if not actionData then\n alertError(triggerId, \"❌ 선택한 카드 데이터를 복원할 수 없습니다.\")\n return\n end\n\n table.insert(playerResults, processPlayerAction(actionData))\n end\n\n local encounterSourceActions = loadEncounterActions()\n local encounterResults = {}\n\n for _, actionData in ipairs(encounterSourceActions) do\n table.insert(encounterResults, processEncounterAction(actionData))\n end\n\n local companionResults = loadCompanionActions()\n local hasCompanionAction = (#companionResults > 0)\n\n applyImmediateHeroineBuffToSelectedActions(playerResults)\n -- ==========================================\n -- 플레이어 액션 집계\n -- ==========================================\n local totalCost = 0\n\n local playerDamageEffects = {}\n local totalPlayerGuard = 0\n local totalPlayerBuff = 0\n local totalPlayerRage = 0\n local totalPlayerRefresh = 0\n local totalPlayerRecovery = 0\n local totalPlayerSexual = 0\n local totalPlayerCalm = 0\n\n local totalPlayerSleep = 0\n local totalPlayerImmobilized = 0\n local totalPlayerBrainwashed = 0\n\n local totalPlayerEncounterRecovery = 0\n\n local playerGuardbreakEffects = {}\n\n for _, action in ipairs(playerResults) do\n totalCost = totalCost + (tonumber(action.costValue) or 0)\n\n for _, effect in ipairs(action.effects or {}) do\n local code = tonumber(effect.code) or 0\n local value = tonumber(effect.finalValue) or 0\n\n if code == constEffectType.damage\n or code == constEffectType.liberation\n or code == constEffectType.appeasement\n or code == constEffectType.excitement then\n table.insert(playerDamageEffects, {\n parentAction = action,\n actionName = action.actionName,\n code = code,\n attemptValue = value,\n blockedValue = 0,\n finalValue = value,\n fullyNegated = false\n })\n\n elseif code == constEffectType.guardbreak then\n table.insert(playerGuardbreakEffects, {\n parentAction = action,\n actionName = action.actionName,\n code = code,\n attemptValue = value,\n blockedValue = 0,\n finalValue = value,\n fullyNegated = false\n })\n\n elseif code == constEffectType.guard then\n totalPlayerGuard = roundToTenth(totalPlayerGuard + value)\n\n elseif code == constEffectType.buff then\n totalPlayerBuff = roundToTenth(totalPlayerBuff + value)\n\n elseif code == constEffectType.rage then\n totalPlayerRage = roundToTenth(totalPlayerRage + value)\n\n elseif code == constEffectType.refresh then\n totalPlayerRefresh = roundToTenth(totalPlayerRefresh + value)\n\n elseif code == constEffectType.recovery then\n totalPlayerRecovery = roundToTenth(totalPlayerRecovery + value)\n\n elseif code == constEffectType.calm then\n totalPlayerCalm = roundToTenth(totalPlayerCalm + value)\n\n elseif code == constEffectType.sexual then\n totalPlayerSexual = roundToTenth(totalPlayerSexual + value)\n\n elseif code == constEffectType.sleep then\n totalPlayerSleep = roundToTenth(totalPlayerSleep + value)\n\n elseif code == constEffectType.immobilized then\n totalPlayerImmobilized = roundToTenth(totalPlayerImmobilized + value)\n\n elseif code == constEffectType.brainwashed then\n totalPlayerBrainwashed = roundToTenth(totalPlayerBrainwashed + value)\n\n elseif code == constEffectType.encounterRecovery then\n totalPlayerEncounterRecovery = roundToTenth(totalPlayerEncounterRecovery + value)\n\n end\n end\n end\n\n -- ==========================================\n -- 상대 액션 집계\n -- ==========================================\n local encounterAttackEffects = {}\n local encounterSuggestionEffects = {}\n local encounterPressureEffects = {}\n local encounterRhetoricEffects = {}\n local encounterInterceptionEffects = {}\n\n local encounterExhaustionEffects = {}\n local encounterSexualEffects = {}\n\n local encounterDefilementEffects = {}\n local encounterInvasionEffects = {}\n\n local encounterHumiliationEffects = {}\n local encounterEscalationEffects = {}\n\n local totalEncounterGuard = 0\n local totalEncounterBuff = 0\n\n for _, action in ipairs(encounterResults) do\n for _, effect in ipairs(action.effects or {}) do\n local code = tonumber(effect.code) or 0\n local value = tonumber(effect.finalValue) or 0\n\n if code == constEffectType.damage or code == constEffectType.subdual then\n table.insert(encounterAttackEffects, {\n parentAction = action,\n actionName = action.actionName,\n code = code,\n attemptValue = value,\n blockedValue = 0,\n finalValue = value,\n fullyNegated = false\n })\n\n elseif code == constEffectType.suggestion then\n table.insert(encounterSuggestionEffects, {\n parentAction = action,\n actionName = action.actionName,\n code = code,\n attemptValue = value,\n blockedValue = 0,\n finalValue = value,\n fullyNegated = false\n })\n\n elseif code == constEffectType.pressure then\n table.insert(encounterPressureEffects, {\n parentAction = action,\n actionName = action.actionName,\n code = code,\n attemptValue = value,\n blockedValue = 0,\n finalValue = value,\n fullyNegated = false\n })\n\n elseif code == constEffectType.rhetoric then\n table.insert(encounterRhetoricEffects, {\n parentAction = action,\n actionName = action.actionName,\n code = code,\n attemptValue = value,\n blockedValue = 0,\n finalValue = value,\n fullyNegated = false\n })\n\n elseif code == constEffectType.interception then\n table.insert(encounterInterceptionEffects, {\n parentAction = action,\n actionName = action.actionName,\n code = code,\n attemptValue = value,\n blockedValue = 0,\n finalValue = value,\n fullyNegated = false\n })\n\n elseif code == constEffectType.exhaustion then\n table.insert(encounterExhaustionEffects, {\n parentAction = action,\n actionName = action.actionName,\n code = code,\n attemptValue = value,\n blockedValue = 0,\n finalValue = value,\n fullyNegated = false\n })\n\n elseif code == constEffectType.sexual then\n table.insert(encounterSexualEffects, {\n parentAction = action,\n actionName = action.actionName,\n code = code,\n attemptValue = value,\n blockedValue = 0,\n finalValue = value,\n fullyNegated = false\n })\n\n elseif code == constEffectType.humiliation then\n table.insert(encounterHumiliationEffects, {\n parentAction = action,\n actionName = action.actionName,\n code = code,\n attemptValue = value,\n blockedValue = 0,\n finalValue = value,\n fullyNegated = false\n })\n\n elseif code == constEffectType.escalation then\n table.insert(encounterEscalationEffects, {\n parentAction = action,\n actionName = action.actionName,\n code = code,\n attemptValue = value,\n blockedValue = 0,\n finalValue = value,\n fullyNegated = false\n })\n\n elseif code == constEffectType.defilement then\n table.insert(encounterDefilementEffects, {\n parentAction = action,\n actionName = action.actionName,\n code = code,\n attemptValue = value,\n blockedValue = 0,\n finalValue = value,\n fullyNegated = false\n })\n\n elseif code == constEffectType.invasion then\n table.insert(encounterInvasionEffects, {\n parentAction = action,\n actionName = action.actionName,\n code = code,\n attemptValue = value,\n blockedValue = 0,\n finalValue = value,\n fullyNegated = false\n })\n\n elseif code == constEffectType.guard then\n totalEncounterGuard = roundToTenth(totalEncounterGuard + value)\n\n elseif code == constEffectType.buff then\n totalEncounterBuff = roundToTenth(totalEncounterBuff + value)\n end\n end\n end\n\n -- ==========================================\n -- 가드 상쇄 처리\n -- ==========================================\n local blockedByEncounterGuard = 0\n local blockedByPlayerGuard = 0\n\n sortByLowFinalValueFirst(playerDamageEffects)\n do\n local pool = totalEncounterGuard\n pool = consumeGuardPerEffect(playerDamageEffects, pool)\n blockedByEncounterGuard = roundToTenth(sumBlockedValues(playerDamageEffects))\n end\n\n do\n local pool = totalPlayerGuard\n local guardPriority = tonumber(trim(safeGet(triggerId, \"vg_GuardPriority\", \"1\"))) or 1\n local guardTargetLists = {}\n\n if isAbusePhase(triggerId, curPhase) or isConfrontationPhase(triggerId, curPhase) then\n if guardPriority == 2 then\n guardTargetLists = {\n encounterRhetoricEffects,\n encounterPressureEffects,\n encounterSexualEffects\n }\n elseif guardPriority == 3 then\n guardTargetLists = {\n encounterSexualEffects,\n encounterPressureEffects,\n encounterRhetoricEffects\n }\n elseif guardPriority == 4 then\n guardTargetLists = {\n encounterSexualEffects,\n encounterRhetoricEffects,\n encounterPressureEffects\n }\n else\n guardTargetLists = {\n encounterPressureEffects,\n encounterRhetoricEffects,\n encounterSexualEffects\n }\n end\n\n elseif isHypnosisPhase(triggerId, curPhase) then\n if guardPriority == 2 then\n guardTargetLists = {\n encounterSexualEffects,\n encounterSuggestionEffects\n }\n else\n guardTargetLists = {\n encounterSuggestionEffects,\n encounterSexualEffects\n }\n end\n\n elseif isPerformancePhase(triggerId, curPhase) then\n if guardPriority == 2 then\n guardTargetLists = {\n encounterEscalationEffects,\n encounterHumiliationEffects\n }\n else\n guardTargetLists = {\n encounterHumiliationEffects,\n encounterEscalationEffects\n }\n end\n\n elseif isIncapacitatedPhase(triggerId, curPhase)\n or isPromiscuityPhase(triggerId, curPhase)\n or isIntimacyPhase(triggerId, curPhase) then\n if guardPriority == 2 then\n guardTargetLists = {\n encounterDefilementEffects,\n encounterInvasionEffects\n }\n else\n guardTargetLists = {\n encounterInvasionEffects,\n encounterDefilementEffects\n }\n end\n\n elseif isRescuePhase(triggerId, curPhase) then\n if guardPriority == 2 then\n guardTargetLists = {\n encounterSexualEffects,\n encounterInterceptionEffects\n }\n else\n guardTargetLists = {\n encounterInterceptionEffects,\n encounterSexualEffects\n }\n end\n\n else\n if guardPriority == 2 then\n guardTargetLists = {\n encounterSexualEffects,\n encounterAttackEffects\n }\n else\n guardTargetLists = {\n encounterAttackEffects,\n encounterSexualEffects\n }\n end\n end\n\n for _, effectList in ipairs(guardTargetLists) do\n sortByLowFinalValueFirst(effectList)\n pool = consumeGuardPerEffect(effectList, pool)\n end\n\n blockedByPlayerGuard = roundToTenth(\n sumBlockedValues(encounterAttackEffects) +\n sumBlockedValues(encounterSuggestionEffects) +\n sumBlockedValues(encounterPressureEffects) +\n sumBlockedValues(encounterRhetoricEffects) +\n sumBlockedValues(encounterInterceptionEffects) +\n sumBlockedValues(encounterHumiliationEffects) +\n sumBlockedValues(encounterEscalationEffects) +\n sumBlockedValues(encounterSexualEffects) +\n sumBlockedValues(encounterDefilementEffects) +\n sumBlockedValues(encounterInvasionEffects)\n )\n end\n\n -- ==========================================\n -- 최종 적용 수치 계산\n -- ==========================================\n local totalPlayerDamage = sumFinalValues(playerDamageEffects)\n\n local totalEncounterDamage = roundToTenth(\n sumFinalValues(encounterAttackEffects) +\n sumFinalValues(encounterSuggestionEffects) +\n sumFinalValues(encounterPressureEffects) +\n sumFinalValues(encounterRhetoricEffects) +\n sumFinalValues(encounterInterceptionEffects)\n )\n\n local totalEncounterExhaustion = sumFinalValues(encounterExhaustionEffects)\n local totalEncounterSexual = sumFinalValues(encounterSexualEffects)\n\n local totalEncounterDefilement = sumFinalValues(encounterDefilementEffects)\n local totalEncounterInvasion = sumFinalValues(encounterInvasionEffects)\n\n local totalEncounterHumiliation = sumFinalValues(encounterHumiliationEffects)\n local totalEncounterEscalation = sumFinalValues(encounterEscalationEffects)\n\n local totalEncounterViolationSelfDamage = 0\n if isHEventPhase(triggerId, curPhase) == false then\n totalEncounterViolationSelfDamage = roundToTenth(\n totalEncounterDefilement +\n totalEncounterInvasion\n )\n end\n\n local totalEncounterSelfDamage = roundToTenth(\n totalEncounterHumiliation +\n totalEncounterEscalation +\n totalEncounterViolationSelfDamage\n )\n\n local totalEncounterViolationArousal = roundToTenth(totalEncounterDefilement + totalEncounterInvasion)\n local totalEncounterPerformanceArousal = roundToTenth(totalEncounterHumiliation)\n\n local totalEncounterArousal = roundToTenth(\n totalEncounterSexual +\n totalEncounterPerformanceArousal +\n totalEncounterViolationArousal\n )\n\n local function applyResolvedValueBack(flatEffectList)\n for _, flat in ipairs(flatEffectList or {}) do\n local parentAction = flat.parentAction\n local targetCode = tonumber(flat.code) or 0\n local targetAttempt = tonumber(flat.attemptValue) or 0\n\n for _, effect in ipairs(parentAction.effects or {}) do\n local code = tonumber(effect.code) or 0\n local attempt = tonumber(effect.attemptValue) or 0\n\n if code == targetCode and attempt == targetAttempt and (tonumber(effect.blockedValue) or 0) == 0 then\n effect.blockedValue = tonumber(flat.blockedValue) or 0\n effect.finalValue = tonumber(flat.finalValue) or 0\n effect.fullyNegated = ((tonumber(effect.finalValue) or 0) <= 0)\n break\n end\n end\n end\n end\n\n applyResolvedValueBack(playerDamageEffects)\n applyResolvedValueBack(encounterAttackEffects)\n applyResolvedValueBack(encounterSuggestionEffects)\n applyResolvedValueBack(encounterPressureEffects)\n applyResolvedValueBack(encounterRhetoricEffects)\n applyResolvedValueBack(encounterExhaustionEffects)\n applyResolvedValueBack(encounterSexualEffects)\n applyResolvedValueBack(encounterHumiliationEffects)\n applyResolvedValueBack(encounterEscalationEffects)\n applyResolvedValueBack(encounterDefilementEffects)\n applyResolvedValueBack(encounterInvasionEffects)\n applyResolvedValueBack(encounterInterceptionEffects)\n\n local heroineStat = loadDualFlagCurrentMap(triggerId, vgHeroineStat)\n local heroineEffect = loadDualFlagCurrentMap(triggerId, vgHeroineEffect)\n local encounterStat = loadDualFlagCurrentMap(triggerId, vgEncounterStat)\n\n local curHP = tonumber(heroineStat.hp) or 0\n local curMaxHP = tonumber(heroineStat.hp_Max) or 0\n local curResource = tonumber(heroineStat.resource) or 0\n local curMaxResource = tonumber(heroineStat.resource_Max) or 0\n local curArousal = tonumber(heroineStat.arousal) or 0\n local curMaxArousal = tonumber(heroineStat.arousal_Max) or 0\n\n local curEncounterHP = tonumber(encounterStat.hp) or 0\n local curEncounterMaxHP = tonumber(encounterStat.hp_Max) or 0\n\n local curEncounterDefense = tonumber(encounterStat.defense) or 0\n\n local curHeroineBuff = tonumber(heroineEffect.buff) or 0\n local curClimaxCount = tonumber(heroineEffect.climaxCount) or 0\n\n local curEncounterBuff = tonumber(encounterStat.buff) or 0\n\n local turnEncounterBuffGain = 0\n\n if isIncapacitatedPhase(triggerId, curPhase)\n or isPerformancePhase(triggerId, curPhase)\n or isPromiscuityPhase(triggerId, curPhase)\n or isIntimacyPhase(triggerId, curPhase) then\n turnEncounterBuffGain = math.max(0.1, floorToTenth(getEncounterStatValue(triggerId) * 0.01))\n elseif isHEventPhase(triggerId, curPhase) then\n turnEncounterBuffGain = math.max(0.1, floorToTenth(curMaxArousal * 0.005))\n end\n\n local encounterTraitValue = tonumber(getResolvedDualFlag(triggerId, vgSessionInfo.varName, vgSessionInfo.trait, constEncounterTrait.none)) or constEncounterTrait.none\n if encounterTraitValue == constEncounterTrait.adaptive and not isExhibitionismPhase(triggerId, curPhase) then\n turnEncounterBuffGain = roundToTenth(turnEncounterBuffGain + math.max(0.1, floorToTenth(getEncounterStatValue(triggerId) * 0.02)))\n end\n\n local totalArousalChange = roundToTenth(totalEncounterArousal + totalPlayerSexual + totalPlayerCalm)\n\n local nextResource = roundToTenth(clamp(curResource - totalCost - totalEncounterExhaustion + totalPlayerRefresh, 0, curMaxResource))\n local nextArousal = roundToTenth(clamp(curArousal + totalArousalChange, 0, curMaxArousal))\n local nextEncounterHP = roundToTenth(clamp(curEncounterHP - totalPlayerDamage - totalEncounterSelfDamage + totalPlayerEncounterRecovery, 0, curEncounterMaxHP))\n\n local nextHeroineBuff = roundToTenth(curHeroineBuff + totalPlayerBuff)\n local nextEncounterBuff = roundToTenth(curEncounterBuff + totalEncounterBuff + turnEncounterBuffGain + totalPlayerRage)\n\n local nextEncounterDefense = curEncounterDefense\n\n for _, effect in ipairs(playerGuardbreakEffects) do\n local guardbreakPercent = tonumber(effect.finalValue) or 0\n nextEncounterDefense = applyGuardbreakToDefense(nextEncounterDefense, guardbreakPercent)\n end\n\n nextEncounterDefense = roundToTenth(nextEncounterDefense)\n\n -- ==========================================\n -- 상태 이상\n -- ==========================================\n local function calcNextStatusTurn(curValue, addValue)\n curValue = math.max(0, tonumber(curValue) or 0)\n addValue = math.max(0, tonumber(addValue) or 0)\n\n local nextValue = curValue\n\n -- 턴 종료로 1 감소\n if nextValue >= 1 then\n nextValue = nextValue - 1\n end\n\n -- 선택한 카드 효과로 연장\n nextValue = nextValue + addValue\n\n -- 상태이상 턴은 정수 취급\n return math.max(0, nextValue)\n end\n\n local curSleep = math.max(0, tonumber(heroineEffect.sleep) or 0)\n local nextSleep = calcNextStatusTurn(curSleep, totalPlayerSleep)\n\n local curImmobilized = math.max(0, tonumber(heroineEffect.immobilized) or 0)\n local nextImmobilized = calcNextStatusTurn(curImmobilized, totalPlayerImmobilized)\n\n local curBrainwashed = math.max(0, tonumber(heroineEffect.brainwashed) or 0)\n local nextBrainwashed = calcNextStatusTurn(curBrainwashed, totalPlayerBrainwashed)\n\n -- ==========================================\n -- 절정 계산\n -- ==========================================\n local mustClimaxCount = 0\n local climaxHPDamage = 0\n local climaxDamagePerHit = 0\n\n local overflowTriggerValue = math.max(1, curMaxArousal * 0.5)\n\n local rawCurrentOverflow = math.max(0, tonumber(heroineEffect.climaxPoint) or 0)\n local currentOverflow = rawCurrentOverflow % overflowTriggerValue\n local nextOverflow = currentOverflow\n\n local overflowDisplayValue = 0\n local overflowDisplayDelta = 0\n\n local totalNetArousalGainForClimax = math.max(0, totalArousalChange)\n\n if totalNetArousalGainForClimax > 0 and nextArousal >= curMaxArousal then\n local overflowGain = totalNetArousalGainForClimax\n\n -- 아래에서 100%를 찍은 경우, 100%까지 채우는 데 쓴 분량은 overflow에서 제외\n if curArousal < curMaxArousal then\n local needToMax = math.max(0, curMaxArousal - curArousal)\n overflowGain = math.max(0, totalNetArousalGainForClimax - needToMax)\n end\n\n -- 일반 phase만 100% 도달 시 기본 1회 절정\n -- masturbation phase는 여기서 +1 하지 않음\n if curClimaxCount == 0 and isMasturbationPhase(triggerId, curPhase) == false then\n mustClimaxCount = mustClimaxCount + 1\n end\n\n -- 초과분 누적 -> 50% 임계치 넘을 때마다 추가 절정\n if overflowGain > 0 then\n local totalOverflow = currentOverflow + overflowGain\n local extraClimaxCount = math.floor(totalOverflow / overflowTriggerValue)\n\n overflowDisplayValue = roundToTenth(totalOverflow)\n overflowDisplayDelta = roundToTenth(overflowGain)\n nextOverflow = roundToTenth(totalOverflow % overflowTriggerValue)\n\n mustClimaxCount = mustClimaxCount + extraClimaxCount\n end\n else\n -- 100% 아래로 내려가면 overflow 초기화\n if nextArousal < curMaxArousal then\n nextOverflow = 0\n end\n end\n\n if mustClimaxCount >= 1 then\n if isMasturbationPhase(triggerId, curPhase) == false\n and isIncapacitatedPhase(triggerId, curPhase) == false\n and isHEventPhase(triggerId, curPhase) == false\n and isPerformancePhase(triggerId, curPhase) == false\n and isExhibitionismPhase(triggerId, curPhase) == false\n and isPromiscuityPhase(triggerId, curPhase) == false\n and isIntimacyPhase(triggerId, curPhase) == false then\n local lewdnessLevel = getLewdnessLevel(triggerId)\n\n local baseRate = 0.10\n local bonusRatePerClimax = math.max(0, lewdnessLevel - 1) * 0.005 -- 1렙 0%, 11렙 5%\n\n climaxHPDamage = 0\n climaxDamagePerHit = 0\n\n for i = 1, mustClimaxCount do\n local climaxIndex = curClimaxCount + i\n local bonusCount = math.max(0, climaxIndex - 1)\n local damageRate = baseRate + (bonusRatePerClimax * bonusCount)\n\n local damage = math.max(1, math.floor(curMaxHP * damageRate))\n\n climaxHPDamage = climaxHPDamage + damage\n climaxDamagePerHit = damage\n end\n end\n end\n\n local nextClimaxCount = curClimaxCount + mustClimaxCount\n\n -- ==========================================\n -- HP 계산\n -- ==========================================\n local nextHP = roundToTenth(clamp(curHP - totalEncounterDamage - climaxHPDamage + totalPlayerRecovery, 0, curMaxHP))\n\n -- ==========================================\n -- 실금 계산\n -- ==========================================\n local didCombatIncontinence = false\n local curIncontinence = tonumber(heroineEffect.incontinence) or 0\n local nextIncontinence = curIncontinence\n\n if isCombatPhase(triggerId, curPhase) and totalEncounterDamage > 0 then\n local totalHeroineHpLoss = math.max(0, roundToTenth(curHP - nextHP))\n\n didCombatIncontinence =\n rollCombatIncontinenceByDamageAndState(\n totalHeroineHpLoss,\n nextHP,\n curMaxHP,\n nextArousal,\n curMaxArousal,\n curIncontinence\n )\n\n if didCombatIncontinence then\n nextIncontinence = curIncontinence + 1\n end\n end\n\n -- ==========================================\n -- 클리어 여부\n -- ==========================================\n local isDefeated = not isIncapacitatedPhase(triggerId, curPhase) and not isHEventPhase(triggerId, curPhase) and nextHP <= 0\n local isCleared = false\n\n if isMasturbationPhase(triggerId, curPhase) then\n isCleared = (mustClimaxCount >= 1)\n else\n isCleared = (not isDefeated) and (nextEncounterHP <= 0)\n end\n\n clearNextDualFlag(triggerId, vgSessionInfo.varName)\n\n setChatVar(triggerId, \"vg_Reward_Choice\", \"\")\n setChatVar(triggerId, \"vg_Reward_Count\", \"0\")\n setChatVar(triggerId, \"vg_Reward_Selected\", \"\")\n\n if isDefeated then\n nextHP = 0\n nextResource = 0\n\n setPhase(triggerId, tostring(constPhase.incapacitated))\n \n elseif isCleared then\n if isMasturbationPhase(triggerId, curPhase) then\n nextArousal = 0\n\n if curPhase == constPhase.masturbation_Interlude then\n setPhase(triggerId, tostring(constPhase.interlude))\n\n elseif curPhase == constPhase.masturbation_Daily or curPhase == constPhase.masturbation_Story then\n setPhase(triggerId, tostring(constPhase.daily))\n\n elseif curPhase == constPhase.masturbation_Unfree then\n setPhase(triggerId, tostring(constPhase.unfree))\n\n end\n\n elseif (isIncapacitatedPhase(triggerId, curPhase) or isHEventPhase(triggerId, curPhase)) and curPhase ~= constPhase.transaction and curPhase ~= constPhase.rape then\n if curPhase == constPhase.event_Defiant or curPhase == constPhase.event_Yielding\n or curPhase == constPhase.event_Defiant_Hypnosis or curPhase == constPhase.event_Yielding_Hypnosis then\n nextArousal = 0\n elseif isHEventPhase(triggerId, curPhase) then\n nextHP = curMaxHP\n nextResource = curMaxResource\n nextArousal = 0\n end\n\n setPhase(triggerId, tostring(constPhase.unfree))\n\n else\n if isPromiscuityPhase(triggerId, curPhase) or isIntimacyPhase(triggerId, curPhase) then\n nextArousal = 0\n end\n\n local sessionSlotList = getDualFlagSlotList(triggerId, vgSessionInfo.varName)\n local storyTypeValue = tonumber(getResolvedDualFlagFromSlotList(sessionSlotList, vgSessionInfo.storyType, constStoryType.side)) or constStoryType.side\n local stageValue = tonumber(getResolvedDualFlagFromSlotList(sessionSlotList, vgSessionInfo.stage, 1)) or 1\n\n if storyTypeValue == constStoryType.core then\n stageValue = stageValue + 1\n setDualFlag(triggerId, vgSessionInfo.varName, vgSessionInfo.stage, tostring(stageValue), true)\n\n setPhase(triggerId, constPhase.interlude)\n else\n setPhase(triggerId, constPhase.daily)\n end\n end\n end\n\n if curPhase == constPhase.indoctrination then\n if isCleared or isDefeated then\n local currentKnowledge = tonumber(getDualFlag(triggerId, vgSexKnowledge.varName, vgSexKnowledge.level, false)) or 0\n\n if currentKnowledge < 5 then\n setDualFlag(triggerId, vgSexKnowledge.varName, vgSexKnowledge.level, tostring(currentKnowledge + 1), true)\n end\n else\n clearNextDualFlag(triggerId, vgSexKnowledge.varName)\n end\n end\n\n -- ==========================================\n -- 변수 적용\n -- ==========================================\n batchResetAndSetNextDualFlags(triggerId, vgHeroineStat.varName, {\n { position = vgHeroineStat.hp, value = tostring(nextHP) },\n { position = vgHeroineStat.resource, value = tostring(nextResource) },\n { position = vgHeroineStat.arousal, value = tostring(nextArousal) },\n })\n\n batchResetAndSetNextDualFlags(triggerId, vgHeroineEffect.varName, {\n { position = vgHeroineEffect.buff, value = tostring(nextHeroineBuff) },\n { position = vgHeroineEffect.climaxPoint, value = tostring(nextOverflow) },\n { position = vgHeroineEffect.climaxCount, value = tostring(nextClimaxCount) },\n { position = vgHeroineEffect.sleep, value = tostring(nextSleep) },\n { position = vgHeroineEffect.immobilized, value = tostring(nextImmobilized) },\n { position = vgHeroineEffect.brainwashed, value = tostring(nextBrainwashed) },\n { position = vgHeroineEffect.incontinence, value = tostring(nextIncontinence) },\n })\n\n batchResetAndSetNextDualFlags(triggerId, vgEncounterStat.varName, {\n { position = vgEncounterStat.hp, value = tostring(nextEncounterHP) },\n { position = vgEncounterStat.buff, value = tostring(nextEncounterBuff) },\n { position = vgEncounterStat.defense, value = tostring(nextEncounterDefense) },\n })\n\n if isCleared and isHEventPhase(triggerId, curPhase) == false then\n buildRewardChoiceBlock(triggerId, curPhase)\n end\n\n -- ==========================================\n -- 상태 갱신\n -- ==========================================\n buildCurrentState(triggerId)\n buildPlayDeckSetting(triggerId)\n buildEncounterActionSetting(triggerId)\n\n if isHEventPhase(triggerId, curPhase) then\n buildSexualProfile(triggerId)\n end\n\n -- ==========================================\n -- 메시지 구성\n -- ==========================================\n local appendLines = {}\n\n if (isCleared or isDefeated) and not isHEventPhase(triggerId, curPhase) and (not isIncapacitatedPhase(triggerId, curPhase) or curPhase == constPhase.rape or curPhase == constPhase.transaction) then\n local lang = getLanguage(triggerId)\n local aftermathText = \"\"\n\n if lang == 1 then\n aftermathText = \"추가 입력 메시지\"\n elseif lang == 3 then\n aftermathText = \"追加入力メッセージ\"\n else\n aftermathText = \"Additional Input Message\"\n end\n\n table.insert(appendLines, \"[Aftermath Message: \" .. aftermathText ..\" | Emoji: 📝]\")\n table.insert(appendLines, \"[Blank Line]\")\n end\n\n if #encounterResults > 0 then\n for i, r in ipairs(encounterResults) do\n table.insert(appendLines, buildEncounterResultLine(i, r))\n end\n\n table.insert(appendLines, \"[Blank Line]\")\n end\n\n if #companionResults > 0 then\n for i, r in ipairs(companionResults) do\n table.insert(appendLines, buildCompanionResultLine(i, r))\n end\n\n table.insert(appendLines, \"[Blank Line]\")\n end \n\n for i, r in ipairs(playerResults) do\n table.insert(appendLines, buildPlayerResultLine(i, r))\n end\n\n table.insert(appendLines, \"▷ [Instead of repeating the above actions' names verbatim, describe them naturally in the narration.]\")\n if not isHEventPhase(triggerId, curPhase) then\n appendGuardResultLines(appendLines, encounterResults, playerResults, isCleared)\n end\n table.insert(appendLines, \"▷ [The above actions are not fixed in the listed order; arrange them into the narration in the natural sequence.]\")\n\n if curSleep >= 1 then\n table.insert(appendLines, \"▷ [The heroine's actions have no meaningful effect and leave her completely defenseless.]\")\n\n if nextSleep <= 0 then\n table.insert(appendLines, \"▷ [At the end of this response, the heroine must wake up.]\")\n else\n table.insert(appendLines, \"▷ [During this response, the heroine must remain asleep throughout and must not wake up under any circumstances.]\")\n end\n elseif curImmobilized >= 1 then\n table.insert(appendLines, \"▷ [The heroine remains awake and aware, but immobilized through paralysis, drugging, numbing agents, or similar means rather than physical restraint. Her actions have no meaningful force and cannot amount to normal resistance.]\")\n\n if nextImmobilized <= 0 then\n table.insert(appendLines, \"▷ [At the end of this response, the heroine must recover from the immobilized state.]\")\n else\n table.insert(appendLines, \"▷ [During this response, the heroine must remain conscious but immobilized throughout.]\")\n end\n\n elseif curBrainwashed >= 1 and curPhase ~= constPhase.event_Brainwashing then\n table.insert(appendLines, \"▷ [The heroine is under complete mental control. Though conscious, she can neither govern her own speech nor her body's actions according to her own will, and both her words and movements follow the imposed influence instead.]\")\n\n if nextBrainwashed <= 0 then\n table.insert(appendLines, \"▷ [At the end of this response, the heroine must begin to regain the ability to act according to her own will.]\")\n else\n table.insert(appendLines, \"▷ [During this response, the heroine must remain conscious but unable to act according to her own will throughout.]\")\n end\n end\n\n if didCombatIncontinence then\n table.insert(appendLines, \"▷ [Due to the impact of the encounter's attack, the heroine ends up experiencing urinary incontinence.]\")\n end\n\n local climaxGuide = \"0\"\n\n if mustClimaxCount >= 1 then\n local isEncounterClimax = (totalEncounterArousal > 0)\n\n local climaxPrefix = \"▷ [In this response, the heroine climaxes\"\n local climaxSuffix = \".\"\n\n if isEncounterClimax then\n climaxPrefix = \"▷ [In this response, the encounter brings the heroine to climax\"\n\n if isHEventPhase(triggerId, curPhase) == false and isCleared == false and isDefeated == false then\n climaxSuffix = \". After bringing the heroine to climax, the encounter attempts to push her further with more intense and explicit sexual acts. Further sexual acts involving other orifices may be implied within this response or included in the next Encounter Action Setting, but this response must not yet depict actual penetration involving any new orifice beyond the body parts explicitly specified above.\"\n\n if nextClimaxCount >= 2 and\n (isCombatPhase(triggerId, curPhase)\n or isHarassmentPhase(triggerId, curPhase)\n or isAbusePhase(triggerId, curPhase)\n or isConfrontationPhase(triggerId, curPhase)\n or isHypnosisPhase(triggerId, curPhase)\n or isRescuePhase(triggerId, curPhase)) then\n\n if nextClimaxCount >= 5 then\n climaxSuffix = \". Because repeated climaxes leave her strength completely drained, the heroine becomes almost completely defenseless against further sexual assault\" .. climaxSuffix\n elseif curClimaxCount < 4 and nextClimaxCount >= 4 then\n climaxSuffix = \". Because repeated climaxes drain her strength, the heroine becomes far more vulnerable to further sexual assault\" .. climaxSuffix\n elseif curClimaxCount < 3 and nextClimaxCount >= 3 then\n climaxSuffix = \". Because of repeated climaxes, the heroine becomes more vulnerable to further sexual assault\" .. climaxSuffix\n end\n\n climaxGuide = \"1\"\n end\n end\n end\n\n local climaxCountText = \"\"\n if mustClimaxCount > 1 then\n climaxCountText = \" \" .. tostring(mustClimaxCount) .. \" times\"\n end\n\n table.insert(appendLines, climaxPrefix .. climaxCountText .. climaxSuffix .. \"]\")\n end\n\n setChatVar(triggerId, \"vg_Encounter_Climax\", climaxGuide)\n\n local endResolutionTempLog = \"\"\n\n if isMasturbationPhase(triggerId, curPhase) then\n if isCleared then\n table.insert(appendLines, \"▷ [After the heroine climaxes, her arousal begins to ease, but throughout this response she still lingers in the afterglow of her climax, remaining softened, dazed, or emotionally affected rather than suddenly becoming fully calm, cold, or rational. The heroine must remain in the current area, and she must not leave it or move on to a new location yet.]\")\n local lewdnessLevel = getLewdnessLevel(triggerId)\n\n if lewdnessLevel >= 11 then\n table.insert(appendLines, \"▷ [Describe naturally how this experience leaves her with an almost overwhelming lingering craving, eager private anticipation, and a nearly irresistible pull toward stronger, more vivid, and more real forms of sexual stimulation, as solitary pleasure begins to feel insufficient beside the more direct sensations she keeps imagining. Her thoughts may drift toward the imagined reality of direct physical contact, warmth, weight, presence, or being physically desired, as if the afterglow itself keeps turning into a renewed urge that she can no longer honestly dismiss as a passing impulse, in a way that fits the heroine's overall sexual experience.]\")\n elseif lewdnessLevel >= 9 then\n table.insert(appendLines, \"▷ [Describe naturally how this experience leaves her with a strong lingering craving, eager private anticipation, a hard-to-suppress urge that keeps returning beneath the afterglow, and increasingly vivid thoughts about what she may want to seek out next time, such as stronger stimulation, different tools or methods, more intense forms of pleasure, or even the imagined reality of direct physical contact, warmth, weight, presence, or other sensations beyond solitary pleasure, in a way that fits the heroine's overall sexual experience.]\")\n elseif lewdnessLevel >= 7 then\n table.insert(appendLines, \"▷ [Describe naturally how this experience leaves her with some lingering expectation, curiosity, or private thoughts about what she may want to try, change, or seek out next time, such as stronger stimulation, different tools or methods, more intense sensations, or brief thoughts of something more direct or real than solitary pleasure, even if she quickly denies, rationalizes, or feels embarrassed by those thoughts, while some part of her still cannot honestly dismiss them as mere passing impulses, in a way that fits the heroine's overall sexual experience.]\")\n elseif lewdnessLevel >= 5 then\n table.insert(appendLines, \"▷ [Describe naturally how this experience leaves her with lingering embarrassment, yet also with a quiet private awareness that some parts of it felt better than she wants to admit, in a way that fits the heroine's overall sexual experience.]\")\n elseif lewdnessLevel >= 3 then\n table.insert(appendLines, \"▷ [Describe naturally how this experience leaves her with a quiet conflict between shame over what she did and the lingering memory of undeniable pleasure, in a way that fits the heroine's overall sexual experience.]\")\n else\n table.insert(appendLines, \"▷ [Describe naturally how this experience leaves her with lingering embarrassment, discomfort, or unease, while still leaving behind a faint awareness that she cannot easily dismiss, in a way that fits the heroine's overall sexual experience.]\")\n end\n\n endResolutionTempLog = \"[Temporary Log: 💦 절정과 함께 자위 종료]\"\n else\n table.insert(appendLines, \"▷ [During this response, the heroine must not reach climax yet. The current sexual masturbation situation must continue, and the narration must remain consistent with that ongoing state.]\")\n end\n\n table.insert(appendLines, \"▷ [No new encounter, threat, or hostile presence must appear during this response. Even if any encounter, person, or other presence is already nearby, present, or watching, they must not touch the heroine, and must instead only watch.]\")\n\n elseif isIncapacitatedPhase(triggerId, curPhase) then\n if isCleared then\n --table.insert(appendLines, \"▷ [In this exchange, the encounter performs its finishing act and becomes fully satisfied. That finishing act must not be depicted as arising from any newly introduced sexual act or from any act that is completely negated in this input. A partially blocked act may still contribute partially.]\")\n local finishingActIntro = \"In this exchange, the encounter performs its finishing act and becomes fully satisfied.\"\n local finishingActConstraint = \" That finishing act must not be depicted as arising from any newly introduced sexual act or from any act that is completely negated in this input. A partially blocked act may still contribute partially.\"\n if curPhase == constPhase.rape then\n finishingActIntro = \"In this exchange, after the encounter performs its finishing act, the encounter attempts to escalate the assault further, but that attempt must be interrupted, prevented, or otherwise fail before any further sexual act or sexual contact is carried out.\"\n end\n table.insert(appendLines, \"▷ [\" .. finishingActIntro .. finishingActConstraint .. \"]\")\n\n endResolutionTempLog = \"[Temporary Log: 🔞 상황 종료]\"\n\n if curPhase == constPhase.event_Defiant_Hypnosis or curPhase == constPhase.event_Yielding_Hypnosis or curPhase == constPhase.event_Brainwashing then\n table.insert(appendLines, \"▷ [During this response, the heroine is fully caught under the encounter's mental influence, so her actions and words must be described as following the encounter's commands or instructions, even if they do not truly come from her own will.]\")\n end\n\n if curPhase == constPhase.event_Defiant or curPhase == constPhase.event_Yielding\n or curPhase == constPhase.event_Defiant_Hypnosis or curPhase == constPhase.event_Yielding_Hypnosis then\n table.insert(appendLines, \"▷ [Near the end of this response, the heroine loses consciousness. What follows after that must not be established, fixed, or presented as already determined within this response.]\")\n table.insert(appendLines, \"▷ [The encounter must still remain near the heroine at the end of this response, and it must not leave yet within this response. However, it must not perform any further sexual act or initiate any further sexual contact within this response.]\")\n elseif curPhase == constPhase.transaction then\n table.insert(appendLines, \"▷ [Describe only the decisive finishing moment of the ongoing transactional sexual situation and its immediate aftermath. By the end of this response, the immediate sexual transaction must be over, and the heroine must receive or secure the promised benefit, whatever form it takes. What follows after that must remain limited to its immediate aftermath, and no new encounter, threat, or conflict may begin in the same response.]\")\n endResolutionTempLog = \"[Temporary Log: 💰 상황 종료]\"\n elseif curPhase == constPhase.rape then\n table.insert(appendLines, \"▷ [By the end of this response, the heroine must be separated from the encounter and the immediate situation must be over, whether through her own escape, outside rescue, the encounter leaving, a chance to flee, or another plausible development that fits the situation.]\")\n table.insert(appendLines, \"▷ [What follows must remain limited to the immediate aftermath. There must be no new encounter, threat, or conflict in this response, and there must be no further sexual act or sexual contact.]\")\n else\n table.insert(appendLines, \"▷ [By the end of this response, the encounter's immediate sexual action has ended. The encounter's subsequent behavior—such as leaving, remaining nearby, moving the heroine elsewhere, keeping watch over her, securing or confining her more firmly, waiting, resting, sleeping, reporting to others, handing her over, or turning to something else—should be determined naturally by the encounter and the situation. In any case, the heroine must remain unfree and unable to leave or break out of the current place or condition on her own.]\")\n table.insert(appendLines, \"▷ [After the finishing act, the encounter must not perform any further sexual act or initiate any further sexual contact within this response.]\")\n end\n\n else\n if curPhase == constPhase.transaction then\n table.insert(appendLines, \"▷ [During this response, the ongoing transactional sexual situation must continue. It must not imply resolution, completion, or the encounter's full satisfaction yet.]\")\n else\n if hasCompanionAction then\n table.insert(appendLines, \"▷ [The companion is incapacitated and therefore unable to meaningfully help the heroine or alter either her own situation or the heroine's. If the companion is female, she is also sexually violated.]\")\n end\n\n if curPhase == constPhase.event_Defiant_Hypnosis or curPhase == constPhase.event_Yielding_Hypnosis or curPhase == constPhase.event_Brainwashing then\n table.insert(appendLines, \"▷ [During this response, the heroine is fully caught under the encounter's mental influence, so her actions and words must be described as following the encounter's commands or instructions, even if they do not truly come from her own will.]\")\n end\n \n table.insert(appendLines, \"▷ [During this response, the sexual violation must continue while the heroine remains incapacitated. It must not imply resolution, completion, or the encounter's satisfaction.]\")\n end\n end\n\n elseif isExhibitionismPhase(triggerId, curPhase) then\n local lewdnessLevel = getLewdnessLevel(triggerId)\n\n if isCleared then\n local exhibitionismEndLead = \"\"\n\n if lewdnessLevel < 5 then\n exhibitionismEndLead = \"In this exchange, the heroine removes herself from the exposure-related situation.\"\n elseif lewdnessLevel < 7 then\n exhibitionismEndLead = \"In this exchange, the heroine reaches exhibitionistic fulfillment, or otherwise removes herself from the exposure-related situation.\"\n else\n exhibitionismEndLead = \"In this exchange, the heroine reaches exhibitionistic fulfillment.\"\n end\n\n table.insert(appendLines, \"▷ [\" .. exhibitionismEndLead .. \" Describe only the moment the exhibitionistic situation comes to an end and its immediate aftermath. An encounter may or may not exist nearby, but if it exists it remains completely unaware of the heroine's exhibitionistic behavior. No direct interaction, confrontation, or physical contact may occur, and no new encounter or threat may be introduced in the same response.]\")\n endResolutionTempLog = \"[Temporary Log: 👗 상황 종료]\"\n else\n local exhibitionismContinueLead = \"\"\n\n if lewdnessLevel < 5 then\n exhibitionismContinueLead = \"The heroine must not yet remove herself from the exposure-related situation, and the situation must not end or resolve.\"\n else\n exhibitionismContinueLead = \"The heroine must not yet reach full exhibitionistic fulfillment, and the situation must not end or resolve.\"\n end\n\n table.insert(appendLines, \"▷ [During this response, the exhibitionistic situation must continue. \" .. exhibitionismContinueLead .. \" An encounter may or may not exist nearby, but if it exists it remains completely unaware of the heroine's exhibitionistic behavior. No direct interaction, confrontation, or physical contact may occur.]\")\n end\n\n elseif isPromiscuityPhase(triggerId, curPhase) or isIntimacyPhase(triggerId, curPhase) then\n if isCleared then\n table.insert(appendLines, \"▷ [In this exchange, the encounter performs its finishing act and becomes fully satisfied, and the heroine and the encounter reach full sexual fulfillment. That finishing moment must not be depicted as arising from any newly introduced sexual act or from any act that is completely negated in this input. A partially blocked act may still contribute partially.]\")\n table.insert(appendLines, \"▷ [During this response, rather than becoming fully calm, cold, rational, or emotionally detached, the heroine should remain softened, dazed, or emotionally affected by the lingering afterglow of the finishing moment.]\")\n table.insert(appendLines, \"▷ [Describe only the decisive finishing moment of the ongoing sexual situation and its immediate aftermath. The situation must end here, and no new encounter, threat, or conflict may begin in the same response.]\")\n endResolutionTempLog = \"[Temporary Log: 🥂 상황 종료]\"\n else\n table.insert(appendLines, \"▷ [During this response, the ongoing sexual situation must continue. Neither the heroine nor the encounter must yet reach full sexual fulfillment, and the situation must not end, resolve, break apart, or turn into combat, escape, or a full rejection of the situation.]\")\n end\n\n elseif isHEventPhase(triggerId, curPhase) then\n if isCleared then\n table.insert(appendLines, \"▷ [In this exchange, the encounter performs its finishing act, bringing its immediate sexual action to an end. No further sexual act or sexual contact occurs for the time being, and this response must end with the heroine remaining asleep or unconscious without waking up.]\")\n endResolutionTempLog = \"[Temporary Log: 🔞 상황 종료]\"\n else\n if hasCompanionAction then\n table.insert(appendLines, \"▷ [The companion is incapacitated and therefore unable to meaningfully help the heroine or alter either her own situation or the heroine's. If the companion is female, she is also sexually violated.]\")\n end\n\n table.insert(appendLines, \"▷ [The heroine is completely incapacitated; neither her own actions nor any outside intervention can end the situation, enable escape, or remove her from the current situation.]\")\n table.insert(appendLines, \"▷ [Avoid continuing with the same or overly similar sexual action across consecutive turns for too long; once it has been sufficiently developed, such as after ejaculation or an equivalent release event, meaningfully vary the scene instead of looping the same action, including changes to the sexual position, penetration target, stimulation focus, interaction dynamic, physical setup, or overall sexual act.]\")\n table.insert(appendLines, \"▷ [During this response, the sexual violation must continue. It must not imply resolution, completion, or the encounter's satisfaction.]\")\n end\n\n elseif isDefeated then\n local defeatText = \"\"\n local defeatFollowText = \"\"\n\n if isCombatPhase(triggerId, curPhase) and curPhase ~= constPhase.brainwashing then\n defeatText = \"The heroine is defeated in this exchange.\"\n endResolutionTempLog = \"[Temporary Log: 🏳️ 패배]\"\n\n else\n defeatFollowText = \"The encounter either keeps her there or takes her somewhere else, as suits the situation, in order to carry out more explicit sexual acts against her.\"\n\n if curPhase == constPhase.aphrodisiac then\n defeatText = \"Describe the encounter taking additional actions to intensify the aphrodisiac influence, causing the heroine to fall completely under its effect and become unable to resist.\"\n endResolutionTempLog = \"[Temporary Log: 💊 최음 효과 강화]\"\n\n elseif isHarassmentPhase(triggerId, curPhase) then\n defeatText = \"The heroine is momentarily left extremely vulnerable and defenseless, and the encounter exploits that opening to completely subdue her.\"\n endResolutionTempLog = \"[Temporary Log: 🚨 제압됨]\"\n\n elseif isAbusePhase(triggerId, curPhase) then\n defeatText = \"The heroine is cornered into a position where she can no longer meaningfully refuse, withdraw, or resist, and the encounter uses that opening to take full control of the situation.\"\n endResolutionTempLog = \"[Temporary Log: 🙇 굴복]\"\n\n elseif isConfrontationPhase(triggerId, curPhase) then\n defeatText = \"The heroine is pushed into a position where she can no longer meaningfully push back, hold her ground, or get out of the situation, and the encounter uses that opening to seize full control of what follows.\"\n endResolutionTempLog = \"[Temporary Log: 🔇 주도권 상실]\"\n\n elseif isHypnosisPhase(triggerId, curPhase) or curPhase == constPhase.brainwashing then\n defeatText = \"The heroine falls completely under the encounter's mental influence, becoming unable to resist its commands or instructions, and must be described as obeying them while moving and speaking, even if her actions do not truly come from her own will.\"\n endResolutionTempLog = \"[Temporary Log: 👁️🗨️ 정신 잠식]\"\n\n elseif isRescuePhase(triggerId, curPhase) then\n defeatText = \"The rescue attempt fails, and the encounter keeps control of the situation. Depending on the situation, the rescuer is either forced away from the place altogether or captured and kept within it.\"\n endResolutionTempLog = \"[Temporary Log: 🚨 구출 실패]\"\n\n else\n endResolutionTempLog = \"[Temporary Log: 🏳️ 패배]\"\n end\n end\n\n if hasCompanionAction then\n table.insert(appendLines, \"▷ [The companion becomes fully aware that the heroine has been incapacitated and is in grave danger. However, the companion cannot alter her situation or meaningfully help her, and must either leave the scene entirely, be separated from the heroine as she is removed from the scene, or be incapacitated and subdued alongside the heroine.]\")\n end\n\n table.insert(appendLines, \"▷ [\" .. defeatText .. (defeatFollowText ~= \"\" and \" \" .. defeatFollowText or \"\") .. \" The response must proceed with the encounter continuing or initiating a sexual violation against the incapacitated heroine, but the response must end before the situation escalates further.]\")\n\n elseif isCombatPhase(triggerId, curPhase) then\n if isCleared then\n local commonSuffix = \"A new encounter or threat must not be introduced in the same response.\"\n\n if curPhase == constPhase.escape then\n table.insert(appendLines, \"▷ [The heroine has successfully escaped in this exchange. Describe the heroine successfully escaping and becoming free. \" .. commonSuffix .. \"]\")\n endResolutionTempLog = \"[Temporary Log: 🏃♀️ 탈출]\"\n else\n table.insert(appendLines, \"▷ [The encounter has been fully overcome in this exchange. Describe only the moment the encounter is neutralized and the immediate aftermath. \" .. commonSuffix .. \"]\")\n endResolutionTempLog = \"[Temporary Log: 🕊️ 승리]\"\n end\n else\n table.insert(appendLines, \"▷ [During this response, the heroine must remain engaged in the current battle. The current encounter must not be fully overcome yet, and no new hostile presence may appear.]\")\n end\n\n elseif isPerformancePhase(triggerId, curPhase) then\n if isCleared then\n table.insert(appendLines, \"▷ [In this exchange, the encounter reaches sexual fulfillment and the performance comes to an end. Describe only the decisive finishing moment of the performance and its immediate aftermath. A new encounter or threat must not be introduced in the same response.]\")\n endResolutionTempLog = \"[Temporary Log: 🎬 상황 종료]\"\n else\n table.insert(appendLines, \"▷ [During this response, the heroine must remain in a non-contact situation where she is expected to sexually satisfy observers. The encounter must continue only as a watcher and must not reach sexual fulfillment yet. There must be no direct physical contact between the heroine and the encounter, and the situation must not shift into any state where such contact becomes possible.]\")\n end\n\n elseif isHypnosisPhase(triggerId, curPhase) then\n if isCleared then\n --table.insert(appendLines, \"▷ [The heroine fully breaks free from the encounter's mental influence in this exchange. Describe only the heroine breaking free from the encounter's mental influence and the immediate aftermath. A new encounter or threat must not be introduced in the same response.]\")\n table.insert(appendLines, \"▷ [The heroine fully breaks free from the encounter's mental influence in this exchange. Describe only her breaking free, the current encounter reaching a clear conclusion appropriate to the established situation, and the immediate aftermath. The current encounter must not remain unresolved, and a new encounter or threat must not be introduced in the same response.]\")\n endResolutionTempLog = \"[Temporary Log: 🔔 상황 종료]\"\n else\n table.insert(appendLines, \"▷ [During this response, the heroine must remain under the encounter's mental influence. She must not fully break free from it, fully regain herself, or completely reclaim clear control over her own mind and will yet.]\")\n end\n\n elseif isRescuePhase(triggerId, curPhase) then\n if isCleared then\n table.insert(appendLines, \"▷ [The rescuer successfully frees the heroine in this exchange. Describe the heroine successfully escaping and becoming free. A new encounter or threat must not be introduced in the same response.]\")\n endResolutionTempLog = \"[Temporary Log: 🚑 구출 성공]\"\n else\n table.insert(appendLines, \"▷ [During this response, the heroine must remain physically incapacitated and unable to free herself. The rescuer must continue the current rescue attempt. The current encounter and the rescuer must not be fully overcome yet, and the heroine must not be freed yet. No new hostile presence may appear.]\")\n end\n\n else\n if isCleared then\n local commonSuffix = \"A new encounter or threat must not be introduced in the same response.\"\n table.insert(appendLines, \"▷ [The heroine fully resolves the current situation in this exchange. Describe only the decisive moment that allows her to end it and the immediate aftermath. \" .. commonSuffix .. \"]\")\n\n if isHarassmentPhase(triggerId, curPhase) then\n endResolutionTempLog = \"[Temporary Log: 🚪 상황 종료]\"\n elseif isAbusePhase(triggerId, curPhase) then\n endResolutionTempLog = \"[Temporary Log: ⚖️ 상황 종료]\"\n elseif isConfrontationPhase(triggerId, curPhase) then\n endResolutionTempLog = \"[Temporary Log: 🌱 상황 종료]\"\n end\n else\n --table.insert(appendLines, \"▷ [During this response, the heroine must remain in the current situation. It must continue through the response, must not be fully escaped, shut down, ended, or resolved yet, and must not escalate into direct physical confrontation or full combat.]\")\n table.insert(appendLines, \"▷ [During this response, the heroine must remain in the current situation. It must continue through the response, must not be fully escaped, shut down, ended, or resolved yet, and must not become a fight or full combat.]\")\n end\n end\n\n if curPhase == constPhase.indoctrination then\n local currentKnowledge = tonumber(getDualFlag(triggerId, vgSexKnowledge.varName, vgSexKnowledge.level, false)) or 0\n\n if currentKnowledge < 5 then\n local currentKnowledgeText = getSexKnowledgeText(currentKnowledge)\n local nextKnowledgeText = getSexKnowledgeText(currentKnowledge + 1)\n\n if isCleared or isDefeated then\n table.insert(appendLines, \"▷ [As a result of this session, the heroine's sexual knowledge has advanced from \\\"\" .. currentKnowledgeText .. \"\\\" to \\\"\" .. nextKnowledgeText .. \"\\\"]\")\n else\n table.insert(appendLines, \"▷ [This session is the process through which the heroine comes to advance in sexual knowledge from \\\"\" .. currentKnowledgeText .. \"\\\" to \\\"\" .. nextKnowledgeText .. \"\\\".]\")\n end\n end\n end\n\n if endResolutionTempLog ~= \"\" then\n table.insert(appendLines, endResolutionTempLog)\n end\n\n if blockedByPlayerGuard > 0 then\n table.insert(appendLines, \"[Temporary Log: 🛡️ \" .. formatFloat(blockedByPlayerGuard) .. \"의 피해를 막았습니다.]\")\n end\n\n if blockedByEncounterGuard > 0 then\n table.insert(appendLines, \"[Temporary Log: 🧱 상대가 \" .. formatFloat(blockedByEncounterGuard) .. \"의 피해를 막았습니다.]\")\n end\n\n if mustClimaxCount > 0 then\n local logText = \"\"\n\n if mustClimaxCount >= 2 then\n logText = \"[Temporary Log: 💔 \" .. tostring(mustClimaxCount) .. \"회 연속 절정\"\n else\n logText = \"[Temporary Log: 💔 1회 절정\"\n end\n\n if climaxDamagePerHit > 0 then\n logText = logText .. \"으로 인해 \" .. tostring(climaxHPDamage) .. \" 데미지\"\n end\n\n logText = logText .. \"]\"\n table.insert(appendLines, logText)\n end\n\n appendMeterLog(appendLines, \"❤️\", curHP, nextHP, curMaxHP)\n appendMeterLog(appendLines, \"⚡\", curResource, nextResource, curMaxResource)\n appendMeterLog(appendLines, \"💗\", curArousal, nextArousal, curMaxArousal)\n appendClimaxOverflowLog(appendLines, overflowDisplayValue, overflowTriggerValue, overflowDisplayDelta)\n\n if not isHEventPhase(triggerId, curPhase) then\n local encounterEmoji, displayCurEncounterHP, displayNextEncounterHP, displayEncounterMaxHP = getEncounterDisplayInfo(triggerId, curPhase, curEncounterHP, nextEncounterHP, curEncounterMaxHP)\n appendMeterLog(appendLines, encounterEmoji, displayCurEncounterHP, displayNextEncounterHP, displayEncounterMaxHP)\n end\n\n appendBuffLog(appendLines, getEffectTypeEmoji(triggerId, constEffectType.buff, curPhase), curHeroineBuff, nextHeroineBuff)\n appendBuffLog(appendLines, \"💢\", curEncounterBuff, nextEncounterBuff)\n appendPercentBuffLog(appendLines, \"☢️\", curEncounterDefense, nextEncounterDefense)\n\n local nextAvailableCount = math.max(1, getActionAvailableCount(triggerId))\n if availableCount ~= nextAvailableCount and isDeckPhase(triggerId) then\n table.insert(\n appendLines,\n \"[Temporary Log: 🃏 다음 턴 행동 기회 \" ..\n string.rep(\"⚪\", availableCount) ..\n \" → \" ..\n string.rep(\"⚪\", nextAvailableCount) ..\n \"]\"\n )\n end\n\n pushChoiceInput(table.concat(appendLines, \"\\n\"))\n return\n end\nend)"
}
],
"lowLevelAccess": false
}
],
"lowLevelAccess": false,
"assets": []
}