RisuMaou
by JamesCane· on RisuAI· added · license CC BY-NC-SA 4.0
Tokens: 200,314 permanent · 22 first greeting · budget 200,336
Creator notes
en
era마왕의 리수버전 스핀오프 알파 테스트 버전으로 마오에 대한 조교와 관련한 기능들만 제한적으로 구현되어있습니다. 캐릭터가 사망하면 되살릴 수 없습니다!!
No description.
First message22 tokens
{{#when::__screen::vis::"gameStart"}} {{getvar::html}} {{/when}}
87 entries
- #1ability.sysconstant
Keys:
-- ability.sys.txt -- 어빌리티 관리 통합 모듈 -- 사용법: sysFunction(triggerId, "ability.sys", "levelUp", abl, condition, alert) -- sysFunction(triggerId, "ability.sys", "selectCondition", abl, conditions)
(function() --------------------------------------- -- 로컬 함수 정의 ---------------------------------------
--------------------------------------- -- 디스패처 테이블 및 로컬 변수 --------------------------------------- local funcs = {} -- 어빌리티 레벨업 처리 function funcs.levelUp(triggerId, abl, selectedCondition, alert) local target = getState(triggerId, "target") local ablLv = tonumber(target[abl]) if alert == nil then alert = true end -- 레벨업 조건 확인 (trace가 target에 포함되어 있음) for k, v in pairs(selectedCondition) do local currentVal = tonumber(target[k]) or 0 if currentVal < v then if alert == true then alertNormal(triggerId, "레벨업 불가: " .. k .. " " .. (v - currentVal) .. " 부족") end setChatVar(triggerId, "selectedAbl", "none") return false end end -- t/f확인용이면 true 반환하고 끝내기 if alert == false then return true end -- 레벨업 처리 alertNormal(triggerId, abl .. "레벨 상승! (" .. ablLv .. "->" .. tostring(ablLv + 1) .. ")") target[abl] = tostring(ablLv + 1) -- 조건(흔적 등) 차감 for k, v in pairs(selectedCondition) do local currentVal = tonumber(target[k]) or 0 target[k] = tostring(currentVal - v) end -- 능력 선택 초기화 setChatVar(triggerId, "selectedAbl", "none") -- 변경된 target 저장 setState(triggerId, "target", target) stateToVar(triggerId, "target", target) -- 캐릭터 정보를 로컬로어북에 저장 local option = { alwaysActive = false, insertOrder = 100, key = "", secondKey = "", regex = false } upsertLocalLoreBook(triggerId, target["이름"], json.encode(target), option) -- [신규] 특성 진화 체크 (Abl 레벨업 시점) local traitTarget, traitLog = sysFunction(triggerId, "traitEvolution.sys", target) if traitLog ~= "" then -- 진화로 인해 target이 변경되었으므로 다시 저장 target = traitTarget upsertLocalLoreBook(triggerId, target["이름"], json.encode(target), option) stateToVar(triggerId, "target", target) -- Screen Effect 연출 (HTML) -- 기존 ScreenEffect가 있다면 덮어씌우거나 추가 local existEffect = getChatVar(triggerId, "screenEffect") setChatVar(triggerId, "screenEffect", existEffect .. traitLog) reloadDisplay(triggerId) -- 화면 갱신으로 이펙트 출력 end end -- 어빌리티 레벨업 조건 선택 UI 생성 function funcs.selectCondition(triggerId, abl, lvUpConditions) local target = getState(triggerId, "target") local ablLv = tonumber(target[abl]) if ablLv >= 10 then alertNormal(triggerId, abl .. ": 이미 최대레벨입니다.") return end -- 레벨업 조건 html표현식 local selectedAbl = "" local selectedCondition = { abl } for i, condition in ipairs(lvUpConditions) do selectedCondition[i + 1] = {} for k, v in pairs(condition) do selectedCondition[i + 1][k] = v[ablLv + 1] end -- 조건을 미리 검토해 렙업이 가능한 조건에는 녹색 테두리로 표기 local available = "notAvailable" if funcs.levelUp(triggerId, abl, selectedCondition[i + 1], false) then available = "available" end selectedAbl = selectedAbl .. "<div class='lvUpConditions btn " .. available .. "' risu-btn='ablLvUp" .. i .. "'>" for k, v in pairs(condition) do selectedAbl = selectedAbl .. "<span><b>" .. k .. "</b>: " .. v[ablLv + 1] .. "</span><br>" end selectedAbl = selectedAbl .. "</div>" end setChatVar(triggerId, "selectedAbl", selectedAbl) setState(triggerId, "selectedCondition", selectedCondition) end --------------------------------------- -- 메인 함수 (외부에서 호출됨) --------------------------------------- return function(triggerId, subFunc, ...) if funcs[subFunc] then return funcs[subFunc](triggerId, ...) else debug("ability.sys: 알 수 없는 함수 - " .. tostring(subFunc)) end endend)()
- #2alertEffect.sysconstant
Keys:
-- alertEffect.sys.txt -- 스크린이펙트 알림 시스템
function(triggerId, message, alertType) alertType = alertType or "info"
-- 타입별 색상 설정 local colors = { info = { bg = "#111", text = "#fff", glow = "#60d9f7" }, unlock = { bg = "#111", text = "#ffd700", glow = "#ffd700" }, success = { bg = "#111", text = "#4caf50", glow = "#4caf50" }, warning = { bg = "#111", text = "#ff9800", glow = "#ff9800" }, danger = { bg = "#111", text = "#f44336", glow = "#f44336" } } local c = colors[alertType] or colors.info -- 순차 표시를 위한 알림 카운트 (1.4초씩 딜레이) local alertCount = tonumber(getChatVar(triggerId, "alertEffectCount")) or 0 local delay = alertCount * 1.4 alertCount = alertCount + 1 setChatVar(triggerId, "alertEffectCount", alertCount) local effectHtml = string.format([[]], c.text, c.glow, delay, message)%s-- 기존 효과 앞에 추가 (역순으로 쌓아서 먼저 뜬 알림이 위에 오도록 함 - 클릭 가능하게) local existingEffect = getChatVar(triggerId, "screenEffect") or "" setChatVar(triggerId, "screenEffect", effectHtml .. existingEffect)end
- #3battle.sysconstant
Keys:
-- battle.sys.txt -- Dungeon Battle System Module
(function() --------------------------------------- -- ★ 밸런싱 상수 정의 (Balancing Constants) -- 전투 시스템의 핵심 수치를 조정하려면 이 섹션의 값을 수정하세요. ---------------------------------------
-- [확률 상수] 기력(SP) 0 이하 시 발생하는 이벤트 확률 local PROB_CAPTURE = 0.1 -- 나포 확률 (10%). 용사가 기력 소진 시 사로잡힐 확률. local PROB_FLEE_SUCCESS = 0.1 -- 도주 성공 확률 (10%). 용사가 기력 소진 시 도망칠 확률. (나머지 80%는 전투 지속) -- [전투 판정 상수] local CRIT_THRESHOLD = 0.9 -- 치명타 판정 기준. 공격 데미지가 (최대 공격력 * 이 값) 이상이면 치명타로 간주. local SP_DMG_MIN = 5 -- 최소 기력(SP) 피해량. 몬스터 공격 시 용사 기력 감소의 최솟값. (레이팅 차이보다 이 값이 클 경우 적용) -- [용사 퇴각 조건] local RETREAT_HP_RATIO = 0.3 -- HP 퇴각 기준. 용사 HP가 (최대 HP * 이 값) 미만이면 부상으로 도망. local RETREAT_SP_RATIO = 0.2 -- SP 퇴각 기준. 용사 SP가 (최대 SP * 이 값) 미만이면 공포로 도망. -- [침공 참여 조건] local INVADER_HP_RATIO = 0.8 -- 침공 참여 HP 조건. 용사 HP가 (최대 HP * 이 값) 이상이어야 침공에 참여. local INVADER_SP_RATIO = 0.8 -- 침공 참여 SP 조건. 용사 SP가 (최대 SP * 이 값) 이상이어야 침공에 참여. --------------------------------------- -- 로컬 함수 정의 --------------------------------------- -- 로그 추가 헬퍼 local function addLog(battleState, msg) table.insert(battleState.logs, msg) end -- 데미지 계산 헬퍼 local function calcDamage(atkMin, atkMax, def) local atk = math.random(atkMin, atkMax) local defense = math.random(0, def) local dmg = math.max(0, atk - defense) local isCrit = false if dmg >= (atkMax * CRIT_THRESHOLD) then isCrit = true end return dmg, isCrit end -- 공격력 파싱 헬퍼 (Min~Max or Single) local function parseAtk(val) if type(val) == "string" and string.find(val, "~") then local min, max = val:match("(%d+)~(%d+)") return tonumber(min), tonumber(max) else local v = tonumber(val) or 10 return v, v end end -- 시너지 로드 (placeholder) -- 시너지 로드 local function loadSynergyModifiers(triggerId) local jsonStr = getChatVar(triggerId, "dungeon_modifiers") if jsonStr and jsonStr ~= "" then -- json 라이브러리는 전역에 있다고 가정 (battle.sys 다른곳에서 씀) local status, res = pcall(json.decode, jsonStr) if status then return res end end return {} end -- 개별 몬스터 전투 루프 local function runCombatLoop(battleState, monster, slotId) local hero = battleState.currentHero local floor = battleState.currentFloor local slotNum = battleState.currentSlot local prefix = string.format("%dF-%d", floor, slotNum) local monBase = monster.data local monMod = battleState.modifiers[slotId] or {} local function applyMod(base, modVal) if not modVal then return base end if type(modVal) == "table" then local add = modVal.add or 0 local mul = modVal.mul or 1.0 return math.floor((base + add) * mul) elseif type(modVal) == "number" then return base + modVal end return base end local monStats = { name = monBase.name, hp = monster.hp, maxHp = monster.maxHp, atkMin = applyMod((monBase.atkMin or 0), monMod.atkMin), atkMax = applyMod((monBase.atkMax or 0), monMod.atkMax), def = applyMod((monBase.def or 0), monMod.def), rating = monBase.rating or 0 } addLog(battleState, string.format("%s%s와(과) 조우! (HP: %d/%d, Atk: %d~%d, Def: %d)", prefix, monStats.name, monStats.hp, monStats.maxHp, monStats.atkMin, monStats.atkMax, monStats.def)) local turn = 1 local loopResult = nil -- "Win", "Lose", "Retreat", "Captured" while monStats.hp > 0 and hero.hp > 0 do local heroFirst = (math.random() < 0.5) local order = {} if heroFirst then order = {"hero", "monster"} else order = {"monster", "hero"} end for _, attackerType in ipairs(order) do if hero.hp <= 0 or monStats.hp <= 0 then break end if attackerType == "hero" then local dmg, crit = calcDamage(hero.atkMin, hero.atkMax, monStats.def) local desc = "공격" if crit then desc = "치명타 공격" end if dmg == 0 then desc = "공격이 막힘" end monStats.hp = monStats.hp - dmg addLog(battleState, string.format("[Turn %d] %s 용사의 %s! %s에게 %d 피해. (남은 HP: %d)", turn, prefix, desc, monStats.name, dmg, math.max(0, monStats.hp))) else local dmg, crit = calcDamage(monStats.atkMin, monStats.atkMax, hero.def) local desc = "공격" if crit then desc = "치명타 공격" end if dmg == 0 then desc = "공격이 빗나감" end hero.hp = hero.hp - dmg if dmg > 0 then local spDmg = math.max(SP_DMG_MIN, monStats.rating - (hero.rating or 0)) hero.sp = hero.sp - spDmg addLog(battleState, string.format("[Turn %d] %s %s의 %s! 용사에게 %d 피해(기력 %d 감소). (남은 HP: %d, SP: %d)", turn, prefix, monStats.name, desc, dmg, spDmg, math.max(0, hero.hp), math.max(0, hero.sp))) -- 기력 부족 시 도주/전투/나포 판정 if hero.sp <= 0 then local roll = math.random() if roll < PROB_CAPTURE then -- 10% 나포 addLog(battleState, string.format("[Turn %d] %s 용사가 정신을 잃고 쓰러졌습니다! (나포됨)", turn, prefix)) loopResult = "Captured" elseif roll < (PROB_CAPTURE + PROB_FLEE_SUCCESS) then -- 10% 도주 성공 addLog(battleState, string.format("[Turn %d] %s 용사가 공포에 질려 도망칩니다!", turn, prefix)) loopResult = "Retreat" else -- 80% 전투 지속 addLog(battleState, string.format("[Turn %d] %s 용사가 공포에 질렸지만 이 악물고 버팁니다!", turn, prefix)) -- continue end end else -- 데미지 0일때 addLog(battleState, string.format("[Turn %d] %s %s의 %s! 용사에게 %d 피해. (남은 HP: %d)", turn, prefix, monStats.name, desc, dmg, math.max(0, hero.hp))) end end if loopResult then break end end if loopResult then return loopResult, turn end turn = turn + 1 end if monStats.hp <= 0 then addLog(battleState, string.format("%s %s 처치!", prefix, monStats.name)) return "Win", turn elseif hero.hp <= 0 then return "Lose", turn end return nil, turn end -- 보스전 루프 local function runBossFight(battleState, hero, userStats) addLog(battleState, "=== [BOSS BATTLE] 던전 최심부, 마왕의 알현실 ===") addLog(battleState, string.format("마왕 %s(이)가 침입자 %s(을)를 맞이합니다!", battleState.userName, hero.name)) -- 마왕 공격력 파싱 -- 마왕 공격력 파싱 local bossAtkMin, bossAtkMax = parseAtk(userStats.atk) local bossStats = { name = battleState.userName, hp = userStats.hp, maxHp = userStats.maxHp, atkMin = bossAtkMin, atkMax = bossAtkMax, def = userStats.def } local turn = 1 while bossStats.hp > 0 and hero.hp > 0 and hero.sp > 0 do local dmg, crit = calcDamage(bossStats.atkMin, bossStats.atkMax, hero.def) hero.hp = hero.hp - dmg addLog(battleState, string.format("[Turn %d] 마왕의 강력한 일격! %s에게 %d 피해. (남은 HP: %d)", turn, hero.name, dmg, math.max(0, hero.hp))) if hero.hp <= 0 then return "Win", bossStats.hp end dmg, crit = calcDamage(hero.atkMin, hero.atkMax, bossStats.def) bossStats.hp = bossStats.hp - dmg addLog(battleState, string.format("[Turn %d] %s의 필사적인 공격! 마왕에게 %d 피해. (남은 HP: %d)", turn, hero.name, dmg, math.max(0, bossStats.hp))) turn = turn + 1 end if bossStats.hp <= 0 then return "Lose", 0 else return "Win", bossStats.hp end end -- 메인 침공 프로세스 local function processInvasion(triggerId) local dungeon = getState(triggerId, "dungeon") or { floors=1, grid={} } local modifiers = loadSynergyModifiers(triggerId) local monstersDB = getDB(triggerId, "monsters.db") -- Safe decode chars local charsStr = getChatVar(triggerId, "chars") local chars = {} if charsStr and charsStr ~= "" then local status, res = pcall(json.decode, charsStr) if status then chars = res else debug("battle.sys: chars decode failed") end end -- Safe decode user local userStr = getLoreBookContent(triggerId, "user") or "{}" local userJson = {} if userStr and userStr ~= "" then local status, res = pcall(json.decode, userStr) if status then userJson = res else debug("battle.sys: user decode failed") end end local userName = userJson["이름"] or "마왕" local heroes = {} local waitingHeroes = {} for _, name in ipairs(chars) do local charStr = getLoreBookContent(triggerId, name) local charData = {} if charStr and charStr ~= "" then local status, res = pcall(json.decode, charStr) if status then charData = res end end local hp = tonumber(charData["체력"]) or 0 local maxHp = tonumber(charData["최대체력"]) or 100 local sp = tonumber(charData["기력"]) or 0 local maxSp = tonumber(charData["최대기력"]) or 100 -- 침공 조건: 정비중 + 체력/기력 조건 충족 if charData.condition == "REST" and hp >= (maxHp * INVADER_HP_RATIO) and sp >= (maxSp * INVADER_SP_RATIO) then table.insert(waitingHeroes, charData) end end local requiredHeroes = 1 local loopCount = 0 local MAX_LOOP = 5 while #waitingHeroes < requiredHeroes and loopCount < MAX_LOOP do loopCount = loopCount + 1 sysFunction(triggerId, "newChar.sys", "invasion") charsStr = getChatVar(triggerId, "chars") chars = {} if charsStr and charsStr ~= "" then local status, res = pcall(json.decode, charsStr) if status then chars = res end end local newName = chars[#chars] if newName then local newCharStr = getLoreBookContent(triggerId, newName) local newChar = {} if newCharStr and newCharStr ~= "" then local status, res = pcall(json.decode, newCharStr) if status then newChar = res end end local hp = tonumber(newChar["체력"]) or 0 local maxHp = tonumber(newChar["최대체력"]) or 100 local sp = tonumber(newChar["기력"]) or 0 local maxSp = tonumber(newChar["최대기력"]) or 100 if newChar.condition == "REST" and hp >= (maxHp * INVADER_HP_RATIO) and sp >= (maxSp * INVADER_SP_RATIO) then table.insert(waitingHeroes, newChar) end else debug("battle.sys: Failed to get new character name") end end -- If strictly required, pick whatever we have or duplicate if #waitingHeroes == 0 then -- Emergency fallback table.insert(waitingHeroes, {["이름"]="용사(임시)", ["체력"]=100, ["최대체력"]=100, ["기력"]=100, ["최대기력"]=100, ["공격력"]=10, ["방어력"]=5, ["레이팅"]=10}) end for i = 1, math.min(#waitingHeroes, requiredHeroes) do table.insert(heroes, waitingHeroes[i]) end local battleState = { heroes = heroes, currentHeroIdx = 1, currentHero = nil, userName = userName, modifiers = modifiers, logs = {}, monstersList = {}, currentFloor = 1, currentSlot = 1 } addLog(battleState, string.format("🚨 경보! %d명의 용사가 던전을 침공했습니다!", #heroes)) local finalResult = "Ongoing" for i, h in ipairs(heroes) do h.hp = tonumber(h["체력"]) or 100 h.maxHp = tonumber(h["최대체력"]) or 100 h.sp = tonumber(h["기력"]) or 100 h.maxSp = tonumber(h["최대기력"]) or 100 -- 공격력 파싱 (min~max) local amin, amax = parseAtk(h["공격력"]) h.atkMin = amin h.atkMax = amax h.def = tonumber(h["방어력"]) or 10 h.rating = tonumber(h["레이팅"]) or 10 h.name = h["이름"] end local clearedSlots = {} local timeline = {} -- [NEW] 전투 타임라인 기록 local globalTurnIndex = 0 -- 로그 턴 배치 순서를 위한 누적 인덱스 while battleState.currentHeroIdx <= #heroes do local hero = heroes[battleState.currentHeroIdx] battleState.currentHero = hero addLog(battleState, string.format(">>> %d번째 용사 [%s] (Rating: %d) 출격!", battleState.currentHeroIdx, hero.name, hero.rating)) -- 출격 로그는 턴으로 치지 않거나, 0턴으로 취급 local heroDefeated = false for f = battleState.currentFloor, dungeon.floors do battleState.currentFloor = f local floorHasUnits = false local startSlot = 1 if f == battleState.currentFloor then startSlot = battleState.currentSlot end if startSlot > 5 then startSlot = 1 end for s = startSlot, 5 do battleState.currentSlot = s local slotId = string.format("%d_%d", f, s) local node = dungeon.grid and dungeon.grid[slotId] -- 타임라인 이벤트 기록 (이동/조우) local currentDefender = { type="none", key="" } if node then floorHasUnits = true if node.type == "monster" then local monData = monstersDB[node.key] if monData then currentDefender = { type="monster", key=node.key } local monster = { key = node.key, hp = monData.hp, maxHp = monData.hp, data = monData } battleState.monstersList[monData.name] = monData.desc -- 타임라인 저장 (이 조우가 시작되는 누적 턴 인덱스 기록) table.insert(timeline, { startTurnIndex = globalTurnIndex, floor = f, slot = s, defender = currentDefender, slotId = slotId }) local outcome, combatTurns = runCombatLoop(battleState, monster, slotId) globalTurnIndex = globalTurnIndex + (combatTurns or 1) -- 이 전투에서 사용된 턴만큼 증가 if outcome == "Lose" then addLog(battleState, hero.name.."이(가) 쓰러졌습니다!") hero["condition"] = "FAINT" heroDefeated = true break elseif outcome == "Captured" then addLog(battleState, hero.name.."이(가) 적에게 사로잡혔습니다!") hero["condition"] = "TRAINABLE" heroDefeated = true break elseif outcome == "Retreat" then addLog(battleState, hero.name.."이(가) 도망쳤습니다!") heroDefeated = true break end -- 승리 clearedSlots[slotId] = true if hero.hp < (hero.maxHp * RETREAT_HP_RATIO) then addLog(battleState, hero.name.."이(가) 부상을 입고 도망칩니다...") heroDefeated = true break elseif hero.sp < (hero.maxSp * RETREAT_SP_RATIO) then addLog(battleState, hero.name.."이(가) 공포에 질려 도망칩니다...") heroDefeated = true break end end elseif node.type == "trap" then currentDefender = { type="trap", key=node.key } table.insert(timeline, { startTurnIndex = globalTurnIndex, floor = f, slot = s, defender = currentDefender, slotId = slotId }) globalTurnIndex = globalTurnIndex + 1 -- 함정은 1턴만 소모 addLog(battleState, string.format("%dF-%d 함정 발동!", f, s)) hero.hp = hero.hp - 50 clearedSlots[slotId] = true if hero.hp <= 0 then addLog(battleState, hero.name.."이(가) 함정에 걸려 쓰러졌습니다!") hero["condition"] = "FAINT" heroDefeated = true break end end else -- 빈 슬롯 clearedSlots[slotId] = true -- 빈 슬롯도 턴(시간)을 소모하는가? 로그에는 "고요합니다"가 층 단위로 뜸. -- 시각적으로는 '지나가는' 모션이 필요할 수 있음. 일단 스킵하거나 빠르게 지나가도록. end end if not floorHasUnits then addLog(battleState, string.format("%dF는 고요합니다... 용사가 발걸음을 재촉합니다.", f)) end if heroDefeated then break end battleState.currentSlot = 1 end if not heroDefeated then table.insert(timeline, { startTurnIndex = globalTurnIndex, floor = 99, -- Boss Room slot = 99, defender = { type="boss", key="user" }, slotId = "boss" }) addLog(battleState, hero.name.."이(가) 마왕의 방에 도달했습니다!") local userStats = { hp = tonumber(userJson["체력"]) or 1000, maxHp = tonumber(userJson["최대체력"]) or 1000, atk = tonumber(userJson["공격력"]) or 100, def = tonumber(userJson["방어력"]) or 100, } local bossResult, remainingHp = runBossFight(battleState, hero, userStats) -- 유저 HP 업데이트 및 저장 userJson["체력"] = math.max(0, remainingHp) upsertLocalLoreBook(triggerId, "user", json.encode(userJson), DEFAULT_LORE_OPTION) if bossResult == "Lose" then finalResult = "BadEnding" break else addLog(battleState, "마왕이 용사를 직접 제압했습니다!") hero["condition"] = "FAINT" heroDefeated = true end end -- HP/SP 변화를 로어북 키에 반영 hero["체력"] = math.max(0, hero.hp) hero["기력"] = math.max(0, hero.sp) debug(string.format("[battle.sys] Save Hero State: %s (HP:%d, SP:%d)", hero.name, hero["체력"], hero["기력"])) upsertLocalLoreBook(triggerId, hero.name, json.encode(hero), DEFAULT_LORE_OPTION) battleState.currentHeroIdx = battleState.currentHeroIdx + 1 end if finalResult == "BadEnding" then addLog(battleState, "마왕성이 함락되었습니다...") else addLog(battleState, "방어에 성공했습니다.") finalResult = "Win" end local bestiary = {} for name, desc in pairs(battleState.monstersList) do table.insert(bestiary, string.format("- %s: %s", name, desc)) end local heroList = {} for _, h in ipairs(heroes) do table.insert(heroList, string.format("- %s (HP:%s/Rating:%s)", h.name, h.maxHp, h.rating)) end local cleanLogs = {} for _, line in ipairs(battleState.logs) do table.insert(cleanLogs, line) end local logText = table.concat(cleanLogs, "\n") local summary = { result = finalResult, logs = battleState.logs, logText = logText, heroList = table.concat(heroList, "\n"), bestiary = table.concat(bestiary, "\n"), heroCount = #heroes, dungeonLv = dungeon.floors, userName = userName, clearedSlots = clearedSlots, timeline = timeline, -- [NEW] heroes = heroes -- [NEW] For image generation } -- Final summary return return summary end --------------------------------------- -- 디스패처 테이블 --------------------------------------- local funcs = { processInvasion = processInvasion } --------------------------------------- -- 메인 함수 (외부에서 호출됨) --------------------------------------- return function(triggerId, subFunc, ...) if funcs[subFunc] then return funcs[subFunc](triggerId, ...) else print("battle.sys: 알 수 없는 함수 - " .. tostring(subFunc)) end endend)()
- #4char.sysconstant
Keys:
-- char.sys.txt -- 캐릭터 관리 통합 모듈 -- 사용법: sysFunction(triggerId, "char.sys", "card", name) -- sysFunction(triggerId, "char.sys", "list")
(function() --------------------------------------- -- 로컬 함수 정의 ---------------------------------------
--------------------------------------- -- 디스패처 테이블 및 로컬 변수 --------------------------------------- local funcs = {} -- 캐릭터 카드 선택 처리 function funcs.card(triggerId, name) debug(string.format("[Char:Select] Target: %s, Screen: %s", name, getState(triggerId, "screen"))) local screen = getState(triggerId, "screen") -- traceList 생성 (UI용) local traceDB = getDB(triggerId, "trace.db") local traceList = {} for k, v in pairs(traceDB) do if v.name then table.insert(traceList, v.name) end end table.sort(traceList) setChatVar(triggerId, "traceList", json.encode(traceList)) if screen == "charList" then --캐릭터 조회화면 if name == getPersonaName(triggerId) then name = "user" end debug("[CharSys:Card] Loading charInfo for: " .. tostring(name)) local rawContent = getLoreBookContent(triggerId, name) debug("[CharSys:Card] Raw content length: " .. tostring(rawContent and #rawContent or "nil")) local target = json.decode(rawContent) debug("[CharSys:Card] Decoded target: " .. tostring(target and "OK" or "nil")) if target then debug("[CharSys:Card] Target condition: " .. tostring(target.condition or target["condition"])) end setState(triggerId, "target", target) stateToVar(triggerId, "target", target) setChatVar(triggerId, "category", "appearance") screenChange(triggerId, "charInfo") --캐릭터 상세 정보 화면으로 이동 elseif screen == "disposal" then -- 포로 처분 화면 -- 처분 방식 선택 실행 sysFunction(triggerId, "disposal.sys", "selectMethod", name) elseif screen == "preTrain" then --조교 대상 화면 sysFunction(triggerId, "train.sys", "prepare", name) screenChange(triggerId, "train") --조교 화면으로 이동 end end -- 버튼 클릭 핸들러 -- data format: "char.sys_card_name" function funcs.click(triggerId, data) -- sys파일명_card_name 형태에서 name 추출 local name = string.match(data, "card_(.*)") if name then funcs.card(triggerId, name) else debug("char.sys: 잘못된 데이터 포맷 - " .. tostring(data)) end end -- 캐릭터 목록 생성 function funcs.list(triggerId) local chars = json.decode(getChatVar(triggerId, "chars")) local html = "" local screen = getState(triggerId, "screen") local sortMode = getChatVar(triggerId, "category") or "index" -- 기본값 index -- 1. 데이터 수집 및 전처리 local charDataList = {} -- [Refactoring] 필터링 로직 함수화 (확장성을 위해) local function checkFilter(scr, c) local cond = c["condition"] if scr == "preTrain" then -- preTrain: 오직 '조교가능' 상태인 캐릭터만 표시 return cond == "TRAINABLE" elseif scr == "charList" then -- charInfo: 사망한 캐릭터도 포함하여 모두 표시 return true elseif scr == "disposal" then -- disposal: 사망(미처분) + 조교가능 + 실신 상태만 표시 local disposed = c["처분"] ~= nil return (cond == "DEAD" and not disposed) or cond == "TRAINABLE" or cond == "FAINT" else -- 기본값: 사망하지 않은 캐릭터만 표시 return cond ~= "DEAD" end end for i, name in ipairs(chars) do -- preTrain 화면에서는 user 제외 (user는 chars 리스트에 보통 없지만 체크 필요시 추가) local char = json.decode(getLoreBookContent(triggerId, name)) if checkFilter(screen, char) then -- 정렬 및 표시에 필요한 데이터 미리 추출 local data = { index = i, name = name, raw = char, lv = tonumber(char["레벨"] or 0), hp = tonumber(char["체력"] or 0), hpMax = tonumber(char["최대체력"] or 0), st = tonumber(char["기력"] or 0), stMax = tonumber(char["최대기력"] or 0), atk = char["공격력"] or "0", def = char["방어력"] or "0", rating = tonumber(char["레이팅"] or 0), condition = char["condition"] or "", title = char["칭호"] or "", disposed = char["처분"] ~= nil } table.insert(charDataList, data) -- Condition DB Load (Lazy) (Cached outside or loaded every list call is fine for now) local condDB = getDB(triggerId, "condition.db") if condDB[data.condition] and condDB[data.condition].name then data.conditionDisplay = condDB[data.condition].name else data.conditionDisplay = data.condition -- Fallback (e.g. if already Korean or unknown) end end end -- 2. 정렬 로직 수행 if sortMode == "level" then table.sort(charDataList, function(a, b) return a.lv > b.lv end) -- 레벨 내림차순 elseif sortMode == "name" then table.sort(charDataList, function(a, b) return a.name < b.name end) -- 이름 오름차순 elseif sortMode == "hp" then table.sort(charDataList, function(a, b) return a.hp > b.hp end) -- 체력 비율 오름차순 elseif sortMode == "rating" then table.sort(charDataList, function(a, b) return a.rating > b.rating end) -- 레이팅 내림차순 else -- index 정렬 (기본) table.sort(charDataList, function(a, b) return a.index < b.index end) end -- 3. HTML 생성 for _, data in ipairs(charDataList) do local name = data.name -- 체력/기력 비율 계산 local hpRatio = 0 if data.hpMax > 0 then hpRatio = (data.hp / data.hpMax) * 100 end local stRatio = 0 if data.stMax > 0 then stRatio = (data.st / data.stMax) * 100 end -- 칭호 태그 스타일 결정 local tagClass = "" local tagHtml = "" local title = data.title if title ~= "" then if title == "음란" then tagClass = "tag-lewd" elseif title == "연모" then tagClass = "tag-love" elseif title == "암캐" then tagClass = "tag-puppy" elseif title == "붕괴" then tagClass = "tag-destroyed" elseif title == "광기" then tagClass = "tag-mad" else tagClass = "tag-unconquered" -- 기본값 end tagHtml = [[<span class="tag ]] .. tagClass .. [["> 〈]] .. title .. [[〉</span>]] end -- 상태에 따른 클래스 추가 (사망 등) local cardClass = "character-card" if data.condition == "DEAD" or data.condition == "사망" then cardClass = cardClass .. " 사망" end if name == "user" then name = "{{" .. name .. "}}" end -- HTML 조립 -- HTML 조립 local portraitHtml = "" local p_pre = data.raw.portrait_pre local p_app = data.raw.portrait_append local portraitContent = "" if p_pre and p_app then portraitContent = p_pre .. p_app elseif data.raw.portrait and data.raw.portrait ~= "" then -- 기존 방식 (Fallback) local val = data.raw.portrait -- 일반 경로 텍스트면 {{raw::...}} 로 감싸고 img 태그 추가 if not val:match("^{{") and not val:match("^<img") then val = "{{raw::" .. val .. "}}" end -- 이미지가 아니라 inlay등의 매크로라면 그대로, 아니면 img 태그로 if val:match("{{inlay") or val:match("^<div") or val:match("^<img") then portraitContent = val else portraitContent = [[<img src="]] .. val .. [[" style="width:100%; height:100%; object-fit:cover;">]] end end if portraitContent ~= "" then local xOverlay = "" if data.disposed then xOverlay = [[<img src="{{raw::x.png}}" style="position:absolute; top:0; left:0; width:100%; height:100%; object-fit:contain; z-index:10;">]] end portraitHtml = [[<div style="position:relative; width:50px; height:50px; overflow:hidden; border-radius:4px; margin-right:8px; flex-shrink:0;">]] .. portraitContent .. xOverlay .. [[</div>]] end local card = [[]]]] .. portraitHtml .. [[[]] .. data.conditionDisplay .. [[] ]] .. name .. [[ Lv.]] .. data.lv .. [[ ]] .. tagHtml .. [[
html = html .. card end if html == "" then html = "<div class='status-message'><p>표시할 캐릭터가 없습니다.</p></div>" end setChatVar(triggerId, "charList", html) end --------------------------------------- -- 메인 함수 (외부에서 호출됨) --------------------------------------- return function(triggerId, subFunc, ...) if funcs[subFunc] then return funcs[subFunc](triggerId, ...) else debug("char.sys: 알 수 없는 함수 - " .. tostring(subFunc)) end endend)()
- #5customEvent.sysconstant
Keys:
--커스텀 이벤트 실행 엔진 --캐릭터가 보유한 trait/abl/mark/stat/skills/buff의 해당 이벤트를 실행 --@param eventName: 이벤트 이름 (onDCCalc, onStatEXP 등) --@param context: 이벤트에 전달할 변수 테이블 (must include: tags, target, stat) --@return context: 수정된 변수 테이블 function(triggerId, eventName, context)
------------------------------------------------------- -- Helper Functions ------------------------------------------------------- --이벤트 함수 실행 (문자열 or 함수) local function executeEventFunc(funcOrStr, ctx) if type(funcOrStr) == "function" then local status, result = pcall(funcOrStr, ctx) if not status then debug("customEvent V2 실행 에러: " .. tostring(result)) end return ctx end local funcStr = funcOrStr local env = {} for k, v in pairs(ctx) do env[k] = v end setmetatable(env, {__index = _G}) local func, err = load(funcStr, "eventFunc", "t", env) if func then local status, result = pcall(func) if status then for k, _ in pairs(ctx) do if env[k] ~= nil then ctx[k] = env[k] end end else debug("customEvent 레거시 실행 에러: " .. tostring(result)) end else debug("customEvent 레거시 함수 로드 에러: " .. (err or "unknown")) end return ctx end --태그 매칭 로직 local function tagMatch(tags, hasTags, notTags) if hasTags then for _, requiredTag in ipairs(hasTags) do local found = false for _, tag in ipairs(tags) do if tag == requiredTag then found = true; break end end if not found then return false end end end if notTags then for _, excludeTag in ipairs(notTags) do for _, tag in ipairs(tags) do if tag == excludeTag then return false end end end end return true end ------------------------------------------------------- -- Global Cache Logic (Lazy Init) ------------------------------------------------------- if not _G.CustomEventCache then debug("[CustomEvent] 캐시 초기화 시작...") _G.CustomEventCache = {} -- 등록할 DB 목록 local dbList = { {name="trait", file="trait.db"}, {name="abl", file="abl.db"}, {name="mark", file="mark.db"}, {name="stat", file="stat.db"}, {name="skill", file="skills.db"}, {name="buff", file="buff.db"} } for _, dbInfo in ipairs(dbList) do local dbData = getDB(triggerId, dbInfo.file) if dbData then for key, def in pairs(dbData) do -- events 필드가 있는 경우에만 등록 if def.events then local entryName = def.name or key for evtKey, evtDef in pairs(def.events) do -- 이벤트 이름 추출 (onDCCalc_1 -> onDCCalc) local realEventName = evtKey:match("^([^_]+)") or evtKey if not _G.CustomEventCache[realEventName] then _G.CustomEventCache[realEventName] = {} end table.insert(_G.CustomEventCache[realEventName], { type = dbInfo.name, key = entryName, -- 한글 이름 or 키 rawKey = evtKey, -- 실제 이벤트 키 (onDCCalc_2 등) def = evtDef, sourceName = entryName .. "(" .. dbInfo.name .. ")" }) end end end end end debug("[CustomEvent] 캐시 초기화 완료.") end ------------------------------------------------------- -- Main Logic (Cache Based) ------------------------------------------------------- local listeners = _G.CustomEventCache[eventName] if not listeners then return context end -- 해당 이벤트 리스너가 없으면 바로 종료 local target = context.target or getState(triggerId, "target") local currentStat = context.stat or getState(triggerId, "stat") local tags = context.tags or {} for _, listener in ipairs(listeners) do -- 보유 여부 확인 local level = 0 local hasItem = false if listener.type == "stat" then -- stat은 currentStat 테이블에서 확인 local val = currentStat[listener.key] level = tonumber(val) or 0 if level > 0 then hasItem = true end else -- trait, abl, mark, skill, buff는 target 테이블에서 확인 local val = target[listener.key] if val then level = tonumber(val) or 1 -- 숫자가 아니면 1 취급 (trait 등) if level > 0 then hasItem = true end end end if hasItem then -- 정의된 이벤트 실행 (배열 지원) local evt = listener.def local actionList = {} if evt[1] then actionList = evt else table.insert(actionList, evt) end for _, action in ipairs(actionList) do -- 태그 조건 local matched = tagMatch(tags, action.hasTags, action.notTags) -- 타겟 스탯 조건 if matched and action.targetStat and context.statName then local statMatched = false for _, tStat in ipairs(action.targetStat) do if tStat == context.statName then statMatched = true; break end end if not statMatched then matched = false end end if matched then -- debug(string.format("[Event:Trigger] %s -> %s", listener.sourceName, eventName)) context.level = level context = executeEventFunc(action.func, context) end end end end return contextend
- #6debugCmds.sysconstant
Keys:
function (triggerId) debug("debugCmds 실행") local key, functionBody local funcs = {} local screen = getState(triggerId, "screen") if screen == "gameStart" or screen == "intro" then setState(triggerId, "funcs", funcs) return --메인화면 진입 전까지는 실행하지 않음 end --0000: 메인화면으로 복귀 key = "0000" functionBody = [[ function(triggerId) alertNormal(triggerId, "강제커맨드 발생. 메인화면으로 돌아갑니다.") setChatVar(triggerId, "category", "items") screenChange(triggerId, "main") setChat(triggerId, getChatLength(triggerId)-1, "{{".."getvar::html".."}}") end]] funcs[key] = functionBody
--911: 화면 리프레시 key = "911" functionBody = [[ function(triggerId) alertNormal(triggerId, "강제커맨드 발생. 화면을 다시 로딩합니다.") setChat(triggerId, getChatLength(triggerId)-1, "{{".."getvar::html".."}}") end]] funcs[key] = functionBody --6974: 빈 챗 추가 key = "6974" functionBody = [[ function(triggerId) alertNormal(triggerId, "강제커맨드 발생. 빈 챗을 추가합니다.") addChat(triggerId, "user", "") end]] funcs[key] = functionBody --치트 메뉴 활성화 key = "https://arca.live/b/characterai"-- key = "dd" functionBody = [[ function(triggerId) setChatVar(triggerId, "unlock_cheat_menu", "1") alertNormal(triggerId, "치트 메뉴가 해금되었습니다! 메인 화면을 확인하세요.") setChatVar(triggerId, "category", "items") screenChange(triggerId, "main") setChat(triggerId, getChatLength(triggerId)-1, "{{".."getvar::html".."}}") end]] funcs[key] = functionBody
setState(triggerId, "funcs", funcs)end
- #7dictSelect.sysconstant
Keys:
(function() local funcs = {}
-- 기존 로직을 select 함수로 이동 function funcs.select(triggerId, category, name) -- 카테고리에 맞는 DB 파일 매핑 local dbMap = { ["abl"] = "abl.db", ["mark"] = "mark.db", ["trace"] = "trace.db", ["exp"] = "exp.db", ["stat"] = "stat.db", ["trait"] = "trait.db", ["item"] = "items.db", ["monster"] = "monsters.db" } local dbName = dbMap[category] or category..".db" -- 아이템의 경우 trap.db도 검색해야 하므로 별도 처리 local data = nil if category == "item" then local itemsDB = getDB(triggerId, "items.db") local trapDB = getDB(triggerId, "trap.db") -- items.db는 중첩 구조: itemsDB[카테고리][키] -- 모든 카테고리를 순회하여 키 검색 for catName, items in pairs(itemsDB) do if type(items) == "table" and items[name] then data = items[name] break end end -- items.db에서 못 찾으면 trap.db 검색 if not data and trapDB[name] then data = trapDB[name] end else local db = getDB(triggerId, dbName) if db then -- 1. 키로 직접 검색 시도 if db[name] then data = db[name] else -- 2. 키 검색 실패 시, 이름(v.name)으로 검색 시도 for k, v in pairs(db) do if v.name == name then data = v break end end end end end local title = name local desc = "" local imgHtml = "" if data then title = data.name or name desc = data.desc or "설명이 없습니다." -- 추가 정보 표시 if category == "monster" then local rating = data.rating or "?" local hp = data.hp or "?" local atkMin = data.atkMin or "?" local atkMax = data.atkMax or "?" local def = data.def or "?" desc = desc .. "<br><br>" .. "Rating: " .. rating .. " | HP: " .. hp .. "<br>" .. "ATK: " .. atkMin .. "~" .. atkMax .. " | DEF: " .. def -- 몬스터 이미지: 키.webp -- 팝업 트리거 버튼 추가 imgHtml = string.format('<div style="width:100%%;height:100%%;cursor:pointer;" risu-btn="dictSelect.sys_popup_%s"><img src="{{raw::%s.webp}}"></div>', name, name) end if category == "item" then if data.price then desc = desc .. "<br>가격: " .. data.price .. " Spirit" end -- 아이템 이미지: 키.webp (영문 키 사용) -- 팝업 트리거 버튼 추가 imgHtml = string.format('<div style="width:100%%;height:100%%;cursor:pointer;" risu-btn="dictSelect.sys_popup_%s"><img src="{{raw::%s.webp}}"></div>', name, name) end else desc = "정보를 찾을 수 없습니다." end setChatVar(triggerId, "dictTitle", title) setChatVar(triggerId, "dictDesc", desc) setChatVar(triggerId, "dictImg", imgHtml) end -- 버튼 클릭 핸들러 -- data format: "dictSelect.sys_category_name" function funcs.click(triggerId, data) local category, name = string.match(data, "dictSelect%.sys_([^_]+)_(.*)") if category == "popup" then if name == "close" then setChatVar(triggerId, "dictPopupImg", "") else -- 팝업 이미지 설정 (raw 태그 사용) local imgTag = string.format("{{raw::%s.webp}}", name) setChatVar(triggerId, "dictPopupImg", imgTag) end return end if category and name then funcs.select(triggerId, category, name) else debug("dictSelect.sys: 잘못된 데이터 포맷 - " .. tostring(data)) end end return function(triggerId, subFunc, ...) if funcs[subFunc] then return funcs[subFunc](triggerId, ...) else debug("dictSelect.sys: 알 수 없는 함수 - " .. tostring(subFunc)) end endend)()
- #8disposal.sysconstant
Keys:
-- disposal.sys.txt -- 포로 처분 시스템 (State Machine Pattern) -- sysFunction(triggerId, "disposal.sys", "main")
(function() local funcs = {}
--------------------------------------- -- Spirit 계산 함수 -- Base Spirit = floor(0.8 × Rating^1.55) -- 보너스(합연산): 어빌레벨 합계×10%, 각인레벨 합계×10% -- 배율(곱연산): 칭호, 상태, 반발각인(마지막) --------------------------------------- local function calculateSpirit(triggerId, target) debug("[Disposal:CalcSpirit] Starting calculation...") local rating = tonumber(target["레이팅"]) or 0 local baseSpirit = math.floor(0.8 * (rating ^ 1.55)) debug("[Disposal:CalcSpirit] Rating: " .. rating .. ", Base: " .. baseSpirit) -- A. 어빌리티 레벨 합계 (10% per level) local ablDB = getDB(triggerId, "abl.db") local totalAblLv = 0 for key, data in pairs(ablDB) do if data.name then totalAblLv = totalAblLv + (tonumber(target[data.name]) or 0) end end local ablBonus = totalAblLv * 0.10 debug("[Disposal:CalcSpirit] AblLv: " .. totalAblLv .. ", Bonus: " .. ablBonus) -- B. 각인 합계 (반발각인 제외, 10% per level) local posMarks = {"고통각인", "쾌락각인", "공포각인", "복종각인", "치욕각인"} local totalMarkLv = 0 for _, markName in ipairs(posMarks) do totalMarkLv = totalMarkLv + (tonumber(target[markName]) or 0) end local markBonus = totalMarkLv * 0.10 debug("[Disposal:CalcSpirit] MarkLv: " .. totalMarkLv .. ", Bonus: " .. markBonus) -- C. 칭호 배율 local multTitle = 1.0 local title = target["칭호"] or "" if title == "연모" or title == "음란" or title == "암캐" then multTitle = 1.5 elseif title == "붕괴" or title == "광기" then multTitle = 0.5 end -- D. 상태 배율 (실신 20% 차감, 사망 50% 차감) local condMult = 1.0 local cond = target["condition"] if cond == "FAINT" then condMult = 0.8 elseif cond == "DEAD" then condMult = 0.5 end -- E. 반발각인 (마지막, 곱연산, 레벨당 20% 차감) local rebelLv = tonumber(target["반발각인"]) or 0 local rebelMult = 0.8 ^ rebelLv -- 최종 계산 local totalBonus = 1 + ablBonus + markBonus local spirit = baseSpirit * totalBonus * multTitle * condMult * rebelMult spirit = math.floor(spirit) debug("[Disposal:CalcSpirit] Final Spirit: " .. spirit) -- 계산 내역 local breakdown = { base = baseSpirit, ablBonus = ablBonus, markBonus = markBonus, titleMult = multTitle, condMult = condMult, rebelMult = rebelMult, total = spirit } local breakdownText = string.format( "기본(%d) × (1+어빌%.0f%%+각인%.0f%%) × 칭호(%.1f) × 상태(%.1f) × 반발(%.2f) = %d", baseSpirit, ablBonus*100, markBonus*100, multTitle, condMult, rebelMult, spirit ) return spirit, breakdownText, breakdown end --------------------------------------- -- 조건 체크 함수 (Lua load 사용) -- condition 필드에 Lua 조건문 문자열 작성 -- 예: "return target['성별'] == '여성'" --------------------------------------- local function checkCondition(conditionStr, target) if not conditionStr or conditionStr == "" then return true -- 조건 없으면 항상 표시 end -- 조건 문자열을 함수로 컴파일 (target을 환경에 포함) local env = {target = target, tonumber = tonumber, tostring = tostring} local func, err = load(conditionStr, "condition", "t", env) if not func then debug("[Disposal:Condition] Parse error: " .. tostring(err)) return false end -- 실행하여 결과 반환 local ok, result = pcall(func) if not ok then debug("[Disposal:Condition] Eval error: " .. tostring(result)) return false end return result == true end --------------------------------------- -- [화면 진입] 처분 대상 목록 표시 --------------------------------------- function funcs.main(triggerId) debug("[Disposal:Main] Entering disposal screen") setChatVar(triggerId, "category", "index") setChatVar(triggerId, "disposal_phase", "list") setChatVar(triggerId, "targetName", "") setChatVar(triggerId, "methodList", "") setChatVar(triggerId, "disposalResult", "") screenChange(triggerId, "disposal") sysFunction(triggerId, "char.sys", "list") -- 뒤로가기 버튼 local cmds = "<div class='menu-btn' risu-btn='disposal_back'>뒤로가기</div>" setChatVar(triggerId, "cmds", cmds) reloadDisplay(triggerId) end --------------------------------------- -- [캐릭터 선택] 처분 방식 목록 표시 --------------------------------------- function funcs.selectMethod(triggerId, charName) debug("[Disposal:SelectMethod] Character selected: " .. tostring(charName)) local disposalDB = getDB(triggerId, "disposal.db") local target = json.decode(getLoreBookContent(triggerId, charName)) setChatVar(triggerId, "disposal_phase", "method") setChatVar(triggerId, "targetName", charName) -- 대상 정보 저장 setState(triggerId, "disposalTarget", target) setState(triggerId, "disposalTargetName", charName) -- 처분 방식 목록 생성 (HTML) -- condition 필드로 필터링, 버튼 ID에서 캐릭터 이름 대신 state 사용 local html = "" local methodCount = 0 local filteredCount = 0 for key, data in pairs(disposalDB) do methodCount = methodCount + 1 -- 조건 체크 if checkCondition(data.condition, target) then filteredCount = filteredCount + 1 local btn = string.format( "<div class='menu-btn' risu-btn='disposal.sys_execute_%s'><h3>%s</h3><p>%s</p></div>", key, data.name, data.desc ) html = html .. btn debug("[Disposal:SelectMethod] Added method button: " .. key) else debug("[Disposal:SelectMethod] Filtered out: " .. key) end end debug("[Disposal:SelectMethod] Total: " .. methodCount .. ", Shown: " .. filteredCount) setChatVar(triggerId, "methodList", html) -- 뒤로가기 버튼 local cmds = "<div class='menu-btn' risu-btn='disposal_back'>뒤로가기</div>" setChatVar(triggerId, "cmds", cmds) reloadDisplay(triggerId) end --------------------------------------- -- [실행] 처분 실행 및 Spirit 계산 -- 즉시 저장 방식 --------------------------------------- function funcs.execute(triggerId, methodKey) -- state에서 캐릭터 이름 가져오기 local charName = getState(triggerId, "disposalTargetName") debug("[Disposal:Execute] Method: " .. tostring(methodKey) .. ", Char: " .. tostring(charName)) if not charName then debug("[Disposal:Execute] ERROR: No target character in state!") alertNormal(triggerId, "처분 대상이 선택되지 않았습니다.") return end local disposalDB = getDB(triggerId, "disposal.db") local methodData = disposalDB[methodKey] if not methodData then debug("[Disposal:Execute] ERROR: Invalid method key: " .. tostring(methodKey)) alertNormal(triggerId, "잘못된 처분 방식입니다.") return end debug("[Disposal:Execute] Method found: " .. methodData.name) local target = json.decode(getLoreBookContent(triggerId, charName)) local cond = target["condition"] debug("[Disposal:Execute] Target condition: " .. tostring(cond)) -- 1. Spirit 계산 local spirit, breakdownText, _ = calculateSpirit(triggerId, target) debug("[Disposal:Execute] Spirit calculated: " .. spirit) -- 2. 로딩 표시 setChatVar(triggerId, "disposal_phase", "result") setChatVar(triggerId, "methodList", "") setChatVar(triggerId, "cmds", "<div class='loading-text'>처분 진행 중...</div>") reloadDisplay(triggerId) -- 3. LLM 호출 (조건부 대사 포함) local includeDialogue = (cond ~= "DEAD" and cond ~= "FAINT") local dialogueInstruction = "" if includeDialogue then dialogueInstruction = "Include a brief final quote from the character before disposal (1 sentence in Korean). Then describe the disposal result." else dialogueInstruction = "The character is unconscious or already dead, so do NOT include any dialogue. Describe only the disposal process." end debug("[Disposal:Execute] Dialogue included: " .. tostring(includeDialogue)) local prompt = { promptBuild("system", string.format([[You are an AI assistant for a dark fantasy text game.Your task is to write a short, one-paragraph description of a captured hero's execution or disposal. The tone should be dark, serious, and consistent with a Demon King's dungeon.
Disposal Method: %s Method Description: %s
%s
Keep the response within one paragraph (3-4 sentences max). Write in Korean.]], methodData.name, methodData.prompt, dialogueInstruction)), promptBuild("user", string.format("Character Name: %s\nCharacter Info: %s", charName, json.encode(target))) }
debug("[Disposal:Execute] Calling LLM...") local llmResult = LLM(triggerId, prompt, "disposal") local description = "처분이 완료되었습니다." if llmResult.success then description = llmResult.result debug("[Disposal:Execute] LLM success") else debug("[Disposal:Execute] LLM failed: " .. tostring(llmResult.result)) end -- 4. 데이터 업데이트 (즉시 저장) -- Spirit 추가 local currentSpirit = tonumber(getChatVar(triggerId, "spirit") or "0") currentSpirit = currentSpirit + spirit setChatVar(triggerId, "spirit", tostring(currentSpirit)) debug("[Disposal:Execute] Spirit updated: " .. currentSpirit) -- 캐릭터 업데이트 (condition → DEAD, 처분 필드 추가) target["condition"] = "DEAD" target["처분"] = description local option = {alwaysActive = false, insertOrder = 100, key = "", secondKey = "", regex = false} upsertLocalLoreBook(triggerId, charName, json.encode(target), option) debug("[Disposal:Execute] Character updated in lorebook") -- 5. 결과 표시 local resultHtml = string.format([[]], charName, description, spirit, breakdownText)[처분 완료] %s
%s획득 Spirit: %d
%s
setChatVar(triggerId, "disposalResult", resultHtml) setChatVar(triggerId, "cmds", "<div class='menu-btn' risu-btn='disposal_back'>확인</div>") -- 캐릭터 리스트 갱신 (처분된 캐릭터 제거) sysFunction(triggerId, "char.sys", "list") -- 6. 로그 기록 local newLog = getChatVar(triggerId, "newLog") or "" local logEntry = string.format("<div class='disposal-log'><h3>[처분] %s</h3><p>%s</p><p class='spirit-gain'>획득 Spirit: %d</p></div>", charName, description, spirit) newLog = newLog .. "<br>" .. logEntry setChatVar(triggerId, "newLog", newLog) -- state 정리 setState(triggerId, "disposalTarget", nil) setState(triggerId, "disposalTargetName", nil) debug("[Disposal:Execute] Execution complete") end --------------------------------------- -- [뒤로가기] 상태별 분기 --------------------------------------- function funcs.back(triggerId) local phase = getChatVar(triggerId, "disposal_phase") or "list" debug("[Disposal:Back] Current phase: " .. phase) if phase == "result" then -- 결과 화면 → 목록으로 복귀 debug("[Disposal:Back] Returning from result to list") funcs.main(triggerId) elseif phase == "method" then -- 처분 방식 선택 → 목록으로 복귀 (처분방법 숨김) debug("[Disposal:Back] Returning from method to list") setChatVar(triggerId, "disposal_phase", "list") setChatVar(triggerId, "targetName", "") setChatVar(triggerId, "methodList", "") setState(triggerId, "disposalTarget", nil) setState(triggerId, "disposalTargetName", nil) sysFunction(triggerId, "char.sys", "list") local cmds = "<div class='menu-btn' risu-btn='disposal_back'>뒤로가기</div>" setChatVar(triggerId, "cmds", cmds) reloadDisplay(triggerId) else -- 목록 화면 → 메인 메뉴로 debug("[Disposal:Back] Returning to main menu") screenChange(triggerId, "main") end end --------------------------------------- -- 버튼 클릭 핸들러 -- data format: "disposal.sys_execute_methodKey" (Script.lua에서 전달) --------------------------------------- function funcs.click(triggerId, data) debug("[Disposal:Click] Received: " .. tostring(data)) -- disposal.sys_execute_xxx 또는 execute_xxx 둘 다 처리 local methodKey = data:match("execute_(.+)$") if methodKey then debug("[Disposal:Click] Parsed methodKey: " .. methodKey) funcs.execute(triggerId, methodKey) else debug("[Disposal:Click] Unknown click data: " .. tostring(data)) end end return function(triggerId, subFunc, ...) debug("[Disposal:Dispatch] Called with: " .. tostring(subFunc)) if funcs[subFunc] then return funcs[subFunc](triggerId, ...) elseif subFunc and subFunc:find("execute_") then debug("[Disposal:Dispatch] Routing to click handler") funcs.click(triggerId, subFunc) else debug("[Disposal:Dispatch] Routing to click handler (fallback)") funcs.click(triggerId, subFunc) end endend)()
- #9dungeon.sysconstant
Keys:
-- dungeon.sys.txt -- 던전 관리 시스템 (배치, 확장, 시너지 계산)
(function() local funcs = {}
-- [NEW] 스킬 설명 매핑 (Loaded from skills.db) local SKILL_DESCS = {} -- 던전 확장 로직 function funcs.expand(triggerId) local dungeon = getState(triggerId, "dungeon") or {} local currentFloors = dungeon.floors or 1 if currentFloors >= 9 then alertNormal(triggerId, "더 이상 확장할 수 없습니다.") return end local cost = currentFloors * 5000 local spirit = tonumber((getChatVar(triggerId, "spirit"):gsub(",", ""))) or 0 if spirit < cost then alertNormal(triggerId, "Spirit이 부족합니다. (필요: " .. intForm(cost) .. ")") return end -- 비용 지불 및 확장 spirit = spirit - cost setChatVar(triggerId, "spirit", intForm(spirit)) dungeon.floors = currentFloors + 1 setState(triggerId, "dungeon", dungeon) alertEffect(triggerId, "던전이 " .. dungeon.floors .. "층으로 확장되었습니다!", "success") end -- 버튼 클릭 핸들러 (Script.lua 연동) function funcs.click(triggerId, data) -- growth.sys 관련 버튼 처리 if data == "growth.sys_execute" then sysFunction(triggerId, "growth.sys", "execute") return end -- 접두사 제거 ("dungeon.sys_" 제거) local cmd = data:gsub("^dungeon%.sys_", "") if cmd == "expand" then funcs.expand(triggerId) funcs.render(triggerId) -- 화면 갱신 elseif cmd:find("^tab_") then -- 탭 전환 처리 (tab_place, tab_summon, tab_throne) local category = cmd:gsub("^tab_", "") setChatVar(triggerId, "category", category) funcs.render(triggerId) elseif cmd:find("^selectSlot_") then local slotId = cmd:match("^selectSlot_(.+)") local selectedUnit = getChatVar(triggerId, "selectedUnit") if selectedUnit and selectedUnit ~= "" then -- 유닛 선택 상태: 빈 슬롯이면 배치 시도 if funcs.equip(triggerId, slotId, selectedUnit) then setChatVar(triggerId, "selectedUnit", "") setChatVar(triggerId, "selectedSlot", "") end else -- 유닛 미선택 상태: -- 1. 이미 배치된 슬롯이면 배치 해제 local dungeon = getState(triggerId, "dungeon") or {} if dungeon.grid and dungeon.grid[slotId] then funcs.unequip(triggerId, slotId) setChatVar(triggerId, "selectedSlot", "") -- 선택 해제 else -- 2. 빈 슬롯이면 선택 상태 토글 (기존 로직) local current = getChatVar(triggerId, "selectedSlot") if current == slotId then setChatVar(triggerId, "selectedSlot", "") else setChatVar(triggerId, "selectedSlot", slotId) end end end funcs.render(triggerId) elseif cmd:find("^selectUnit_") then -- "selectUnit_monster:key" 형태 local unitKey = cmd:match("^selectUnit_(.+)") local selectedSlot = getChatVar(triggerId, "selectedSlot") -- 배치 제한 및 트랩 예외 처리 local type, key = unitKey:match("^(%w+):(.+)$") local inventory = json.decode(getChatVar(triggerId, "inventory") or "{}") local owned = tonumber(inventory[unitKey]) or 0 local placed = funcs.countPlaced(triggerId, unitKey) local isFull = (type ~= "trap" and placed >= owned) if isFull then -- 이미 가득 찬 유닛을 클릭하면 배치 해제 (마지막 인스턴스) local dungeon = getState(triggerId, "dungeon") or {} local grid = dungeon.grid or {} local lastSlotId = nil -- 가장 높은 슬롯 번호(혹은 임의의) 찾기 for k, v in pairs(grid) do if v.type == type and v.key == key then lastSlotId = k end end if lastSlotId then funcs.unequip(triggerId, lastSlotId) end else -- 일반 선택 로직 if selectedSlot and selectedSlot ~= "" then if funcs.equip(triggerId, selectedSlot, unitKey) then setChatVar(triggerId, "selectedUnit", "") setChatVar(triggerId, "selectedSlot", "") end else local current = getChatVar(triggerId, "selectedUnit") if current == unitKey then setChatVar(triggerId, "selectedUnit", "") else setChatVar(triggerId, "selectedUnit", unitKey) -- 슬롯 선택이 있으면 해제? (요청사항: 몬스터>슬롯 방식이면 몬스터 강조) -- 기존 selectedSlot을 유지할지 말지는 정책 나름이나, "몬스터>슬롯" 흐름을 위해선 유지하지 않는게 깔끔할 수 있음. -- 하지만 "슬롯 선택 상태에서 몬스터만 바꾸는" 케이스도 있으므로 유지. end end end funcs.render(triggerId) end end -- 유닛 배치 로직 -- unitKey format: "type:key" (e.g., "monster:slime_minor") function funcs.equip(triggerId, slotId, unitKey) local dungeon = getState(triggerId, "dungeon") or {} dungeon.grid = dungeon.grid or {} -- ":"로 파싱하거나, 없는 경우 타입 추론 필요. 현재는 UI에서 항상 "type:key"로 보냄. local type, key = unitKey:match("^(%w+):(.+)$") if not type then return false end -- 보유량 체크 local inventory = json.decode(getChatVar(triggerId, "inventory") or "{}") local owned = tonumber(inventory[unitKey]) or 0 local placed = funcs.countPlaced(triggerId, unitKey) if type ~= "trap" and placed >= owned then alertNormal(triggerId, "보유한 수량만큼 이미 배치했습니다.") return false end dungeon.grid[slotId] = { type = type, key = key } setState(triggerId, "dungeon", dungeon) funcs.recalcStats(triggerId) -- 스탯 재계산 -- 메인 화면 던전 현황 탭 표시 setChatVar(triggerId, "show_dungeon", "1") return true end -- 배치 해제 function funcs.unequip(triggerId, slotId) local dungeon = getState(triggerId, "dungeon") or {} dungeon.grid = dungeon.grid or {} if dungeon.grid[slotId] then dungeon.grid[slotId] = nil setState(triggerId, "dungeon", dungeon) funcs.recalcStats(triggerId) end end -- 현재 배치된 특정 유닛 수량 카운트 function funcs.countPlaced(triggerId, targetKey) local dungeon = getState(triggerId, "dungeon") or {} local grid = dungeon.grid or {} local count = 0 -- targetKey format: "type:key" or just "key" (if type known) -- safe check local tType, tKey = targetKey:match("^(%w+):(.+)$") if not tType then return 0 end for k, v in pairs(grid) do if v.type == tType and v.key == tKey then count = count + 1 end end return count end -- 시너지 및 스탯 재계산 (핵심 로직) function funcs.recalcStats(triggerId) local dungeon = getState(triggerId, "dungeon") or {} local grid = dungeon.grid or {} local synergyDB = getDB(triggerId, "synergy.db") local monomerDB = getDB(triggerId, "monsters.db") local trapDB = getDB(triggerId, "trap.db") -- 1. 그리드 데이터 강화 (태그, 스탯 등 미리 로드하여 접근성 향상) local richGrid = {} local totalRating = 0 local floorRatings = {} -- 층별 레이팅 저장 local floorCount = dungeon.floors or 1 for f=1, floorCount do floorRatings[f] = 0 end if dungeon.grid then for k, v in pairs(grid) do local data = (v.type == "monster" and monomerDB[v.key]) or (v.type == "trap" and trapDB[v.key]) if data then richGrid[k] = { slot = k, type = v.type, key = v.key, tags = data.tags or {}, rating = data.rating or 0, data = data, -- 편의 메서드 hasTag = function(self_obj, t) for _, tag in ipairs(self_obj.tags) do if tag == t then return true end end return false end } totalRating = totalRating + (data.rating or 0) -- 층별 합계 계산 local f, s = k:match("^(%d+)_(%d+)$") if f then f = tonumber(f) if floorRatings[f] then floorRatings[f] = floorRatings[f] + (data.rating or 0) end end end end end -- 2. 스킬 및 시너지 엔진 (New Skill Engine) local skills = {} -- skills.sys 로드 (sysFunction 사용) local skills = sysFunction(triggerId, "skills.sys", "load") or {} local buffDB = getDB(triggerId, "buff.db") local buffQueue = {} -- {targetSlot, buffId, sourceSlot} local activeSynergies = {} local floorSynergies = {} for f = 1, floorCount do floorSynergies[f] = {} end local modifiers = {} -- battle.sys에 넘길 스탯 보정치 -- (A) 스킬 실행 (Buff Collection) for slotId, unit in pairs(richGrid) do if unit.data and unit.data.skills then for _, skillId in ipairs(unit.data.skills) do local skillObj = skills[skillId] if skillObj and skillObj.active then -- 스킬 실행 (skills.sys의 execute로 위임) local resultBuffs = skills.execute(skillId, triggerId, slotId, richGrid, unit) if resultBuffs then for _, b in ipairs(resultBuffs) do table.insert(buffQueue, b) end end -- 활성 시너지 목록 (일단 생략 or resultBuffs 있을때만) if resultBuffs and #resultBuffs > 0 then -- activeSynergies[skillId] = true end end end end end -- (B) 버프 적용 (Stat Calculation Pipeline) -- Pipeline: (Base + Add) * Mul for _, request in ipairs(buffQueue) do local targetUnit = richGrid[request.target] local buffData = buffDB[request.buff] if targetUnit and buffData then -- Modifier 초기화 if not modifiers[request.target] then modifiers[request.target] = {} end local mod = modifiers[request.target] -- Buff Normalization: Convert legacy single-stat buff to effects list local effects = {} if buffData.effects then effects = buffData.effects else -- Legacy or Single (with support for multiple stats in 'stat' field from previous patch) effects = { buffData } end for _, eff in ipairs(effects) do if eff.type == "stat" then -- Handle 'stat' potentially being a table (from previous multi-stat patch) or string local targetStats = {} if type(eff.stat) == "table" then targetStats = eff.stat else targetStats = { eff.stat } end local method = eff.method local val = tonumber(eff.value) or 0 -- Modifier Apply for _, statName in ipairs(targetStats) do if not mod[statName] then mod[statName] = {add=0, mul=1.0} end if method == "add" then mod[statName].add = mod[statName].add + val elseif method == "mul" then mod[statName].mul = mod[statName].mul * val end end end end -- UI용 버프 텍스트 추가 (Only once per buff, not per effect) if not targetUnit.buffs then targetUnit.buffs = {} end table.insert(targetUnit.buffs, buffData.name) -- 시너지 텍스트 기록 local fIdx = tonumber(request.target:match("^(%d+)_")) if fIdx then -- 중복 방지 로직이 없어서 단순 추가만 함 -- table.insert(floorSynergies[fIdx], buffData.name) end end end -- (C) 최종 스탯 반영 및 레이팅 재계산 -- 기존 totalRating, floorRatings 초기화 (버프 후 재계산) totalRating = 0 for f=1, floorCount do floorRatings[f] = 0 end for slotId, unit in pairs(richGrid) do local mod = modifiers[slotId] local ratingBonus = 0 if mod then for stat, values in pairs(mod) do if unit.data[stat] then local base = unit.data[stat] local final = (base + values.add) * values.mul unit.data[stat] = math.floor(final) -- 정수화 -- 레이팅 보정 (LvUp.sys 공식 적용) local diff = unit.data[stat] - base -- Rating Factors (Synced with LvUp.sys.txt -> balancing.db) -- [Load Balancing DB] - Optimization: Load once outside loop if possible, but safe here local balanceDB = getDB(triggerId, "balancing.db") local rConfig = balanceDB.rating or { hp=0.3, def=4.5, atkMin=4.0, atkMax=2.7 } local factor = 0 if stat == "maxHp" then factor = (rConfig.hp or 0.3) elseif stat == "def" then factor = (rConfig.def or 4.5) elseif stat == "atkMin" then factor = (rConfig.atkMin or 4.0) elseif stat == "atkMax" then factor = (rConfig.atkMax or 2.7) end ratingBonus = ratingBonus + (diff * factor) end end end -- 레이팅 업데이트 (버프 반영) if unit.data and unit.data.rating then unit.data.baseRating = unit.data.rating -- 원본 저장 unit.data.rating = unit.data.rating + math.floor(ratingBonus) totalRating = totalRating + unit.data.rating -- 층별 합계 local f, s = slotId:match("^(%d+)_(%d+)$") if f then f = tonumber(f) if floorRatings[f] then floorRatings[f] = floorRatings[f] + unit.data.rating end end end end -- (D) 결과 포매팅 (UI용) -- floorSynergies를 순회하며 텍스트 정리 -- modifiers를 ChatVar에 저장하여 battle.sys가 사용하도록 함 setChatVar(triggerId, "dungeon_modifiers", json.encode(modifiers)) -- [UI Display] 버프 상태 저장 (Renderer용) local uiBuffs = {} for slotId, unit in pairs(richGrid) do local info = {} if unit.buffs and #unit.buffs > 0 then info.buffs = unit.buffs end -- 변경된 레이팅 정보도 함께 저장 if unit.data.baseRating and unit.data.rating ~= unit.data.baseRating then info.baseRating = unit.data.baseRating info.newRating = unit.data.rating end if next(info) then uiBuffs[slotId] = info end end setChatVar(triggerId, "dungeon_buff_UI", json.encode(uiBuffs)) -- 결과 저장 setChatVar(triggerId, "totalRating", intForm(totalRating)) -- activeSynergies 구성을 위해 버프 기반 텍스트 생성 -- (상세 구현은 생략, 필요시 추가) return floorSynergies, floorRatings end -- (Helper) 던전 그리드 HTML 생성 function funcs.getGridHtml(triggerId, readOnly) local dungeon = getState(triggerId, "dungeon") or { floors = 1, grid = {} } local floorSyns, floorRatings = funcs.recalcStats(triggerId) -- 툴팁용 DB 로드 local monsters = getDB(triggerId, "monsters.db") local traps = getDB(triggerId, "trap.db") local floorHtml = "" local floorCount = dungeon.floors or 1 for f = 1, floorCount do -- 시너지 텍스트 생성 local synText = "" if floorSyns[f] and #floorSyns[f] > 0 then synText = table.concat(floorSyns[f], "<br>") else synText = "<span style='color:#555'>-</span>" end -- 층별 래퍼 시작 floorHtml = floorHtml .. string.format([[]], floorRatingText, synText)%dF]], f, f)for s = 1, 5 do local slotId = string.format("%d_%d", f, s) local content = dungeon.grid[slotId] local inner = '<div class="slot-empty">+</div>' local selectedClass = "" local btnAttr = "" local inlineStyle = "" local uiBuffs = json.decode(getChatVar(triggerId, "dungeon_buff_UI") or "{}") local buffInfo = uiBuffs[slotId] -- Buff Visuals (Golden Glow from Bottom) local buffEffectStyle = "" if buffInfo and buffInfo.buffs and #buffInfo.buffs > 0 then -- 골드 하이라이트 (바닥 강조) buffEffectStyle = "box-shadow: inset 0 -10px 15px -5px rgba(255, 215, 0, 0.5); border-bottom: 3px solid #FFD700;" end -- 툴팁 생성 local tooltipAttr = "" if content then local name = "" local rating = 0 if content.type == "monster" and monsters[content.key] then name = monsters[content.key].name rating = monsters[content.key].rating elseif content.type == "trap" and traps[content.key] then name = traps[content.key].name rating = traps[content.key].rating end if name ~= "" then local ratingStr = string.format("★%d", rating) if buffInfo and buffInfo.newRating and buffInfo.newRating ~= rating then local diff = buffInfo.newRating - rating if diff > 0 then ratingStr = string.format("★%d ▲%d", rating, diff) else ratingStr = string.format("★%d ▼%d", rating, math.abs(diff)) end end local tip = string.format("%s (%s)", name, ratingStr) if buffInfo and buffInfo.buffs and #buffInfo.buffs > 0 then tip = tip .. "\n[Active Buffs]" for _, bName in ipairs(buffInfo.buffs) do tip = tip .. "\n- " .. bName end end tooltipAttr = string.format('title="%s"', tip) end end if readOnly then -- 읽기 전용 모드 if content then inner = string.format('<img src="{{raw::%s}}" %s>', content.key, tooltipAttr) selectedClass = "occupied" inlineStyle = "cursor:default;" .. buffEffectStyle else inner = '<div class="slot-empty" style="opacity:0.3">+</div>' end else -- 관리 모드 if getChatVar(triggerId, "selectedSlot") == slotId then selectedClass = "selected" inner = inner .. "<div style='position:absolute; top:0; left:0; width:100%; height:100%; border:2px solid #60d9f7; box-shadow:0 0 8px rgba(96, 217, 247, 0.5); pointer-events:none; box-sizing:border-box;'></div>" end if content then inner = string.format('<img src="{{raw::%s}}" %s>', content.key, tooltipAttr) selectedClass = selectedClass .. "occupied" inlineStyle = buffEffectStyle end btnAttr = string.format('risu-btn="dungeon.sys_selectSlot_%s"', slotId) end floorHtml = floorHtml .. string.format([[%s]], selectedClass, inlineStyle, btnAttr, inner) end-- 래퍼 종료 및 시너지 섹션 추가 -- 층별 레이팅 표시 (시너지 영역) local floorRatingText = "" if floorRatings[f] and floorRatings[f] > 0 then floorRatingText = string.format(" <span style='font-size:0.7em; color:#fbc02d;'>(Total: %s)</span>", intForm(floorRatings[f])) end floorHtml = floorHtml .. string.format([[Synergy%s%s-- 확장 버튼 (관리 모드 && 마지막층 && 최대층 미만) if not readOnly and (f == dungeon.floors and f < 9) then floorHtml = floorHtml .. string.format([[확장 ({{getvar::expandCost}} Sp)]]) end end-- readOnly 모드(메인 화면)일 때 상단에 Total Rating 표시 추가 if readOnly then local totalRating = getChatVar(triggerId, "totalRating") or "0" local topStats = string.format([[]], totalRating) floorHtml = topStats .. floorHtml endRating 합계: %sreturn floorHtml end -- [NEW] 전투용 던전 현황 HTML (Compact, Red Highlights, Timed Animation) function funcs.getBattleStatusHtml(triggerId, clearedSlots, timeline, turnInterval) local dungeon = getState(triggerId, "dungeon") or { floors = 1, grid = {} } -- 타임라인을 검색에 용이하게 변환 (Visit Times for slots) -- 여기서는 '언제' Active가 되어야 하는지, '얼마나' 유지되는지 계산 local slotEvents = {} for idx, event in ipairs(timeline or {}) do if event.slotId then local nextEvent = timeline[idx + 1] local duration = turnInterval * 5 -- 기본 5턴분 지속 if nextEvent and nextEvent.startTurnIndex then duration = (nextEvent.startTurnIndex - event.startTurnIndex) * turnInterval end if not slotEvents[event.slotId] then slotEvents[event.slotId] = {} end table.insert(slotEvents[event.slotId], { delay = event.startTurnIndex * turnInterval, duration = duration }) end end local floorHtml = '<div style="display:flex; flex-direction:column; gap:4px; width:100%;">' local floorCount = dungeon.floors or 1 for f = 1, floorCount do floorHtml = floorHtml .. string.format('<div style="display:flex; align-items:center; gap:4px;"><div style="font-size:0.7rem; color:#60d9f7; width:20px; text-align:right;">%dF</div>', f) floorHtml = floorHtml .. '<div style="display:flex; flex:1; gap:2px; height:30px;">' for s = 1, 5 do local slotId = string.format("%d_%d", f, s) local content = dungeon.grid[slotId] local isCleared = clearedSlots[slotId] local inner = '' local style = 'flex:1; background:#000; border:1px solid #333; display:flex; justify-content:center; align-items:center; position:relative; overflow:hidden;' local imgStyle = 'width:100%; height:100%; object-fit:cover;' -- 돌파된 슬롯 처리 (붉은 배경) -> 애니메이션으로 처리 local maxEventTime = 0.0 if slotEvents[slotId] then for _, ev in ipairs(slotEvents[slotId]) do local endTime = ev.delay + ev.duration if endTime > maxEventTime then maxEventTime = endTime end end end if isCleared then -- style = style .. 'background:rgba(211, 47, 47, 0.4); border-color:#d32f2f;' (기존 정적 스타일 제거) if not content then -- 컨텐츠가 없으면(함정 발동후 소멸 등) X 표시도 딜레이 적용 inner = inner .. string.format('<div class="cleared-overlay" style="animation-delay:%.2fs; display:flex; justify-content:center; align-items:center;"><span style="color:#d32f2f; font-size:1.2rem;">/</span></div>', maxEventTime) else -- 컨텐츠가 있으면 오버레이만 inner = inner .. string.format('<div class="cleared-overlay" style="animation-delay:%.2fs;"></div>', maxEventTime) end end -- Timed Highlights if slotEvents[slotId] then for _, ev in ipairs(slotEvents[slotId]) do -- ev = { delay = ..., duration = ... } inner = inner .. string.format('<div class="active-marker" style="animation-delay:%.2fs; animation-duration:%.2fs;"></div>', ev.delay, ev.duration) end end if content then -- 몬스터/함정 아이콘 if isCleared then imgStyle = imgStyle .. 'filter:grayscale(100%) opacity(0.7);' end if content.key then inner = inner .. string.format('<img src="{{raw::%s}}" style="%s">', content.key, imgStyle) end end floorHtml = floorHtml .. string.format('<div style="%s">%s</div>', style, inner) end floorHtml = floorHtml .. '</div></div>' end floorHtml = floorHtml .. '</div>' return floorHtml end -- 던전 화면 렌더링 (HTML 생성 -> ChatVar 설정) function funcs.render(triggerId) local dungeon = getState(triggerId, "dungeon") or {} if not dungeon.floors then dungeon.floors = 1 dungeon.grid = {} setState(triggerId, "dungeon", dungeon) end local expandCostBase = getChatVar(triggerId, "expandCostBase") or 5000 setChatVar(triggerId, "dungeonFloors", dungeon.floors) setChatVar(triggerId, "expandCost", intForm(dungeon.floors * expandCostBase)) setChatVar(triggerId, "category", getChatVar(triggerId, "category") or "place") local html = funcs.getGridHtml(triggerId, false) setChatVar(triggerId, "floorHtml", html) -- 소환 투자금 & 미리보기 계산 (기존 유지) if not getChatVar(triggerId, "summonInvest") then setChatVar(triggerId, "summonInvest", 2500) end local investStr = intForm(getChatVar(triggerId, "summonInvest")) setChatVar(triggerId, "summonInvestDisplay", investStr) local invest = tonumber(getChatVar(triggerId, "summonInvest")) or 2500 local maxRating = math.floor(math.sqrt(invest * 5)) setChatVar(triggerId, "summonMaxRating", maxRating) -- 소환 후보군 미리보기용 로직 local monsters = getDB(triggerId, "monsters.db") local inventory = json.decode(getChatVar(triggerId, "inventory") or "{}") local candidates = {} for k, v in pairs(monsters) do local qty = tonumber(inventory["monster:"..k]) or 0 if qty == 0 and v.rating <= maxRating then table.insert(candidates, {name=v.name, rating=v.rating}) end end -- 레이팅 내림차순 정렬 table.sort(candidates, function(a, b) return a.rating > b.rating end) local previewHtml = "" if #candidates > 0 then local limit = 4 for i = 1, math.min(#candidates, limit) do local c = candidates[i] previewHtml = previewHtml .. string.format([[%s (★%d)]], c.name, c.rating) endif #candidates > limit then previewHtml = previewHtml .. '<span style="color:#888; font-size:0.8rem; margin-left:4px;">... 등 ' .. (#candidates) .. '종</span>' else previewHtml = previewHtml .. '<span style="color:#888; font-size:0.8rem; margin-left:4px;">총 ' .. (#candidates) .. '종</span>' end else previewHtml = '<div style="color:#e57373; font-size:0.8rem;">소환 가능한 몬스터가 없습니다.</div>' end setChatVar(triggerId, "summonPreviewHtml", previewHtml) -- 유닛 리스트 HTML 생성 (placement 탭일 때) -- 유닛 리스트 HTML 생성 local category = getChatVar(triggerId, "category") if category == "throne" then sysFunction(triggerId, "growth.sys", "render") setChatVar(triggerId, "monsterListHtml", "") setChatVar(triggerId, "trapListHtml", "") setChatVar(triggerId, "unitCount", 0) elseif category == "place" then local inventory = json.decode(getChatVar(triggerId, "inventory") or "{}") local monsters = getDB(triggerId, "monsters.db") local traps = getDB(triggerId, "trap.db") -- [NEW] Lua 기반 스킬 DB 로드 (via getDB) SKILL_DESCS = getDB(triggerId, "skills.db") local count = 0 -- 몬스터 리스트 local monHtml = "" for k, v in pairs(monsters) do -- 인벤토리를 순회하는게 아니라 전체 DB를 순회하므로 인벤토리 확인 필요 -- inventory 키는 "monster:k" local unitKey = "monster:"..k local qty = tonumber(inventory[unitKey]) or 0 if qty > 0 then count = count + 1 local selectedClass = "" local btnAttr = string.format('risu-btn="dungeon.sys_selectUnit_%s"', unitKey) local opacityStyle = "" -- 배치 제한 체크 (함정은 무제한) local placed = funcs.countPlaced(triggerId, unitKey) local isFull = false if placed >= qty then selectedClass = selectedClass .. " disabled" isFull = true opacityStyle = "opacity:0.5; filter:grayscale(100%); cursor:not-allowed;" -- 클릭 이벤트는 유지하되 click 핸들러에서 처리 else if getChatVar(triggerId, "selectedUnit") == unitKey then selectedClass = "selected" opacityStyle = opacityStyle .. " border:2px solid #60d9f7; box-shadow:0 0 5px rgba(96, 217, 247, 0.5);" end end -- [NEW] 스킬 툴팁 및 마커 생성 local markerHtml = "" local tooltip = string.format("%s (★%d)", v.name, v.rating) if v.skills and #v.skills > 0 then local descList = {} local hasPlacementSkill = false for _, s in ipairs(v.skills) do if SKILL_DESCS[s] then hasPlacementSkill = true local sInfo = SKILL_DESCS[s] -- sInfo is table {name="...", desc="..."} or nil if sInfo and sInfo.name then table.insert(descList, string.format("- %s: %s", sInfo.name, sInfo.desc)) else table.insert(descList, "- " .. s) end else table.insert(descList, "- " .. s) end end if #descList > 0 then tooltip = tooltip .. "\n[보유 스킬]\n" .. table.concat(descList, "\n") end if hasPlacementSkill then -- 역삼각형 마커 (상단 중앙) - 크기 2배(1.1rem), 글로우 효과 추가 markerHtml = '<div style="position:absolute; top:-5px; left:50%; transform:translateX(-50%); color:#FFD700; font-size:1.5rem; text-shadow:0 0 10px #FFD700, 0 0 10px #FFA500, 0 3px 5px yellow; z-index:10;">🔻</div>' end end local finalTooltip = string.format('title="%s"', tooltip) monHtml = monHtml .. string.format([[]], selectedClass, opacityStyle, finalTooltip, btnAttr, k, v.name, v.rating, placed, qty, markerHtml) end end[image]%s%d/%d%s-- 함정 리스트 local trapHtml = "" for k, v in pairs(traps) do -- 함정 보유 체크 (접두사 있거나 없거나) local hasTrap = false local qty = 0 local unitKey = "trap:"..k -- 1. trap:접두사 체크 if tonumber(inventory[unitKey]) and tonumber(inventory[unitKey]) > 0 then hasTrap = true qty = tonumber(inventory[unitKey]) end -- 2. 접두사 없는 경우 체크 (우선순위 낮음 or 합산? 현재는 단순 체크) if not hasTrap and tonumber(inventory[k]) and tonumber(inventory[k]) > 0 then hasTrap = true qty = tonumber(inventory[k]) -- 인벤토리 키 포맷 통일 필요할 수 있음. 일단 unitKey는 trap:k로 고정 end if hasTrap then count = count + 1 local selectedClass = "" local btnAttr = string.format('risu-btn="dungeon.sys_selectUnit_%s"', unitKey) local opacityStyle = "" -- 배치 제한 체크 (함정은 무제한) local placed = funcs.countPlaced(triggerId, unitKey) -- 함정은 예외이므로 제한 체크 없음 if getChatVar(triggerId, "selectedUnit") == unitKey then selectedClass = "selected" opacityStyle = opacityStyle .. " border:2px solid #60d9f7; box-shadow:0 0 5px rgba(96, 217, 247, 0.5);" end trapHtml = trapHtml .. string.format([[]], selectedClass, opacityStyle, btnAttr, k, v.name, v.rating, placed, qty) end end[image]%s%d/%dsetChatVar(triggerId, "monsterListHtml", monHtml) setChatVar(triggerId, "trapListHtml", trapHtml) setChatVar(triggerId, "unitCount", count) end -- 커맨드 버튼 (뒤로가기) setChatVar(triggerId, "cmds", "<div class='btn command-btn' risu-btn='199'>뒤로가기</div>") end --------------------------------------- -- 메인 함수 --------------------------------------- return function(triggerId, subFunc, ...) if funcs[subFunc] then return funcs[subFunc](triggerId, ...) end endend)()
- #10getMark.sysconstant
Keys:
--[[ 각인 획득 시스템 (리팩토링 버전) 반복되는 각인 처리 로직을 데이터 기반으로 통합 ]] function (triggerId, stats, target) -- mark.db 직접 로드 제거: 내부 정의 사용 local EXPtbl = getDB(triggerId, "EXPtable.db") local target = target local stats = stats local markBase = 25 --각인 획득 베이스 수치 local markSum = 0 --모든 각인의 합계 local screenEffect = ""
--[[ 각인 정의 테이블 name: 각인명 icon: 이모지 아이콘 isNegative: 네거티브 각인 여부 (반발, 고통, 공포) calculator: maxP 계산 함수 ]] local markDefinitions = { { name = "반발각인", icon = "💢", isNegative = true, calculator = function(s) return math.max(s["저항"] or 0, ((s["수치"] or 0) + (s["고통"] or 0) + (s["불쾌"] or 0)) * 0.1 + (s["저항"] or 0) * 0.5) end }, { name = "고통각인", icon = "🪱", isNegative = true, calculator = function(s) return math.max(s["고통"] or 0, ((s["공포"] or 0) + (s["불쾌"] or 0)) * 0.15 + (s["고통"] or 0) * 0.5) end }, { name = "공포각인", icon = "🩻", isNegative = true, calculator = function(s) return math.max(s["공포"] or 0, ((s["굴복"] or 0) + (s["고통"] or 0) + (s["불쾌"] or 0)) * 0.1 + (s["공포"] or 0) * 0.5) end }, { name = "복종각인", icon = "🙏", isNegative = false, calculator = function(s) return math.max(s["온순"] or 0, ((s["욕정"] or 0) + (s["굴복"] or 0)) * 0.15 + (s["온순"] or 0) * 0.5) end }, { name = "치욕각인", icon = "⛓️💥", isNegative = false, calculator = function(s) return math.max(s["수치"] or 0, (s["굴복"] or 0) * 0.4 + (s["수치"] or 0) * 0.4) end }, { name = "쾌락각인", icon = "⭐", isNegative = false, calculator = function(s) return math.max( s["C쾌락"] or 0, s["V쾌락"] or 0, s["B쾌락"] or 0, s["A쾌락"] or 0, s["U쾌락"] or 0, s["M쾌락"] or 0, s["S쾌락"] or 0, ((s["C쾌락"] or 0) + (s["V쾌락"] or 0) + (s["B쾌락"] or 0) + (s["A쾌락"] or 0)) * 0.2 ) end } } --각인레벨 합계 도출 (정의된 각인 목록 기준) for _, def in ipairs(markDefinitions) do if tonumber(target[def.name]) and tonumber(target[def.name]) > 0 then markSum = markSum + 1 end end --[[ 각인 처리 함수 한 턴에 하나의 각인만 획득 가능 ]] local function processMark(markDef) local currentLevel = tonumber(target[markDef.name]) or 0 if currentLevel >= 3 then return false --이미 최대 레벨 end local maxP = markDef.calculator(stats) local cssClass = "neggative" -- CSS 클래스 통일 (기존 코드 유지) --레벨업 판정 (한번에 최대 3까지 가능) local newLevel = currentLevel -- EXPtbl 키가 문자열인지 숫자인지 확인 필요하나 일단 tostring 유지 local threshold2 = markBase * (EXPtbl[tostring(markSum + 2)] or 99999) local threshold1 = markBase * (EXPtbl[tostring(markSum + 1)] or 99999) local threshold0 = markBase * (EXPtbl[tostring(markSum)] or 99999) if maxP > threshold2 then newLevel = 3 elseif maxP > threshold1 then newLevel = math.min(currentLevel + 2, 3) elseif maxP > threshold0 then newLevel = currentLevel + 1 end if newLevel > currentLevel then target[markDef.name] = tostring(newLevel) screenEffect = "<div class='getMark'><div class='getMarkText " .. cssClass .. "'>" .. markDef.icon .. " " .. markDef.name .. " Lv." .. newLevel .. " 획득!!</div></div>" return true --각인 획득 성공 end return false end --모든 각인에 대해 순차적으로 처리 for _, markDef in ipairs(markDefinitions) do if processMark(markDef) then return target, screenEffect --하나의 각인을 얻으면 다른 각인은 얻을 수 없음 end end return target, screenEffectend
- #11growth.sysconstant
Keys:
-- growth.sys.txt -- 유저(마왕) 성장 시스템 (Spirit 소모 -> 레벨업)
(function() local funcs = {}
-- [설정] 교환비 계산 (1 EXP당 필요 Spirit) -- 초반(Lv1) 15 -> 후반(Lv15+) 5으로 점진적 감소 local function getExchangeRate(level) local discount = math.floor((level - 1) / 2) local rate = math.max(5, 15 - discount) return rate end -- 레벨업 비용 계산 -- return: canLevelUp(bool), cost(int), nextLevel(int), currentEXP(int), requiredEXP(int) function funcs.calcCost(triggerId) local userStr = getChatVar(triggerId, "user") or "{}" local user = json.decode(userStr) local lv = tonumber(user["레벨"]) or 1 local exp = tonumber(user["경험치"]) or 0 -- 다음 레벨 요구 경험치 조회 (LvUp.sys.check 로직 활용) -- LvUp.sys의 check 함수는 (성공여부, 현재EXP, 필요EXP)를 반환 -- 하지만 여기서는 '다음 레벨이 되기 위해 필요한 EXP량'이 필요함. -- LvUp.sys 내부 로직: requiredEXP = EXPtable[Lv + 3] local expTable = getDB(triggerId, "EXPtable.db") local nextLv = lv + 1 local reqKey = tostring(lv + 3) -- EXP_LEVEL_OFFSET = 3 가정 local reqTotalEXP = expTable[reqKey] if not reqTotalEXP then -- 만렙 도달 return false, 0, lv, exp, 0 end local neededEXP = reqTotalEXP - exp if neededEXP < 0 then neededEXP = 0 end -- 이미 경험치가 넘치면 0 local rate = getExchangeRate(lv) local cost = neededEXP * rate -- 100 단위 반올림 (Round) -- x + 50 / 100 * 100 cost = math.floor((cost + 50) / 100) * 100 return true, cost, nextLv, exp, reqTotalEXP, rate end -- 레벨업 실행 function funcs.execute(triggerId) local can, cost, nextLv, curExp, reqExp, rate = funcs.calcCost(triggerId) if not can then alertNormal(triggerId, "이미 최고 레벨에 도달했습니다.") return end local spirit = tonumber((getChatVar(triggerId, "spirit"):gsub(",", ""))) or 0 if spirit < cost then alertNormal(triggerId, "영혼(Spirit)이 부족합니다. (필요: " .. intForm(cost) .. ")") return end -- 자원 소모 spirit = spirit - cost setChatVar(triggerId, "spirit", intForm(spirit)) -- 유저 데이터 업데이트 (EXP 채우기 & 레벨업) local userStr = getChatVar(triggerId, "user") or "{}" local user = json.decode(userStr) -- 경험치를 요구량만큼 채움 (혹은 그 이상) user["경험치"] = tostring(reqExp) -- LvUp.sys.execute 호출하여 실제 스탯 상승 및 레벨 증가 처리 -- 주의: LvUp.sys는 target 테이블을 직접 수정함. -- LvUp.sys는 "레벨"도 1 증가시킴. local updatedUser, effectHtml, gained = sysFunction(triggerId, "LvUp.sys", "execute", user, 1, false) -- 1업, silent=false(효과텍스트 받음) -- 저장 setChatVar(triggerId, "user", json.encode(updatedUser)) -- [FIX] Lorebook에 저장 (turnEnd시 초기화 방지) upsertLocalLoreBook(triggerId, "user", json.encode(updatedUser), DEFAULT_LORE_OPTION) -- 효과 출력 (화면 갱신은 호출측에서 담당하거나 여기서 refresh) -- 알현실 UI 갱신을 위해 render 호출 권장 funcs.render(triggerId) -- 시스템 메시지 (로그) debug(string.format("마왕이 Lv.%d로 성장했습니다! (소모 Spirit: %s)", updatedUser["레벨"], intForm(cost), rate)) -- 화면 효과 (LvUp.sys에서 생성된 HTML을 screenEffect 변수에 추가하여 메인화면 갱신시 표시) -- 현재 구조상 즉시 표시하려면 별도 처리가 필요할 수 있음. -- 여기서는 일단 메시지만 띄우고, render에서 UI가 갱신됨. local existEffect = getChatVar(triggerId, "screenEffect") or "" setChatVar(triggerId, "screenEffect", existEffect .. effectHtml) end -- 버튼 클릭 핸들러 function funcs.click(triggerId, data) if data == "growth.sys_execute" then funcs.execute(triggerId) end end -- 알현실(Throne Room) UI 렌더링 -- dungeon.sys에서 호출됨 (category='throne'일 때) function funcs.render(triggerId) local userStr = getChatVar(triggerId, "user") or "{}" local user = json.decode(userStr) local spirit = tonumber((getChatVar(triggerId, "spirit"):gsub(",", ""))) or 0 local lv = tonumber(user["레벨"]) or 1 local atk = user["공격력"] or "0" -- min~max local def = user["방어력"] or 0 local hp = user["최대체력"] or 0 local sp = user["최대기력"] or 0 local rating = user["레이팅"] or 0 local can, cost, nextLv, curExp, reqExp, rate = funcs.calcCost(triggerId) local btnClass = "btn command-btn" local btnText = "성장 불가" local btnAttr = "" if can then btnText = string.format("레벨업!! (Lv.%d→%d)<span>필요 Spirit: %s</span>", lv, nextLv, intForm(cost)) if spirit >= cost then btnAttr = "risu-btn='growth.sys_execute'" -- execute 호출 else btnClass = btnClass .. " disabled" btnText = btnText .. "<br><span style='color:#e57373'>(Spirit 부족)</span>" end else btnClass = btnClass .. " disabled" btnText = "최고 레벨 도달" end -- HTML 조립 local html = string.format([[]], user["이름"], lv, user["칭호"] or "", intForm(rating), atk, def, intForm(hp), intForm(sp), btnClass, btnAttr, btnText)마왕 %s Lv.%d %sRating %sATK: %sDEF: %sHP: %sSP: %s[image]setChatVar(triggerId, "throneHtml", html) end -- 메인 함수 return function(triggerId, subFunc, ...) if funcs[subFunc] then return funcs[subFunc](triggerId, ...) end endend)()
- #12initVars.sysconstant
Keys:
function (triggerId) --초기 변수 설정 local option = {alwaysActive = false, insertOrder = 100, key = "", secondKey = "", regex = false} --로어북 저장용 옵션 local initVars = { spirit = "10,000", day = "1", ampm = "0", --0은 낮 1은 밤 dungeonLv = "1", --던전레벨 1로 시작 category = "user", chars = '["user","마오"]', --리수배열 스타일에 맞게 저장 trainDifficulty = "보통", --조교난이도 보통 blockDeath = 1, --사망방지 O ageLimit = "13", --등장 용사 나이제한 13세 ntrLimit = "1", --ntr금지 On futaLimit = "0", --후타용사 등장 X maleLimit = "0", --남성용사 등장 X raceLimit = "1", --인외종족 등장 0:모두, 1:인간만 trainTargetPanel = "1", --조교화면 대상정보 상단고정 O trainUserPanel = "0", --조교화면 유저정보 상단고정 X combatDifficulty = "보통", --전투난이도 보통 heroLvLimit = "5", --용사의 레벨 제한 showLog = "none", --과거 조교기록 보기. 기본값 OFF debug = DEBUG, --콘솔 디버그 표시 minSummonInvest = 2500, --몬스터 소환비용 최소치 summonInvestChange = 500, --몬스터 소환비용 최소변동치 expandCostBase = 10000, --던전 확장비용 기본값 king = '{"성별":"남성","페니스":"대물","음모":"무성함", "외형":"중년","헤어스타일":"날렵하게 세워올린 짧은 은발"}', --광왕 디폴트 설정 models = '{"trainResponse":"0","trainLog":"1", "newChar":"0", "battleNarration":"1", "disposal":"0"}', --상황별 사용 모델 설정. 0:보조모델, 1:메인모델
--[진행도 관련 변수] trainCount = 0, --조교 횟수 defenseCount = 0, --방어 성공 횟수 --[해금 플래그] (0: 잠김, 1: 해금됨) unlock_dungeon = 0, --던전 관리 unlock_shop = 0, --마계 상점 unlock_invasion = 0, --침공 unlock_disposal = 0, --포로 처분 --[메뉴 표시 관련] use_dictionary = 0, --마계사전 활성화 (0: Off, 1: On) unlock_cheat_menu = 0, --치트 메뉴 활성화 (0: Locked, 1: Unlocked) show_dungeon = 0, --던전 현황 탭 표시 (0: Hidden, 1: Visible) --[시스템 설정] llmRetries = 3, --LLM 재시도 횟수 } for key, value in pairs(initVars) do setChatVar(triggerId, key, value) end --인벤토리 초기화 local inventory = {} local items = getDB(triggerId, "items.db") for cat, list in pairs(items) do for k, v in pairs(list) do inventory[k] = 0 end end -- trap.db의 함정들도 인벤토리에 추가 local traps = getDB(triggerId, "trap.db") for k, v in pairs(traps) do inventory[k] = 0 end stateToVar(triggerId, "inventory", inventory) --수정구 기록 초기화 upsertLocalLoreBook(triggerId, "crystalRec", "{}", option) --user 및 고유캐릭터 정보를 로컬 로어북에 저장 for _, v in ipairs(json.decode(initVars.chars)) do --리수배열 스타일에 맞게 저장 local content = json.decode(getLoreBookContent(triggerId, v..".init")) -- 대상 캐릭터의 로어북을 불러와 테이블로 저장 --캐릭터에는 정의가 필요한 부분만 들어있음. 캐릭터 시트에 없는 trait, trace등의 필드를 db에서 읽어와서 추가 --새 구조에서는 mark.db, abl.db, exp.db, trace.db의 키 값을 전부 0으로 초기화해서 추가 local dbNames = {"mark.db", "abl.db", "exp.db", "trace.db"} local added = "" for _, dbName in ipairs(dbNames) do local dbData = getDB(triggerId, dbName) for key, val in pairs(dbData) do -- DB의 한글 이름(name 필드)을 키로 사용 if val.name then local korKey = val.name if not content[korKey] then content[korKey] = "0" added = added .. ", ".. korKey end else -- name 필드가 없으면 키 자체를 사용 (비상시) if not content[key] then content[key] = "0" added = added .. ", ".. key end end end end debug(v.."의 캐릭터 테이블에 미정의 필드 추가") --캐릭터 정보를 로컬로어북에 저장 upsertLocalLoreBook(triggerId, v, json.encode(content), option) end --유저 정보를 user변수에 테이블로 할당 local user = json.decode(getLoreBookContent(triggerId, "user")) stateToVar(triggerId, "user", user) --########################### --DB 로딩 및 리스트 생성 (JSON 기반 + 정렬) --########################### -- DB 로딩 및 리스트 생성 (JSON 기반 + 정렬) -- useName: true이면 한글 이름(v.name)을 리스트에 저장, false/nil이면 영문 키(k)를 저장 local function createListFromDB(dbName, useName) local db = getDB(triggerId, dbName) local list = {} for k, v in pairs(db) do if useName and v.name then table.insert(list, v.name) else table.insert(list, k) end end table.sort(list) debug("chatVar: " .. dbName.." 리스트 생성") return list end -- 1. 단순 구조 DB - 키(Key)를 사용하는 리스트 -- race, class, monster는 RNG나 시스템 내부에서 키로 접근함 local keyListDBs = { ["race"] = "race.db", ["class"] = "class.db", ["monsters"] = "monsters.db" } for name, filename in pairs(keyListDBs) do if name == "monsters" then -- 몬스터는 DB 파일의 순서(Rating순)를 유지하기 위해 텍스트 파싱 local raw = getLoreBookContent(triggerId, filename) local list = {} -- Lua 키 패턴 매칭: key = { for key in raw:gmatch('([%w_]+)%s*=%s*{') do table.insert(list, key) end setChatVar(triggerId, "monstersList", json.encode(list)) debug("chatVar: monstersList 생성 (정렬 보존)") else local list = createListFromDB(filename, false) setChatVar(triggerId, name.."List", json.encode(list)) end end -- 2. 단순 구조 DB - 이름(Name)을 사용하는 리스트 -- abl, exp, mark, stat은 캐릭터 데이터에 한글 이름으로 저장되고 HTML에서 순회함 local nameListDBs = { ["abl"] = "abl.db", ["exp"] = "exp.db", ["mark"] = "mark.db", ["stat"] = "stat.db" } for name, filename in pairs(nameListDBs) do local list = createListFromDB(filename, true) setChatVar(triggerId, name.."List", json.encode(list)) end -- traceList (trace.db 기반 한글 이름 리스트) local traceDB = getDB(triggerId, "trace.db") local traceList = {} for k, v in pairs(traceDB) do if v.name then table.insert(traceList, v.name) end end table.sort(traceList) setChatVar(triggerId, "traceList", json.encode(traceList)) debug("chatVar: traceList 생성") -- 2. Items & Traps (카테고리 분류) - 아이템은 영문 키 유지 (상점에서 키 사용) local itemsDB = getDB(triggerId, "items.db") local trapDB = getDB(triggerId, "trap.db") local toolsList = {} local consumablesList = {} local unlockList = {} local trapsList = {} local itemNameMap = {} -- items.db는 [카테고리][키] 구조임 for category, items in pairs(itemsDB) do if category == "tools" then for k, v in pairs(items) do table.insert(toolsList, k) if v.name then itemNameMap[k] = v.name end end elseif category == "consumables" then for k, v in pairs(items) do table.insert(consumablesList, k) if v.name then itemNameMap[k] = v.name end end elseif category == "unlock" then for k, v in pairs(items) do table.insert(unlockList, k) if v.name then itemNameMap[k] = v.name end end end end for k, v in pairs(trapDB) do table.insert(trapsList, k) if v.name then itemNameMap[k] = v.name end end table.sort(toolsList); table.sort(consumablesList); table.sort(unlockList); table.sort(trapsList) setChatVar(triggerId, "toolsList", json.encode(toolsList)) debug("chatVar: toolsList 생성") setChatVar(triggerId, "consumablesList", json.encode(consumablesList)) debug("chatVar: consumablesList 생성") setChatVar(triggerId, "unlockList", json.encode(unlockList)) debug("chatVar: unlockList 생성") setChatVar(triggerId, "trapsList", json.encode(trapsList)) debug("chatVar: trapsList 생성") setChatVar(triggerId, "itemNameMap", json.encode(itemNameMap)) debug("chatVar: itemNameMap 생성") -- 3. Trait (평탄화된 구조에서 카테고리 분류) - 특성은 한글 이름 사용 (캐릭터 데이터 키) local traitDB = getDB(triggerId, "trait.db") local traitLists = { ["성격"] = {}, ["신체특성"] = {}, ["성적취향"] = {}, ["칭호"] = {} } for k, v in pairs(traitDB) do if v.category and traitLists[v.category] then table.insert(traitLists[v.category], v.name) -- 한글 이름 저장 end end for k, list in pairs(traitLists) do table.sort(list) setChatVar(triggerId, k.."List", json.encode(list)) debug("chatVar: " .. k.."List 생성") end addChat(triggerId, "user", "{{".."getvar::html".."}}")end
- #13abl.dbconstant
Keys:
-- (Lua Config Style) -- abl{어빌} c_sensibility = { name = "C민감도", desc = "클리토리스의 민감도. 높을수록 쾌감을 느끼기 쉽다.", events = { onStatEXP = { func = function(e) if e.statName == 'C쾌락' then e.statEXP = e.statEXP * (1 + e.level * 0.1) end end } } } b_sensibility = { name = "B민감도", desc = "가슴 및 유두의 민감도. 높을수록 쾌감을 느끼기 쉽다.", events = { onStatEXP = { func = function(e) if e.statName == 'B쾌락' then e.statEXP = e.statEXP * (1 + e.level * 0.1) end end } } } v_sensibility = { name = "V민감도", desc = "질의 민감도. 높을수록 쾌감을 느끼기 쉽다.", events = { onStatEXP = { func = function(e) if e.statName == 'V쾌락' then e.statEXP = e.statEXP * (1 + e.level * 0.1) end end } } } a_sensibility = { name = "A민감도", desc = "애널의 민감도. 높을수록 쾌감을 느끼기 쉽다.", events = { onStatEXP = { func = function(e) if e.statName == 'A쾌락' then e.statEXP = e.statEXP * (1 + e.level * 0.1) end end } } } u_sensibility = { name = "U민감도", desc = "요도의 민감도. 높을수록 쾌감을 느끼기 쉽다. 요도성감 trait을 가진 경우에만 활성화됩니다.", events = { onStatEXP = { func = function(e) if e.statName == 'U쾌락' then e.statEXP = e.statEXP * (1 + e.level * 0.1) end end } } } m_sensibility = { name = "M민감도", desc = "목구멍의 민감도. 높을수록 쾌감을 느끼기 쉽다. 인후성감 trait을 가진 경우에만 활성화됩니다.", events = { onStatEXP = { func = function(e) if e.statName == 'M쾌락' then e.statEXP = e.statEXP * (1 + e.level * 0.1) end end } } } v_expand = { name = "V확장", desc = "질의 확장정도, 높을수록 더 큰 물건을 받아들일 수 있지만 질삽입으로인한 쾌감을 느끼기 어려워집니다." } a_expand = { name = "A확장", desc = "애널의 확장정도, 높을수록 더 큰 물건을 받아들일 수 있지만 애널삽입으로인한 쾌감을 느끼기 어려워집니다." } u_expand = { name = "U확장", desc = "요도의 확장정도, 높을수록 더 큰 물건을 받아들일 수 있지만 요도삽입으로인한 쾌감을 느끼기 어려워집니다." } obedience = { name = "순응", desc = "{{user}}의 명령에 잘 따르는 정도. 높을수록 각종 조교 커맨드의 성공확률이 증가한다.", events = { onDCCalc = { func = function(e) e.dc = e.dc - e.level * 0.5 end } } } desire = { name = "성욕", desc = "쾌락을 갈구하는 정도. 높을수록 각종 조교 커맨드의 성공확률이 증가한다.", events = { onDCCalc = { func = function(e) e.dc = e.dc - e.level * 0.3 end } } } service = { name = "봉사기술", desc = "말그대로 봉사기술. 높을수록 봉사관련 조교의 효과가 증가한다.", events = { onStatEXP = { func = function(e) if e.statName == '봉사기술' then e.statEXP = e.statEXP * (1 + e.level * 0.1) end end } } } sex_technique = { name = "성교기술", desc = "말그대로 섹스기술. 높을수록 삽입관련 조교의 효과가 증가한다.", events = { onStatEXP = { func = function(e) if e.statName == '성교기술' then e.statEXP = e.statEXP * (1 + e.level * 0.1) end end } } } speech = { name = "화술", desc = "대화를 이끌어나가는 기술. 높을수록 음탕한 말로 상대를 유혹하는데 익숙해진다." } exhibitionism = { name = "노출벽", desc = "자신의 부끄러운 부위 혹은 행위를 보여지면서 쾌감을 느끼는 정도. 높을수록 수치를 느낄때 얻는 쾌감이 증가하고 부정은 감소한다.", events = { onStatEXP = { func = function(e) if e.statName == '수치' then e.statEXP = e.statEXP * (1 + e.level * 0.1) end end } } } masochism = { name = "마조끼", desc = "육체적 고통을 느낄때 쾌감을 느끼는 정도. 높을수록 고통을 느낄때 얻는 쾌감이 증가하고 부정은 감소한다.", events = { onStatEXP = { func = function(e) if e.statName == '고통' then e.statEXP = e.statEXP * (1 + e.level * 0.1); e.allStatEXP['S쾌락'] = (e.allStatEXP['S쾌락'] or 0) + e.statEXP * e.level * 0.5 end end }, onOrgasmCalc = { hasTags = { "pain" }, func = function(e) e.orgasm = e.orgasm + e.level * 100 end } } }
- #14balancing.dbconstant
Keys:
-- balancing.db (Lua Game Balance Constants)
rating = { hp = 0.3, def = 4.5, atkMin = 4.0, atkMax = 2.7 }
- #15buff.dbconstant
Keys:
-- Buff DB (Lua Config Style)
basic_atk_up = { name = "공격력 증가", effects = { { type="stat", stat="atkMin", value=1, method="add" } }, desc = "최소 공격력이 1 증가합니다." }
basic_def_up = { name = "방어력 증가", effects = { { type="stat", stat="def", value=1, method="add" } }, desc = "방어력이 1 증가합니다." }
horde_mentality = { name = "군단 본능", effects = { { type="stat", stat="atkMin", value=2, method="add" } }, desc = "최소 공격력이 2 증가합니다." }
fire_empower = { name = "화염 강화", effects = { { type="stat", stat="atkMin", value=3, method="add" } }, desc = "최소 공격력이 3 증가합니다." }
rage_mode = { name = "광폭화", effects = { { type="stat", stat="atkMin", value=1.5, method="mul" } }, desc = "최소 공격력이 50% 증가합니다." }
elite_def_boost = { name = "정예 방어", effects = { { type="stat", stat="def", value=10, method="add" } }, desc = "방어력이 10 증가합니다." }
weak_link_boost = { name = "약점 보완", effects = { { type="stat", stat="def", value=15, method="add" } }, desc = "방어력이 15 증가합니다." }
vanguard_def_up = { name = "선봉 방어", effects = { { type="stat", stat="def", value=5, method="add" } }, desc = "방어력이 5 증가합니다." }
abyss_power = { name = "심연의 힘", effects = { { type="stat", stat="atkMin", value=2, method="add" } }, desc = "최소 공격력이 2 증가합니다." }
rear_snipe = { name = "후방 지원", effects = { { type="stat", stat="atkMax", value=4, method="add" } }, desc = "최대 공격력이 4 증가합니다." }
undead_march_buff = { name = "망자의 행진", effects = { { type="stat", stat="atkMin", value=2, method="add" } }, desc = "최소 공격력이 2 증가합니다." }
swarm_def_up = { name = "밀집 대형", effects = { { type="stat", stat="def", value=3, method="add" } }, desc = "방어력이 3 증가합니다." }
magic_resonance = { name = "마력 공명", effects = { { type="stat", stat="atkMax", value=5, method="add" } }, desc = "최대 공격력이 5 증가합니다." }
ambush_bonus = { name = "기습 우위", effects = { { type="stat", stat="atkMin", value=4, method="add" } }, desc = "최소 공격력이 4 증가합니다." }
lonely_strength = { name = "고독한 힘", effects = { { type="stat", stat="atkMin", value=10, method="add" }, { type="stat", stat="atkMax", value=15, method="add" } }, desc = "최소 공격력이 10, 최대 공격력이 15 증가합니다." }
poison_assist = { name = "독성 지원", effects = { { type="stat", stat="atkMin", value=2, method="add" } }, desc = "최소 공격력이 2 증가합니다." }
flying_squad_buff = { name = "비행 편대", effects = { { type="stat", stat="def", value=2, method="add" } }, desc = "방어력이 2 증가합니다." }
gatekeeper_def = { name = "철벽 수비", effects = { { type="stat", stat="def", value=8, method="add" } }, desc = "방어력이 8 증가합니다." }
sacrifice_power = { name = "희생의 힘", effects = { { type="stat", stat="atkMin", value=5, method="add" } }, desc = "최소 공격력이 5 증가합니다." }
dark_shroud = { name = "어둠의 장막", effects = { { type="stat", stat="def", value=2, method="add" } }, desc = "방어력이 2 증가합니다." }
beast_howl_buff = { name = "야수의 포효", effects = { { type="stat", stat="atkMax", value=3, method="add" } }, desc = "최대 공격력이 3 증가합니다." }
- #16class.dbconstant
Keys:
-- (Lua Config Style) -- class demon_king = { name = "마왕", desc = "모든 마족의 정점에 선 절대자. 압도적인 힘을 자랑한다.", hp_inc = 225, sp_inc = 150, def_inc = 16, atk_min = 6, atk_max = 10, weapon = "마왕의 검", armor = "마왕의 갑주" } swordsman = { name = "검사", desc = "검을 다루는 전사. 공수 밸런스가 잡혀있다.", hp_inc = 130, sp_inc = 100, def_inc = 8, atk_min = 2, atk_max = 10, weapon = "롱소드", armor = "가죽 갑옷" } knight = { name = "기사", desc = "갑옷으로 무장한 전사. 방어력이 뛰어나다.", hp_inc = 150, sp_inc = 80, def_inc = 11, atk_min = 1, atk_max = 6, weapon = "랜스", armor = "플레이트 메일" } mage = { name = "마법사", desc = "마나를 다룰 줄 아는 마법사. 체력은 약하지만 기력이 높다.", hp_inc = 90, sp_inc = 200, def_inc = 3, atk_min = 4, atk_max = 18, weapon = "지팡이", armor = "로브" } priest = { name = "성직자", desc = "신을 섬기는 사제. 회복 능력과 굳건한 정신력을 가졌다.", hp_inc = 180, sp_inc = 150, def_inc = 6, atk_min = 1, atk_max = 8, weapon = "메이스", armor = "사제복" } thief = { name = "도적", desc = "민첩한 몸놀림을 가진 무법자. 공격력이 높지만 맷집이 약하다.", hp_inc = 100, sp_inc = 120, def_inc = 4, atk_min = 4, atk_max = 15, weapon = "단검", armor = "천옷" } archer = { name = "궁수", desc = "원거리 공격에 능한 사수.", hp_inc = 110, sp_inc = 120, def_inc = 4, atk_min = 3, atk_max = 16, weapon = "활", armor = "가죽 조끼" } berserker = { name = "광전사", desc = "분노를 힘으로 바꾸는 전사. 방어를 도외시하고 공격에 치중한다.", hp_inc = 150, sp_inc = 60, def_inc = 2, atk_min = 2, atk_max = 16, weapon = "대검", armor = "경갑" } paladin = { name = "성기사", desc = "신성한 힘으로 아군을 보호하는 기사.", hp_inc = 130, sp_inc = 100, def_inc = 10, atk_min = 1, atk_max = 9, weapon = "워해머", armor = "전신 판금 갑옷" } assassin = { name = "암살자", desc = "어둠 속에 숨어 적의 급소를 노리는 살수.", hp_inc = 90, sp_inc = 130, def_inc = 3, atk_min = 4, atk_max = 17, weapon = "카타르", armor = "검은 타이즈" } hunter = { name = "사냥꾼", desc = "야생의 짐승을 사냥하는 전문가.", hp_inc = 140, sp_inc = 100, def_inc = 5, atk_min = 2, atk_max = 13, weapon = "장궁", armor = "가죽 갑옷" } monk = { name = "무도가", desc = "극한의 신체를 단련한 격투가.", hp_inc = 160, sp_inc = 100, def_inc = 5, atk_min = 2, atk_max = 10, weapon = "너클", armor = "도복" }
- #17condition.dbconstant
Keys:
-- Condition Database with English Keys -- name: Display Name in Korean -- desc: Tooltip Description return { REST = { name="정비중", desc="던전을 공략하기 위해 마을에서 재정비 중인 상태" }, TRAINABLE = { name="조교가능", desc="대상 캐릭터가 던전에서 대기중이며 조교 가능한 상태." }, INVASION = { name="침입", desc="마왕의 던전에 침입한 상태." }, FAINT = { name="실신", desc="가혹한 전투 또는 조교로 인해 정신을 잃은 상태." }, DEAD = { name="사망", desc="이 용사는 사망했습니다." } }
- #18disposal.dbconstant
Keys:
-- (Lua Config Style) release_alive = { condition = "return target.condition ~= 'DEAD'", name = "석방", desc = "용사의 기억을 지워 던전 밖으로 내던집니다.", prompt = "용사의 기억을 지우고 알몸인 채로 던전 밖에서 깨어나도록 합니다. 용사는 곧 무방비하게 야생동물들에게 잡아먹히거나, 운 좋게 인간에게 발견되더라도 자아를 잃은 채 무참히 유린당하다 처참한 최후를 맞이할 것입니다." }
release_dead = { condition = "return target.condition == 'DEAD'", name = "인간 표지판", desc = "용사의 시체를 기괴한 자세로 결박하여 마을 어귀에 전시합니다.", prompt = "알몸으로 두 다리를 치욕스럽게 벌린 채 결박된 용사의 시체를 마을 어귀에 효시합니다. 수평으로 길게 뻗은 한쪽 다리는 던전 방향을 가리키는 이정표가 되며, 입은 억지로 벌려져 구걸하는 듯한 비참한 형상을 띱니다." }
cannibalism = { name = "식육형", desc = "용사를 살아있는 채로 몬스터들의 식재료로 사용합니다.", prompt = "용사에게 고통을 느낄 수 없는 저주를 겁니다. 용사는 맨정신으로 자신의 장기와 팔다리가 몬스터들에게 뜯겨나가고 요리되는 과정을 맑은 정신으로 지켜보며 영원한 고통에 빠집니다." }
burning = { condition = "return target.condition ~= 'DEAD'", name = "화형", desc = "용사를 불태워 처형하며 공포의 본보기를 보여줍니다.", prompt = "말뚝에 묶인 채 발끝부터 타오르는 불길 속에 던져집니다. 살점이 타들어 가는 지독한 악취와 절규가 공포를 자아내며, 용사의 강인했던 육체는 서서히 검게 그을린 재로 스러집니다." }
taxidermy = { name = "박제형", desc = "용사를 음란한 포즈 그대로 고정하여 영구히 보존합니다.", prompt = "용사를 음란한 포즈로 박제합니다. 네발로 엎드려 암캐 자세로 다리를 든 채 배설감을 견디는 표정, M자 다리로 자위하며 쾌락에 찌든 아헤가오 등을 연출합니다. 특수 방부 처리를 거친 피부는 생생한 홍조를 띠며, 눈동자는 구경꾼과 시선이 맞닿도록 고정되어 영구적인 수치를 줍니다." }
petrification = { name = "석화형", desc = "용사를 음란한 포즈의 석상으로 만들어 정원 장식물로 삼습니다.", prompt = "용사를 가장 수치스러운 자세로 고정시킨 뒤 마법으로 석화시킵니다. 자신의 손가락으로 치부를 벌리거나 기구에 의해 몸이 뒤틀린 찰나의 순간이 차가운 돌 속에 갇히게 됩니다. 이 석상은 마왕성 정원에 배치되어 몬스터들의 배설물과 침을 받아내는 장식품으로 전락합니다." }
painting_female = { condition = "return target['성별'] == '여성'", name = "회화형", desc = "용사를 살아있는 그림 세상 안에 봉인하여 영원히 유린합니다.", prompt = "용사를 캔버스 안의 그림으로 봉인합니다. 정적인 그림처럼 보이지만 그 안에서 용사는 시간의 흐름 없이 끝없이 몰려드는 촉수와 몬스터들에게 온몸의 구멍을 유린당하며 쾌락과 절망의 비명을 지릅니다." }
painting_male = { condition = "return target['성별'] == '남성'", name = "회화형", desc = "용사를 살아있는 그림 세상 안에 봉인하여 영원히 착정합니다.", prompt = "용사를 살아있는 그림 속에 가둡니다. 그림 속 세상에서 용사는 사지가 묶인 채 수많은 몬스터와 촉수에게 끊임없이 정기를 착정당하며, 죽고 싶어도 죽지 못한 채 영원히 마왕군의 마력원으로 쓰이게 됩니다." }
painting_futa = { condition = "return target['성별'] == '후타나리'", name = "회화형", desc = "용사를 그림 세상에 가두어 양성 모두를 유린합니다.", prompt = "그림 세상 속에서 용사는 모든 성기 부위가 촉수에게 점거당한 채 강간당하는 동시에 정기를 착정당합니다. 비정상적으로 부풀어 오르는 복부와 쾌락에 뒤틀린 표정이 캔버스 위에 생생하게 영구히 박제됩니다." }
onahole_female = { condition = "return target['성별'] == '여성'", name = "사지절단 오나홀형", desc = "용사의 사지를 절단하여 살아있는 생체 오나홀로 만듭니다.", prompt = "용사의 팔다리를 절단하고 목소리를 빼앗아 오직 성적인 기능만 남은 고깃덩이로 만듭니다. 목구멍과 보지, 애널이 모두 몬스터들의 배설과 성욕 처리를 위한 소모품으로 쓰이며, 자아는 사라진 채 오직 자극에 반응하는 구멍으로서만 존재하게 됩니다." }
onahole_male = { condition = "return target['성별'] == '남성'", name = "사지절단 딜도형", desc = "용사의 사지를 절단하고 살아있는 인간 딜도로 만듭니다.", prompt = "용사의 팔다리를 절단하고 자지를 비정상적으로 비대화시켜 살아있는 성기구로 개조합니다. 몬스터들의 욕구 해소를 위해 강제로 발기 상태가 유지되며, 자아를 잃은 채 사정만 반복하는 비참한 기구로 전락합니다." }
onahole_futa = { condition = "return target['성별'] == '후타나리'", name = "사지절단 섹스토이형", desc = "용사의 사지를 절단하여 모든 구멍을 활용하는 장난감으로 만듭니다.", prompt = "사지가 제거된 채 다목적 성기구로 개조됩니다. 앞뒤의 구멍은 오나홀로, 비대해진 자지는 딜도로 사용되며 모든 방향에서 마물들의 성욕을 받아내는 기괴한 장난감이 됩니다." }
- #19equipments.dbconstant
Keys:
--equipment check
return { ["꿀단지웜"] = "미착용", ["애널웜"] = "미착용", ["애널플러그"] = "미착용", ["애널비즈"] = "미착용", ["눈가리개"] = "미착용", ["볼개그"] = "미착용", ["O링개그"] = "미착용", ["점액달팽이"] = "미착용", ["우유달팽이"] = "미착용", ["로프"] = "미착용", ["착유기"] = "미착용", ["개목걸이"] = "미착용" }
- #20exp.dbconstant
Keys:
-- (Lua Config Style) v_exp = {name = "V경험", desc = "질삽입 관련 경험을 한 회수"} a_exp = {name = "A경험", desc = "애널 관련 경험을 한 회수"} c_exp = {name = "C경험", desc = "클리 관련 경험을 한 회수"} b_exp = {name = "B경험", desc = "가슴 및 유두관련 경험을 한 회수"} orgasm_exp = {name = "절정경험", desc = "절정을 경험한 회수"} ejaculation_exp = {name = "사정경험", desc = "사정을 경험한 회수"} sex_exp = {name = "성교경험", desc = "오럴, 애널을 포함해 삽입섹스를 가진 회수"} anal_sex_exp = {name = "애널성교경험", desc = "애널을 이용한 삽입섹스를 경험한 회수"} masturbation_exp = {name = "자위경험", desc = "스스로를 만지며 자위한 회수"} semen_exp = {name = "정액경험", desc = "정액을 질 또는 애널로 받아내거나 마신 회수"} fellatio_exp = {name = "펠라경험", desc = "펠라치오를 한 회수"} service_exp = {name = "봉사경험", desc = "상대방의 기쁨을 위해 봉사한 회수"} shame_exp = {name = "수치경험", desc = "수치스러운 조교를 경험한 회수"} love_exp = {name = "애정경험", desc = "사랑스러운 경험을 한 회수"} masochist_exp = {name = "피학경험", desc = "체벌, 고문, 매도 등 신체적 정신적으로 괴롭힘 당한 회수"} sadist_exp = {name = "가학경험", desc = "체벌, 고문, 매도 등 신체적 정신적으로 괴롭힌 회수"} drink_semen_exp = {name = "정음경험", desc = "정액을 마신 회수"} dry_orgasm_exp = {name = "비접촉절정", desc = "성감대에 직접적인 자극 없이 절정을 경험한 회수"} denied_orgasm_exp = {name = "절정컨트롤", desc = "절정에 이르지 못하고 제한당한 회수"} urine_exp = {name = "방뇨경험", desc = "공공장소에서 또는 다른 사람 앞에서 오줌싸는 모습을 보인 회수"} urethra_exp = {name = "요도경험", desc = "요도 관련된 경험을 한 회수"} scat_exp = {name = "오물경험", desc = "배설물로 인하여 더러워진 경험의 회수"} lesbian_exp = {name = "레즈경험", desc = "여성끼리의 쾌락을 탐미한 회수"} bl_exp = {name = "BL경험", desc = "남성끼리의 쾌락을 탐미한 회수"} bondage_exp = {name = "긴박경험", desc = "도구나 힘에 의해 강제로 속박당해본 회수"} v_expand_exp = {name = "V확장경험", desc = "질의 확장과 관련된 경험을 한 회수"} a_expand_exp = {name = "A확장경험", desc = "애널의 확장과 관련된 경험을 한 회수"} lactation_exp = {name = "분유경험", desc = "모유가 흘러나오는 경험을 한 회수"} tentacle_exp = {name = "촉수경험", desc = "촉수를 이용한 조교를 경험한 회수"} beast_exp = {name = "수간경험", desc = "개, 돼지, 말 등 4족 보행하는 동물과 성행위를 한 회수"} drug_exp = {name = "약물경험", desc = "약물을 마시거나 주입당한 회수"} birth_exp = {name = "출산경험", desc = "마족의 알이나 새끼를 포함해 출산을 경험한 회수."} faint_exp = {name = "조교실신경험", desc = "조교중 체력이 다해 실신한 회수"} mad_king_exp = {name = "광왕조교경험", desc = "광왕으로부터 조교받은 회수"} record_exp = {name = "피사경험", desc = "수정구에 성행위가 기록된 회수"} talk_exp = {name = "조교회화경험", desc = "조교간에 조교자와 대화를 나눈 회수"} prostitution_exp = {name = "매춘경험", desc = "돈을 벌기 위해 몸을 판 회수"} battle_exp = {name = "전투경험", desc = "마족 또는 다른 용사와 전투를 경험한 회수"} train_count = {name = "조교회수", desc = "{{user}}의 조교를 경험한 회수"}
- #21EXPtable.dbconstant
Keys:
--자연상수 지수함수 테이블. 스탯치를 모아 지수로 나타내는 지수연산은 자원을 많이 먹으므로 스탯변화량/미리 테이블로 설정된 로그값 확률적으로 레벨업 하도록 구성
return { ["0"] = 3, ["1"] = 7, ["2"] = 20, ["3"] = 55, ["4"] = 148, ["5"] = 403, ["6"] = 1097, ["7"] = 2981, ["8"] = 8103, ["9"] = 22026, ["10"] = 59874, ["11"] = 162755, ["12"] = 442413, ["13"] = 1202604, ["14"] = 3269017, ["15"] = 8886111, ["16"] = 24154953, ["17"] = 65659969, ["18"] = 178482301, ["19"] = 485165195, ["20"] = 1318815734 }
- #22items.dbconstant
Keys:
-- (Lua Config Style) tools = { heart_stone = { name = "심장석", desc = "마계 화산지대에서 발견되는 차가운 돌. 따뜻한 곳에 닿으면 진동하는 특징을 이용해 클리나 유두를 자극하기 좋다.", price = 200 }, honey_worm = { name = "꿀단지웜", desc = "마계온천에 서식하는 벌레. 질에 들어가면 살아있는 딜도처럼 꿈틀대며 숙주를 흥분시킨다. 만월에는 구멍안에 산란하는 습성이 있다.", price = 500 }, anal_worm = { name = "애널웜", desc = "꿀담지 웜의 아종. 질보다는 애널 같은 더러운 곳을 좋아한다. 마찬가지로 안에서 살아있는 딜도처럼 꿈틀대며 숙주를 흥분시킨다. 만월에는 구멍안에 산란하는 습성이 있다.", price = 5000 }, anal_plug = { name = "애널플러그", desc = "마이코니드 버섯의 갓 부분을 잘라내어 만든 애널 장식품.", price = 8000 }, anal_beads = { name = "애널비즈", desc = "둥근 보석이 여러개 연결된 애널 장식품.", price = 7000 }, blindfold = { name = "눈가리개", desc = "가고일 가죽으로 만든 눈가리개. 착용자가 직접 벗을 수 없는 마법이 걸려있다.", price = 4000 }, ball_gag = { name = "볼개그", desc = "나무와 가죽으로 만들어진 입마개.", price = 1000 }, ring_gag = { name = "O링개그", desc = "금속으로 만들어진 링이 달려있어 입을 다물 수 없게 된다.", price = 4000 }, slime_snail = { name = "점액달팽이", desc = "마계의 민달팽이. 음핵이나 귀두에 붙여두면 지속적인 자극을 가한다.", price = 2000 }, milk_snail = { name = "우유달팽이", desc = "점액달팽이의 아종으로 유두에 붙어 자극을 가한다.", price = 3000 }, whip = { name = "채찍", desc = "그림자 거미의 실을 이어만든 채찍. 단단하면서도 유연하다.", price = 200 }, rope = { name = "로프", desc = "서큐버스의 머리카락을 꼬아만든 속박용 로프.", price = 3000 }, enema = { name = "관장기", desc = "커다란 주사기 형태의 관장기구.", price = 6000 }, -- milker = {name = "착유기", desc = "시체꽃의 꽃봉우리로 만든 착유기구.", price = 1500}, 임신시스템 구현 후 임신한 상태에서만 사용가능하게 할 것 collar = { name = "개목걸이", desc = "새끼 헬하운드를 위해 만들어진 개목걸이. 물론 헬하운드만 쓰라는 법은 없다.", price = 5000 } } consumables = { -- condom = {name = "콘돔", desc = "유령꽃의 포자낭으로 만든 일회용 피임도구. 매우 얇고 부드러워 크기에 무관하게 착용하지 않은듯한 느낌을 준다.", price = 100}, 임신시스템 구현 후 추가 oil = { name = "오일", desc = "슬라임을 녹여 만든 끈적한 오일. 대단히 미끈거린다.", price = 200 }, crystal_ball = { name = "수정구", desc = "주변의 풍경을 기록할 수 있는 수정구.", price = 1500 }, nutrient = { name = "영양제", desc = "만드라고라를 갈아 만든 약. 체력회복에 효과적이다.", price = 1500 }, incense = { name = "향", desc = "바포메트의 사향주머니에서 채취한 향. 기분을 안정시키고 기력을 회복시킨다.", price = 3000 }, -- diuretic = {name = "이뇨제", desc = "사막의 으로 만든 약으로 소변을 유도하는 효과가 있다.", price = 500}, 요의시스템 구현 후 추가 aphrodisiac = { name = "미약", desc = "환각나비의 날개가루로 만든 약. 복용시 성감대가 활성화되며 쾌감이 증폭된다.", price = 2000 }, dewormer = { name = "구충제", desc = "채내에 기생하는 기생충을 제거하는 약.", price = 500 }, hair_tonic = { name = "강모제", desc = "사티로스의 갈기털로 만든 약. 음모가 빠르게 자라는 효과가 있다.", price = 5000 }, ovulation_inducer = { name = "배란유발제", desc = "마계의 그리마 알로 만든 약. 배란을 유도하는 효과가 있다.", price = 8000 } } unlock = { unlock1 = { name = "조합지식", desc = "다양한 약품을 조제할 수 있다.", price = 15000 }, unlock2 = { name = "마족정보", desc = "강력한 몬스터들에 대한 정보가 담긴 책. 상급 몬스터들을 소환 할 수 있다.", price = 50000 }, unlock3 = { name = "음마지식", desc = "마계의 다양한 지식이 담긴 책. 실험실을 만들어 다양한 실험을 할 수 있다.", price = 100000 }, unlock4 = { name = "금단의지식", desc = "금단의 지식이 담긴 책. 조교시 촉수를 소환할 수 있다.", price = 1000000 } }
- #23mark.dbconstant
Keys:
-- (Lua Config Style) -- mark rebel_mark = { name = "반발각인", desc = "{{user}}에 대한 강한 반발심이 치솟을때 획득. 단계가 높을수록 명령을 잘 따르지 않고 거세게 반항해 조교가 어려워진다.", events = { onDCCalc = { func = function(e) e.dc = e.dc + e.level * 5 end } } } pain_mark = { name = "고통각인", desc = "견디기 힘든 큰 고통을 겪었을때 획득. 단계가 높을수록 한번에 큰 고통을 받았음을 의미한다. 대상은 강한 고통을 피하고자 하기 때문에 명령에 순응할 확률이 높아진다.", events = { onDCCalc = { func = function(e) e.dc = e.dc - e.level * 2 end } } } fear_mark = { name = "공포각인", desc = "트라우마가 될만큼 강한 공포를 느꼈을 때 획득. 단계가 높을수록 {{user}}에 대한 공포심이 강하기 때문에 명령을 거스르려는 마음이 약해진다.", events = { onDCCalc = { func = function(e) e.dc = e.dc - e.level * 2 end } } } submit_mark = { name = "복종각인", desc = "반발심이 한풀 꺾여 {{user}}의 명령에 순응하게 되었을 때 획득. 단계가 높을수록 {{user}}에게 더 강한 종속감을 느낀다. 반발 각인이 있으면 획득할 수 없다.", events = { onDCCalc = { func = function(e) e.dc = e.dc - e.level * 3 end } } } shame_mark = { name = "치욕각인", desc = "엄청나게 수치스러운 상황을 겪을때 획득. 스스로를 수치스럽다거나 더럽다고 느끼는 정도. 단계가 높을수록 자기 몸과 마음을 포기하기 때문에 반항하는 경향이 낮아진다.", events = { onDCCalc = { func = function(e) e.dc = e.dc - e.level * 2 end } } } pleasure_mark = { name = "쾌락각인", desc = "강렬한 쾌감을 맛보았을때 획득. 단계가 높을수록 한번에 더 큰 쾌락을 맛보았음을 의미한다. 대상은 더 강한 쾌락을 얻고자 하기 때문에 명령에 순응할 확률이 높아진다.", events = { onDCCalc = { hasTags = { "sex" }, func = function(e) e.dc = e.dc - e.level * 1 end }, onOrgasmCalc = { func = function(e) e.orgasm = e.orgasm * (1 + e.level * 0.1) end } } }
- #24monsters.dbconstant
Keys:
-- Monster DB (Lua Config Style) lemure = { name = "레무어", desc = "녹아내린 살점 덩어리 같은 형상을 한 최하급 악마. 지능이 없고 고통을 느끼지 못하며, 무기력한 손을 휘두르며 끊임없이 몰려옵니다.", rating = 30, hp = 12, atkMin = 1, atkMax = 7, def = 1, tags = { "몬스터", "하급", "악마", "물리", "무리" }, skills = {"horde_tactic", "swarm_density"} } manes = { name = "마네스", desc = "심연의 기운으로 뭉쳐진 부패한 살덩이의 악마. 약하지만 엄청난 숫자로 밀어붙이며, 죽으면 검은 안개로 흩어져 시야를 가립니다.", rating = 35, hp = 9, atkMin = 1, atkMax = 8, def = 2, tags = { "몬스터", "하급", "악마", "암흑", "무리" } } stirge = { name = "스티리지", desc = "박쥐의 날개와 모기의 주둥이를 가진 기괴한 비행 생물. 떼를 지어 날아다니며 무방비한 희생자에게 달라붙어 긴 주둥이를 꽂고 배가 터질 듯 부풀어 오를 때까지 피를 빱니다.", rating = 40, hp = 20, atkMin = 1, atkMax = 11, def = 0, tags = { "몬스터", "하급", "야수", "물리", "무리", "비행" } } giant_rat = { name = "자이언트 랫", desc = "일반적인 쥐보다 훨씬 거대하며, 시궁창에서 떼를 지어 나타납니다. 털이 곤두서 있고 눈이 붉게 충혈되어 있으며, 날카로운 앞니로 물어뜯어 상처를 입힙니다.", rating = 45, hp = 19, atkMin = 1, atkMax = 13, def = 0, tags = { "몬스터", "하급", "야수", "맹독", "무리" } } kobold = { name = "코볼트", desc = "비늘 덮인 피부와 쥐 같은 꼬리를 가진 소형 파충류 인간. 조잡한 창과 투석구를 사용하며, 정면 승부보다는 무리를 지어 적을 함정으로 유인하는 교활한 전술을 구사합니다.", rating = 50, hp = 26, atkMin = 1, atkMax = 14, def = 0, tags = { "몬스터", "하급", "인간형", "물리", "무리" }, skills = {"vanguard_tactic"} } skeleton = { name = "스켈레톤", desc = "강령술로 되살아나 뼈만 남은 상태로 움직이는 병사. 생전의 기억을 잃었지만 군대처럼 대열을 맞춰 전진하며, 낡은 검과 방패를 들고 적이 쓰러질 때까지 공격합니다.", rating = 55, hp = 21, atkMin = 1, atkMax = 14, def = 2, tags = { "몬스터", "하급", "언데드", "물리", "무리" } } goblin = { name = "고블린", desc = "녹색 피부에 날카로운 이빨을 가진 소형 인간형. 항상 떼를 지어 다니며, 톱니 모양의 낡은 시미터와 단궁으로 무장했습니다. 비겁하지만 잔인한 성격으로 약한 상대를 집요하게 괴롭힙니다.", rating = 60, hp = 32, atkMin = 1, atkMax = 17, def = 0, tags = { "몬스터", "하급", "인간형", "물리", "무리" } } blood_hawk = { name = "블러드 호크", desc = "피처럼 붉은 깃털을 가진 흉포한 매. 날카로운 부리로 눈을 노리며, 무리를 지어 자신보다 훨씬 큰 사냥감도 집요하게 공격합니다.", rating = 65, hp = 31, atkMin = 1, atkMax = 19, def = 0, tags = { "몬스터", "하급", "야수", "물리", "무리", "비행" } } zombie = { name = "좀비", desc = "부패하여 살점이 떨어져 나가는 시체. 생각 없이 무리지어 배회하며, 고통을 느끼지 않아 멈추지 않고 다가옵니다. 강한 악력으로 희생자를 붙잡아 산 채로 뜯어먹으려 합니다.", rating = 75, hp = 22, atkMin = 1, atkMax = 17, def = 4, tags = { "몬스터", "하급", "언데드", "물리", "무리" } } giant_bat = { name = "자이언트 배트", desc = "가죽 날개를 펼치면 사람을 감쌀 정도로 거대한 박쥐. 동굴 천장에 수십 마리가 매달려 있다가 어둠 속에서 초음파로 적을 감지하며 덮쳐옵니다.", rating = 75, hp = 35, atkMin = 1, atkMax = 21, def = 1, tags = { "몬스터", "하급", "야수", "물리", "무리", "비행" } } giant_centipede = { name = "자이언트 센티피드", desc = "1미터가 넘는 크기의 거대한 지네. 어둡고 축축한 곳에서 무더기로 발견되며, 수많은 다리로 벽과 천장을 빠르게 기어 다니며 적을 위협합니다.", rating = 80, hp = 34, atkMin = 1, atkMax = 23, def = 1, tags = { "몬스터", "하급", "곤충", "맹독", "무리" } } lightning_mephit = { name = "라이트닝 메피트", desc = "번개와 구름으로 이루어진 작은 여자아이 모습의 정령. 쉴 새 없이 파지직거리며 날아다니고, 전격 마법을 사용하거나 몸에서 스파크를 튀겨 충격을 줍니다.", rating = 80, hp = 34, atkMin = 1, atkMax = 23, def = 1, tags = { "몬스터", "하급", "정령", "전격", "비행" } } skeleton_archer = { name = "스켈레톤 아처", desc = "부서진 뼈로 만든 활을 든 해골 병사. 감정 없는 빈 눈구멍으로 적을 응시하며, 지치지 않는 팔로 쉴 새 없이 화살을 쏘아댑니다.", rating = 85, hp = 41, atkMin = 1, atkMax = 24, def = 1, tags = { "몬스터", "하급", "언데드", "물리" }, skills = {"rear_guard_sniping", "undead_march"} } troglodyte = { name = "트로그로다이트", desc = "지하 깊은 곳에 사는 눈 퇴화된 파충류. 부족 단위로 영역을 지키며, 주변에 견딜 수 없는 구역질 나는 악취를 풍겨 적의 전의를 상실시킵니다.", rating = 95, hp = 32, atkMin = 1, atkMax = 22, def = 5, tags = { "몬스터", "하급", "인간형", "맹독", "무리" } } imp = { name = "임프", desc = "붉은 피부와 작은 날개, 꼬리에 독침을 가진 소형 악마. 투명화하여 적의 배후를 노리거나 마법으로 환영을 만들어 혼란을 줍니다.", rating = 95, hp = 47, atkMin = 1, atkMax = 27, def = 1, tags = { "몬스터", "하급", "악마", "화염", "비행", "마법", "기습" }, skills = {"magic_resonance", "shadow_ambush"} } quasit = { name = "쿼짓", desc = "녹색 피부와 뿔을 가진 변신 능력이 있는 소형 악마. 두꺼비나 박쥐로 변해 정찰을 하며, 발톱에 묻은 독은 극심한 고통을 유발합니다.", rating = 95, hp = 47, atkMin = 1, atkMax = 27, def = 1, tags = { "몬스터", "하급", "악마", "맹독", "비행" } } giant_spider = { name = "자이언트 스파이더", desc = "검은 털로 뒤덮인 거대한 거미. 끈적한 거미줄을 발사해 상대를 묶은 뒤, 재빨리 접근하여 독이 든 송곳니로 숨통을 끊어 놓습니다.", rating = 100, hp = 46, atkMin = 1, atkMax = 29, def = 1, tags = { "몬스터", "하급", "곤충", "맹독" } } giant_viper = { name = "자이언트 바이퍼", desc = "사람을 통째로 삼킬 수 있을 만큼 거대한 독사. 똬리를 틀고 있다가 스프링처럼 튀어나와 물며, 혈관을 태우는 듯한 맹독을 주입합니다.", rating = 100, hp = 46, atkMin = 1, atkMax = 29, def = 1, tags = { "몬스터", "하급", "야수", "맹독" } } mud_golem = { name = "머드 골렘", desc = "진흙과 물로 빚어진 부정형의 골렘. 타격 공격을 흡수하여 데미지를 줄이며, 적을 진흙 속에 가두어 질식시키는 공격을 합니다.", rating = 110, hp = 36, atkMin = 1, atkMax = 24, def = 6, tags = { "몬스터", "하급", "마법생물", "물리" } } lizardfolk = { name = "리자드포크", desc = "늪지에 적응하여 물갈퀴와 단단한 비늘을 가진 도마뱀 인간. 부족의 영토를 침범한 자를 무리지어 사냥하며, 뼈와 돌로 만든 원시적인 무기를 능숙하게 다룹니다.", rating = 110, hp = 53, atkMin = 3, atkMax = 25, def = 4, tags = { "몬스터", "하급", "인간형", "물리", "무리" } } spined_devil = { name = "스파인 데블", desc = "온몸이 가시로 뒤덮인 작은 날개 달린 악마. 지옥의 군세로서 편대를 지어 비행하며, 몸의 가시를 화살처럼 발사하거나 불타는 삼지창으로 급강하 공격을 합니다.", rating = 110, hp = 51, atkMin = 1, atkMax = 32, def = 1, tags = { "몬스터", "하급", "악마", "화염", "무리", "비행" } } darkmantle = { name = "다크멘틀", desc = "평소에는 천장의 종유석처럼 위장하고 있는 오징어 모양의 생물. 사냥감이 지나가면 어둠의 장막을 펼치며 떨어져 머리를 감싸고 질식시킵니다.", rating = 120, hp = 32, atkMin = 1, atkMax = 26, def = 7, tags = { "몬스터", "하급", "마법생물", "암흑", "기습" } } hobgoblin = { name = "홉고블린", desc = "붉은 피부에 잘 정돈된 갑옷을 입은 고블린의 상위종. 체계적인 군사 훈련을 받아 대열을 유지하며 싸우고, 장검과 활을 능숙하게 다룹니다.", rating = 120, hp = 61, atkMin = 1, atkMax = 31, def = 4, tags = { "몬스터", "하급", "인간형", "물리", "무리" } } grimlock = { name = "그림록", desc = "눈이 없고 회색 피부를 가진 지하의 식인종. 소름 끼치는 침묵 속에서 무리를 지어 사냥하며, 발달한 청각과 후각으로 어둠 속에서도 적을 정확히 감지합니다.", rating = 120, hp = 55, atkMin = 4, atkMax = 27, def = 4, tags = { "몬스터", "하급", "인간형", "물리", "무리" } } gnoll = { name = "놀", desc = "하이에나의 머리를 한 흉포한 수인. 광기 어린 웃음소리와 함께 떼지어 약탈을 일삼으며, 피 냄새를 맡으면 더욱 흥분하여 잔혹하게 공격합니다.", rating = 130, hp = 72, atkMin = 1, atkMax = 36, def = 1, tags = { "몬스터", "하급", "인간형", "물리", "무리" } } blink_dog = { name = "블링크 도그", desc = "지능이 높고 황금빛 털을 가진 개였으나 마기에 타락했습니다. 짧은 거리를 순간이동하며 적의 공격을 피하고 배후에서 나타나 물어뜯습니다.", rating = 130, hp = 72, atkMin = 1, atkMax = 37, def = 1, tags = { "몬스터", "하급", "야수", "물리", "마법", "기습" } } dire_wolf = { name = "다이어 울프", desc = "말만큼 거대한 덩치와 밤처럼 검은 털을 가진 늑대. 지능이 높아 무리를 지어 사냥감의 퇴로를 차단하며, 뼈를 부수는 강력한 턱힘을 자랑합니다.", rating = 140, hp = 69, atkMin = 1, atkMax = 39, def = 2, tags = { "몬스터", "하급", "야수", "물리", "무리" }, skills = {"beast_howl"} } cockatrice = { name = "코카트리스", desc = "수탉의 머리와 도마뱀의 꼬리를 가진 기괴한 마수. 그 부리에 쪼이면 살이 서서히 돌로 변하며 굳어가, 결국엔 움직일 수 없는 석상이 됩니다.", rating = 140, hp = 68, atkMin = 1, atkMax = 40, def = 2, tags = { "몬스터", "하급", "마법생물", "맹독", "비행" } } choker = { name = "초커", desc = "고무처럼 늘어나는 팔과 다리를 가진 기형적인 인간형. 좁은 틈새나 천장에 숨어 있다가 긴 팔로 순식간에 내려와 희생자의 목을 휘감고 조릅니다.", rating = 140, hp = 68, atkMin = 1, atkMax = 40, def = 2, tags = { "몬스터", "하급", "이형", "물리", "기습" } } orc = { name = "오크", desc = "근육질의 회색 피부와 튀어나온 송곳니를 가진 야만적인 전사. 항상 무리를 지어 행동하며, 전투의 함성을 지르고 거대한 도끼를 휘두르며 맹렬하게 돌진합니다.", rating = 150, hp = 67, atkMin = 5, atkMax = 34, def = 5, tags = { "몬스터", "하급", "인간형", "물리", "무리" }, skills = {"gatekeeper"} } rust_monster = { name = "러스트 몬스터", desc = "갑각으로 덮인 곤충형 괴물. 더듬이가 닿는 즉시 금속을 순식간에 녹슬게 하여 가루로 만든 뒤, 그 녹을 주식으로 삼습니다. 전사들에게 공포의 대상입니다.", rating = 160, hp = 60, atkMin = 1, atkMax = 36, def = 9, tags = { "몬스터", "하급", "마법생물", "맹독" } } ghoul = { name = "구울", desc = "무덤의 흙이 묻은 채 시체를 탐하는 언데드. 길게 자란 손톱에는 마비 성분이 있어, 스치기만 해도 근육이 굳어버려 산 채로 뜯어먹히게 됩니다.", rating = 160, hp = 78, atkMin = 1, atkMax = 45, def = 2, tags = { "몬스터", "하급", "언데드", "맹독" } } grick = { name = "그릭", desc = "뱀 같은 몸통에 네 개의 촉수가 달린 주둥이를 가진 지하 포식자. 바위 틈에 숨어 있다가 촉수로 먹이를 휘감고, 앵무새 같은 부리로 살점을 뜯어냅니다.", rating = 170, hp = 56, atkMin = 1, atkMax = 39, def = 10, tags = { "몬스터", "하급", "이형", "물리", "기습" } } worg = { name = "워그", desc = "사악한 지성과 언어 능력을 갖춘 거대한 늑대. 고블린이나 오크를 등에 태우고 전투에 참여하며, 교활한 전술로 적을 포위하여 사냥합니다.", rating = 170, hp = 86, atkMin = 1, atkMax = 49, def = 2, tags = { "몬스터", "하급", "야수", "물리", "무리" } } harpy = { name = "하피", desc = "인간의 상체에 독수리의 날개와 다리를 가진 괴물. 무리 지어 하늘을 선회하며 폭풍우를 부르거나 매혹적인 노래로 모험가를 유혹하여 절벽으로 끌어들입니다.", rating = 240, hp = 140, atkMin = 4, atkMax = 60, def = 4, tags = { "몬스터", "하급", "인간형", "전격", "무리", "비행" }, skills = {"flying_squadron"} } bugbear = { name = "버그베어", desc = "온몸이 털로 뒤덮인 거구의 고블린. 덩치에 어울리지 않게 발소리를 죽여 은밀히 다가온 뒤, 거대한 모닝스타로 단숨에 머리를 부수는 기습의 명수입니다.", rating = 260, hp = 160, atkMin = 5, atkMax = 62, def = 5, tags = { "몬스터", "하급", "인간형", "물리", "기습" }, skills = {"shadow_ambush"} } duergar = { name = "듀에르가", desc = "지하 깊은 곳에 사는 회색 피부의 사악한 드워프. 몸을 투명하게 하거나 거대하게 만드는 마법 능력을 가졌으며, 강철 사슬과 워픽으로 무장했습니다.", rating = 280, hp = 150, atkMin = 8, atkMax = 62, def = 8, tags = { "몬스터", "하급", "인간형", "암흑", "마법" } } ankheg = { name = "앙크헤그", desc = "땅속을 파고 다니는 거대한 곤충. 갑작스럽게 땅을 뚫고 튀어나와 낫 같은 앞다리로 먹이를 낚아채며, 입에서 강력한 산성 소화액을 뿜어냅니다.", rating = 300, hp = 140, atkMin = 5, atkMax = 63, def = 15, tags = { "몬스터", "하급", "곤충", "맹독", "기습" }, skills = {"lonely_predator"} } gibbering_mouther = { name = "기버링 마우더", desc = "수많은 눈과 입으로 이루어진 흉측한 점액 괴물. 주변의 지면을 물렁하게 만들고, 끊임없이 미친 소리를 중얼거려 듣는 자의 정신을 붕괴시킵니다.", rating = 320, hp = 160, atkMin = 5, atkMax = 70, def = 15, tags = { "몬스터", "하급", "이형", "암흑" } } ghast = { name = "가스트", desc = "구울보다 더욱 강력하고 끔찍한 악취를 풍기는 언데드. 그 냄새만으로도 숨이 막힐 지경이며, 더욱 빠르고 교활하게 희생자를 사냥합니다.", rating = 350, hp = 180, atkMin = 10, atkMax = 78, def = 10, tags = { "몬스터", "하급", "언데드", "맹독" } } gargoyle = { name = "가고일", desc = "사악한 마법으로 생명을 얻은 날개 달린 석상. 건물 옥상에 무리지어 조각상처럼 굳어 있다가, 돌처럼 단단한 피부로 공격을 튕겨내며 침입자를 급습합니다.", rating = 380, hp = 200, atkMin = 8, atkMax = 65, def = 25, tags = { "몬스터", "중급", "마법생물", "물리", "무리", "비행", "기습" }, skills = {"shadow_ambush"} } wight = { name = "와이트", desc = "죽음에서 돌아온 사악한 전사의 시체. 증오로 불타오르는 눈빛을로 적을 노려보며, 그 검에 베이면 생명력이 영구적으로 빨려나가 쇠약해집니다.", rating = 400, hp = 250, atkMin = 15, atkMax = 68, def = 18, tags = { "몬스터", "중급", "언데드", "암흑" } } hell_hound = { name = "헬 하운드", desc = "시커먼 털과 불타는 눈을 가진 지옥의 사냥개. 무리를 지어 사냥감를 포위하고, 입에서 뜨거운 유황 불길을 토해내어 적을 태워 죽입니다.", rating = 420, hp = 300, atkMin = 5, atkMax = 98, def = 10, tags = { "몬스터", "중급", "악마", "화염", "무리" }, skills = {"beast_howl"} } wraith = { name = "레이스", desc = "검은 연기 같은 로브를 두른 실체 없는 유령. 벽과 장애물을 통과해 소리 없이 다가오며, 차가운 손길이 스치면 영혼까지 얼어붙고 체력이 급격히 소진됩니다.", rating = 450, hp = 300, atkMin = 5, atkMax = 110, def = 10, tags = { "몬스터", "중급", "언데드", "암흑", "비행" } } ogre = { name = "오우거", desc = "거대한 덩치와 불룩 나온 배, 엄청난 힘을 가진 식인 거인. 통나무만 한 몽둥이로 사람을 떡 반죽처럼 으깨버리며, 눈에 보이는 고기는 무엇이든 탐합니다.", rating = 480, hp = 350, atkMin = 20, atkMax = 75, def = 20, tags = { "몬스터", "중급", "인간형", "물리" }, skills = {"gatekeeper"} } doppelganger = { name = "도플갱어", desc = "회색의 민무늬 피부를 가진 변신 괴물. 타인의 마음을 읽고 모습과 목소리를 완벽하게 흉내 내어, 아군으로 위장한 뒤 등 뒤에서 칼을 꽂습니다.", rating = 480, hp = 284, atkMin = 1, atkMax = 136, def = 6, tags = { "몬스터", "중급", "이형", "암흑", "기습" } } winter_wolf = { name = "윈터 울프", desc = "설원보다 하얀 털을 가진 거대한 늑대. 지능이 높아 인간의 말을 이해하며, 입을 열면 모든 것을 얼려버리는 강력한 냉기 브레스를 뿜습니다.", rating = 490, hp = 380, atkMin = 10, atkMax = 100, def = 15, tags = { "몬스터", "중급", "야수", "냉기" } } satyr = { name = "사티로스", desc = "타락하여 마왕군에 가담한 염소 다리의 요정. 피리를 불어 적을 잠재우거나 매혹시키며, 뿔로 들이받고 날렵하게 움직이며 단검을 찌릅니다.", rating = 520, hp = 208, atkMin = 5, atkMax = 128, def = 20, tags = { "몬스터", "중급", "요정", "암흑", "마법" } } phase_spider = { name = "페이즈 스파이더", desc = "청백색 몸통을 가진 거대한 거미. 현실과 에테르 계를 자유롭게 넘나들며, 적의 눈앞에서 사라졌다가 갑자기 뒤에서 나타나 독니를 꽂습니다.", rating = 520, hp = 304, atkMin = 1, atkMax = 145, def = 7, tags = { "몬스터", "중급", "곤충", "암흑", "마법", "기습" }, skills = {"lonely_predator"} } mummy = { name = "미라", desc = "방부 처리되어 붕대를 감은 고대의 시체. 신성 모독자에게 내려진 저주를 품고 있어, 주먹에 맞으면 살이 썩어들어가는 끔찍한 질병에 걸리게 됩니다.", rating = 520, hp = 380, atkMin = 15, atkMax = 85, def = 25, tags = { "몬스터", "중급", "언데드", "암흑" } } troll = { name = "트롤", desc = "고무처럼 질긴 녹색 피부와 긴 코를 가진 괴물. 사지가 잘려도 금방 다시 자라나는 경이로운 재생력을 가졌으나, 불과 산성에는 재생하지 못해 공포에 떱니다.", rating = 530, hp = 152, atkMin = 6, atkMax = 118, def = 31, tags = { "몬스터", "중급", "인간형", "물리" } } werewolf = { name = "웨어울프", desc = "반은 인간, 반은 늑대의 형상을 한 야수. 보름달 아래에서 흉포한 본성이 깨어나며, 평범한 무기로는 상처 입힐 수 없고 은이나 마법 무기만이 통합니다.", rating = 550, hp = 400, atkMin = 20, atkMax = 95, def = 20, tags = { "몬스터", "중급", "인간형", "물리" } } yeti = { name = "예티", desc = "설산에 서식하는 하얀 털복숭이 설인. 눈보라 속에 숨어 있다가 나타나며, 그 차가운 눈빛과 마주치면 공포에 질려 몸이 마비됩니다.", rating = 560, hp = 450, atkMin = 20, atkMax = 95, def = 20, tags = { "몬스터", "중급", "인간형", "냉기", "기습" }, skills = {"shadow_ambush"} } griffon = { name = "그리폰", desc = "독수리의 머리와 날개, 사자의 뒷다리를 가진 긍지 높은 맹수. 폭풍우 속에서도 비행하며 날카로운 부리와 발톱으로 적을 채가거나 갈기갈기 찢습니다.", rating = 580, hp = 349, atkMin = 1, atkMax = 162, def = 8, tags = { "몬스터", "중급", "야수", "전격", "비행" } } lamia = { name = "라미아", desc = "상반신은 아름다운 여인, 하반신은 사자의 모습을 한 괴물. 마법으로 환영을 만들어 모험가를 현혹시킨 뒤, 날카로운 발톱으로 살해하고 피를 마십니다.", rating = 580, hp = 350, atkMin = 1, atkMax = 161, def = 8, tags = { "몬스터", "중급", "이형", "암흑", "마법" } } minotaur = { name = "미노타우로스", desc = "소의 머리와 인간의 몸을 가진 거대한 괴수. 미궁 속에서도 절대 길을 잃지 않으며, 거대한 전투 도끼를 휘두르고 뿔을 앞세워 모든 것을 들이받습니다.", rating = 620, hp = 253, atkMin = 20, atkMax = 135, def = 23, tags = { "몬스터", "중급", "인간형", "물리" }, skills = {"vanguard_tactic"} } nightmare = { name = "나이트메어", desc = "칠흑 같은 몸체에 불타는 갈기와 발굽을 가진 지옥의 군마. 달리면서 땅에 불길을 남기고, 입과 코에서 뜨거운 연기를 뿜으며 공중을 달릴 수 있습니다.", rating = 620, hp = 391, atkMin = 1, atkMax = 172, def = 8, tags = { "몬스터", "중급", "악마", "화염", "비행" } } owlbear = { name = "아울베어", desc = "곰의 육중한 몸에 부엉이의 머리와 깃털을 가진 야수. 성질이 매우 사납고 흉포하여, 거대한 부리로 갑옷을 찢고 곰 같은 팔로 뼈를 으스러뜨립니다.", rating = 630, hp = 261, atkMin = 20, atkMax = 136, def = 23, tags = { "몬스터", "중급", "야수", "물리" } } specter = { name = "스펙터", desc = "죽음의 고통과 원한에 사로잡힌 강력한 유령. 생명체에 대한 증오로 가득 차 있으며, 생명력을 흡수하고 살해한 적을 짦은 시간 동안 자신의 노예로 부립니다.", rating = 630, hp = 377, atkMin = 1, atkMax = 176, def = 8, tags = { "몬스터", "중급", "언데드", "암흑", "비행" } } otyugh = { name = "오티", desc = "세 개의 다리와 가시 달린 촉수를 가진 오물더미 속의 괴물. 쓰레기와 배설물을 먹고 살며, 촉수로 먹이를 잡아 거대한 이빨이 가득한 입으로 끌고 갑니다.", rating = 650, hp = 178, atkMin = 7, atkMax = 146, def = 38, tags = { "몬스터", "중급", "이형", "맹독" } } hook_horror = { name = "훅 호러", desc = "새 부리 같은 얼굴과 거대한 갈고리 팔을 가진 지하 괴수. 암벽을 타고 다니며, 갈고리로 적을 꿰뚫어 들어올린 뒤 부리로 쪼아먹습니다.", rating = 650, hp = 272, atkMin = 21, atkMax = 140, def = 24, tags = { "몬스터", "중급", "이형", "물리" } } manticore = { name = "맨티코어", desc = "사자의 몸, 인간의 얼굴, 박쥐의 날개, 전갈의 꼬리를 가진 흉물. 꼬리에서 굵은 강철 가시를 비 오듯 발사하여 원거리의 적을 꿰뚫어 버립니다.", rating = 650, hp = 404, atkMin = 1, atkMax = 179, def = 9, tags = { "몬스터", "중급", "야수", "맹독", "비행" }, skills = {"flying_squadron"} } succubus = { name = "서큐버스", desc = "박쥐 날개와 꼬리를 가진 매혹적인 악마. 절세미녀의 모습으로 변해 꿈속이나 현실에서 이성을 유혹하고, 입맞춤을 통해 생명력을 빨아들입니다.", rating = 650, hp = 397, atkMin = 1, atkMax = 180, def = 9, tags = { "몬스터", "중급", "악마", "암흑", "비행", "마법" } } invisible_stalker = { name = "인비저블 스토커", desc = "전기와 공기로 이루어진 암살자 정령. 매우 투명하여 시각적으로 파악하기 어렵다. 바람 또는 전류의 흐름으로 존재를 인지하게 되며 은밀하게 다가오는 재빠른 전류에 상대는 영문도 모른 채 쓰러집니다.", rating = 650, hp = 404, atkMin = 1, atkMax = 178, def = 9, tags = { "몬스터", "중급", "정령", "전격", "비행", "기습" } } wyvern = { name = "와이번", desc = "드래곤의 먼 친척으로 앞다리 없이 날개와 뒷다리만 있는 비룡. 공중에서 적을 낚아채거나, 꼬리 끝에 달린 치명적인 독침으로 찔러 중독시킵니다.", rating = 680, hp = 429, atkMin = 1, atkMax = 189, def = 9, tags = { "몬스터", "중급", "용족", "맹독", "비행" } } xorn = { name = "쏜", desc = "세 개의 팔과 세 개의 다리, 머리 꼭대기에 입이 달린 기이한 땅의 정령. 바위와 흙을 물처럼 통과하며 이동하고, 귀중한 보석과 금속을 찾아 먹어치웁니다.", rating = 700, hp = 206, atkMin = 7, atkMax = 158, def = 41, tags = { "몬스터", "중급", "정령", "물리" } } oni = { name = "오니", desc = "푸른 피부와 뿔을 가진 동방의 오우거 마법사. 흑마법을 부려 하늘을 날거나 투명해질 수 있으며, 눈에서 번개를 쏘고 언월도를 휘두릅니다.", rating = 710, hp = 310, atkMin = 6, atkMax = 175, def = 27, tags = { "몬스터", "중급", "인간형", "전격", "비행", "마법" } } chimera = { name = "키메라", desc = "사자, 염소, 용의 머리가 한 몸에 달린 합성 마수. 세 개의 머리가 각기 물어뜯고 들이받으며, 용의 머리에서는 뜨거운 화염 브레스를 내뿜습니다.", rating = 710, hp = 500, atkMin = 25, atkMax = 120, def = 30, tags = { "몬스터", "중급", "야수", "화염", "비행" } } flesh_golem = { name = "플레시 골렘", desc = "여러 시체의 조각을 실로 기워 만들고 번개로 생명을 불어넣은 괴물. 고통을 모르고 마법에 내성이 있으며, 체내의 번개를 방출하는 공격을 합니다.", rating = 710, hp = 500, atkMin = 15, atkMax = 110, def = 45, tags = { "몬스터", "중급", "마법생물", "전격" } } medusa = { name = "메두사", desc = "꿈틀거리는 뱀 머리카락을 가진 저주받은 여인. 그녀의 끔찍한 얼굴을 직시한 자는 공포 속에 비명을 지를 새도 없이 차가운 석상으로 변합니다.", rating = 720, hp = 435, atkMin = 2, atkMax = 200, def = 10, tags = { "몬스터", "중급", "인간형", "맹독" }, skills = {"toxic_cloud"} } earth_elemental = { name = "어스 엘리멘탈", desc = "거대한 바위와 흙이 뭉쳐 만들어진 땅의 정령. 땅속을 자유롭게 이동하며, 엄청난 무게의 주먹으로 지면을 강타하여 지진을 일으킵니다.", rating = 720, hp = 550, atkMin = 15, atkMax = 110, def = 45, tags = { "몬스터", "중급", "정령", "물리" } } salamander = { name = "살라만더", desc = "도마뱀의 형상을 한 화염의 정령. 온몸이 고열로 타오르고 있어 가까이 가기만 해도 화상을 입으며, 뜨겁게 달궈진 창을 휘두릅니다.", rating = 730, hp = 441, atkMin = 2, atkMax = 201, def = 10, tags = { "몬스터", "중급", "정령", "화염" } } fire_elemental = { name = "파이어 엘리멘탈", desc = "살아 움직이는 불로 휩쌓인 화염 정령으로, 매력적인 여성의 형상을 가지고 있다. 닿는 모든 것을 태워버리며, 불길을 채찍처럼 휘둘러 주변을 순식간에 잿더미로 만듭니다.", rating = 740, hp = 460, atkMin = 2, atkMax = 204, def = 10, tags = { "몬스터", "중급", "정령", "화염" } } basilisk = { name = "바실리스크", desc = "여덟 개의 다리를 가진 육중한 도마뱀. 그 초록색 눈과 시선이 마주치는 순간, 생명체는 서서히 돌로 변하여 영원히 굳어버리게 됩니다.", rating = 750, hp = 222, atkMin = 8, atkMax = 169, def = 44, tags = { "몬스터", "중급", "야수", "맹독" } } shambling_mound = { name = "솀블링 마운드", desc = "썩은 식물과 진흙이 엉겨 붙어 움직이는 덩어리. 덩굴로 적을 휘감아 질식시키며, 번개를 흡수하여 신체를 강화하고 방출하는 능력이 있습니다.", rating = 750, hp = 220, atkMin = 8, atkMax = 168, def = 44, tags = { "몬스터", "중급", "식물", "전격" } } banshee = { name = "밴시", desc = "억울하게 죽은 매혹적인 여성의 원혼. 그녀의 슬픈 울음소리를 들은 산 자는 심장이 멎을 정도의 공포와 함께 생명력을 잃고 즉사할 수도 있습니다.", rating = 790, hp = 499, atkMin = 2, atkMax = 217, def = 10, tags = { "몬스터", "중급", "언데드", "암흑" } } bulette = { name = "뷸렛", desc = "상어 같은 지느러미와 두꺼운 등껍질을 가진 '육상 상어'. 땅속을 헤엄치다 솟구쳐 올라 먹이를 통째로 삼키며, 그 도약은 지면을 흔들 정도입니다.", rating = 830, hp = 250, atkMin = 9, atkMax = 186, def = 49, tags = { "몬스터", "중급", "야수", "물리", "기습" } } hill_giant = { name = "힐 자이언트", desc = "거대한 덩치에 비해 머리가 작고 배가 나온 거인. 지능은 낮지만 엄청난 힘으로 집채만 한 바위를 던지거나 통나무를 휘둘러 적을 납작하게 만듭니다.", rating = 830, hp = 376, atkMin = 26, atkMax = 178, def = 30, tags = { "몬스터", "중급", "인간형", "물리" }, skills = {"gatekeeper"} } gorgon = { name = "고르곤", desc = "강철 비늘로 온몸이 뒤덮인 거대한 황소. 콧김으로 치명적인 녹색 석화 가스를 내뿜어, 범위 내의 모든 적을 순식간에 돌 조각상으로 만들어버립니다.", rating = 860, hp = 265, atkMin = 9, atkMax = 193, def = 50, tags = { "몬스터", "중급", "야수", "맹독" } } ettin = { name = "에틴", desc = "두 개의 머리를 가진 흉측한 거인. 한 머리가 잘 때 다른 머리가 깨어 있어 기습이 불가능하며, 양손에 든 두 개의 몽둥이로 쉴 새 없이 공격을 퍼붓습니다.", rating = 870, hp = 397, atkMin = 28, atkMax = 185, def = 32, tags = { "몬스터", "중급", "인간형", "물리" } } stone_giant = { name = "스톤 자이언트", desc = "바위처럼 단단한 회색 피부를 가진 거인. 돌과 대화하며 바위 속에 자연스럽게 동화될 수 있고, 거대한 암석을 던져 공성무기급의 파괴력을 냅니다.", rating = 1190, hp = 381, atkMin = 13, atkMax = 264, def = 68, tags = { "몬스터", "상급", "인간형", "물리" } } treant = { name = "트리언트", desc = "오염되어 미쳐버린 고대 숲의 수호자. 뿌리로 땅을 뒤흔들고 가지로 적을 후려치며, 주변의 일반 나무들을 깨워 숲 전체가 적을 공격하게 만듭니다.", rating = 1220, hp = 414, atkMin = 13, atkMax = 270, def = 71, tags = { "몬스터", "상급", "식물", "물리" }, skills = {"gatekeeper"} } fire_giant = { name = "파이어 자이언트", desc = "검은 피부와 붉은 수염을 가진 화염 거인. 화산 깊은 곳의 대장간에서 만든 두꺼운 판금 갑옷을 입고, 불타는 거대한 대검으로 전장을 휩씁니다.", rating = 1330, hp = 459, atkMin = 14, atkMax = 291, def = 77, tags = { "몬스터", "상급", "인간형", "화염" } } stone_golem = { name = "스톤 골렘", desc = "고대 마법 문자가 새겨진 거대한 바위 거인. 움직임은 느리지만, 그 육중한주먹 한 방은 성벽조차 무너뜨릴 수 있으며 마법에 강한 내성을 지닙니다.", rating = 1370, hp = 471, atkMin = 15, atkMax = 299, def = 80, tags = { "몬스터", "상급", "마법생물", "물리" } } hydra = { name = "히드라", desc = "다섯 개의 머리를 가진 늪지의 괴수. 머리를 잘라도 불로 지지지 않으면 순식간에 두 개가 새로 자라나며, 모든 머리가 동시에 달려들어 물어뜯습니다.", rating = 1420, hp = 506, atkMin = 15, atkMax = 311, def = 83, tags = { "몬스터", "상급", "용족", "물리" } } remorhaz = { name = "레모라즈", desc = "극지방의 얼음 밑에 사는 거대 다지류 괴수. 등에서 엄청난 고열을 내뿜어 닿는 무기를 녹여버리며, 얼음과 바위를 뚫고 나와 먹이를 덮칩니다.", rating = 1510, hp = 794, atkMin = 46, atkMax = 315, def = 53, tags = { "몬스터", "상급", "이형", "화염", "기습" } } iron_golem = { name = "아이언 골렘", desc = "최고급 강철로 주조된 무적의 파수꾼. 모든 무기를 튕겨내고 불을 흡수하여 체력을 회복하며, 입에서 치명적인 독가스 구름을 내뿜습니다.", rating = 1580, hp = 576, atkMin = 17, atkMax = 344, def = 91, tags = { "몬스터", "상급", "마법생물", "물리" }, skills = {"gatekeeper"} } bone_dragon = { name = "본 드래곤", desc = "죽어서 뼈만 남은 상태로 강령술에 의해 되살아난 드래곤. 썩은 살점 사이로 죽음의 냉기 브레스를 뿜으며, 생전의 마법적 본능으로 적을 파괴합니다.", rating = 1670, hp = 598, atkMin = 18, atkMax = 367, def = 94, tags = { "몬스터", "상급", "언데드", "냉기", "비행" }, skills = {"undead_march"} } purple_worm = { name = "퍼플 웜", desc = "집채만 한 입을 가진 보라색의 초거대 지렁이. 땅속을 고속으로 이동하며 진동을 감지하고, 무엇이든 한입에 삼켜 강력한 위산으로 녹여버립니다.", rating = 1690, hp = 642, atkMin = 18, atkMax = 367, def = 97, tags = { "몬스터", "상급", "이형", "맹독", "기습" } } mummy_lord = { name = "미라 로드", desc = "고대 왕국의 왕이었던 강력한 미라. 신성 모독적인 흑마법을 구사하고 모래 폭풍을 일으키며, 그를 보는 것만으로도 나약한 자는 공포에 질려 마비됩니다.", rating = 1740, hp = 663, atkMin = 19, atkMax = 375, def = 100, tags = { "몬스터", "상급", "언데드", "암흑" } } vampire_lord = { name = "뱀파이어 로드", desc = "수백 년의 세월을 살아온 밤의 제왕. 안개나 박쥐로 변신하고 늑대 떼를 부리며, 눈빛만으로 상대를 지배하고 닿는 즉시 생명력을 갈취합니다.", rating = 1760, hp = 982, atkMin = 16, atkMax = 414, def = 63, tags = { "몬스터", "상급", "언데드", "암흑", "비행", "마법" } } death_knight = { name = "데스 나이트", desc = "타락하여 죽음의 저주를 받은 전설적인 성기사. 칠흑의 판금 갑옷을 입고 해골 말을 타며, 지옥불을 소환하여 성스러운 힘을 모독합니다.", rating = 1860, hp = 1055, atkMin = 56, atkMax = 379, def = 65, tags = { "몬스터", "상급", "언데드", "암흑" } } behir = { name = "베히르", desc = "열두 개의 다리를 가진 거대한 파란색 파충류. 뱀처럼 움직여 적을 휘감아 조르고, 입에서 강력한 번개를 내뿜는 드래곤의 천적입니다.", rating = 1900, hp = 1558, atkMin = 4, atkMax = 484, def = 24, tags = { "몬스터", "상급", "이형", "전격" } } rakshasa = { name = "락샤사", desc = "온몸이 호랑이 털로 뒤덮였으며 매혹적인 인간 여성의 몸과 호랑이의 머리를 하고 날카로운 발톱을 가진 사악한 악마. 하급 주문을 완전히 무시하는 마법 면역을 가졌으며, 환영과 기만술로 적을 농락하고 잡아먹습니다.", rating = 2060, hp = 1276, atkMin = 18, atkMax = 472, def = 74, tags = { "몬스터", "상급", "악마", "암흑" } } blue_dragon = { name = "블루 드래곤", desc = "사막의 모래 폭풍 속에 사는 거대한 드래곤. 입에서 일직선으로 뻗어나가는 강력한 번개 브레스를 내뿜어 적을 감전시켜 숯덩이로 만듭니다.", rating = 2080, hp = 1755, atkMin = 5, atkMax = 524, def = 26, tags = { "몬스터", "상급", "용족", "전격", "비행" } } lich = { name = "리치", desc = "영생을 얻기 위해 스스로 언데드가 된 대마법사. 썩어가는 육신에 강력한 마력이 깃들어 있으며, 손가락 하나 까딱하여 죽음의 주문으로 군대를 전멸시킵니다.", rating = 2210, hp = 1886, atkMin = 5, atkMax = 558, def = 27, tags = { "몬스터", "상급", "언데드", "암흑", "마법" } } marilith = { name = "마릴리스", desc = "여섯 개의 팔에 각기 다른 명검을 쥔 뱀 몸통의 악마. 춤추듯 우아하고 치명적인 검술로 공격을 막아낼 틈도 없이 적을 토막 냅니다.", rating = 2270, hp = 2046, atkMin = 5, atkMax = 560, def = 28, tags = { "몬스터", "상급", "악마", "물리" } } pit_fiend = { name = "핏 핀드", desc = "지옥의 군단을 지휘하는 최고위 악마. 거대한 날개와 뱀 같은 꼬리를 가졌으며, 맹독이 묻은 이빨과 불타는 철퇴로 신들조차 두려워할 위용을 뽐냅니다.", rating = 2490, hp = 1636, atkMin = 22, atkMax = 564, def = 87, tags = { "몬스터", "상급", "악마", "화염", "비행", "마법" } } balor = { name = "발러", desc = "심연의 화염으로 이루어진 파괴의 대악마. 한 손에는 번개 검, 다른 손에는 화염 채찍을 들고 있으며, 죽는 순간 거대한 폭발을 일으켜 동귀어진합니다.", rating = 2550, hp = 1638, atkMin = 76, atkMax = 506, def = 87, tags = { "몬스터", "상급", "악마", "화염", "비행", "마법" } } red_dragon = { name = "레드 드래곤", desc = "화산을 둥지로 삼는 가장 거대하고 강력한 드래곤. 오만하고 탐욕스러우며, 모든 것을 재로 만드는 화염 브레스와 마법으로 세상을 공포에 떨게 합니다.", rating = 2860, hp = 1928, atkMin = 84, atkMax = 560, def = 96, tags = { "몬스터", "상급", "용족", "화염", "비행", "마법" } }
- #25race.dbconstant
Keys:
-- (Lua Config Style) human = { name = "인간", desc = "가장 평균적인 종족. 적응력이 뛰어나다.", base_hp = 110, base_sp = 100, bonus_atk = 1, bonus_def = 0, height = 160, life_mod = 1.0 } elf = { name = "엘프", desc = "숲에 사는 고귀한 종족. 마력(기력)이 높고 수명이 길지만 체력이 약하다.", base_hp = 90, base_sp = 120, bonus_atk = 1, bonus_def = 2, height = 170, life_mod = 5.0 } dwarf = { name = "드워프", desc = "다부진 체격의 종족. 체력이 높고 손재주가 좋다.", base_hp = 130, base_sp = 80, bonus_atk = 0, bonus_def = 1, height = 135, life_mod = 3.0 } beastkin = { name = "수인", desc = "동물의 특징을 가진 종족. 신체 능력이 뛰어나고 야성적이다.", base_hp = 115, base_sp = 95, bonus_atk = 1, bonus_def = 0, height = 158, life_mod = 0.8 }
- #26skills.dbconstant
Keys:
--다른 db파일과 달리 json이 아닌 lua테이블로 데이터가 정의 되었음-- 1. Horde Tactic (군단 전술) horde_tactic = { name = "군단 전술", desc = "전체 아군 중 '인간형' 5기 이상 시, 모든 '인간형'에게 버프 부여", active = function(ctx) if ctx:dungeon():filter("인간형"):count() >= 5 then return ctx:dungeon():filter("인간형"):buff("horde_mentality", ctx.slotId) end return {} end }
-- 2. Fire Resonance (화염 공명) fire_resonance = { name = "화염 공명", desc = "전체 아군 중 '화염' 3기 이상 시, 모든 '화염'에게 버프 부여", active = function(ctx) if ctx:dungeon():filter("화염"):count() >= 3 then return ctx:dungeon():filter("화염"):buff("fire_empower", ctx.slotId) end return {} end }
-- 3. Beast Flank (야수의 호위) beast_flank = { name = "야수의 호위", desc = "양 옆이 '야수'일 경우 아군들 방어력 증가", active = function(ctx) if ctx:neighbors():filter("야수"):count() == 2 then return ctx:neighbors():filter("야수"):buff("basic_def_up", ctx.slotId) end return {} end }
-- 4. Elite Squad (정예 부대) elite_squad = { name = "정예 부대", desc = "던전 총점 2000 이상 시, Rating 150 이상 유닛 방어력 증가", active = function(ctx) if ctx:dungeon():sum("rating") >= 2000 then return ctx:dungeon():filter(function(u) return (u.data and u.data.rating and u.data.rating >= 150) end):buff("basic_def_up", ctx.slotId) end return {} end }
-- 5. Weak Link (약점 보완) weak_link = { name = "약점 보완", desc = "자신 방어력이 낮고(5↓) 주변 방어력이 높을 때(20↑) 방어력 증가", active = function(ctx) local selfUnit = ctx.grid[ctx.slotId] if not selfUnit or not selfUnit.data then return {} end
local selfDef = tonumber(selfUnit.data.def) or 0 if selfDef <= 5 then local neighbors = ctx:neighbors() if neighbors:count() == 2 then local conditionMet = true for _, item in ipairs(neighbors.units) do local def = (item.unit.data and tonumber(item.unit.data.def)) or 0 if def < 20 then conditionMet = false break end end if conditionMet then return {{ target = ctx.slotId, buff = "weak_link_boost", source = ctx.slotId }} end end end return {} end}
-- 6. Vanguard Tactic (선봉 전술) vanguard_tactic = { name = "선봉 전술", desc = "1번 슬롯 배치 시 방어력 증가", active = function(ctx) local s = ctx.slotId:match("_(%d+)$") if s == "1" then return {{ target = ctx.slotId, buff = "vanguard_def_up", source = ctx.slotId }} end return {} end }
-- 7. Abyss Adaptation (심연 적응) abyss_adaptation = { name = "심연 적응", desc = "심연(5층 이상) 배치 시 공격력 증가", active = function(ctx) local f = ctx.slotId:match("^(%d+)_") if f and tonumber(f) >= 5 then return {{ target = ctx.slotId, buff = "abyss_power", source = ctx.slotId }} end return {} end }
-- 8. Rear Guard Sniping (후방 저격) rear_guard_sniping = { name = "후방 저격", desc = "후방(4,5번) 슬롯 배치 시 공격력 증가", active = function(ctx) local s = ctx.slotId:match("_(%d+)$") if s == "4" or s == "5" then return {{ target = ctx.slotId, buff = "rear_snipe", source = ctx.slotId }} end return {} end }
-- 9. Undead March (망자의 행진) undead_march = { name = "망자의 행진", desc = "앞 열에 언데드가 있을 경우 공격력 증가", active = function(ctx) local f, s = ctx.slotId:match("^(%d+)_(%d+)$") s = tonumber(s)
local count = 0 for i = 1, s-1 do local prevKey = string.format("%d_%d", f, i) local unit = ctx.grid[prevKey] if unit and unit.hasTag and unit:hasTag("언데드") then count = count + 1 end end if count > 0 then return {{ target = ctx.slotId, buff = "undead_march_buff", source = ctx.slotId }} end return {} end}
-- 10. Swarm Density (밀집 대형) swarm_density = { name = "밀집 대형", desc = "양 옆이 '무리'일 경우 방어력 증가", active = function(ctx) local neighbors = ctx:neighbors() if neighbors:count() == 2 and neighbors:filter("무리"):count() == 2 then return {{ target = ctx.slotId, buff = "swarm_def_up", source = ctx.slotId }} end return {} end }
-- 11. Magical Resonance (마력 공명) magic_resonance = { name = "마력 공명", desc = "층 내 마법 유닛 3기 이상 시 공격력 증가", active = function(ctx) local floorMages = ctx:dungeon():filter(function(u) return (u:hasTag("마법") or u:hasTag("마법생물")) and u.slot:match("^(%d+)") == ctx.slotId:match("^(%d+)") -- Using string match for floor comparison if .floor prop is missing end)
if floorMages:count() >= 3 then return {{ target = ctx.slotId, buff = "magic_resonance", source = ctx.slotId }} end return {} end}
-- 12. Shadow Ambush (그림자 습격) shadow_ambush = { name = "그림자 습격", desc = "측면(1,5번) 배치 시 공격력 증가", active = function(ctx) local s = tonumber(ctx.slotId:match("_(%d+)$")) if (s == 1 or s == 5) and ctx.owner:hasTag("기습") then return {{ target = ctx.slotId, buff = "ambush_bonus", source = ctx.slotId }} end return {} end }
-- 13. Lonely Predator (고독한 포식자) lonely_predator = { name = "고독한 포식자", desc = "양 옆이 비어있을 경우 공격력 대폭 증가", active = function(ctx) if ctx:neighbors():count() == 0 then return {{ target = ctx.slotId, buff = "lonely_strength", source = ctx.slotId }} end return {} end }
-- 14. Toxic Cloud (독성 구름) toxic_cloud = { name = "독성 구름", desc = "인접한 아군에게 독성 버프 부여", active = function(ctx) local neighbors = ctx:neighbors() if neighbors:count() > 0 then return neighbors:buff("poison_assist", ctx.slotId) end return {} end }
-- 15. Flying Squadron (비행 편대) flying_squadron = { name = "비행 편대", desc = "층 내 비행 유닛 3기 이상 시 방어력 증가 (전체)", active = function(ctx) local flyers = ctx:dungeon():filter(function(u) return u:hasTag("비행") and u.slot:match("^(%d+)") == ctx.slotId:match("^(%d+)") end)
if flyers:count() >= 3 then return flyers:buff("flying_squad_buff", ctx.slotId) end return {} end}
-- 16. Gatekeeper (문지기) gatekeeper = { name = "문지기", desc = "1번 슬롯에서 후방을 지키면 방어력 대폭 증가", active = function(ctx) local f, s = ctx.slotId:match("^(%d+)_(%d+)$") s = tonumber(s)
if s == 1 then local count = 0 for i = 2, 5 do if ctx.grid[string.format("%d_%d", f, i)] then count = count + 1 end end if count >= 2 then return {{ target = ctx.slotId, buff = "gatekeeper_def", source = ctx.slotId }} end end return {} end}
-- 17. Sacrificial Lamb (희생양) sacrificial_lamb = { name = "희생양", desc = "인접한 아군이 약할수록(Rating 50↓) 공격력 증가", active = function(ctx) local neighbors = ctx:neighbors() local weaklings = neighbors:filter(function(u) return u.data.rating and u.data.rating <= 50 end)
if weaklings:count() > 0 then return {{ target = ctx.slotId, buff = "sacrifice_power", source = ctx.slotId }} end return {} end}
-- 18. Darkness Shroud (어둠의 장막) darkness_shroud = { name = "어둠의 장막", desc = "층 내 암흑 유닛 2기 이상 시 방어력 증가 (전체)", active = function(ctx) local darks = ctx:dungeon():filter(function(u) return u:hasTag("암흑") and u.slot:match("^(%d+)") == ctx.slotId:match("^(%d+)") end)
if darks:count() >= 2 then return darks:buff("dark_shroud", ctx.slotId) end return {} end}
-- 19. Beast Howl (야수의 포효) beast_howl = { name = "야수의 포효", desc = "중앙(3번) 배치 시 좌우 아군 강화", active = function(ctx) local f, s = ctx.slotId:match("^(%d+)_(%d+)$") s = tonumber(s)
if s == 3 and ctx.owner:hasTag("야수") then local targets = {} local left = ctx.grid[string.format("%d_%d", f, 2)] local right = ctx.grid[string.format("%d_%d", f, 4)] if left then table.insert(targets, { target=string.format("%d_%d", f, 2), buff="beast_howl_buff", source=ctx.slotId }) end if right then table.insert(targets, { target=string.format("%d_%d", f, 4), buff="beast_howl_buff", source=ctx.slotId }) end return targets end return {} end}
- #27stat.dbconstant
Keys:
-- (Lua Config Style) -- stat. 대상의 현재 신체 혹은 심리상태를 나타낸다. c_pleasure = { name = "C쾌락", desc = "조교 대상이 음핵 자극으로 쾌락을 느낀 정도.", related = { "C감각의 흔적", "발정의 흔적", "복종의 흔적", "애정의 흔적" } } b_pleasure = { name = "B쾌락", desc = "조교 대상이 유두 자극으로 쾌락을 느낀 정도.", related = { "B감각의 흔적", "발정의 흔적", "복종의 흔적", "애정의 흔적" } } v_pleasure = { name = "V쾌락", desc = "조교 대상이 질 삽입으로 쾌락을 느낀 정도.", related = { "V감각의 흔적", "발정의 흔적", "기술의 흔적", "애정의 흔적" } } a_pleasure = { name = "A쾌락", desc = "조교 대상이 애널 삽입으로 쾌락을 느낀 정도.", related = { "A감각의 흔적", "발정의 흔적", "굴욕의 흔적", "기술의 흔적" } } u_pleasure = { name = "U쾌락", desc = "조교 대상이 요도 자극으로 쾌락을 느낀 정도.", related = { "U감각의 흔적", "굴욕의 흔적", "발정의 흔적", "C감각의 흔적" } } m_pleasure = { name = "M쾌락", desc = "조교 대상이 구강 자극으로 쾌락을 느낀 정도.", related = { "발정의 흔적", "기술의 흔적", "복종의 흔적" } } s_pleasure = { name = "S쾌락", desc = "조교 대상이 정신적인 자극으로 쾌락을 느낀 정도.", related = { "발정의 흔적", "순종의 흔적", "애정의 흔적", "불안의 흔적" } } v_wet = { name = "V윤활", desc = "조교 대상의 음부가 젖은 정도. 윤활이 부족한채로 삽입을 시도하면 성공확률이 감소한다.", related = { "발정의 흔적", "굴욕의 흔적", "순종의 흔적", "애정의 흔적" }, events = { onDCCalc = { hasTags = { "insert_V" }, func = function(e) e.dc = e.dc - (e.level - 5) * 1.0 end } } } a_wet = { name = "A윤활", desc = "조교 대상의 애널이 젖은 정도. 윤활이 부족한채로 삽입을 시도하면 성공확률이 감소한다.", related = { "발정의 흔적", "굴욕의 흔적", "불안의 흔적", "복종의 흔적" }, events = { onDCCalc = { hasTags = { "insert_A" }, func = function(e) e.dc = e.dc - (e.level - 5) * 1.0 end } } } lust = { name = "욕정", desc = "조교 대상의 발정 정도. 욕정이 높을수록 조교의 성공확률이 증가한다.", related = { "발정의 흔적", "순종의 흔적", "애정의 흔적", "기술의 흔적" }, events = { onDCCalc = { hasTags = { "sex", "service" }, func = function(e) e.dc = e.dc - e.level * 0.5 end } } } obedient = { name = "온순", desc = "조교 대상이 순순히 명령을 따르는 정도. 온순이 낮은 상태에서는 조교 성공확률이 감소한다.", related = { "순종의 흔적", "복종의 흔적", "애정의 흔적", "발정의 흔적" }, events = { onDCCalc = { func = function(e) e.dc = e.dc - e.level * 0.3 end } } } submission = { name = "굴복", desc = "조교 대상이 강제로 명령을 따르는 정도. 굴복이 높으면 조교의 성공확률이 증가한다.", related = { "굴욕의 흔적", "복종의 흔적", "부정의 흔적", "불안의 흔적" }, events = { onDCCalc = { func = function(e) e.dc = e.dc - e.level * 0.2 end } } } shame = { name = "수치", desc = "조교 대상이 느끼는 수치심의 정도.", related = { "굴욕의 흔적", "불안의 흔적", "부정의 흔적", "순종의 흔적" }, events = { onDCCalc = { hasTags = { "shame" }, func = function(e) e.dc = e.dc + e.level * 0.5 end } } } pain = { name = "고통", desc = "조교 대상이 느끼는 고통의 정도.", related = { "상처의 흔적", "불안의 흔적", "부정의 흔적", "복종의 흔적" }, events = { onDCCalc = { hasTags = { "pain", "hard" }, func = function(e) e.dc = e.dc + e.level * 0.5 end } } } fear = { name = "공포", desc = "조교 대상이 느끼는 공포의 정도.", related = { "불안의 흔적", "부정의 흔적", "굴욕의 흔적", "복종의 흔적" }, events = { onDCCalc = { hasTags = { "hard", "beast", "tentacle" }, func = function(e) e.dc = e.dc + e.level * 0.5 end }, onDCCalc_2 = { hasTags = { "gentle", "love" }, func = function(e) e.dc = e.dc + e.level * 0.1 end } } } disgust = { name = "불쾌", desc = "조교 대상이 느끼는 더러움의 정도.", related = { "굴욕의 흔적", "부정의 흔적", "상처의 흔적", "불안의 흔적" }, events = { onDCCalc = { hasTags = { "filth" }, func = function(e) e.dc = e.dc + e.level * 1.0 end }, onDCCalc_2 = { hasTags = { "oral", "A" }, func = function(e) e.dc = e.dc + e.level * 0.5 end } } } resistance = { name = "저항", desc = "조교 대상이 느끼는 부정적 감정의 정도.", related = { "부정의 흔적" }, events = { onDCCalc = { func = function(e) e.dc = e.dc + e.level * 1.0 end } } }
- #28synergy.dbconstant
Keys:
-- (Lua Config Style) humanoid_horde = { name = "인간형 군단", desc = "던전 전체 인간형 5기 이상 시, 모든 인간형 공격력 +2", scope = "dungeon", condition = "count('tag', '인간형') >= 5", target = "dungeon", targetFilter = "tag == '인간형'", effect = "atkMin + 2" } beast_flank = { name = "야수의 호위", desc = "인간형 좌우에 야수가 있으면, 해당 야수들의 방어력 +5", scope = "lr", condition = "self.hasTag('인간형') and left.hasTag('야수') and right.hasTag('야수')", target = "lr", targetFilter = "tag == '야수'", effect = "def + 5" } fire_resonance = { name = "화염 공명", desc = "같은 층에 화염 속성 3기 이상 시, 층 전체 화염 속성 유닛 공격력 +3", scope = "floor", condition = "count('tag', '화염') >= 3", target = "floor", targetFilter = "tag == '화염'", effect = "atkMin + 3" } elite_squad = { name = "정예 부대", desc = "던전 전체 레이팅 2000 이상 시, 레이팅 150 이상인 유닛 방어력 +10", scope = "dungeon", condition = "sum('rating') >= 2000", target = "dungeon", targetFilter = "rating >= 150", effect = "def + 10" } weak_link = { name = "약점 보완", desc = "자신이 방어력 5 이하이고 좌우 아군이 방어력 20 이상이면, 자신의 방어력 +15", scope = "lr", condition = "self.def <= 5 and left.def >= 20 and right.def >= 20", target = "self", targetFilter = "", effect = "def + 15" }
- #29trace.dbconstant
Keys:
-- (Lua Config Style) c_sensation = {name = "C감각의 흔적", desc = "클리자극으로 쾌락을 느낄때 쌓인 흔적"} b_sensation = {name = "B감각의 흔적", desc = "유두 혹은 가슴자극으로 쾌락을 느낄때 쌓인 흔적"} v_sensation = {name = "V감각의 흔적", desc = "질 삽입으로 쾌락을 느낄때 쌓인 흔적"} a_sensation = {name = "A감각의 흔적", desc = "애널로 쾌락을 느낄때 흔적"} u_sensation = {name = "U감각의 흔적", desc = "요도자극으로 쾌락을 느낄때 쌓인 흔적. 요도성감이 있는 경우에만 활성화됨"} affection = {name = "애정의 흔적", desc = "애무나 포옹등의 애정행위를 할때 쌓인 흔적"} lust = {name = "발정의 흔적", desc = "쾌락을 느낄때 쌓인 흔적"} obedience = {name = "순종의 흔적", desc = "명령에 순응할때 쌓인 흔적"} submission = {name = "복종의 흔적", desc = "명령에 강제로 따를때 쌓인 흔적"} technique = {name = "기술의 흔적", desc = "섹스 또은 봉사행위를 할때 흔적"} humiliation = {name = "굴욕의 흔적", desc = "캐릭터가 수치심을 느낄때 쌓인 흔적"} pain = {name = "상처의 흔적", desc = "캐릭터가 고통을 느낄때 쌓인 흔적"} anxiety = {name = "불안의 흔적", desc = "캐릭터가 공포를 느낄때 쌓인 흔적"} negative = {name = "부정의 흔적", desc = "캐릭터의 부정적 감정으로 쌓인 흔적"}
- #30train.dbconstant
Keys:
-- (Lua Config Style) -- train - 조교 커맨드 정의 use_oil = { name = "오일 사용", category = "소모품", baseDC = 0, costHP = 10, costSP = 20, exps = {"애정경험"}, feeling = 0, command = "{{user}}는 오일을 {target}의 몸 구석구석에 펴바른다." } use_aphrodisiac = { name = "미약 사용", category = "소모품", baseDC = 0, costHP = 10, costSP = 20, exps = {"약물경험"}, feeling = 0, command = "{{user}}는 {target}에게 강제로 미약을 먹인다." } use_crystal_ball = { name = "수정구 사용", category = "소모품", baseDC = 0, costHP = 10, costSP = 20, exps = {"피사경험"}, feeling = 0, command = "{{user}}는 수정구를 꺼내 {target}의 모습을 기록하기 시작한다." } observe = { name = "가만 지켜본다", category = "애무계", tags = { "passive", "safe" }, baseDC = 0, costHP = 5, costSP = 10, exps = { "수치경험" }, feeling = 0, command = "{{user}}는 아무것도 하지 않고 {target}을 가만히 관찰한다.", } talk = { name = "대화한다", category = "애무계", tags = { "passive", "safe" }, baseDC = 0, costHP = 5, costSP = 10, exps = { "조교회화경험" }, feeling = 0, command = "{{user}}는 {target}에게 말을 건다.", } caress = { name = "애무", category = "애무계", tags = { "touch", "gentle", "love" }, baseDC = 5, costHP = 10, costSP = 20, exps = { "애정경험" }, feeling = 0, command = "{{user}}는 {target}을 어루만진다.", } cunnilingus = { name = "커닐링구스", category = "애무계", tags = { "oral", "C", "service_recv" }, baseDC = 20, costHP = 15, costSP = 30, exps = { "C경험" }, feeling = 0, command = "{{user}}는 {target}의 다리를 벌리고 비부를 혀로 핥는다.", conditions = { targetGender = "!남성" } } anal_caress = { name = "애널애무", category = "애무계", tags = { "touch", "A" }, baseDC = 30, costHP = 15, costSP = 40, exps = { "A경험" }, feeling = 0, command = "{{user}}는 {target}의 애널을 부드럽게 어루만진다.", } masturbation = { name = "자위", category = "애무계", tags = { "shame", "C" }, baseDC = 25, costHP = 15, costSP = 30, exps = { "수치경험", "자위경험", "C경험" }, feeling = 0, command = "{{user}}는 {target}에게 스스로 자위 할 것을 명령한다.", } breast_caress = { name = "가슴 애무", category = "애무계", tags = { "touch", "B", "gentle" }, baseDC = 5, costHP = 10, costSP = 20, exps = { "B경험" }, feeling = 0, command = "{{user}}는 {target}의 가슴을 어루만진다.", } kiss = { name = "키스", category = "애무계", tags = { "oral", "love", "gentle" }, baseDC = 20, costHP = 10, costSP = 20, exps = { "애정경험" }, feeling = 0, command = "{{user}}는 {target}와 입을 맞춘다.", } missionary = { name = "정상위", category = "V계", tags = { "sex", "V", "insert_V", "gentle" }, baseDC = 20, costHP = 20, costSP = 30, exps = { "V경험", "성교경험" }, feeling = 10, command = "{{user}}는 {target}의 다리 사이를 파고들어 정상위로 질에 삽입한다.", insertion = { type = "V", target = "V확장", difficulty = 1 } } doggy = { name = "후배위", category = "V계", tags = { "sex", "V", "insert_V" }, baseDC = 25, costHP = 20, costSP = 30, exps = { "V경험", "성교경험" }, feeling = 12, command = "{{user}}는 엎드린 {target}의 엉덩이를 들어올려 후배위로 질에 삽입한다.", insertion = { type = "V", target = "V확장", difficulty = 1 } } cowgirl = { name = "기승위", category = "V계", tags = { "sex", "V", "insert_V", "service" }, baseDC = 35, costHP = 25, costSP = 35, exps = { "V경험", "성교경험", "봉사경험" }, feeling = 12, command = "{{user}}는 {target}에게 기승위로 올라와 스스로 질에 삽입하도록 명령한다.", insertion = { type = "V", target = "V확장", difficulty = 1 } } anal_missionary = { name = "정상위애널", category = "A계", tags = { "sex", "A", "insert_A" }, baseDC = 30, costHP = 25, costSP = 35, exps = { "A경험", "애널성교경험", "성교경험" }, feeling = 10, command = "{{user}}는 {target}를 눕히고 다리를 잡아벌려 정상위로 애널에 삽입한다.", insertion = { type = "A", target = "A확장", difficulty = 2 } } anal_doggy = { name = "후배위애널", category = "A계", tags = { "sex", "A", "insert_A" }, baseDC = 35, costHP = 25, costSP = 35, exps = { "A경험", "애널성교경험", "성교경험" }, feeling = 10, command = "{{user}}는 엎드린 {target}의 엉덩이를 들어올려 후배위로 애널에 삽입한다.", insertion = { type = "A", target = "A확장", difficulty = 2 } } handjob = { name = "수음", category = "봉사계", tags = { "service", "touch" }, baseDC = 15, costHP = 10, costSP = 20, exps = { "봉사경험" }, feeling = 6, command = "{{user}}는 {target}에게 손으로 자지를 쓰다듬으며 봉사하라고 명령한다.", conditions = { userGender = "!여성" } } fellatio = { name = "펠라치오", category = "봉사계", tags = { "oral", "service", "M", "insert_M" }, baseDC = 25, costHP = 15, costSP = 25, exps = { "봉사경험", "펠라경험" }, feeling = 10, command = "{{user}}는 {target}에게 펠라치오를 명령한다.", insertion = { type = "M", target = null, difficulty = 1 }, conditions = { userGender = "!여성" } } rimming = { name = "리밍", category = "봉사계", tags = { "oral", "service", "filth", "A" }, baseDC = 40, costHP = 15, costSP = 35, exps = { "봉사경험", "오물경험" }, feeling = 8, command = "{{user}}는 {target}에게 {{user}}의 항문을 핥으라고 명령한다.", } honey_worm = { name = "꿀단지웜", category = "도구계", tags = { "tool", "V", "insert_V", "tentacle", "beast" }, baseDC = 30, costHP = 20, costSP = 20, exps = { "V경험", "촉수경험", "수간경험" }, feeling = 0, equipment = "honey_worm", commands = { equip = "{{user}}는 꿀단지웜을 {target}의 질에 밀어넣는다.", unequip = "{{user}}는 꿀단지웜을 {target}의 질에서 뽑아낸다." }, conditions = { item = "honey_worm", targetGender = "!남성" } } anal_worm = { name = "애널웜", category = "도구계", tags = { "tool", "A", "insert_A", "tentacle", "beast" }, baseDC = 40, costHP = 20, costSP = 20, exps = { "A경험", "촉수경험", "수간경험" }, feeling = 0, equipment = "anal_worm", commands = { equip = "{{user}}는 애널웜을 {target}의 항문에 밀어넣는다.", unequip = "{{user}}는 애널웜을 {target}의 애널에서 뽑아낸다." }, conditions = { item = "anal_worm" } } whip = { name = "채찍", category = "도구계", tags = { "tool", "pain", "hard" }, baseDC = 30, costHP = 30, costSP = 20, exps = { "피학경험" }, feeling = 0, command = "{{user}}는 채찍을 들어 {target}의 등을 내리친다.", conditions = { item = "whip" } } spanking = { name = "스팽킹", category = "하드계", tags = { "pain", "hard" }, baseDC = 20, costHP = 20, costSP = 15, exps = { "피학경험" }, feeling = 0, command = "{{user}}는 손바닥으로 {target}의 엉덩이를 세게 내려친다.", } urination = { name = "방뇨", category = "하드계", tags = { "filth", "shame" }, baseDC = 40, costHP = 15, costSP = 40, exps = { "방뇨경험", "오물경험" }, feeling = 0, command = "{{user}}는 {target}에게 그 자리에서 다리를 벌리고 오줌을 싸라고 명령한다.", } fist_v = { name = "피스트퍽", category = "하드계", tags = { "hard", "V", "insert_V", "pain" }, baseDC = 70, costHP = 50, costSP = 50, exps = { "V경험", "피학경험" }, feeling = 0, command = "{{user}}는 {target}의 질 안으로 주먹을 밀어넣는다.", conditions = { targetVExpand = ">=3" } } fist_a = { name = "애널피스트", category = "하드계", tags = { "hard", "A", "insert_A", "pain" }, baseDC = 80, costHP = 50, costSP = 50, exps = { "A경험", "피학경험" }, feeling = 0, command = "{{user}}는 {target}의 애널 안으로 주먹을 밀어넣는다.", conditions = { targetAExpand = ">=3" } } back_seated = { name = "배면좌위", category = "V계", tags = { "sex", "V", "insert_V" }, baseDC = 30, costHP = 20, costSP = 30, exps = { "V경험", "성교경험" }, feeling = 12, command = "{{user}}는 {target}을 뒤에서 안아올려 배면좌위로 질에 삽입한다.", insertion = { type = "V", target = "V확장", difficulty = 1 } } front_seated = { name = "대면좌위", category = "V계", tags = { "sex", "V", "insert_V", "gentle", "love" }, baseDC = 25, costHP = 15, costSP = 30, exps = { "V경험", "성교경험", "애정경험" }, feeling = 12, command = "{{user}}는 {target}을 마주보고 안아올려 대면좌위로 질에 삽입한다.", insertion = { type = "V", target = "V확장", difficulty = 1 } } standing = { name = "들박", category = "V계", tags = { "sex", "V", "insert_V", "hard" }, baseDC = 30, costHP = 20, costSP = 35, exps = { "V경험", "성교경험" }, feeling = 14, command = "{{user}}는 선 채로 {target}을 들어올려 대면입위로 질에 삽입한다.", insertion = { type = "V", target = "V확장", difficulty = 1 } } back_standing = { name = "뒷들박", category = "V계", tags = { "sex", "V", "insert_V", "hard" }, baseDC = 35, costHP = 20, costSP = 40, exps = { "V경험", "성교경험", "긴박경험" }, feeling = 12, command = "{{user}}는 선 채로 {target}을 들어올려 배면입위로 질에 삽입한다.", insertion = { type = "V", target = "V확장", difficulty = 1 } } mating_press = { name = "교배프레스", category = "V계", tags = { "sex", "V", "insert_V", "hard", "bondage" }, baseDC = 30, costHP = 30, costSP = 40, exps = { "V경험", "성교경험", "긴박경험" }, feeling = 15, command = "{{user}}는 {target}의 두다리를 잡고 꼼짝 못하게 누르며 교배프레스 자세로 질에 삽입한다.", insertion = { type = "V", target = "V확장", difficulty = 1 } } full_nelson = { name = "풀넬슨", category = "V계", tags = { "sex", "V", "insert_V", "hard", "bondage" }, baseDC = 40, costHP = 30, costSP = 45, exps = { "V경험", "성교경험", "긴박경험" }, feeling = 12, command = "{{user}}는 {target}의 겨드랑이 사이로 팔을 넣어 다리를 잡고 풀넬슨 체위로 질에 삽입한다.", insertion = { type = "V", target = "V확장", difficulty = 1 } } anal_front_seated = { name = "대면좌위애널", category = "A계", tags = { "sex", "A", "insert_A" }, baseDC = 35, costHP = 20, costSP = 30, exps = { "A경험", "애널성교경험", "성교경험" }, feeling = 12, command = "{{user}}는 {target}을 마주보고 안아 대면좌위로 애널에 삽입한다.", insertion = { type = "A", target = "A확장", difficulty = 2 } } anal_back_seated = { name = "배면좌위애널", category = "A계", tags = { "sex", "A", "insert_A" }, baseDC = 40, costHP = 20, costSP = 30, exps = { "A경험", "애널성교경험", "성교경험" }, feeling = 12, command = "{{user}}는 {target}을 뒤에서 안아올려 배면좌위로 애널에 삽입한다.", insertion = { type = "A", target = "A확장", difficulty = 2 } } anal_cowgirl = { name = "기승위애널", category = "A계", tags = { "sex", "A", "insert_A", "service" }, baseDC = 45, costHP = 15, costSP = 25, exps = { "A경험", "애널성교경험", "성교경험", "봉사경험" }, feeling = 12, command = "{{user}}는 {target}에게 자신의 자지위로 올라와 스스로 애널에 삽입하도록 명령한다", insertion = { type = "A", target = "A확장", difficulty = 2 } } anal_standing = { name = "들박애널", category = "A계", tags = { "sex", "A", "insert_A", "hard" }, baseDC = 30, costHP = 20, costSP = 35, exps = { "A경험", "성교경험", "애정경험" }, feeling = 14, command = "{{user}}는 선 채로 {target}을 들어올려 대면입위로 애널에 삽입한다.", insertion = { type = "A", target = "A확장", difficulty = 2 } } anal_back_standing = { name = "뒷들박애널", category = "A계", tags = { "sex", "A", "insert_A", "hard" }, baseDC = 40, costHP = 20, costSP = 45, exps = { "A경험", "성교경험", "긴박경험" }, feeling = 12, command = "{{user}}는 선 채로 {target}을 들어올려 배면입위로 애널에 삽입한다.", insertion = { type = "A", target = "A확장", difficulty = 2 } } anal_mating_press = { name = "교배프레스애널", category = "A계", tags = { "sex", "A", "insert_A", "hard", "bondage" }, baseDC = 35, costHP = 30, costSP = 45, exps = { "A경험", "성교경험", "긴박경험" }, feeling = 15, command = "{{user}}는 {target}의 두다리를 잡고 꼼짝 못하게 누르며 교배프레스 자세로 애널에 삽입한다.", insertion = { type = "A", target = "A확장", difficulty = 2 } } anal_full_nelson = { name = "풀넬슨애널", category = "A계", tags = { "sex", "A", "insert_A", "hard", "bondage" }, baseDC = 45, costHP = 30, costSP = 50, exps = { "A경험", "성교경험", "긴박경험" }, feeling = 12, command = "{{user}}는 {target}의 겨드랑이 사이로 팔을 넣어 다리를 잡고 풀넬슨 체위로 애널에 삽입한다.", insertion = { type = "A", target = "A확장", difficulty = 2 } } anal_plug = { name = "애널플러그", category = "도구계", tags = { "tool", "A", "insert_A" }, equipment = "anal_plug", baseDC = 25, costHP = 15, costSP = 10, exps = { "A경험", "A확장경험" }, commands = { equip = "{{user}}는 {target}의 항문에 애널플러그를 꽂아넣는다.", unequip = "{{user}}는 {target}의 항문에서 애널플러그를 뽑아낸다." } } anal_beads = { name = "애널비즈", category = "도구계", tags = { "tool", "A", "insert_A" }, equipment = "anal_beads", baseDC = 35, costHP = 20, costSP = 20, exps = { "A경험", "A확장경험" }, commands = { equip = "{{user}}는 {target}의 애널에 비즈를 하나씩 밀어넣는다.", unequip = "{{user}}는 {target}의 애널에 비즈를 하나씩 뽑아낸다." } } blindfold = { name = "눈가리개", category = "도구계", tags = { "tool", "bondage" }, equipment = "blindfold", baseDC = 10, costHP = 5, costSP = 5, exps = { "긴박경험" }, commands = { equip = "{{user}}는 {target}의 눈을 가리개로 가려 시야를 차단한다.", unequip = "{{user}}는 {target}에게서 눈 가리개를 떼어낸다." } } ball_gag = { name = "볼개그", category = "도구계", tags = { "tool", "bondage", "shame" }, equipment = "ball_gag", baseDC = 15, costHP = 10, costSP = 10, exps = { "수치경험", "긴박경험" }, commands = { equip = "{{user}}는 {target}의 입에 볼개그를 물린다.", unequip = "{{user}}는 {target}의 입에서 볼개그를 뺀다." } } ring_gag = { name = "O링개그", category = "도구계", tags = { "tool", "bondage", "shame" }, equipment = "ring_gag", baseDC = 20, costHP = 15, costSP = 15, exps = { "수치경험", "긴박경험" }, commands = { equip = "{{user}}는 {target}의 입을 O링 개그로 고정시켜 벌려둔다.", unequip = "{{user}}는 {target}의 입에서 O링 개그를 뺀다." } } clit_cap = { name = "클리캡", category = "도구계", tags = { "tool", "C", "tentacle" }, equipment = "slime_snail", baseDC = 20, costHP = 15, costSP = 10, exps = { "C경험", "촉수경험" }, commands = { equip = "{{user}}는 점액달팽이를 {target}의 성기에 붙인다.", unequip = "{{user}}는 점액달팽이를 {target}의 성기에서 떼어낸다." } } nipple_cap = { name = "유두캡", category = "도구계", tags = { "tool", "B", "tentacle" }, equipment = "milk_snail", baseDC = 20, costHP = 15, costSP = 10, exps = { "B경험", "촉수경험" }, commands = { equip = "{{user}}는 우유달팽이를 {target}의 유두에 붙인다.", unequip = "{{user}}는 우유달팽이를 {target}의 유두에서 떼어낸다." } } milker = { name = "착유기", category = "도구계", tags = { "tool", "B", "machine" }, equipment = "milker", baseDC = 25, costHP = 20, costSP = 20, exps = { "B경험", "분유경험" }, commands = { equip = "{{user}}는 {target}의 가슴에 착유기를 장착해 착유를 시도한다.", unequip = "{{user}}는 {target}의 가슴에 착유기를 떼어내 우유를 확인한다." } } double_fist = { name = "더블피스트퍽", category = "하드계", tags = { "hard", "V", "insert_V", "pain", "fist" }, baseDC = 80, costHP = 60, costSP = 60, exps = { "V경험", "피학경험" }, command = "{{user}}는 {target}의 질 안으로 두 주먹을 모두 밀어넣는다." } anal_double_fist = { name = "애널더블피스트", category = "하드계", tags = { "hard", "A", "insert_A", "pain", "fist" }, baseDC = 90, costHP = 60, costSP = 60, exps = { "A경험", "피학경험" }, command = "{{user}}는 {target}의 애널 안으로 두 주먹을 모두 밀어넣는다." } double_hole_fist = { name = "양구멍피스트", category = "하드계", tags = { "hard", "V", "A", "insert_V", "insert_A", "pain", "fist" }, baseDC = 100, costHP = 70, costSP = 70, exps = { "V경험", "A경험", "피학경험" }, command = "{{user}}는 {target}의 질과 애널 양쪽에 주먹을 쑤셔넣는다." } sumata = { name = "스마타", category = "봉사계", tags = { "service", "sex" }, baseDC = 20, costHP = 15, costSP = 20, exps = { "봉사경험", "성교경험" }, feeling = 8, command = "{{user}}는 {target}에게 허벅지로 자지를 비비며 봉사하도록 명령한다." } paizuri = { name = "파이즈리", category = "봉사계", tags = { "service", "B", "sex" }, baseDC = 25, costHP = 15, costSP = 25, exps = { "봉사경험", "B경험" }, feeling = 6, command = "{{user}}는 {target}에게 가슴을 자신의 자지에 비비며 봉사하라고 명령한다." } breast_rubbing = { name = "가슴비비기", category = "봉사계", tags = { "service", "B" }, baseDC = 15, costHP = 15, costSP = 25, exps = { "봉사경험", "B경험" }, feeling = 6, command = "{{user}}는 {target}에게 가슴을 자신의 가슴에 비비며 봉사하라고 명령한다." } footjob = { name = "풋잡", category = "봉사계", tags = { "service", "touch", "leg", "shame" }, baseDC = 30, costHP = 15, costSP = 30, exps = { "봉사경험", "피학경험" }, feeling = 5, command = "{{user}}는 {target}에게 발로 자지를 밟고 문지르며 봉사하도록 명령한다.", } six_nine = { name = "식스나인", category = "봉사계", tags = { "service", "oral", "sex" }, baseDC = 35, costHP = 20, costSP = 35, exps = { "봉사경험", "펠라경험", "C경험" }, feeling = 12, command = "{{user}}는 {target}와 서로의 성기를 마주대고 69자세로 서로를 핥는다.", } oil_massage = { name = "오일마사지", category = "봉사계", tags = { "service", "touch", "oil" }, baseDC = 20, costHP = 15, costSP = 25, exps = { "봉사경험", "애정경험" }, feeling = 5, command = "{{user}}는 {target}에게 몸에 오일을 바르고 {user}의 몸을 마사지하도록 명령한다.", } cunnilingus_service = { name = "커닐링구스봉사", category = "봉사계", tags = { "service", "oral" }, baseDC = 30, costHP = 15, costSP = 30, exps = { "봉사경험", "C경험" }, feeling = 8, command = "{{user}}는 {target}에게 자신의 보지를 핥으라고 명령한다.", } tortoise_shell = { name = "귀갑묶기", category = "하드계", tags = { "bondage", "rope", "hard" }, equipment = "rope", baseDC = 20, costHP = 10, costSP = 20, exps = { "긴박경험" }, dcPenaltyMod = 0.9, commands = { equip = "{{user}}는 밧줄로 {target}을 귀갑묶기로 결박한다.", unequip = "{{user}}는 {target}를 결박했던 로프를 풀어준다." } } outdoor_play = { name = "야외플레이", category = "하드계", tags = { "outdoor", "shame", "hard" }, equipment = "collar", baseDC = 0, costHP = 10, costSP = 30, exps = { "수치경험" }, dcPenaltyMod = 1.1, commands = { equip = "{{user}}는 {target}에게 개목걸이를 채워 몬스터들이 돌아다니는 야외로 끌고나간다.", unequip = "{{user}}는 {target}를 조교실로 데려와 개목걸이를 풀어준다." } } bestiality = { name = "수간", category = "하드계", tags = { "bestiality", "V", "insert_V", "hard" }, baseDC = 60, costHP = 40, costSP = 50, exps = { "수간경험", "V경험" }, command = "{{user}}는 {target}을 짐승과 교미하게 한다." } enema = { name = "관장", category = "하드계", tags = { "filth", "A", "liquid", "hard" }, baseDC = 40, costHP = 30, costSP = 40, exps = { "오물경험", "A경험", "피학경험" }, command = "{{user}}는 {target}의 항문에 관장액을 주입하고 참으라고 명령한다." } enema_plug = { name = "관장+플러그", category = "하드계", tags = { "filth", "A", "insert_A", "hard" }, equipment = "anal_plug", baseDC = 40, costHP = 30, costSP = 40, exps = { "A경험", "A확장경험", "오물경험" }, commands = { equip = "{{user}}는 {target}에게 관장액을 주입하고 플러그로 막아버린다.", unequip = "{{user}}는 {target}에게서 애널플러그를 빼낸다." } } drink_urine = { name = "음뇨", category = "하드계", tags = { "filth", "oral", "shame", "hard" }, baseDC = 60, costHP = 20, costSP = 50, exps = { "오물경험", "봉사경험", "피학경험" }, command = "{{user}}는 {target}에게 자신의 오줌을 받아 마시게 한다." } spread_pussy = { name = "조개벌리기", category = "애무계", tags = { "shame", "C", "open_body" }, baseDC = 18, costHP = 15, costSP = 30, exps = { "수치경험", "자위경험" }, command = "{{user}}는 {target}가 스스로 보지를 벌려보이도록 명령한다.", conditions = { targetGender = "!남성" } } fingering = { name = "손가락 넣기", category = "애무계", tags = { "sex", "V", "insert_V", "finger" }, baseDC = 18, costHP = 15, costSP = 30, exps = { "V경험" }, insertion = { type = "V", target = null, difficulty = 1 }, command = "{{user}}는 {target}의 질 안으로 손가락을 밀어넣는다.", conditions = { targetGender = "!남성" } } anal_lick = { name = "애널 핥기", category = "애무계", tags = { "oral", "A", "filth" }, baseDC = 45, costHP = 15, costSP = 35, exps = { "A경험" }, command = "{{user}}는 {target}의 애널 주변을 혀로 핥는다." } grinding = { name = "가랑이 비비기", category = "애무계", tags = { "service", "C", "sex" }, baseDC = 20, costHP = 15, costSP = 25, exps = { "봉사경험", "C경험" }, feeling = 5, command = "{{user}}는 {target}의 가랑이에 자신의 가랑이를 문지르며 애무한다.", conditions = { targetGender = "!남성", userGender = "!남성" } } heart_stone = { name = "심장석 애무", category = "도구계", tags = { "tool", "C", "B" }, equipment = "heart_stone", baseDC = 10, costHP = 10, costSP = 10, exps = { "C경험", "B경험" }, command = "{{user}}는 심장석을 사용해 {target}의 성감대를 자극한다.", conditions = { item = "heart_stone" } }
- #31trait.dbconstant
Keys:
-- (Lua Config Style) -- trait - 평탄화된 구조 (category, group 활용) curious = { name = "호기심", desc = "새로운 자극과 변화를 선호함. 조교 성공 확률이 높아진다. abl이 높은 분야의 조교 효과가 감소한다.", category = "성격", group = "openness", events = { onDCCalc = { func = function(e) e.dc = e.dc - 5 end } } } cautious = { name = "신중함", desc = "익숙하고 안정적인 것을 선호함. abl이 높은 분야의 조교 효과가 증가한다. 조교 성공 확률이 낮아진다.", category = "성격", group = "openness", events = { onDCCalc = { func = function(e) e.dc = e.dc + 5 end } } } cowardly = { name = "겁쟁이", desc = "비관적이며 겁이 많고 우울함. 굴복 흔적을 더 많이 획득한다. 공포 흔적과 부정 흔적도 더 많이 획득한다.", category = "성격", group = "optimism", events = { onStatEXP = { func = function(e) if e.statName == '굴복' or e.statName == '공포' or e.statName == '부정' then e.statEXP = e.statEXP * 1.3 end end } } } optimistic = { name = "낙관적", desc = "상황을 긍정적으로 보며 활기참. 공포 흔적과 부정 흔적을 적게 획득한다. 기술 흔적과 수치 흔적도 적게 획득한다.", category = "성격", group = "optimism", events = { onStatEXP = { func = function(e) if e.statName == '공포' or e.statName == '부정' or e.statName == '굴욕' or e.statName == '수치' then e.statEXP = e.statEXP * 0.7 end end } } } sociable = { name = "사교적", desc = "타인의 감정에 쉽게 공감하며 칭찬과 비난에 민감함. 각종 쾌락 흔적의 획득량이 증가한다.", category = "성격", group = "social", events = { onStatEXP = { hasTags = { "C", "V", "A", "B" }, func = function(e) if e.statName == 'C쾌락' or e.statName == 'V쾌락' or e.statName == 'A쾌락' or e.statName == 'B쾌락' then e.statEXP = e.statEXP * 1.2 end end } } } indifferent = { name = "무던함", desc = "타인의 시선을 신경쓰지 않으며 감정적으로 안정됨. 조교시 기력의 소모가 감소한다. 모든 흔적의 획득량이 감소한다.", category = "성격", group = "social", events = { onCostCalc = { func = function(e) e.costSP = e.costSP * 0.8 end }, onStatEXP = { func = function(e) if e.statName == '공포' or e.statName == '부정' or e.statName == '굴욕' or e.statName == '수치' or e.statName == '고통' or e.statName == '불쾌' or e.statName == '저항' then e.statEXP = e.statEXP * 0.8 end end } } } steadfast = { name = "굳건함", desc = "어떤 상황에서도 쉽게 포기하지 않음. 조교 명령에 따를 확률이 크게 내려가지만 고통 흔적과 부정 흔적의 획득량이 감소한다.", category = "성격", group = "resilience", events = { onDCCalc = { func = function(e) e.dc = e.dc + 5 end } } } fragile = { name = "쉽게 포기함", desc = "쉽게 지치고 좌절함. 조교 명령에 따를 확률이 높아진다. 소모되는 체력과 기력이 크게 증가한다.", category = "성격", group = "resilience", events = { onDCCalc = { func = function(e) e.dc = e.dc - 5 end }, onCostCalc = { func = function(e) e.costHP = e.costHP * 1.3; e.costSP = e.costSP * 1.3 end } } } mature = { name = "성숙함", desc = "충동을 잘 조절하며 인격적으로 성숙함. 반발각인이 3으로 올라가지 않지만 중독 관련 abl의 레벨업에 필요한 흔적이 크게 증가한다.", category = "성격", group = "maturity" } dependent = { name = "의존적", desc = "목표의식이 부족하고 자신의 문제를 남 탓으로 돌리며 충동적임. 조교 성공확률에 행운이 적용된다. 부정 흔적의 획득량이 증가한다.", category = "성격", group = "maturity" } altruistic = { name = "이타적", desc = "타인을 위해 본인을 희생하는 것에서 기쁨을 느낀다. 봉사 계열 조교의 효과가 증가한다. 애무 계열 조교의 효과가 감소한다.", category = "성격", group = "egoism", events = { onStatEXP = { { hasTags = { "service" }, func = function(e) e.statEXP = e.statEXP * 1.3 end }, { hasTags = { "touch" }, notTags = { "service" }, func = function(e) e.statEXP = e.statEXP * 0.7 end } } } } selfish = { name = "이기적", desc = "자신의 이익만을 좇는다. 애무 계열 조교의 효과가 증가한다. 봉사 계열 조교의 효과가 감소한다.", category = "성격", group = "egoism", events = { onStatEXP = { { hasTags = { "touch" }, func = function(e) e.statEXP = e.statEXP * 1.3 end }, { hasTags = { "service" }, func = function(e) e.statEXP = e.statEXP * 0.7 end } } } } simple = { name = "단순함", desc = "생각이 깊지 않고 단순하다. 쾌락에 솔직하다.", category = "성격", group = "complexity", events = { onOrgasmCalc = { func = function(e) e.orgasm = e.orgasm * 1.2 end } } } c_dull = { name = "C둔감", desc = "클리토리스에 대한 자극에 둔감하다. 관련 조교의 효과나 성공확률이 감소한다.", category = "신체특성", group = "c_sensation", events = { onDCCalc = { hasTags = { "C" }, func = function(e) e.dc = e.dc + 5 end }, onStatEXP = { hasTags = { "C" }, func = function(e) if e.statName == 'C쾌락' then e.statEXP = e.statEXP * 0.7 end end } } } c_sensitive = { name = "C민감", desc = "클리토리스에 대한 자극에 민감하다. 관련 조교의 효과나 성공확률이 증가한다.", category = "신체특성", group = "c_sensation", events = { onDCCalc = { hasTags = { "C" }, func = function(e) e.dc = e.dc - 5 end }, onStatEXP = { hasTags = { "C" }, func = function(e) if e.statName == 'C쾌락' then e.statEXP = e.statEXP * 1.5 end end } } } v_dull = { name = "V둔감", desc = "질 삽입에 대한 자극에 둔감하다. 관련 조교의 효과나 성공확률이 감소한다.", category = "신체특성", group = "v_sensation", events = { onDCCalc = { hasTags = { "V" }, func = function(e) e.dc = e.dc + 5 end }, onStatEXP = { hasTags = { "V" }, func = function(e) if e.statName == 'V쾌락' then e.statEXP = e.statEXP * 0.7 end end } } } v_sensitive = { name = "V민감", desc = "질 삽입에 대한 자극에 민감하다. 관련 조교의 효과나 성공확률이 증가한다.", category = "신체특성", group = "v_sensation", events = { onDCCalc = { hasTags = { "V" }, func = function(e) e.dc = e.dc - 5 end }, onStatEXP = { hasTags = { "V" }, func = function(e) if e.statName == 'V쾌락' then e.statEXP = e.statEXP * 1.5 end end } } } v_masterpiece = { name = "V명기", desc = "질 삽입 시 상대에게도 큰 자극을 준다. 관련 조교의 효과가 크게 증가하고 상대방의 사정게이지 획득에 보너스.", category = "신체특성", group = "v_sensation", events = { onDCCalc = { hasTags = { "V" }, func = function(e) e.dc = e.dc - 10 end }, onStatEXP = { hasTags = { "V" }, func = function(e) if e.statName == 'V쾌락' then e.statEXP = e.statEXP * 2.0 end end } } } a_dull = { name = "A둔감", desc = "애널 삽입에 대한 자극에 둔감하다. 관련 조교의 효과나 성공확률이 감소한다.", category = "신체특성", group = "a_sensation", events = { onDCCalc = { hasTags = { "A" }, func = function(e) e.dc = e.dc + 5 end }, onStatEXP = { hasTags = { "A" }, func = function(e) if e.statName == 'A쾌락' then e.statEXP = e.statEXP * 0.7 end end } } } a_sensitive = { name = "A민감", desc = "애널 삽입에 대한 자극에 민감하다. 관련 조교의 효과나 성공확률이 증가한다.", category = "신체특성", group = "a_sensation", events = { onDCCalc = { hasTags = { "A" }, func = function(e) e.dc = e.dc - 5 end }, onStatEXP = { hasTags = { "A" }, func = function(e) if e.statName == 'A쾌락' then e.statEXP = e.statEXP * 1.5 end end } } } a_masterpiece = { name = "A명기", desc = "애널 삽입 시 상대에게도 큰 자극을 준다. 관련 조교의 효과가 크게 증가하고 상대방의 사정게이지 획득에 보너스.", category = "신체특성", group = "a_sensation", events = { onDCCalc = { hasTags = { "A" }, func = function(e) e.dc = e.dc - 10 end }, onStatEXP = { hasTags = { "A" }, func = function(e) if e.statName == 'A쾌락' then e.statEXP = e.statEXP * 2.0 end end } } } b_dull = { name = "B둔감", desc = "가슴이나 유두에 대한 자극에 둔감하다. 관련 조교의 효과나 성공확률이 감소한다.", category = "신체특성", group = "b_sensation", events = { onDCCalc = { hasTags = { "B" }, func = function(e) e.dc = e.dc + 5 end }, onStatEXP = { hasTags = { "B" }, func = function(e) if e.statName == 'B쾌락' then e.statEXP = e.statEXP * 0.7 end end } } } b_sensitive = { name = "B민감", desc = "가슴이나 유두에 대한 자극에 민감하다. 관련 조교의 효과나 성공확률이 증가한다.", category = "신체특성", group = "b_sensation", events = { onDCCalc = { hasTags = { "B" }, func = function(e) e.dc = e.dc - 5 end }, onStatEXP = { hasTags = { "B" }, func = function(e) if e.statName == 'B쾌락' then e.statEXP = e.statEXP * 1.5 end end } } } throat_pleasure = { name = "인후성감", desc = "펠라 관련 조교의 효과가 증가하고 M쾌락으로 사정게이지를 얻을 수 있다.", category = "신체특성", events = { onStatEXP = { hasTags = { "M" }, func = function(e) if e.statName == 'M쾌락' then e.statEXP = e.statEXP * 1.5 end end } } } urethra_pleasure = { name = "요도성감", desc = "요도 조교의 효과가 증가하고 U쾌락으로 사정게이지를 얻을 수 있다.", category = "신체특성", events = { onStatEXP = { hasTags = { "insert_U" }, func = function(e) if e.statName == 'U쾌락' then e.statEXP = e.statEXP * 1.5 end end } } } easy_wet = { name = "젖기쉬움", desc = "작은 자극에도 쉽게 애액이 나온다. 윤활 stat 증가에 보너스.", category = "신체특성", group = "wetness" } hard_wet = { name = "젖기어려움", desc = "성적으로 흥분한 상태에서도 애액이 잘 나오지 않는다. 윤활 stat 증가에 페널티.", category = "신체특성", group = "wetness" } enteric_body = { name = "장액체질", desc = "흥분하면 애널에서도 애액이 분비되는 특이체질. A윤활이 쉽게 증가하고 애널관련 조교의 효과도 증가한다.", category = "신체특성", events = { onStatEXP = { hasTags = { "A" }, func = function(e) if e.statName == 'A윤활' then e.statEXP = e.statEXP * 1.2 end end } } } forked_tongue = { name = "갈라진 혀", desc = "뱀처럼 갈라진 혀. 키스나 펠라관련 봉사의 효과가 증가한다.", category = "신체특성", events = { onStatEXP = { hasTags = { "oral" }, func = function(e) if e.statName == 'M쾌락' or e.statName == '봉사기술' then e.statEXP = e.statEXP * 1.3 end end } } } fast_recovery = { name = "빠른회복", desc = "낮에도 체력 및 기력을 조금 회복합니다.", category = "신체특성", group = "recovery", events = { onDayPass = { func = function(e) e.hpGen = e.hpGen + 0.05; e.spGen = e.spGen + 0.05 end } } } slow_recovery = { name = "느린회복", desc = "체력 및 기력 회복속도가 느립니다.", category = "신체특성", group = "recovery", events = { onDayPass = { func = function(e) e.hpGen = e.hpGen - 0.05; if not e.isNight then e.hpGen = 0 end end } } } immature = { name = "미숙", desc = "아직 다 자라지 않았다. V조교 실행 불가.", category = "신체특성" } always_heat = { name = "상시발정", desc = "별다른 자극 없이도 늘 젖어있다.", category = "신체특성", events = { onDCCalc = { hasTags = { "sex" }, func = function(e) e.dc = e.dc - 5 end } } } hairless = { name = "무모증", desc = "음부 털이 자라지 않는다.", category = "신체특성", group = "hair" } hairy = { name = "다모증", desc = "음부 털이 빠르게 자란다.", category = "신체특성", group = "hair" } flat_chest = { name = "빈유", desc = "가슴이 작다.", category = "신체특성", group = "chest" } big_boobs = { name = "폭유", desc = "가슴이 매우 크다.", category = "신체특성", group = "chest" } sex_addict = { name = "성교중독", desc = "섹스행위에 중독되었다.", category = "성적취향", events = { onDCCalc = { hasTags = { "sex" }, func = function(e) e.dc = e.dc - 10 end }, onOrgasmCalc = { hasTags = { "sex" }, func = function(e) e.orgasm = e.orgasm * 1.3 end } } } masturbation_addict = { name = "자위중독", desc = "자위행위에 중독되었다.", category = "성적취향" } semen_addict = { name = "정액중독", desc = "상대의 사정을 보거나 그 정액을 마실때 흥분한다.", category = "성적취향" } urine_addict = { name = "방뇨중독", desc = "자꾸 오줌을 싸고 싶어한다. 오줌을 쌀때 쾌감을 느낌다.", category = "성적취향", events = { onDCCalc = { hasTags = { "filth" }, func = function(e) e.dc = e.dc - 10 end } } } prostitution_addict = { name = "매춘중독", desc = "매춘이 주는 돈과 쾌락에 푹 빠졌다.", category = "성적취향" } tentacle_addict = { name = "촉수중독", desc = "촉수와의 섹스를 갈구하게 되었다.", category = "성적취향", events = { onDCCalc = { hasTags = { "tentacle" }, func = function(e) e.dc = e.dc - 10 end }, onOrgasmCalc = { hasTags = { "tentacle" }, func = function(e) e.orgasm = e.orgasm * 1.5 end } } } beast_addict = { name = "수간중독", desc = "인간형 상대보다는 동물과 섹스하는데 몰두하고있다.", category = "성적취향", events = { onDCCalc = { hasTags = { "beast" }, func = function(e) e.dc = e.dc - 10 end }, onOrgasmCalc = { hasTags = { "beast" }, func = function(e) e.orgasm = e.orgasm * 1.5 end } } } masochist = { name = "마조", desc = "피학행위를 통해 쾌락을 느낌. S쾌락으로 사정게이지를 얻을 수 있다.", category = "성적취향", group = "sm", events = { onDCCalc = { hasTags = { "pain" }, func = function(e) e.dc = e.dc - 10 end }, onOrgasmCalc = { hasTags = { "pain" }, func = function(e) e.orgasm = e.orgasm + (e.statEXP['고통'] or 0) end } } } degrader = { name = "디그레이더", desc = "수치를 당하는 데서 쾌감을 느낌. 수치 흔적 획득시 같은 양의 욕정 흔적을 획득.", category = "성적취향", group = "sub", events = { onOrgasmCalc = { hasTags = { "shame" }, func = function(e) e.orgasm = e.orgasm + (e.allStatEXP['수치'] or 0) end }, onStatEXP = { func = function(e) if e.statName == '수치' then e.allStatEXP['욕정'] = (e.allStatEXP['욕정'] or 0) + e.statEXP end end } } } brat = { name = "브랫", desc = "일부러 반항하거나 말썽을 부리고 강제로 굴복당하는데서 쾌감을 느낌.", category = "성적취향", group = "sub", events = { onDCCalc = { func = function(e) e.dc = e.dc + 5 end } } } rope_bunny = { name = "로프버니", desc = "로프나 수갑 등으로 구속당할때 쾌락을 느낌. 구속 조교의 효과가 증가하고 S쾌락 stat을 추가로 획득", category = "성적취향", events = { onDCCalc = { hasTags = { "bondage" }, func = function(e) e.dc = e.dc - 10 end }, onStatEXP = { hasTags = { "bondage" }, func = function(e) if e.statName == 'S쾌락' then e.statEXP = e.statEXP * 1.5 end end } } } ignore_filth = { name = "불결무시", desc = "다소 더러운 행위를 당하거나 강요받아도 불쾌 stat과 저항 stat이 적게 증가한다.", category = "성적취향", group = "hygiene", events = { onDCCalc = { hasTags = { "filth" }, func = function(e) e.dc = e.dc - 5 end } } } sensitive_hygiene = { name = "위생민감", desc = "조금만 더러운 행위를 당하거나 강요받아도 불쾌 stat과 저항 stat이 많이 증가한다.", category = "성적취향", group = "hygiene", events = { onDCCalc = { hasTags = { "filth" }, func = function(e) e.dc = e.dc + 10 end } } } title_unfallen = { name = "미함락", desc = "아직 {{user}}에게 함락당하지 않았다.", category = "칭호", group = "title" } title_love = { name = "연모", desc = "{{user}}를 좋아하게 되어 무엇이든 잘 따른다.", category = "칭호", group = "title", events = { onDCCalc = { func = function(e) e.dc = e.dc - 15 end } } } title_lewd = { name = "음란", desc = "섹스에 흠뻑 빠진 상태. 쾌락을 위해서라면 무엇이든 기꺼이 할 수 있다.", category = "칭호", group = "title", events = { onDCCalc = { hasTags = { "sex" }, func = function(e) e.dc = e.dc - 20 end }, onOrgasmCalc = { func = function(e) e.orgasm = e.orgasm * 1.3 end } } } title_bitch = { name = "암캐", desc = "짐승과 섹스하는 것에 심취한 상태.", category = "칭호", group = "title", events = { onDCCalc = { hasTags = { "beast" }, func = function(e) e.dc = e.dc - 20 end } } } title_broken = { name = "붕괴", desc = "혹독한 경험으로 인해 자아를 잃고 모든걸 포기한 상태.", category = "칭호", group = "title", events = { onDCCalc = { func = function(e) e.dc = e.dc - 30 end } } } title_madness = { name = "광기", desc = "혹독한 경험으로 인해 미쳐버림. 매춘 또는 매각시 상당한 페널티를 갖는다.", category = "칭호", group = "title" }
- #32trap.dbconstant
Keys:
-- (Lua Config Style) arrow_trap = { name = "화살 함정", desc = "벽면에 숨겨진 발사 장치에서 화살이 발사되는 고전적인 함정. 압력 감지판을 밟으면 작동한다.", price = 500, rating = 100, effects = {}, tags = {"함정", "물리", "원거리"} } poison_gas_trap = { name = "독가스 함정", desc = "밀폐된 공간에 독가스를 분사하는 함정. 해독제가 없으면 치명적일 수 있다.", price = 1500, rating = 250, effects = {}, tags = {"함정", "독", "범위"} } spike_pit = { name = "가시구덩이", desc = "위장된 바닥 아래 날카로운 가시가 촘촘히 박혀 있는 구덩이 함정.", price = 800, rating = 150, effects = {}, tags = {"함정", "물리", "낙하"} } fire_glyph = { name = "화염의 문양", desc = "바닥에 새겨진 마법 문양. 밟으면 폭발적인 화염이 솟아오른다.", price = 2500, rating = 400, effects = {}, tags = {"함정", "화염", "마법"} } ice_floor_trap = { name = "빙결 바닥", desc = "마력이 깃든 바닥 타일. 지나가는 자의 발을 얼려 이동을 봉쇄한다.", price = 2000, rating = 350, effects = {}, tags = {"함정", "냉기", "속박"} } shock_wire = { name = "감전 와이어", desc = "눈에 잘 보이지 않는 전기가 흐르는 가느다란 와이어. 접촉하면 강한 전류가 흐른다.", price = 1200, rating = 200, effects = {}, tags = {"함정", "전격", "감지"} } net_launcher = { name = "그물 발사 장치", desc = "천장에 설치된 그물 발사기. 침입자를 생포할 때 유용하다.", price = 1000, rating = 150, effects = {}, tags = {"함정", "속박", "포획"} } sleep_mist = { name = "수면 연무", desc = "흡입하면 깊은 잠에 빠지게 만드는 마법의 연무를 분사하는 장치.", price = 3000, rating = 500, effects = {}, tags = {"함정", "정신", "수면"} } acid_spray = { name = "산성 분무기", desc = "강력한 산성 액체를 분사하는 함정. 장비를 녹이고 심한 화상을 입힌다.", price = 2200, rating = 380, effects = {}, tags = {"함정", "산성", "부식"} } blade_corridor = { name = "회전 칼날 복도", desc = "복도 양쪽에서 회전하는 거대한 칼날이 주기적으로 지나가는 구역.", price = 4000, rating = 600, effects = {}, tags = {"함정", "물리", "연속"} } teleport_trap = { name = "전이 함정", desc = "밟으면 미리 지정된 장소로 강제 이동시키는 마법 함정.", price = 5000, rating = 450, effects = {}, tags = {"함정", "마법", "이동"} } darkness_trap = { name = "암흑 함정", desc = "작동하면 주변 일대를 칠흑같은 어둠으로 뒤덮는 마법 장치.", price = 1800, rating = 280, effects = {}, tags = {"함정", "암흑", "마법"} }
- #33System
Keys: folder:0ab6e9ab-0d70-4849-b3f4-9e61f1dffa4e
- #34item.sysconstant
Keys:
-- item.sys.txt -- 아이템/상점 관리 통합 모듈 -- 사용법: sysFunction(triggerId, "item.sys", "select", itemKey) -- sysFunction(triggerId, "item.sys", "category", itemCat)
(function() --------------------------------------- -- 디스패처 테이블 및 로컬 변수 --------------------------------------- local funcs = {}
--------------------------------------- -- 함수 정의 --------------------------------------- -- 개별 아이템 선택 (상세정보 표시) function funcs.select(triggerId, itemKey) -- debug(string.format("[Item:Select] Key: %s", itemKey)) setChatVar(triggerId, "itemKey", itemKey) setChatVar(triggerId, "itemQuantity", 1) -- trap.db에서 먼저 검색 local trapDB = getDB(triggerId, "trap.db") if trapDB[itemKey] then local trap = trapDB[itemKey] setChatVar(triggerId, "itemName", trap.name) setChatVar(triggerId, "itemPrice", intForm(trap.price)) setChatVar(triggerId, "itemTotalPrice", intForm(trap.price)) setChatVar(triggerId, "itemDetail", trap.desc) return end -- items.db에서 검색 (새 객체 구조) local items = getDB(triggerId, "items.db") for _, category in pairs(items) do if category[itemKey] then local item = category[itemKey] setChatVar(triggerId, "itemName", item.name) setChatVar(triggerId, "itemPrice", intForm(item.price)) setChatVar(triggerId, "itemTotalPrice", intForm(item.price)) setChatVar(triggerId, "itemDetail", item.desc) return end end end -- 카테고리별 아이템 목록 생성 function funcs.category(triggerId, itemCat) debug(string.format("[Item:Category] Switched to: %s", itemCat)) local itemList local orderedKeys -- traps는 별도의 trap.db에서 로드 if itemCat == "traps" then itemList = getDB(triggerId, "trap.db") orderedKeys = json.decode(getChatVar(triggerId, "trapsList")) else itemList = getDB(triggerId, "items.db")[itemCat] orderedKeys = json.decode(getChatVar(triggerId, itemCat .. "List")) end local contents = "" local inventory = json.decode(getChatVar(triggerId, "inventory")) -- 조합지식이 필요한 아이템 목록 local needKnowl = { ["nutrient"] = true, ["diuretic"] = true, ["aphrodisiac"] = true, ["dewormer"] = true, ["hair_tonic"] = true } -- orderedKeys를 순회하여 itemList에서 정보를 가져옴 for _, k in ipairs(orderedKeys) do local v = itemList[k] if v then local condition = true local reason = "" -- 새 객체 구조에서 필드 접근 local itemName = v.name local itemPrice = v.price if k == "unlock2" and inventory["unlock1"] == "0" then condition = false reason = "조합지식 필요" elseif k == "unlock3" and inventory["unlock2"] == "0" then condition = false reason = "마족정보 필요" elseif k == "unlock4" and inventory["unlock3"] == "0" then condition = false reason = "음마지식 필요" elseif (itemCat == "tools" or itemCat == "unlock") and inventory[k] == "1" then condition = false reason = "이미 보유중" elseif tonumber(inventory[k] or 0) >= 99 then condition = false reason = "이미 최대로 보유중" elseif needKnowl[k] and inventory["unlock1"] == "0" then condition = false reason = "조합지식 필요" end if condition then local price = intForm(itemPrice) contents = contents .. "<div class='shop-item btn' risu-btn='" .. k .. "'><div class='item-image'><img src='{{raw::" .. k .. ".webp}}'></div><div class='item-info'><span class='item-name'>" .. itemName .. "</span><span class='item-price'>" .. price .. " spirit</span></div></div>" end end end if contents == "" then contents = "<div class='shop-item btn'><div class='item-image'><img src='{{raw::empty.png}}'></div><div class='item-info'><span class='item-name'>모두 구매하였습니다.</span></div></div>" end setChatVar(triggerId, "tools", contents) setChatVar(triggerId, "itemName", "아이템을 선택하세요") setChatVar(triggerId, "itemQuantity", 0) setChatVar(triggerId, "itemTotalPrice", 0) setChatVar(triggerId, "itemDetail", "") end -- 아이템 소모 function funcs.consume(triggerId, itemKey, quantity) quantity = quantity or 1 local inventory = json.decode(getChatVar(triggerId, "inventory")) local current = tonumber(inventory[itemKey]) or 0 if current < quantity then alertNormal(triggerId, "아이템이 부족합니다.") return false end inventory[itemKey] = tostring(max(0, current - quantity)) setChatVar(triggerId, "inventory", json.encode(inventory)) return true end --------------------------------------- -- 메인 함수 (외부에서 호출됨) --------------------------------------- return function(triggerId, subFunc, ...) if funcs[subFunc] then return funcs[subFunc](triggerId, ...) else debug("item.sys: 알 수 없는 함수 - " .. tostring(subFunc)) end endend)()
- #35LvUp.sysconstant
Keys:
-- LvUp.sys.txt -- 캐릭터 레벨업 처리 모듈
(function() --------------------------------------- -- ★ 밸런싱 상수 정의 (Balancing Constants) ---------------------------------------
-- [경험치 테이블 오프셋] local EXP_LEVEL_OFFSET = 3 -- 레벨 N 도달에 필요한 경험치 = EXPtable[N + 이 값] -- [경험치 테이블 오프셋] local EXP_LEVEL_OFFSET = 3 -- 레벨 N 도달에 필요한 경험치 = EXPtable[N + 이 값] --------------------------------------- local funcs = {} local function getClassData(triggerId, className) local classDB = getDB(triggerId, "class.db") for key, data in pairs(classDB) do if data.name == className then return data end end return { hp_inc=100, sp_inc=100, def_inc=5, atk_min=3, atk_max=6 } end function funcs.calcRating(triggerId, target) local hp = tonumber(target["최대체력"]) or 0 local def = tonumber(target["방어력"]) or 0 local atkStr = target["공격력"] or "0~0" local atkMin, atkMax = atkStr:match("(%d+)~(%d+)") atkMin = tonumber(atkMin) or 0 atkMax = tonumber(atkMax) or 0 -- [밸런싱 DB 로드] local balanceDB = getDB(triggerId, "balancing.db") local rConfig = balanceDB.rating or { hp=0.3, def=4.5, atkMin=4.0, atkMax=2.7 } local rating = math.floor( hp * (rConfig.hp or 0.3) + def * (rConfig.def or 4.5) + atkMin * (rConfig.atkMin or 4.0) + atkMax * (rConfig.atkMax or 2.7) ) target["레이팅"] = rating return target end function funcs.check(triggerId, target) local expTable = getDB(triggerId, "EXPtable.db") local currentLevel = tonumber(target["레벨"]) or 1 local currentEXP = tonumber(target["경험치"]) or 0 local reqKey = tostring(currentLevel + EXP_LEVEL_OFFSET) local reqEXP = expTable[reqKey] if not reqEXP then return false, 0, 0 end return currentEXP >= reqEXP, currentEXP, reqEXP end function funcs.execute(triggerId, target, count, silent) count = count or 1 silent = silent or false local classData = getClassData(triggerId, target["클래스"]) local totalHPGain = 0 local totalSPGain = 0 local totalAtkMinGain = 0 local totalAtkMaxGain = 0 local totalDefGain = 0 local levelsGained = 0 for i = 1, count do local hpGain = math.random(1, classData.hp_inc or 100) local spGain = math.random(1, classData.sp_inc or 100) -- [Round] HP/SP 10단위 반올림 hpGain = math.floor((hpGain + 5) / 10) * 10 spGain = math.floor((spGain + 5) / 10) * 10 -- 최소값 보정 (0이 되면 섭섭하니 10으로) if hpGain < 10 then hpGain = 10 end if spGain < 10 then spGain = 10 end local defGain = math.random(1, classData.def_inc or 5) local atkMinGain = math.random(1, classData.atk_min or 3) local atkMaxGain = math.random(1, classData.atk_max or 6) local maxHP = tonumber(target["최대체력"]) or 100 local maxSP = tonumber(target["최대기력"]) or 100 local currentHP = tonumber(target["체력"]) or maxHP local currentSP = tonumber(target["기력"]) or maxSP local def = tonumber(target["방어력"]) or 0 local atkStr = target["공격력"] or "0~0" local atkMin, atkMax = atkStr:match("(%d+)~(%d+)") atkMin = tonumber(atkMin) or 0 atkMax = tonumber(atkMax) or 0 maxHP = maxHP + hpGain maxSP = maxSP + spGain currentHP = currentHP + hpGain currentSP = currentSP + spGain atkMin = atkMin + atkMinGain atkMax = atkMax + atkMaxGain -- [안전장치] min > max 역전 시 스왑 if atkMax < atkMin then local temp = atkMin atkMin = atkMax atkMax = temp end def = def + defGain target["최대체력"] = tostring(maxHP) target["최대기력"] = tostring(maxSP) target["체력"] = tostring(math.min(currentHP, maxHP)) target["기력"] = tostring(math.min(currentSP, maxSP)) target["공격력"] = atkMin .. "~" .. atkMax target["방어력"] = def local currentLevel = tonumber(target["레벨"]) or 1 target["레벨"] = tostring(currentLevel + 1) totalHPGain = totalHPGain + hpGain totalSPGain = totalSPGain + spGain totalAtkMinGain = totalAtkMinGain + atkMinGain totalAtkMaxGain = totalAtkMaxGain + atkMaxGain totalDefGain = totalDefGain + defGain levelsGained = levelsGained + 1 end target = funcs.calcRating(triggerId, target) local screenEffect = "" if not silent and levelsGained > 0 then local name = target["이름"] or "대상" local newLevel = target["레벨"] screenEffect = string.format([[]], name, newLevel, totalHPGain, totalSPGain, totalAtkMinGain, totalAtkMaxGain, totalDefGain)LEVEL UP!%s (Lv.%s)HP +%dSP +%dATK +%d~%dDEF +%ddebug("[LvUp] " .. name .. ": Lv " .. newLevel .. " HP+" .. totalHPGain) end return target, screenEffect, levelsGained end return function(triggerId, subFunc, ...) if funcs[subFunc] then return funcs[subFunc](triggerId, ...) end debug("LvUp.sys: Unknown function " .. tostring(subFunc)) endend)()
- #36newChar.sysconstant
Keys:
function (triggerId, reason) debug("newChar system initiated. Reason: " .. (reason or "unknown"))
--------------------------------------- -- ★ 밸런싱 상수 정의 (Balancing Constants) -- 신규 캐릭터 생성 시스템의 핵심 수치를 조정하려면 이 섹션의 값을 수정하세요. --------------------------------------- -- [초상화 생성 프롬프트 CONSTANT] local PORTRAIT_PROMPT_PREFIX = "seductive character illustration, high quality, 4k, face focus, moody anime style, simple white background" local negPrompt = "ugly, low quality, realistic" -- [HP/SP 랜덤 변동폭] local HP_SP_RANDOM_MIN = 90 -- HP/SP 최종값 최소 배율 (%). 최종 HP/SP는 기본값의 이 % ~ MAX % 사이에서 결정. local HP_SP_RANDOM_MAX = 110 -- HP/SP 최종값 최대 배율 (%). -- [신체특성 부여 확률] (순차 적용) local TRAIT_PROB_BODY_1 = 0.6 -- 첫 번째 신체특성 부여 확률 (60%). local TRAIT_PROB_BODY_2 = 0.3 -- 두 번째 신체특성 부여 확률 (30%). 첫 번째 획득 시에만 판정. local TRAIT_PROB_BODY_3 = 0.15 -- 세 번째 신체특성 부여 확률 (15%). 두 번째 획득 시에만 판정. -- [성적취향 부여 확률] local TRAIT_PROB_SEXUAL = 0.1 -- 성적취향 특성 부여 확률 (10%). 10% 확률로 성적취향 1개 부여. -- [성격 부여 확률] local TRAIT_PROB_EXTRA_PERS = 0.5 -- 추가 성격 부여 확률 (50%). 기본 1개 + 이 확률로 1개 추가. --------------------------------------- -- 1. 설정 및 데이터 로드 local raceDB = getDB(triggerId, "race.db") local classDB = getDB(triggerId, "class.db") local traitDB = getDB(triggerId, "trait.db") local expTable = getDB(triggerId, "EXPtable.db") local dungeonLv = tonumber(getChatVar(triggerId, "dungeonLv") or 1) local ageLimit = tonumber(getChatVar(triggerId, "ageLimit") or 12) local raceLimit = getChatVar(triggerId, "raceLimit") or "0" local maleLimit = getChatVar(triggerId, "maleLimit") or "0" local futaLimit = getChatVar(triggerId, "futaLimit") or "0" local heroLvLimit = tonumber(getChatVar(triggerId, "heroLvLimit") or 5) -- 2. 기본 골격 생성 (RNG) math.randomseed(os.time()) local level = math.max(1, dungeonLv + math.random(-2, heroLvLimit)) -- 종족 결정 local raceListKey = json.decode(getChatVar(triggerId, "raceList")) local raceKey = "" local raceData = {} if raceLimit == "1" then -- 인간만 raceKey = "human" else raceKey = raceListKey[math.random(#raceListKey)] end raceData = raceDB[raceKey] -- 객체 구조 -- 혹시 데이터 없으면 인간으로 폴백 if not raceData then raceKey = "human"; raceData = raceDB["human"] end -- 클래스 결정 local classListKey = json.decode(getChatVar(triggerId, "classList")) local classKey = classListKey[math.random(#classListKey)] local classData = classDB[classKey] -- 성별 결정 local genderPool = {"여성", "여성", "여성"} if maleLimit == "1" then table.insert(genderPool, "남성") end if futaLimit == "1" then table.insert(genderPool, "후타나리") end local gender = genderPool[math.random(#genderPool)] -- 나이 결정 local ageBase = math.max(ageLimit, 8) local lifeSpanMult = raceData.life_mod or 1.0 -- 필드명 변경 반영 local age = math.floor((ageBase + math.random(0, 10)) * lifeSpanMult) if age < ageLimit then age = ageLimit end -- 3. 스탯 계산 (1레벨 기본 스탯 설정 후 LvUp.sys로 성장) local raceBaseHP = raceData.base_hp or 100 local raceBaseSP = raceData.base_sp or 100 local raceAtkBonus = raceData.bonus_atk or 0 local raceDefBonus = raceData.bonus_def or 0 -- 1레벨 기본 스탯 (종족 기본치 + 종족 보너스 + 1레벨 클래스 성장치) local maxHP = math.floor(raceBaseHP * (math.random(HP_SP_RANDOM_MIN, HP_SP_RANDOM_MAX) * 0.01)) local maxSP = math.floor(raceBaseSP * (math.random(HP_SP_RANDOM_MIN, HP_SP_RANDOM_MAX) * 0.01)) -- 클래스 초기 스탯 (1레벨분) local classAtkMin = classData.atk_min or 3 local classAtkMax = classData.atk_max or 6 local classDef = classData.def_inc or 5 local atkMin = raceAtkBonus + math.random(1, classAtkMin) local atkMax = raceAtkBonus + math.random(1, classAtkMax) -- [안전장치] min > max 역전 시 스왑 if atkMax < atkMin then local temp = atkMin atkMin = atkMax atkMax = temp end local def = raceDefBonus + math.random(1, classDef) -- 신체 사이즈 (나이/성별/종족 보정) local heightBase = raceData.height or 160 -- 성별에 따른 키 보정 (기본 height는 여성 기준) local genderOffset = 0 if gender == "남성" then genderOffset = 13 -- 남성은 여성보다 평균 13cm 큼 elseif gender == "후타나리" then genderOffset = 2 -- 후타나리는 여성보다 약간 큼 (요청사항) end -- 성인 예상 키 (기본키 + 성별보정 + 랜덤변동) -- 랜덤 변동: 정규분포에 가깝게 (주사위 2개 합) -5 ~ +5 범위 두 번 더해서 -10 ~ +10 local randomVariance = math.random(-5, 5) + math.random(-5, 5) local adultHeight = math.floor(heightBase + genderOffset + randomVariance) -- 성장 비율 계산 local normAge = age if lifeSpanMult > 0 then normAge = age / lifeSpanMult end -- 성장이 멈추는 나이 (18~22세 랜덤) local maturityAge = math.random(18, 22) local growthRatio = 1.0 if normAge < maturityAge then -- 성장 곡선: (현재나이/성숙나이)^0.7 -- 0세일 때 0이 되지 않도록 기본값(출생시 키 비율) 0.3 정도 추가 local ageRatio = normAge / maturityAge growthRatio = 0.3 + 0.7 * (ageRatio ^ 0.7) end -- 최종 키 local height = math.floor(adultHeight * growthRatio) -- 몸무게 계산 (BMI 기반) -- 성인 BMI: 19~23, 아동 BMI: 14~18 local targetBMI = 21 -- 성인 평균 if normAge < maturityAge then -- 14 ~ 21 사이로 보간 targetBMI = 14 + (21 - 14) * ((normAge / maturityAge) ^ 0.5) end -- 종족별 BMI 보정 if raceKey == "dwarf" then targetBMI = targetBMI * 1.2 -- 드워프는 근육질/단단함 (+20%) elseif raceKey == "elf" then targetBMI = targetBMI * 0.95 -- 엘프는 가벼움 (-5%) end -- 랜덤 변동폭 (±10%) targetBMI = targetBMI * (math.random(90, 110) * 0.01) -- Weight = BMI * (Height/100)^2 local weight = math.floor(targetBMI * (height * 0.01) * (height * 0.01)) -- 최소 몸무게 보정 if weight < 3 then weight = 3 end local bust = "평범" local nipple = "갈색" local penis = "" local pubes = "보통" if gender ~= "남성" then local bustSizes = {"절벽", "빈유", "평범", "거유", "폭유"} bust = bustSizes[math.random(#bustSizes)] end if gender ~= "여성" then local penisSizes = {"실좆", "작음", "평범", "대물", "말자지"} penis = penisSizes[math.random(#penisSizes)] end -- 4. 특성(Trait) 부여 (한글 이름 직접 선택) local personalityList = json.decode(getChatVar(triggerId, "성격List")) local bodyTraitList = json.decode(getChatVar(triggerId, "신체특성List")) local assignedTraits = {} -- 한글 이름 저장 -- 특성 추가 함수 (중복 체크) local function addTrait(list) if not list or #list == 0 then return false end local name = list[math.random(#list)] local dup = false for _, n in ipairs(assignedTraits) do if n == name then dup = true; break end end if not dup then table.insert(assignedTraits, name) return true end return false end -- 성격 1~2개 local pAdded = 0 local retry = 10 while pAdded < 1 and retry > 0 do if addTrait(personalityList) then pAdded = pAdded + 1 end retry = retry - 1 end if math.random() > TRAIT_PROB_EXTRA_PERS then addTrait(personalityList) end -- 신체특성 추가 (확률 순차 적용, 최대 3개) if math.random() <= TRAIT_PROB_BODY_1 then addTrait(bodyTraitList) if math.random() <= TRAIT_PROB_BODY_2 then addTrait(bodyTraitList) if math.random() <= TRAIT_PROB_BODY_3 then addTrait(bodyTraitList) end end end -- 성적취향 추가 (확률 기반 1개) local sexualPrefList = json.decode(getChatVar(triggerId, "성적취향List")) if math.random() <= TRAIT_PROB_SEXUAL then addTrait(sexualPrefList) end -- 5. 스켈레톤 데이터 구성 (1레벨 기본 스탯으로 시작) local skeleton = { ["종족"] = raceData.name, -- "인간" ["성별"] = gender, ["나이"] = tostring(age), ["클래스"] = classData.name, -- "기사" ["레벨"] = "1", -- 1레벨에서 시작하여 LvUp.sys로 성장 ["경험치"] = "0", ["체력"] = tostring(maxHP), ["최대체력"] = tostring(maxHP), ["기력"] = tostring(maxSP), ["최대기력"] = tostring(maxSP), ["공격력"] = atkMin .. "~" .. atkMax, ["방어력"] = def, ["레이팅"] = 0, -- LvUp.sys에서 계산 ["자궁정액"]="0", ["애널정액"]="0", ["condition"] = "REST", ["키"] = tostring(height), ["몸무게"] = tostring(weight), ["가슴"] = bust, ["유두"] = nipple, ["음모"] = pubes, ["페니스"] = penis, ["문신"]="", ["피어싱"]="", ["무기"] = classData.weapon, -- 한글 ["방어구"] = classData.armor, ["장신구"]="없음", ["성격"] = {}, ["신체특성"] = {}, ["성적취향"] = {}, ["칭호"] = "미함락", ["첫키스 상대"]="미경험", ["첫경험 상대"]="미경험", ["이상경험 기록"]={} } -- 특성 주입: UI 호환을 위해 최상위에 1로 저장 for _, traitName in ipairs(assignedTraits) do skeleton[traitName] = 1 end -- 6. LvUp.sys를 사용하여 목표 레벨까지 성장 (1레벨에서 시작) if level > 1 then skeleton, _, _ = sysFunction(triggerId, "LvUp.sys", "execute", skeleton, level - 1, true) debug(string.format("캐릭터 레벨업 완료: Lv.%s, ATK:%s, DEF:%s, Rating:%s", skeleton["레벨"], skeleton["공격력"], skeleton["방어력"], skeleton["레이팅"])) else -- 1레벨이면 Rating만 계산 skeleton = sysFunction(triggerId, "LvUp.sys", "calcRating", skeleton) end debug("기초데이터", json.encode(skeleton)) -- 7. LLM 요청 (설정 생성) local prompt = { promptBuild("system", getLoreBookContent(triggerId, "newChar.pt")), promptBuild("system", "existing names: "..getChatVar(triggerId, "chars")), promptBuild("user", json.encode(skeleton)) } debug("신규 용사 생성 요청...") local response = LLM(triggerId, prompt, "newChar") if not response.success then alertError(triggerId, "용사 생성 실패 (LLM Error)") return end local content = response.result content = content:gsub("<Thoughts>.-</Thoughts>", "") local narrative = json.decode(content:match("({.*})")) -- 8. 데이터 병합 & 이미지 생성 for k, v in pairs(narrative) do skeleton[k] = v end -- [초상화 생성 로직] local appearanceParts = {} if skeleton["헤어스타일"] then table.insert(appearanceParts, skeleton["헤어스타일"]) end if skeleton["복장"] then table.insert(appearanceParts, skeleton["복장"]) end local appearanceDesc = table.concat(appearanceParts, ", ") if appearanceDesc == "" then appearanceDesc = skeleton["appearance"] or "" end -- 간단한 번역/매핑 local genderStr = (skeleton["성별"] == "남성" and "male") or (skeleton["성별"] == "후타나리" and "futanari") or "female" local raceStr = skeleton["종족"] or "human" local posPrompt = PORTRAIT_PROMPT_PREFIX .. ", " .. genderStr .. ", " .. raceStr .. ", " .. appearanceDesc local imgResult = nil for i = 1, 3 do debug("초상화 생성 시도 (" .. i .. "/3): " .. posPrompt) imgResult = generateImage(triggerId, posPrompt, negPrompt):await() if imgResult and string.find(imgResult, "inlay") then break end debug("초상화 생성 실패 또는 inlay 누락. 재시도 중...") imgResult = nil end if imgResult then local cleanImg = imgResult:gsub("{{inlay:", "") -- [JSON Parsing Fix] Split ':' into two fields skeleton["portrait_pre"] = "{{inlay:" skeleton["portrait_append"] = cleanImg debug("초상화 저장 완료") else debug("초상화 생성 실패 (최대 재시도 횟수 초과)") skeleton["portrait_pre"] = "<img src='{{raw:" skeleton["portrait_append"] = ":dummy_portrait}}'>" end -- DB에 없는 필드 0으로 초기화 (trace.db 포함) local extraDBs = {"mark.db", "abl.db", "exp.db", "stat.db", "trace.db"} for _, dbName in ipairs(extraDBs) do local dbContent = getDB(triggerId, dbName) -- 객체 for key, val in pairs(dbContent) do -- 여기서도 한글 매핑 필요. charInfo.html 등에서 한글 키로 접근할 것임. if val.name then if not skeleton[val.name] then skeleton[val.name] = "0" end else if not skeleton[key] then skeleton[key] = "0" end end end end -- 캐릭터 리스트 업데이트 local chars = json.decode(getChatVar(triggerId, "chars")) table.insert(chars, skeleton["이름"]) setChatVar(triggerId, "chars", json.encode(chars)) -- 로어북 저장 upsertLocalLoreBook(triggerId, skeleton["이름"], json.encode(skeleton), DEFAULT_LORE_OPTION) debug("신규 용사 생성 완료", skeleton["이름"]) if getState(triggerId, "screen") == "charList" then sysFunction(triggerId, "char.sys", "list") endend
- #37skills.sysconstant
Keys:
-- skills.sys.txt -- 몬스터 스킬 및 시너지 정의 (Skill Registry) with Fluent API
(function() local skills = {}
----------------------------------------------------------- -- [DSL Engine] Context & Selector ----------------------------------------------------------- local Selector = {} Selector.__index = Selector function Selector.new(grid, units, validIndices) local self = setmetatable({}, Selector) self.grid = grid self.units = units or {} -- list of {slot, unit} return self end function Selector:sum(field) local total = 0 for _, item in ipairs(self.units) do if item.unit.data and item.unit.data[field] then total = total + (tonumber(item.unit.data[field]) or 0) -- Support direct field access if needed (like rating directly on unit object in some contexts) elseif item.unit[field] then total = total + (tonumber(item.unit[field]) or 0) end end return total end function Selector:filter(tagOrFunc) local filtered = {} for _, item in ipairs(self.units) do local keep = false if type(tagOrFunc) == "string" then if item.unit.hasTag and item.unit:hasTag(tagOrFunc) then keep = true end elseif type(tagOrFunc) == "function" then if tagOrFunc(item.unit) then keep = true end end if keep then table.insert(filtered, item) end end return Selector.new(self.grid, filtered) end function Selector:count() return #self.units end function Selector:buff(buffId, sourceSlot) local buffs = {} for _, item in ipairs(self.units) do table.insert(buffs, { target = item.slot, buff = buffId, source = sourceSlot }) end return buffs end local Context = {} Context.__index = Context function Context.new(triggerId, slotId, grid, ownerUnit) local self = setmetatable({}, Context) self.triggerId = triggerId self.slotId = slotId self.grid = grid self.owner = ownerUnit return self end function Context:dungeon() local units = {} for k, v in pairs(self.grid) do table.insert(units, {slot=k, unit=v}) end return Selector.new(self.grid, units) end function Context:neighbors() local f, s = self.slotId:match("^(%d+)_(%d+)$") local units = {} if f then f = tonumber(f) s = tonumber(s) local leftKey = string.format("%d_%d", f, s-1) local rightKey = string.format("%d_%d", f, s+1) if self.grid[leftKey] then table.insert(units, {slot=leftKey, unit=self.grid[leftKey]}) end if self.grid[rightKey] then table.insert(units, {slot=rightKey, unit=self.grid[rightKey]}) end end return Selector.new(self.grid, units) end ----------------------------------------------------------- -- [Engine] DB Loader & Executor ----------------------------------------------------------- local skills = {} local loaded = false local funcs = {} function funcs.load(triggerId) if loaded then return skills end -- Load Lua Table DB local dbContent = getLoreBookContent(triggerId, "skills.db") if not dbContent or dbContent == "" then return {} end -- Safe Load with Sandbox Environment -- We provide Context and Selector classes to the DB environment local env = { pairs = pairs, ipairs = ipairs, table = table, math = math, tonumber = tonumber, string = string, print = print, -- debug Context = Context, Selector = Selector } local loader, err = load(dbContent, "skills.db", "t", env) if not loader then -- debug("Skill DB Compile Error: " .. tostring(err)) return {} end local success, result = pcall(loader) if success then -- Hybrid Load: If result is table, use it. Else, usage env (Config Style). if type(result) == "table" then skills = result else skills = env end -- [Helper] Execute Wrapper -- Allows dungeon.sys to call: skills.execute(skillId, ...) skills.execute = function(skillId, triggerId, slotId, grid, ownerUnit) local skill = skills[skillId] if skill and skill.active then -- Create Context using the Class defined in this scope local ctx = Context.new(triggerId, slotId, grid, ownerUnit) -- Call the embedded function return skill.active(ctx) end return nil end loaded = true return skills else -- debug("Skill DB Execution Error: " .. tostring(result)) return {} end end -- Main Execution Entry Point -- subFunc here is the skillId (e.g. "horde_tactic") being called as a function? -- No, dungeon.sys calls `skills[skillId](triggerId, ...)` -- So we need to emulate that structure if possible, OR return the table directly. -- Current structure returns a dispatcher: return function(triggerId, subFunc, ...) -- We want: skills.sys("load") -> returns dict { skillId = { active = func, ... } } -- But dungeon.sys expects: skillFunc = skills[skillId] -> skillFunc() ?? -- Previously: skills[skillId] was a function. -- Now: skills[skillId] is a table { active = function }. -- So we need an adapter or change dungeon.sys. -- Let's change this module to return the raw DB table, -- BUT we wrap the 'active' function to be compatible? -- Easier: dungeon.sys logic: local skillFunc = skills[skillId]; result = skillFunc(...) -- We can make skills[skillId] be a callable table (metatable __call) or wrapper. -- Or just return the raw table and let dungeon.sys handle it: `skills[skillId].active(...)` -- Decision: Let's create a wrapper here so dungeon.sys changes are minimal or cleaner. -- Wait, dungeon.sys refactor is part of the plan. Let's make dungeon.sys aware of the new structure. -- So here we just return the loaded DB table. return function(triggerId, subFunc, ...) if subFunc == "load" then return funcs.load(triggerId) end endend)()
- #38summon.sysconstant
Keys:
-- summon.sys.txt -- 몬스터 소환 시스템
(function() local funcs = {}
-- 투자금 조절 (증감/설정) function funcs.adjustInvest(triggerId, input) local minInvest = tonumber(getChatVar(triggerId, "minSummonInvest")) local invest = tonumber(getChatVar(triggerId, "summonInvest")) or minInvest local spiritStr = getChatVar(triggerId, "spirit") or "0" local spirit = tonumber((spiritStr:gsub(",", ""))) or 0 -- 피보나치 수열 (1, 2, 3, 5, 8, 13...) 및 누적합 계산 -- Invest = minInvest + 500 * Sum(Fib(1)..Fib(n)) local function getFib(n) if n < 1 then return 0 end if n == 1 then return 1 end if n == 2 then return 2 end -- 1, 2, 3, 5, 8... starts here local a, b = 1, 2 for i = 3, n do a, b = b, a + b end return b end local function getCumulative(n) local sum = 0 for i = 1, n do sum = sum + getFib(i) end return minInvest + sum * 500 end -- Invest값에 해당하는 n 찾기 local function getN(val) if val <= minInvest then return 0 end local n = 1 while true do local current = getCumulative(n) if current >= val then local prev = getCumulative(n-1) if (val - prev) < (current - val) then return n - 1 else return n end end n = n + 1 if n > 100 then return 100 end -- Safety break end end local target = minInvest local nextN = 0 if input == "plus" then local currentN = getN(invest) nextN = currentN + 1 target = getCumulative(nextN) -- Spirit 체크 if target > spirit then alertNormal(triggerId, "보유 Spirit이 부족하여 설정할 수 없습니다. (필요: " .. intForm(target) .. ")") return end elseif input == "minus" then local currentN = getN(invest) nextN = math.max(0, currentN - 1) target = getCumulative(nextN) else -- 직접 입력 (500단위 내림) local val = tonumber(input) or minInvest target = math.floor(val / 500) * 500 -- 보유 Spirit 초과 시 최대 가능 금액으로 조정 if target > spirit then target = math.floor(spirit / 500) * 500 end -- 최소 금액 minInvest 유지 if target < minInvest then target = minInvest end nextN = getN(target) end -- Change값은 다음 단계 비용(Fib(n+1) * 500)을 표시 -- 현재 단계 인덱스의 수열 값 local changeVal = getFib(nextN + 1) * 500 setChatVar(triggerId, "summonInvest", tostring(math.floor(target))) setChatVar(triggerId, "summonInvestChange", tostring(math.floor(changeVal))) end -- 소환 실행 function funcs.summon(triggerId) local spiritStr = getChatVar(triggerId, "spirit") or "0" local spirit = tonumber((spiritStr:gsub(",", ""))) or 0 local invest = tonumber(getChatVar(triggerId, "summonInvest")) or tonumber(getChatVar(triggerId, "minSummonInvest")) if spirit < invest then alertNormal(triggerId, "Spirit이 부족합니다. (필요: " .. intForm(invest) .. ")") return end -- 1. 소환 가능한 최대 레이팅 계산 (공식: sqrt(invest * 5)) local maxRating = math.floor(math.sqrt(invest * 5)) -- 2. 몬스터 풀 로드 및 필터링 local monsterDB = getDB(triggerId, "monsters.db") local inventory = json.decode(getChatVar(triggerId, "inventory") or "{}") local candidates = {} for k, v in pairs(monsterDB) do local qty = tonumber(inventory["monster:"..k]) or 0 if qty == 0 and v.rating <= maxRating then table.insert(candidates, {key=k, data=v}) end end if #candidates == 0 then alertNormal(triggerId, "조건에 맞는 소환 가능한 몬스터가 없습니다.\n(보유한 Spirit 부족 또는 해당 등급 내 모든 몬스터 보유)") return end -- 3. 소환 성공 (랜덤 선택) local pick = candidates[math.random(#candidates)] -- 비용 차감 spirit = spirit - invest setChatVar(triggerId, "spirit", intForm(spirit)) -- 인벤토리 추가 inventory["monster:"..pick.key] = 1 setChatVar(triggerId, "inventory", json.encode(inventory)) -- 관리 패널 가시성 변수 설정 setChatVar(triggerId, "has_assets", "1") -- Screen Effect로 성공 알림 local color = "#ffffff" -- 기본: 하급 (흰색) -- 태그 기반 색상 결정 local isMiddle = false local isUpper = false if pick.data.tags then for _, tag in ipairs(pick.data.tags) do if tag == "중급" then isMiddle = true end if tag == "상급" then isUpper = true end end end if isMiddle then color = "#1e90ff" end -- 파란색 if isUpper then color = "#b04ef0" end -- 보라색 -- 레이팅 2000 이상은 노란색 (최우선) if pick.data.rating >= 2000 then color = "#ffd700" end local effectHtml = string.format([[SUMMON SUCCESS!
[image]%sRating: ★%d]], color, color, color, pick.key, pick.data.name, pick.data.rating)
setChatVar(triggerId, "screenEffect", effectHtml) end return function(triggerId, subFunc, ...) if funcs[subFunc] then return funcs[subFunc](triggerId, ...) end endend)()
- #39train.sysconstant
Keys:
-- train.sys.txt -- 조교 액션 및 로직 통합 모듈 -- 사용법: sysFunction(triggerId, "train.sys", "action", trainKey, actor) -- sysFunction(triggerId, "train.sys", "insertionLogic", trainData, actor, difficulty) -- sysFunction(triggerId, "train.sys", "tagMatch", tags, hasTags, notTags)
(function() --------------------------------------- -- 로컬 함수 정의 ---------------------------------------
--------------------------------------- -- 디스패처 테이블 및 로컬 변수 --------------------------------------- local funcs = {} -- 태그 매칭 로직 function funcs.tagMatch(triggerId, tags, hasTags, notTags) -- hasTags 검사: 모두 있어야 함 if hasTags then for _, requiredTag in ipairs(hasTags) do local found = false for _, tag in ipairs(tags) do if tag == requiredTag then found = true break end end if not found then return false end end end -- notTags 검사: 하나라도 있으면 실패 if notTags then for _, excludeTag in ipairs(notTags) do for _, tag in ipairs(tags) do if tag == excludeTag then return false end end end end return true end -- 삽입 로직: DC, feeling, command 보정 function funcs.insertionLogic(triggerId, trainData, actor, difficulty) local insertion = trainData.insertion if not insertion then return { dc = trainData.baseDC, feeling = trainData.feeling or 0, command = trainData.command, exps = trainData.exps, tooTight = false } end -- actor 데이터 로드 -- actor 데이터 로드 local actorStr = getChatVar(triggerId, actor) local actorData = {} if actorStr and actorStr ~= "" then local status, res = pcall(json.decode, actorStr) if status then actorData = res end end local target = getState(triggerId, "target") local penis = actorData["페니스"] -- 확장도 속성 local expandAttr = insertion.target -- "V확장", "A확장" 등 local expandLv = expandAttr and tonumber(target[expandAttr]) or 10 -- 페니스 크기 매핑 local penisSize = { ["실좆"] = 1, ["작음"] = 2, ["평범"] = 3, ["대물"] = 4, ["말자지"] = 5 } local pSize = penisSize[penis] or 3 -- 필요 확장도 = (페니스크기 × 난이도) - 1 local reqExpand = (pSize * difficulty) - 1 local dc = trainData.baseDC local feeling = trainData.feeling or 0 local command = trainData.command local exps = {} for _, v in ipairs(trainData.exps or {}) do table.insert(exps, v) end local tooTight = false -- 확장도 부족 시: DC 증가, command 추가, 확장경험 추가 if expandLv < reqExpand then local gap = reqExpand - expandLv dc = dc + (gap * difficulty) -- 난이도 배수 적용 tooTight = true local typeKor = { V = "질", A = "애널", U = "요도", M = "입" } command = command .. " {{user}}의 것이 들어가기에 {target}의 " .. (typeKor[insertion.type] or "") .. "은 너무 좁다." if insertion.type ~= "M" then table.insert(exps, insertion.type .. "확장경험") end end -- 확장도 여유 시: Feeling 감소 local looseGap = math.max(0, expandLv - reqExpand) if looseGap > 0 then feeling = feeling - math.floor(looseGap * 0.5) feeling = math.max(0, feeling) -- 음수 방지 end if tooTight then debug(string.format("[Train:Insertion] Depth: Tight (Expand: %d < Req: %d)", expandLv, reqExpand)) end return { dc = dc, feeling = feeling, command = command, exps = exps, tooTight = tooTight } end -- 조교 액션 처리 function funcs.action(triggerId, trainKey, actor) actor = actor or "user" -- train.db 로드 local trainDB = getDB(triggerId, "train.db") local trainData = trainDB[trainKey] if not trainData then alertNormal(triggerId, "train.db에서 '" .. trainKey .. "'를 찾을 수 없습니다.") return false end local target = getState(triggerId, "target") local stat = getState(triggerId, "stat") local tags = trainData.tags or {} --[[1. DC 계산]] debug(string.format("[Train:Action] Key: %s, Actor: %s, BaseDC: %d", trainKey, actor, trainData.baseDC)) local dc = DC_PENALTY * trainData.baseDC -- customEvent: onDCCalc 발동 local ctx = { dc = dc, tags = tags, stat = stat, category = trainData.category, target = target } ctx = sysFunction(triggerId, "customEvent.sys", "onDCCalc", ctx) dc = ctx.dc --[[2. 비용 계산]] local costHP = trainData.costHP local costSP = trainData.costSP -- customEvent: onCostCalc 발동 local ctx = { costHP = costHP, costSP = costSP, tags = tags, stat = stat, category = trainData.category, target = target } debug("[Train:Debug] Pre-CostCalc") ctx = sysFunction(triggerId, "customEvent.sys", "onCostCalc", ctx) debug("[Train:Debug] Post-CostCalc") costHP = ctx.costHP costSP = ctx.costSP --[[3. 경험치 및 커맨드]] debug("[Train:Debug] Step 3: EXP/Command") local exps = {} for _, v in ipairs(trainData.exps or {}) do table.insert(exps, v) end local feeling = trainData.feeling or 0 local command = trainData.command --[[4. 삽입 로직 처리]] if trainData.insertion then debug("[Train:Debug] Step 4: Insertion") local difficulty = trainData.insertion.difficulty or 1 -- 내부 로컬 함수 호출 local insertResult = funcs.insertionLogic(triggerId, trainData, actor, difficulty) dc = dc + (insertResult.dc - trainData.baseDC) feeling = insertResult.feeling command = insertResult.command exps = insertResult.exps end --[[5. 장비 토글 커맨드 처리]] if trainData.equipment then debug("[Train:Debug] Step 5: Equipment logic for " .. tostring(trainData.equipment)) local eqStr = getChatVar(triggerId, "equipments") local equipments = {} if eqStr and eqStr ~= "" then local status, res = pcall(json.decode, eqStr) if status then equipments = res else debug("[Train:Error] Failed to decode equipments JSON: " .. tostring(res)) end else debug("[Train:Warn] 'equipments' chatVar is empty or nil.") end -- trainData.commands가 nil일 경우를 대비 local cmds = trainData.commands or {} if equipments[trainData.equipment] == "착용" then command = cmds.unequip or command else command = cmds.equip or command end end -- 커맨드에서 {target} 치환 debug(string.format("[Train:Debug] Step 6: Gsub (Cmd: %s, Target: %s)", tostring(command), tostring(target["이름"]))) if command then command = command:gsub("{target}", target["이름"] or "Target") end --[[6. DC_PENALTY 수정 (장비 착탈 등)]] if trainData.dcPenaltyMod then DC_PENALTY = DC_PENALTY * trainData.dcPenaltyMod debug(string.format("[Train:Action] DC_PENALTY modified by %.2f -> %.2f", trainData.dcPenaltyMod, DC_PENALTY)) end --[[7. trainProcess.sys 호출]] local abbrCmd = (command or ""):sub(1, 20) .. "..." return sysFunction(triggerId, "trainProcess.sys", dc, costHP, costSP, exps, feeling, command, trainData.equipment, tags) end function funcs.orgasm(triggerId, who, statEXP, feeling, tags) local ORGASM_MAX_DEFAULT = 10000 local ORGASM_MULTIPLIER = 1.2 local DECAY_RATE = 0.95 local countVar = "ej_" .. who .. "_count" local ratioVar = "ej_" .. who .. "_ratio" local count = tonumber(getChatVar(triggerId, countVar)) or 0 local maxVal = ORGASM_MAX_DEFAULT * (ORGASM_MULTIPLIER ^ count) local ratio = tonumber(getChatVar(triggerId, ratioVar)) or 0 ratio = math.floor(ratio * DECAY_RATE) -- 자연감소 local current = math.floor(ratio * maxVal * 0.01) if who == "target" then local target = getState(triggerId, "target") if not target then debug("[Train:Error] Target state is nil in orgasm function") return { isOrgasm = false, count = count, ratio = newRatio or ratio } end -- 쾌락 stat으로 증가 local plusStats = { "C쾌락", "V쾌락", "A쾌락", "B쾌락", "U쾌락", "M쾌락", "S쾌락" } for _, s in ipairs(plusStats) do current = current + (statEXP[s] or 0) end -- 부정 stat으로 감소 local minusStats = { "공포", "불쾌", "부정" } for _, s in ipairs(minusStats) do current = current - (statEXP[s] or 0) end -- 조건부 stat (abl에 따라 + 또는 -) -- 굴복: 복종각인 있거나 봉사기술 > 4면 + local ejCon = statEXP["굴복"] or 0 if tonumber(target["복종각인"] or 0) > 0 or tonumber(target["봉사기술"] or 0) > 4 then current = current + ejCon else current = current - ejCon end -- 수치: 노출벽 > 4면 + ejCon = statEXP["수치"] or 0 if tonumber(target["노출벽"] or 0) > 4 then current = current + ejCon else current = current - ejCon end -- 고통: 마조끼 > 4면 + ejCon = statEXP["고통"] or 0 if tonumber(target["마조끼"] or 0) > 4 then current = current + ejCon else current = current - ejCon end -- onOrgasmCalc 이벤트 발동 local ctx = { orgasm = current, statEXP = statEXP, tags = tags or {}, target = target } ctx = sysFunction(triggerId, "customEvent.sys", "onOrgasmCalc", ctx) current = ctx.orgasm elseif who == "user" then -- user는 feeling으로만 증가 current = current + (feeling or 0) * ORGASM_MAX_DEFAULT * 0.01 end -- 최대/최소 클램핑 current = math.min(maxVal, math.max(0, math.floor(current))) local newRatio = current / maxVal * 100 -- 절정 여부 체크 local isOrgasm = false if current >= maxVal then isOrgasm = true count = count + 1 newRatio = 0 end -- 변수 저장 setChatVar(triggerId, countVar, count) setChatVar(triggerId, ratioVar, string.format("%.2f", newRatio)) debug(string.format("[Train:Orgasm] Who: %s, Ratio: %.2f%%, Orgasm: %s", who, newRatio, tostring(isOrgasm))) return { isOrgasm = isOrgasm, count = count, ratio = newRatio } end -- 조교 종료 처리 (postTrain.sys 통합, 최적화됨) local cachedValidTraces = nil local cachedStatDBMap = nil function funcs.post(triggerId) local target = getState(triggerId, "target") if not target or not target["이름"] then debug("[Train:Error] Target state invalid in post function") return end local exp = getDB(triggerId, "EXPtable.db") local totalTrace = 0 -- traceList 생성 (UI용) local traceDB = getDB(triggerId, "trace.db") local traceList = {} for k, v in pairs(traceDB) do if v.name then table.insert(traceList, v.name) end end table.sort(traceList) setChatVar(triggerId, "traceList", json.encode(traceList)) -- 1. 캐시 초기화 (한 번만 실행) if not cachedValidTraces then local traceDB = getDB(triggerId, "trace.db") cachedValidTraces = {} for k, v in pairs(traceDB) do if v.name then cachedValidTraces[v.name] = true end end local statDB = getDB(triggerId, "stat.db") cachedStatDBMap = {} for k, v in pairs(statDB) do if v.name then cachedStatDBMap[v.name] = v.related end end end -- 2. Stat에 따른 경험치(traceEXP) 계산 local stat = getState(triggerId, "stat") local traceEXP = {} -- 이번 조교로 얻은 잠재적 경험치 for k, v in pairs(stat) do local val = tonumber(v) or 0 if val > 0 then local minExp = exp[tostring(val - 1)] or 0 local maxExp = exp[tostring(val)] or 0 if maxExp > 0 then local gained = math.random(minExp, maxExp) if gained > 0 then traceEXP[k] = gained totalTrace = totalTrace + gained end end end end -- 3. traceEXP를 실제 흔적(trace)으로 변환 local traceAdd = {} -- 이번에 추가될 흔적 양 local traceSum = {} -- 기존 + 추가 합산 (차감 전) -- 기존 흔적을 target에서 추출하여 복사 local traceCurrent = {} for _, traceName in ipairs(traceList) do local val = tonumber(target[traceName]) or 0 if val > 0 then traceCurrent[traceName] = val traceSum[traceName] = val end end for statName, expVal in pairs(traceEXP) do local relatedList = cachedStatDBMap[statName] if relatedList and #relatedList > 0 then local len = #relatedList local totalWeight = len * (len + 1) * 0.5 for i, traceName in ipairs(relatedList) do if cachedValidTraces[traceName] then local weight = len - i + 1 local addAmount = math.floor(expVal * weight / totalWeight) traceAdd[traceName] = (traceAdd[traceName] or 0) + addAmount traceSum[traceName] = (traceSum[traceName] or 0) + addAmount end end end end -- 4. 부정 흔적에 의한 차감 로직 (Water-leveling deduction) local deductLevel = 0 -- 삭감 기준 높이 (이 높이 이상인 흔적은 잘려나감) local deductTotal = traceSum["부정의 흔적"] or 0 -- 삭감에 사용할 총 자원 (부정 흔적의 총량) local deductUsed = 0 -- 실제로 사용된 삭감 자원 if deductTotal > 0 then -- 차감 대상 흔적 수집 (부정 제외, 0보다 큰 것) local targets = {} for k, v in pairs(traceSum) do if k ~= "부정의 흔적" and v > 0 then table.insert(targets, v) end end table.sort(targets) -- 오름차순 정렬 -- 삭감 로직 for i, val in ipairs(targets) do local remainingCount = #targets - i + 1 local heightDiff = val - deductLevel local costToNextLevel = heightDiff * remainingCount if deductTotal >= costToNextLevel then -- 충분한 자원이 있다면 다음 높이까지 모두 삭감 deductTotal = deductTotal - costToNextLevel deductUsed = deductUsed + costToNextLevel deductLevel = val else -- 자원이 부족하면 가능한 만큼만 높이를 올리고 중단 local lift = math.floor(deductTotal / remainingCount) deductLevel = deductLevel + lift deductUsed = deductUsed + (lift * remainingCount) break end end end -- 5. 결과 적용 및 로그 생성 local traceText = "조교로부터 " .. intForm(totalTrace) .. " Spirit을 획득했습니다.<br>" for k, v in pairs(traceSum) do local added = traceAdd[k] or 0 local deducted = 0 if k == "부정의 흔적" then deducted = deductUsed -- 부정 흔적은 사용된 만큼 차감 else -- 일반 흔적은 deductLevel을 초과하는 부분을 잘라냄 (min(deductLevel, v) 만큼 차감하는 효과) -- 즉 최종값 = v - min(deductLevel, v) = max(v - deductLevel, 0) -- 차감량 = v - max(..., 0) = min(v, deductLevel) deducted = math.min(deductLevel, v) end -- 로그 출력 (변동이 있는 경우만) if added ~= 0 or deducted ~= 0 then local finalVal = (traceCurrent[k] or 0) + added - deducted traceText = traceText .. string.format("%s: %d + %d - %d = %d<br>", k, (traceCurrent[k] or 0), added, deducted, finalVal) -- 값 적용 (target에 직접 저장) target[k] = tostring(math.max(0, finalVal)) end end -- 스피릿 업데이트 local oldSpiritStr = getChatVar(triggerId, "spirit") local oldSpirit = 0 if type(oldSpiritStr) == "string" then oldSpirit = tonumber((oldSpiritStr:gsub(",", ""))) or 0 else oldSpirit = tonumber(oldSpiritStr) or 0 end local newSpirit = oldSpirit + totalTrace setChatVar(triggerId, "spirit", intForm(newSpirit)) debug(string.format("[Train:Post] Spirit: %d -> %d (Gained: %d)", oldSpirit, newSpirit, totalTrace)) -- 데이터 저장 및 업데이트 (trace가 target에 직접 포함됨) setState(triggerId, "target", target) stateToVar(triggerId, "target", target) upsertLocalLoreBook(triggerId, target["이름"], json.encode(target), DEFAULT_LORE_OPTION) setChatVar(triggerId, "traceText", traceText) -- 6. 경험치 누적 및 레벨업 체크 -- 부정 차감 후 순수 트레이스 합계를 경험치로 환산 local netTrace = 0 for k, v in pairs(traceCurrent) do if k ~= "부정의 흔적" then local added = traceAdd[k] or 0 local deducted = math.min(deductLevel, traceSum[k] or 0) netTrace = netTrace + math.max(added - deducted, 0) end end local oldEXP = tonumber(target["경험치"]) or 0 local newEXP = oldEXP + netTrace target["경험치"] = tostring(newEXP) debug(string.format("[Train:Post] EXP: %d -> %d (Gained: %d)", oldEXP, newEXP, netTrace)) -- 레벨업 체크 및 실행 local lvUpEffect = "" local canLevelUp, currentEXP, requiredEXP = sysFunction(triggerId, "LvUp.sys", "check", target) debug(string.format("[Train:Post] EXP Progress: %d/%d (Level Up: %s)", currentEXP, requiredEXP or 0, canLevelUp and "YES" or "NO")) if canLevelUp then target, lvUpEffect, _ = sysFunction(triggerId, "LvUp.sys", "execute", target, 1, false) -- 레벨업 후 화면 효과 저장 local existEffect = getChatVar(triggerId, "screenEffect") or "" setChatVar(triggerId, "screenEffect", existEffect .. lvUpEffect) end -- 로그 및 화면 갱신 addChat(triggerId, "user", "{{" .. "getvar::html" .. "}}") local ampmVal = getChatVar(triggerId, "ampm") local ampmStr = (ampmVal == "0") and "낮" or "밤" local lastTrainLog = string.format( "<div class='showLog'><p>%s일차 %s / 조교 대상:%s</p><p>%s%s</p></div>", getChatVar(triggerId, "day"), ampmStr, target["이름"], getChatVar(triggerId, "oldLog"), getChatVar(triggerId, "newLog") ) setChat(triggerId, getChatLength(triggerId) - 2, lastTrainLog) -- [조건 해금] 마계 상점 (조교 1회 완료) local trainCount = tonumber(getChatVar(triggerId, "trainCount")) or 0 trainCount = trainCount + 1 setChatVar(triggerId, "trainCount", trainCount) local unlockShop = tonumber(getChatVar(triggerId, "unlock_shop")) or 0 if trainCount >= 1 and unlockShop == 0 then setChatVar(triggerId, "unlock_shop", 1) setChatVar(triggerId, "pending_shop_unlock", "1") -- ablUp→main 전환 시 알림 end -- 초기화 및 화면 이동 setChatVar(triggerId, "selectedAbl", "none") screenChange(triggerId, "ablUp") end -- 조교 초기 설정 (preTrainSetting.sys 통합) function funcs.prepare(triggerId, name) --target를 로어북에서 불러와 1차원으로 flatten local target = json.decode(getLoreBookContent(triggerId, name)) setState(triggerId, "target", target) --대상 캐릭터 로어북 정보를 state에 저장 target = flatten(target) local day = getChatVar(triggerId, "day") local ampm = tonumber(getChatVar(triggerId, "ampm")) local date = day .. "일째, " if ampm == 0 then date = date .. "낮" else date = date .. "밤" end --조교경험 추가 target["조교회수"] = tostring(target["조교회수"] + 1) --조교화면 변수 초기화 setChatVar(triggerId, "category", "애무계") setChatVar(triggerId, "oldLog", date .. " ") setChatVar(triggerId, "newLog", "[" .. target["이름"] .. "의 " .. target["조교회수"] .. "번째 조교]<br>") setChatVar(triggerId, "statLvUp", " ") setChatVar(triggerId, "ej_target_ratio", 0) --조교대상 절정치 setChatVar(triggerId, "ej_user_ratio", 0) --유저 절정치 setChatVar(triggerId, "요의", 0) --조교대상 요의 setChatVar(triggerId, "ej_target_count", 0) --조교대상 절정 회수 초기화 setChatVar(triggerId, "ej_user_count", 0) --유저 절정 회수 초기화 setChatVar(triggerId, "statFold", "0") --stat 세부 내역이 보이도록 초기화 setChatVar(triggerId, "equipments", json.encode(getDB(triggerId, "equipments.db"))) --착용품 모두 미착용으로 초기화 --수정구 영상 초기화 TRAIN_REC = 0 setState(triggerId, "recLog", "") --스탯 초기화 (캐싱 메커니즘 사용) local statDB = getDB(triggerId, "stat.db") local stat = {} for k, v in pairs(statDB) do -- statBonus 등이 한글 키를 사용하므로 한글 이름으로 초기화 if v.name then stat[v.name] = 0 else stat[k] = 0 end end --stat에 영향을 주는 모든 속성을 나열한 후 대상 캐릭터의 데이터를 모두 순회하며 stat에 영향을 주는 항목이 있는지 체크 --abl같은 특정테이블만 순회하는것보다 확장성이 뛰어남 local ablBase = 0.2 --abl*ablBase가 stat의 최소치를 보장해 높은 abl의 효과를 보장하도록 함 local ablRatio = 0.5 --abl*ablRatio*math.random()을 더해 매 조교마다 조금씩 다른 stat치로 시작하도록 함 local statBonus = { --한글키값을 넣기위해 ["키값"]구조 적용 --abl ["C감각"] = function(v) stat["C쾌락"] = stat["C쾌락"] + v * (ablBase + ablRatio * math.random()) --자동 형변환 하겠지.. end, ["B감각"] = function(v) stat["B쾌락"] = stat["B쾌락"] + v * (ablBase + ablRatio * math.random()) end, ["V감각"] = function(v) stat["V쾌락"] = stat["V쾌락"] + v * (ablBase + ablRatio * math.random()) end, ["A감각"] = function(v) stat["A쾌락"] = stat["A쾌락"] + v * (ablBase + ablRatio * math.random()) end, ["U감각"] = function(v) stat["U쾌락"] = stat["U쾌락"] + v * (ablBase + ablRatio * math.random()) end, ["M감각"] = function(v) stat["M쾌락"] = stat["M쾌락"] + v * (ablBase + ablRatio * math.random()) end, ["V확장"] = function(v) --확장이 높아지면 쾌감이 감소 stat["V쾌락"] = stat["V쾌락"] - v * (ablBase + ablRatio * math.random()) * 0.5 stat["V윤활"] = stat["V윤활"] + v * (ablBase + ablRatio * math.random()) * 1.5 end, ["A확장"] = function(v) --확장이 높아지면 쾌감이 감소 stat["A쾌락"] = stat["A쾌락"] - v * (ablBase + ablRatio * math.random()) * 0.5 stat["A윤활"] = stat["A윤활"] + v * (ablBase + ablRatio * math.random()) * 1.5 end, ["U확장"] = function(v) --확장이 높아지면 쾌감이 감소. 요도의 경우에는 확장되지 않으면 할 수 있는게 적으므로 페널티 감소 stat["U쾌락"] = stat["U쾌락"] - v * (ablBase + ablRatio * math.random()) * 0.2 stat["V윤활"] = stat["V윤활"] + v * (ablBase + ablRatio * math.random()) * 1.2 --V윤활이라도 올려줘야.. end, ["순종"] = function(v) stat["온순"] = stat["온순"] + v * (ablBase + ablRatio * math.random()) end, ["욕망"] = function(v) stat["욕정"] = stat["욕정"] + v * (ablBase + ablRatio * math.random()) end, ["봉사기술"] = function(v) stat["굴복"] = stat["굴복"] + v * (ablBase + ablRatio * math.random()) * 0.5 stat["온순"] = stat["온순"] + v * (ablBase + ablRatio * math.random()) * 0.25 stat["습득"] = stat["습득"] + v * (ablBase + ablRatio * math.random()) * 0.25 end, ["성교기술"] = function(v) stat["욕정"] = stat["욕정"] + v * (ablBase + ablRatio * math.random()) * 0.2 stat["습득"] = stat["습득"] + v * (ablBase + ablRatio * math.random()) * 0.2 stat["온순"] = stat["온순"] + v * (ablBase + ablRatio * math.random()) * 0.1 stat["C쾌락"] = stat["C쾌락"] + v * (ablBase + ablRatio * math.random()) * 0.1 stat["V쾌락"] = stat["V쾌락"] + v * (ablBase + ablRatio * math.random()) * 0.1 stat["A쾌락"] = stat["A쾌락"] + v * (ablBase + ablRatio * math.random()) * 0.1 stat["B쾌락"] = stat["B쾌락"] + v * (ablBase + ablRatio * math.random()) * 0.1 stat["V윤활"] = stat["V윤활"] + v * (ablBase + ablRatio * math.random()) * 0.1 end, ["노출벽"] = function(v) stat["굴복"] = stat["굴복"] + v * (ablBase + ablRatio * math.random()) * 0.5 stat["수치"] = stat["수치"] + v * (ablBase + ablRatio * math.random()) * 0.5 end, ["마조끼"] = function(v) stat["굴복"] = stat["굴복"] + v * (ablBase + ablRatio * math.random()) * 0.5 stat["고통"] = stat["고통"] + v * (ablBase + ablRatio * math.random()) * 0.5 end, --trait_체질 ["성교중독"] = function(v) stat["온순"] = stat["온순"] + 0.5 stat["V쾌락"] = stat["V쾌락"] + 0.25 stat["A쾌락"] = stat["A쾌락"] + 0.25 end, ["자위중독"] = function(v) stat["온순"] = stat["온순"] + 0.5 stat["V윤활"] = stat["V윤활"] + 0.5 stat["A윤활"] = stat["A윤활"] + 0.5 end, ["정액중독"] = function(v) stat["굴복"] = stat["굴복"] + 0.5 stat["습득"] = stat["습득"] + 0.5 end, ["매춘중독"] = function(v) stat["온순"] = stat["온순"] + 0.5 end, ["방뇨중독"] = function(v) stat["수치"] = stat["수치"] + 1.5 stat["불쾌"] = stat["불쾌"] + 0.5 end, ["촉수중독"] = function(v) stat["온순"] = stat["온순"] + 0.5 end, ["수간중독"] = function(v) stat["온순"] = stat["온순"] + 0.5 end, ["상시발정"] = function(v) stat["V윤활"] = stat["V윤활"] + 1 end, --trait_칭호 ["음란"] = function(v) stat["V윤활"] = stat["V윤활"] + 2 stat["욕정"] = stat["욕정"] + 3 end, ["연모"] = function(v) stat["온순"] = stat["온순"] + 5 end, ["암캐"] = function(v) stat["C쾌락"] = stat["C쾌락"] + 1 stat["V쾌락"] = stat["V쾌락"] + 1 stat["A쾌락"] = stat["A쾌락"] + 1 stat["B쾌락"] = stat["B쾌락"] + 1 stat["욕정"] = stat["욕정"] + 1 end, ["붕괴"] = function(v) stat["공포"] = stat["공포"] + 5 end, ["광기"] = function(v) stat["부정"] = stat["부정"] + 5 end, --mark ["고통각인"] = function(v) stat["고통"] = stat["고통"] + v * 1 end, ["쾌락각인"] = function(v) stat["욕정"] = stat["욕정"] + v * 1 end, ["공포각인"] = function(v) stat["공포"] = stat["공포"] + v * 1 end, ["복종각인"] = function(v) stat["온순"] = stat["온순"] + v * 1 end, ["치욕각인"] = function(v) stat["굴복"] = stat["굴복"] + v * 1 end, ["반발각인"] = function(v) stat["저항"] = stat["저항"] + v * 2 end } --평탄화된 target테이블을 순회하며 statBonus 적용 for k, v in pairs(target) do local v = tonumber(v) or v if v ~= 0 then local suc, _ = pcall(statBonus[k], v) if suc then -- print(k.."보너스 적용") -- debug removed or kept end end end --stat을 정수화하고 0~10사이에 있도록 제한 for k, v in pairs(stat) do stat[k] = math.min(math.max(math.floor(v), 0), 10) end setState(triggerId, "target", target) stateToVar(triggerId, "target", target) --대상 캐릭터를 챗변수에 저장 setState(triggerId, "stat", stat) --계산된 stat를 state에 저장 --챗변수를 위한 사전처리 local stat_c = "{" for k, v in pairs(stat) do stat_c = stat_c .. '"' .. k .. '":"' .. v .. '",' end stat_c = string.gsub(stat_c, ",$", "}") -- 마지막 쉼표 대신 중괄호 닫기 setChatVar(triggerId, "stat", stat_c) --가공된 stat을 챗변수에 저장 end --------------------------------------- -- 메인 함수 (외부에서 호출됨) --------------------------------------- return function(triggerId, subFunc, ...) if funcs[subFunc] then return funcs[subFunc](triggerId, ...) else debug("train.sys: 알 수 없는 함수 - " .. tostring(subFunc)) end endend)()
- #40trainProcess.sysconstant
Keys:
--조교 커맨드에 따른 LLM의 출력을 처리 function(triggerId, dc, HP, SP, exps, feeling, command, equipTarget, tags) debug("trainProcess 실행: DC="..tostring(dc))
--------------------------------------- -- ★ 밸런싱 상수 정의 (Balancing Constants) -- 조교 시스템의 핵심 수치를 조정하려면 이 섹션의 값을 수정하세요. --------------------------------------- -- [보너스/페널티 계수] local MARK_BONUS_MULT = 2 -- 각인 보너스 배율. 각인 1레벨당 보너스 값. local REBELLION_BASE = 2 -- 반발각인 페널티 기준. 반발각인 레벨에 대해 (이 값)^레벨 만큼 페널티. -- [대성공 보상] local GREAT_SUCCESS_COST_MULT = 0.75 -- 대성공 시 HP/SP 소모 배율 (75%). 조교 대성공 시 체력/기력 소모가 이 비율로 감소. -- [기력 부족 페널티] local SP_SHORTAGE_HP_MULT = 1.0 -- 기력 부족 시 추가 체력 소모 배율. 부족한 기력의 이 배수만큼 체력 추가 소모. -- [스탯 경험치 획득 제한] local STAT_EXP_DC_FACTOR = 0.15 -- DC 기반 최대 레벨 계산 계수. (DC * 이 값)이 획득 가능한 최대 스탯 레벨. -- 예: DC 60 → 최대 9렙, DC 67 → 최대 10렙 경험치까지 획득 가능. --------------------------------------- --[[초기화]] math.randomseed(os.time()) local screenEffect = "" --screenEffect 초기화 --날짜계산 (유틸리티 함수 사용) local date = getDateString(triggerId, "full") --DB호출 (캐싱 메커니즘 사용) local EXPtbl = getDB(triggerId, "EXPtable.db") local markDB = getDB(triggerId, "mark.db") local ablDB = getDB(triggerId, "abl.db") local traitDB = getDB(triggerId, "trait.db") local statDB = getDB(triggerId, "stat.db") local expDB = getDB(triggerId, "exp.db") local itemDB = getDB(triggerId, "items.db") --statdb에서 설명파트만 추출 (shallow copy로 원본 캐시 보호) local statDescDB = {} for k,v in pairs(statDB) do -- 한글 이름을 키로 사용해 LLM에 전달 if v.name then statDescDB[v.name] = v.desc else statDescDB[k] = v.desc end end --[[유저와 조교대상의 정보 호출]] local user = getLoreBookContent(triggerId, "user") --유저의 정보를 로어북에서 string으로 호출 local target_c = getChatVar(triggerId, "target") --대상정보를 chatvar에서 string으로 호출 local target = getState(triggerId, "target") --대상정보를 state에서 테이블로 호출 local currentStat = getState(triggerId, "stat") --현재 stat을 호출 local equipments = getChatVar(triggerId, "equipments") --현재 각종 장비의 착용 여부를 호출 local king = getChatVar(triggerId, "king") --광왕의 정보 호출 --[[버튼 비활성화 및 응답중 표시]] local old_cmds = getChatVar(triggerId, "cmds") local process_comment = "[stat 계산중]" local loading_prepend = [[<div class="loading-container"><div class="loading-text">]] local loading_append = [[</div> <div class="loading-dots"> <span class="dot-1"></span> <span class="dot-2"></span> <span class="dot-3"></span> </div> </div>]] setChatVar(triggerId, "cmds", loading_prepend .. process_comment .. loading_append) reloadDisplay(triggerId) --[[성공굴림]] --보너스 요소: 유저 레벨당 1, 대상의 반발 외 각인당 2, 긍정적 Stat레벨당 1 --페널티 요소: 대상의 2^반발각인, 저항 stat당 1 local rollBonus = 0 rollBonus = rollBonus + json.decode(user)["레벨"] - 1 --유저 레벨당 1, 1레벨부터 시작이므로 1을 차감 --최종 주사위굴림 local roll = math.random()*MAXROLL local rollMsg = "조교 성공" if roll > MAXROLL - 1 then rollMsg = "조교 대성공!" screenEffect = [[<div class="success-animation"><div class="animation-text success">]]..rollMsg..[[</div></div>]] debug(string.format("Roll: %.0f (Total: %.0f) vs DC: %.0f", roll, roll+rollBonus, dc), rollMsg) elseif roll + rollBonus <= dc then rollMsg = "조교 실패" screenEffect = [[<div class="success-animation"><div class="animation-text fail">]]..rollMsg..[[</div></div>]] debug(string.format("Roll: %.0f (Total: %.0f) vs DC: %.0f", roll, roll+rollBonus, dc), rollMsg) else debug(string.format("Roll: %.0f (Total: %.0f) vs DC: %.0f", roll, roll+rollBonus, dc), "성공") end --성공한 경우에만 경험 추가 if roll + rollBonus > dc then if #exps > 0 then for _, k in ipairs(exps) do target[k] = target[k] + 1 end end --성공 시 장비 탈착 변경 if equipTarget then local equipTable = json.decode(equipments) if equipTable[equipTarget] == "착용" then equipTable[equipTarget] = "미착용" else equipTable[equipTarget] = "착용" end setChatVar(triggerId, "equipments", json.encode(equipTable)) end end --[[HP,SP 소모 계산]] local costHP, costSP = HP, SP if string.find(rollMsg, "실패") then --조교가 실패하는 경우 체력 기력소모 증가 costHP = costHP + dc - math.abs(roll + rollBonus) costSP = costSP + dc - math.abs(roll + rollBonus) elseif string.find(rollMsg, "대성공") then --조교가 대성공 하면 체력 기력 소모 감소 costHP = costHP * GREAT_SUCCESS_COST_MULT costSP = costSP * GREAT_SUCCESS_COST_MULT end --비용 정수화 costHP = math.floor(costHP) costSP = math.floor(costSP) --실제 HP,SP 차감처리 local currentHP, currentSP = tonumber(target["체력"]), tonumber(target["기력"]) if currentSP < costSP then --기력이 부족한 경우 부족한 기력의 배수만큼 체력소모 costHP = costHP + ((costSP-currentSP) * SP_SHORTAGE_HP_MULT) end debug("hp: "..currentHP, "cost: "..costHP) debug("sp: "..currentSP, "cost: "..costSP) currentHP = math.max(currentHP - costHP, 0) currentSP = math.max(currentSP - costSP, 0) --HP가 낮을 경우 설정에 따라 실신 혹은 사망 처리 if currentHP < 1 then local ampm = getChatVar(triggerId,"ampm") if ampm == 0 then ampm ="낮" else ampm="밤" end local blockDeath = tonumber(getChatVar(triggerId, "blockDeath")) if blockDeath == 1 then --실신처리 target["조교실신경험"] = target["조교실신경험"] + 1 target["condition"] = "FAINT" setState(triggerId, "target", target) setChatVar(triggerId, "target", target) local newLog = getChatVar(triggerId, "newLog") newLog = newLog .. "<p>대상은 가혹한 조교를 견디지 못하고 실신했다.</p>" setChatVar(triggerId, "newLog", newLog) alertEffect(triggerId, "대상은 가혹한 조교를 견디지 못하고 실신했습니다.\n조교를 종료합니다.", "warning") sysFunction(triggerId, "train.sys", "post") --조교종료 화면으로 넘어가 trace를 얻고 어빌처리는 할 수 있도록 함 else --사망처리. --변수 초기화 setState(triggerId, "target", "") setChatVar(triggerId, "target", "") setState(triggerId, "stat", "") setChatVar(triggerId, "stat", "") setChatVar(triggerId, "targetTrace", "") --캐릭터 정보 변경 target["condition"] = "DEAD" target["체력"] = 0 target["기력"] = 0 local option = {alwaysActive = false, insertOrder = 100, key = "", secondKey = "", regex = false} upsertLocalLoreBook(triggerId, target["이름"], json.encode(target), option) --사망 로그 적용 local totalLog if getChatVar(triggerId, "oldLog") == "[조교시작]" then totalLog = getChatVar(triggerId, "newLog") else totalLog = getChatVar(triggerId, "oldLog") .. getChatVar(triggerId, "newLog") end local lastTrainLog = "<div class='showLog'><p>"..getChatVar(triggerId,"day").."일차 "..ampm.." / 조교 대상:"..target["이름"].."</p><p>"..totalLog.."</p><p>대상은 가혹한 조교를 견디지 못하고 사망했다.</p></div>" addChat(triggerId, "user", "{{".."getvar::html".."}}") setChat(triggerId, getChatLength(triggerId)-2, lastTrainLog) setChatVar(triggerId, "category", "items") alertEffect(triggerId, "대상이 가혹한 조교를 견디지 못하고 사망했습니다.\n조교를 종료합니다.", "danger") screenChange(triggerId, "main") processAndStoreLore(triggerId, "main.lua") end return false end --계산된 체력 기력을 포매팅된 문자열로 저장 target["체력"] = intForm(currentHP) target["기력"] = intForm(currentSP) --[[프롬프트 빌딩]] local prompt = {} --스탯을 n/10으로 작성해 LLM이 현재 수준이 어느정도인지 판단하도록 함 local statProm = {} for k,v in pairs(currentStat) do statProm[k] = v.."/10" end --statdb에서 설명파트만 추출 (shallow copy로 원본 캐시 보호 done at lines 42-50) --target가 보유한 trait에 대한 설명만 추출 local traitProm = {} for k, v in pairs(traitDB) do local tName = v.name or k -- 한글 이름으로 체크 local found, foundVal = hasVal(target, tName) if found then traitProm[tName] = v.desc --target한테 있는 trait 설명만 입력 end end -- traitProm is now a table, will be encoded later local p_base = getLoreBookContent(triggerId, "trainResponse.pt") local json_stat = json.encode(statDescDB) -- markDB sanitization local markDescDB = {} for k, v in pairs(markDB) do markDescDB[v.name or k] = v.desc end local json_mark = json.encode(markDescDB) -- ablDB sanitization local ablDescDB = {} for k, v in pairs(ablDB) do ablDescDB[v.name or k] = v.desc end local json_abl = json.encode(ablDescDB) local json_trait = json.encode(traitProm) -- itemDB sanitization (handling categories) local itemDescDB = {} if itemDB then for cat, items in pairs(itemDB) do itemDescDB[cat] = {} for k, v in pairs(items) do itemDescDB[cat][v.name or k] = v.desc end end end local json_items = json.encode(itemDescDB) local json_exp = json.encode(EXPtbl) local json_stat_lv = json.encode(statProm) prompt = { promptBuild("system", p_base or ""), promptBuild("system", "- STAT: "..json_stat), promptBuild("system", "- MARK: "..json_mark), promptBuild("system", "- ABILITY: "..json_abl), promptBuild("system", "- TRAIT: "..json_trait), promptBuild("system", "- ITEMS: "..json_items), promptBuild("system", "- EXP_TABLE: "..json_exp), promptBuild("system", "- character_sheet: "..(target_c or "")), promptBuild("system", "- character_stats_Lv: "..json_stat_lv), promptBuild("system", "- training_command: "..command), promptBuild("system", "- command_result: "..rollMsg) } debug("trainProcess: LLM (trainResponse) 호출 시작") local trainResponse = LLM(triggerId, prompt, "trainResponse") debug("trainProcess: LLM (trainResponse) 호출 완료. Success="..tostring(trainResponse.success)) if not trainResponse.success then --LLM응답 실패시 함수 종료 debug("[Train:Error] trainResponse LLM응답 실패: " .. tostring(trainResponse.result)) alertNormal(triggerId, "trainResponse LLM응답 실패: " .. tostring(trainResponse.result)) setChatVar(triggerId, "cmds", old_cmds) reloadDisplay(triggerId) return false end --trainResponse 응답저장 local content = trainResponse.result debug("stat계산: "..content) --statEXP 저장 local statEXPStr = content:match("({.*})") local statEXP = {} if statEXPStr then local status, res = pcall(json.decode, statEXPStr) if status then statEXP = res else debug("[Train:Error] Failed to decode statEXP JSON: " .. tostring(res)) end else debug("[Train:Error] No JSON found in LLM response") end -- customEvent: onStatEXP 처리 tags = tags or {} for statName, amount in pairs(statEXP) do if tonumber(amount) and amount > 0 then local ctx = { statEXP = amount, statName = statName, tags = tags, target = target, stat = currentStat, allStatEXP = statEXP } ctx = sysFunction(triggerId, "customEvent.sys", "onStatEXP", ctx) statEXP[statName] = ctx.statEXP end end --dc에 따른 statEXP 획득 제어 for k,v in pairs(statEXP) do local maxLv = math.floor(dc * STAT_EXP_DC_FACTOR) --DC 기반 최대 레벨 계산 local limit = EXPtbl[tostring(maxLv)] if limit then statEXP[k] = math.min(v, limit) else debug("[Train:Warn] No EXP limit found for level " .. tostring(maxLv)) end end --trait에 따른 statEXP 획득 제어 statEXP["U쾌락"] = (statEXP["U쾌락"] or 0) * (target["요도성감"] or 0) statEXP["M쾌락"] = (statEXP["M쾌락"] or 0) * (target["인후성감"] or 0) --각인획득 판정. 각인 획득시 screenEffect를 덮어 씀 local screenEffect_temp target, screenEffect_temp = sysFunction(triggerId, "getMark.sys", statEXP, target) if screenEffect_temp ~= "" then screenEffect = screenEffect_temp end --[[절정 계산]] -- 실패 시 유저 쾌락(feeling) 제거 (봉사 실패 등) if roll + rollBonus <= dc then feeling = 0 end local resTarget = sysFunction(triggerId, "train.sys", "orgasm", "target", statEXP, 0, tags) local resUser = sysFunction(triggerId, "train.sys", "orgasm", "user", {}, feeling, tags) local orgasm_t = resTarget.isOrgasm local orgasm_u = resUser.isOrgasm debug(target["이름"].."절정치: " .. resTarget.ratio .. "%") debug("{{user}}절정치: " .. resUser.ratio .. "%") --절정여부 체크 local ejMsg = "아무도 절정하지 않음." if orgasm_t then ejMsg = "조교대상 절정." end if orgasm_u then ejMsg = "{{user}} 절정." end if orgasm_t and orgasm_u then ejMsg = "조교대상과 {{user}} 동시 절정." end --절정경험 추가 if orgasm_t then target["절정경험"] = target["절정경험"] + 1 end --[[ 유저의 절정....경험처리가 필요한가? if orgasm_u then user["절정경험"] = user["절정경험"] + 1 end]] --스탯변화값을 가져와 currentstat이 레벨업 하는지를 stat.db의 테이블의 각 레벨값과 비교해 확률적으로 계산 local lvUpcomment = "" local statResult = {} local lvUp lvUp = function(remainStat , currentLv, statName) local lv = tostring(currentLv) local remainStat = remainStat local r = math.random() -- 안전장치: 다음 레벨 테이블이 없으면 성장 중지 (Max Level) local reqExp = EXPtbl[lv] if not reqExp then return lv end debug(statName..": "..remainStat.."/"..reqExp..": "..remainStat/reqExp .. " > " .. r) if remainStat/reqExp > r then remainStat = math.max(remainStat - reqExp, 0) lv = lvUp(remainStat, lv+1, statName) return lv else return lv end end for k, _ in pairs(currentStat) do local v = statEXP[k] or "0" -- statEXP에 해당 키값이 없을 경우 0을 반환하도록 해 에러방지 local lv = currentStat[k] if tonumber(v) > 0 then statResult[k] = tonumber(lvUp(v, lv, k)) currentStat[k] = tonumber(currentStat[k]) if currentStat[k] < statResult[k] then lvUpcomment = lvUpcomment.. "<br>※ " .. k .. "이(가) " .. currentStat[k] .. "에서 " .. statResult[k] .. "로 증가했습니다!" end else statResult[k] = lv end end local process_comment = "[조교 로그 생성중]" setChatVar(triggerId, "cmds", loading_prepend .. process_comment .. loading_append) reloadDisplay(triggerId) --[[trainLog 프롬프트 빌딩]] --기존 로그 불러옴 local oldLog if getChatVar(triggerId, "oldLog") == date then oldLog = getChatVar(triggerId, "newLog") else oldLog = getChatVar(triggerId, "oldLog") .. getChatVar(triggerId, "newLog") end prompt = { promptBuild("system", getLoreBookContent(triggerId, "system.pt")), promptBuild("assistant", getLoreBookContent(triggerId, "jailBreak.pt")), --탈옥 프리필 promptBuild("system", getLoreBookContent(triggerId, "trainLog.pt")), promptBuild("system", getLoreBookContent(triggerId, "worldBriefing.pt")), promptBuild("system", "- STAT: "..json_stat), promptBuild("system", "- MARK: "..json_mark), promptBuild("system", "- ABILITY: "..json_abl), promptBuild("system", "- TRAIT: "..json_trait), promptBuild("system", "- ITEMS: "..json_items), promptBuild("system", "- character_sheet: "..target_c), promptBuild("system", "- character_stats_Lv: "..json.encode(statResult)), promptBuild("system", "- character_stats_Lv: "..equipments), promptBuild("system", "- {{user}}'s_profile: "..user), promptBuild("system", "- King's_profile: "..king), promptBuild("system", "- training_command: "..command), --조교명령 promptBuild("system", "- command_result: "..rollMsg), --성공여부 promptBuild("system", "- orgasm: "..ejMsg), --절정여부 promptBuild("assistant", "## Train Log"), --로그 시작 promptBuild("assistant", oldLog) --기존 로그 } debug("trainProcess: LLM (trainLog) 호출 시작") local trainLog = LLM(triggerId, prompt, "trainLog") --LLM에 응답 요청 debug("trainProcess: LLM (trainLog) 호출 완료. Success="..tostring(trainLog.success)) if not trainLog.success then --LLM응답 실패시 함수 종료 debug("[Train:Error] trainLog LLM응답 실패: " .. tostring(trainLog.result)) alertNormal(triggerId, "trainLog LLM응답 실패: " .. tostring(trainLog.result)) setChatVar(triggerId, "cmds", old_cmds) reloadDisplay(triggerId) return false end --[[응답처리]] --신규로그 저장 local dialog = trainLog.result dialog = dialog:gsub("<Thoughts>.-</Thoughts>", "") --생각의 사슬 제거 dialog = dialog:gsub("<br><br>", "<br>") --불필요한 줄바꿈 제거 debug("조교로그: "..dialog) local newLog = "<br>" .. dialog --수정구 녹화로그 저장 if TRAIN_REC > 0 then debug("수정구영상 녹화중. 잔여회수: "..TRAIN_REC) if TRAIN_REC == 1 then newLog = newLog .. "<br>※ 수정구 영상의 녹화가 종료되었습니다." end executeFunction(triggerId, "train", "200", date, target["이름"], newLog) end --[[마무리 처리]] setChatVar(triggerId, "cmds", old_cmds) --버튼 원복 setChatVar(triggerId, "statLvUp", lvUpcomment) --stat 변화 설명은 log에 포함되지 않도록 별도 처리 setState(triggerId, "stat", statResult) --변경된 stat을 state에 반영 local stat_c = "{" for k, v in pairs(statResult) do stat_c = stat_c .. '"'..k..'":"'..v..'",' end stat_c = string.gsub(stat_c, ",$", "}")-- 마지막 쉼표 대신 중괄호 닫기 setChatVar(triggerId, "stat", stat_c) --변경된 stat을 chatVar에 반영 --절정치 변수에 적용 (train.orgasm에서 처리됨) --로그 처리 setChatVar(triggerId, "oldLog", oldLog) setChatVar(triggerId, "newLog", newLog) --실패/대성공 전역화면 효과 저장. getMark에서 챗변수가 입력되지 않았다면 현재 조교 성공실패 html을 입력 if getChatVar(triggerId, "screenEffect") == "" then setChatVar(triggerId, "screenEffect", screenEffect) end --조교간에 변경된 유저 또는 대상의 정보(hp, sp 절정경험 등)은 state와 챗변수에 쌓아뒀다가 조교 종료시에 로어북으로 반영. setState(triggerId, "target", target) stateToVar(triggerId, "target", target) reloadDisplay(triggerId) debug("trainProcess: 정상 종료, reloadDisplay 호출됨") return trueend
- #41turnEnd.sysconstant
Keys:
function (triggerId, action) debug("turnEnd 실행: " .. tostring(action)) --------------------------------------- -- ★ 밸런싱 상수 정의 (Balancing Constants) -- 턴 종료 시스템의 핵심 수치를 조정하려면 이 섹션의 값을 수정하세요. ---------------------------------------
-- [침공 이벤트 상수] local BATTLE_PROB = 1 -- 전투 이벤트 발생 확률 (50%). 밤에 이 확률로 용사 침공 발생. local INVASION_MIN_DAY = 3 -- 침공 가능 최소 일수. 이 일수 이상부터 침공 이벤트 발생 가능. -- [밤 회복률] (수면/휴식 중) local HP_GEN_NIGHT = 0.1 -- 밤 HP 회복률 (10%). 최대 HP의 이 비율만큼 회복. local SP_GEN_NIGHT = 0.15 -- 밤 SP 회복률 (15%). 최대 SP의 이 비율만큼 회복. -- [낮 회복률] (일과 중) local HP_GEN_DAY = 0.05 -- 낮 HP 회복률 (5%). 최대 HP의 이 비율만큼 회복. local SP_GEN_DAY = 0.1 -- 낮 SP 회복률 (10%). 최대 SP의 이 비율만큼 회복. -- [회복 페널티] local RECOVERY_PENALTY_FAINT = 0.5 -- 실신 상태 시 HP 회복률 감소 배율 (50%). -- [상태 변경 조건] local TRAINABLE_SP_RATIO = 0.9 -- 조교가능 전환 SP 조건. SP가 (최대 SP * 이 값) 이상이면 실신→조교가능 전환. --------------------------------------- --HP/SP 회복 처리 함수 --isNight: true면 밤(높은 회복률), false면 낮(낮은 회복률) local function processRecovery(triggerId, charName, isNight) local char = json.decode(getLoreBookContent(triggerId, charName)) --기본 회복률 설정 local hpGen, spGen if isNight then hpGen = HP_GEN_NIGHT spGen = SP_GEN_NIGHT else hpGen = HP_GEN_DAY spGen = SP_GEN_DAY end --[[ customEvent: onDayPass trait.db에서 정의된 회복 보너스/페널티를 적용 (예: 빠른회복, 느린회복 특성) ]] local recoveryCtx = { target = char, stat = {}, tags = {}, hpGen = hpGen, spGen = spGen, isNight = isNight } recoveryCtx = sysFunction(triggerId, "customEvent.sys", "onDayPass", recoveryCtx) hpGen = recoveryCtx.hpGen spGen = recoveryCtx.spGen --실신 시 HP 회복 감소 if char["condition"] == "FAINT" then hpGen = hpGen * RECOVERY_PENALTY_FAINT end --실제 회복 계산 local maxHP = tonumber(char["최대체력"]) local maxSP = tonumber(char["최대기력"]) local currentHP = tonumber(char["체력"]) local currentSP = tonumber(char["기력"]) char["체력"] = intForm(math.min(currentHP + math.floor(maxHP * hpGen), maxHP)) char["기력"] = intForm(math.min(currentSP + math.floor(maxSP * spGen), maxSP)) --기력이 조건 충족 시 실신에서 조교가능으로 변경 if tonumber(char["기력"]) > maxSP * TRAINABLE_SP_RATIO and char["condition"] == "FAINT" then char["condition"] = "TRAINABLE" end --로어북에 저장 upsertLocalLoreBook(triggerId, charName, json.encode(char), DEFAULT_LORE_OPTION) return char end --시간 정보 호출 local day = getChatVar(triggerId, "day") local ampm = getChatVar(triggerId, "ampm") local charsStr = getChatVar(triggerId, "chars") local chars = {} if charsStr and charsStr ~= "" then local status, res = pcall(json.decode, charsStr) if status then chars = res else debug("turnEnd: chars decoding failed") end end local isNight = (ampm == "1") -- [침공 이벤트 트리거] - 알림보다 먼저 체크 -- 조건: 3일차 이상, 밤(isNight=true), 50% 확률 -- action이 "nextDay"인 경우(전투 후 복귀) 침공 체크 스킵 local invasionTriggered = false local battleParticipants = {} -- [NEW] 전투 참여 인원 추적 if action ~= "nextDay" and isNight and tonumber(day) >= INVASION_MIN_DAY and math.random() < BATTLE_PROB then invasionTriggered = true -- 즉시 배틀화면으로 전환 (로딩 상태 표시) setChatVar(triggerId, "battleLoadingState", "침공 용사 파악중...") setChatVar(triggerId, "battleLogsHtml", "") -- 초기화 setChatVar(triggerId, "battleNarration", "") setChatVar(triggerId, "battleSkipped", "0") -- 스킵 상태 초기화 setChatVar(triggerId, "defenderImage", "") setChatVar(triggerId, "battleDungeonHtml", "") screenChange(triggerId, "battle") reloadDisplay(triggerId) -- 화면 갱신 -- 즉시 배틀화면으로 전환 (로딩 상태 표시) -- 전투 시뮬레이션 실행 (pcall로 감싸서 오류시에도 계속 진행) local ok, summary = pcall(sysFunction, triggerId, "battle.sys", "processInvasion") if not ok then debug("[turnEnd] battle.sys 호출 실패: " .. tostring(summary)) setChatVar(triggerId, "battleLoadingState", "") reloadDisplay(triggerId) alertNormal(triggerId, "전투 시스템 로딩 중 오류가 발생했습니다.") invasionTriggered = false -- 전투 실패시 일반 알림으로 돌아감 else -- battle.sys 호출 성공 end if ok and summary and summary.logs and #summary.logs > 0 then local turnInterval = 1.0 -- CSS 애니메이션(1s)과 싱크를 맞춤 setChatVar(triggerId, "battleTurnInterval", turnInterval) local okD, dungeonStatusHtml = pcall(sysFunction, triggerId, "dungeon.sys", "getBattleStatusHtml", summary.clearedSlots, summary.timeline, turnInterval) if not okD then debug("[turnEnd] dungeon.sys 호출 실패: " .. tostring(dungeonStatusHtml)) dungeonStatusHtml = "Dungeon Status Error" end setChatVar(triggerId, "battleDungeonHtml", dungeonStatusHtml) -- [Attacker Image Generation] local attackerHtml = "" if summary.heroes and #summary.heroes > 0 then -- [NEW] 전투 참여자 기록 (회복 건너뛰기용) for _, h in ipairs(summary.heroes) do if h.name then battleParticipants[h.name] = true debug("[turnEnd] 전투 참여자: " .. h.name .. " (이번 턴 회복 제외)") end end local leader = summary.heroes[1] local portraitContent = "" if leader.portrait_pre and leader.portrait_append then portraitContent = leader.portrait_pre .. leader.portrait_append elseif leader.portrait then local val = leader.portrait if not val:match("^{{") and not val:match("^<img") then val = "{{raw::" .. val .. "}}" end if val:match("{{inlay") or val:match("^<div") or val:match("^<img") then portraitContent = val else portraitContent = [[<img src="]] .. val .. [[" style="width:100%; height:100%; object-fit:cover;">]] end else -- Fallback Default portraitContent = [[<img src="../../Pages/images/icon_sword.png" style="width:100%; height:100%; object-fit:cover;">]] end attackerHtml = portraitContent else attackerHtml = [[<img src="../../Pages/images/icon_sword.png" style="width:100%; height:100%; object-fit:cover;">]] end setChatVar(triggerId, "attackerImage", attackerHtml) -- [No-JS] HTML 로그 생성 (Turn-based Batch 처리) & Defender Image Stack 생성 local logLines = {} local currentTurnLines = {} local turnDelay = 0.0 local lastTurnNum = -1 -- Defender Stack Logic local defenderStack = {} -- 기본 이미지 (마왕) 삭제 - 몬스터만 표시하거나 비워둠 -- 타임라인 기반 Defender Image 생성 -- summary.timeline: { {startTurnIndex=0, defender={type="monster", key="..."}}, ... } -- 타임라인 기반 Defender Image 생성 (역순 적재: 나중 턴이 아래깔림, 앞 턴이 위에 덮임) -- [Boss(Base), ..., Mon2(Timed), Mon1(Timed)] 순서로 DOM에 추가됨 -> Mon1이 맨 위 if summary.timeline then -- 역순 순회 for i = #summary.timeline, 1, -1 do local event = summary.timeline[i] local dKey = event.defender and event.defender.key local dType = event.defender and event.defender.type if dKey and dType ~= "none" then -- Duration 계산 (다음 이벤트까지의 시간) -- 현재 i번째 이벤트의 duration은 정순일 때의 [i+1] - [i] -- 정순 인덱스 i 기준으로 nextEvent는 timeline[i+1] local duration = turnInterval * 5 -- 기본값 local nextEvent = summary.timeline[i + 1] if nextEvent and nextEvent.startTurnIndex then duration = (nextEvent.startTurnIndex - event.startTurnIndex) * turnInterval end -- 퇴장 시간(Delay) = (시작 턴 + 지속 턴) * 턴시간 local exitTime = (event.startTurnIndex * turnInterval) + (duration * 0.9) -- 겹침 방지 위해 살짝 일찍 컷 local src = "" if dType == "monster" then src = "{{raw::" .. dKey .. "}}" elseif dType == "boss" or dKey == "user" then src = "{{source::user}}" else src = "{{raw::" .. dKey .. "}}" end -- 마지막 요소(타임라인상 가장 뒤 = 보스)는 사라지지 않는 Base if i == #summary.timeline then table.insert(defenderStack, string.format('<img src="%s" class="defender-img base">', src)) else -- 그 외 요소는 시간이 되면 사라짐 (Fade Out) -- animation: name duration timing-function delay fill-mode table.insert(defenderStack, string.format( '<img src="%s" class="defender-img timed" style="animation: peel-off 0.5s ease-out %.2fs forwards;">', src, exitTime )) end end end end local defenderHtml = table.concat(defenderStack, "\n") setChatVar(triggerId, "defenderImage", defenderHtml) -- 변수명 재사용 (HTML 삽입) -- 로그 처리: 턴 번호가 바뀔 때마다 딜레이를 누적하여 순차 출력 -- (예: 몬스터1의 Turn1,2,3 -> 몬스터2의 Turn1,2,3 순서로) for i, line in ipairs(summary.logs) do local turnNum = tonumber(line:match("%[Turn (%d+)%]")) or lastTurnNum if turnNum ~= lastTurnNum then -- 이전 턴의 로그들을 현재 딜레이로 출력 if #currentTurnLines > 0 then for _, l in ipairs(currentTurnLines) do table.insert(logLines, string.format('<div class="%s" style="animation-delay: %.2fs">%s</div>', l.class, turnDelay, l.text)) end -- 중요: 누적 딜레이 사용 (절대값 X) turnDelay = turnDelay + turnInterval currentTurnLines = {} end lastTurnNum = turnNum end local class = "log-line" if string.find(line, "처치") or string.find(line, "승리") or string.find(line, "제압") then class = class .. " victory" elseif string.find(line, "피해") or string.find(line, "쓰러졌") then class = class .. " danger" elseif string.find(line, "Turn") then class = class .. " highlight" end table.insert(currentTurnLines, {text=line, class=class}) end if #currentTurnLines > 0 then -- 마지막 턴 로그 처리 -- turnDelay는 위 루프에서 갱신됨 for _, l in ipairs(currentTurnLines) do table.insert(logLines, string.format('<div class="%s" style="animation-delay: %.2fs">%s</div>', l.class, turnDelay, l.text)) end turnDelay = turnDelay + turnInterval end -- 전체 애니메이션 시간 계산 (마지막 턴 기준) local totalAnimTime = turnDelay -- ========== LLM 나레이션 요청 ========== local narrationDelay = turnDelay -- 로그 끝난 후에 나레이션 표시 local narrationHtml = "" -- 프롬프트 데이터 준비 local userJson = getLoreBookContent(triggerId, "user") or "{}" local userName = json.decode(userJson)["이름"] or summary.userName or "마왕" local dungeonLv = summary.dungeonLv or getChatVar(triggerId, "dungeonLv") or "1" -- summary에서 직접 가져옴 (battle.sys에서 이미 생성됨) local bestiaryText = summary.bestiary or "(없음)" local heroListText = summary.heroList or (summary.heroCount .. "명의 용사 침공") local logText = summary.logText or table.concat(summary.logs, "\n") -- 결과 요약 local resultSummary = "" if summary.result == "Win" then resultSummary = "방어 성공 - 침입자 격퇴" elseif summary.result == "BadEnding" then resultSummary = "방어 실패 - 마왕 패배" else resultSummary = summary.result or "미정" end -- 보스전 도달 여부 확인 및 User 정보 포함 local bossReached = false if summary.timeline then for _, event in ipairs(summary.timeline) do if event.defender and event.defender.type == "boss" then bossReached = true break end end end -- 프롬프트 빌딩 local battlePtTemplate = getLoreBookContent(triggerId, "battle.pt") or "" -- 템플릿 변수 치환 (gsub 특수기호 % 우회 처리) local function safeGsub(s, pattern, repl) -- 리터럴 대체를 위해 %를 %%로 이스케이프 local escapedRepl = tostring(repl):gsub("%%", "%%%%") return s:gsub(pattern, escapedRepl) end battlePtTemplate = safeGsub(battlePtTemplate, "{{heroCount}}", summary.heroCount or 1) battlePtTemplate = safeGsub(battlePtTemplate, "{{dungeonLv}}", dungeonLv) battlePtTemplate = safeGsub(battlePtTemplate, "{{userName}}", userName) battlePtTemplate = safeGsub(battlePtTemplate, "{{heroList}}", heroListText) battlePtTemplate = safeGsub(battlePtTemplate, "{{bestiary}}", bestiaryText) battlePtTemplate = safeGsub(battlePtTemplate, "{{logText}}", logText) battlePtTemplate = safeGsub(battlePtTemplate, "{{resultSummary}}", resultSummary) local prompt = { promptBuild("system", getLoreBookContent(triggerId, "worldBriefing.pt") or ""), promptBuild("system", battlePtTemplate) } -- 보스전 도달 시 User 정보 추가 if bossReached then table.insert(prompt, promptBuild("system", "- [마왕 정보] " .. userJson)) end -- LLM 호출 -- setChatVar(triggerId, "battleLoadingState", "전투보고서 작성중...") -- reloadDisplay(triggerId) -- LLM 호출 local narrationResponse = LLM(triggerId, prompt, "battleNarration") local narration = "" local success = false local errorMsg = "알 수 없는 오류" if type(narrationResponse) == "table" then success = narrationResponse.success narration = narrationResponse.result or "" if not success then errorMsg = narration or "API 오류" end elseif type(narrationResponse) == "string" then success = true narration = narrationResponse end if success then narration = narration:gsub("<Thoughts>.-</Thoughts>", "") -- CoT 제거 narrationHtml = string.format('<div class="battle-narration" style="animation-delay: %.2fs">%s</div>', narrationDelay, narration) totalAnimTime = narrationDelay + 0.2 -- 나레이션 표시 시간 최적화 (1s fade + 0.2s buffer) else narrationHtml = string.format('<div class="battle-narration error" style="animation-delay: %.2fs">⚠️ 나레이션 생성 실패: %s</div>', narrationDelay, errorMsg) end -- 저장용 순수 로그 (나레이션 제외) setChatVar(triggerId, "battleLogsOnly", table.concat(logLines, "\n")) table.insert(logLines, narrationHtml) -- 전체 애니메이션 시간 (로그 + 나레이션) local duration = totalAnimTime + 0.3 -- 여유 시간을 1.0s에서 0.3s로 단축 setChatVar(triggerId, "battleLogsHtml", table.concat(logLines, "\n")) setChatVar(triggerId, "battleNarration", narrationHtml:gsub('style="animation%-delay:[^"]*"', '')) -- 애니메이션 제거한 버전 setChatVar(triggerId, "battleResult", summary.result) setChatVar(triggerId, "currentHeroName", "침공 부대 (" .. summary.heroCount .. "명)") setChatVar(triggerId, "animDuration", string.format("%.1fs", duration)) -- 흔들림 효과는 로그 출력 중에만 발생 (나레이션 시간 제외) -- turnDelay(narrationDelay)가 마지막 로그의 시작 시간 + interval이므로, 이를 기준으로 설정 setChatVar(triggerId, "shakeCount", math.ceil(narrationDelay)) -- 로딩 완료 - 오버레이 숨김 setChatVar(triggerId, "battleLoadingState", "") reloadDisplay(triggerId) -- 전투 화면 로딩 완료 reloadDisplay(triggerId) -- 전투 화면 로딩 완료 -- [조건 해금] 침공 (방어 3회 성공) if summary.result == "Win" then local defCount = tonumber(getChatVar(triggerId, "defenseCount")) or 0 defCount = defCount + 1 setChatVar(triggerId, "defenseCount", defCount) local unlockInv = tonumber(getChatVar(triggerId, "unlock_invasion")) or 0 if defCount >= 3 and unlockInv == 0 then setChatVar(triggerId, "unlock_invasion", 1) alertEffect(triggerId, "이제 인간 마을을 직접 침공할 수 있습니다!", "unlock") end end return true -- 침공 발생 시 메인화면 전환 방지 else -- 전투 결과가 없거나 로그가 비어있음 setChatVar(triggerId, "battleLoadingState", "") reloadDisplay(triggerId) alertNormal(triggerId, "던전 침입이 감지되었으나, 흔적을 찾을 수 없습니다.") end end -- 침공이 없을 때만 알림 메시지 표시 if not invasionTriggered then if isNight then alertEffect(triggerId, "하루가 지나갑니다...", "info") else alertEffect(triggerId, "밤이 깊어옵니다...", "info") end setChatVar(triggerId, "category", "items") screenChange(triggerId, "main") end --모든 캐릭터에 대해 회복 처리 (유틸리티 함수 사용) for _, charName in ipairs(chars) do if battleParticipants[charName] then -- 전투 참여자는 이번 턴 회복 스킵 -- (이미 전투 중 소모/변동이 있었고, 휴식을 취하지 못했음) else processRecovery(triggerId, charName, isNight) end end --시간흐름 처리 day = day + ampm ampm = math.abs(ampm - 1) debug(day.."일 "..ampm) setChatVar(triggerId, "day", day) setChatVar(triggerId, "day", day) setChatVar(triggerId, "ampm", ampm) -- [조건 해금] 던전 관리 (2일차 이상) local unlockDungeon = tonumber(getChatVar(triggerId, "unlock_dungeon")) or 0 if tonumber(day) >= 2 and unlockDungeon == 0 then setChatVar(triggerId, "unlock_dungeon", 1) alertEffect(triggerId, "마왕의 부활이 알려졌습니다.<br>몬스터를 소환해 인간들로부터 던전을 방어하세요!", "unlock") end -- [가시성 업데이트] 던전 현황 탭 (배치된 유닛이 있을 때만 표시) local dungeon = getState(triggerId, "dungeon") local showDungeon = 0 if dungeon and dungeon.grid then for k, v in pairs(dungeon.grid) do showDungeon = 1 break end end setChatVar(triggerId, "show_dungeon", showDungeon) -- [조건 해금] 포로 처분 (용사 10명 이상) local unlockDisposal = tonumber(getChatVar(triggerId, "unlock_disposal")) or 0 if unlockDisposal == 0 and #chars >= 10 then setChatVar(triggerId, "unlock_disposal", 1) -- alertNormal(triggerId, "포로 처분 기능이 해금되었습니다.") -- Silent unlock as per plan end --갱신된 유저정보 변수저장 --갱신된 유저정보 변수저장 local userStr = getLoreBookContent(triggerId, "user") local user = {} if userStr and userStr ~= "" then local status, res = pcall(json.decode, userStr) if status then user = res end end stateToVar(triggerId, "user", user)end
- #42traitEvolution.sysconstant
Keys:
-- traitEvolution.sys -- 특성 진화 및 획득 엔진 -- traitRules.sys를 로드하여 조건을 검사하고, 새로운 특성을 부여합니다.
function(triggerId, target) debug("traitEvolution 실행")
]] elseif not success then debug("Rule Check Error [" .. key .. "]: " .. tostring(result)) end end end endlocal traitDB = getDB(triggerId, "trait.db") -- 규칙 파일 로드 (pcall로 안전하게) local rules_ok, rules = pcall(function() return sysFunction(triggerId, "traitRules.sys") end) if not rules_ok or type(rules) ~= "table" then debug("traitRules 로드 실패 또는 형식이 잘못됨: " .. tostring(rules)) return target, "" end -- 필요한 데이터 로드 local stats = getState(triggerId, "stat") local exps = getState(triggerId, "target") -- target 자체가 경험치 필드를 포함하고 있음 (평탄화된 구조) -- 주의: target은 로어북에서 온 평탄화된 데이터. 경험치 키값(한글)이 포함되어 있는지 확인 필요. -- newChar.sys 등을 보면 target["V경험"] 처럼 직접 저장됨. (exp.db의 name 필드가 키로 사용됨) -- 따라서 target을 그대로 exps로 사용해도 무방. local newTraits = {} local logs = "" for key, checkFn in pairs(rules) do -- DB에 존재하는 특성인지 확인 local traitDef = traitDB[key] if traitDef then local traitName = traitDef.name -- 이미 보유 중인지 확인 -- (target[traitName] == 1 또는 "1" 등. 확실하게 체크하기 위해 tonumber 사용) local currentVal = tonumber(target[traitName]) or 0 if currentVal == 0 then -- 조건 검사 local success, result = pcall(checkFn, triggerId, target, stats, target) if success and result == true then -- 특성 부여 target[traitName] = 1 table.insert(newTraits, traitName) debug("새로운 특성 획득: " .. traitName) -- 상충되는 특성 제거 로직 (예: 민감 <-> 둔감) -- traitDef에 "opposite" 필드가 있다면 좋겠지만, 지금은 하드코딩하거나 나중에 DB확장 필요. -- 간단하게 이름 기반으로 추론해서 제거 (예: V민감 <-> V둔감) if traitName:find("민감") then local dullName = traitName:gsub("민감", "둔감") if target[dullName] and tonumber(target[dullName]) > 0 then target[dullName] = 0 debug("상충 특성 제거: " .. dullName) end elseif traitName:find("명기") then -- 명기가 되면 둔감은 확실히 제거 local dullName = traitName:gsub("명기", "둔감") -- V명기 -> V둔감 if target[dullName] and tonumber(target[dullName]) > 0 then target[dullName] = 0 end end -- 멋진 연출을 위한 HTML 구조 logs = logs .. [[return target, logsend
- #43traitRules.sysconstant
Keys:
-- traitRules.sys -- 특성 획득 조건 정의 파일 -- 이 파일은 순수하게 규칙(Logic)만을 정의합니다. -- 각 함수는 (triggerId, target, stats, exps)를 인자로 받아 boolean을 반환해야 합니다.
local rules = {}
-- 헬퍼 함수 local function add(key, fn) rules[key] = fn end
local function getExp(exps, key) return tonumber(exps[key] or 0) end
local function getStat(stats, key) return tonumber(stats[key] or 0) end
local function getAbl(target, key) return tonumber(target[key] or 0) end
local function hasTrait(target, traitName) return tonumber(target[traitName]) and tonumber(target[traitName]) > 0 end
-- ========================================== -- 1. 감각/신체 개발 (Sensation) -- ========================================== -- stat 대신 abl(민감도/확장도)을 체크합니다. -- abl DB에 따르면: C민감도, V민감도, A민감도, B민감도, M민감도, U민감도
-- [C민감]: C경험 100 이상, C민감도 5 이상 add("c_sensitive", function(tid, t, s, e) return getExp(e, "C경험") >= 100 and getAbl(t, "C민감도") >= 5 end)
-- [V민감]: V경험 100 이상, V민감도 5 이상 add("v_sensitive", function(tid, t, s, e) return getExp(e, "V경험") >= 100 and getAbl(t, "V민감도") >= 5 end)
-- [A민감]: A경험 100 이상, A민감도 5 이상 add("a_sensitive", function(tid, t, s, e) return getExp(e, "A경험") >= 100 and getAbl(t, "A민감도") >= 5 end)
-- [B민감]: B경험 100 이상, B민감도 5 이상 add("b_sensitive", function(tid, t, s, e) return getExp(e, "B경험") >= 100 and getAbl(t, "B민감도") >= 5 end)
-- [인후성감]: 펠라경험 100 이상, M민감도 5 이상 add("throat_pleasure", function(tid, t, s, e) return getExp(e, "펠라경험") >= 100 and getAbl(t, "M민감도") >= 5 end)
-- [요도성감]: 요도경험 50 이상, U민감도 5 이상 add("urethra_pleasure", function(tid, t, s, e) return getExp(e, "요도경험") >= 50 and getAbl(t, "U민감도") >= 5 end)
-- [V명기]: V경험 300 이상, V민감도 9 이상, V민감 보유 add("v_masterpiece", function(tid, t, s, e) return getExp(e, "V경험") >= 300 and getAbl(t, "V민감도") >= 9 and hasTrait(t, "V민감") end)
-- [A명기]: A경험 300 이상, A민감도 9 이상, A민감 보유 add("a_masterpiece", function(tid, t, s, e) return getExp(e, "A경험") >= 300 and getAbl(t, "A민감도") >= 9 and hasTrait(t, "A민감") end)
-- ========================================== -- 2. 중독 (Addictions) -- ========================================== -- 욕정(stat) -> 성욕(abl: desire) -- 노출벽(stat) -> 노출벽(abl: exhibitionism) -- 봉사기술(stat) -> 봉사기술(abl: service) -- 마조끼(stat) -> 마조끼(abl: masochism)
-- [성교중독]: 성교경험 100 이상, 성욕 6 이상 add("sex_addict", function(tid, t, s, e) return getExp(e, "성교경험") >= 100 and getAbl(t, "성욕") >= 6 end)
-- [자위중독]: 자위경험 50 이상, 노출벽 3 이상, 성욕 5 이상 add("masturbation_addict", function(tid, t, s, e) return getExp(e, "자위경험") >= 50 and getAbl(t, "노출벽") >= 3 and getAbl(t, "성욕") >= 5 end)
-- [정액중독]: 정액경험 100 이상, 성욕 5 이상, 봉사기술 3 이상 add("semen_addict", function(tid, t, s, e) return getExp(e, "정액경험") >= 100 and getAbl(t, "성욕") >= 5 and getAbl(t, "봉사기술") >= 3 end)
-- [방뇨중독]: 방뇨경험 30 이상, 수치 6 이상 (수치심은 stat 의존이 맞음 - Trait이나 Abl이 아님) -- 하지만 '노출벽' Abl이 수치 플레이와 연관되므로 대체 가능성 고려. -- 일단 수치 Stat은 조교 중 변동이 크므로, '노출벽' Abl이나 '방뇨경험' 비중을 높이는 게 안정적. -- 조건 수정: 방뇨경험 50 이상, 노출벽 3 이상 add("urine_addict", function(tid, t, s, e) return getExp(e, "방뇨경험") >= 50 and getAbl(t, "노출벽") >= 3 end)
-- [마조]: 피학경험 50 이상, 마조끼(Abl) 4 이상 add("masochist", function(tid, t, s, e) return getExp(e, "피학경험") >= 50 and getAbl(t, "마조끼") >= 4 end)
-- [촉수중독]: 촉수경험 50 이상, 성욕 6 이상 add("tentacle_addict", function(tid, t, s, e) return getExp(e, "촉수경험") >= 50 and getAbl(t, "성욕") >= 6 end)
-- [수간중독]: 수간경험 50 이상, 성욕 6 이상 add("beast_addict", function(tid, t, s, e) return getExp(e, "수간경험") >= 50 and getAbl(t, "성욕") >= 6 end)
-- ========================================== -- 3. 칭호 (Titles) -- ==========================================
-- [음란]: (성교경험+자위경험) 300 이상, 성욕 8 이상, 노출벽 5 이상 add("title_lewd", function(tid, t, s, e) local totalSex = getExp(e, "성교경험") + getExp(e, "자위경험") return totalSex >= 300 and getAbl(t, "성욕") >= 8 and getAbl(t, "노출벽") >= 5 end)
-- [암캐]: (수간경험+촉수경험) 100 이상, 복종각인 2 이상 (각인은 유지) add("title_bitch", function(tid, t, s, e) local beastTotal = getExp(e, "수간경험") + getExp(e, "촉수경험") local mark = tonumber(t["복종각인"]) or 0 return beastTotal >= 100 and mark >= 2 end)
-- [연모]: 애정경험 200 이상, 순응 8 이상, 반발각인 0 -- (온순 Stat 대신 순응 Abl 사용) add("title_love", function(tid, t, s, e) local resistMark = tonumber(t["반발각인"]) or 0 return getExp(e, "애정경험") >= 200 and getAbl(t, "순응") >= 8 and resistMark == 0 end)
return rules
- #44char.Formconstant
Keys:
{///주석처리 ///기본정보 "portrait_pre":"{{raw:", ///이미지 출력 앞부분 "portrait_append":":이미지}}", ///이미지 출력 뒷부분 "이름":"", "종족":"", "성별":"", "나이":"", "클래스":"", "레벨":"1", "경험치":"0",
"체력":"", "최대체력":"0", "기력":"0", "최대기력":"0", "공격력":"0", "방어력":"0", "레이팅":"0", "자궁정액":"0", "애널정액":"0", "condition":"", ///외모 "키":"0", "몸무게":"0", "헤어스타일":"", "복장": "", "가슴": "", "유두":"", "음모": "", "페니스":"", "문신": "", "피어싱": "", ///장비 "무기": "없음", "방어구": "없음", "장신구": "없음", ///특성 "성격": [], ///반드시 1개 이상 입력될것. "신체특성": [], "성적취향": [], "칭호": "미함락", ///미함락이 기본값 ///서사 "말투": "", "용사가 되기 전":null, "용사가 된 동기":null, "가족관계":null, "첫키스 상대":"미경험", "첫경험 상대":"미경험", "이상경험 기록":[] ///↑↑↑여기까지가 필수입력 아래는 캐릭터에 미리 설정하고 싶은 항목만 정의 ///abl ///각인 ///경험 ///흔적}
- #45user.initconstant
Keys:
{///주석처리 ///기본정보 "portrait_pre":"[image]", ///이미지 출력 뒷부분 "이름":"{{user}}", "종족":"마족", "성별":"남성", "나이":"청년", "클래스":"마왕", "레벨":"1", "경험치":"0",
"체력":"300", "최대체력":"300", "기력":"300", "최대기력":"300", "공격력":"15~70", "방어력":"13", "레이팅":"400", "자궁정액":"0", "애널정액":"0", "condition":"마왕 업무중", ///외모 "키":"", "몸무게":"", "헤어스타일":"허리까지 내려오는 흑발", "눈":"검은색", "가슴":"", "유두":"", "음모":"보통", "페니스":"평범", "복장":"", "문신":"", "피어싱":"", ///장비 "무기": "마왕의 검", "방어구": "마왕의 갑주", "장신구": "없음", ///특성 "성격": [], "신체특성": [], "성적취향": [], "칭호":"", ///서사 "말투": "", "용사가 되기 전":null, "용사가 된 동기":null, "가족관계":null, "첫키스 상대":null, "첫경험 상대":null}
- #46마오.initconstant
Keys:
{///주석처리 ///기본정보 "portrait_pre":"[image]", ///이미지 출력 뒷부분 "이름":"마오", "종족":"인간", "성별":"여성", "나이":"13", "클래스":"검사", "레벨":"1", "경험치":"0",
"체력":"150", "최대체력":"150", "기력":"150", "최대기력":"150", "공격력":"5~10", "방어력":"3", "레이팅":"100", "자궁정액":"0", "애널정액":"0", "condition":"TRAINABLE", ///외모 "키":"140", "몸무게":"37", "헤어스타일":"붉은 곱슬머리, 숏컷", "복장": "넝마", "가슴": "절벽", "유두": "갈색", "음모": "없음", "페니스":"", "문신": "", "피어싱": "", ///장비 "무기": "낡은 숏소드", "방어구": "없음", "장신구": "없음", ///특성 "성격": ["성숙함", "낙관적"], "신체특성": [], "성적취향": [], "칭호":"미함락", ///서사 "말투": "당돌하고 고집 쎈 말투", "용사가 되기 전":null, "용사가 된 동기":null, "가족관계":null, "첫키스 상대":"미경험", "첫경험 상대":"미경험", "이상경험 기록":[] ///↑↑↑여기까지가 필수입력 아래는 캐릭터에 미리 설정하고 싶은 항목만 정의 ///abl ///각인 ///경험 ///흔적}
- #47DB
Keys: folder:1193244e-1eb7-461b-b9bf-eb9dd38715ce
- #48HTML
Keys: folder:a6bb75af-6123-4fb1-9cae-64c3c818aad0
- #49ablUp.htmlconstant
Keys:
{{getvar::screenEffect}}▋조교 결과
* 조교의 흔적
{{#each {{getvar::traceList}} trace}}{{slot::trace}} {{dict_element::{{getvar::target}}::{{slot::trace}}}}{{/each}}{{settempvar::exps::blank}} {{#each {{getvar::expList}} exp}} {{#if {{? {{dict_element::{{getvar::target}}::{{slot::exp}}}}!=0}}}} {{#if {{? {{gettempvar::exps}}==blank}}}}{{settempvar::exps::{{blank}}}}{{/if}} {{settempvar::exps::{{gettempvar::exps}}
{{slot::exp}}: {{dict_element::{{getvar::target}}::{{slot::exp}}}}}} {{/if}} {{/each}} {{#if {{? {{gettempvar::exps}}!=blank}}}} {{/if}}* 경험
{{gettempvar::exps}} {{#when::__oldScreen::vis::"train"}}* 능력 업그레이드
{{#when::{{? {{getvar::selectedAbl}}==none}}}} {{#each {{getvar::ablList}} abil}} {{#when {{all::{{notequal::{{slot::abil}}::M민감도}}::{{notequal::{{slot::abil}}::U민감도}}::{{notequal::{{slot::abil}}::V확장}}::{{notequal::{{slot::abil}}::A확장}}::{{notequal::{{slot::abil}}::U확장}}}}}}{{slot::abil}}{{/when}} {{#when {{and::{{equal::{{slot::abil}}::M민감도}}::{{equal::{{dict_element::{{getvar::target}}::인후성감}}::1}}}}}}{{slot::abil}}{{/when}} {{#when {{and::{{equal::{{slot::abil}}::U민감도}}::{{equal::{{dict_element::{{getvar::target}}::요도성감}}::1}}}}}}{{slot::abil}}{{/when}} {{#when {{and::{{equal::{{slot::abil}}::V확장}}::{{? {{tonumber::{{dict_element::{{getvar::target}}::V확장경험}}}}>=10}}}}}}{{slot::abil}}{{/when}} {{#when {{and::{{equal::{{slot::abil}}::A확장}}::{{? {{tonumber::{{dict_element::{{getvar::target}}::A확장경험}}}}>=10}}}}}}{{slot::abil}}{{/when}} {{#when {{and::{{equal::{{slot::abil}}::U확장}}::{{? {{tonumber::{{dict_element::{{getvar::target}}::요도경험}}}}>=10}}}}}}{{slot::abil}}{{/when}} {{/each}} {{/when}} {{#when::{{? {{getvar::selectedAbl}}!=none}}}} {{getvar::selectedAbl}}돌아가기{{/when}} {{/when}}* 흔적 변화
{{getvar::traceText}} {{#when::traceText::vis::}}변화없음{{/when}}{{getVar::cmds}} - #50battle.htmlconstant
Keys:
{{#if {{equal::{{getvar::battleLoadingState}}::침공 용사 파악중...}}}}{{/if}}⚠️ 누군가 던전에 침입했습니다..!{{#when::battleLoadingState::vis::}}
{{#if {{equal::{{getvar::battleSkipped}}::0}}}} SKIP ▶▶ {{/if}}{{:else}} {{/when}}{{getvar::battleDungeonHtml}}INVADERVSDEFENDER{{getvar::battleLogsHtml}}{{#when {{equal::{{getvar::battleResult}}::BadEnding}}}}DEFEAT...{{:else}}VICTORY!{{/when}}전투 종료 및 확인
Showing 50 of 87
Extensions: risuai (their contents are in the Raw tab)
133 assets: 133 archived, 0 pending, 0 failed, 0 dead · 0 expression images
1 file
- .pngpng_cardspec 3.0original
18.0 MB1024×1024first seen 8d676533f2d33f77
version alpha 0.1created modified
- source: risurealm:5735ac24-d4ad-449e-b0f7-cec5a0e4f41b
Opens with the tab.
