feat(game): 포션 시스템 및 UI 패널 추가
- 포션 시스템 구현 (PotionService, Potion 모델) - 포션 인벤토리 패널 위젯 - 활성 버프 패널 위젯 - 장비 스탯 패널 위젯 - 스킬 시스템 확장 - 일본어 번역 추가 - 전투 이벤트/상태 모델 개선
This commit is contained in:
@@ -26,6 +26,15 @@ enum CombatEventType {
|
||||
|
||||
/// 플레이어 버프
|
||||
playerBuff,
|
||||
|
||||
/// DOT 틱 데미지
|
||||
dotTick,
|
||||
|
||||
/// 물약 사용
|
||||
playerPotion,
|
||||
|
||||
/// 물약 드랍
|
||||
potionDrop,
|
||||
}
|
||||
|
||||
/// 전투 이벤트 (Combat Event)
|
||||
@@ -188,4 +197,51 @@ class CombatEvent {
|
||||
skillName: skillName,
|
||||
);
|
||||
}
|
||||
|
||||
/// DOT 틱 이벤트 생성
|
||||
factory CombatEvent.dotTick({
|
||||
required int timestamp,
|
||||
required String skillName,
|
||||
required int damage,
|
||||
required String targetName,
|
||||
}) {
|
||||
return CombatEvent(
|
||||
type: CombatEventType.dotTick,
|
||||
timestamp: timestamp,
|
||||
skillName: skillName,
|
||||
damage: damage,
|
||||
targetName: targetName,
|
||||
);
|
||||
}
|
||||
|
||||
/// 물약 사용 이벤트 생성
|
||||
factory CombatEvent.playerPotion({
|
||||
required int timestamp,
|
||||
required String potionName,
|
||||
required int healAmount,
|
||||
required bool isHp,
|
||||
}) {
|
||||
return CombatEvent(
|
||||
type: CombatEventType.playerPotion,
|
||||
timestamp: timestamp,
|
||||
skillName: potionName,
|
||||
healAmount: healAmount,
|
||||
// isHp를 구분하기 위해 targetName 사용 (HP/MP)
|
||||
targetName: isHp ? 'HP' : 'MP',
|
||||
);
|
||||
}
|
||||
|
||||
/// 물약 드랍 이벤트 생성
|
||||
factory CombatEvent.potionDrop({
|
||||
required int timestamp,
|
||||
required String potionName,
|
||||
required bool isHp,
|
||||
}) {
|
||||
return CombatEvent(
|
||||
type: CombatEventType.potionDrop,
|
||||
timestamp: timestamp,
|
||||
skillName: potionName,
|
||||
targetName: isHp ? 'HP' : 'MP',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:askiineverdie/src/core/model/combat_event.dart';
|
||||
import 'package:askiineverdie/src/core/model/combat_stats.dart';
|
||||
import 'package:askiineverdie/src/core/model/monster_combat_stats.dart';
|
||||
import 'package:askiineverdie/src/core/model/potion.dart';
|
||||
import 'package:askiineverdie/src/core/model/skill.dart';
|
||||
|
||||
/// 현재 전투 상태
|
||||
///
|
||||
@@ -17,6 +19,8 @@ class CombatState {
|
||||
required this.turnsElapsed,
|
||||
required this.isActive,
|
||||
this.recentEvents = const [],
|
||||
this.activeDoTs = const [],
|
||||
this.usedPotionTypes = const {},
|
||||
});
|
||||
|
||||
/// 플레이어 전투 스탯
|
||||
@@ -46,6 +50,12 @@ class CombatState {
|
||||
/// 최근 전투 이벤트 목록 (최대 10개)
|
||||
final List<CombatEvent> recentEvents;
|
||||
|
||||
/// 활성 DOT 효과 목록
|
||||
final List<DotEffect> activeDoTs;
|
||||
|
||||
/// 이번 전투에서 사용한 물약 종류 (종류별 1회 제한)
|
||||
final Set<PotionType> usedPotionTypes;
|
||||
|
||||
// ============================================================================
|
||||
// 유틸리티
|
||||
// ============================================================================
|
||||
@@ -65,6 +75,19 @@ class CombatState {
|
||||
/// 몬스터 HP 비율
|
||||
double get monsterHpRatio => monsterStats.hpRatio;
|
||||
|
||||
/// 특정 종류 물약 사용 가능 여부
|
||||
bool canUsePotionType(PotionType type) => !usedPotionTypes.contains(type);
|
||||
|
||||
/// 활성 DOT 존재 여부
|
||||
bool get hasActiveDoTs => activeDoTs.isNotEmpty;
|
||||
|
||||
/// DOT 총 예상 데미지
|
||||
int get totalDotDamageRemaining {
|
||||
return activeDoTs.fold(0, (sum, dot) {
|
||||
return sum + (dot.damagePerTick * dot.remainingTicks);
|
||||
});
|
||||
}
|
||||
|
||||
CombatState copyWith({
|
||||
CombatStats? playerStats,
|
||||
MonsterCombatStats? monsterStats,
|
||||
@@ -75,6 +98,8 @@ class CombatState {
|
||||
int? turnsElapsed,
|
||||
bool? isActive,
|
||||
List<CombatEvent>? recentEvents,
|
||||
List<DotEffect>? activeDoTs,
|
||||
Set<PotionType>? usedPotionTypes,
|
||||
}) {
|
||||
return CombatState(
|
||||
playerStats: playerStats ?? this.playerStats,
|
||||
@@ -88,6 +113,8 @@ class CombatState {
|
||||
turnsElapsed: turnsElapsed ?? this.turnsElapsed,
|
||||
isActive: isActive ?? this.isActive,
|
||||
recentEvents: recentEvents ?? this.recentEvents,
|
||||
activeDoTs: activeDoTs ?? this.activeDoTs,
|
||||
usedPotionTypes: usedPotionTypes ?? this.usedPotionTypes,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -270,9 +270,13 @@ class CombatStats {
|
||||
final baseParryRate = (effectiveDex + effectiveStr) * 0.002;
|
||||
final parryRate = (baseParryRate + equipStats.parryRate).clamp(0.0, 0.4);
|
||||
|
||||
// 공격 속도: DEX 기반 (기본 1000ms, 최소 357ms)
|
||||
// 공격 속도: 무기 기본 공속 + DEX 보정
|
||||
// 무기 attackSpeed가 0이면 기본값 1000ms 사용
|
||||
final weaponItem = equipment.items[0]; // 무기 슬롯
|
||||
final weaponSpeed = weaponItem.stats.attackSpeed;
|
||||
final baseAttackSpeed = weaponSpeed > 0 ? weaponSpeed : 1000;
|
||||
final speedModifier = 1.0 + (effectiveDex - 10) * 0.02;
|
||||
final attackDelayMs = (1000 / speedModifier).round().clamp(357, 1500);
|
||||
final attackDelayMs = (baseAttackSpeed / speedModifier).round().clamp(300, 2000);
|
||||
|
||||
// HP/MP: 기본 + 장비 보너스
|
||||
var totalHpMax = stats.hpMax + equipStats.hpBonus;
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:askiineverdie/src/core/model/combat_state.dart';
|
||||
import 'package:askiineverdie/src/core/model/equipment_item.dart';
|
||||
import 'package:askiineverdie/src/core/model/equipment_slot.dart';
|
||||
import 'package:askiineverdie/src/core/model/item_stats.dart';
|
||||
import 'package:askiineverdie/src/core/model/potion.dart';
|
||||
import 'package:askiineverdie/src/core/model/skill.dart';
|
||||
import 'package:askiineverdie/src/core/util/deterministic_random.dart';
|
||||
|
||||
@@ -23,6 +24,7 @@ class GameState {
|
||||
ProgressState? progress,
|
||||
QueueState? queue,
|
||||
SkillSystemState? skillSystem,
|
||||
PotionInventory? potionInventory,
|
||||
this.deathInfo,
|
||||
}) : rng = DeterministicRandom.clone(rng),
|
||||
traits = traits ?? Traits.empty(),
|
||||
@@ -32,7 +34,8 @@ class GameState {
|
||||
spellBook = spellBook ?? SpellBook.empty(),
|
||||
progress = progress ?? ProgressState.empty(),
|
||||
queue = queue ?? QueueState.empty(),
|
||||
skillSystem = skillSystem ?? SkillSystemState.empty();
|
||||
skillSystem = skillSystem ?? SkillSystemState.empty(),
|
||||
potionInventory = potionInventory ?? const PotionInventory();
|
||||
|
||||
factory GameState.withSeed({
|
||||
required int seed,
|
||||
@@ -44,6 +47,7 @@ class GameState {
|
||||
ProgressState? progress,
|
||||
QueueState? queue,
|
||||
SkillSystemState? skillSystem,
|
||||
PotionInventory? potionInventory,
|
||||
DeathInfo? deathInfo,
|
||||
}) {
|
||||
return GameState(
|
||||
@@ -56,6 +60,7 @@ class GameState {
|
||||
progress: progress,
|
||||
queue: queue,
|
||||
skillSystem: skillSystem,
|
||||
potionInventory: potionInventory,
|
||||
deathInfo: deathInfo,
|
||||
);
|
||||
}
|
||||
@@ -72,6 +77,9 @@ class GameState {
|
||||
/// 스킬 시스템 상태 (Phase 3)
|
||||
final SkillSystemState skillSystem;
|
||||
|
||||
/// 물약 인벤토리
|
||||
final PotionInventory potionInventory;
|
||||
|
||||
/// 사망 정보 (Phase 4, null이면 생존 중)
|
||||
final DeathInfo? deathInfo;
|
||||
|
||||
@@ -88,6 +96,7 @@ class GameState {
|
||||
ProgressState? progress,
|
||||
QueueState? queue,
|
||||
SkillSystemState? skillSystem,
|
||||
PotionInventory? potionInventory,
|
||||
DeathInfo? deathInfo,
|
||||
bool clearDeathInfo = false,
|
||||
}) {
|
||||
@@ -101,6 +110,7 @@ class GameState {
|
||||
progress: progress ?? this.progress,
|
||||
queue: queue ?? this.queue,
|
||||
skillSystem: skillSystem ?? this.skillSystem,
|
||||
potionInventory: potionInventory ?? this.potionInventory,
|
||||
deathInfo: clearDeathInfo ? null : (deathInfo ?? this.deathInfo),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ class ItemStats {
|
||||
this.intBonus = 0,
|
||||
this.wisBonus = 0,
|
||||
this.chaBonus = 0,
|
||||
this.attackSpeed = 0,
|
||||
});
|
||||
|
||||
/// 물리 공격력 보정
|
||||
@@ -97,6 +98,12 @@ class ItemStats {
|
||||
/// CHA 보너스
|
||||
final int chaBonus;
|
||||
|
||||
/// 무기 공격속도 (밀리초, 무기 전용)
|
||||
///
|
||||
/// 0이면 기본값(1000ms) 사용, 값이 클수록 느린 공격.
|
||||
/// 느린 무기는 높은 기본 데미지를 가짐.
|
||||
final int attackSpeed;
|
||||
|
||||
/// 스탯 합계 (가중치 계산용)
|
||||
int get totalStatValue {
|
||||
return atk +
|
||||
@@ -121,6 +128,8 @@ class ItemStats {
|
||||
static const empty = ItemStats();
|
||||
|
||||
/// 두 스탯 합산
|
||||
///
|
||||
/// attackSpeed는 합산 대상 아님 (무기 슬롯 단일 값)
|
||||
ItemStats operator +(ItemStats other) {
|
||||
return ItemStats(
|
||||
atk: atk + other.atk,
|
||||
@@ -139,6 +148,7 @@ class ItemStats {
|
||||
intBonus: intBonus + other.intBonus,
|
||||
wisBonus: wisBonus + other.wisBonus,
|
||||
chaBonus: chaBonus + other.chaBonus,
|
||||
// attackSpeed는 무기에서만 직접 참조
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,6 +169,7 @@ class ItemStats {
|
||||
int? intBonus,
|
||||
int? wisBonus,
|
||||
int? chaBonus,
|
||||
int? attackSpeed,
|
||||
}) {
|
||||
return ItemStats(
|
||||
atk: atk ?? this.atk,
|
||||
@@ -177,6 +188,7 @@ class ItemStats {
|
||||
intBonus: intBonus ?? this.intBonus,
|
||||
wisBonus: wisBonus ?? this.wisBonus,
|
||||
chaBonus: chaBonus ?? this.chaBonus,
|
||||
attackSpeed: attackSpeed ?? this.attackSpeed,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
136
lib/src/core/model/potion.dart
Normal file
136
lib/src/core/model/potion.dart
Normal file
@@ -0,0 +1,136 @@
|
||||
/// 물약 종류
|
||||
enum PotionType {
|
||||
/// HP 회복 물약
|
||||
hp,
|
||||
|
||||
/// MP 회복 물약
|
||||
mp,
|
||||
}
|
||||
|
||||
/// 물약 아이템
|
||||
///
|
||||
/// 전투 중 사용 가능한 소모품.
|
||||
/// 전투당 종류별 1회만 사용 가능.
|
||||
class Potion {
|
||||
const Potion({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.type,
|
||||
required this.tier,
|
||||
this.healAmount = 0,
|
||||
this.healPercent = 0.0,
|
||||
this.price = 0,
|
||||
});
|
||||
|
||||
/// 물약 ID
|
||||
final String id;
|
||||
|
||||
/// 물약 이름
|
||||
final String name;
|
||||
|
||||
/// 물약 종류 (hp / mp)
|
||||
final PotionType type;
|
||||
|
||||
/// 물약 티어 (1~5, 높을수록 강력)
|
||||
final int tier;
|
||||
|
||||
/// 고정 회복량
|
||||
final int healAmount;
|
||||
|
||||
/// 비율 회복량 (0.0 ~ 1.0)
|
||||
final double healPercent;
|
||||
|
||||
/// 구매 가격 (골드)
|
||||
final int price;
|
||||
|
||||
/// HP 물약 여부
|
||||
bool get isHpPotion => type == PotionType.hp;
|
||||
|
||||
/// MP 물약 여부
|
||||
bool get isMpPotion => type == PotionType.mp;
|
||||
|
||||
/// 실제 회복량 계산
|
||||
///
|
||||
/// [maxValue] 최대 HP 또는 MP
|
||||
int calculateHeal(int maxValue) {
|
||||
final percentHeal = (maxValue * healPercent).round();
|
||||
return healAmount + percentHeal;
|
||||
}
|
||||
}
|
||||
|
||||
/// 물약 인벤토리 상태
|
||||
///
|
||||
/// 보유 물약 수량 및 전투 중 사용 기록 관리
|
||||
class PotionInventory {
|
||||
const PotionInventory({
|
||||
this.potions = const {},
|
||||
this.usedInBattle = const {},
|
||||
});
|
||||
|
||||
/// 보유 물약 (물약 ID → 수량)
|
||||
final Map<String, int> potions;
|
||||
|
||||
/// 현재 전투에서 사용한 물약 종류
|
||||
final Set<PotionType> usedInBattle;
|
||||
|
||||
/// 물약 보유 여부
|
||||
bool hasPotion(String potionId) => (potions[potionId] ?? 0) > 0;
|
||||
|
||||
/// 물약 수량 조회
|
||||
int getQuantity(String potionId) => potions[potionId] ?? 0;
|
||||
|
||||
/// 특정 종류 물약 사용 가능 여부
|
||||
///
|
||||
/// 전투당 종류별 1회 제한 체크
|
||||
bool canUseType(PotionType type) => !usedInBattle.contains(type);
|
||||
|
||||
/// 물약 추가
|
||||
PotionInventory addPotion(String potionId, [int count = 1]) {
|
||||
final newPotions = Map<String, int>.from(potions);
|
||||
newPotions[potionId] = (newPotions[potionId] ?? 0) + count;
|
||||
return PotionInventory(
|
||||
potions: newPotions,
|
||||
usedInBattle: usedInBattle,
|
||||
);
|
||||
}
|
||||
|
||||
/// 물약 사용 (수량 감소)
|
||||
PotionInventory usePotion(String potionId, PotionType type) {
|
||||
final currentQty = potions[potionId] ?? 0;
|
||||
if (currentQty <= 0) return this;
|
||||
|
||||
final newPotions = Map<String, int>.from(potions);
|
||||
newPotions[potionId] = currentQty - 1;
|
||||
if (newPotions[potionId] == 0) {
|
||||
newPotions.remove(potionId);
|
||||
}
|
||||
|
||||
final newUsed = Set<PotionType>.from(usedInBattle)..add(type);
|
||||
|
||||
return PotionInventory(
|
||||
potions: newPotions,
|
||||
usedInBattle: newUsed,
|
||||
);
|
||||
}
|
||||
|
||||
/// 전투 종료 시 사용 기록 초기화
|
||||
PotionInventory resetBattleUsage() {
|
||||
return PotionInventory(
|
||||
potions: potions,
|
||||
usedInBattle: const {},
|
||||
);
|
||||
}
|
||||
|
||||
/// 빈 인벤토리
|
||||
static const empty = PotionInventory();
|
||||
|
||||
PotionInventory copyWith({
|
||||
Map<String, int>? potions,
|
||||
Set<PotionType>? usedInBattle,
|
||||
}) {
|
||||
return PotionInventory(
|
||||
potions: potions ?? this.potions,
|
||||
usedInBattle: usedInBattle ?? this.usedInBattle,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,42 @@ enum SkillType {
|
||||
debuff,
|
||||
}
|
||||
|
||||
/// 스킬 속성 (하이브리드: 코드 + 시스템)
|
||||
enum SkillElement {
|
||||
/// 논리 (Logic) - 순수 데미지
|
||||
logic,
|
||||
|
||||
/// 메모리 (Memory) - DoT 특화
|
||||
memory,
|
||||
|
||||
/// 네트워크 (Network) - 다중 타격
|
||||
network,
|
||||
|
||||
/// 화염 (Overheat) - 높은 순간 데미지
|
||||
fire,
|
||||
|
||||
/// 빙결 (Freeze) - 슬로우 효과
|
||||
ice,
|
||||
|
||||
/// 전기 (Surge) - 빠른 연속 타격
|
||||
lightning,
|
||||
|
||||
/// 공허 (Null) - 방어 무시
|
||||
voidElement,
|
||||
|
||||
/// 혼돈 (Glitch) - 랜덤 효과
|
||||
chaos,
|
||||
}
|
||||
|
||||
/// 공격 방식
|
||||
enum AttackMode {
|
||||
/// 단발성 - 즉시 데미지
|
||||
instant,
|
||||
|
||||
/// 지속 피해 - N초간 틱당 데미지
|
||||
dot,
|
||||
}
|
||||
|
||||
/// 버프 효과
|
||||
class BuffEffect {
|
||||
const BuffEffect({
|
||||
@@ -62,6 +98,11 @@ class Skill {
|
||||
this.buff,
|
||||
this.selfDamagePercent = 0.0,
|
||||
this.targetDefReduction = 0.0,
|
||||
this.element,
|
||||
this.attackMode = AttackMode.instant,
|
||||
this.baseDotDamage,
|
||||
this.baseDotDurationMs,
|
||||
this.baseDotTickMs,
|
||||
});
|
||||
|
||||
/// 스킬 ID
|
||||
@@ -100,6 +141,21 @@ class Skill {
|
||||
/// 적 방어력 감소 % (일부 공격 스킬)
|
||||
final double targetDefReduction;
|
||||
|
||||
/// 스킬 속성 (element) - 하이브리드 시스템
|
||||
final SkillElement? element;
|
||||
|
||||
/// 공격 방식 (instant: 단발성, dot: 지속 피해)
|
||||
final AttackMode attackMode;
|
||||
|
||||
/// DOT 기본 틱당 데미지 (스킬 레벨로 결정)
|
||||
final int? baseDotDamage;
|
||||
|
||||
/// DOT 기본 지속시간 (밀리초, 스킬 레벨로 결정)
|
||||
final int? baseDotDurationMs;
|
||||
|
||||
/// DOT 기본 틱 간격 (밀리초, 스킬 레벨로 결정)
|
||||
final int? baseDotTickMs;
|
||||
|
||||
/// 공격 스킬 여부
|
||||
bool get isAttack => type == SkillType.attack;
|
||||
|
||||
@@ -112,6 +168,9 @@ class Skill {
|
||||
/// 디버프 스킬 여부
|
||||
bool get isDebuff => type == SkillType.debuff;
|
||||
|
||||
/// DOT 스킬 여부
|
||||
bool get isDot => attackMode == AttackMode.dot;
|
||||
|
||||
/// MP 효율 (데미지 당 MP 비용)
|
||||
double get mpEfficiency {
|
||||
if (type != SkillType.attack || damageMultiplier <= 0) return 0;
|
||||
@@ -265,3 +324,140 @@ enum SkillFailReason {
|
||||
/// 사용 불가 상태
|
||||
invalidState,
|
||||
}
|
||||
|
||||
/// DOT (지속 피해) 효과
|
||||
///
|
||||
/// 스킬 사용 시 생성되어 전투 중 틱마다 데미지를 적용.
|
||||
/// - INT: 틱당 데미지 증가
|
||||
/// - WIS: 틱 간격 감소 (더 빠른 피해)
|
||||
/// - 스킬 레벨: 기본 데미지, 지속시간, 틱 간격 결정
|
||||
class DotEffect {
|
||||
const DotEffect({
|
||||
required this.skillId,
|
||||
required this.baseDamage,
|
||||
required this.damagePerTick,
|
||||
required this.tickIntervalMs,
|
||||
required this.totalDurationMs,
|
||||
this.remainingDurationMs = 0,
|
||||
this.tickAccumulatorMs = 0,
|
||||
this.element,
|
||||
});
|
||||
|
||||
/// 원본 스킬 ID
|
||||
final String skillId;
|
||||
|
||||
/// 스킬 기본 데미지 (스킬 레벨로 결정)
|
||||
final int baseDamage;
|
||||
|
||||
/// INT 보정 적용된 틱당 실제 데미지
|
||||
final int damagePerTick;
|
||||
|
||||
/// WIS 보정 적용된 틱 간격 (밀리초)
|
||||
final int tickIntervalMs;
|
||||
|
||||
/// 총 지속시간 (밀리초, 스킬 레벨로 결정)
|
||||
final int totalDurationMs;
|
||||
|
||||
/// 남은 지속시간 (밀리초)
|
||||
final int remainingDurationMs;
|
||||
|
||||
/// 다음 틱까지 누적 시간 (밀리초)
|
||||
final int tickAccumulatorMs;
|
||||
|
||||
/// 속성 (선택)
|
||||
final SkillElement? element;
|
||||
|
||||
/// DOT 만료 여부
|
||||
bool get isExpired => remainingDurationMs <= 0;
|
||||
|
||||
/// DOT 활성 여부
|
||||
bool get isActive => remainingDurationMs > 0;
|
||||
|
||||
/// 예상 남은 틱 수
|
||||
int get remainingTicks {
|
||||
if (tickIntervalMs <= 0) return 0;
|
||||
return (remainingDurationMs / tickIntervalMs).ceil();
|
||||
}
|
||||
|
||||
/// 스킬과 플레이어 스탯으로 DotEffect 생성
|
||||
///
|
||||
/// [skill] DOT 스킬
|
||||
/// [playerInt] 플레이어 INT (틱당 데미지 보정)
|
||||
/// [playerWis] 플레이어 WIS (틱 간격 보정)
|
||||
factory DotEffect.fromSkill(Skill skill, {int playerInt = 10, int playerWis = 10}) {
|
||||
assert(skill.isDot, 'DOT 스킬만 DotEffect 생성 가능');
|
||||
assert(skill.baseDotDamage != null, 'baseDotDamage 필수');
|
||||
assert(skill.baseDotDurationMs != null, 'baseDotDurationMs 필수');
|
||||
assert(skill.baseDotTickMs != null, 'baseDotTickMs 필수');
|
||||
|
||||
// INT → 데미지 보정 (INT 10 기준, ±3%/포인트)
|
||||
final intMod = 1.0 + (playerInt - 10) * 0.03;
|
||||
final actualDamage = (skill.baseDotDamage! * intMod).round();
|
||||
|
||||
// WIS → 틱 간격 보정 (WIS 10 기준, ±2%/포인트, 빨라짐)
|
||||
final wisMod = 1.0 + (playerWis - 10) * 0.02;
|
||||
final actualTickMs = (skill.baseDotTickMs! / wisMod).clamp(200, 2000).round();
|
||||
|
||||
return DotEffect(
|
||||
skillId: skill.id,
|
||||
baseDamage: skill.baseDotDamage!,
|
||||
damagePerTick: actualDamage.clamp(1, 9999),
|
||||
tickIntervalMs: actualTickMs,
|
||||
totalDurationMs: skill.baseDotDurationMs!,
|
||||
remainingDurationMs: skill.baseDotDurationMs!,
|
||||
tickAccumulatorMs: 0,
|
||||
element: skill.element,
|
||||
);
|
||||
}
|
||||
|
||||
/// 시간 경과 후 새 DotEffect 반환
|
||||
///
|
||||
/// [elapsedMs] 경과 시간 (밀리초)
|
||||
/// Returns: (새 DotEffect, 이번에 발생한 틱 수)
|
||||
(DotEffect, int) tick(int elapsedMs) {
|
||||
var newAccumulator = tickAccumulatorMs + elapsedMs;
|
||||
var newRemaining = remainingDurationMs - elapsedMs;
|
||||
var ticksTriggered = 0;
|
||||
|
||||
// 틱 발생 체크
|
||||
while (newAccumulator >= tickIntervalMs && newRemaining > 0) {
|
||||
newAccumulator -= tickIntervalMs;
|
||||
ticksTriggered++;
|
||||
}
|
||||
|
||||
final updated = DotEffect(
|
||||
skillId: skillId,
|
||||
baseDamage: baseDamage,
|
||||
damagePerTick: damagePerTick,
|
||||
tickIntervalMs: tickIntervalMs,
|
||||
totalDurationMs: totalDurationMs,
|
||||
remainingDurationMs: newRemaining.clamp(0, totalDurationMs),
|
||||
tickAccumulatorMs: newRemaining > 0 ? newAccumulator : 0,
|
||||
element: element,
|
||||
);
|
||||
|
||||
return (updated, ticksTriggered);
|
||||
}
|
||||
|
||||
DotEffect copyWith({
|
||||
String? skillId,
|
||||
int? baseDamage,
|
||||
int? damagePerTick,
|
||||
int? tickIntervalMs,
|
||||
int? totalDurationMs,
|
||||
int? remainingDurationMs,
|
||||
int? tickAccumulatorMs,
|
||||
SkillElement? element,
|
||||
}) {
|
||||
return DotEffect(
|
||||
skillId: skillId ?? this.skillId,
|
||||
baseDamage: baseDamage ?? this.baseDamage,
|
||||
damagePerTick: damagePerTick ?? this.damagePerTick,
|
||||
tickIntervalMs: tickIntervalMs ?? this.tickIntervalMs,
|
||||
totalDurationMs: totalDurationMs ?? this.totalDurationMs,
|
||||
remainingDurationMs: remainingDurationMs ?? this.remainingDurationMs,
|
||||
tickAccumulatorMs: tickAccumulatorMs ?? this.tickAccumulatorMs,
|
||||
element: element ?? this.element,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user