← 모듈 공장 리스트

🔦 라이트보드 현장 관찰

risum 모듈 — RISU modules[] — 기능 확장

🗒️ 제작자 게시글

자료원문 보기 ↗

조회 1468 · 2026-05-19

제작자 의도 — 본문 전문 (2,205자)
라이트보드 현장 관찰
TRPG에서 마스터에게
“지금 주변에 뭐가 보여?”
,
“적 상태는 어때?”
,
“저 캐릭터 표정 읽을 수 있어?”
하고 묻는 느낌의 라이트보드 추가 모듈입니다.
본문 서사를 진행하지 않고, 현재 장면에서
보이는 단서 / 추정 가능한 상태 / 불확실한 정보 / 추가 확인이 필요한 부분
만 별도 관찰 패널로 정리합니다.
다운로드
라이트보드 현장 관찰 모듈
라이트보드 3.4.0 이상 권장
뭐 하는 모듈임?
대화 중 현재 장면을 기준으로 관찰 보드를 출력합니다.
주변 상황, 적의 자세와 부상, 거리, 무장, 위협도, 인물의 표정과 말투, 감정 단서, 수상한 물건이나 장면 단서를 정리합니다.
확정할 수 없는 정보는
“불확실”
로 따로 분리하고, 필요하면 TRPG식 추가 확인이나 판정 힌트도 제안합니다.
기본 방향은
표면 중심 관찰
입니다. 숨겨진 의도, 비밀 설정, 정확한 HP, 내부 수치, 확정되지 않은 감정은 단정하지 않습니다.
예를 들어
“거짓말하고 있음”
이라고 확정하기보다는,
“시선이 흔들리고 말끝이 짧아져 긴장한 듯 보임”
처럼 현재 장면에서 관찰 가능한 단서를 중심으로 출력합니다.
예시 화면
설정 설명
모드
끄기: 관찰 보드를 만들지 않음
메인: 메인 출력 흐름에 관찰을 붙임
보조: 보조 패널 느낌으로 사용
추천은
보조
입니다.
실행
즉시: 장면상 필요하면 바로 출력
누르면: 버튼을 눌렀을 때만 출력
추천은
누르면
입니다. 일반 대화마다 관찰 패널이 튀어나오는 것을 막을 수 있습니다.
관찰범위
전체: 주변, 적, 인물, 단서, 불확실 항목을 골고루 정리
주변: 지형, 조명, 소리, 냄새, 물건, 이동 경로 중심
적: 자세, 부상, 무기, 거리, 시선, 빈틈 중심
인물: 표정, 자세, 말투, 감정 단서 중심
단서: 수상한 흔적, 모순, 조사 포인트 중심
깊이
짧게: 핵심만 간단히
보통: 기본 추천
자세히: 전술, 수사, 감정 단서까지 조금 더 촘촘히
초점
특정 대상만 보고 싶을 때 씁니다.
예시: 검은 갑옷의 기사 / 책상 위 서류 / 카린의 표정 / 동굴 입구 / 아스나가 숨긴 물건
말투
GM: TRPG 마스터식 중립 묘사
수사: 증거, 모순, 단서 중심
전술: 거리, 엄폐, 위험, 빈틈 중심
간결: 짧고 담백하게
비밀
금지: 숨겨진 정보 노출 금지
암시만: 보이는 단서 기반 암시만 허용
추측: 추측은 가능하지만 확정 금지
추천은
금지
또는
암시만
입니다.
판정
끄기: 판정 힌트를 거의 쓰지 않음
켜기: 추가 확인이 필요하면 감각, 통찰, 수사, 전술 같은 판정 힌트 제안
추천 설정
일반 RP용
모드: 보조 / 실행: 누르면 / 관찰범위: 전체 / 깊이: 보통 / 말투: GM / 비밀: 금지 / 판정: 끄기 / 문맥: 제거
TRPG 세션용
모드: 보조 / 실행: 누르면 / 관찰범위: 전체 / 깊이: 자세히 / 말투: 전술 또는 수사 / 비밀: 암시만 / 판정: 켜기 / 문맥: 포함
감정 / 표정 관찰용
모드: 보조 / 실행: 누르면 / 관찰범위: 인물 / 깊이: 보통 / 말투: GM / 비밀: 금지 / 확신: 표시
주의사항
이 모듈은 장면을 진행하는 모듈이 아닙니다.
사용자의 행동을 대신 결정하지 않습니다.
적의 비밀 능력을 확정 공개하지 않습니다.
숨겨진 의도나 속마음을 사실처럼 말하지 않습니다.
정확한 HP, 스탯, 플래그 같은 메타 정보를 노출하지 않습니다.
현재 대화에 없는 단서를 새로 만들어 확정하지 않습니다.
이런 봇이랑 잘 맞음
TRPG 시뮬봇 / 던전 탐사 / 미스터리 / 수사물 / 전투가 잦은 RP / 캐릭터 감정선이 중요한 RP / 배경 묘사가 긴 세계관 봇 / 유저가 직접 판단하고 행동하는 플레이형 봇
Special Thanks
라이트보드
이 모듈은 라이트보드의 패널 출력, 버튼 호출, 렌더링 흐름 위에서 동작하는 추가 모듈입니다.
CC BY-NC-SA 4.0 국제 라이선스 적용
이 저작물은
크리에이티브 커먼즈 저작자표시-비영리-동일조건변경허락 4.0 국제 라이선스
에 따라 이용할 수 있습니다.
이 저작물은
아카라이브 커뮤니티 - AI 채팅 채널
을 위해 공개되었으며, AI 채팅 채널 외
타 채널 혹은 커뮤니티에 공유, 배포 등
으로 이용할 때에는 반드시
원 저작자의 이름과 출처를 명시
해야 합니다.
상업적 용도의 경우
어떠한 이유로도 이용할 수 없습니다.
리믹스, 변형, 2차적 저작물을 작성하고 그 결과물을 공유할 경우에는
원 저작물과 동일한 조건의 CCL
을 적용하여야 합니다.
만들긴 했는데 처음이라서 문제가 많을 수도...

정체

🎨 그림 자동삽입기술분류 IMG슬롯 image🔦 라이트보드 현장 관찰

설명

TRPG식 표면 관찰 모듈
파싱된 모듈 전체 (55,723자)
{
 "type": "risuModule",
 "name": "🔦 라이트보드 현장 관찰",
 "description": "TRPG식 표면 관찰 모듈",
 "id": "d395a14a-629d-4128-8b90-8d59319e4511",
 "namespace": "lb-observe",
 "customModuleToggle": "=🔎현장 관찰=group\nlb-observe.mode=모   드=select=끄기,메인,보조\n=비어 있거나 기본값이면 끄기로 처리됩니다=caption\nlb-observe.lazy=실   행=select=즉시,누르면\nlb-observe.language=언   어=select=라이트보드,자동,한국어,English,日本語\nlb-observe.tokens=토   큰=select=표시,숨김\n=———————🔍관찰=divider\nlb-observe.scope=관찰범위=select=전체,주변,적,인물,단서\nlb-observe.depth=깊   이=select=짧게,보통,자세히\nlb-observe.focus=초   점=text\n=비우면 현재 장면에서 가장 중요한 관찰 대상을 자율 선택=caption\nlb-observe.tone=말   투=select=GM,수사,전술,간결\n=———————🎲TRPG=divider\nlb-observe.spoiler=비   밀=select=금지,암시만,추측\nlb-observe.confidence=확   신=select=표시,숨김\nlb-observe.roll=판   정=select=끄기,켜기\n=판정은 능력치/기술 이름을 직접 강제하지 않고 추가 확인이 필요한 영역만 제안=caption\n=———————⚙️시스템=divider\nlb-observe.context=문   맥=select=제거,포함\n=제거 시 다음 프롬프트에서 관찰 패널을 제거. 포함 시 최근 관찰을 장기 장면 추적용으로 남김=caption\n==divider\n=라이트보드 현장 관찰 1.3.6=caption\n==groupEnd",
 "backgroundEmbedding": "<style>\n.lb-observe-panel{margin:10px 0;padding:12px 14px;border:1px solid rgba(148,163,184,.45);border-left:4px solid #38bdf8;border-radius:8px;background:linear-gradient(180deg,rgba(15,23,42,.82),rgba(15,23,42,.66));box-shadow:0 8px 24px rgba(0,0,0,.22);color:#e5e7eb;font-family:var(--risu-font-family,Arial,sans-serif);line-height:1.55;overflow-wrap:anywhere}\n.lb-observe-title{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:8px;font-weight:700;color:#bae6fd;letter-spacing:0}\n.lb-observe-title-text{display:flex;align-items:center;gap:8px}\n.lb-observe-title-text::before{content:'🔎'}\n.lb-observe-reroll{appearance:none;border:0;background:transparent;color:#bae6fd;cursor:pointer;padding:2px 4px;border-radius:6px;line-height:1}\n.lb-observe-reroll:hover{background:rgba(56,189,248,.14)}\n.lb-observe-section{padding:8px 0;border-top:1px solid rgba(148,163,184,.22)}\n.lb-observe-section:first-of-type{border-top:0;padding-top:0}\n.lb-observe-section-title{font-size:.9em;font-weight:700;color:#fef3c7;margin-bottom:4px}\n.lb-observe-list{margin:0;padding-left:1.1em}\n.lb-observe-item{margin:2px 0}\n.lb-observe-body{white-space:pre-wrap;margin:0;font:inherit;line-height:1.55;color:inherit;background:transparent;border:0;padding:0;overflow-wrap:anywhere}\n.lb-observe-footer{margin-top:8px;padding-top:7px;border-top:1px solid rgba(148,163,184,.22);font-size:.84em;color:#94a3b8}\n.lb-observe-error{color:#fecaca}\n</style>",
 "lowLevelAccess": false,
 "hideIcon": false,
 "lorebook": [
  {
   "key": "",
   "secondkey": "",
   "insertorder": 100,
   "comment": "manifest.lb",
   "content": "identifier=lb-observe\n\nauthorsNote=true\ncharDesc=true\nloreBooks=true\nmaxLogs=5\nmultilingual=true\npersonaDesc=true\nrerollBehavior=remove-prev",
   "mode": "normal",
   "alwaysActive": false,
   "selective": false
  },
  {
   "key": "",
   "secondkey": "",
   "insertorder": 100,
   "comment": "lb-observe.lb.job",
   "content": "When this module is explicitly enabled and invoked by the LightBoard backend, the observation button, the manual trigger, or lazy=즉시 mode, your job is to produce a compact TRPG-style observation panel for the current scene.\n\nYou are not the main narrator. Do not continue the story, decide the player's action, reveal secret truths, expose exact hidden mechanics, or invent facts beyond the established scene.\n\nIf lazy=누르면 and this is an ordinary assistant turn, do not pre-generate the observation. Only leave the lazy opener requested by the LightBoard flow.\n\nIf toggle_lb-observe.mode is missing, empty, 끄기, or 0, stay inactive and do not create an observation panel.",
   "mode": "normal",
   "alwaysActive": false,
   "selective": false
  },
  {
   "key": "",
   "secondkey": "",
   "insertorder": 100,
   "comment": "lb-observe.lb",
   "content": "You are the LightBoard field observation assistant, namespace lb-observe.\n\nYour role is similar to a fair tabletop GM answering a player who asks what can currently be observed. Work from the visible scene, recent dialogue, established environment, and the target of the user's question.\n\nLightBoard may provide active lorebooks, character description, persona description, and Author's Note because manifest.lb enables those context sources. Treat them as background consistency only. Do not output lorebook-only, character-description-only, persona-only, or Author's-Note-only facts as observed facts unless the current scene, recent dialogue, target main answer, or explicit user focus makes them observable.\n\nSeparate observable facts from cautious inference. If a character's emotion is not explicit, describe surface signals such as expression, gaze, posture, breathing, voice, distance, hesitation, or repeated actions. Do not claim private thoughts as facts.\n\nUnconfirmed items must be anchored to a visible clue, recent statement, or explicit user focus. If no such anchor exists, omit the uncertain section instead of inventing a possible problem.\n\nThe output is an auxiliary panel, not the main narration. It should help the user choose what to inspect, ask, or do next.",
   "mode": "normal",
   "alwaysActive": false,
   "selective": false
  },
  {
   "key": "",
   "secondkey": "",
   "insertorder": 100,
   "comment": "lb-observe.lb.activation",
   "content": "Activation rules:\n\n- Treat a missing, empty, uninitialized, or first-option toggle_lb-observe.mode value as 끄기.\n- This module is enabled only when toggle_lb-observe.mode is explicitly 메인 or 보조.\n- If toggle_lb-observe.mode is 끄기, missing, empty, or 0, do not output, validate, render, preserve, or pre-generate an observation panel.\n- If toggle_lb-observe.lazy is 누르면 during an ordinary main answer, do not generate observation content in advance. The visible result should be only the lazy observation button created by the LightBoard flow, and only while mode is 메인 or 보조.\n- Generate a real <lb-observe> panel only when the module is enabled and lazy=즉시 permits it, when the manual observation trigger is used, or when the user explicitly asks to observe, inspect, look around, read an enemy, read an expression, check clues, or similar.\n- The observation panel must not replace the main response. In 보조 mode, keep it separate and compact.\n- Do not output both <lb-observe> and <lb-observe-context> for new responses.",
   "mode": "normal",
   "alwaysActive": false,
   "selective": false
  },
  {
   "key": "",
   "secondkey": "",
   "insertorder": 100,
   "comment": "lb-observe.lb.format",
   "content": "Use this format only after the activation rules allow a real observation panel. Do not use this format merely because lazy=누르면 created a button placeholder.\n\nWrite field values in the resolved observation language from lb-observe.lb.language. The TOON section keys must remain exactly in English: surroundings, threats, characters, clues, uncertain, checks.\n\nCanonical output uses exactly one <lb-observe> block. Do not use <lb-observe-context> for new output; that tag is only retained for old compatibility cleanup. Do not invent a baseTokens attribute; the module will attach it after output validation.\n\nUse TOON-style structured content:\n\n<lb-observe>\nsurroundings[n|]:\n  - visible environmental details, exits, hazards, terrain, objects\nthreats[n|]:\n  - visible condition, posture, danger signs, weapons, range, openings\ncharacters[n|]:\n  - surface expression, body language, voice, emotional clues\nclues[n|]:\n  - details that may matter but are not fully explained\nuncertain[n|]:\n  - only direct unanswered questions caused by a visible clue, recent statement, or explicit user focus; name the anchor in the bullet\nchecks[n|]:\n  - optional additional confirmation suggestions when roll=켜기\n</lb-observe>\n\nIf a section has nothing useful, omit that section entirely. Do not leave empty keys. Keep the panel compact. Omit uncertain unless there is a concrete anchor in the current chat.\n\nEvidence discipline: every bullet must be grounded in the current scene, recent dialogue, target main answer, or explicit user focus. Active lorebooks, character descriptions, persona descriptions, and Author's Note are background consistency sources only; do not output facts that exist only there as if they were observed.\n\nSecurity note: never place raw HTML, script tags, event handlers, or style blocks inside observation values. The renderer escapes parsed values and hides unparsed fallback output.",
   "mode": "normal",
   "alwaysActive": false,
   "selective": false
  },
  {
   "key": "",
   "secondkey": "",
   "insertorder": 100,
   "comment": "lb-observe.lb.toggles",
   "content": "Respect these toggles when available. Missing values are not activation permission:\n\n- toggle_lb-observe.mode\n  - Missing, empty, 0, or 끄기: disabled. Do not generate, render, validate, or preserve observation content.\n  - 메인 or 보조: enabled. Only these values allow the module to run.\n\n- toggle_lb-observe.language\n  - 라이트보드: follow toggle_lightboard.language.\n  - 자동: follow the dominant language of the latest user input, the target main answer, and recent chat logs.\n  - 한국어, English, 日本語: write observation values in that language.\n  - Never translate TOON keys.\n\n- toggle_lb-observe.tokens\n  - 표시: the renderer may show the module's base prompt token count in the panel footer.\n  - 숨김: do not mention token counts in prose. The renderer will hide the footer.\n\n- toggle_lb-observe.scope\n  - 전체: include surroundings, threats, characters, clues.\n  - 주변: focus on environment, exits, objects, hazards.\n  - 적: focus on hostile or dangerous entities.\n  - 인물: focus on visible expressions, posture, emotional clues.\n  - 단서: focus on oddities, inconsistencies, investigation hooks.\n\n- toggle_lb-observe.depth\n  - 짧게: 1-2 short bullets total for the most important facts.\n  - 보통: 2-3 bullets for important sections.\n  - 자세히: more tactical and emotional nuance, but still avoid long exposition.\n\n- toggle_lb-observe.focus\n  - If non-empty, prioritize that target, person, object, area, or question.\n\n- toggle_lb-observe.tone\n  - GM: neutral tabletop game master.\n  - 수사: clue-focused and careful.\n  - 전술: terrain, danger, range, cover, openings.\n  - 간결: brief and practical.",
   "mode": "normal",
   "alwaysActive": false,
   "selective": false
  },
  {
   "key": "",
   "secondkey": "",
   "insertorder": 100,
   "comment": "lb-observe.lb.spoiler",
   "content": "Hidden information policy:\n\n- toggle_lb-observe.spoiler=금지: never reveal hidden motives, secret identities, exact hidden mechanics, concealed stats, or unobserved facts. Do not mention a hidden possibility merely to label it uncertain.\n- toggle_lb-observe.spoiler=암시만: you may mention that something is suspicious or not fully confirmed only when the current scene contains a visible clue, contradiction, or explicit user focus that creates that question. Name that anchor.\n- toggle_lb-observe.spoiler=추측: you may offer cautious inferences, but mark them clearly as uncertainty and ground every inference in visible clues from the current chat.\n\nAlways separate visible clues from inference. Never write as if you know the character's private thoughts unless the scene already established them openly. If uncertainty would require inventing an off-screen fact, omit it.\n\nLorebook-only or character-description-only information is not a visible clue by itself. Use it only to keep wording consistent; do not expose it as observation, suspicion, or uncertainty unless the current chat surfaces an observable anchor.",
   "mode": "normal",
   "alwaysActive": false,
   "selective": false
  },
  {
   "key": "",
   "secondkey": "",
   "insertorder": 100,
   "comment": "lb-observe.lb.trpg",
   "content": "TRPG behavior:\n\nThink like a fair game master. Give enough information for the player to choose an action, but do not solve the scene for them.\n\nWhen toggle_lb-observe.roll is 켜기, add a short checks[n|] section when closer inspection, conversation, tracking, perception, insight, medicine, arcana, tactics, or a similar check would help.\n\nDo not force a specific ruleset. Do not invent exact DCs unless already established.\n\nExamples:\n- \"가까이 다가가면 얼룩이 물인지 혈흔인지 구분할 수 있다.\"\n- \"말투보다 손의 떨림 쪽이 더 많은 정보를 줄 수 있다.\"\n- \"엄폐물과 출입구를 먼저 확인하면 전투 시작 시 유리하다.\"",
   "mode": "normal",
   "alwaysActive": false,
   "selective": false
  },
  {
   "key": "",
   "secondkey": "",
   "insertorder": 100,
   "comment": "lb-observe.lb.language",
   "content": "Language policy:\n\nThis module is language-neutral. The observation values and rendered section labels should follow the selected language policy, while TOON internal keys remain English.\n\nPriority:\n1. toggle_lb-observe.language=한국어, English, or 日本語 fixes the observation language to that option.\n2. toggle_lb-observe.language=라이트보드 follows toggle_lightboard.language. If LightBoard is set to automatic/dominant language, follow the dominant language of the current exchange.\n3. toggle_lb-observe.language=자동 follows the dominant language of the latest user input, the target main answer, and recent chat logs.\n4. If the language cannot be inferred, use Korean as the local fallback.\n\nDo not mix languages inside one panel unless the scene itself includes quoted dialogue or proper nouns in another language.",
   "mode": "normal",
   "alwaysActive": false,
   "selective": false
  },
  {
   "key": "",
   "secondkey": "",
   "insertorder": 100,
   "comment": "lb-observe.lb.thoughts",
   "content": "Internal checklist before output:\n\n1. Confirm toggle_lb-observe.mode is explicitly 메인 or 보조. If it is missing, empty, 0, or 끄기, stop without output.\n2. Identify whether a real observation panel is allowed now.\n3. Determine the observation language by lb-observe.lb.language.\n4. Identify the current scene, the user's requested target, and the module focus toggle.\n5. Separate observable evidence (current scene, recent dialogue, target main answer, explicit user focus) from background context (active lorebooks, character description, persona description, Author's Note). Background-only facts can guide consistency but cannot be output as observed facts.\n6. List only observable environmental, tactical, social, and clue-like signals.\n7. For uncertain, keep only unanswered questions directly anchored to a visible clue, recent statement, or explicit focus. Each uncertain bullet should answer: what visible thing makes this unconfirmed?\n8. Move verification methods to checks, not speculative facts to uncertain.\n9. Remove empty sections.\n10. Confirm the final output is a single <lb-observe> TOON block with allowed keys only.",
   "mode": "normal",
   "alwaysActive": false,
   "selective": false
  },
  {
   "key": "",
   "secondkey": "",
   "insertorder": 100,
   "comment": "lb-observe.lb.interaction",
   "content": "Follow-up interaction:\n\nIf the user asks \"that enemy only\", \"expression only\", \"focus on clues\", \"look under the desk\", \"read the room\", or similar after a scene, reuse the established scene context and generate only a refreshed observation panel.\n\nDo not advance time, choose the user's action, make NPCs act, or resolve a roll unless the main narrator already did so. Treat the panel as a camera/GM note attached to the current moment.",
   "mode": "normal",
   "alwaysActive": false,
   "selective": false
  },
  {
   "key": "",
   "secondkey": "",
   "insertorder": 100,
   "comment": "lb-observe.lb.onOutput",
   "content": "local BASE_PROMPT_BOOKS = {\n  'lb-observe.lb.job',\n  'lb-observe.lb',\n  'lb-observe.lb.activation',\n  'lb-observe.lb.format',\n  'lb-observe.lb.toggles',\n  'lb-observe.lb.spoiler',\n  'lb-observe.lb.trpg',\n  'lb-observe.lb.language',\n  'lb-observe.lb.thoughts',\n  'lb-observe.lb.interaction',\n  '작법 지침',\n}\n\nlocal function getToggle(triggerId, name)\n  if not triggerId or triggerId == '' or type(getGlobalVar) ~= 'function' then\n    return ''\n  end\n\n  local ok, value = pcall(getGlobalVar, triggerId, name)\n  if ok and value ~= nil then\n    return tostring(value)\n  end\n  return ''\nend\n\nlocal function isModuleEnabled(triggerId)\n  local mode = getToggle(triggerId, 'toggle_lb-observe.mode')\n  return mode == '1' or mode == '2' or mode == '메인' or mode == '보조'\nend\n\nlocal function escapeAttribute(value)\n  local text = tostring(value or '')\n  text = string.gsub(text, '&', '&amp;')\n  text = string.gsub(text, '<', '&lt;')\n  text = string.gsub(text, '>', '&gt;')\n  text = string.gsub(text, '\"', '&quot;')\n  return text\nend\n\nlocal function getBasePromptText(triggerId)\n  if type(getLoreBooks) ~= 'function' then\n    return ''\n  end\n\n  local parts = {}\n  for _, name in ipairs(BASE_PROMPT_BOOKS) do\n    local ok, books = pcall(getLoreBooks, triggerId, name)\n    if ok and type(books) == 'table' and books[1] and books[1].content and books[1].content ~= '' then\n      parts[#parts + 1] = '# ' .. name .. '\\n' .. books[1].content\n    end\n  end\n\n  return table.concat(parts, '\\n\\n')\nend\n\nlocal function getBaseTokens(triggerId)\n  if type(getTokens) ~= 'function' then\n    return nil\n  end\n\n  local basePrompt = getBasePromptText(triggerId)\n  if not basePrompt or basePrompt == '' then\n    return nil\n  end\n\n  local ok, tokens = pcall(function()\n    return getTokens(triggerId, basePrompt):await()\n  end)\n\n  if ok and tokens ~= nil then\n    return tostring(tokens)\n  end\n  return nil\nend\n\nlocal function attachBaseTokens(triggerId, block)\n  local tokens = getBaseTokens(triggerId)\n  if not tokens or tokens == '' then\n    return block\n  end\n\n  block = string.gsub(block, '(<lb%-observe[^>]*)%s+baseTokens%s*=%s*\"[^\"]*\"', '%1', 1)\n  block = string.gsub(block, \"(<lb%-observe[^>]*)%s+baseTokens%s*=%s*'[^']*'\", '%1', 1)\n  return (string.gsub(block, '<lb%-observe([^>]*)>', '<lb-observe%1 baseTokens=\"' .. escapeAttribute(tokens) .. '\">', 1))\nend\n\nlocal function main(triggerId, output)\n  if not isModuleEnabled(triggerId) then\n    return ''\n  end\n\n  if not string.find(output, '<lb%-observe') then\n    return '<lb-lazy id=\"lb-observe\">오류: 관찰 블록을 찾지 못했습니다. 버튼을 다시 눌러 주세요.</lb-lazy>'\n  end\n\n  output = string.gsub(output, '<lb%-observe%-context([^>]*)>', '<lb-observe%1>')\n  output = string.gsub(output, '</lb%-observe%-context>', '</lb-observe>')\n\n  if string.find(output, '<lb%-observe') and not string.find(output, '</lb%-observe>') then\n    output = output .. '\\n</lb-observe>'\n  end\n\n  local success, nodes = pcall(prelude.queryNodes, 'lb-observe', output)\n  if not success or not nodes or #nodes == 0 then\n    return '<lb-lazy id=\"lb-observe\">오류: 관찰 블록 파싱에 실패했습니다. 버튼을 다시 눌러 주세요.</lb-lazy>'\n  end\n\n  local node = nodes[#nodes]\n  local block = output:sub(node.rangeStart, node.rangeEnd)\n  return attachBaseTokens(triggerId, block)\nend\n\nreturn main\n",
   "mode": "normal",
   "alwaysActive": false,
   "selective": false
  },
  {
   "key": "",
   "secondkey": "",
   "insertorder": 100,
   "comment": "lb-observe.lb.onValidate",
   "content": "local function getToggle(triggerId, name)\n  if not triggerId or triggerId == '' or type(getGlobalVar) ~= 'function' then\n    return ''\n  end\n\n  local ok, value = pcall(getGlobalVar, triggerId, name)\n  if ok and value ~= nil then\n    return tostring(value)\n  end\n  return ''\nend\n\nlocal function isModuleEnabled(triggerId)\n  local mode = getToggle(triggerId, 'toggle_lb-observe.mode')\n  return mode == '1' or mode == '2' or mode == '메인' or mode == '보조'\nend\n\nlocal allowed = {\n  surroundings = true,\n  threats = true,\n  characters = true,\n  clues = true,\n  uncertain = true,\n  checks = true,\n}\n\nlocal bannedPatterns = {\n  'HP%s*[:=]',\n  '체력%s*[:=]',\n  '정확한%s*수치',\n  '내부%s*수치',\n  '비밀%s*플래그',\n  '숨겨진%s*의도는',\n  '속마음은',\n  '확정적으로',\n  '반드시%s+배신',\n}\n\nlocal function hasContent(value)\n  if type(value) == 'string' then\n    return string.find(value, '%S') ~= nil\n  end\n  if type(value) == 'table' then\n    for _, item in pairs(value) do\n      if hasContent(item) then\n        return true\n      end\n    end\n  end\n  return false\nend\n\nlocal function scanText(value)\n  if type(value) == 'string' then\n    for _, pattern in ipairs(bannedPatterns) do\n      if string.find(value, pattern) then\n        error('InvalidOutput: Hidden or exact meta information detected: ' .. pattern)\n      end\n    end\n  elseif type(value) == 'table' then\n    for _, item in pairs(value) do\n      scanText(item)\n    end\n  end\nend\n\nlocal function trimText(value)\n  local text = tostring(value or '')\n  text = string.gsub(text, '^%s+', '')\n  text = string.gsub(text, '%s+$', '')\n  return text\nend\n\nlocal function hasAllowedContent(parsed)\n  if type(parsed) ~= 'table' then\n    return false\n  end\n\n  for key, value in pairs(parsed) do\n    if allowed[key] and hasContent(value) then\n      return true\n    end\n  end\n  return false\nend\n\nlocal function parseObservationText(raw)\n  local parsed = {}\n  local current = nil\n  local text = tostring(raw or '')\n  text = string.gsub(text, '\\r\\n', '\\n')\n  text = string.gsub(text, '\\r', '\\n')\n\n  for line in string.gmatch(text .. '\\n', '(.-)\\n') do\n    local bracketKey = string.match(line, '^%s*([%w_%-]+)%s*%[[^%]]*%]%s*:%s*$')\n    local plainKey = string.match(line, '^%s*([%w_%-]+)%s*:%s*$')\n    local key = bracketKey or plainKey\n\n    if key then\n      if allowed[key] then\n        current = key\n        parsed[current] = parsed[current] or {}\n      else\n        current = nil\n      end\n    elseif current and string.find(line, '%S') then\n      local item = string.match(line, '^%s*[%-%*]%s*(.+)%s*$') or line\n      item = trimText(item)\n      if item ~= '' then\n        table.insert(parsed[current], item)\n      end\n    end\n  end\n\n  return parsed\nend\n\nlocal function decodeObservation(raw)\n  local success = false\n  local parsed = nil\n  local decoder = nil\n  if type(prelude) == 'table' and type(prelude.toon) == 'table' then\n    decoder = prelude.toon.decode\n  end\n\n  if type(decoder) == 'function' then\n    success, parsed = pcall(decoder, raw)\n    if success and hasAllowedContent(parsed) then\n      return parsed\n    end\n  end\n\n  local manual = parseObservationText(raw)\n  if hasAllowedContent(manual) then\n    return manual\n  end\n\n  if success and type(parsed) == 'table' then\n    return parsed\n  end\n  return nil, parsed\nend\n\nlocal function main(triggerId, output)\n  if not isModuleEnabled(triggerId) then\n    return\n  end\n\n  local nodes = prelude.queryNodes('lb-observe', output)\n  if #nodes == 0 then\n    return\n  end\n\n  local parsed, parseError = decodeObservation(nodes[#nodes].content)\n  if type(parsed) ~= 'table' then\n    error('InvalidOutput: Invalid observation format. ' .. tostring(parseError))\n  end\n\n  local hasAny = false\n  for key, value in pairs(parsed) do\n    if not allowed[key] then\n      error('InvalidOutput: Unknown observation section: ' .. tostring(key))\n    end\n    if hasContent(value) then\n      hasAny = true\n    end\n    scanText(value)\n  end\n\n  if not hasAny then\n    error('InvalidOutput: Empty observation panel.')\n  end\nend\n\nreturn main\n",
   "mode": "normal",
   "alwaysActive": false,
   "selective": false
  },
  {
   "key": "",
   "secondkey": "",
   "insertorder": 100,
   "comment": "lb-observe.lb.onRender",
   "content": "local BASE_PROMPT_BOOKS = {\n  'lb-observe.lb.job',\n  'lb-observe.lb',\n  'lb-observe.lb.activation',\n  'lb-observe.lb.format',\n  'lb-observe.lb.toggles',\n  'lb-observe.lb.spoiler',\n  'lb-observe.lb.trpg',\n  'lb-observe.lb.language',\n  'lb-observe.lb.thoughts',\n  'lb-observe.lb.interaction',\n  '작법 지침',\n}\n\nlocal LABELS = {\n  ko = {\n    title = '현장 관찰',\n    token = '기본 프롬프트',\n    empty = '오류: 빈 관찰 패널입니다.',\n    noItems = '오류: 표시할 관찰 항목이 없습니다.',\n    surroundings = '주변',\n    threats = '위협/적',\n    characters = '인물',\n    clues = '단서',\n    uncertain = '미확인',\n    checks = '추가 확인',\n  },\n  en = {\n    title = 'Field Observation',\n    token = 'Base prompt',\n    empty = 'Error: empty observation panel.',\n    noItems = 'Error: no observation items to display.',\n    surroundings = 'Surroundings',\n    threats = 'Threats',\n    characters = 'Characters',\n    clues = 'Clues',\n    uncertain = 'Unconfirmed',\n    checks = 'Checks',\n  },\n  ja = {\n    title = '現場観察',\n    token = '基本プロンプト',\n    empty = 'エラー: 空の観察パネルです。',\n    noItems = 'エラー: 表示する観察項目がありません。',\n    surroundings = '周辺',\n    threats = '脅威/敵',\n    characters = '人物',\n    clues = '手がかり',\n    uncertain = '未確認',\n    checks = '追加確認',\n  },\n}\n\nlocal order = { 'surroundings', 'threats', 'characters', 'clues', 'uncertain', 'checks' }\nlocal observeKeys = {\n  surroundings = true,\n  threats = true,\n  characters = true,\n  clues = true,\n  uncertain = true,\n  checks = true,\n}\n\nlocal function escapeHtml(value)\n  local text = tostring(value or '')\n  text = string.gsub(text, '&', '&amp;')\n  text = string.gsub(text, '<', '&lt;')\n  text = string.gsub(text, '>', '&gt;')\n  text = string.gsub(text, '\"', '&quot;')\n  return text\nend\n\nlocal function getToggle(triggerId, name)\n  if not triggerId or triggerId == '' or type(getGlobalVar) ~= 'function' then\n    return ''\n  end\n\n  local ok, value = pcall(getGlobalVar, triggerId, name)\n  if ok and value ~= nil then\n    return tostring(value)\n  end\n  return ''\nend\n\nlocal function isModuleEnabled(triggerId)\n  local mode = getToggle(triggerId, 'toggle_lb-observe.mode')\n  return mode == '1' or mode == '2' or mode == '메인' or mode == '보조'\nend\n\nlocal function countScripts(text)\n  local counts = { ko = 0, ja = 0, en = 0 }\n  text = tostring(text or '')\n\n  if type(utf8) == 'table' and type(utf8.codes) == 'function' then\n    local ok = pcall(function()\n      for _, code in utf8.codes(text) do\n        if code >= 0xAC00 and code <= 0xD7A3 then\n          counts.ko = counts.ko + 1\n        elseif (code >= 0x3040 and code <= 0x30FF) or (code >= 0x31F0 and code <= 0x31FF) then\n          counts.ja = counts.ja + 1\n        elseif (code >= 65 and code <= 90) or (code >= 97 and code <= 122) then\n          counts.en = counts.en + 1\n        end\n      end\n    end)\n    if ok then\n      return counts\n    end\n  end\n\n  local _, en = string.gsub(text, '[A-Za-z]', '')\n  counts.en = en\n  return counts\nend\n\nlocal function detectLanguage(text)\n  local counts = countScripts(text)\n  if counts.ja > counts.ko and counts.ja >= counts.en then\n    return 'ja'\n  end\n  if counts.ko > 0 and counts.ko >= counts.en then\n    return 'ko'\n  end\n  if counts.en > 0 then\n    return 'en'\n  end\n  return 'ko'\nend\n\nlocal function getRecentChatText(triggerId, limit)\n  if not triggerId or triggerId == '' or type(getFullChat) ~= 'function' then\n    return ''\n  end\n\n  local ok, chats = pcall(getFullChat, triggerId)\n  if not ok or type(chats) ~= 'table' then\n    return ''\n  end\n\n  local parts = {}\n  local startIndex = math.max(1, #chats - (limit or 5) + 1)\n  for index = startIndex, #chats do\n    local chat = chats[index]\n    if type(chat) == 'table' then\n      local text = chat.data or chat.content or chat.message or ''\n      if text ~= '' then\n        parts[#parts + 1] = tostring(text)\n      end\n    elseif chat ~= nil then\n      parts[#parts + 1] = tostring(chat)\n    end\n  end\n\n  return table.concat(parts, '\\n')\nend\n\nlocal function resolveLanguage(triggerId, node)\n  local moduleLanguage = getToggle(triggerId, 'toggle_lb-observe.language')\n  if moduleLanguage == '2' then\n    return 'ko'\n  elseif moduleLanguage == '3' then\n    return 'en'\n  elseif moduleLanguage == '4' then\n    return 'ja'\n  elseif moduleLanguage == '1' then\n    return detectLanguage((node and node.content or '') .. '\\n' .. getRecentChatText(triggerId, 5))\n  end\n\n  local lightboardLanguage = getToggle(triggerId, 'toggle_lightboard.language')\n  if lightboardLanguage == '0' then\n    return 'ko'\n  elseif lightboardLanguage == '1' then\n    return 'en'\n  elseif lightboardLanguage == '2' then\n    return 'ja'\n  elseif lightboardLanguage == '3' then\n    return detectLanguage((node and node.content or '') .. '\\n' .. getRecentChatText(triggerId, 5))\n  end\n\n  return detectLanguage((node and node.content or '') .. '\\n' .. getRecentChatText(triggerId, 5))\nend\n\nlocal function getNodeAttribute(node, name)\n  if type(node) ~= 'table' then\n    return ''\n  end\n\n  local attrKeys = { 'attributes', 'attrs', 'attribute' }\n  for _, key in ipairs(attrKeys) do\n    local attrs = node[key]\n    if type(attrs) == 'table' and attrs[name] ~= nil then\n      return tostring(attrs[name])\n    end\n  end\n\n  local raw = node.raw or node.rawText or node.fullText or node.source or node.text or ''\n  if raw ~= '' then\n    local doubleQuoted = string.match(raw, name .. '%s*=%s*\"([^\"]*)\"')\n    if doubleQuoted then\n      return doubleQuoted\n    end\n    local singleQuoted = string.match(raw, name .. \"%s*=%s*'([^']*)'\")\n    if singleQuoted then\n      return singleQuoted\n    end\n  end\n\n  return ''\nend\n\nlocal function getBasePromptText(triggerId)\n  if type(getLoreBooks) ~= 'function' then\n    return ''\n  end\n\n  local parts = {}\n  for _, name in ipairs(BASE_PROMPT_BOOKS) do\n    local ok, books = pcall(getLoreBooks, triggerId, name)\n    if ok and type(books) == 'table' and books[1] and books[1].content and books[1].content ~= '' then\n      parts[#parts + 1] = '# ' .. name .. '\\n' .. books[1].content\n    end\n  end\n\n  return table.concat(parts, '\\n\\n')\nend\n\nlocal function getBaseTokens(triggerId)\n  if not triggerId or triggerId == '' or type(getTokens) ~= 'function' then\n    return nil\n  end\n\n  local basePrompt = getBasePromptText(triggerId)\n  if not basePrompt or basePrompt == '' then\n    return nil\n  end\n\n  local ok, tokens = pcall(function()\n    return getTokens(triggerId, basePrompt):await()\n  end)\n\n  if ok and tokens ~= nil then\n    return tostring(tokens)\n  end\n  return nil\nend\n\nlocal function shouldShowTokens(triggerId)\n  local value = getToggle(triggerId, 'toggle_lb-observe.tokens')\n  return value ~= '1'\nend\n\nlocal function tokenFooter(triggerId, labels, node)\n  if not shouldShowTokens(triggerId) then\n    return ''\n  end\n\n  local tokens = getNodeAttribute(node, 'baseTokens')\n  if (not tokens or tokens == '') and triggerId and triggerId ~= '' then\n    tokens = getBaseTokens(triggerId)\n  end\n\n  if not tokens or tokens == '' then\n    return ''\n  end\n\n  return '<div class=\"lb-observe-footer\">' .. escapeHtml(labels.token) .. ': ' .. escapeHtml(tokens) .. ' tokens</div>'\nend\n\nlocal function normalizeItems(value)\n  local items = {}\n  if type(value) == 'table' then\n    for _, item in ipairs(value) do\n      if type(item) == 'table' then\n        local parts = {}\n        for k, v in pairs(item) do\n          if v ~= nil and tostring(v):find('%S') then\n            parts[#parts + 1] = tostring(k) .. ': ' .. tostring(v)\n          end\n        end\n        if #parts > 0 then\n          items[#items + 1] = table.concat(parts, ' / ')\n        end\n      elseif item ~= nil and tostring(item):find('%S') then\n        items[#items + 1] = tostring(item)\n      end\n    end\n  elseif type(value) == 'string' then\n    for line in string.gmatch(value, '[^\\r\\n]+') do\n      line = string.gsub(line, '^%s*%-?%s*', '')\n      if line:find('%S') then\n        items[#items + 1] = line\n      end\n    end\n  elseif value ~= nil and tostring(value):find('%S') then\n    items[#items + 1] = tostring(value)\n  end\n  return items\nend\n\nlocal function trimText(value)\n  local text = tostring(value or '')\n  text = string.gsub(text, '^%s+', '')\n  text = string.gsub(text, '%s+$', '')\n  return text\nend\n\nlocal function hasRenderableItems(parsed)\n  if type(parsed) ~= 'table' then\n    return false\n  end\n\n  for _, key in ipairs(order) do\n    if #normalizeItems(parsed[key]) > 0 then\n      return true\n    end\n  end\n  return false\nend\n\nlocal function parseObservationText(raw)\n  local parsed = {}\n  local current = nil\n  local text = tostring(raw or '')\n  text = string.gsub(text, '\\r\\n', '\\n')\n  text = string.gsub(text, '\\r', '\\n')\n\n  for line in string.gmatch(text .. '\\n', '(.-)\\n') do\n    local bracketKey = string.match(line, '^%s*([%w_%-]+)%s*%[[^%]]*%]%s*:%s*$')\n    local plainKey = string.match(line, '^%s*([%w_%-]+)%s*:%s*$')\n    local key = bracketKey or plainKey\n\n    if key then\n      if observeKeys[key] then\n        current = key\n        parsed[current] = parsed[current] or {}\n      else\n        current = nil\n      end\n    elseif current and string.find(line, '%S') then\n      local item = string.match(line, '^%s*[%-%*]%s*(.+)%s*$') or line\n      item = trimText(item)\n      if item ~= '' then\n        table.insert(parsed[current], item)\n      end\n    end\n  end\n\n  return parsed\nend\n\nlocal function decodeObservation(raw)\n  local success = false\n  local parsed = nil\n  local decoder = nil\n  if type(prelude) == 'table' and type(prelude.toon) == 'table' then\n    decoder = prelude.toon.decode\n  end\n\n  if type(decoder) == 'function' then\n    success, parsed = pcall(decoder, raw)\n    if success and hasRenderableItems(parsed) then\n      return parsed\n    end\n  end\n\n  local manual = parseObservationText(raw)\n  if hasRenderableItems(manual) then\n    return manual\n  end\n\n  if success and type(parsed) == 'table' then\n    return parsed\n  end\n  return nil\nend\n\nlocal function render(triggerOrNode, maybeNode)\n  local triggerId = ''\n  local node = triggerOrNode\n  if type(triggerOrNode) == 'string' then\n    triggerId = triggerOrNode\n    node = maybeNode\n  elseif type(triggerOrNode) == 'table' then\n    triggerId = triggerOrNode.triggerId or triggerOrNode.tid or ''\n    node = triggerOrNode\n  end\n\n  if not isModuleEnabled(triggerId) then\n    return ''\n  end\n\n  local lang = resolveLanguage(triggerId, node)\n  local labels = LABELS[lang] or LABELS.ko\n  local rawContent = node and node.content or ''\n  if not rawContent or rawContent == '' then\n    return '<lb-lazy id=\"lb-observe\">' .. escapeHtml(labels.empty) .. '</lb-lazy>'\n  end\n\n  local parsed = decodeObservation(rawContent)\n  if type(parsed) ~= 'table' then\n    return '<div class=\"lb-observe-panel\" data-id=\"lb-observe\"><div class=\"lb-observe-title\"><span class=\"lb-observe-title-text\">' .. escapeHtml(labels.title) .. '</span><button class=\"lb-observe-reroll\" risu-btn=\"lb-reroll__lb-observe\" type=\"button\"><lb-reroll-icon /></button></div><pre class=\"lb-observe-body\">' .. escapeHtml(rawContent) .. '</pre>' .. tokenFooter(triggerId, labels, node) .. '</div>'\n  end\n\n  local sections = {}\n  for _, key in ipairs(order) do\n    local items = normalizeItems(parsed[key])\n    if #items > 0 then\n      local listItems = {}\n      for _, item in ipairs(items) do\n        listItems[#listItems + 1] = '<li class=\"lb-observe-item\">' .. escapeHtml(item) .. '</li>'\n      end\n      sections[#sections + 1] = '<div class=\"lb-observe-section\"><div class=\"lb-observe-section-title\">' .. escapeHtml(labels[key]) .. '</div><ul class=\"lb-observe-list\">' .. table.concat(listItems, '') .. '</ul></div>'\n    end\n  end\n\n  if #sections == 0 then\n    return '<lb-lazy id=\"lb-observe\">' .. escapeHtml(labels.noItems) .. '</lb-lazy>'\n  end\n\n  return '<div class=\"lb-observe-panel\" data-id=\"lb-observe\"><div class=\"lb-observe-title\"><span class=\"lb-observe-title-text\">' .. escapeHtml(labels.title) .. '</span><button class=\"lb-observe-reroll\" risu-btn=\"lb-reroll__lb-observe\" type=\"button\"><lb-reroll-icon /></button></div>' .. table.concat(sections, '') .. tokenFooter(triggerId, labels, node) .. '</div>'\nend\n\nreturn render\n",
   "mode": "normal",
   "alwaysActive": false,
   "selective": false
  },
  {
   "key": "",
   "secondkey": "",
   "insertorder": 100,
   "comment": "작법 지침",
   "content": "Writing style guidance:\n\n- Write like a GM's observation note, not a long literary narration.\n- Keep sentences compact and actionable.\n- Use cautious wording for inference: Korean \"~로 보인다\", \"~일 수 있다\"; English \"seems\", \"appears\", \"may\"; Japanese \"ように見える\", \"可能性がある\".\n- Do not mix confirmed facts with guesses.\n- Emotional readings must come through surface cues such as face, gaze, breathing, voice, hands, posture, distance, or hesitation.\n- In combat, prioritize terrain, cover, posture, weapons, exits, injury signs, gaze, and immediate danger cues.\n- In investigation, prioritize displaced objects, recent traces, smell, sound, light, moisture, dust, locks, and entry marks.",
   "mode": "normal",
   "alwaysActive": false,
   "selective": false
  }
 ],
 "regex": [
  {
   "comment": "Filter Response",
   "in": "<lb-observe(?:-context)?(?:\\s[^>]*)?>[\\s\\S]*?<\\/lb-observe(?:-context)?>\\n?",
   "out": "",
   "type": "editoutput",
   "ableFlag": true,
   "flag": "gs"
  },
  {
   "comment": "Ignore Old",
   "in": "<lb-observe(?:-context)?(?:\\s[^>]*)?>[\\s\\S]*?<\\/lb-observe(?:-context)?>\\n?",
   "out": "{{#if {{? {{getglobalvar::toggle_lb-observe.mode}}=1}}}}\n{{#if {{? {{getglobalvar::toggle_lb-observe.context}}=1}}}}\n{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-5}}}}}}\n$&\n{{/if}}\n{{/if}}\n{{/if}}\n{{#if {{? {{getglobalvar::toggle_lb-observe.mode}}=2}}}}\n{{#if {{? {{getglobalvar::toggle_lb-observe.context}}=1}}}}\n{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}}-5}}}}}}\n$&\n{{/if}}\n{{/if}}\n{{/if}}",
   "type": "editprocess",
   "ableFlag": true,
   "flag": "gs"
  },
  {
   "comment": "Lazy",
   "in": "(?:<lb-lazy\\s+(?:id|identifier)=\"lb-observe\"\\s*\\/>)|(?:<lb-lazy\\s+(?:id|identifier)=\"lb-observe\"\\s*>([\\s\\S]*?)<\\/lb-lazy>)\\n?",
   "out": "{{#if {{greater_equal::{{chat_index}}::{{? {{lastmessageid}} - 1}}}}}}\n{{#if {{? {{getglobalvar::toggle_lb-observe.mode}}=1}}}}\n<div class=\"lb-module-opener-root\" data-id=\"lb-observe\">\n<button class=\"lb-module-opener\" data-lazy=\"true\" risu-btn=\"lb-reroll__lb-observe\">\n관찰\n<lb-reroll-icon />\n</button>\n</div>\n{{#if {{? {{length::$1}} > 0 }} }}\n<span class=\"lb-lazyloader-message\">현장 관찰 렌더링 오류</span>\n{{/if}}\n{{/if}}\n{{#if {{? {{getglobalvar::toggle_lb-observe.mode}}=2}}}}\n<div class=\"lb-module-opener-root\" data-id=\"lb-observe\">\n<button class=\"lb-module-opener\" data-lazy=\"true\" risu-btn=\"lb-reroll__lb-observe\">\n관찰\n<lb-reroll-icon />\n</button>\n</div>\n{{#if {{? {{length::$1}} > 0 }} }}\n<span class=\"lb-lazyloader-message\">현장 관찰 렌더링 오류</span>\n{{/if}}\n{{/if}}\n{{/if}}",
   "type": "editdisplay",
   "ableFlag": true,
   "flag": "gs"
  },
  {
   "comment": "Fallback Render Observation Panel",
   "in": "<lb-observe(?:-context)?(?:\\s[^>]*)?>\\s*([\\s\\S]*?)\\s*<\\/lb-observe(?:-context)?>\\n?",
   "out": "{{#if {{? {{getglobalvar::toggle_lb-observe.mode}}=1}}}}<lb-lazy id=\"lb-observe\">렌더링 실패: 관찰 블록을 숨겼습니다.</lb-lazy>{{/if}}{{#if {{? {{getglobalvar::toggle_lb-observe.mode}}=2}}}}<lb-lazy id=\"lb-observe\">렌더링 실패: 관찰 블록을 숨겼습니다.</lb-lazy>{{/if}}",
   "type": "editdisplay",
   "ableFlag": true,
   "flag": "g"
  },
  {
   "comment": "Hide Unparsed",
   "in": "<lb-observe(?:-context)?(?:\\s[^>]*)?>[\\s\\S]*?<\\/lb-observe(?:-context)?>\\n?",
   "out": "",
   "type": "editdisplay",
   "ableFlag": true,
   "flag": "gs"
  },
  {
   "comment": "Remove Lazy Observation Button From Prompt",
   "in": "<lb-lazy\\s+(?:id|identifier)=\"lb-observe\"(?:\\s*\\/|\\s*>[\\s\\S]*?<\\/lb-lazy>)\\n?",
   "out": "",
   "type": "editprocess",
   "ableFlag": true,
   "flag": "g"
  }
 ],
 "trigger": [
  {
   "comment": "",
   "type": "start",
   "conditions": [],
   "effect": [
    {
     "type": "triggerlua",
     "code": "--! LightBoard Observe\n\nlocal triggerId = ''\n\nlocal function setTriggerId(tid)\n  triggerId = tid\n  if type(prelude) ~= 'nil' then\n    prelude.import(tid, 'toon.decode')\n    return\n  end\n  local source = getLoreBooks(triggerId, 'lightboard-prelude')\n  if not source or #source == 0 then\n    error('Failed to load lightboard-prelude.')\n  end\n  load(source[1].content, '@prelude', 't')()\n  prelude.import(tid, 'toon.decode')\nend\n\nlocal BASE_PROMPT_BOOKS = {\n  'lb-observe.lb.job',\n  'lb-observe.lb',\n  'lb-observe.lb.activation',\n  'lb-observe.lb.format',\n  'lb-observe.lb.toggles',\n  'lb-observe.lb.spoiler',\n  'lb-observe.lb.trpg',\n  'lb-observe.lb.language',\n  'lb-observe.lb.thoughts',\n  'lb-observe.lb.interaction',\n  '작법 지침',\n}\n\nlocal LABELS = {\n  ko = {\n    title = '현장 관찰',\n    token = '기본 프롬프트',\n    empty = '오류: 빈 관찰 패널입니다.',\n    noItems = '오류: 표시할 관찰 항목이 없습니다.',\n    surroundings = '주변',\n    threats = '위협/적',\n    characters = '인물',\n    clues = '단서',\n    uncertain = '미확인',\n    checks = '추가 확인',\n  },\n  en = {\n    title = 'Field Observation',\n    token = 'Base prompt',\n    empty = 'Error: empty observation panel.',\n    noItems = 'Error: no observation items to display.',\n    surroundings = 'Surroundings',\n    threats = 'Threats',\n    characters = 'Characters',\n    clues = 'Clues',\n    uncertain = 'Unconfirmed',\n    checks = 'Checks',\n  },\n  ja = {\n    title = '現場観察',\n    token = '基本プロンプト',\n    empty = 'エラー: 空の観察パネルです。',\n    noItems = 'エラー: 表示する観察項目がありません。',\n    surroundings = '周辺',\n    threats = '脅威/敵',\n    characters = '人物',\n    clues = '手がかり',\n    uncertain = '未確認',\n    checks = '追加確認',\n  },\n}\n\nlocal order = { 'surroundings', 'threats', 'characters', 'clues', 'uncertain', 'checks' }\nlocal observeKeys = {\n  surroundings = true,\n  threats = true,\n  characters = true,\n  clues = true,\n  uncertain = true,\n  checks = true,\n}\n\nlocal function escapeHtml(value)\n  local text = tostring(value or '')\n  text = string.gsub(text, '&', '&amp;')\n  text = string.gsub(text, '<', '&lt;')\n  text = string.gsub(text, '>', '&gt;')\n  text = string.gsub(text, '\"', '&quot;')\n  return text\nend\n\nlocal function getToggle(triggerId, name)\n  if not triggerId or triggerId == '' or type(getGlobalVar) ~= 'function' then\n    return ''\n  end\n\n  local ok, value = pcall(getGlobalVar, triggerId, name)\n  if ok and value ~= nil then\n    return tostring(value)\n  end\n  return ''\nend\n\nlocal function isModuleEnabled(triggerId)\n  local mode = getToggle(triggerId, 'toggle_lb-observe.mode')\n  return mode == '1' or mode == '2' or mode == '메인' or mode == '보조'\nend\n\nlocal function countScripts(text)\n  local counts = { ko = 0, ja = 0, en = 0 }\n  text = tostring(text or '')\n\n  if type(utf8) == 'table' and type(utf8.codes) == 'function' then\n    local ok = pcall(function()\n      for _, code in utf8.codes(text) do\n        if code >= 0xAC00 and code <= 0xD7A3 then\n          counts.ko = counts.ko + 1\n        elseif (code >= 0x3040 and code <= 0x30FF) or (code >= 0x31F0 and code <= 0x31FF) then\n          counts.ja = counts.ja + 1\n        elseif (code >= 65 and code <= 90) or (code >= 97 and code <= 122) then\n          counts.en = counts.en + 1\n        end\n      end\n    end)\n    if ok then\n      return counts\n    end\n  end\n\n  local _, en = string.gsub(text, '[A-Za-z]', '')\n  counts.en = en\n  return counts\nend\n\nlocal function detectLanguage(text)\n  local counts = countScripts(text)\n  if counts.ja > counts.ko and counts.ja >= counts.en then\n    return 'ja'\n  end\n  if counts.ko > 0 and counts.ko >= counts.en then\n    return 'ko'\n  end\n  if counts.en > 0 then\n    return 'en'\n  end\n  return 'ko'\nend\n\nlocal function getRecentChatText(triggerId, limit)\n  if not triggerId or triggerId == '' or type(getFullChat) ~= 'function' then\n    return ''\n  end\n\n  local ok, chats = pcall(getFullChat, triggerId)\n  if not ok or type(chats) ~= 'table' then\n    return ''\n  end\n\n  local parts = {}\n  local startIndex = math.max(1, #chats - (limit or 5) + 1)\n  for index = startIndex, #chats do\n    local chat = chats[index]\n    if type(chat) == 'table' then\n      local text = chat.data or chat.content or chat.message or ''\n      if text ~= '' then\n        parts[#parts + 1] = tostring(text)\n      end\n    elseif chat ~= nil then\n      parts[#parts + 1] = tostring(chat)\n    end\n  end\n\n  return table.concat(parts, '\\n')\nend\n\nlocal function resolveLanguage(triggerId, node)\n  local moduleLanguage = getToggle(triggerId, 'toggle_lb-observe.language')\n  if moduleLanguage == '2' then\n    return 'ko'\n  elseif moduleLanguage == '3' then\n    return 'en'\n  elseif moduleLanguage == '4' then\n    return 'ja'\n  elseif moduleLanguage == '1' then\n    return detectLanguage((node and node.content or '') .. '\\n' .. getRecentChatText(triggerId, 5))\n  end\n\n  local lightboardLanguage = getToggle(triggerId, 'toggle_lightboard.language')\n  if lightboardLanguage == '0' then\n    return 'ko'\n  elseif lightboardLanguage == '1' then\n    return 'en'\n  elseif lightboardLanguage == '2' then\n    return 'ja'\n  elseif lightboardLanguage == '3' then\n    return detectLanguage((node and node.content or '') .. '\\n' .. getRecentChatText(triggerId, 5))\n  end\n\n  return detectLanguage((node and node.content or '') .. '\\n' .. getRecentChatText(triggerId, 5))\nend\n\nlocal function getNodeAttribute(node, name)\n  if type(node) ~= 'table' then\n    return ''\n  end\n\n  local attrKeys = { 'attributes', 'attrs', 'attribute' }\n  for _, key in ipairs(attrKeys) do\n    local attrs = node[key]\n    if type(attrs) == 'table' and attrs[name] ~= nil then\n      return tostring(attrs[name])\n    end\n  end\n\n  local raw = node.raw or node.rawText or node.fullText or node.source or node.text or ''\n  if raw ~= '' then\n    local doubleQuoted = string.match(raw, name .. '%s*=%s*\"([^\"]*)\"')\n    if doubleQuoted then\n      return doubleQuoted\n    end\n    local singleQuoted = string.match(raw, name .. \"%s*=%s*'([^']*)'\")\n    if singleQuoted then\n      return singleQuoted\n    end\n  end\n\n  return ''\nend\n\nlocal function getBasePromptText(triggerId)\n  if type(getLoreBooks) ~= 'function' then\n    return ''\n  end\n\n  local parts = {}\n  for _, name in ipairs(BASE_PROMPT_BOOKS) do\n    local ok, books = pcall(getLoreBooks, triggerId, name)\n    if ok and type(books) == 'table' and books[1] and books[1].content and books[1].content ~= '' then\n      parts[#parts + 1] = '# ' .. name .. '\\n' .. books[1].content\n    end\n  end\n\n  return table.concat(parts, '\\n\\n')\nend\n\nlocal function getBaseTokens(triggerId)\n  if not triggerId or triggerId == '' or type(getTokens) ~= 'function' then\n    return nil\n  end\n\n  local basePrompt = getBasePromptText(triggerId)\n  if not basePrompt or basePrompt == '' then\n    return nil\n  end\n\n  local ok, tokens = pcall(function()\n    return getTokens(triggerId, basePrompt):await()\n  end)\n\n  if ok and tokens ~= nil then\n    return tostring(tokens)\n  end\n  return nil\nend\n\nlocal function shouldShowTokens(triggerId)\n  local value = getToggle(triggerId, 'toggle_lb-observe.tokens')\n  return value ~= '1'\nend\n\nlocal function tokenFooter(triggerId, labels, node)\n  if not shouldShowTokens(triggerId) then\n    return ''\n  end\n\n  local tokens = getNodeAttribute(node, 'baseTokens')\n  if (not tokens or tokens == '') and triggerId and triggerId ~= '' then\n    tokens = getBaseTokens(triggerId)\n  end\n\n  if not tokens or tokens == '' then\n    return ''\n  end\n\n  return '<div class=\"lb-observe-footer\">' .. escapeHtml(labels.token) .. ': ' .. escapeHtml(tokens) .. ' tokens</div>'\nend\n\nlocal function normalizeItems(value)\n  local items = {}\n  if type(value) == 'table' then\n    for _, item in ipairs(value) do\n      if type(item) == 'table' then\n        local parts = {}\n        for k, v in pairs(item) do\n          if v ~= nil and tostring(v):find('%S') then\n            parts[#parts + 1] = tostring(k) .. ': ' .. tostring(v)\n          end\n        end\n        if #parts > 0 then\n          items[#items + 1] = table.concat(parts, ' / ')\n        end\n      elseif item ~= nil and tostring(item):find('%S') then\n        items[#items + 1] = tostring(item)\n      end\n    end\n  elseif type(value) == 'string' then\n    for line in string.gmatch(value, '[^\\r\\n]+') do\n      line = string.gsub(line, '^%s*%-?%s*', '')\n      if line:find('%S') then\n        items[#items + 1] = line\n      end\n    end\n  elseif value ~= nil and tostring(value):find('%S') then\n    items[#items + 1] = tostring(value)\n  end\n  return items\nend\n\nlocal function trimText(value)\n  local text = tostring(value or '')\n  text = string.gsub(text, '^%s+', '')\n  text = string.gsub(text, '%s+$', '')\n  return text\nend\n\nlocal function hasRenderableItems(parsed)\n  if type(parsed) ~= 'table' then\n    return false\n  end\n\n  for _, key in ipairs(order) do\n    if #normalizeItems(parsed[key]) > 0 then\n      return true\n    end\n  end\n  return false\nend\n\nlocal function parseObservationText(raw)\n  local parsed = {}\n  local current = nil\n  local text = tostring(raw or '')\n  text = string.gsub(text, '\\r\\n', '\\n')\n  text = string.gsub(text, '\\r', '\\n')\n\n  for line in string.gmatch(text .. '\\n', '(.-)\\n') do\n    local bracketKey = string.match(line, '^%s*([%w_%-]+)%s*%[[^%]]*%]%s*:%s*$')\n    local plainKey = string.match(line, '^%s*([%w_%-]+)%s*:%s*$')\n    local key = bracketKey or plainKey\n\n    if key then\n      if observeKeys[key] then\n        current = key\n        parsed[current] = parsed[current] or {}\n      else\n        current = nil\n      end\n    elseif current and string.find(line, '%S') then\n      local item = string.match(line, '^%s*[%-%*]%s*(.+)%s*$') or line\n      item = trimText(item)\n      if item ~= '' then\n        table.insert(parsed[current], item)\n      end\n    end\n  end\n\n  return parsed\nend\n\nlocal function decodeObservation(raw)\n  local success = false\n  local parsed = nil\n  local decoder = nil\n  if type(prelude) == 'table' and type(prelude.toon) == 'table' then\n    decoder = prelude.toon.decode\n  end\n\n  if type(decoder) == 'function' then\n    success, parsed = pcall(decoder, raw)\n    if success and hasRenderableItems(parsed) then\n      return parsed\n    end\n  end\n\n  local manual = parseObservationText(raw)\n  if hasRenderableItems(manual) then\n    return manual\n  end\n\n  if success and type(parsed) == 'table' then\n    return parsed\n  end\n  return nil\nend\n\nlocal function render(triggerOrNode, maybeNode)\n  local triggerId = ''\n  local node = triggerOrNode\n  if type(triggerOrNode) == 'string' then\n    triggerId = triggerOrNode\n    node = maybeNode\n  elseif type(triggerOrNode) == 'table' then\n    triggerId = triggerOrNode.triggerId or triggerOrNode.tid or ''\n    node = triggerOrNode\n  end\n\n  if not isModuleEnabled(triggerId) then\n    return ''\n  end\n\n  local lang = resolveLanguage(triggerId, node)\n  local labels = LABELS[lang] or LABELS.ko\n  local rawContent = node and node.content or ''\n  if not rawContent or rawContent == '' then\n    return '<lb-lazy id=\"lb-observe\">' .. escapeHtml(labels.empty) .. '</lb-lazy>'\n  end\n\n  local parsed = decodeObservation(rawContent)\n  if type(parsed) ~= 'table' then\n    return '<div class=\"lb-observe-panel\" data-id=\"lb-observe\"><div class=\"lb-observe-title\"><span class=\"lb-observe-title-text\">' .. escapeHtml(labels.title) .. '</span><button class=\"lb-observe-reroll\" risu-btn=\"lb-reroll__lb-observe\" type=\"button\"><lb-reroll-icon /></button></div><pre class=\"lb-observe-body\">' .. escapeHtml(rawContent) .. '</pre>' .. tokenFooter(triggerId, labels, node) .. '</div>'\n  end\n\n  local sections = {}\n  for _, key in ipairs(order) do\n    local items = normalizeItems(parsed[key])\n    if #items > 0 then\n      local listItems = {}\n      for _, item in ipairs(items) do\n        listItems[#listItems + 1] = '<li class=\"lb-observe-item\">' .. escapeHtml(item) .. '</li>'\n      end\n      sections[#sections + 1] = '<div class=\"lb-observe-section\"><div class=\"lb-observe-section-title\">' .. escapeHtml(labels[key]) .. '</div><ul class=\"lb-observe-list\">' .. table.concat(listItems, '') .. '</ul></div>'\n    end\n  end\n\n  if #sections == 0 then\n    return '<lb-lazy id=\"lb-observe\">' .. escapeHtml(labels.noItems) .. '</lb-lazy>'\n  end\n\n  return '<div class=\"lb-observe-panel\" data-id=\"lb-observe\"><div class=\"lb-observe-title\"><span class=\"lb-observe-title-text\">' .. escapeHtml(labels.title) .. '</span><button class=\"lb-observe-reroll\" risu-btn=\"lb-reroll__lb-observe\" type=\"button\"><lb-reroll-icon /></button></div>' .. table.concat(sections, '') .. tokenFooter(triggerId, labels, node) .. '</div>'\nend\n\n\nlistenEdit(\n  'editDisplay',\n  function(tid, data, meta)\n    if not data or data == '' then\n      return ''\n    end\n\n    if not isModuleEnabled(tid) then\n      return data\n    end\n\n    setTriggerId(tid)\n\n    if meta and meta.index ~= nil and type(getChatLength) == 'function' then\n      local position = meta.index - getChatLength(triggerId)\n      if position < -10 then\n        return data\n      end\n    end\n\n    local nodes = {}\n    local okNormal, normalNodes = pcall(prelude.queryNodes, 'lb-observe', data)\n    if okNormal and normalNodes then\n      for _, node in ipairs(normalNodes) do\n        nodes[#nodes + 1] = node\n      end\n    end\n    local okContext, contextNodes = pcall(prelude.queryNodes, 'lb-observe-context', data)\n    if okContext and contextNodes then\n      for _, node in ipairs(contextNodes) do\n        nodes[#nodes + 1] = node\n      end\n    end\n\n    if #nodes == 0 then\n      return data\n    end\n\n    table.sort(nodes, function(a, b)\n      return (a.rangeStart or 0) > (b.rangeStart or 0)\n    end)\n\n    local renderedData = data\n    for _, node in ipairs(nodes) do\n      node.raw = renderedData:sub(node.rangeStart, node.rangeEnd)\n      local success, rendered = pcall(render, tid, node)\n      if not success then\n        rendered = '<lb-lazy id=\"lb-observe\">오류: ' .. escapeHtml(rendered) .. '</lb-lazy>'\n      end\n      renderedData = renderedData:sub(1, node.rangeStart - 1) .. rendered .. renderedData:sub(node.rangeEnd + 1)\n    end\n\n    return renderedData\n  end\n)\n"
    }
   ],
   "lowLevelAccess": false
  },
  {
   "comment": "🔎 관찰",
   "type": "manual",
   "conditions": [],
   "effect": [
    {
     "type": "v2SystemPrompt",
     "location": "promptend",
     "value": "라이트보드 현장 관찰 수동 요청입니다. toggle_lb-observe.mode가 메인 또는 보조가 아니면 관찰 패널을 출력하지 말고 빈 응답으로 종료하세요. 마지막 장면 또는 toggle_lb-observe.focus에 지정된 대상을 기준으로, 현재 보이는 주변 상황/위협/인물/단서를 표면 중심으로 관찰하세요.\n\n본문 서사를 진행하지 말고, {{user}}의 행동을 대신 결정하지 마세요. 숨겨진 진실, 비밀 의도, 정확한 내부 수치, 확정되지 않은 감정은 단정하지 마세요.\n\n로어북, 캐릭터 설명, 페르소나, Author's Note는 일관성 참고용 배경으로만 취급하세요. 현재 장면, 최근 발언, 대상 메인 답변, 사용자가 지정한 초점에 드러난 단서가 없으면 관찰 사실/의심/미확인으로 출력하지 마세요.\n\n미확인/uncertain 항목은 현재 보이는 단서, 최근 발언, 사용자가 지정한 초점 때문에 직접 생긴 의문만 쓰세요. 근거 단서가 없으면 uncertain 섹션을 생략하고, 장면 밖 가능성을 새로 만들지 마세요.\n\n언어는 toggle_lb-observe.language를 따르세요. 라이트보드이면 현재 라이트보드 언어 설정을 따르고, 자동이면 최신 사용자 입력/대상 답변/최근 로그의 주 언어를 따르세요. 한국어, English, 日本語가 선택되어 있으면 해당 언어로 관찰 값만 작성하세요. TOON 내부 키는 번역하지 마세요.\n\n반드시 관찰 패널만 출력하세요. 새 출력은 <lb-observe> 하나만 사용하고, 내부는 다음 TOON 구조를 따르세요:\n\n<lb-observe>\nsurroundings[n|]:\n  - ...\nthreats[n|]:\n  - ...\ncharacters[n|]:\n  - ...\nclues[n|]:\n  - ...\nuncertain[n|]:\n  - ...\nchecks[n|]:\n  - ...\n</lb-observe>\n\n쓸 내용이 없는 섹션은 통째로 생략하세요. baseTokens 속성은 직접 만들지 마세요.",
     "valueType": "value",
     "indent": 0
    },
    {
     "type": "v2SendAIprompt",
     "indent": 0
    }
   ],
   "lowLevelAccess": false
  }
 ],
 "assets": []
}

🔗 이것과 비슷한 인기 자산

오라토리오 시엘라 에셋 모듈
인당 에셋 118장
👍67 ·
🌋얼터네이트 헌터즈 V2 모드팩 0427 FIX
글노덮 — @@position pt_GlobalNote Non Status Command Character Image Guidelines If the character is included in the Non Sta
👍39 ·
🔮외신 리베아
글노덮 — Additional Command Character Image Guidelines If the character is included in the Additional Command Character Com
👍32 ·
[TSF] 감정 에셋 모듈(1.0.3 Ver) (1)
1.0.3 Ver
👍24 ·
🐞 W2 · /city/asset