작성일: 2026-03-27
용도: 다른 세션의 Claude가 현재 구현 기준 전투/상태/시뮬레이션 공식을 직접 읽고 밸런스를 조정할 수 있도록 정리한 문서
현재 프로젝트는 전투 계산이 3단계로 나뉩니다.
1. 캐릭터 관리 층
- 힘, 민첩, 지능 같은 RPG형 스탯을 다룸
- HP, ATK, DEF 같은 파생 스탯을 만듦
2. 전투 스냅샷 층
- 실제 전투 계산용 값으로 변환
- atk, def, hp_max, move_speed_rate, attack_speed_rate, vision_radius, 원소 속성
3. 런타임 전투 층
- 스킬 채널별 피해 계산
- 상태 이상, 원소 반응, 지속시간, 턴 시뮬레이션 계산
밸런스를 조절할 때는 어느 층을 건드리는지 먼저 정해야 합니다.
현재 구현의 핵심 상수는 아래입니다.
```text
BASE_MOVE_SPEED_MPS = 5.0
BASE_ATTACK_INTERVAL_SEC = 1.0
DEFENSE_SCALE = 100.0
MAX_RESIST = 0.9
MIN_RESIST = -0.75
```
의미:
- 이동속도 배율 1.0이면 초당 5m 이동
- 공격속도 배율 1.0이면 기본 공격 간격 1초
- 방어 공식의 기준 분모는 100
- 원소 저항은 최대 90%, 최소 -75%
현재 캐릭터 파생 스탯 공식은 web/app.js의 calculateCharacterDerivedStats() 기준입니다.
### 3-1. 기본 스탯
- strength
- agility
- intelligence
- wisdom
- vitality
- charm
### 3-2. 추천 스탯
- focus
- endurance
- luck
useRecommendedStats = false이면 추천 스탯은 0으로 처리됩니다.
```text
hp = vitality 44 + strength 9 + level * 18 + equipment.derived.hp
mp = intelligence 18 + wisdom 17 + charm 5 + level 11 + equipment.derived.mp
atk = strength 3.4 + agility 1.2 + focus 0.7 + level 1.6 + equipment.derived.atk
def = vitality 2.6 + endurance 1.8 + strength 0.8 + level 1.25 + equipment.derived.def
moveSpeed = 4 + agility 0.038 + luck 0.012 + equipment.derived.moveSpeed
attackSpeed = 0.92 + agility 0.015 + focus 0.009 + equipment.derived.attackSpeed
critRate = 0.03 + agility 0.002 + focus 0.0018 + luck * 0.0012 + equipment.derived.critRate
critDamage = 1.35 + strength 0.01 + charm 0.004 + focus * 0.006 + equipment.derived.critDamage
accuracy = 70 + agility 2.3 + focus 2.1 + level * 1.05 + equipment.derived.accuracy
evasion = 15 + agility 1.9 + luck 1.4 + level * 0.7 + equipment.derived.evasion
statusResist = 0.05 + wisdom 0.003 + endurance 0.0045 + equipment.derived.statusResist
skillPower = 1 + intelligence 0.011 + wisdom 0.008 + charm * 0.003 + equipment.derived.skillPower
```
```text
powerScore =
hp * 0.26 +
mp * 0.16 +
atk * 14 +
def * 10 +
moveSpeed * 80 +
attackSpeed * 135 +
critRate * 850 +
critDamage * 90 +
accuracy * 1.5 +
evasion * 1.35 +
statusResist * 540 +
skillPower * 190
```
이 값은 난이도 미리보기, 기준 캐릭터 비교, 성장 비교에서 많이 쓰입니다.
전투 시뮬레이션은 캐릭터 파생 스탯을 그대로 쓰지 않고, 전투 엔티티 스냅샷으로 다시 바꿉니다.
```text
atk = round(derived.atk)
def = round(derived.def)
hp_max = round(derived.hp)
move_speed_rate = clamp(derived.moveSpeed / BASE_MOVE_SPEED_MPS, 0.5, 3)
attack_speed_rate = clamp(derived.attackSpeed, 0.5, 3)
vision_radius = clamp(7 + focus 0.14 + wisdom 0.06, 4, 16)
crit_rate = clamp(derived.critRate, 0, 1)
crit_damage = clamp(derived.critDamage - 1, 0, 5)
accuracy = round(derived.accuracy)
evasion = round(derived.evasion)
armor_pen_flat = round(focus * 0.35)
armor_pen_rate = clamp(focus 0.0025 + agility 0.0008, 0, 0.9)
```
### 4-2. 의미
- 캐릭터 관리 수치와 전투 수치가 완전히 같은 것은 아님
- 특히 moveSpeed, attackSpeed, critDamage는 전투층에서 다시 정규화됨
- 밸런스를 잡을 때 “캐릭터 시트에서 강해 보이는데 실전이 약한” 경우는 이 변환층에서 확인해야 함
장비는 세 종류입니다.
- weapon
- armor
- accessory
### 5-1. 아이템 보너스 적용
- statBonuses는 캐릭터 기본/추천 스탯에 더해짐
- derivedBonuses는 캐릭터 파생 스탯에 직접 더해짐
- elements는 캐릭터의 원소 속성과 같은 항목끼리 합산됨
mergeManagedSecondaryElements()는 같은 원소의 같은 필드를 단순 합산합니다.
예:
```text
캐릭터 fire.attack = 12
무기 fire.attack = 16
최종 fire.attack = 28
```
합산 대상 필드:
- attack
- resist
- pen
- mastery
- buildup
- status_duration_rate
전투용 스냅샷은 다시 런타임 파생값을 만듭니다.web/app.js의 computeDerivedSnapshot() / engine.py의 compute_derived_attributes() 기준입니다.
```text
move_speed_mps = BASE_MOVE_SPEED_MPS * move_speed_rate
attack_interval_sec = BASE_ATTACK_INTERVAL_SEC / attack_speed_rate
aps = 1 / attack_interval_sec
sustain_dps = atk * aps
damageTakenMul = DEFENSE_SCALE / (DEFENSE_SCALE + def)
ehp = hp_max / damageTakenMul
= hp_max * (1 + def / DEFENSE_SCALE)
engage_index = move_speed_mps * vision_radius
status_throughput = aps * highest_buildup
```
여기서 highest_buildup은 가진 원소들 중 가장 큰 buildup입니다.
현재 스킬은 여러 damageChannels를 가질 수 있습니다.
예:
- 물리 채널
- 불 채널
- 얼음 채널
```text
base_share = skillBase / channelCount
```
각 채널의 위력:
```text
channelPower = base_share + Σ( lookupStat(attacker, statPath) * coeff )
```
전체 원시 위력:
```text
rawPower = Σ channelPower
```
lookupStat는 다음을 읽을 수 있습니다.
- 1차 속성: atk, def, hp_max 등
- 런타임 파생 속성: aps, ehp, sustain_dps 등
- 원소 속성: fire.attack, ice.attack 등
물리 채널은 방어/방어관통을 사용합니다.
```text
effective_def = max(0, target.def * (1 - armor_pen_rate) - armor_pen_flat)
```
```text
physical_mitigation = DEFENSE_SCALE / (DEFENSE_SCALE + effective_def)
```
```text
physical_damage = channelPower * physical_mitigation
```
원소 채널은 원소 숙련, 저항, 관통을 사용합니다.
```text
effective_resist = clamp(target.element.resist - attacker.element.pen, -0.75, 0.9)
```
```text
element_damage = channelPower (1 + mastery) (1 - effective_resist)
```
여기서:
- mastery = attacker.elements[elementId].mastery
- effective_resist = target.elements[elementId].resist - attacker.elements[elementId].pen
원소 피해 중 가장 큰 채널이 반응 트리거 원소가 됩니다.
### 10-1. 트리거 원소
- 가장 높은 element_damage를 낸 원소 채널
```text
reaction_bonus =
max(
0,
triggerElementDamage * (reaction.damageMultiplier - 1)
+ reaction.bonusFlatDamage
)
```
```text
elemental_damage_total = Σ element_damage
secondary_damage = elemental_damage_total + reaction_bonus
```
```text
final_damage = physical_damage + secondary_damage
```
상태:
- burning
- chilled
- frozen
- shocked
- poisoned
- wet
### 11-1. 핵심 파라미터
- threshold
- baseDurationSec
- decayPerSec
- maxStacks
- exclusiveGroup
- stage
```text
buildup_per_hit = element.buildup + element.attack * 0.1
```
반응이 있으면:
```text
buildup_per_hit *= reaction.buildupMultiplier
```
```text
total_buildup = buildup_per_hit * hits
```
같은 원소의 상태 후보를 stage 오름차순으로 훑으며,
```text
if total_buildup >= status.threshold:
applied = that_status
```
즉, 임계치를 넘는 가장 높은 단계 상태 하나가 최종 후보가 됩니다.
예:
- 얼음 100 이상 -> chilled
- 얼음 160 이상 -> frozen
```text
status_duration =
baseDurationSec * (1 + attacker.elements[sourceElement].status_duration_rate)
```
현재 구현상 타깃의 상태 저항에 의한 지속시간 감소는 캐릭터 관리 층에는 있으나,
런타임 상태 지속시간 공식은 공격자 지속시간 증가만 직접 반영합니다.
상태 적용은 _apply_resolved_status() 계열 로직을 따릅니다.
### 14-1. 같은 상태 재적용
- 기존 지속시간과 새 지속시간 중 더 긴 값으로 갱신
### 14-2. 배타 그룹 없음
- 그냥 공존
### 14-3. 배타 그룹 있음
- 같은 exclusiveGroup 내에서는 더 높은 stage가 승리
- 약한 상태는 막히거나 교체됨
예:
- chilled 위에 frozen -> 승급 가능
- frozen 위에 chilled -> 차단 가능
현재는 가장 우선순위가 높은 반응 하나만 적용합니다.
반응 후보 예:
- fire + chilled -> melt
- fire + frozen -> melt_strong
- fire + wet -> evaporate
- fire + poisoned -> combustion
- ice + burning -> quench
- ice + wet -> freeze
- lightning + wet -> conduct
- poison + burning -> combustion
- poison + wet -> contaminate
- water + burning -> douse
- water + frozen -> thaw
- water + poisoned -> dilute
```text
reaction =
highest priority rule
where attackerElement == incomingElement
and requiredTargetStates ⊆ currentTargetStates
```
resolve_elemental_interaction() 기준:
시작 상태 기록
반응 매칭
반응이 있으면 consumeStates 제거
반응이 있으면 applyStates 적용
축적 기반 predictedStates 적용
최종 상태 목록 반환
반응과 축적은 둘 다 상태를 바꿀 수 있습니다.
시뮬레이션은 현재 “상호 공방” 구조입니다.
```text
attackerInterval = 1 / attackerAPS
targetInterval = 1 / targetAPS
roundIntervalSec = max(attackerInterval, targetInterval)
```
즉, 현재 구현은 느린 쪽의 템포를 라운드 시간으로 잡습니다.
공격자가 스킬 사용
공격자 상태 미리보기/상태 반영
대상 HP 감소
대상이 살아 있으면 반격 스킬 사용
공격자 HP 감소
대상 상태 지속시간 감쇠
턴 로그 저장
### 17-3. 시뮬레이션 결과 핵심 값
- totalDamage
- totalPhysicalDamage
- totalSecondaryDamage
- totalReactionBonus
- totalCounterDamage
- winner
- ttkSec
- timeline[]
몬스터는 캐릭터와 달리 이미 런타임형 속성 구조에 가깝습니다.
핵심 성장 항목:
- atk
- def
- hp_max
- move_speed_rate
- attack_speed_rate
- vision_radius
레벨별 값은 monsterAttributesAtLevel(monster, level)로 얻고,
시뮬레이션에는 현재 previewLevel 값이 들어갑니다.
캐릭터 관리의 “예상 난이도”는 실제 전투 시뮬레이터와는 별도의 예측 모델입니다.
기준 캐릭터의 동일/보정 레벨 파생 스탯을 기준으로 가상 몬스터를 만듭니다.
```text
comparisonLevel = clamp(character.previewLevel + recommendedLevelGap, 1, baseline.maxLevel)
monsterPower = baseline.powerScore difficultyScale monsterPowerBias
monsterHp = baseline.hp difficultyScale monsterHpBias
monsterAtk = baseline.atk difficultyScale monsterAttackBias
monsterDef = baseline.def difficultyScale monsterDefenseBias
```
```text
powerRatio = character.powerScore / monsterPower
durabilityFactor =
(monsterHp / baseline.hp) * 0.65 +
(monsterDef / baseline.def) * 0.35
pressureFactor =
(monsterAtk / baseline.atk) * 0.65 +
monsterPowerBias * 0.35
```
```text
expectedBattleTimeSec =
(16 * clamp(durabilityFactor, 0.72, 1.9)) /
clamp(powerRatio, 0.55, 1.85)
```
```text
challengeScore =
(1 / powerRatio) * 0.34 +
timePressureRatio * 0.34 +
durabilityFactor * 0.18 +
pressureFactor * 0.14
```
### 19-5. 난이도 라벨
- <= 0.88 쉬움
- <= 1.04 적정
- <= 1.22 어려움
- 그 이상 매우 어려움
엔진 apply_modifiers() 기준으로 modifier는 다음 순서로 적용됩니다.
```text
current = current + sum(flat)
current = current * (1 + sum(add_rate))
current = current * each(more)
current = current + sum(post_flat)
if override exists:
current = highest_priority_override
```
즉, 순서는 다음과 같습니다.
flat
add_rate
more
post_flat
override
실제로 체감이 크게 바뀌는 순서:
atk, def, hp_max
armor_pen_rate, armor_pen_flat
원소 attack, mastery, resist, pen
상태 buildup, threshold, baseDurationSec
attack_speed_rate
반응 damageMultiplier, bonusFlatDamage
캐릭터 관리 공식과 런타임 전투 공식은 층이 다르다.
캐릭터는 전투 전에 스냅샷 변환을 한 번 더 거친다.
아이템 원소 속성은 캐릭터 원소 속성에 단순 합산된다.
원소 반응은 다중 적용이 아니라 “우선순위 1개”만 선택한다.
시뮬레이션은 현재 느린 쪽 공격 간격을 라운드 시간으로 쓴다.
난이도 미리보기는 실제 전투 로그가 아니라 예측 공식이다.
이 문서는 아래 구현을 기준으로 정리했습니다.
캐릭터/시뮬레이션/페이지 로직: web/app.js
엔진 계산 코어: engine.py
상태/반응/속성 계약: docs/contracts/attribute_schema.json, docs/contracts/status_reaction_matrix.json
밸런스를 조정할 때는 아키텍처 설명 문서보다 현재 코드와 contracts를 우선 기준으로 보는 것이 안전합니다.
감탄과 신고는 로그인 후 이용할 수 있습니다.
Replies