feat: Add new traits and requirements for war mechanics

- Introduced new traits including '귀병', '신산', '환술', '집중', '신중', '반계', '돌격', '무쌍', '견고', '위압', '저격', '필살', '격노', and '척사'.
- Implemented selection criteria for each trait, specifying weight, weight type, and requirements based on leadership, strength, intelligence, and army types.
- Updated existing traits to include selection properties for better trait management.
- Added a new requirements module to define trait requirements and weight types.
- Enhanced the trait selection logic to accommodate new traits and their specific conditions.
This commit is contained in:
2026-01-06 15:10:00 +00:00
parent 55a9230a36
commit 0cfdebbb08
33 changed files with 482 additions and 4 deletions
@@ -1,3 +1,4 @@
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/triggers/special/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
// 내정 특기: 경작
@@ -6,6 +7,11 @@ export const traitModule: TraitModule = {
name: '경작',
info: '[내정] 농지 개간 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
kind: 'domestic',
selection: {
requirements: [TraitRequirement.STAT_INTEL],
weight: 1,
weightType: TraitWeightType.NORM,
},
getName: () => '경작',
getInfo: () => '[내정] 농지 개간 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
onCalcDomestic: (_context, turnType, varType, value) => {
@@ -1,3 +1,4 @@
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/triggers/special/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
// 내정 특기: 귀모
@@ -6,6 +7,11 @@ export const traitModule: TraitModule = {
name: '귀모',
info: '[계략] 화계·탈취·파괴·선동 : 성공률 +20%p',
kind: 'domestic',
selection: {
requirements: [TraitRequirement.STAT_LEADERSHIP, TraitRequirement.STAT_STRENGTH, TraitRequirement.STAT_INTEL],
weight: 2.5,
weightType: TraitWeightType.PERCENT,
},
getName: () => '귀모',
getInfo: () => '[계략] 화계·탈취·파괴·선동 : 성공률 +20%p',
onCalcDomestic: (_context, turnType, varType, value) => {
@@ -1,3 +1,4 @@
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/triggers/special/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
// 내정 특기: 발명
@@ -6,6 +7,11 @@ export const traitModule: TraitModule = {
name: '발명',
info: '[내정] 기술 연구 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
kind: 'domestic',
selection: {
requirements: [TraitRequirement.STAT_INTEL],
weight: 1,
weightType: TraitWeightType.NORM,
},
getName: () => '발명',
getInfo: () => '[내정] 기술 연구 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
onCalcDomestic: (_context, turnType, varType, value) => {
@@ -1,3 +1,4 @@
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/triggers/special/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
// 내정 특기: 상재
@@ -6,6 +7,11 @@ export const traitModule: TraitModule = {
name: '상재',
info: '[내정] 상업 투자 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
kind: 'domestic',
selection: {
requirements: [TraitRequirement.STAT_INTEL],
weight: 1,
weightType: TraitWeightType.NORM,
},
getName: () => '상재',
getInfo: () => '[내정] 상업 투자 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
onCalcDomestic: (_context, turnType, varType, value) => {
@@ -1,3 +1,4 @@
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/triggers/special/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
// 내정 특기: 수비
@@ -6,6 +7,11 @@ export const traitModule: TraitModule = {
name: '수비',
info: '[내정] 수비 강화 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
kind: 'domestic',
selection: {
requirements: [TraitRequirement.STAT_STRENGTH],
weight: 1,
weightType: TraitWeightType.NORM,
},
getName: () => '수비',
getInfo: () => '[내정] 수비 강화 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
onCalcDomestic: (_context, turnType, varType, value) => {
@@ -1,3 +1,4 @@
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/triggers/special/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
// 내정 특기: 인덕
@@ -6,6 +7,11 @@ export const traitModule: TraitModule = {
name: '인덕',
info: '[내정] 주민 선정·정착 장려 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
kind: 'domestic',
selection: {
requirements: [TraitRequirement.STAT_LEADERSHIP],
weight: 1,
weightType: TraitWeightType.NORM,
},
getName: () => '인덕',
getInfo: () => '[내정] 주민 선정·정착 장려 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
onCalcDomestic: (_context, turnType, varType, value) => {
@@ -1,3 +1,4 @@
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/triggers/special/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
// 내정 특기: 축성
@@ -6,6 +7,11 @@ export const traitModule: TraitModule = {
name: '축성',
info: '[내정] 성벽 보수 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
kind: 'domestic',
selection: {
requirements: [TraitRequirement.STAT_STRENGTH],
weight: 1,
weightType: TraitWeightType.NORM,
},
getName: () => '축성',
getInfo: () => '[내정] 성벽 보수 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
onCalcDomestic: (_context, turnType, varType, value) => {
@@ -1,3 +1,4 @@
import { TraitRequirement, TraitWeightType } from '@sammo-ts/logic/triggers/special/requirements.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
// 내정 특기: 통찰
@@ -6,6 +7,11 @@ export const traitModule: TraitModule = {
name: '통찰',
info: '[내정] 치안 강화 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
kind: 'domestic',
selection: {
requirements: [TraitRequirement.STAT_STRENGTH],
weight: 1,
weightType: TraitWeightType.NORM,
},
getName: () => '통찰',
getInfo: () => '[내정] 치안 강화 : 기본 보정 +10%, 성공률 +10%p, 비용 -20%',
onCalcDomestic: (_context, turnType, varType, value) => {
@@ -1,4 +1,6 @@
export * from './types.js';
export * from './requirements.js';
export * from './selector.js';
export * from './registry.js';
export * from './domestic/index.js';
export * from './war/index.js';
@@ -0,0 +1,30 @@
export enum TraitRequirement {
DISABLED = 0x1,
STAT_LEADERSHIP = 0x2,
STAT_STRENGTH = 0x4,
STAT_INTEL = 0x8,
ARMY_FOOTMAN = 0x100,
ARMY_ARCHER = 0x200,
ARMY_CAVALRY = 0x400,
ARMY_WIZARD = 0x800,
ARMY_SIEGE = 0x1000,
REQ_DEXTERITY = 0x4000,
STAT_NOT_LEADERSHIP = 0x20000,
STAT_NOT_STRENGTH = 0x40000,
STAT_NOT_INTEL = 0x80000,
}
export enum TraitWeightType {
NORM = 1,
PERCENT = 2,
}
export interface TraitSelection {
weightType: TraitWeightType;
weight: number;
requirements: TraitRequirement[];
}
@@ -0,0 +1,150 @@
import { RandUtil } from '@sammo-ts/common';
import { TraitRequirement, TraitWeightType } from './requirements.js';
import type { TraitModule } from './types.js';
import type { ScenarioStatBlock } from '../../scenario/types.js';
export class TraitSelector {
/**
* 기초 스탯 기반 선택 조건 계산 (calcCondGeneric)
*/
static calcCondGeneric(
stats: { leadership: number; strength: number; intelligence: number },
scenarioStat: ScenarioStatBlock
): number {
const { leadership, strength, intelligence } = stats;
const chiefMin = scenarioStat.chiefMin;
let myCond = 0;
if (leadership < chiefMin || strength < chiefMin || intelligence < chiefMin) {
if (leadership < chiefMin) myCond |= TraitRequirement.STAT_NOT_LEADERSHIP;
if (strength < chiefMin) myCond |= TraitRequirement.STAT_NOT_STRENGTH;
if (intelligence < chiefMin) myCond |= TraitRequirement.STAT_NOT_INTEL;
}
if (myCond === 0) {
if (leadership * 0.9 > strength && leadership * 0.9 > intelligence) {
myCond |= TraitRequirement.STAT_LEADERSHIP;
} else if (strength >= intelligence) {
myCond |= TraitRequirement.STAT_STRENGTH;
} else {
myCond |= TraitRequirement.STAT_INTEL;
}
}
return myCond;
}
/**
* 숙련도 기반 선택 조건 계산 (calcCondDexterity)
*/
static calcCondDexterity(rng: RandUtil, dex: number[]): number {
const dexMap: Record<number, number> = {
[TraitRequirement.ARMY_FOOTMAN]: dex[0] || 0,
[TraitRequirement.ARMY_ARCHER]: dex[1] || 0,
[TraitRequirement.ARMY_CAVALRY]: dex[2] || 0,
[TraitRequirement.ARMY_WIZARD]: dex[3] || 0,
[TraitRequirement.ARMY_SIEGE]: dex[4] || 0,
};
const dexSum = Object.values(dexMap).reduce((a, b) => a + b, 0);
// 루트(합)/4 확률 기반 로직 (Legacy: sqrt(dexSum)/4)
const dexProb = Math.sqrt(dexSum) / 4;
// Legacy: 80% 확률로 0 반환 (이전 연도에 이미 얻었거나 기타 이유로 제한하는 인지)
// 실제로는 pickSpecialWar에서 이 메서드 호출 전후에 별도 확률을 둘 수도 있으나,
// Legacy SpecialityHelper.php의 로직을 그대로 따름.
if (rng.nextBool(0.8)) {
return 0;
}
if (rng.nextRangeInt(0, 99) < dexProb) {
return 0;
}
if (dexSum === 0) {
return Number(rng.choice(Object.keys(dexMap)));
}
const maxDex = Math.max(...Object.values(dexMap));
const candidates = Object.keys(dexMap)
.map(Number)
.filter((k) => dexMap[k] === maxDex);
return Number(rng.choice(candidates));
}
/**
* 사용 가능한 특기 목록에서 하나를 무작위로 선택 (pickTrait)
*/
static pickTrait(rng: RandUtil, myCond: number, traits: TraitModule[], prevTraitKeys: string[]): string | null {
const normPool: Record<string, number> = {};
const percentPool: { key: string; weight: number }[] = [];
for (const trait of traits) {
if (!trait.selection) continue;
if (prevTraitKeys.includes(trait.key)) continue;
let valid = false;
for (const req of trait.selection.requirements) {
if (req === (req & myCond)) {
valid = true;
break;
}
}
if (!valid) continue;
if (trait.selection.weightType === TraitWeightType.PERCENT) {
percentPool.push({ key: trait.key, weight: trait.selection.weight });
} else {
normPool[trait.key] = trait.selection.weight;
}
}
// PERCENT 타입 특기 먼저 우선권 확인
for (const item of percentPool) {
if (rng.nextBool(item.weight / 100)) {
return item.key;
}
}
// NORM 타입 특기들 중 가중치 기반 선택
if (Object.keys(normPool).length === 0) return null;
return String(rng.choiceUsingWeight(normPool));
}
/**
* 전투 특기 선택 통합 로직
*/
static pickWarTrait(
rng: RandUtil,
stats: { leadership: number; strength: number; intelligence: number },
dex: number[],
traits: TraitModule[],
prevTraitKeys: string[],
scenarioStat: ScenarioStatBlock
): string | null {
let myCond = this.calcCondGeneric(stats, scenarioStat);
const dexCond = this.calcCondDexterity(rng, dex);
if (dexCond) {
myCond |= dexCond | TraitRequirement.REQ_DEXTERITY;
}
return this.pickTrait(rng, myCond, traits, prevTraitKeys);
}
/**
* 내정 특기 선택 통합 로직
*/
static pickDomesticTrait(
rng: RandUtil,
stats: { leadership: number; strength: number; intelligence: number },
traits: TraitModule[],
prevTraitKeys: string[],
scenarioStat: ScenarioStatBlock
): string | null {
const myCond = this.calcCondGeneric(stats, scenarioStat);
return this.pickTrait(rng, myCond, traits, prevTraitKeys);
}
}
@@ -1,6 +1,7 @@
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
import type { GeneralActionModule } from '@sammo-ts/logic/triggers/general-action.js';
import type { WarActionModule } from '@sammo-ts/logic/war/actions.js';
import type { TraitSelection } from './requirements.js';
export type TraitKind = 'domestic' | 'war' | 'personality' | 'nation';
@@ -9,6 +10,7 @@ export interface TraitSpec {
name: string;
info: string;
kind: TraitKind;
selection?: TraitSelection;
}
export type TraitModule<TriggerState extends GeneralTriggerState = GeneralTriggerState> = TraitSpec &
@@ -2,6 +2,7 @@ import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
import { LogFormat } from '@sammo-ts/logic/logging/types.js';
@@ -98,6 +99,11 @@ export const traitModule: TraitModule = {
getName: () => '격노',
getInfo: () =>
'[전투] 상대방 필살 시 격노(필살) 발동, 회피 시도시 25% 확률로 격노 발동, 공격 시 일정 확률로 진노(1페이즈 추가), 격노마다 대미지 20% 추가 중첩',
selection: {
requirements: [TraitRequirement.STAT_STRENGTH],
weight: 1,
weightType: TraitWeightType.NORM,
},
getWarPowerMultiplier: (_context, unit, _oppose) => {
const activatedCnt = unit.hasActivatedSkillOnLog('격노');
return [1 + 0.2 * activatedCnt, 1];
@@ -2,6 +2,7 @@ import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
@@ -44,6 +45,11 @@ export const traitModule: TraitModule = {
kind: 'war',
getName: () => '견고',
getInfo: () => '[전투] 상대 필살 확률 -20%p, 상대 계략 시도시 성공 확률 -10%p, 부상 없음, 아군 피해 -10%',
selection: {
requirements: [TraitRequirement.STAT_STRENGTH],
weight: 1,
weightType: TraitWeightType.NORM,
},
onCalcOpposeStat: ((_context, statName, value, _aux) => {
if (statName === 'warMagicSuccessProb' && typeof value === 'number') {
return value - 0.1;
@@ -1,3 +1,4 @@
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
@@ -49,13 +50,18 @@ export const traitModule: TraitModule = {
name: '공성',
info: '[군사] 차병 계통 징·모병비 -10%<br>[전투] 성벽 공격 시 대미지 +100%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 차병 숙련을 가산',
kind: 'war',
selection: {
weight: 1,
weightType: TraitWeightType.NORM,
requirements: [TraitRequirement.STAT_LEADERSHIP | TraitRequirement.REQ_DEXTERITY | TraitRequirement.ARMY_SIEGE],
},
getName: () => '공성',
getInfo: () =>
'[군사] 차병 계통 징·모병비 -10%<br>[전투] 성벽 공격 시 대미지 +100%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 차병 숙련을 가산',
onCalcDomestic: (_context, turnType, varType, value, aux) => {
if (turnType === '징병' || turnType === '모병') {
const armType = getAuxArmType(aux);
if (varType === 'cost' && armType === 4) {
if (varType === 'cost' && armType === 5) {
return value * 0.9;
}
}
@@ -1,3 +1,4 @@
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
@@ -52,6 +53,17 @@ export const traitModule: TraitModule = {
name: '궁병',
info: '[군사] 궁병 계통 징·모병비 -10%<br>[전투] 회피 확률 +20%p,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 궁병 숙련을 가산',
kind: 'war',
selection: {
weight: 1,
weightType: TraitWeightType.NORM,
requirements: [
TraitRequirement.STAT_LEADERSHIP |
TraitRequirement.REQ_DEXTERITY |
TraitRequirement.ARMY_ARCHER |
TraitRequirement.STAT_NOT_INTEL,
TraitRequirement.STAT_STRENGTH | TraitRequirement.REQ_DEXTERITY | TraitRequirement.ARMY_ARCHER,
],
},
getName: () => '궁병',
getInfo: () =>
'[군사] 궁병 계통 징·모병비 -10%<br>[전투] 회피 확률 +20%p,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 궁병 숙련을 가산',
@@ -0,0 +1,79 @@
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
import { getAuxArmType, parseWarDexAux } from './aux.js';
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
function onCalcStat(
context: WarActionContext,
statName: WarStatName,
value: number | [number, number],
aux?: unknown
): number | [number, number];
function onCalcStat(
context: GeneralActionContext | WarActionContext,
statName: GeneralStatName | WarStatName,
value: number | [number, number],
aux?: unknown
): number | [number, number] {
if (statName === 'warMagicSuccessProb') {
return (value as number) + 0.2;
}
if (!('unit' in context) || !context.unit) {
return value;
}
const unit = context.unit;
const wizardType = unit.getGameConfig().armTypes.wizard;
if (wizardType === undefined) {
return value;
}
if (statName.startsWith('dex')) {
const myDex = getMetaNumber(context.general.meta, `dex${wizardType}`);
const { isAttacker, opposeType } = parseWarDexAux(aux);
if (isAttacker && opposeType && statName === `dex${opposeType.armType}`) {
return (value as number) + myDex;
}
if (!isAttacker && statName === `dex${wizardType}`) {
return (value as number) + myDex;
}
}
return value;
}
// 전투 특기: 귀병
export const traitModule: TraitModule = {
key: 'che_귀병',
name: '귀병',
info: '[군사] 귀병 계통 징·모병비 -10%<br>[전투] 계략 성공 확률 +20%p,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 귀병 숙련을 가산',
kind: 'war',
selection: {
weight: 1,
weightType: TraitWeightType.NORM,
requirements: [
TraitRequirement.STAT_INTEL |
TraitRequirement.ARMY_WIZARD |
TraitRequirement.REQ_DEXTERITY |
TraitRequirement.STAT_NOT_STRENGTH,
],
},
getName: () => '귀병',
getInfo: () =>
'[군사] 귀병 계통 징·모병비 -10%<br>[전투] 계략 성공 확률 +20%p,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 귀병 숙련을 가산',
onCalcDomestic: (_context, turnType, varType, value, aux) => {
if (turnType === '징병' || turnType === '모병') {
const armType = getAuxArmType(aux);
if (varType === 'cost' && armType === 4) {
return value * 0.9;
}
}
return value;
},
onCalcStat,
};
@@ -1,3 +1,4 @@
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
@@ -48,6 +49,17 @@ export const traitModule: TraitModule = {
name: '기병',
info: '[군사] 기병 계통 징·모병비 -10%<br>[전투] 수비 시 대미지 +10%, 공격 시 대미지 +20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 기병 숙련을 가산',
kind: 'war',
selection: {
weight: 1,
weightType: TraitWeightType.NORM,
requirements: [
TraitRequirement.STAT_LEADERSHIP |
TraitRequirement.REQ_DEXTERITY |
TraitRequirement.ARMY_CAVALRY |
TraitRequirement.STAT_NOT_INTEL,
TraitRequirement.STAT_STRENGTH | TraitRequirement.REQ_DEXTERITY | TraitRequirement.ARMY_CAVALRY,
],
},
getName: () => '기병',
getInfo: () =>
'[군사] 기병 계통 징·모병비 -10%<br>[전투] 수비 시 대미지 +10%, 공격 시 대미지 +20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 기병 숙련을 가산',
@@ -1,3 +1,4 @@
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
@@ -61,6 +62,11 @@ export const traitModule: TraitModule = {
name: '돌격',
info: '[전투] 공격 시 대등/유리한 병종에게는 퇴각 전까지 전투, 공격 시 페이즈 + 2, 공격 시 대미지 +5%',
kind: 'war',
selection: {
weight: 1,
weightType: TraitWeightType.NORM,
requirements: [TraitRequirement.STAT_STRENGTH],
},
getName: () => '돌격',
getInfo: () => '[전투] 공격 시 대등/유리한 병종에게는 퇴각 전까지 전투, 공격 시 페이즈 + 2, 공격 시 대미지 +5%',
getWarPowerMultiplier: (_context, unit, _oppose) => {
@@ -2,6 +2,7 @@ import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import { getMetaNumber } from '@sammo-ts/logic/war/utils.js';
import { WarUnit } from '@sammo-ts/logic/war/units.js';
@@ -40,6 +41,11 @@ export const traitModule: TraitModule = {
getName: () => '무쌍',
getInfo: () =>
'[전투] 대미지 +5%, 피해 -2%, 공격 시 필살 확률 +10%p, <br>승리 수의 로그 비례로 대미지 상승(10회 ⇒ +5%, 40회 ⇒ +15%)<br>승리 수의 로그 비례로 피해 감소(10회 ⇒ -2%, 40회 ⇒ -6%)',
selection: {
requirements: [TraitRequirement.STAT_STRENGTH],
weight: 1,
weightType: TraitWeightType.NORM,
},
getWarPowerMultiplier: (_context, unit, _oppose) => {
let attackMultiplier = 1.05;
let defenceMultiplier = 0.98;
@@ -2,6 +2,7 @@ import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
import { LogFormat } from '@sammo-ts/logic/logging/types.js';
@@ -99,6 +100,11 @@ export const traitModule: TraitModule = {
getName: () => '반계',
getInfo: () =>
'[전투] 상대의 계략 성공 확률 -10%p, 상대의 계략을 40% 확률로 되돌림, 반목 성공시 대미지 추가(+60% → +150%)',
selection: {
requirements: [TraitRequirement.STAT_INTEL],
weight: 1,
weightType: TraitWeightType.NORM,
},
onCalcOpposeStat: ((_context, statName, value, _aux) => {
if (statName === 'warMagicSuccessProb' && typeof value === 'number') {
return value - 0.1;
@@ -1,3 +1,4 @@
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
@@ -48,6 +49,17 @@ export const traitModule: TraitModule = {
name: '보병',
info: '[군사] 보병 계통 징·모병비 -10%<br>[전투] 공격 시 아군 피해 -10%, 수비 시 아군 피해 -20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 보병 숙련을 가산',
kind: 'war',
selection: {
weight: 1,
weightType: TraitWeightType.NORM,
requirements: [
TraitRequirement.STAT_LEADERSHIP |
TraitRequirement.REQ_DEXTERITY |
TraitRequirement.ARMY_FOOTMAN |
TraitRequirement.STAT_NOT_INTEL,
TraitRequirement.STAT_STRENGTH | TraitRequirement.REQ_DEXTERITY | TraitRequirement.ARMY_FOOTMAN,
],
},
getName: () => '보병',
getInfo: () =>
'[군사] 보병 계통 징·모병비 -10%<br>[전투] 공격 시 아군 피해 -10%, 수비 시 아군 피해 -20%,<br>공격시 상대 병종에/수비시 자신 병종 숙련에 보병 숙련을 가산',
@@ -2,6 +2,7 @@ import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
function onCalcStat(
@@ -32,6 +33,11 @@ export const traitModule: TraitModule = {
kind: 'war',
getName: () => '신산',
getInfo: () => '[계략] 화계·탈취·파괴·선동 : 성공률 +10%p<br>[전투] 계략 시도 확률 +20%p, 계략 성공 확률 +20%p',
selection: {
requirements: [TraitRequirement.STAT_INTEL],
weight: 1,
weightType: TraitWeightType.NORM,
},
onCalcDomestic: (_context, turnType, varType, value, _aux) => {
if (turnType === '계략') {
if (varType === 'success') return value + 0.1;
@@ -2,6 +2,7 @@ import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
function onCalcStat(
@@ -29,5 +30,10 @@ export const traitModule: TraitModule = {
kind: 'war',
getName: () => '신중',
getInfo: () => '[전투] 계략 성공 확률 100%',
selection: {
requirements: [TraitRequirement.STAT_INTEL],
weight: 1,
weightType: TraitWeightType.NORM,
},
onCalcStat,
};
@@ -2,6 +2,7 @@ import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
import { LogFormat } from '@sammo-ts/logic/logging/types.js';
@@ -84,6 +85,11 @@ export const traitModule: TraitModule = {
kind: 'war',
getName: () => '위압',
getInfo: () => '[전투] 첫 페이즈 위압 발동(적 공격, 회피 불가, 사기 5 감소)',
selection: {
requirements: [TraitRequirement.STAT_STRENGTH],
weight: 1,
weightType: TraitWeightType.NORM,
},
getBattlePhaseTriggerList: (_context) => {
if (!_context.unit) return null;
return new WarTriggerCaller(new che_위압시도(_context.unit), new che_위압발동(_context.unit));
@@ -3,6 +3,7 @@ import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import { CheUisulCityHealTrigger } from '@sammo-ts/logic/triggers/generalTriggers/che_도시치료.js';
import { triggerModule as cheUisulTriggerModule } from '@sammo-ts/logic/war/triggers/che_의술.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
// 전투 특기: 의술
export const traitModule: TraitModule = {
@@ -13,6 +14,11 @@ export const traitModule: TraitModule = {
getName: () => '의술',
getInfo: () =>
'[군사] 매 턴마다 자신(100%)과 소속 도시 장수(적 포함 50%) 부상 회복<br>[전투] 페이즈마다 40% 확률로 치료 발동(아군 피해 30% 감소, 부상 회복)',
selection: {
requirements: [TraitRequirement.STAT_LEADERSHIP, TraitRequirement.STAT_STRENGTH, TraitRequirement.STAT_INTEL],
weight: 2,
weightType: TraitWeightType.PERCENT,
},
getPreTurnExecuteTriggerList: (context) => new GeneralTriggerCaller(new CheUisulCityHealTrigger(context.general)),
getBattlePhaseTriggerList: (context: WarActionContext) => {
const unit = context.unit;
@@ -2,6 +2,7 @@ import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
import { LogFormat } from '@sammo-ts/logic/logging/types.js';
@@ -122,6 +123,11 @@ export const traitModule: TraitModule = {
kind: 'war',
getName: () => '저격',
getInfo: () => '[전투] 새로운 상대와 전투 시 50% 확률로 저격 발동, 성공 시 사기+20',
selection: {
requirements: [TraitRequirement.STAT_LEADERSHIP, TraitRequirement.STAT_STRENGTH, TraitRequirement.STAT_INTEL],
weight: 1,
weightType: TraitWeightType.NORM,
},
getBattlePhaseTriggerList: (_context) => {
if (!_context.unit) return null;
return new WarTriggerCaller(new che_저격시도(_context.unit, 0.5, 20, 40, 20), new che_저격발동(_context.unit));
@@ -2,6 +2,7 @@ import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
function onCalcStat(
@@ -29,5 +30,10 @@ export const traitModule: TraitModule = {
kind: 'war',
getName: () => '집중',
getInfo: () => '[전투] 계략 성공 시 대미지 +50%',
selection: {
requirements: [TraitRequirement.STAT_INTEL],
weight: 1,
weightType: TraitWeightType.NORM,
},
onCalcStat,
};
@@ -2,6 +2,7 @@ import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
const RECRUIT_TRAIN = 70;
const CONSCRIPT_TRAIN = 84;
@@ -44,6 +45,11 @@ export const traitModule: TraitModule = {
getName: () => '징병',
getInfo: () =>
'[군사] 징병/모병 시 훈사 70/84 제공<br>[기타] 통솔 순수 능력치 보정 +25%, 징병/모병/소집해제 시 인구 변동 없음',
selection: {
requirements: [TraitRequirement.STAT_LEADERSHIP, TraitRequirement.STAT_STRENGTH, TraitRequirement.STAT_INTEL],
weight: 1,
weightType: TraitWeightType.NORM,
},
onCalcDomestic: (_context, turnType, varType, value) => {
if (turnType === '징병' || turnType === '모병') {
if (varType === 'train' || varType === 'atmos') {
@@ -2,6 +2,7 @@ import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
function onCalcStat(
@@ -26,6 +27,11 @@ export const traitModule: TraitModule = {
kind: 'war',
getName: () => '척사',
getInfo: () => '[전투] 지역·도시 병종 상대로 대미지 +20%, 아군 피해 -20%',
selection: {
requirements: [TraitRequirement.STAT_LEADERSHIP, TraitRequirement.STAT_STRENGTH, TraitRequirement.STAT_INTEL],
weight: 1,
weightType: TraitWeightType.NORM,
},
getWarPowerMultiplier: (_context, _unit, oppose) => {
const opposeCrewType = oppose.getCrewType();
if (opposeCrewType.reqCities() || opposeCrewType.reqRegions()) {
@@ -2,6 +2,7 @@ import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
import { BaseWarUnitTrigger, WarTriggerCaller } from '@sammo-ts/logic/war/triggers.js';
import type { WarUnit } from '@sammo-ts/logic/war/units.js';
@@ -55,6 +56,11 @@ export const traitModule: TraitModule = {
kind: 'war',
getName: () => '필살',
getInfo: () => '[전투] 필살 확률 +30%p, 필살 발동시 대상 회피 불가, 필살 계수 향상',
selection: {
requirements: [TraitRequirement.STAT_LEADERSHIP, TraitRequirement.STAT_STRENGTH, TraitRequirement.STAT_INTEL],
weight: 1,
weightType: TraitWeightType.NORM,
},
getBattlePhaseTriggerList: (_context) => {
if (!_context.unit) return null;
return new WarTriggerCaller(new che_필살강화_회피불가(_context.unit));
@@ -2,6 +2,7 @@ import type { GeneralActionContext } from '@sammo-ts/logic/triggers/general.js';
import type { GeneralStatName, WarStatName } from '@sammo-ts/logic/triggers/types.js';
import type { WarActionContext } from '@sammo-ts/logic/war/actions.js';
import type { TraitModule } from '@sammo-ts/logic/triggers/special/types.js';
import { TraitRequirement, TraitWeightType } from '../requirements.js';
function onCalcStat(context: GeneralActionContext, statName: GeneralStatName, value: number, aux?: unknown): number;
function onCalcStat(
@@ -32,5 +33,10 @@ export const traitModule: TraitModule = {
kind: 'war',
getName: () => '환술',
getInfo: () => '[전투] 계략 성공 확률 +10%p, 계략 성공 시 대미지 +30%',
selection: {
requirements: [TraitRequirement.STAT_INTEL],
weight: 5,
weightType: TraitWeightType.PERCENT,
},
onCalcStat,
};
@@ -1,6 +1,27 @@
import type { TraitModule, TraitModuleExport } from '@sammo-ts/logic/triggers/special/types.js';
export const WAR_TRAIT_KEYS = ['che_의술', 'che_징병', 'che_보병', 'che_궁병', 'che_기병', 'che_공성'] as const;
export const WAR_TRAIT_KEYS = [
'che_귀병',
'che_신산',
'che_환술',
'che_집중',
'che_신중',
'che_반계',
'che_보병',
'che_궁병',
'che_기병',
'che_공성',
'che_돌격',
'che_무쌍',
'che_견고',
'che_위압',
'che_저격',
'che_필살',
'che_징병',
'che_의술',
'che_격노',
'che_척사',
] as const;
export type WarTraitKey = (typeof WAR_TRAIT_KEYS)[number];
@@ -9,12 +30,26 @@ export type WarTraitModule = TraitModule;
export type WarTraitImporter = () => Promise<TraitModuleExport>;
const defaultImporters: Record<WarTraitKey, WarTraitImporter> = {
che_의술: async () => import('./che_의술.js'),
che_징병: async () => import('./che_징병.js'),
che_귀병: async () => import('./che_귀병.js'),
che_신산: async () => import('./che_신산.js'),
che_환술: async () => import('./che_환술.js'),
che_집중: async () => import('./che_집중.js'),
che_신중: async () => import('./che_신중.js'),
che_반계: async () => import('./che_반계.js'),
che_보병: async () => import('./che_보병.js'),
che_궁병: async () => import('./che_궁병.js'),
che_기병: async () => import('./che_기병.js'),
che_공성: async () => import('./che_공성.js'),
che_돌격: async () => import('./che_돌격.js'),
che_무쌍: async () => import('./che_무쌍.js'),
che_견고: async () => import('./che_견고.js'),
che_위압: async () => import('./che_위압.js'),
che_저격: async () => import('./che_저격.js'),
che_필살: async () => import('./che_필살.js'),
che_징병: async () => import('./che_징병.js'),
che_의술: async () => import('./che_의술.js'),
che_격노: async () => import('./che_격노.js'),
che_척사: async () => import('./che_척사.js'),
};
export const isWarTraitKey = (value: string): value is WarTraitKey => WAR_TRAIT_KEYS.includes(value as WarTraitKey);