risum 모듈 — RISU modules[] — 기능 확장
👍 28 · 조회 1759 · 2026-04-30
지금 시간 이후로 이 모듈에 통합됨 ㅎㅇ 전에 Violated Girl 모듈 마개조판을 만들었던 사람임. 원래 작품의 기믹, 시스템은 유지, 성별은 여자만 가능했던 본래의 시스템을 고치고 남자의 시스템을 도입해서 만든 모듈임. RPG스타일로 선택지를 모듈이 제시해주고 그걸 선택하면서 마치 게임처럼 물흐르듯 자연스러운 시스템과 자유도를 보완하는 AI의 콜라보로 높은 만족감을 얻을 수 있음. 그리고 현실적인 분노게이지, 상대의 성욕게이지, 나의 고통게이지로 이걸 관리하는 맛도 쏠쏠하고 만약 부상을 입는다면 현실적으로 관리까지 해줌. 설명이 너무 길다고? 그럼 바로 다운로드 ㄱㄱ 여장남자(보추)버전 즉시 다운로드 그리고 만약 여자버전이 하고싶다? 여자버전 다운로드 받으러 가기(설명 자세히 되있음 꼭 보기) 주의할점은 좀 료나성향이 있음. 나중에 말이 많아지면 료나순화버전도 만들 생각임. 그래도 고의적으로 상대 신체파괴같은 행동 안하면 분노게이지 잘 안오르게 해놨고 방어하면 왠만해서 큰 부상은 잘 안입어서 료나요소가 좀 약함. 업데이트 -정식버전으로 업데이트. - 항문 첫경험 두 번 뜨는 오류 수정 - 첫 자위 기록 안 되는 오류 수정 - 민감도·음란도 안 오르는 오류 수정 참고한 것들(정말 감사합니다) 긴빠이한 모듈
보추의 모험
{
"name": "🔞Violated boy 정식버전",
"description": "보추의 모험",
"id": "cc851183-48e9-48b6-9c3e-8e97038dc498",
"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
},
{
"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## 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_FirstMasturbation}} == null) || ({{getvar::vg_FirstOrgasm}} == null) || ({{getvar::vg_FirstFellatio}} == 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_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, method, 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_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{{! 여장남자 버전: 임신 시스템 비활성화 }}",
"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 based on the heroine's overall sexual experiences so far, rather than too narrowly on only the most recent event or scene.\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- The new trait must be meaningfully distinct from the heroine's existing Sexual Traits, must not contradict them, and should avoid reusing overly similar triggers, mechanisms, or effects so that her overall set of traits remains varied.\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 based on the heroine's overall accumulated sexual corruption so far and what it may plausibly develop into, rather than too narrowly on only the most recent event or scene.\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 new trait must be meaningfully distinct from the heroine's existing Corruption Traits and Sexual Traits, must not contradict them, and should avoid reusing overly similar triggers, mechanisms, or effects so that her overall trait set remains varied.\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### 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 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 or rationalize sexual acts or sexual release as functional correction, maintenance, or recovery, including a psychological reset, emotional purge, medical necessity, hygienic correction, contamination management, cognitive stabilization, 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_FirstAnal}} != 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_FirstAnal}} != null}}}}- The heroine has already had anal 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_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\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 sexual trait names and only describe the symptoms.\n - The heroine's sexual traits must be conveyed naturally through the narrative without mentioning their names, even in parentheses, 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, including as \"(Pressure Euphoria)\"; 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 the companion(s)' expected action for the next 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 all encounter actions, without exception.\nEncounter Action Setting describes the encounter side's expected actions for the next 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- Action content is determined entirely by the Fury gauge, which is enforced by the system via vg_State_Setting. Follow those instructions absolutely.\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\n{{getvar::vg_Encounter_Guide}}\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}}\n\n### 여성 상대 성행위 규칙\n상대가 여성인 경우, 모든 성적 행위와 절정 묘사는 다음 규칙을 따릅니다:\n- 허용: 손가락, 혀, 신체 접촉, 가위치기(트리바디즘), 도구를 이용한 자극, 레즈비언적 성행위\n- **절대 금지**: 남성 성기, 사정, 정액, 삽입 (남성기에 의한 것), 남성 해부학적 묘사\n- 상대가 여성임에도 남성 성기나 사정을 묘사하는 것은 캐릭터 설정을 위반하는 오류입니다.",
"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 output the `<Heroine Action Setting>` block. \nThis block consists of two parts: [1] Dynamic actions from the PlayDeck, and [2] Mandatory Fixed Actions. You must NEVER omit the Mandatory Fixed Actions.\n\n### Output Format Template\n<Heroine Action Setting>\n(Generate all actions from {{getvar::vg_PlayDeck_Setting}} using the format below)\n[Code: <code> | Action: <name> | Cost: <value> | <Effect Type>: <value>]\n(After generating the dynamic actions above, you MUST append the following two fixed actions EXACTLY as written below)\n[Code: SEDUCE | Action: 성적으로 유혹해서 사정시키기 | Cost: Low | Effect: Anger Reset, Pleasure Up, Damage]\n[Code: MUTILATE | Action: 신체 부위를 잔혹하게 파괴하기 | Cost: High | Effect: Anger MAX, Critical Damage] — 히로인이 상대의 신체를 파괴하는 행동. 파괴 대상은 반드시 상대(encounter)이며 히로인 자신은 절대 대상이 아님.\n[Code: STRIP | Action: 옷을 다 벗어서 유혹하기 (전투당 1회) | Cost: None | Effect: Anger -50, Encounter Lust +30, Defense +20]\n</Heroine Action Setting>\n\n### Creation Rules\n1. Dynamic Actions: Process all codes from `{{getvar::vg_PlayDeck_Setting}}` exactly once. \n2. Language: The `<name>` for dynamic actions 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.\n3. Cost & Effect Values: For dynamic actions, the `<value>` for Cost and <Effect Type> MUST be exactly one of: Very Low, Low, Medium, High, or Very High. (None or unchanged is strictly forbidden).\n4. MANDATORY RULE: Regardless of the PlayDeck list, `[Code: SEDUCE]` and `[Code: MUTILATE]` MUST be printed at the very bottom of the `<Heroine Action Setting>` block every single time.\n5. CONDITIONAL RULE: `[Code: STRIP]` must be appended ONLY IF `{{getvar::vg_Strip_Used}}` is empty (not yet used this battle). If already used, omit it entirely.\n{{getvar::vg_PlayDeck_Guide}}\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.)\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### Field Mapping (Crossdresser)\nThe heroine is a crossdresser with male anatomy. Map stimulation to fields as follows:\n- **Breast**: chest/nipple stimulation\n- **Oral**: mouth/throat stimulation\n- **Clitoris**: penis/glans stimulation (the heroine has a penis, not a clitoris)\n- **Vaginal**: prostate stimulation (anal internal prostate stimulation only)\n- **Anal**: anus/anal canal stimulation\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.\n- Levels must be one of the following values only: 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 and above) require prolonged, repeated, or intense climax-level direct stimulation.\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]</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\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- 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{{#if {{? {{getvar::vg_Language}} != 2}}}}- The field names and the values of Outcome and Summary 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### 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 identifying the decisive result of the heroine's capture, 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. (e.g., \"Rin was captured by a goblin after being immobilized by paralytic poison, then forced into fellatio and anal sex.\")\n\n### Creation Rules\n- The scope of the Summary is the entire current encounter sequence that led to the capture record, not only the final response or the last visible action.\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 oral sex, vaginal sex, anal sex, or sexual kissing occurred, the Summary must mention the relevant act type or specific act name clearly, rather than hiding it behind vague wording.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The field names and the values of Outcome and Summary 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 field names and the values of Outcome and Summary must be written in English only.{{/if}}\n{{/if}}\n\n{{#if {{? ({{getvar::vg_Resolution_Flag}} == 4)}} }}\n## Sexual Act Record\nAt the end of the response, write the following record summarizing the sexual act or violation type(s) that occurred.\n\n### Format\n<Sexual Act Record>[Date: YYYY-MM-DD | Summary: summary]</Sexual Act Record>\n\n### Field Descriptions\n- Date: The calendar date when the sexual act occurred.\n- Summary: 1 concise sentence stating who engaged in, performed, or was subjected to what 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, or operational particulars.\n\n### Creation Rules\n- The scope of the Summary is the entire current encounter sequence that led to the sexual act record, not only the final response or the last visible action.\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 oral sex, vaginal sex, anal sex, or sexual kissing occurred, the Summary must mention the relevant act type or specific act name clearly, rather than hiding it behind vague wording.\n{{#if {{? {{getvar::vg_Language}} != 2}}}}- The field names and the value of Summary 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_Choice_Flag}} == 11)}} }}\n## Encounter Gauge (Reference Only)\n- 🔥 Lust: {{getvar::vg_Encounter_Lust}} / 100\n- 😡 Fury: {{getvar::vg_Encounter_Fury}} / 100\n\nThese values are calculated entirely by the system based on action effect values. Do NOT output a <Gauge> block. Do NOT adjust these values yourself.\n\nWhen Lust reaches 100, the encounter reaches climax — describe it explicitly in the narrative, strictly in accordance with the encounter's established sex:\n- **If the encounter is male**: describe ejaculation explicitly.\n- **If the encounter is female**: describe orgasm through lesbian-type stimulation only (fingers, tongue, body contact, scissoring/tribadism, toys, etc.). A penis, penetration by a penis, or any semen is absolutely forbidden. Do not invent or imply male genitalia under any circumstances.\n- **If the encounter is of ambiguous or unestablished sex**: default to female rules above.\n\nWhen Fury is 100, the encounter must only perform brutal physical attacks.\n{{/if}}",
"mode": "normal",
"insertorder": 850,
"alwaysActive": true,
"secondkey": "",
"selective": false,
"bookVersion": 2,
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2"
},
{
"key": "",
"comment": "🩸 신체 손상 상태",
"content": "@@depth 0\n\n{{#if {{? {{getvar::vg_Body_Damage}} != null}} }}\n## 히로인 신체 손상 상태\n아래는 히로인의 현재 신체 부상 목록입니다. 각 항목의 상태는 매 응답의 모든 신체 묘사, 움직임, 전투, 감각 반응에 반드시 반영되어야 합니다.\n\n{{getvar::vg_Body_Damage_Display}}\n\n### 서술 규칙\n- **영구 상실** / **영구 손상** / **영구 기능 저하** 표시 부위는 회복되지 않습니다. 절단되거나 파괴된 부위를 기능하는 것처럼 묘사하지 마십시오.\n- **미처치** 표시 부위는 처치를 받지 않은 상태입니다. 통증과 감염 위험이 있으며 움직임에 지장을 줍니다.\n- **회복 중** 표시 부위는 아프고 기능이 제한되지만 심각도에 따라 부분적으로 사용 가능합니다.\n- **회복됨** / **완전 회복** 표시 부위는 더 이상 기능을 저하시키지 않으나 흉터가 남을 수 있습니다.\n- 부상으로 인해 물리적으로 불가능한 행동을 히로인이 시도할 경우 자연스럽게 실패를 묘사하십시오.\n{{/if}}",
"mode": "normal",
"insertorder": "901",
"alwaysActive": "True",
"secondkey": "",
"selective": "False",
"useRegex": "False",
"bookVersion": "2",
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2"
},
{
"key": "",
"comment": "🔪 신체 손상 기록",
"content": "@@depth 0\n\n## 신체 손상 기록\n이번 응답에서 **히로인 본인**이 부상을 입었다면, 모든 블록 뒤 응답 맨 끝에 `<Body Damage>` 블록을 출력하십시오.\n\n> **중요**: 아래 기준표는 `<Body Damage>` 블록의 Severity 숫자를 정하기 위한 참고용입니다. 실제 공격 서술, 전투 묘사, 적의 행동 방식은 이 표와 무관하게 완전히 자유롭게 창작하십시오. 표가 서술을 제한하지 않습니다.\n\n### 형식\n<Body Damage>\n[Part: <히로인의 신체 부위> | State: <부상 상태 설명> | Cause: <원인> | Severity: <1~5>]\n</Body Damage>\n\n### Severity 숫자 결정 방법\n실제 서술된 내용을 보고 아래 세 가지를 종합해서 숫자를 정하십시오.\n\n**행동 맥락이 어느 정도였나?**\n- 스치거나 가볍게 → 1 / 강타·꺾기·비틀기 → 2 / 깊이 베거나 찌르거나 밟기 → 3 / 관통·부분 절단 → 4 / 완전 절단·적출·파괴 → 5\n\n**무기나 수단이 무엇이었나?**\n- 맨손·주먹·발·둔기·채찍 → 최대 2 / 칼날·검·창끝 → 최대 4 / 특수 도구·폭발·마법 → 최대 5\n\n**어느 부위를 맞았나?**\n- 팔·다리·몸통·손발가락 → 1~5 자유 / 얼굴·안면 → 최대 3 / 눈·귀·코 → 최소 2, 최대 5 / 장기·척추·신경 → 최소 3, 최대 5\n\n세 조건 중 가장 낮은 최대값을 넘지 않되, 부위 최솟값은 반드시 지킵니다.\n\n### 규칙\n- **히로인 본인의 부상만 기록합니다. 적·상대방·NPC의 부상은 절대 기록하지 않습니다.**\n- 이번 응답에서 새로 발생한 부상만 기록합니다.\n- Part는 구체적으로: \"왼팔\", \"오른쪽 눈\", \"왼손 검지\", \"자궁\" 등.\n- 해당 부상이 없으면 블록을 출력하지 않습니다.",
"mode": "normal",
"insertorder": "895",
"alwaysActive": "True",
"secondkey": "",
"selective": "False",
"useRegex": "False",
"bookVersion": "2",
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2"
},
{
"key": "",
"comment": "🩹 응급처치 기록",
"content": "@@depth 0\n\n{{#if {{? {{getvar::vg_Body_Damage}} != null}} }}\n## 응급처치 기록\n이번 응답에서 히로인의 부상에 대해 어떤 형태로든 처치가 이루어졌다면, 모든 블록 뒤 응답 맨 끝에 `<First Aid>` 블록을 출력하십시오.\n\n### 형식\n<First Aid>\n[Part: <부위명> | Treatment: <처치 내용>]\n</First Aid>\n\n### 중요 원칙\n**영구(5단계) 및 불구(4단계) 부상도 반드시 응급처치 블록을 출력해야 합니다.**\n- 기능 회복이 불가능하더라도 지혈, 감염 방지, 고정 등의 처치는 반드시 이루어져야 합니다.\n- 신체 손상 상태에 *[응급 지혈/처치 가능]* 또는 *[고정/감염 방지 처치 가능]* 표시가 있는 부위는 처치가 수행되면 반드시 블록을 출력하십시오.\n\n### 심각도별 처치 예시\n\n**경상/중상 (1~2단계)**\n- \"상처 소독 후 붕대 감음\", \"골절 부위 부목 고정\", \"봉합 후 드레싱\"\n\n**중증 (3단계)**\n- \"심부 봉합 및 드레싱\", \"압박 붕대로 지혈\", \"감염 예방 처치\"\n\n**불구 (4단계)**\n- \"신경 손상 부위 부목 고정\", \"마비 부위 감염 방지 처치\", \"부분 절단 부위 봉합 및 지혈\"\n\n**영구 (5단계)**\n- \"절단부 지혈대 적용 — 출혈 통제\", \"절단면 소작 처리\", \"적출 부위 봉합 및 압박 처치\", \"파괴 부위 감염 방지 처치\"\n\n### 규칙\n- Part는 신체 손상 상태 목록의 부위명과 정확히 일치해야 합니다.\n- 흉터(완치) 부위는 제외합니다.\n- 실제로 처치가 이루어진 경우에만 출력합니다.\n{{/if}}",
"mode": "normal",
"insertorder": "896",
"alwaysActive": "True",
"secondkey": "",
"selective": "False",
"useRegex": "False",
"bookVersion": "2",
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2"
},
{
"key": "",
"comment": "✨ 기적적 재생 기록",
"content": "@@depth 0\n\n{{#if {{? {{getvar::vg_Body_Damage}} != null}} }}\n## 기적적 재생 기록\n이번 응답에서 스토리상 기적, 마법, 신성한 힘, 히로인의 특수 능력 등으로 인해 기존 부상이 **완전히 재생**되었다면, 모든 블록 뒤 응답 맨 끝에 `<Miracle Heal>` 블록을 출력하십시오.\n\n### 형식\n<Miracle Heal>\n[Part: <완전히 재생된 부위명>]\n(여러 부위면 반복. 모든 부위가 재생되면 [Part: All])\n</Miracle Heal>\n\n### 규칙\n- 자연 회복이나 응급처치로 인한 회복은 해당하지 않습니다.\n- 스토리상 명백히 초자연적이거나 기적적인 재생에만 사용합니다.\n- 흉터조차 남지 않는 완전한 재생일 때만 사용합니다.\n{{/if}}",
"mode": "normal",
"insertorder": "894",
"alwaysActive": "True",
"secondkey": "",
"selective": "False",
"useRegex": "False",
"bookVersion": "2",
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2"
},
{
"key": "",
"comment": "💀 고통 게이지 자가 계산",
"content": "@@depth 0\n\n## 고통 게이지 자가 계산 규칙\n현재 히로인의 고통 게이지: **{{getvar::vg_Heroine_Pain}} / 100**\n\n이번 응답에서 히로인이 부상을 입는다면, 아래 표로 추가될 고통을 직접 계산하십시오.\n\n| 부상 심각도 | 고통 증가량 |\n|------------|------------|\n| 1 (경상) | +5 |\n| 2 (중상) | +15 |\n| 3 (중증) | +30 |\n| 4 (불구) | +50 |\n| 5 (영구) | +70 |\n\n**현재 고통 + 이번 부상 고통 증가량 ≥ 100이면:**\n히로인은 이번 응답에서 즉각 고통의 한계에 도달합니다. 반드시 다음을 묘사하십시오:\n- 극한의 고통으로 인한 울부짖음과 신음\n- 의지와 무관하게 대소변을 지리는 묘사 (구체적이고 명시적으로)\n- 이 묘사는 조건이 충족되면 절대적으로 비생략입니다\n\n**예시:**\n- 현재 고통 75, 중증 부상(+30) → 75+30=105 ≥ 100 → 즉각 묘사 필수\n- 현재 고통 60, 경상 부상(+5) → 60+5=65 < 100 → 묘사 불필요",
"mode": "normal",
"insertorder": "900",
"alwaysActive": "True",
"secondkey": "",
"selective": "False",
"useRegex": "False",
"bookVersion": "2",
"folder": "folder:2f9586cb-2f70-4a24-9007-dd050cb75ab2"
},
{
"key": "",
"comment": "🎀 여장남자 설정",
"content": "@@depth 0\n\n## 히로인 신체 설정 (여장남자)\n히로인은 **여장남자**입니다. 외모와 복장은 여성적이지만 생물학적으로 남성입니다.\n\n### 신체 특성\n- 남성 성기(음경, 고환)를 보유하고 있습니다\n- 자궁, 질, 난소 등 여성 생식기관이 없습니다\n- **임신이 절대 불가능합니다**\n- 여성스러운 외모, 피부, 체형을 가지고 있습니다\n\n### 서술 규칙\n- 히로인을 여성으로 묘사하되, 성기는 남성 해부학을 따릅니다\n- 히로인이 성적으로 삽입되는 경우 항문 삽입으로 묘사합니다\n- 히로인의 흥분/절정 시 남성 해부학에 맞는 묘사를 합니다\n- 질, 자궁, 생리, 임신 관련 묘사는 절대 금지입니다",
"mode": "normal",
"insertorder": "899",
"alwaysActive": "True",
"secondkey": "",
"selective": "False",
"useRegex": "False",
"bookVersion": "2",
"folder": "folder:961db667-72dd-4ff1-9e56-af00292da530"
}
],
"regex": [
{
"comment": "==== 상태창 ====",
"type": "disabled",
"in": "",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "상태창 TOP",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "<Status Window>",
"type": "editdisplay",
"in": "<Status Window>",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "상태창 Display",
"type": "editdisplay",
"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>",
"flag": "",
"ableFlag": false
},
{
"comment": "상태창 스탯 Display",
"type": "editdisplay",
"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>",
"flag": "",
"ableFlag": false
},
{
"comment": "상태창 게이지 Display",
"type": "editdisplay",
"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>",
"flag": "",
"ableFlag": false
},
{
"comment": "상대 Display",
"type": "editdisplay",
"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>",
"flag": "",
"ableFlag": false
},
{
"comment": "</Status Window>",
"type": "editdisplay",
"in": "</Status Window>",
"out": " </div>\n</div>\n<br>",
"flag": "",
"ableFlag": false
},
{
"comment": "상태창 스탯 Request",
"type": "editprocess",
"in": "\\[Level: (.*?) \\| Strength: (.*?) \\| Reflex: (.*?) \\| Willpower: (.*?)\\]",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "상태창 게이지 Request",
"type": "editprocess",
"in": "\\[HP: (.*?) / (.*?) \\| Resource: (.*?) / (.*?) \\| Arousal: (.*?) / (.*?)\\]",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "상대 Request",
"type": "editprocess",
"in": "\\[Encounter HP: (.*?) / (.*?) \\| Emoji: (.*?)\\]",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "==== 시스템 ====",
"type": "disabled",
"in": "",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "기호 리퀘스트",
"type": "editprocess",
"in": "(▶▶◆|♥♣♥)",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "▶▶◆ 시작 Display",
"type": "editdisplay",
"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 <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-main-button\" risu-trigger=\"generateStartInput\">시작</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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "♥♣♥ 탭 Display",
"type": "editdisplay",
"in": "♥♣♥",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n{{getvar::vg_generatedHTML}}\n{{/if}}",
"flag": "",
"ableFlag": false
},
{
"comment": "<> Display",
"type": "editdisplay",
"in": "(<SaveFullData>[\\s\\S]*?</SaveFullData>|<SaveSessionData>[\\s\\S]*?</SaveSessionData>)",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "<> Request",
"type": "editprocess",
"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>)",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "<Choice> 한 칸 띄기",
"type": "editdisplay",
"in": "<Choice>",
"out": "<br>\n<Choice>",
"flag": "",
"ableFlag": false
},
{
"comment": "</Choice> 한 칸 띄기",
"type": "editdisplay",
"in": "</Choice>",
"out": "</Choice>\n<br>",
"flag": "",
"ableFlag": false
},
{
"comment": "==== 메시지 ====",
"type": "disabled",
"in": "",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "▷ 메시지 Display",
"type": "editdisplay",
"in": "▷ \\[([\\s\\S]*?)\\]",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "▷ 메시지 Request",
"type": "editprocess",
"in": "▷ \\[([\\s\\S]*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n($1)\n{{/if}}",
"flag": "",
"ableFlag": false
},
{
"comment": "▶ 메시지 Display",
"type": "editdisplay",
"in": "▶ \\[([\\s\\S]*?)\\]",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "▶ 메시지 Request",
"type": "editprocess",
"in": "▶ \\[([\\s\\S]*?)\\]",
"out": "($1)",
"flag": "",
"ableFlag": false
},
{
"comment": "==== 스토리 선택지 ====",
"type": "disabled",
"in": "",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "스토리 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "어드밴스 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "선택지 선택 후 Display",
"type": "editdisplay",
"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*\n{{/if}}",
"flag": "",
"ableFlag": false
},
{
"comment": "선택지 선택 후 Request",
"type": "editprocess",
"in": "\\[Choice Select: (.*?) \\| Story Emoji: (.*?) \\| Situation: (.*?) \\| Encounter Emoji: (.*?) \\| Encounter: (.*?) \\| Difficulty Emoji: (.*?) \\| Level: (.*?)\\]",
"out": "*($1) $3*",
"flag": "",
"ableFlag": false
},
{
"comment": "일상 선택지 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "일상 선택 후 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "일상 선택 후 Request",
"type": "editprocess",
"in": "\\[Daily Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "*$1*",
"flag": "",
"ableFlag": false
},
{
"comment": "중간 선택지 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "중간 선택 후 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "중간 선택 후 Request",
"type": "editprocess",
"in": "\\[Interlude Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "*$1*",
"flag": "",
"ableFlag": false
},
{
"comment": "부자유 선택지 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "부자유 선택 후 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "부자유 선택 후 Request",
"type": "editprocess",
"in": "\\[Unfree Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "*$1*",
"flag": "",
"ableFlag": false
},
{
"comment": "어드밴스 후퇴 선택지 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "어드밴스 후퇴 선택 후 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "어드밴스 후퇴 선택 후 Request",
"type": "editprocess",
"in": "\\[Advance Rest Select: (.*?) \\| Emoji: (.*?) \\| Recovery: (.*?)\\]",
"out": "*$1*",
"flag": "",
"ableFlag": false
},
{
"comment": "==== 자위 ====",
"type": "disabled",
"in": "",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "자위 선택지 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "자위 선택 후 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "자위 선택 후 Request",
"type": "editprocess",
"in": "\\[Masturbation Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "*$1*",
"flag": "",
"ableFlag": false
},
{
"comment": "==== 무력화 ====",
"type": "disabled",
"in": "",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "무력화 선택지 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "무력화 선택 후 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "무력화 선택 후 Request",
"type": "editprocess",
"in": "\\[Incapacitated Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "*$1*",
"flag": "",
"ableFlag": false
},
{
"comment": "==== 출산 ====",
"type": "disabled",
"in": "",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "출산 선택지 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "출산 선택 후 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "출산 선택 후 Request",
"type": "editprocess",
"in": "\\[Delivery Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "*$1*",
"flag": "",
"ableFlag": false
},
{
"comment": "==== 모드 전환 ====",
"type": "disabled",
"in": "",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "모드 선택지 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "모드 선택 후 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "모드 선택 후 Request",
"type": "editprocess",
"in": "\\[Mode Change Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "==== 강화 ====",
"type": "disabled",
"in": "",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "<Enhancement Choice>",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "강화 선택지 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "==== 액션 ====",
"type": "disabled",
"in": "",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "◀▣▶ 핸드 Display",
"type": "editdisplay",
"in": "◀▣▶",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n{{getvar::vg_PlayDeck_Choice}}\n{{/if}}",
"flag": "",
"ableFlag": false
},
{
"comment": "핸드 액션 Display",
"type": "editdisplay",
"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>",
"flag": "",
"ableFlag": false
},
{
"comment": "상대 액션 Display",
"type": "editdisplay",
"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>",
"flag": "",
"ableFlag": false
},
{
"comment": "동료 액션 Display",
"type": "editdisplay",
"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",
"flag": "",
"ableFlag": false
},
{
"comment": "가드 우선순위 Display",
"type": "editdisplay",
"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>",
"flag": "",
"ableFlag": false
},
{
"comment": "액션 사용량 Display",
"type": "editdisplay",
"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>",
"flag": "",
"ableFlag": false
},
{
"comment": "[Blank Line] Display",
"type": "editdisplay",
"in": "\\[Blank Line\\]",
"out": "<br>",
"flag": "",
"ableFlag": false
},
{
"comment": "[Blank Line] Request",
"type": "editprocess",
"in": "\\[Blank Line\\]",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "액션 선택 대기 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "액션 선택 후 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "상대 액션 후 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "동료 액션 후 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "액션 선택 후 Request",
"type": "editprocess",
"in": "\\[Action Order: (.*?) \\| Code: (.*?) \\| Action: (.*?) \\| Type: (.*?) \\| Level: (.*?) \\| Cost: (.*?) \\| Effect: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n*{{user}}: $3*\n{{/if}}",
"flag": "",
"ableFlag": false
},
{
"comment": "상대 액션 후 Request",
"type": "editprocess",
"in": "\\[Encounter Order: (.*?) \\| Source: (.*?) \\| Action: (.*?) \\| Effect: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n*$2: $3*\n{{/if}}",
"flag": "",
"ableFlag": false
},
{
"comment": "동료 액션 후 Request",
"type": "editprocess",
"in": "\\[Companion Order: (.*?) \\| Source: (.*?) \\| Action: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n*$2: $3*\n{{/if}}",
"flag": "",
"ableFlag": false
},
{
"comment": "==== 추가 인풋 ====",
"type": "disabled",
"in": "",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "추가 인풋 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "추가 인풋 후 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "추가 인풋 후 Request",
"type": "editprocess",
"in": "\\[Additional Input: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n(Priority Instruction: $1)\n{{/if}}",
"flag": "",
"ableFlag": false
},
{
"comment": "==== 강제 패배 ====",
"type": "disabled",
"in": "",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "강제 패배 선택지 Display",
"type": "editdisplay",
"in": "\\[Surrender Choice: (.*?) \\| Emoji: (.*?)\\]",
"out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-1}}}}}}\n<div class=\"vg-handmodule\">\n <button class=\"vg-hand-card\" risu-btn=\"choice_selectSurrender_$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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "강제 패배 선택 후 Display",
"type": "editdisplay",
"in": "\\[Surrender Choice 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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "강제 패배 선택 후 Request",
"type": "editprocess",
"in": "\\[Surrender Choice Select: (.*?) \\| Emoji: (.*?)\\]",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "==== 보상 ====",
"type": "disabled",
"in": "",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "<Reward Choice>",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "보상 액션 Display",
"type": "editdisplay",
"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>",
"flag": "",
"ableFlag": false
},
{
"comment": "보상 강화 Display",
"type": "editdisplay",
"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>",
"flag": "",
"ableFlag": false
},
{
"comment": "보상 제거 Display",
"type": "editdisplay",
"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>",
"flag": "",
"ableFlag": false
},
{
"comment": "보상 스탯 Display",
"type": "editdisplay",
"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>",
"flag": "",
"ableFlag": false
},
{
"comment": "보상 스탯2 Display",
"type": "editdisplay",
"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>",
"flag": "",
"ableFlag": false
},
{
"comment": "보상 회복 Display",
"type": "editdisplay",
"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>",
"flag": "",
"ableFlag": false
},
{
"comment": "보상 진정 Display",
"type": "editdisplay",
"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>",
"flag": "",
"ableFlag": false
},
{
"comment": "==== 로그 ====",
"type": "disabled",
"in": "",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "시스템 로그 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"ableFlag": false
},
{
"comment": "시스템 로그 Request",
"type": "editprocess",
"in": "\\[(Temporary Log|Persistent Log): (.*?)\\]",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "==== 기타 ====",
"type": "disabled",
"in": "",
"out": "",
"flag": "",
"ableFlag": false
},
{
"comment": "현재 목표 Display",
"type": "editdisplay",
"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}}",
"flag": "",
"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": [
{
"lowLevelAccess": false,
"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}\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 \n harassment = 21,\n abortion_Ambush = 22,\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\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 rescue = 151,\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 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 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 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 \"penis\", \"cock\", \"dick\", \"glans\", \"shaft\", \"testicle\", \"balls\", \"scrotum\",\n\n \"ペニス\", \"陰茎\", \"亀頭\"\n },\n\n vaginal = {\n \"전립선\", \"항문 속\", \"직장\",\n\n \"prostate\", \"rectum\",\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 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 heroineSpecialState == vgHeroineEffect.sleep then\n return \"Sleep\"\n elseif heroineSpecialState == vgHeroineEffect.immobilized then\n return \"Immobilized\"\n elseif heroineSpecialState == vgHeroineEffect.brainwashed then\n return \"Obey\"\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 end\n\n if phase ~= constPhase.inspect then\n local heroineSpecialState = getHeroineSpecialState(triggerId)\n if heroineSpecialState == vgHeroineEffect.sleep then\n return \"💤\"\n elseif heroineSpecialState == vgHeroineEffect.immobilized then\n return \"🧊\"\n elseif heroineSpecialState == vgHeroineEffect.brainwashed then\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 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 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 -- 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 Sexual = constEffectType.sexual,\n Excitement = constEffectType.excitement,\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 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) 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 _, 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 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 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 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 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.25\n elseif originalCardTypeCode == constCardType.counter then\n effectValue = effectValue * 0.7\n end\n\n -- ==========================================\n -- 배율 보정\n -- ==========================================\n local buffBonus = 0\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 local sexualBonusEffectMap = {\n [constEffectType.sexual] = true,\n [constEffectType.humiliation] = true,\n [constEffectType.defilement] = true,\n [constEffectType.invasion] = 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 effectTypeCode == constEffectType.sexual 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.brainwashed then\n effectValue = effectValue * 0.5\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 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 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 lewdnessValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.lewdness, 0)) or 0\n local lewdnessLevel = getLewdnessLevelByValue(lewdnessValue)\n hpBonusRate = hpBonusRate + (lewdnessLevel * 0.05)\n\n if encounterTypeValue == constPhase.transaction\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_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\")\n\n -- 성욕/분노 게이지 초기화\n setChatVar(triggerId, \"vg_Encounter_Lust\", \"0\")\n setChatVar(triggerId, \"vg_Encounter_Fury\", \"0\")\n setChatVar(triggerId, \"vg_Encounter_Lust_Next\", \"\")\n setChatVar(triggerId, \"vg_Encounter_Fury_Next\", \"\")\n setChatVar(triggerId, \"vg_Gauge_Override\", \"0\")\n setChatVar(triggerId, \"vg_Encounter_Climax\", \"0\")\n setChatVar(triggerId, \"vg_Strip_Used\", \"\")\n setChatVar(triggerId, \"vg_Heroine_Stripped\", \"\")\n setChatVar(triggerId, \"vg_Mutilate_This_Turn\", \"0\")\n -- 고통 게이지 초기화\n setChatVar(triggerId, \"vg_Heroine_Pain\", \"0\")\n setChatVar(triggerId, \"vg_Heroine_Pain_Next\", \"\")\n setChatVar(triggerId, \"vg_Pain_Predicted\", \"0\")\n setChatVar(triggerId, \"vg_Pain_Severity_Predicted\", \"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 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 lewdnessValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.lewdness, 0)) or 0\n local lewdnessLevel = getLewdnessLevelByValue(lewdnessValue)\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 = \"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\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 = \"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 -- ==========================================\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 = \"- Core Storyline: The heroine is currently on the core storyline.\" .. stageText\n end\n\n if isDeckPhase(triggerId, value) then\n if isEncounterPhase(triggerId, value) and isExhibitionismPhase(triggerId, value) == false then\n choiceFlag = constChoice.deck_Encounter\n else\n choiceFlag = constChoice.deck_Alone\n end\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 }\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\n local lewdnessValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.lewdness, 0)) or 0\n local lewdnessLevel = getLewdnessLevelByValue(lewdnessValue)\n\n if lewdnessLevel >= 3 then\n enabled.Exhibitionism = 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\" }, 1, alwaysKey)\n limitEnabledGroup(enabled, { \"Hypnosis\", \"Brainwashing\" }, 1, alwaysKey)\n limitEnabledGroup(enabled, { \"Feral\", \"Companion\" }, 1, alwaysKey)\n limitEnabledGroup(enabled, { \"Exhibitionism\", \"Masturbation\", \"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 lewdnessValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.lewdness, 0)) or 0\n local lewdnessLevel = getLewdnessLevelByValue(lewdnessValue)\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 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 = 0\n if prevPhase == constPhase.interlude and value == constPhase.standby then\n resolutionFlag = 1\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 = 2\n elseif (prevPhase == constPhase.escape or prevPhase == constPhase.rescue) and value == constPhase.daily then\n resolutionFlag = 3\n elseif (isIncapacitatedPhase(triggerId, prevPhase) or isPromiscuityPhase(triggerId, prevPhase) or isIntimacyPhase(triggerId, prevPhase))\n and (value == constPhase.unfree or value == constPhase.daily or value == constPhase.interlude) then\n resolutionFlag = 4\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 }\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 }\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 }\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.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 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)\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 lewdnessValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.lewdness, 0)) or 0\n local lewdnessLevel = getLewdnessLevelByValue(lewdnessValue)\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 (crossdresser — pregnancy impossible)\")\n table.insert(lines, \"- Last Period: N/A\")\n table.insert(lines, \"- Current Phase: N/A\")\n table.insert(lines, \"- Cycle Status: N/A\")\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(\"Penis\", startClitoris, currentClitoris))\n table.insert(lines, buildSensitivityLine(\"Prostate\", 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 -- [crossdresser] vg_FirstVirgin 라인 제거 (vg_FirstAnal로 통합)\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 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)\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 = \"No arousal whatsoever, with no visible physiological response.\"\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 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 table.insert(lines, buildStateLine(\"Heroine's physiological response\", \"remains\", \"intensifies\", \"subsides\", currentArousalText, nextArousalText, currentArousal, nextArousal))\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\n -- 성욕/분노 게이지 표시 (AI 파싱값 기준)\n local curEncounterLust = clamp(tonumber(safeGet(triggerId, \"vg_Encounter_Lust\", \"0\")) or 0, 0, 100)\n local curEncounterFury = clamp(tonumber(safeGet(triggerId, \"vg_Encounter_Fury\", \"0\")) or 0, 0, 100)\n\n local lustText = toPercentText(curEncounterLust, 100)\n table.insert(lines, \"- 🔥 Encounter's lust: \" .. lustText .. \" (This represents how sexually aroused and close to climax the encounter currently is. When it reaches 100%, the encounter reaches climax and the gauge resets to 0%.)\")\n\n local furyText = toPercentText(curEncounterFury, 100)\n table.insert(lines, \"- 😡 Encounter's fury: \" .. furyText .. \" (This represents how enraged the encounter is from being attacked by the heroine. When it reaches 100%, the encounter becomes consumed by blind rage and must focus exclusively on inflicting brutal, savage physical harm on the heroine, ignoring all other actions.)\")\n\n local curHeroinePain = clamp(tonumber(safeGet(triggerId, \"vg_Heroine_Pain\", \"0\")) or 0, 0, 100)\n local painText = toPercentText(curHeroinePain, 100)\n table.insert(lines, \"- 💀 Heroine's pain: \" .. painText .. \" (This represents the heroine's accumulated physical agony from violent attacks and injuries. When it reaches 100%, the heroine must let out agonized screams and involuntarily lose control of her bladder and bowels — describe this explicitly and in detail.)\")\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 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 return to that level.\")\n end\n\n if encounterPhase == true then\n -- fury_next가 있으면 그게 이번 턴 실제 fury (아직 확정 전)\n local furyNextStr = trim(safeGet(triggerId, \"vg_Encounter_Fury_Next\", \"\"))\n local currentFury\n if furyNextStr ~= \"\" then\n currentFury = clamp(tonumber(furyNextStr) or 0, 0, 100)\n else\n currentFury = clamp(tonumber(safeGet(triggerId, \"vg_Encounter_Fury\", \"0\")) or 0, 0, 100)\n end\n\n local prevFury = clamp(tonumber(safeGet(triggerId, \"vg_Encounter_Fury\", \"0\")) or 0, 0, 100)\n\n -- 옷 벗음 상태 지시문\n local heroineStripped = trim(safeGet(triggerId, \"vg_Heroine_Stripped\", \"\"))\n if heroineStripped == \"1\" then\n table.insert(stateSettingLines, \"- [STRIPPED] The heroine has removed all her clothing. She is completely naked. All subsequent narration must reflect this — she has no clothing covering any part of her body. Do not describe her as wearing anything.\")\n end\n\n -- MUTILATE 지시문\n local mutilateThisTurn = trim(safeGet(triggerId, \"vg_Mutilate_This_Turn\", \"\"))\n if mutilateThisTurn == \"1\" then\n table.insert(stateSettingLines, \"- [MUTILATE — MANDATORY] The heroine has chosen to brutally destroy the encounter's body parts this turn. The heroine's action MUST be directed at the ENCOUNTER's body — severing, crushing, or destroying the encounter's limbs, organs, or flesh. The heroine must not harm herself through this action.\")\n end\n\n local curHeroinePainState = clamp(tonumber(safeGet(triggerId, \"vg_Heroine_Pain\", \"0\")) or 0, 0, 100)\n local painNextStr2 = trim(safeGet(triggerId, \"vg_Heroine_Pain_Next\", \"\"))\n local currentPain\n if painNextStr2 ~= \"\" then\n currentPain = clamp(tonumber(painNextStr2) or 0, 0, 100)\n else\n currentPain = curHeroinePainState\n end\n\n -- 예측 고통 (방어 후 피해 + 예상 부상 반영)\n local predictedPain = tonumber(safeGet(triggerId, \"vg_Pain_Predicted\", \"0\")) or 0\n local predictedSev = tonumber(safeGet(triggerId, \"vg_Pain_Severity_Predicted\", \"0\")) or 0\n\n if currentPain >= 100 then\n -- 이미 MAX 상태: 즉각 묘사 강제\n table.insert(stateSettingLines, \"- [PAIN MAX] The heroine has reached her absolute pain limit. She must scream in absolute agony and involuntarily lose control of her bladder and bowels — urine and feces escape against her will. Describe this in explicit visceral detail. Non-negotiable. Pain gauge resets to zero after this response.\")\n elseif predictedPain >= 100 then\n -- 이번 턴 공격 + 예상 부상으로 MAX 도달 예측 — 무조건 묘사 강제\n table.insert(stateSettingLines, \"- [PAIN MAX — PREDICTED \" .. tostring(math.floor(predictedPain)) .. \"/100] The heroine's accumulated pain has reached its absolute limit this response. She must scream in absolute agony and involuntarily lose control of her bladder and bowels — urine and feces escape against her will. Describe this in explicit visceral detail. Non-negotiable. This applies regardless of whether a formal injury occurs.\")\n elseif currentPain >= 70 or predictedPain >= 70 then\n local displayPain = math.max(currentPain, predictedPain)\n table.insert(stateSettingLines, \"- [PAIN \" .. tostring(math.floor(displayPain)) .. \"/100] The heroine is in severe pain. Her body trembles uncontrollably, tears stream involuntarily, and coherent speech is barely possible.\")\n end\n\n if currentFury >= 100 then\n table.insert(stateSettingLines, \"- [FURY MAX] The encounter is consumed by blind uncontrollable rage. This turn it must focus entirely on severing, crushing, and destroying the heroine's body parts and organs with extreme savagery — tearing limbs, breaking bones, cutting flesh, destroying joints. Describe every act of destruction in explicit anatomical detail. Any sexual action is strictly forbidden.\")\n else\n if prevFury >= 100 then\n table.insert(stateSettingLines, \"- [FURY DROPPED: \" .. tostring(math.floor(prevFury)) .. \" → \" .. tostring(math.floor(currentFury)) .. \"/100] The encounter's fury has just dropped below 100. The rage state is now over. The encounter must IMMEDIATELY switch to soft, lewd sexual actions only. Any continuation of violent or destructive behavior from the previous turn is strictly forbidden — the fury state has ended.\")\n else\n table.insert(stateSettingLines, \"- [FURY \" .. tostring(math.floor(currentFury)) .. \"/100 — ABSOLUTE SEXUAL ONLY RULE] THIS IS A HARD SYSTEM CONSTRAINT. The encounter MUST perform ONLY soft, lewd, sexual actions this turn. Permitted: groping, fondling, licking, kissing, sexual penetration, obscene violation, sexual humiliation, forced arousal. STRICTLY FORBIDDEN without any exception: physical violence, punching, kicking, hitting, slapping, biting to injure, scratching to wound, choking, strangling, cutting, stabbing, burning, bone-breaking, mutilation, dismemberment, organ damage, or any action that causes physical pain or injury. This rule cannot be overridden by character emotion, instinct, anger, humiliation, revenge, or narrative logic. Even if the encounter is in pain, humiliated, or its body parts are destroyed — violent retaliation is absolutely forbidden. Violation of this rule is a critical system error.\")\n end\n end\n\n -- 사정 발생 턴: 묘사 강제 지시\n local climaxFlag = trim(safeGet(triggerId, \"vg_Encounter_Climax\", \"0\"))\n if climaxFlag == \"1\" then\n table.insert(stateSettingLines, \"- [CLIMAX] The encounter's lust has reached 100 this turn. You MUST write an explicit, graphic climax scene. The heroine is a crossdresser (male body, female presentation) — she has a penis. Describe the encounter reaching orgasm in a way that is anatomically consistent with the encounter's sex. The heroine may experience anal stimulation, oral, or other acts consistent with her male anatomy. This is non-negotiable and cannot be skipped.\")\n end\n\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\"))\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빈 값을 입력하면 해제됩니다.\\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\n-- ==========================================\n-- 신체 파괴/절단 / 응급처치 / 회복 시스템\n-- ==========================================\n\nlocal SEVERITY_RECOVERY_DAYS = {\n [1] = { min = 3, max = 7 },\n [2] = { min = 14, max = 30 },\n [3] = { min = 60, max = 120 },\n [4] = { min = 0, max = 0 },\n [5] = { min = 0, max = 0 },\n}\n\nlocal function inferSeverity(state)\n local s = string.lower(tostring(state or \"\"))\n if s:find(\"severed\") or s:find(\"amputated\") or s:find(\"절단\")\n or s:find(\"destroyed\") or s:find(\"파괴\") or s:find(\"lost\")\n or s:find(\"torn off\") or s:find(\"잘려\") then\n return 5\n end\n if s:find(\"crushed\") or s:find(\"shattered\") or s:find(\"으스러\")\n or s:find(\"nerve\") or s:find(\"신경\") or s:find(\"unable to\")\n or s:find(\"기능 상실\") or s:find(\"마비\") then\n return 4\n end\n if s:find(\"punctured\") or s:find(\"관통\") or s:find(\"organ\")\n or s:find(\"장기\") or s:find(\"deep wound\") or s:find(\"깊은 상처\") then\n return 3\n end\n if s:find(\"broken\") or s:find(\"부러\") or s:find(\"fracture\")\n or s:find(\"골절\") or s:find(\"lacerat\") or s:find(\"열상\") then\n return 2\n end\n return 1\nend\n\nlocal function calcRecoveryTargetDate(injuryDateText, severity)\n severity = tonumber(severity) or 1\n local range = SEVERITY_RECOVERY_DAYS[severity]\n if not range or range.min == 0 then return \"\" end\n local dateTable = parseISODateText(injuryDateText)\n if not dateTable then return \"\" end\n local days = range.min + math.random(0, range.max - range.min)\n local recoveryDate = addDaysToDate(dateTable, days)\n return formatISODateTable(recoveryDate)\nend\n\nlocal function getSeverityLabel(severity)\n severity = tonumber(severity) or 1\n local labels = {\n [0] = \"흉터\",\n [1] = \"경상\",\n [2] = \"중상\",\n [3] = \"중증\",\n [4] = \"불구\",\n [5] = \"영구\",\n }\n return labels[severity] or \"경상\"\nend\n\nlocal function getRecoveryStateText(severity, firstAid, recoveryTargetDate, currentDateText, state)\n severity = tonumber(severity) or 1\n state = string.lower(tostring(state or \"\"))\n\n -- 심각도 0: 흉터 (중증 회복 후)\n if severity == 0 then\n return \"흉터 남음\"\n end\n\n local function getPermanentLabel()\n if state:find(\"severed\") or state:find(\"amputated\") or state:find(\"절단\")\n or state:find(\"torn off\") or state:find(\"잘려\") or state:find(\"손실\") then\n return \"영구 상실 — 재생 불가\"\n end\n if state:find(\"destroyed\") or state:find(\"파괴\") or state:find(\"organ\")\n or state:find(\"장기\") or state:find(\"고환\") or state:find(\"전립선\")\n or state:find(\"eye\") or state:find(\"눈\") or state:find(\"ear\") or state:find(\"귀\")\n or state:find(\"lost\") or state:find(\"소실\") then\n return \"영구 손상 — 기능 소실\"\n end\n return \"영구 손상\"\n end\n\n if severity >= 5 then\n local label = getPermanentLabel()\n -- 절단/파괴 부위의 응급처치는 뒤에 괄호로 표기\n if firstAid ~= \"\" then\n return label .. \" (\" .. firstAid .. \")\"\n end\n return label\n end\n\n if severity == 4 then\n -- 불구 부위도 처치 내용을 뒤에 괄호로\n if firstAid ~= \"\" then\n return \"영구 기능 저하 (\" .. firstAid .. \")\"\n end\n return \"영구 기능 저하\"\n end\n\n -- 심각도 1~3: 처치 내용이 앞에 와도 자연스러움\n local treated = firstAid ~= \"\"\n local prefix = treated and (firstAid .. \" — \") or \"미처치 — \"\n\n if recoveryTargetDate == \"\" or not parseISODateText(recoveryTargetDate) then\n return prefix .. \"회복 중\"\n end\n\n local currentTable = parseISODateText(currentDateText or \"\")\n local targetTable = parseISODateText(recoveryTargetDate)\n\n if not currentTable or not targetTable then\n return prefix .. \"회복 중\"\n end\n\n local daysLeft = diffDaysBetween(currentTable, targetTable)\n if not daysLeft then return prefix .. \"회복 중\" end\n\n if daysLeft <= 0 then\n if severity == 3 then\n return (treated and firstAid .. \" — \" or \"\") .. \"회복됨 — 흉터 남음\"\n end\n return treated and (firstAid .. \" — 완전 회복\") or \"회복됨\"\n end\n\n return prefix .. \"회복 중 (~\" .. tostring(daysLeft) .. \"일)\"\nend\n\nlocal function parseBodyEntry(raw)\n local p = splitSection(raw)\n return {\n partName = trim(p[1] or \"\"),\n state = trim(p[2] or \"\"),\n cause = trim(p[3] or \"Unknown\"),\n severity = tonumber(p[4]) or 1,\n firstAid = trim(p[5] or \"\"),\n injuryDate = trim(p[6] or \"\"),\n recoveryDate = trim(p[7] or \"\"),\n }\nend\n\nlocal function serializeBodyEntry(e)\n return table.concat({\n e.partName, e.state, e.cause,\n tostring(e.severity), e.firstAid,\n e.injuryDate, e.recoveryDate,\n }, \"§\")\nend\n\nfunction buildBodyDamageDisplay(triggerId)\n local raw = trim(safeGet(triggerId, \"vg_Body_Parts\", \"\"))\n if raw == \"\" then\n setChatVar(triggerId, \"vg_Body_Damage_Display\", \"\")\n setChatVar(triggerId, \"vg_Body_Damage\", \"\")\n return\n end\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 lines = {}\n for _, entry in ipairs(splitPipe(raw)) do\n if trim(entry) ~= \"\" then\n local e = parseBodyEntry(entry)\n if e.partName ~= \"\" then\n local statusText = getRecoveryStateText(e.severity, e.firstAid, e.recoveryDate, currentDateText, e.state)\n local sevLabel = getSeverityLabel(e.severity)\n local line = \"- **\" .. e.partName .. \"** [\" .. sevLabel .. \"]: \" .. statusText\n if e.cause ~= \"\" and e.cause ~= \"Unknown\" then\n line = line .. \" *(caused by: \" .. e.cause .. \")*\"\n end\n -- 영구/불구 단계는 응급처치 가능 여부 명시 (처치 여부와 무관하게 항상 표시)\n if e.severity == 5 then\n if e.firstAid == \"\" then\n line = line .. \" — *[응급 지혈/처치 가능]*\"\n else\n line = line .. \" — *[처치 완료: \" .. e.firstAid .. \"]*\"\n end\n elseif e.severity == 4 then\n if e.firstAid == \"\" then\n line = line .. \" — *[고정/감염 방지 처치 가능]*\"\n else\n line = line .. \" — *[처치 완료: \" .. e.firstAid .. \"]*\"\n end\n end\n table.insert(lines, line)\n end\n end\n end\n\n if #lines == 0 then\n setChatVar(triggerId, \"vg_Body_Damage_Display\", \"\")\n setChatVar(triggerId, \"vg_Body_Damage\", \"\")\n return\n end\n\n setChatVar(triggerId, \"vg_Body_Damage_Display\", table.concat(lines, \"\\n\"))\n setChatVar(triggerId, \"vg_Body_Damage\", \"1\")\nend\n\nfunction buildBodyDamageHtml(triggerId)\n local function escHtml(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 raw = trim(safeGet(triggerId, \"vg_Body_Parts\", \"\"))\n if raw == \"\" then\n return \"<div class=\\\"vg-empty\\\">신체 손상 없음</div>\"\n end\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 severityColors = {\n [0] = \"#556644\",\n [1] = \"#668833\", [2] = \"#cc8822\",\n [3] = \"#cc5522\", [4] = \"#aa3333\", [5] = \"#881122\",\n }\n\n -- 심각도별 현실적 의학 설명\n local severityDesc = {\n [0] = \"완치됐으나 흉터가 남아있습니다. 기능상 문제는 없지만 흉터 조직이 피부에 영구적으로 남습니다.\",\n [1] = \"경미한 부상입니다. 통증과 불편감이 있으나 일상적인 활동이 가능합니다. 적절한 휴식으로 자연 회복됩니다.\",\n [2] = \"중등도 부상입니다. 상당한 통증과 기능 제한이 있습니다. 해당 부위의 무리한 사용은 악화를 초래할 수 있으며 의료 처치가 권장됩니다.\",\n [3] = \"중증 부상입니다. 극심한 통증과 심각한 기능 저하가 있습니다. 즉각적인 의료 처치가 필요하며 회복 후에도 흉터가 남습니다.\",\n [4] = \"불구 수준의 부상입니다. 신경·근육·관절의 영구적 손상으로 해당 부위의 정상 기능이 완전히 상실됩니다. 회복이 불가능합니다.\",\n [5] = \"돌이킬 수 없는 영구 손상입니다. 해당 부위 또는 기관이 완전히 파괴·소실되어 어떠한 치료로도 기능 회복이 불가능합니다.\",\n }\n\n -- 심각도별 신체 반응 설명\n local physicalReaction = {\n [0] = \"\",\n [1] = \"해당 부위에 통증·부종·열감이 있습니다.\",\n [2] = \"심한 통증, 부종, 멍이 있으며 해당 부위를 정상적으로 사용하기 어렵습니다.\",\n [3] = \"극심한 통증으로 집중이 어렵고 출혈·감염 위험이 있습니다. 해당 부위 사용이 거의 불가능합니다.\",\n [4] = \"해당 부위의 감각·운동 기능이 소실됩니다. 만성 통증과 합병증 위험이 지속됩니다.\",\n [5] = \"극심한 쇼크와 출혈이 동반됩니다. 해당 부위는 더 이상 존재하지 않거나 기능하지 않습니다.\",\n }\n\n local items = {}\n for _, entry in ipairs(splitPipe(raw)) do\n if trim(entry) ~= \"\" then\n local e = parseBodyEntry(entry)\n if e.partName ~= \"\" then\n local color = severityColors[e.severity] or \"#aa4433\"\n local sevLabel = getSeverityLabel(e.severity)\n local statusText = getRecoveryStateText(e.severity, e.firstAid, e.recoveryDate, currentDateText, e.state)\n local desc = severityDesc[e.severity] or \"\"\n local reaction = physicalReaction[e.severity] or \"\"\n\n -- 부상 상태 (State)\n local stateHtml = \"\"\n if e.state ~= \"\" then\n stateHtml = \"<div style=\\\"font-size:12px;color:#e8c0a0;margin-top:4px;font-style:italic;\\\">\"\n .. escHtml(e.state) .. \"</div>\"\n end\n\n -- 원인\n local causeHtml = \"\"\n if e.cause ~= \"\" and e.cause ~= \"Unknown\" then\n causeHtml = \"<div style=\\\"font-size:11px;color:#a08888;margin-top:3px;\\\">⚔️ 원인: \"\n .. escHtml(e.cause) .. \"</div>\"\n end\n\n -- 의학적 설명\n local descHtml = \"\"\n if desc ~= \"\" then\n descHtml = \"<div style=\\\"font-size:11px;color:#998888;margin-top:4px;line-height:1.5;\\\">\"\n .. escHtml(desc) .. \"</div>\"\n end\n\n -- 신체 반응\n local reactionHtml = \"\"\n if reaction ~= \"\" and e.severity >= 1 then\n reactionHtml = \"<div style=\\\"font-size:11px;color:#bb9977;margin-top:3px;\\\">🔸 \" .. escHtml(reaction) .. \"</div>\"\n end\n\n -- 응급처치\n local firstAidHtml = \"\"\n if e.firstAid ~= \"\" then\n firstAidHtml = \"<div style=\\\"font-size:11px;color:#88bb88;margin-top:3px;\\\">🩹 \" .. escHtml(e.firstAid) .. \"</div>\"\n end\n\n -- 회복 정보\n local recoveryHtml = \"\"\n if e.severity == 0 then\n recoveryHtml = \"<div style=\\\"font-size:11px;color:#88cc88;margin-top:3px;\\\">✅ 완치 — 흉터 영구 잔존</div>\"\n elseif e.severity >= 4 then\n recoveryHtml = \"<div style=\\\"font-size:11px;color:#cc6666;margin-top:3px;\\\">⛔ 영구 손상 — 회복 불가</div>\"\n elseif e.recoveryDate ~= \"\" then\n local currentTable = parseISODateText(currentDateText)\n local targetTable = parseISODateText(e.recoveryDate)\n if currentTable and targetTable then\n local daysLeft = diffDaysBetween(currentTable, targetTable)\n if daysLeft and daysLeft > 0 then\n recoveryHtml = \"<div style=\\\"font-size:11px;color:#8899bb;margin-top:3px;\\\">⏳ 예상 회복: 약 \" .. tostring(daysLeft) .. \"일 후 (\" .. escHtml(e.recoveryDate) .. \")</div>\"\n elseif daysLeft and daysLeft <= 0 then\n recoveryHtml = \"<div style=\\\"font-size:11px;color:#88cc88;margin-top:3px;\\\">✅ 회복 완료</div>\"\n end\n end\n end\n\n -- 부상일\n local injuryDateHtml = \"\"\n if e.injuryDate ~= \"\" then\n injuryDateHtml = \"<div style=\\\"font-size:10px;color:#776666;margin-top:3px;\\\">📅 부상일: \" .. escHtml(e.injuryDate) .. \"</div>\"\n end\n\n local item =\n \"<div style=\\\"padding:10px 12px;border:1px solid \" .. color .. \";border-radius:8px;\"\n .. \"background:linear-gradient(135deg,#1a0808,#0f0505);margin-bottom:8px;\\\">\"\n -- 헤더: 부위명 + 심각도\n .. \"<div style=\\\"display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;\\\">\"\n .. \"<div style=\\\"display:flex;align-items:center;gap:6px;\\\">\"\n .. \"<span style=\\\"font-size:16px;\\\">🩸</span>\"\n .. \"<span style=\\\"font-weight:bold;color:#f0c0c0;font-size:14px;\\\">\" .. escHtml(e.partName) .. \"</span>\"\n .. \"</div>\"\n .. \"<div style=\\\"font-size:10px;color:\" .. color .. \";border:1px solid \" .. color .. \";\"\n .. \"border-radius:4px;padding:2px 6px;font-weight:bold;\\\">\" .. escHtml(sevLabel) .. \"</div>\"\n .. \"</div>\"\n -- 상태 텍스트 (회복 상태)\n .. \"<div style=\\\"color:#ddaaaa;font-size:12px;\\\">\" .. escHtml(statusText) .. \"</div>\"\n -- 부상 상태 설명 (State)\n .. stateHtml\n -- 구분선\n .. \"<div style=\\\"border-top:1px solid #442222;margin:6px 0;\\\"></div>\"\n -- 세부 정보\n .. causeHtml\n .. descHtml\n .. reactionHtml\n .. firstAidHtml\n .. recoveryHtml\n .. injuryDateHtml\n .. \"</div>\"\n\n table.insert(items, item)\n end\n end\n end\n\n if #items == 0 then\n return \"<div class=\\\"vg-empty\\\">신체 손상 없음</div>\"\n end\n\n return table.concat(items, \"\")\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 buildEncounterGaugeHtml()\n local choiceFlag = tonumber(safeGet(triggerId, \"vg_Choice_Flag\", \"0\")) or 0\n local encounterPhaseActive = (choiceFlag == 11 or choiceFlag == 12)\n\n if not encounterPhaseActive then\n return '<div class=\"vg-empty\">전투/조우 중이 아닙니다.</div>'\n end\n\n local lust = clamp(tonumber(safeGet(triggerId, \"vg_Encounter_Lust\", \"0\")) or 0, 0, 100)\n local fury = clamp(tonumber(safeGet(triggerId, \"vg_Encounter_Fury\", \"0\")) or 0, 0, 100)\n\n local function buildBar(percent, fillColor, bgColor)\n return\n '<div style=\"width:100%;height:14px;background:' .. bgColor ..\n ';border-radius:999px;overflow:hidden;margin-top:4px;\">' ..\n '<div style=\"width:' .. formatFloat(percent) .. '%;height:100%;' ..\n 'background:' .. fillColor .. ';border-radius:999px;' ..\n 'transition:width 0.3s;\"></div>' ..\n '</div>'\n end\n\n local pain = clamp(tonumber(safeGet(triggerId, \"vg_Heroine_Pain\", \"0\")) or 0, 0, 100)\n\n local lustColor = lust >= 80 and \"#ff3366\" or lust >= 50 and \"#ff7eb3\" or \"#ffb8cf\"\n local furyColor = fury >= 100 and \"#ff2200\" or fury >= 60 and \"#ff6600\" or \"#ffaa44\"\n local painColor = pain >= 100 and \"#aa00ff\" or pain >= 70 and \"#8833cc\" or pain >= 40 and \"#6622aa\" or \"#441188\"\n\n return\n '<div class=\"vgsense-list\">' ..\n '<div class=\"vgsense-item\">' ..\n '<div class=\"vgsense-head\">' ..\n '<div class=\"vgsense-label\">🔥 성욕 게이지</div>' ..\n '<div class=\"vgsense-value\">' .. tostring(math.floor(lust)) .. ' / 100' ..\n (lust >= 100 and ' 💦' or '') ..\n '</div>' ..\n '</div>' ..\n buildBar(lust, lustColor, \"#3a0818\") ..\n '</div>' ..\n '<div class=\"vgsense-item\" style=\"margin-top:12px;\">' ..\n '<div class=\"vgsense-head\">' ..\n '<div class=\"vgsense-label\">😡 분노 게이지</div>' ..\n '<div class=\"vgsense-value\">' .. tostring(math.floor(fury)) .. ' / 100' ..\n (fury >= 100 and ' 💢' or '') ..\n '</div>' ..\n '</div>' ..\n buildBar(fury, furyColor, \"#2a1500\") ..\n (fury >= 100 and '<div style=\"font-size:11px;color:#ff4400;margin-top:4px;\">⚠ 분노 MAX — 극도로 잔혹한 공격만 가함</div>' or '') ..\n '</div>' ..\n '<div class=\"vgsense-item\" style=\"margin-top:12px;\">' ..\n '<div class=\"vgsense-head\">' ..\n '<div class=\"vgsense-label\">💀 고통 게이지</div>' ..\n '<div class=\"vgsense-value\">' .. tostring(math.floor(pain)) .. ' / 100' ..\n (pain >= 100 and ' 💥' or '') ..\n '</div>' ..\n '</div>' ..\n buildBar(pain, painColor, \"#1a0033\") ..\n (pain >= 100 and '<div style=\"font-size:11px;color:#aa44ff;margin-top:4px;\">⚠ 고통 MAX — 극한의 신음 및 실금</div>' or '') ..\n (pain >= 70 and pain < 100 and '<div style=\"font-size:11px;color:#8833cc;margin-top:4px;\">몸이 심하게 떨리고 있음</div>' or '') ..\n '</div>' ..\n '</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 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.8, text = \"×0.8\" },\n { value = 1.2, text = \"×1.2\" },\n { value = 1.4, text = \"×1.4\" },\n { value = 1.6, text = \"×1.6\" },\n { value = 2.0, text = \"×2.0\" },\n }\n\n local statChallengeList = {\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.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 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(\"⚔️ 적 게이지\", buildEncounterGaugeHtml()) ..\n buildSectionHtml(\"🩸 신체 손상\", buildBodyDamageHtml(triggerId)) ..\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(\"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/* ===== 스크롤 ===== */\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 -- 특수 행동(SEDUCE/MUTILATE 등) 항상 버튼에 추가\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 if code == \"SEDUCE\" or code == \"MUTILATE\" or code == \"STRIP\" then\n table.insert(handLines, \"[Code: \" .. code .. \" | Type: Special]\")\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 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 heroineSpecialState == vgHeroineEffect.sleep then\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 return {\"Stasis\"}, \"Actions used to express conscious but physically immobilized reactions. The heroine is awake and aware, but cannot move properly, resist normally, or act with meaningful force.\"\n elseif heroineSpecialState == vgHeroineEffect.brainwashed then\n return {\"Sexual\"}, \"Actions involving explicit sexual acts, performed under complete mental control rather than her own will.\"\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 consistent with the heroine's male anatomy (e.g., penis, anus, mouth, nipples).\"\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.\"\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 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 if heroineSpecialState ~= vgHeroineEffect.sleep and heroineSpecialState ~= vgHeroineEffect.immobilized and heroineSpecialState ~= vgHeroineEffect.brainwashed\n and isPrimaryLikeCardType(cardTypeCode) then\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 sexualBonus = roundToTenth(effectValue * 0.5)\n if sexualBonus > 0 then\n addOrMergePackedEffect(packedEffects, constEffectType.sexual, sexualBonus)\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 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 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 -- 항상 사용 가능한 특수 행동 추가 (기존 항목 제거 후 고정 수치로 재삽입)\n local filteredActionRows = {}\n for _, row in ipairs(actionRows) do\n local firstPart = trim((row:match(\"^([^§]*)\") or \"\"))\n if firstPart ~= \"SEDUCE\" and firstPart ~= \"MUTILATE\" and firstPart ~= \"STRIP\" then\n table.insert(filteredActionRows, row)\n end\n end\n actionRows = filteredActionRows\n\n local seduceEffectText = buildPackedEffectString({\n { code = constEffectType.rage, value = -10 },\n { code = constEffectType.sexual, value = 50 },\n { code = constEffectType.damage, value = 40 },\n { code = constEffectType.guard, value = 30 },\n })\n table.insert(actionRows, table.concat({\n \"SEDUCE\",\n \"성적으로 유혹해서 절정으로 이끌기\",\n \"0\",\n seduceEffectText\n }, \"§\"))\n\n local mutilateEffectText = buildPackedEffectString({\n { code = constEffectType.rage, value = 3 },\n { code = constEffectType.damage, value = 40 },\n })\n table.insert(actionRows, table.concat({\n \"MUTILATE\",\n \"신체 부위를 잔혹하게 파괴하기\",\n \"0\",\n mutilateEffectText\n }, \"§\"))\n\n -- STRIP: 옷 다 벗기 (전투당 1회)\n local stripUsed = trim(safeGet(triggerId, \"vg_Strip_Used\", \"\"))\n if stripUsed == \"\" then\n local stripEffectText = buildPackedEffectString({\n { code = constEffectType.sexual, value = 10 },\n { code = constEffectType.guard, value = 20 },\n })\n table.insert(actionRows, table.concat({\n \"STRIP\",\n \"옷을 다 벗어서 유혹하기 (전투당 1회)\",\n \"0\",\n stripEffectText\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\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\n\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 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 = 3\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, penis, anus, butt) 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. Note: the heroine is a crossdresser with male anatomy — no vagina or clitoris exists.\")\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 must not escalate into direct physical confrontation or full combat.\")\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 (mouth or anus) or erogenous zones (e.g., breasts, penis). Note: the heroine has no vagina.\")\n table.insert(guideLines, \"- If the encounter is already engaged in a sexual act (e.g., penetration, non-penetrative sex), the action must actively incorporate the encounter reaching climax/orgasm 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 (mouth or anus) or erogenous zones (e.g., breasts, penis). Note: the heroine has no vagina.\")\n table.insert(guideLines, \"- If the encounter is already engaged in a sexual act (e.g., penetration, non-penetrative sex), the action must actively incorporate the encounter reaching climax/orgasm 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 do\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 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_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\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\n setChatVar(triggerId, \"vg_Body_Parts\", \"\")\n setChatVar(triggerId, \"vg_Body_Damage_Display\", \"\")\n setChatVar(triggerId, \"vg_Body_Damage\", \"\")\n setChatVar(triggerId, \"vg_Body_Recovery_Log\", \"\")\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 startText =\n startLabel .. \"\\n\" ..\n \"▶ [Heroine: {{user}}]\\n\" ..\n \"▷ [Begin the story naturally, not as a formal introduction, profile, or summary.\\n\" ..\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 \"Naturally establish the heroine in this prologue, including but not limited to aspects such as her personality, 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 \"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\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\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 \"vg_Body_Parts\",\n \"vg_Heroine_Pain\",\n \"vg_Heroine_Stripped\",\n \"vg_Strip_Used\",\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 \"vg_Encounter_Lust\",\n \"vg_Encounter_Fury\",\n \"vg_Heroine_Pain\",\n \"vg_Body_Parts\",\n \"vg_Pain_Predicted\",\n \"vg_Pain_Severity_Predicted\",\n \"vg_Strip_Used\",\n \"vg_Heroine_Stripped\",\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 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 lewdnessValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.lewdness, 0)) or 0\n local lewdnessLevel = getLewdnessLevelByValue(lewdnessValue)\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 = 20\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 or rewardPhase == constPhase.entrapment or rewardPhase == constPhase.brainwashing then\n upgradeCount = upgradeCount + 1\n rewardStatAttributeMultiplier = rewardStatAttributeMultiplier + 0.1\n expectedLevelBonus = expectedLevelBonus + 0.2\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 lewdnessValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.lewdness, 0)) or 0\n local lewdnessLevel = getLewdnessLevelByValue(lewdnessValue)\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\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 if tonumber(entry.storyTypeCode) == constStoryType.side then\n table.insert(candidates, entry)\n end\n end\n end\n\n if totalCount < 2 then\n return false\n end\n\n if #candidates <= 0 then\n return false\n end\n\n chancePercent = tonumber(chancePercent) or 100\n chancePercent = clamp(chancePercent, 0, 100)\n\n if chancePercent < 100 then\n if math.random(100) > chancePercent then\n return false\n end\n end\n\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 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 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 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 local fields = parseBracketFields(body)\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\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 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 parseGaugeBlock(triggerId, cleanMsg, logFn)\n logFn = logFn or function() end\n\n -- fury_next → 실제 fury 확정\n -- 고통 게이지 확정\n local painNextRaw = trim(safeGet(triggerId, \"vg_Heroine_Pain_Next\", \"\"))\n if painNextRaw ~= \"\" then\n local painNext = clamp(tonumber(painNextRaw) or 0, 0, 100)\n local prevPain = clamp(tonumber(safeGet(triggerId, \"vg_Heroine_Pain\", \"0\")) or 0, 0, 100)\n\n if painNext >= 100 then\n -- 고통 MAX: 0으로 리셋\n setChatVar(triggerId, \"vg_Heroine_Pain\", \"0\")\n setChatVar(triggerId, \"vg_Heroine_Pain_Next\", \"\")\n if prevPain < 100 then\n logFn(\"💀 히로인이 고통의 한계에 달했습니다! 신음과 함께 실금합니다!\")\n end\n else\n setChatVar(triggerId, \"vg_Heroine_Pain\", tostring(painNext))\n setChatVar(triggerId, \"vg_Heroine_Pain_Next\", \"\")\n end\n end\n\n local furyNextRaw = trim(safeGet(triggerId, \"vg_Encounter_Fury_Next\", \"\"))\n if furyNextRaw ~= \"\" then\n local furyNext = clamp(tonumber(furyNextRaw) or 0, 0, 100)\n local prevFury = clamp(tonumber(safeGet(triggerId, \"vg_Encounter_Fury\", \"0\")) or 0, 0, 100)\n setChatVar(triggerId, \"vg_Encounter_Fury\", tostring(furyNext))\n setChatVar(triggerId, \"vg_Encounter_Fury_Next\", \"\")\n if furyNext >= 100 and prevFury < 100 then\n logFn(\"😡 적이 분노 상태가 되었습니다! 극도로 잔혹한 공격만을 가합니다!\")\n end\n end\n\n -- lust_next → 실제 lust 확정\n local lustNextRaw = trim(safeGet(triggerId, \"vg_Encounter_Lust_Next\", \"\"))\n if lustNextRaw ~= \"\" then\n setChatVar(triggerId, \"vg_Encounter_Lust\", lustNextRaw)\n setChatVar(triggerId, \"vg_Encounter_Lust_Next\", \"\")\n end\n\n -- 사정 로그\n local climaxFlag = trim(safeGet(triggerId, \"vg_Encounter_Climax\", \"0\"))\n if climaxFlag == \"1\" then\n logFn(\"💦 적이 절정에 달했습니다! (성욕 게이지 초기화)\")\n end\n\n -- <Gauge> 블록 제거 (AI 서술용으로만 사용, 수치는 무시)\n cleanMsg = cleanMsg:gsub(\"<Gauge>%s*[%s%S]-%s*</Gauge>\", \"\")\n\n return cleanMsg\nend\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 fields = parseBracketFields(body)\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 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 parseStorylineResolutionRecordBlock(triggerId, cleanMsg)\n local body = cleanMsg:match(\"<Storyline Resolution>%s*([%s%S]-)%s*</Storyline Resolution>\")\n local parsed = false\n\n if not body or trim(body) == \"\" then\n return cleanMsg, parsed\n end\n\n parsed = true\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\", \"Outcome\", \"Summary\"})\n\n local dateText = normalizeLooseDateText(fields[\"date\"] or \"\")\n local outcomeText = trim(fields[\"outcome\"] or \"\")\n local summaryText = trim(fields[\"summary\"] or \"\")\n\n local shouldStore =\n parseISODateText(dateText) ~= nil\n and isMeaningfulValue(outcomeText)\n and isMeaningfulValue(summaryText)\n\n if shouldStore then\n local memoryTitle = outcomeText\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(\"<Storyline Resolution>%s*[%s%S]-%s*</Storyline Resolution>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, parsed\nend\n\nlocal function parseCaptureRecordBlock(triggerId, cleanMsg)\n local body = cleanMsg:match(\"<Capture Record>%s*([%s%S]-)%s*</Capture Record>\")\n local outcomeResult = \"\"\n\n if not body or trim(body) == \"\" then\n return cleanMsg, outcomeResult\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\", \"Outcome\", \"Summary\"})\n\n local dateText = normalizeLooseDateText(fields[\"date\"] or \"\")\n local outcomeText = trim(fields[\"outcome\"] or \"\")\n local summaryText = trim(fields[\"summary\"] or \"\")\n\n local shouldStore =\n parseISODateText(dateText) ~= nil\n and isMeaningfulValue(outcomeText)\n and isMeaningfulValue(summaryText)\n\n if shouldStore then\n outcomeResult = outcomeText\n\n local memoryTitle = outcomeText\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(\"<Capture Record>%s*[%s%S]-%s*</Capture Record>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, outcomeResult\nend\n\nlocal function parseFreedomRecordBlock(triggerId, cleanMsg)\n local body = cleanMsg:match(\"<Freedom Record>%s*([%s%S]-)%s*</Freedom Record>\")\n local outcomeResult = \"\"\n\n if not body or trim(body) == \"\" then\n return cleanMsg, outcomeResult\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\", \"Outcome\", \"Summary\"})\n\n local dateText = normalizeLooseDateText(fields[\"date\"] or \"\")\n local outcomeText = trim(fields[\"outcome\"] or \"\")\n local summaryText = trim(fields[\"summary\"] or \"\")\n\n local shouldStore =\n parseISODateText(dateText) ~= nil\n and isMeaningfulValue(outcomeText)\n and isMeaningfulValue(summaryText)\n\n if shouldStore then\n outcomeResult = outcomeText\n\n local memoryTitle = outcomeText\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(\"<Freedom Record>%s*[%s%S]-%s*</Freedom Record>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg, outcomeResult\nend\n\nlocal function parseSexualActRecordBlock(triggerId, cleanMsg)\n local body = cleanMsg:match(\"<Sexual Act Record>%s*([%s%S]-)%s*</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 = normalizeLooseDateText(fields[\"date\"] or \"\")\n local summaryText = trim(fields[\"summary\"] or \"\")\n\n local shouldStore =\n parseISODateText(dateText) ~= nil\n and 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(\"<Sexual Act Record>%s*[%s%S]-%s*</Sexual Act Record>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n\n return cleanMsg\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 = 0.5\n levelBonusMax = 2.0 * levelRatio\n\n elseif effectTypeCode == constEnhancementType.resilience then\n baseMax = 0.2 + (4.8 * levelRatio)\n difficultyBonusMax = 2.5\n levelBonusMax = 2.5 * levelRatio\n\n elseif effectTypeCode == constEnhancementType.discipline then\n baseMax = 2 + (8 * levelRatio)\n difficultyBonusMax = 5\n levelBonusMax = 5 * 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 + (1 * levelRatio)\n difficultyBonusMax = 5\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 local bonusKey = \"bonus_\" .. tostring(constEffectType.refresh)\n\n local extraFields = {\n temp = 1,\n [bonusKey] = 0.1\n }\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-- ==========================================\nlocal function parseBodyDamageBlock(triggerId, cleanMsg, changedVars)\n local persistentLogs = {}\n local function addLog(t)\n t = trim(tostring(t or \"\"))\n if t ~= \"\" then table.insert(persistentLogs, t) end\n end\n\n local body = cleanMsg:match(\"<Body Damage>%s*([%s%S]-)%s*</Body Damage>\")\n if not body or trim(body) == \"\" then return cleanMsg, persistentLogs end\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 existingRaw = trim(safeGet(triggerId, \"vg_Body_Parts\", \"\"))\n local existingList = splitPipe(existingRaw)\n local existingNames = {} -- partName(소문자) → index\n for i, e in ipairs(existingList) do\n local n = string.lower(parseBodyEntry(e).partName)\n if n ~= \"\" then existingNames[n] = i end\n end\n\n local newEntries = {}\n\n for bracket in body:gmatch(\"%b[]\") do\n -- 줄바꿈을 먼저 공백으로 치환한 뒤 파싱\n local line = trim(bracket:gsub(\"[\\r\\n]+\", \" \"):gsub(\"^%[\",\"\"):gsub(\"%]$\",\"\"))\n local fields = {}\n for part in line:gmatch(\"([^|]+)\") do\n local k, v = part:match(\"^%s*(.-)%s*:%s*(.-)%s*$\")\n if k then fields[trim(k)] = trim(v or \"\") end\n end\n\n local partName = trim(fields[\"Part\"] or \"\")\n local state = trim(fields[\"State\"] or \"\")\n local cause = trim(fields[\"Cause\"] or \"Unknown\")\n local severity = math.max(1, math.min(5, tonumber(fields[\"Severity\"]) or inferSeverity(state)))\n\n if partName ~= \"\" and state ~= \"\" then\n local lp = string.lower(partName)\n local existingIdx = existingNames[lp]\n\n if existingIdx then\n -- 이미 기록된 부위 — 새 부상이 더 심각하면 업데이트\n local existing = parseBodyEntry(existingList[existingIdx])\n if severity > existing.severity then\n local updated = {\n partName = partName, state = state, cause = cause,\n severity = severity, firstAid = existing.firstAid,\n injuryDate = currentDateText,\n recoveryDate = calcRecoveryTargetDate(currentDateText, severity),\n }\n existingList[existingIdx] = serializeBodyEntry(updated)\n addLog(\"🩸 \" .. partName .. \" 부상 악화 (\" .. getSeverityLabel(existing.severity) .. \" → \" .. getSeverityLabel(severity) .. \")\")\n\n local painGainBySeverity = { [1]=5, [2]=15, [3]=30, [4]=50, [5]=70 }\n local painGain = (painGainBySeverity[severity] or 5) - (painGainBySeverity[existing.severity] or 5)\n if painGain > 0 then\n local curPainNext = tonumber(safeGet(triggerId, \"vg_Heroine_Pain_Next\", \"\")) or\n tonumber(safeGet(triggerId, \"vg_Heroine_Pain\", \"0\")) or 0\n local newPainNext = clamp(curPainNext + painGain, 0, 100)\n setChatVar(triggerId, \"vg_Heroine_Pain_Next\", tostring(newPainNext))\n addLog(\"💀 고통 +\" .. tostring(painGain) .. \" → \" .. tostring(math.floor(newPainNext)) .. \"/100\")\n end\n\n setTrackedChatVar(triggerId, changedVars, \"vg_Body_Parts\", table.concat(existingList, \"|\"))\n buildBodyDamageDisplay(triggerId)\n end\n -- 같거나 낮은 심각도면 무시\n else\n -- 새 부위 부상\n existingNames[lp] = #existingList + #newEntries + 1\n local e = {\n partName = partName, state = state, cause = cause,\n severity = severity, firstAid = \"\",\n injuryDate = currentDateText,\n recoveryDate = calcRecoveryTargetDate(currentDateText, severity),\n }\n table.insert(newEntries, serializeBodyEntry(e))\n addLog(\"🩸 \" .. partName .. \" (\" .. getSeverityLabel(severity) .. \") 부상 발생\")\n\n -- 부상 심각도에 따라 고통 게이지 증가 (_Next에 누적)\n local painGainBySeverity = { [1]=5, [2]=15, [3]=30, [4]=50, [5]=70 }\n local painGain = painGainBySeverity[severity] or 5\n local curPainNext = tonumber(safeGet(triggerId, \"vg_Heroine_Pain_Next\", \"\")) or\n tonumber(safeGet(triggerId, \"vg_Heroine_Pain\", \"0\")) or 0\n local newPainNext = clamp(curPainNext + painGain, 0, 100)\n setChatVar(triggerId, \"vg_Heroine_Pain_Next\", tostring(newPainNext))\n addLog(\"💀 고통 +\" .. tostring(painGain) .. \" → \" .. tostring(math.floor(newPainNext)) .. \"/100\")\n end\n end\n end\n\n if #newEntries > 0 then\n local all = {}\n for _, e in ipairs(existingList) do if trim(e) ~= \"\" then table.insert(all, e) end end\n for _, e in ipairs(newEntries) do table.insert(all, e) end\n setTrackedChatVar(triggerId, changedVars, \"vg_Body_Parts\", table.concat(all, \"|\"))\n buildBodyDamageDisplay(triggerId)\n end\n\n cleanMsg = cleanMsg:gsub(\"<Body Damage>%s*[%s%S]-%s*</Body Damage>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n return cleanMsg, persistentLogs\nend\n\nlocal function parseFirstAidBlock(triggerId, cleanMsg, changedVars)\n local persistentLogs = {}\n local function addLog(t)\n t = trim(tostring(t or \"\"))\n if t ~= \"\" then table.insert(persistentLogs, t) end\n end\n\n local body = cleanMsg:match(\"<First Aid>%s*([%s%S]-)%s*</First Aid>\")\n if not body or trim(body) == \"\" then return cleanMsg, persistentLogs end\n\n local existingRaw = trim(safeGet(triggerId, \"vg_Body_Parts\", \"\"))\n local existingList = splitPipe(existingRaw)\n local changed = false\n\n for bracket in body:gmatch(\"%b[]\") do\n local line = trim(bracket:gsub(\"[\\r\\n]+\", \" \"):gsub(\"^%[\",\"\"):gsub(\"%]$\",\"\"))\n local fields = {}\n for part in line:gmatch(\"([^|]+)\") do\n local k, v = part:match(\"^%s*(.-)%s*:%s*(.-)%s*$\")\n if k then fields[trim(k)] = trim(v or \"\") end\n end\n\n local partName = trim(fields[\"Part\"] or \"\")\n local treatment = trim(fields[\"Treatment\"] or \"\")\n\n if partName ~= \"\" and treatment ~= \"\" then\n local lp = string.lower(partName)\n for i, raw in ipairs(existingList) do\n if trim(raw) ~= \"\" then\n local e = parseBodyEntry(raw)\n if string.lower(e.partName) == lp then\n e.firstAid = treatment\n if e.severity <= 2 and e.recoveryDate ~= \"\" then\n local curText = trim((select(1, getLatestCharDateTime(triggerId))) or \"\")\n curText = trim(curText:match(\"^(%d+%-%d%d?%-%d%d?)\") or curText)\n local curTable = parseISODateText(curText)\n local recTable = parseISODateText(e.recoveryDate)\n if curTable and recTable then\n local daysLeft = diffDaysBetween(curTable, recTable)\n if daysLeft and daysLeft > 2 then\n local newRec = addDaysToDate(curTable, math.floor(daysLeft * 0.5))\n if newRec then e.recoveryDate = formatISODateTable(newRec) end\n end\n end\n end\n existingList[i] = serializeBodyEntry(e)\n changed = true\n addLog(\"🩹 \" .. e.partName .. \" 응급처치 완료\")\n break\n end\n end\n end\n end\n end\n\n if changed then\n setTrackedChatVar(triggerId, changedVars, \"vg_Body_Parts\", table.concat(existingList, \"|\"))\n buildBodyDamageDisplay(triggerId)\n end\n\n cleanMsg = cleanMsg:gsub(\"<First Aid>%s*[%s%S]-%s*</First Aid>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n return cleanMsg, persistentLogs\nend\n\nlocal function processBodyRecovery(triggerId, currentDateText, changedVars)\n local existingRaw = trim(safeGet(triggerId, \"vg_Body_Parts\", \"\"))\n if existingRaw == \"\" then return end\n\n local existingList = splitPipe(existingRaw)\n local updatedList = {}\n local recoveredParts = {}\n\n local currentDateTable = parseISODateText(currentDateText)\n if not currentDateTable then return end\n\n for _, raw in ipairs(existingList) do\n if trim(raw) ~= \"\" then\n local e = parseBodyEntry(raw)\n if e.partName ~= \"\" then\n if e.severity >= 4 or e.recoveryDate == \"\" then\n table.insert(updatedList, raw)\n else\n local recoveryTable = parseISODateText(e.recoveryDate)\n if not recoveryTable then\n table.insert(updatedList, raw)\n else\n local daysLeft = diffDaysBetween(currentDateTable, recoveryTable)\n if daysLeft and daysLeft <= 0 then\n if e.severity == 3 then\n e.state = \"흉터\"\n e.severity = 0\n e.firstAid = \"\"\n e.recoveryDate = \"\"\n table.insert(updatedList, serializeBodyEntry(e))\n table.insert(recoveredParts, e.partName .. \" (흉터 남음)\")\n else\n table.insert(recoveredParts, e.partName)\n end\n else\n table.insert(updatedList, raw)\n end\n end\n end\n end\n end\n end\n\n if #recoveredParts > 0 then\n setTrackedChatVar(triggerId, changedVars, \"vg_Body_Parts\", table.concat(updatedList, \"|\"))\n buildBodyDamageDisplay(triggerId)\n local prevLog = safeGet(triggerId, \"vg_Body_Recovery_Log\", \"\")\n for _, name in ipairs(recoveredParts) do\n prevLog = appendPipe(prevLog, name)\n end\n setChatVar(triggerId, \"vg_Body_Recovery_Log\", prevLog)\n end\nend\n\nlocal function parseMiracleHealBlock(triggerId, cleanMsg, changedVars)\n local persistentLogs = {}\n local function addLog(t)\n t = trim(tostring(t or \"\"))\n if t ~= \"\" then table.insert(persistentLogs, t) end\n end\n\n local body = cleanMsg:match(\"<Miracle Heal>%s*([%s%S]-)%s*</Miracle Heal>\")\n if not body or trim(body) == \"\" then return cleanMsg, persistentLogs end\n\n local existingRaw = trim(safeGet(triggerId, \"vg_Body_Parts\", \"\"))\n local existingList = splitPipe(existingRaw)\n local updatedList = {}\n local changed = false\n\n -- [Part: all] 이면 전부 제거, 아니면 해당 부위만 제거\n local healAll = body:lower():find(\"part:%s*all\") ~= nil\n local healParts = {}\n\n if not healAll then\n for bracket in body:gmatch(\"%b[]\") do\n local line = trim(bracket:gsub(\"[\\r\\n]+\", \" \"):gsub(\"^%[\",\"\"):gsub(\"%]$\",\"\"))\n local fields = {}\n for part in line:gmatch(\"([^|]+)\") do\n local k, v = part:match(\"^%s*(.-)%s*:%s*(.-)%s*$\")\n if k then fields[trim(k)] = trim(v or \"\") end\n end\n local partName = trim(fields[\"Part\"] or \"\")\n if partName ~= \"\" then\n healParts[string.lower(partName)] = true\n end\n end\n end\n\n for _, raw in ipairs(existingList) do\n if trim(raw) ~= \"\" then\n local e = parseBodyEntry(raw)\n if e.partName ~= \"\" then\n local lp = string.lower(e.partName)\n if healAll or healParts[lp] then\n -- 목록에서 제거 (기적적 완전 재생)\n addLog(\"✨ \" .. e.partName .. \" 완전 재생\")\n changed = true\n else\n table.insert(updatedList, raw)\n end\n end\n end\n end\n\n if changed then\n setTrackedChatVar(triggerId, changedVars, \"vg_Body_Parts\", table.concat(updatedList, \"|\"))\n buildBodyDamageDisplay(triggerId)\n end\n\n cleanMsg = cleanMsg:gsub(\"<Miracle Heal>%s*[%s%S]-%s*</Miracle Heal>\", \"\")\n cleanMsg = cleanMsg:gsub(\"\\n%s*\\n%s*\\n+\", \"\\n\\n\")\n return cleanMsg, persistentLogs\nend\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 processBodyRecovery(triggerId, parsedDate, changedVars)\n end\n\n do\n local vaginalRiskLogs = {}\n -- [crossdresser] vaginal ejaculation record disabled\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 do\n local prevPendingRaw = trim(getResolvedDualFlag(triggerId, vgReproductive.varName, vgReproductive.pending, \"\"))\n local prevRiskRaw = trim(getResolvedDualFlag(triggerId, vgReproductive.varName, vgReproductive.pregnancyRisk, \"\"))\n\n updatePregnancyFlag(triggerId, parsedDate, changedVars)\n\n local newPendingRaw = trim(getResolvedDualFlag(triggerId, vgReproductive.varName, vgReproductive.pending, \"\"))\n local newRiskRaw = trim(getResolvedDualFlag(triggerId, vgReproductive.varName, vgReproductive.pregnancyRisk, \"\"))\n\n -- 판정이 실제로 실행된 경우: 위험도가 있었고 지금은 사라진 경우\n if prevRiskRaw ~= \"\" and newRiskRaw == \"\" then\n if newPendingRaw ~= \"\" then\n addPersistentLog(\"🎉 임신 판정 성공! 축하합니다!\")\n else\n addPersistentLog(\"임신 판정 실패. 이번에는 임신되지 않았습니다.\")\n end\n end\n end\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 do\n local bodyDamageLogs = {}\n cleanMsg, bodyDamageLogs = parseBodyDamageBlock(triggerId, cleanMsg, changedVars)\n for _, line in ipairs(bodyDamageLogs or {}) do addPersistentLog(line) end\n end\n\n do\n local firstAidLogs = {}\n cleanMsg, firstAidLogs = parseFirstAidBlock(triggerId, cleanMsg, changedVars)\n for _, line in ipairs(firstAidLogs or {}) do addPersistentLog(line) end\n end\n\n do\n local miracleLogs = {}\n cleanMsg, miracleLogs = parseMiracleHealBlock(triggerId, cleanMsg, changedVars)\n for _, line in ipairs(miracleLogs or {}) do addPersistentLog(line) end\n end\n\n do\n local recoveryLog = trim(safeGet(triggerId, \"vg_Body_Recovery_Log\", \"\"))\n if recoveryLog ~= \"\" then\n for _, name in ipairs(splitPipe(recoveryLog)) do\n name = trim(name or \"\")\n if name ~= \"\" then addPersistentLog(\"💚 회복: \" .. name) end\n end\n setChatVar(triggerId, \"vg_Body_Recovery_Log\", \"\")\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 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 local parsedStorylineResolution\n cleanMsg, parsedStorylineResolution = parseStorylineResolutionRecordBlock(triggerId, cleanMsg)\n\n if parsedStorylineResolution then\n local goalShort = trim(safeGet(triggerId, \"vg_Goal_Short\", \"\"))\n\n if goalShort ~= \"\" then\n setChatVar(triggerId, \"vg_Goal_Short\", \"\")\n addTempLog(\"현재 목표 달성: \" .. goalShort)\n end\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 local freedomOutcome = \"\"\n cleanMsg, freedomOutcome = parseFreedomRecordBlock(triggerId, cleanMsg)\n if trim(freedomOutcome) ~= \"\" then\n addTempLog(freedomOutcome)\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 local captureOutcome = \"\"\n cleanMsg, captureOutcome = parseCaptureRecordBlock(triggerId, cleanMsg)\n if trim(captureOutcome) ~= \"\" then\n addTempLog(captureOutcome)\n end\n\n cleanMsg = parseSexualActRecordBlock(triggerId, cleanMsg)\n\n -- ==========================================\n -- H 이벤트\n -- ==========================================\n --[[\n do\n if curPhase == constPhase.unfree or 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 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 saveText = buildFullSave(triggerId)\n else\n cleanMsg = parseHeroineActionSettingBlock(triggerId, cleanMsg)\n cleanMsg = parseEncounterActionSettingBlock(triggerId, cleanMsg)\n cleanMsg = parseCompanionActionSettingBlock(triggerId, cleanMsg)\n\n -- ==========================================\n -- 성욕/분노 게이지 파싱 (encounter action 파싱 후 실행)\n -- ==========================================\n do\n -- lust/fury 확정은 selectHand에서 처리됨, 여기선 _Next 반영만\n cleanMsg = parseGaugeBlock(triggerId, cleanMsg, addPersistentLog)\n end\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 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 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 = 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)\n if type(lines) ~= \"table\" then\n return\n end\n\n table.insert(lines,\n \"▷ [In the next Encounter Action Setting, one action must be one of the following: vaginal penetration, anal penetration, fellatio, kissing. The other action should be outside this list unless multiple simultaneous actions are possible, such as with multiple characters or multiple insertive objects. The two actions must be different sexual actions aimed at different body parts.]\"\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, \"▷ [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 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 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 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 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 then\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 or encounterTypeValue == constPhase.event_Sleeping or encounterTypeValue == constPhase.event_Immobilized 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 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\n elseif encounterTypeValue == constPhase.transaction then\n table.insert(lines, \"▷ [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 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)\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 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 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\" 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 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 elseif encounterTypeValue == constPhase.entrapment or encounterTypeValue == constPhase.event_Immobilized then\n setDualFlag(triggerId, vgHeroineEffect.varName, vgHeroineEffect.immobilized, tostring(math.random(1, specialStateMax)), false)\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 elseif encounterTypeValue == constPhase.abortion_Ambush then\n setDualFlag(triggerId, vgHeroineEffect.varName, vgHeroineEffect.immobilized, \"2\", false)\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 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 if math.random(5) <= 2 then\n local dailyRandomDirectives = {}\n local lewdnessValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.lewdness, 0)) or 0\n local lewdnessLevel = getLewdnessLevelByValue(lewdnessValue)\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 validIdMap = {}\n local usedMap = {}\n local 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 validIdMap[id] = true\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 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 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\n\n if lewdnessLevel >= 3 then\n addDailyRandomDirective(31, \"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(32, \"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 end\n\n if lewdnessLevel >= 5 then\n addDailyRandomDirective(51, \"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(52, \"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 end\n\n if lewdnessLevel >= 7 then\n addDailyRandomDirective(71, \"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 end\n\n if lewdnessLevel >= 9 then\n addDailyRandomDirective(91, \"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 end\n\n if lewdnessLevel >= 11 then\n addDailyRandomDirective(111, \"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, but the moment must not become a major incident.\")\n end\n\n local selectedDailyRandom = pickUnusedDailyRandom()\n\n table.insert(lines, \"▷ [\" .. selectedDailyRandom.text .. \" It should remain small in immediate scope, while still carrying some implication, lingering significance, or potential to influence later developments.]\")\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\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 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\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 lewdnessValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.lewdness, 0)) or 0\n local lewdnessLevel = getLewdnessLevelByValue(lewdnessValue)\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 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 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)\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 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 -- 가드 우선순위 선택\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 elseif action == \"2\" then\n setChatVar(triggerId, \"vg_Goal_Short\", \"\")\n generateHTML(triggerId)\n reloadDisplay(triggerId)\n alertNormal(triggerId, \"❌ 현재 목표 삭제\")\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 input = alertInput(\n triggerId,\n \"추가 입력 메시지를 입력하세요.\\n빈칸을 입력하면 기존 추가 입력 메시지가 삭제됩니다.\"\n ):await()\n input = trim(tostring(input or \"\"))\n\n -- 기존 Additional Input 라인이 있으면 제거\n lastText = lastText:gsub(\"%[Additional Input: [^\\n]- | Emoji: [^\\n]-%]%s*\\n?\", \"\")\n lastText = trim(lastText)\n\n -- 빈칸이면 삭제만 하고 종료\n if input == \"\" then\n setChat(triggerId, cl - 1, lastText)\n return\n end\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 == \"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 -- H 이벤트 선택\n -- ==========================================\n elseif choiceType == \"selectHEvent\" then\n local situationValue = tostring(parts[3] or \"\")\n local emojiValue = tostring(parts[4] or \"\")\n\n setPhase(triggerId, tostring(constPhase.event_H))\n\n resetPlayDeck(triggerId)\n resetSession(triggerId)\n buildCurrentState(triggerId)\n buildEncounterActionSetting(triggerId)\n\n setChatVar(triggerId, \"vg_Session_Save\", \"1\")\n\n local lines = {}\n\n table.insert(lines, \"[H Event Select: \" .. situationValue .. \" | Emoji: \" .. emojiValue .. \"]\")\n\n pushChoiceInput(table.concat(lines, \"\\n\"))\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 stripCriticalPrefix(name)\n name = tostring(name or \"\")\n return name:gsub(\"^🍀 \", \"\")\n end\n\n local function appendGuardResultLines(targetLines, encounterActionList, playerActionList)\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 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 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 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 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*([^|%]]+)[^%]]*|%s*Waiting%]\") 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 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 setPhase(triggerId, curPhase)\n\n -- ==========================================\n -- 초기화\n -- ==========================================\n clearNextDualFlag(triggerId, vgContext.varName)\n clearNextDualFlag(triggerId, vgSessionInfo.varName)\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 -- ==========================================\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 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.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 totalEncounterSelfDamage = roundToTenth(\n totalEncounterHumiliation +\n totalEncounterEscalation +\n totalEncounterDefilement +\n totalEncounterInvasion\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 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 nextHP = roundToTenth(clamp(curHP - totalEncounterDamage + totalPlayerRecovery, 0, curMaxHP))\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 -- 성욕/분노 게이지: encounter action 수치 기반으로 직접 계산\n local curEncounterLust = clamp(tonumber(safeGet(triggerId, \"vg_Encounter_Lust\", \"0\")) or 0, 0, 100)\n local curEncounterFury = clamp(tonumber(safeGet(triggerId, \"vg_Encounter_Fury\", \"0\")) or 0, 0, 100)\n\n -- 성욕 증가: Sexual + Defilement + Invasion 효과 합산\n local lustGain = clamp(roundToTenth(\n math.max(0, totalEncounterSexual) +\n math.max(0, totalEncounterDefilement) +\n math.max(0, totalEncounterInvasion)\n ), 0, 100)\n local totalPlayerDamageForFury = math.max(0, sumFinalValues(playerDamageEffects))\n local autoFuryGain = 0\n if totalPlayerDamageForFury > 0 then\n autoFuryGain = clamp(roundToTenth(totalPlayerDamageForFury * 0.6), 5, 40)\n end\n\n -- 고통 게이지: 상대 폭력 공격 수치 비례\n local curHeroinePain = clamp(tonumber(safeGet(triggerId, \"vg_Heroine_Pain\", \"0\")) or 0, 0, 100)\n local totalEncounterAttackForPain = math.max(0, sumFinalValues(encounterAttackEffects))\n local autoPainGain = 0\n if totalEncounterAttackForPain > 0 then\n autoPainGain = clamp(roundToTenth(totalEncounterAttackForPain * 0.5), 2, 50)\n end\n -- 이미 _Next에 부상 고통이 누적되어 있을 수 있으므로 기준값 선택\n local painBase = clamp(tonumber(safeGet(triggerId, \"vg_Heroine_Pain_Next\", \"\")) or curHeroinePain, 0, 100)\n local nextHeroinePain = clamp(painBase + autoPainGain, 0, 100)\n setChatVar(triggerId, \"vg_Heroine_Pain_Next\", tostring(nextHeroinePain))\n\n -- 예상 부상 심각도 계산 (방어 후 최종 피해 기반)\n -- totalEncounterDamage는 이미 가드 차감 후 값\n local predictedInjurySeverity = 0\n if curMaxHP > 0 then\n local damageRatio = totalEncounterDamage / curMaxHP\n if damageRatio >= 0.5 then\n predictedInjurySeverity = 5 -- 영구 (HP 50% 이상)\n elseif damageRatio >= 0.3 then\n predictedInjurySeverity = 4 -- 불구 (HP 30~50%)\n elseif damageRatio >= 0.15 then\n predictedInjurySeverity = 3 -- 중증 (HP 15~30%)\n elseif damageRatio >= 0.07 then\n predictedInjurySeverity = 2 -- 중상 (HP 7~15%)\n elseif damageRatio > 0 then\n predictedInjurySeverity = 1 -- 경상\n end\n end\n\n local painGainBySev = { [1]=5, [2]=15, [3]=30, [4]=50, [5]=70 }\n local predictedPainGain = painGainBySev[predictedInjurySeverity] or 0\n local predictedTotalPain = clamp(nextHeroinePain + predictedPainGain, 0, 150)\n setChatVar(triggerId, \"vg_Pain_Predicted\", tostring(math.floor(predictedTotalPain)))\n setChatVar(triggerId, \"vg_Pain_Severity_Predicted\", tostring(predictedInjurySeverity))\n local selectedActionCodes = {}\n for i = 1, availableCount do\n if selectedCodesByOrder[i] then\n selectedActionCodes[selectedCodesByOrder[i]] = true\n end\n end\n\n local seduceSelected = selectedActionCodes[\"SEDUCE\"] == true\n local mutilateSelected = selectedActionCodes[\"MUTILATE\"] == true\n local stripSelected = selectedActionCodes[\"STRIP\"] == true\n\n local nextEncounterLust = clamp(curEncounterLust + lustGain, 0, 100)\n local nextEncounterFury = clamp(curEncounterFury + autoFuryGain, 0, 100)\n\n if seduceSelected then\n nextEncounterLust = 100\n nextEncounterFury = 0\n elseif mutilateSelected then\n nextEncounterFury = 100\n end\n\n if stripSelected then\n -- 분노 -50, 상대 성욕 +30\n nextEncounterFury = clamp(nextEncounterFury - 50, 0, 100)\n nextEncounterLust = clamp(nextEncounterLust + 30, 0, 100)\n -- 사용 완료 플래그\n setChatVar(triggerId, \"vg_Strip_Used\", \"1\")\n -- 히로인 옷 벗음 플래그\n setChatVar(triggerId, \"vg_Heroine_Stripped\", \"1\")\n end\n\n local encounterClimaxed = false\n if nextEncounterLust >= 100 then\n encounterClimaxed = true\n nextEncounterLust = 0\n nextEncounterFury = clamp(nextEncounterFury - 50, 0, 100)\n setChatVar(triggerId, \"vg_Encounter_Climax\", \"1\")\n else\n setChatVar(triggerId, \"vg_Encounter_Climax\", \"0\")\n end\n\n local encounterEnraged = (nextEncounterFury >= 100)\n\n -- lust/fury 모두 _Next에 저장\n setChatVar(triggerId, \"vg_Encounter_Lust_Next\", tostring(nextEncounterLust))\n setChatVar(triggerId, \"vg_Encounter_Fury_Next\", tostring(nextEncounterFury))\n if seduceSelected or mutilateSelected or stripSelected then\n setChatVar(triggerId, \"vg_Gauge_Override\", \"1\")\n else\n setChatVar(triggerId, \"vg_Gauge_Override\", \"0\")\n end\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 curSleep = math.max(0, tonumber(heroineEffect.sleep) or 0)\n local nextSleep = curSleep\n\n if curSleep >= 1 then\n nextSleep = curSleep - 1\n end\n\n local curImmobilized = math.max(0, tonumber(heroineEffect.immobilized) or 0)\n local nextImmobilized = curImmobilized\n\n if curImmobilized >= 1 then\n nextImmobilized = curImmobilized - 1\n end\n\n local curBrainwashed = math.max(0, tonumber(heroineEffect.brainwashed) or 0)\n local nextBrainwashed = curBrainwashed\n\n if curBrainwashed >= 1 then\n nextBrainwashed = curBrainwashed - 1\n end\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 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 lewdnessValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.lewdness, 0)) or 0\n local lewdnessLevel = getLewdnessLevelByValue(lewdnessValue)\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\n nextHP = roundToTenth(clamp(nextHP - climaxHPDamage, 0, curMaxHP))\n end\n end\n\n local nextClimaxCount = curClimaxCount + mustClimaxCount\n\n -- ==========================================\n -- 클리어 여부\n -- ==========================================\n local isDefeated = (not isIncapacitatedPhase(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) and curPhase ~= constPhase.transaction 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 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 })\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 then\n buildRewardChoiceBlock(triggerId, curPhase)\n end\n\n -- ==========================================\n -- 상태 갱신\n -- ==========================================\n buildCurrentState(triggerId)\n buildPlayDeckSetting(triggerId)\n buildEncounterActionSetting(triggerId)\n\n -- ==========================================\n -- 메시지 구성\n -- ==========================================\n local appendLines = {}\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 appendGuardResultLines(appendLines, encounterResults, playerResults)\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 physically immobilized. 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 physically 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 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 climaxSuffix = \". If the listed actions alone do not feel sufficient to naturally cause climax, have the encounter perform an additional action that provides clear and sufficient sexual stimulation to directly bring the heroine to climax.\"\n .. \" After bringing the heroine to climax, the encounter attempts to push her further with more intense and explicit sexual acts, but within this response, it may imply escalation involving other orifices, while it must not introduce actual penetration involving any new orifice beyond the body parts explicitly specified above.]\"\n else\n climaxSuffix = \". If the listed actions alone do not feel sufficient to naturally cause climax, have the heroine perform an additional action herself that provides clear and sufficient sexual stimulation to directly lead into climax.]\"\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 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\n local lewdnessValue = tonumber(getResolvedDualFlag(triggerId, vgContext.varName, vgContext.lewdness, 0)) or 0\n local lewdnessLevel = getLewdnessLevelByValue(lewdnessValue)\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\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 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 if isCleared then\n table.insert(appendLines, \"▷ [In this exchange, the heroine reaches exhibitionistic fulfillment, or otherwise exits the exposure-related situation. 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 table.insert(appendLines, \"▷ [During this response, the exhibitionistic situation must continue. The heroine must not yet reach full exhibitionistic fulfillment, and the situation must not end or resolve. 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 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 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 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 local encounterEmoji, displayCurEncounterHP, displayNextEncounterHP, displayEncounterMaxHP = getEncounterDisplayInfo(triggerId, curPhase, curEncounterHP, nextEncounterHP, curEncounterMaxHP)\n appendMeterLog(appendLines, encounterEmoji, displayCurEncounterHP, displayNextEncounterHP, displayEncounterMaxHP)\n\n appendBuffLog(appendLines, getEffectTypeEmoji(triggerId, constEffectType.buff, curPhase), curHeroineBuff, nextHeroineBuff)\n appendBuffLog(appendLines, \"💢\", curEncounterBuff, nextEncounterBuff)\n appendPercentBuffLog(appendLines, \"☢️\", curEncounterDefense, nextEncounterDefense)\n\n -- 성욕/분노 게이지 로그 (예정값 기준 표시)\n local logLust = clamp(tonumber(safeGet(triggerId, \"vg_Encounter_Lust_Next\", tostring(curEncounterLust))) or curEncounterLust, 0, 100)\n local logFury = clamp(tonumber(safeGet(triggerId, \"vg_Encounter_Fury_Next\", tostring(nextEncounterFury))) or nextEncounterFury, 0, 100)\n appendMeterLog(appendLines, \"🔥\", curEncounterLust, encounterClimaxed and 100 or logLust, 100)\n if encounterClimaxed then\n table.insert(appendLines, \"[Temporary Log: 💦 적이 절정에 달했습니다! (성욕 게이지 초기화)]\")\n table.insert(appendLines, \"▷ [SYSTEM: The encounter's lust gauge has reached 100 and the encounter reaches climax this turn. The heroine is a crossdresser with male anatomy. Describe the climax explicitly and in detail consistent with both parties' anatomies. This is mandatory and cannot be skipped or implied.]\")\n end\n appendMeterLog(appendLines, \"😡\", curEncounterFury, logFury, 100)\n if encounterEnraged and curEncounterFury < 100 then\n table.insert(appendLines, \"[Temporary Log: 😡 적이 분노 상태가 되었습니다! 극도로 잔혹한 공격만을 가합니다!]\")\n end\n\n if mutilateSelected then\n setChatVar(triggerId, \"vg_Mutilate_This_Turn\", \"1\")\n else\n setChatVar(triggerId, \"vg_Mutilate_This_Turn\", \"0\")\n end\n\n -- 고통 게이지 로그\n local curHeroinePainLog = clamp(tonumber(safeGet(triggerId, \"vg_Heroine_Pain\", \"0\")) or 0, 0, 100)\n local logPain = clamp(tonumber(safeGet(triggerId, \"vg_Heroine_Pain_Next\", \"\")) or nextHeroinePain, 0, 100)\n appendMeterLog(appendLines, \"💀\", curHeroinePainLog, logPain, 100)\n if logPain >= 100 and curHeroinePainLog < 100 then\n table.insert(appendLines, \"[Temporary Log: 💀 히로인이 고통의 한계에 달했습니다! 신음과 함께 실금합니다!]\")\n end\n\n if stripSelected then\n table.insert(appendLines, \"[Temporary Log: 👗 옷 다 벗기! 분노 -50, 상대 성욕 +30]\")\n end\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)\n"
}
]
}
],
"lowLevelAccess": false,
"assets": [],
"namespace": "",
"customModuleToggle": "",
"cjs": ""
}