refactor: 特殊属性定义方式

This commit is contained in:
unanmed 2026-10-09 20:09:51 +08:00
parent 46bcafcfbb
commit 1bcdcc3677
14 changed files with 439 additions and 360 deletions

View File

@ -74,8 +74,8 @@ function createEnemy(
*/
function createSpecial(code: number, value: number): ISpecial<number> {
return new modules.CommonSerializableSpecial<number>(code, value, {
getSpecialName: () => `special-${code}`,
getDescription: () => `description-${code}`,
name: () => `special-${code}`,
desc: () => `description-${code}`,
fromLegacyEnemy: () => value
});
}

View File

@ -78,8 +78,8 @@ function createEnemy(
/** 构造一个可序列化的数值特殊属性 */
function createSpecial(code: number, value: number): ISpecial<number> {
return new modules.CommonSerializableSpecial<number>(code, value, {
getSpecialName: () => `special-${code}`,
getDescription: () => `description-${code}`,
name: () => `special-${code}`,
desc: () => `description-${code}`,
fromLegacyEnemy: () => value
});
}

View File

@ -46,8 +46,8 @@ afterAll(() => {
*/
function makeCommonConfig(): ICommonSpecialConfig<number> {
return {
getSpecialName: special => `连击${special.getValue()}`,
getDescription: special => `怪物每回合攻击${special.getValue()}次。`,
name: special => `连击${special.getValue()}`,
desc: special => `怪物每回合攻击${special.getValue()}次。`,
fromLegacyEnemy: () => 0
};
}
@ -57,8 +57,8 @@ function makeCommonConfig(): ICommonSpecialConfig<number> {
*/
function makeNoneConfig(): ICommonSpecialConfig<void> {
return {
getSpecialName: () => '先攻',
getDescription: () => '怪物首先攻击。',
name: () => '先攻',
desc: () => '怪物首先攻击。',
fromLegacyEnemy: () => undefined
};
}

View File

@ -2,13 +2,13 @@ import { isEqual } from 'lodash-es';
import { SaveCompression } from '@user/data-common';
import { ISpecial, SpecialCreation } from './types';
// TODO: 颜色参数
export interface ICommonSpecialConfig<T> {
/** 获取特殊属性的名称 */
getSpecialName: (special: ISpecial<T>) => string;
name: string | ((special: ISpecial<T>) => string);
/** 获取特殊属性的描述 */
getDescription: (special: ISpecial<T>) => string;
desc: string | ((special: ISpecial<T>) => string);
/** 获取特殊属性的名称显示颜色 */
color: string | ((special: ISpecial<T>) => string);
}
export class CommonSerializableSpecial<T> implements ISpecial<T> {
@ -26,12 +26,30 @@ export class CommonSerializableSpecial<T> implements ISpecial<T> {
return this.value;
}
/**
* 获取配置数据的值,是字面量则直接返回,是函数则返回其调用结果
* @param config 配置源数据
*/
private getConfigValue(
config: string | ((special: ISpecial<T>) => string)
) {
if (typeof config === 'string') {
return config;
} else {
return config(this);
}
}
getSpecialName(): string {
return this.config.getSpecialName(this);
return this.getConfigValue(this.config.name);
}
getDescription(): string {
return this.config.getDescription(this);
return this.getConfigValue(this.config.desc);
}
getNameColor(): string {
return this.getConfigValue(this.config.color);
}
clone(): ISpecial<T> {
@ -72,12 +90,30 @@ export class NonePropertySpecial implements ISpecial<void> {
return void 0;
}
/**
* 获取配置数据的值,是字面量则直接返回,是函数则返回其调用结果
* @param config 配置源数据
*/
private getConfigValue(
config: string | ((special: ISpecial<void>) => string)
) {
if (typeof config === 'string') {
return config;
} else {
return config(this);
}
}
getSpecialName(): string {
return this.config.getSpecialName(this);
return this.getConfigValue(this.config.name);
}
getDescription(): string {
return this.config.getDescription(this);
return this.getConfigValue(this.config.desc);
}
getNameColor(): string {
return this.getConfigValue(this.config.color);
}
clone(): ISpecial<void> {

View File

@ -55,6 +55,13 @@ export interface ISpecial<T = void> extends ISaveableContent<T> {
*/
getDescription(): string;
// TODO: 后续需要改进渲染端的文字渲染,颜色使用内嵌方式写入到字符串里面,而不是单独指定
/**
* 获取此特殊属性的名称颜色
*/
getNameColor(): string;
/**
* 深拷贝此特殊属性
*/

View File

@ -22,7 +22,7 @@ import { CoreState } from '../src/core';
import { MainDamageCalculator } from '../src/enemy/calculator';
import { CommonAuraConverter, GuardAuraConverter } from '../src/enemy/aura';
import { MainEnemyFinalEffect } from '../src/enemy/final';
import { type IHaloValue, type IVampireValue } from '../src/enemy/special';
import { type ISpecial23Value, type IVampireValue } from '../src/enemy/special';
/** 按 08-08 约定的 CoreState 装配创建一个独立状态实例 */
function createCoreState(): CoreState {
@ -203,7 +203,7 @@ function createPipelineSpecials(haloAtkBuff: number): ISpecial<any>[] {
createSpecial<IVampireValue>(11, { vampire: 10, add: true }),
createSpecial<void>(17, undefined),
createSpecial<number>(22, 7),
createSpecial<IHaloValue>(25, {
createSpecial<ISpecial23Value>(25, {
haloRange: 0,
haloSquare: false,
hpBuff: 0,
@ -255,7 +255,7 @@ describe('enemy combination stage 1 - component baselines', () => {
code: 25,
attrs: { atk: 20 },
specials: [
createSpecial<IHaloValue>(25, {
createSpecial<ISpecial23Value>(25, {
haloRange: 0,
haloSquare: false,
hpBuff: 0,
@ -489,7 +489,7 @@ describe('enemy combination stage 2 - multi-special and system pipelines', () =>
id: 'query-target',
code: 25,
specials: [
createSpecial<IHaloValue>(25, {
createSpecial<ISpecial23Value>(25, {
haloRange: 0,
haloSquare: false,
hpBuff: 0,

View File

@ -15,7 +15,10 @@ import {
} from '@user/data-base';
import { type IDamageSystem, type IMapDamage } from '@user/data-system';
import { CoreState } from '../src/core';
import { type IHaloValue, type IZoneValue } from '../src/enemy/special';
import {
type ISpecial23Value,
type ISpecial13Value
} from '../src/enemy/special';
import floorDataset from './fixtures/floors.json';
/** 按 08-08 约定的 CoreState 装配创建一个独立状态实例 */
@ -153,7 +156,7 @@ const MONSTER_PORTFOLIO: readonly IMonsterPrefab[] = [
attrs: { hp: 120, atk: 12, def: 3, money: 5, exp: 7, point: 1 },
specials: [
// haloRange 为 0 时 25 会落到 FullRange,即同楼层全体光环
createSpecial<IHaloValue>(25, {
createSpecial<ISpecial23Value>(25, {
haloRange: 0,
haloSquare: false,
hpBuff: 0,
@ -168,7 +171,7 @@ const MONSTER_PORTFOLIO: readonly IMonsterPrefab[] = [
attrs: { hp: 90, atk: 9, def: 2, money: 4, exp: 6, point: 1 },
specials: [
// 十字范围光环,让光环拓扑随怪物坐标变化而非全图连通
createSpecial<IHaloValue>(25, {
createSpecial<ISpecial23Value>(25, {
haloRange: 3,
haloSquare: false,
hpBuff: 20,
@ -183,7 +186,7 @@ const MONSTER_PORTFOLIO: readonly IMonsterPrefab[] = [
attrs: { hp: 150, atk: 15, def: 5, money: 8, exp: 12, point: 2 },
specials: [
// 九宫格范围光环,与十字范围共用同一转换器但走不同 range 实现
createSpecial<IHaloValue>(25, {
createSpecial<ISpecial23Value>(25, {
haloRange: 2,
haloSquare: true,
hpBuff: 0,
@ -207,7 +210,7 @@ const MONSTER_PORTFOLIO: readonly IMonsterPrefab[] = [
code: 104,
attrs: { hp: 60, atk: 6, def: 1, money: 2, exp: 3, point: 1 },
specials: [
createSpecial<IZoneValue>(15, {
createSpecial<ISpecial13Value>(15, {
zone: 5,
zoneSquare: false,
range: 2
@ -219,7 +222,7 @@ const MONSTER_PORTFOLIO: readonly IMonsterPrefab[] = [
code: 105,
attrs: { hp: 70, atk: 7, def: 2, money: 3, exp: 4, point: 1 },
specials: [
createSpecial<IZoneValue>(15, {
createSpecial<ISpecial13Value>(15, {
zone: 8,
zoneSquare: true,
range: 1

View File

@ -54,10 +54,10 @@ import {
MainEnemyFinalEffect,
MainMapDamageConverter,
MainMapDamageReducer,
registerSpecials,
createSpecials,
MainEnemyComparer
} from './enemy';
import { HERO_DEFAULT_ATTRIBUTE, TILE_HEIGHT, TILE_WIDTH } from './shared';
import { HERO_DEFAULT_ATTRIBUTE } from './shared';
import { DefaultHeroMoveTopImpl, DefaultPassPredicateImpl } from './hero';
import { createEventRegistrations } from './event';
import {
@ -109,120 +109,145 @@ export class CoreState implements ICoreState {
> = new Map();
constructor(config: Readonly<ICoreStateConfig>) {
//#region L0 初始化
//#region L0 定义
// 朝向
this.roleFace = new RoleFaceBinder();
this.faceManager = new FaceManager();
this.tileStore = new TileStore();
this.itemStore = new ItemStore<IHeroAttr>(this.tileStore);
this.mapStore = new MapStore();
this.eventStore = new GameEventStore();
this.initializeL0();
//#endregion
//#region L1 定义
this.flags = new FlagSystem();
this.maps = new MapState(this);
const dir8 = this.faceManager.get(FaceGroup.Dir8)!;
const heroAttribute = new HeroAttribute(HERO_DEFAULT_ATTRIBUTE);
this.hero = new HeroState(this, dir8, heroAttribute);
this.enemyManager = new EnemyManager<IEnemyAttr>(this.tileStore);
this.initializeL1();
//#endregion
//#region L2 定义
this.enemyContext = new EnemyContext<IEnemyAttr, IHeroAttr>(this);
const events = createEventRegistrations();
this.eventSystem = new GameEventSystem(this, events);
this.replaySystem = new ReplaySystem();
this.pathfinding = new PathfindingSystem(this);
this.initializeL2();
//#endregion
//#region L3 定义
this.loadManager = new LoadManager();
this.loader = new MotaDataLoader(this.loadManager, config.loadStarter);
this.initializeL3(config);
//#endregion
}
//#region 初始化方法
/**
* 初始化数据端 L0
*/
private initializeL0() {
const dir4 = new Dir4FaceHandler();
const dir8 = new Dir8FaceHandler();
this.faceManager.register(FaceGroup.Dir4, dir4);
this.faceManager.registerById('dir4', dir4);
this.faceManager.register(FaceGroup.Dir8, dir8);
this.faceManager.registerById('dir8', dir8);
}
// 图块
this.tileStore = new TileStore();
// 道具
this.itemStore = new ItemStore<IHeroAttr>(this.tileStore);
// 地图
this.mapStore = new MapStore();
// 游戏事件
this.eventStore = new GameEventStore();
//#endregion
//#region L1 初始化
// Flag 系统
this.flags = new FlagSystem();
// 地图
this.maps = new MapState(this);
// 勇士
const heroAttribute = new HeroAttribute(HERO_DEFAULT_ATTRIBUTE);
const heroState = new HeroState(this, dir8, heroAttribute);
this.hero = heroState;
// 怪物管理器
/**
* 初始化数据端 L1
*/
private initializeL1() {
const comparer = new MainEnemyComparer();
const enemyManager = new EnemyManager<IEnemyAttr>(this.tileStore);
enemyManager.attachEnemyComparer(comparer);
registerSpecials(enemyManager);
this.enemyManager = enemyManager;
this.enemyManager.attachEnemyComparer(comparer);
//#endregion
const specials = createSpecials(this);
for (const [code, cons] of specials) {
this.enemyManager.registerSpecial(code, cons);
}
}
//#region L2 初始化
// 怪物上下文
const enemyContext = new EnemyContext<IEnemyAttr, IHeroAttr>(this);
const damageSystem = new DamageSystem(enemyContext);
const mapDamage = new MapDamage(enemyContext);
damageSystem.useCalculator(new MainDamageCalculator());
mapDamage.useReducer(new MainMapDamageReducer());
mapDamage.useConverter(new MainMapDamageConverter());
enemyContext.attachDamageSystem(damageSystem);
enemyContext.attachMapDamage(mapDamage);
enemyContext.registerAuraConverter(new CommonAuraConverter());
enemyContext.registerAuraConverter(new GuardAuraConverter());
enemyContext.registerFinalEffect(new MainEnemyFinalEffect());
enemyContext.resize(TILE_WIDTH, TILE_HEIGHT);
enemyContext.bindHero(heroAttribute);
this.enemyContext = enemyContext;
// 游戏事件系统
const eventSystem = new GameEventSystem(
this,
createEventRegistrations()
);
this.eventSystem = eventSystem;
// 录像系统
this.replaySystem = new ReplaySystem();
// 寻路系统
this.pathfinding = new PathfindingSystem(this);
this.pathfinding.useMover(this.hero.location.mover);
// 初始状态下勇士不在任何楼层,切换楼层后再具体设置
this.pathfinding.finder.useMapLayer(null);
/**
* 初始化数据端 L2
*/
private initializeL2() {
const predicate = new DefaultPassPredicateImpl(this);
this.pathfinding.finder.usePassPredicate(predicate);
// 初始状态下勇士不在任何楼层,切换楼层后再具体设置
this.pathfinding.finder.useMapLayer(null);
this.pathfinding.useMover(this.hero.location.mover);
//#endregion
this.initializeEnemy();
}
//#region L3 初始化
private initializeL3(config: Readonly<ICoreStateConfig>) {
// 勇士顶层初始化
const heroMoveTopImpl = new DefaultHeroMoveTopImpl(this);
this.hero.location.mover.useTopImplementation(heroMoveTopImpl);
// 加载
this.loadManager = new LoadManager();
this.loader = new MotaDataLoader(this.loadManager, config.loadStarter);
this.loader.addHook(new DefaultDataLoaderHook());
this.loader.addCoreConfig('core', config.coreURL);
// 存档内容
this.initializeSave();
this.initializeReplay();
}
/**
* 初始化怪物信息
*/
private initializeEnemy() {
// 初始化怪物上下文
const ctx = this.enemyContext;
const damageSystem = new DamageSystem(ctx);
const mapDamage = new MapDamage(ctx);
damageSystem.useCalculator(new MainDamageCalculator());
mapDamage.useReducer(new MainMapDamageReducer());
mapDamage.useConverter(new MainMapDamageConverter());
ctx.attachDamageSystem(damageSystem);
ctx.attachMapDamage(mapDamage);
ctx.bindHero(this.hero.getAttribute());
// 注册光环转换器
ctx.registerAuraConverter(new CommonAuraConverter());
ctx.registerAuraConverter(new GuardAuraConverter());
// 注册怪物最终效果
ctx.registerFinalEffect(new MainEnemyFinalEffect());
}
/**
* 初始化存档内容
*/
private initializeSave() {
this.addSaveableContent('@system/hero', this.hero);
this.addSaveableContent('@system/flags', this.flags);
this.addSaveableContent('@system/maps', this.maps);
this.addSaveableContent('@system/enemy', this.enemyManager);
this.addSaveableContent('@system/replay', this.replaySystem);
// 勇士顶层初始化
const heroMoveTopImpl = new DefaultHeroMoveTopImpl(this);
this.hero.location.mover.useTopImplementation(heroMoveTopImpl);
// 录像系统初始化注册
this.registerReplayCommands();
//#endregion
}
//#region 私有方法
/**
* 注册全部录像指令
*/
private registerReplayCommands() {
private initializeReplay() {
const replay = this.replaySystem;
const up = new ReplayMove(this, FaceDirection.Up);

View File

@ -13,7 +13,7 @@ import {
type IReadonlyEnemy,
type ISpecial
} from '@user/data-base';
import { type IHaloValue } from '../special';
import { type ISpecial23Value } from '../special';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
@ -180,7 +180,7 @@ function createWritableHandler(
* 构造一个完整的光环数值
* @param overrides 覆盖的光环字段
*/
function createHalo(overrides: Partial<IHaloValue> = {}): IHaloValue {
function createHalo(overrides: Partial<ISpecial23Value> = {}): ISpecial23Value {
return {
haloRange: 0,
haloSquare: false,

View File

@ -13,7 +13,7 @@ import {
type IReadonlyHeroAttribute,
type ISpecial
} from '@user/data-base';
import { type IZoneValue } from '../special';
import { type ISpecial13Value } from '../special';
const testGlobals = vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
@ -215,7 +215,7 @@ function createInfo(
describe('ZoneDamageView', () => {
// 验证十字领域使用曼哈顿范围参数并输出领域伤害
it('builds a manhattan range and zone damage for a cross zone', () => {
const special = createSpecial<IZoneValue>(15, {
const special = createSpecial<ISpecial13Value>(15, {
zone: 7,
zoneSquare: false,
range: 1
@ -235,7 +235,7 @@ describe('ZoneDamageView', () => {
// 验证九宫格领域使用矩形范围参数并输出领域伤害
it('builds a rect range and zone damage for a square zone', () => {
const special = createSpecial<IZoneValue>(15, {
const special = createSpecial<ISpecial13Value>(15, {
zone: 4,
zoneSquare: true,
range: 2
@ -255,7 +255,7 @@ describe('ZoneDamageView', () => {
// 验证领域范围判定命中范围内的坐标并忽略范围外坐标
it('checks coordinates against the zone range', () => {
const special = createSpecial<IZoneValue>(15, {
const special = createSpecial<ISpecial13Value>(15, {
zone: 7,
zoneSquare: false,
range: 1

View File

@ -1,7 +1,7 @@
// 测试怪物特殊属性注册:代码 0-27 全量注册、守卫默认值、名称与描述生成
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { type ISpecial } from '@user/data-base';
import { type IHaloValue } from '../special';
import { type ISpecial23Value } from '../special';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
@ -30,7 +30,7 @@ vi.hoisted(() => {
});
interface TestModules {
registerSpecials: typeof import('../special').registerSpecials;
registerSpecials: typeof import('../special').createSpecials;
}
let modules: TestModules;
@ -39,7 +39,7 @@ beforeAll(async () => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
const specialModule = await import('../special');
modules = { registerSpecials: specialModule.registerSpecials };
modules = { registerSpecials: specialModule.createSpecials };
});
type SpecialCreation = (enemy: unknown) => ISpecial<any>;
@ -146,7 +146,7 @@ describe('registerSpecials', () => {
expect(halo.getDescription()).toContain('同楼层所有怪物');
expect(halo.getDescription()).toContain('线性叠加。');
const value: IHaloValue = {
const value: ISpecial23Value = {
haloRange: 2,
haloSquare: true,
hpBuff: 10,

View File

@ -17,7 +17,7 @@ import {
IReadonlyEnemyHandler
} from '@user/data-system';
import { IReadonlyEnemy, ISpecial } from '@user/data-base';
import { IHaloValue } from './special';
import { ISpecial23Value } from './special';
import { IHeroAttr, IEnemyAttr } from '@user/data-common';
const FULL_RANGE = new FullRange();
@ -35,7 +35,7 @@ export class CommonAuraConverter implements IAuraConverter<
}
convert(
special: ISpecial<IHaloValue>,
special: ISpecial<ISpecial23Value>,
handler: IReadonlyEnemyHandler<IEnemyAttr, IHeroAttr>
): CommonAura {
return new CommonAura(handler.enemy, special, handler.locator);
@ -45,7 +45,7 @@ export class CommonAuraConverter implements IAuraConverter<
export class CommonAura implements IEnemyAuraView<
IEnemyAttr,
IRectRangeParam | IManhattanRangeParam | void,
IHaloValue
ISpecial23Value
> {
readonly priority: number = 25;
readonly couldApplyBase: boolean = true;
@ -54,7 +54,7 @@ export class CommonAura implements IEnemyAuraView<
constructor(
readonly enemy: IReadonlyEnemy<IEnemyAttr>,
readonly special: ISpecial<IHaloValue>,
readonly special: ISpecial<ISpecial23Value>,
readonly locator: ITileLocator
) {
this.range = this.createRange();

View File

@ -24,7 +24,7 @@ import {
IReadonlyHeroAttribute,
IReadonlyEnemy
} from '@user/data-base';
import { IZoneValue } from './special';
import { ISpecial13Value } from './special';
import { MapDamageType } from './types';
import { IHeroAttr, IEnemyAttr } from '@user/data-common';
import { FaceGroup } from '@user/data-common';
@ -87,7 +87,7 @@ export class ZoneDamageView extends BaseMapDamageView<
constructor(
context: IEnemyContext<IEnemyAttr, IHeroAttr>,
private readonly locator: Readonly<ITileLocator>,
private readonly special: Readonly<ISpecial<IZoneValue>>
private readonly special: Readonly<ISpecial<ISpecial13Value>>
) {
super(context);
}
@ -296,7 +296,7 @@ export class MainMapDamageConverter implements IMapDamageConverter<
const views: IMapDamageView<any>[] = [];
const { enemy, locator, hero } = handler;
const zone = enemy.getSpecial<IZoneValue>(15);
const zone = enemy.getSpecial<ISpecial13Value>(15);
if (zone) {
views.push(new ZoneDamageView(context, locator, zone));
}

View File

@ -1,43 +1,63 @@
import {
defineCommonSerializableSpecial,
defineNonePropertySpecial,
IEnemyManager
SpecialCreation
} from '@user/data-base';
import { IEnemyAttr } from '@user/data-common';
import { IStateSystem } from '@user/data-system';
// 重要!!!下面这一行的注释不要删
//#region 复合属性值类型
export interface IVampireValue {
// 对象参数名称必须命名为 ISpecial{code}Value,接口前必须添加如下格式的注释,否则不会被编辑器识别
// /** {code} - {name} */
/** 9 - 吸血 */
export interface ISpecial9Value {
/** 吸血比例 */
vampire: number;
/** 是否加到自身生命值上 */
add: boolean;
}
export interface IZoneValue {
/** 13 - 领域 */
export interface ISpecial13Value {
/** 领域伤害值 */
zone: number;
/** 矩形领域(true)还是十字形领域(false),当范围大于 1 时,十字型领域采用曼哈顿距离计算 */
zoneSquare: boolean;
/** 领域范围值 */
range: number;
}
export interface IDegradationValue {
/** 21 - 退化 */
export interface ISpecial21Value {
/** 攻击下降值 */
atkValue: number;
/** 防御下降值 */
defValue: number;
}
export interface IHaloValue {
export interface ISpecial23Value {
/** 光环范围值 */
haloRange: number;
/** 光环是矩形范围(true)还是十字型范围(false),当范围大于 1 时,十字型范围采用曼哈顿距离计算 */
haloSquare: boolean;
/** 生命值增加比例 */
hpBuff: number;
/** 攻击力增加比例 */
atkBuff: number;
/** 防御力增加比例 */
defBuff: number;
}
// 重要!!!下面这一行的注释不要删
//#endregion
// TODO: 注释需要更新
/**
* 注册所有怪物特殊属性到 enemyManager
*
*
* 属性实现位置一览('./'表示当前文件夹 '../'表示上一级文件夹):
* 1. 调参类属性 | 仅影响战斗过程的属性:./damage.ts calDamageWithTurn 函数
* 2. 地图伤害:./damage.ts DamageEnemy.calMapDamage 方法
@ -47,318 +67,306 @@ export interface IHaloValue {
* 6. 衰弱效果:../state/hero.ts getHeroStatusOf 方法
* 7. 重生属性:还在脚本编辑的 changingFloor
*/
export function registerSpecials(manager: IEnemyManager<IEnemyAttr>): void {
manager.setAttributeDefaults('guard', new Set());
export function createSpecials(
state: IStateSystem
): ReadonlyMap<number, SpecialCreation<any, IEnemyAttr>> {
const hero = state.hero;
const map = new Map<number, SpecialCreation<any, IEnemyAttr>>();
// 注意,不需要参数的特殊属性,如先攻、魔攻、坚固这些纯机制,
// 不需要一个数值来描述的特殊属性,使用 defineNonePropertySpecial 定义。
// 需要参数的特殊属性,如连击、领域、破甲这些需要一个或多个数值来描述的特殊属性,
// 使用 defineCommonSerializableSpecial 定义。
// 特殊属性的参数可以填写数值、字符串,也可以填写对象,参考吸血、领域、退化、光环的写法。
// 注意,每个属性定义前必须添加注释 // {code} - {name},这会作为编辑器识别的匹配模式。
// 重要!!!下面这一行的注释不要删
//#region 属性定义
// 0 - 空
manager.registerSpecial(
map.set(
0,
defineNonePropertySpecial(0, {
getSpecialName: () => '空',
getDescription: () => '空',
fromLegacyEnemy: () => {}
name: '空',
desc: '空',
color: '#FFF'
})
);
// 1 - 先攻
manager.registerSpecial(
map.set(
1,
defineNonePropertySpecial(1, {
getSpecialName: () => '先攻',
getDescription: () => '怪物首先攻击。',
fromLegacyEnemy: () => {}
name: '先攻',
desc: '怪物首先攻击。',
color: '#FC3'
})
);
// 2 - 魔攻
manager.registerSpecial(
map.set(
2,
defineNonePropertySpecial(2, {
getSpecialName: () => '魔攻',
getDescription: () => '怪物攻击无视勇士的防御。',
fromLegacyEnemy: () => {}
name: '魔攻',
desc: '怪物攻击无视勇士的防御。',
color: '#BBB0FF'
})
);
// 3 - 坚固
manager.registerSpecial(
map.set(
3,
defineNonePropertySpecial(3, {
getSpecialName: () => '坚固',
getDescription: () => '怪物防御不小于勇士攻击-1。',
fromLegacyEnemy: () => {}
name: '坚固',
desc: '怪物防御不小于勇士攻击-1。',
color: '#C0B088'
})
);
// 4 - 2连击
manager.registerSpecial(
// 4 - n连击
map.set(
4,
defineNonePropertySpecial(4, {
getSpecialName: () => '2连击',
getDescription: () => '怪物每回合攻击2次。',
fromLegacyEnemy: () => {}
defineCommonSerializableSpecial(4, 2, {
name: special => `${special.value}连击`,
desc: special => `怪物每回合攻击${special.value}次。`,
color: '#FE7'
})
);
// 5 - 3连击
manager.registerSpecial(
// 5 - 破甲
map.set(
5,
defineNonePropertySpecial(5, {
getSpecialName: () => '3连击',
getDescription: () => '怪物每回合攻击3次。',
fromLegacyEnemy: () => {}
defineCommonSerializableSpecial(5, 0, {
name: '破甲',
desc: special =>
`战斗前,附加角色防御的${special.value}%作为伤害。`,
color: '#88C0FF'
})
);
// 6 - n连击
manager.registerSpecial(
// 6 - 反击
map.set(
6,
defineCommonSerializableSpecial(6, 4, {
getSpecialName: special => `${special.value}连击`,
getDescription: special => `怪物每回合攻击${special.value}次。`,
fromLegacyEnemy: enemy => enemy.n ?? 4
defineCommonSerializableSpecial(6, 0, {
name: '反击',
desc: special =>
`战斗时,怪物每回合附加角色攻击的${special.value}%作为伤害,无视角色防御。`,
color: '#FA4'
})
);
// 7 - 破甲
manager.registerSpecial(
// 7 - 净化
map.set(
7,
defineCommonSerializableSpecial(7, 0, {
getSpecialName: () => '破甲',
getDescription: special =>
`战斗前,附加角色防御的${special.value}%作为伤害。`,
fromLegacyEnemy: enemy => enemy.breakArmor ?? 0
})
);
// 8 - 反击
manager.registerSpecial(
8,
defineCommonSerializableSpecial(8, 0, {
getSpecialName: () => '反击',
getDescription: special =>
`战斗时,怪物每回合附加角色攻击的${special.value}%作为伤害,无视角色防御。`,
fromLegacyEnemy: enemy => enemy.counterAttack ?? 0
})
);
// 9 - 净化
manager.registerSpecial(
9,
defineCommonSerializableSpecial(9, 0, {
getSpecialName: () => '净化',
getDescription: special =>
name: '净化',
desc: special =>
`战斗前,怪物附加角色护盾的${special.value}倍作为伤害。`,
fromLegacyEnemy: enemy => enemy.purify ?? 0
color: '#80EED6'
})
);
// 10 - 模仿
manager.registerSpecial(
10,
defineNonePropertySpecial(10, {
getSpecialName: () => '模仿',
getDescription: () => '怪物的攻防与勇士相同。',
fromLegacyEnemy: () => {}
// 8 - 模仿
map.set(
8,
defineNonePropertySpecial(8, {
name: '模仿',
desc: '怪物的攻防与勇士相同。',
color: '#B0C0DD'
})
);
// 11 - 吸血
manager.registerSpecial(
11,
defineCommonSerializableSpecial<IVampireValue>(
11,
// 9 - 吸血
map.set(
9,
defineCommonSerializableSpecial<ISpecial9Value>(
9,
{ vampire: 0, add: false },
{
getSpecialName: () => '吸血',
getDescription: special => {
name: '吸血',
desc: special => {
const { vampire, add } = special.value;
const attr = hero.getAttribute();
return (
`战斗前,怪物首先吸取角色的${vampire}%生命` +
// @ts-expect-error 需要重构
`(约${Math.floor((vampire / 100) * getHeroStatusOn('hp'))}点)作为伤害` +
`(约${Math.floor((vampire / 100) * attr.getFinalAttribute('hp'))}点)作为伤害` +
(add ? `,并把伤害数值加到自身生命上。` : `。`)
);
},
fromLegacyEnemy: enemy => ({
vampire: enemy.vampire ?? 0,
add: enemy.add ?? false
})
color: '#DD4448'
}
)
);
// 12 - 中毒
manager.registerSpecial(
12,
defineNonePropertySpecial(12, {
getSpecialName: () => '中毒',
getDescription: () =>
`战斗后,角色陷入中毒状态,每一步损失生命${core.values.poisonDamage}点。`,
fromLegacyEnemy: () => {}
// 10 - 中毒
map.set(
10,
defineCommonSerializableSpecial(10, 0, {
name: '中毒',
desc: special =>
`战斗后,角色陷入中毒状态,每一步损失生命${special.value}点,多次触发时,${state.flags.getFieldValueDefaults('poisonAdd', false) ? '与已有中毒值叠加' : '取最高值'}。`,
color: '#9E8'
})
);
// 13 - 衰弱
manager.registerSpecial(
13,
defineNonePropertySpecial(13, {
getSpecialName: () => '衰弱',
getDescription: () => {
const weak = core.values.weakValue;
// 11 - 衰弱
map.set(
11,
defineCommonSerializableSpecial(11, 0, {
name: '衰弱',
desc: special => {
const weak = special.value;
const suffix = ',多次触发时取最高值。';
if (weak < 1) {
return `战斗后,角色陷入衰弱状态,攻防暂时下降${Math.floor(weak * 100)}%`;
return (
`战斗后,角色陷入衰弱状态,攻防暂时下降${Math.floor(weak * 100)}%` +
suffix
);
} else {
return `战斗后,角色陷入衰弱状态,攻防暂时下降${weak}点`;
return (
`战斗后,角色陷入衰弱状态,攻防暂时下降${weak}点` +
suffix
);
}
},
fromLegacyEnemy: () => {}
color: '#F0BBCC'
})
);
// 14 - 诅咒
manager.registerSpecial(
14,
defineNonePropertySpecial(14, {
getSpecialName: () => '诅咒',
getDescription: () =>
'战斗后,角色陷入诅咒状态,战斗无法获得金币和经验。',
fromLegacyEnemy: () => {}
// 12 - 诅咒
map.set(
12,
defineNonePropertySpecial(12, {
name: '诅咒',
desc: '战斗后,角色陷入诅咒状态,战斗无法获得金币和经验。',
color: '#BBEEF0'
})
);
// 15 - 领域
manager.registerSpecial(
15,
defineCommonSerializableSpecial<IZoneValue>(
15,
// 13 - 领域
map.set(
13,
defineCommonSerializableSpecial<ISpecial13Value>(
13,
{ zone: 0, zoneSquare: false, range: 1 },
{
getSpecialName: () => '领域',
getDescription: special => {
name: '领域',
desc: special => {
const { zone, zoneSquare, range } = special.value;
return `经过怪物周围${zoneSquare ? '九宫格' : '十字'}范围内${range}格时自动减生命${zone}点。`;
},
fromLegacyEnemy: enemy => ({
zone: enemy.zone ?? 0,
zoneSquare: enemy.zoneSquare ?? false,
range: enemy.range ?? 1
})
color: '#C677DD'
}
)
);
// 16 - 夹击
manager.registerSpecial(
16,
defineNonePropertySpecial(16, {
getSpecialName: () => '夹击',
getDescription: () =>
'经过两只相同的怪物中间,角色生命值变成一半。',
fromLegacyEnemy: () => {}
// 14 - 夹击
map.set(
14,
defineNonePropertySpecial(14, {
name: '夹击',
desc: '经过两只相同的怪物中间,角色生命值变成一半。',
color: '#B9E'
})
);
// 17 - 仇恨
manager.registerSpecial(
// 15 - 仇恨
map.set(
15,
defineNonePropertySpecial(15, {
name: '仇恨',
desc: `战斗前,怪物附加之前积累的仇恨值作为伤害;战斗后,释放一半的仇恨值。(每杀死一个怪物获得${state.flags.getFieldValueDefaults('hatred', 1)}点仇恨值)。`,
color: '#B0B666'
})
);
// 16 - 阻击
map.set(
16,
defineCommonSerializableSpecial(16, 0, {
name: '阻击',
desc: special =>
`经过怪物十字范围内时怪物后退一格,同时对勇士造成${special.value}点伤害。`,
color: '#8888E6'
})
);
// 17 - 自爆
map.set(
17,
defineNonePropertySpecial(17, {
getSpecialName: () => '仇恨',
getDescription: () =>
`战斗前,怪物附加之前积累的仇恨值作为伤害;战斗后,释放一半的仇恨值。(每杀死一个怪物获得${core.values.hatred}点仇恨值)。`,
fromLegacyEnemy: () => {}
name: '自爆',
desc: '战斗后角色的生命值变成1。',
color: '#F66'
})
);
// 18 - 阻击
manager.registerSpecial(
// 18 - 无敌
map.set(
18,
defineCommonSerializableSpecial(18, 0, {
getSpecialName: () => '阻击',
getDescription: special =>
`经过怪物十字范围内时怪物后退一格,同时对勇士造成${special.value}点伤害。`,
fromLegacyEnemy: enemy => enemy.repulse ?? 0
defineNonePropertySpecial(18, {
name: '无敌',
desc: '角色无法打败怪物,除非拥有十字架。',
color: '#AAA'
})
);
// 19 - 自爆
manager.registerSpecial(
// 19 - 退化
map.set(
19,
defineNonePropertySpecial(19, {
getSpecialName: () => '自爆',
getDescription: () => '战斗后角色的生命值变成1。',
fromLegacyEnemy: () => {}
})
);
// 20 - 无敌
manager.registerSpecial(
20,
defineNonePropertySpecial(20, {
getSpecialName: () => '无敌',
getDescription: () => '角色无法打败怪物,除非拥有十字架。',
fromLegacyEnemy: () => {}
})
);
// 21 - 退化
manager.registerSpecial(
21,
defineCommonSerializableSpecial<IDegradationValue>(
21,
defineCommonSerializableSpecial<ISpecial21Value>(
19,
{ atkValue: 0, defValue: 0 },
{
getSpecialName: () => '退化',
getDescription: special => {
name: '退化',
desc: special => {
const { atkValue, defValue } = special.value;
return `战斗后角色永久下降${atkValue}点攻击和${defValue}点防御。`;
},
fromLegacyEnemy: enemy => ({
atkValue: enemy.atkValue ?? 0,
defValue: enemy.defValue ?? 0
})
color: '#FFF'
}
)
);
// 22 - 固伤
manager.registerSpecial(
// 20 - 固伤
map.set(
20,
defineCommonSerializableSpecial(20, 0, {
name: '固伤',
desc: special =>
`战斗前,怪物对角色造成${special.value}点固定伤害,未开启负伤时无视角色护盾。`,
color: '#F97'
})
);
// 21 - 重生
map.set(
21,
defineNonePropertySpecial(21, {
name: '重生',
desc: '怪物被击败后,角色转换楼层则怪物将再次出现。',
color: '#A0E0FF'
})
);
// 22 - 激光
map.set(
22,
defineCommonSerializableSpecial(22, 0, {
getSpecialName: () => '固伤',
getDescription: special =>
`战斗前,怪物对角色造成${special.value}点固定伤害,未开启负伤时无视角色护盾。`,
fromLegacyEnemy: enemy => enemy.damage ?? 0
})
);
// 23 - 重生
manager.registerSpecial(
23,
defineNonePropertySpecial(23, {
getSpecialName: () => '重生',
getDescription: () =>
'怪物被击败后,角色转换楼层则怪物将再次出现。',
fromLegacyEnemy: () => {}
})
);
// 24 - 激光
manager.registerSpecial(
24,
defineCommonSerializableSpecial(24, 0, {
getSpecialName: () => '激光',
getDescription: special =>
name: '激光',
desc: special =>
`经过怪物同行或同列时自动减生命${special.value}点。`,
fromLegacyEnemy: enemy => enemy.laser ?? 0
color: '#D0A0DD'
})
);
// 25 - 光环
manager.registerSpecial(
25,
defineCommonSerializableSpecial<IHaloValue>(
25,
// 23 - 光环
map.set(
23,
defineCommonSerializableSpecial<ISpecial23Value>(
23,
{
haloRange: 0,
haloSquare: false,
@ -367,8 +375,8 @@ export function registerSpecials(manager: IEnemyManager<IEnemyAttr>): void {
defBuff: 0
},
{
getSpecialName: () => '光环',
getDescription: special => {
name: '光环',
desc: special => {
const { haloRange, haloSquare, hpBuff, atkBuff, defBuff } =
special.value;
let str = '';
@ -394,35 +402,35 @@ export function registerSpecials(manager: IEnemyManager<IEnemyAttr>): void {
str += `,线性叠加。`;
return str;
},
fromLegacyEnemy: enemy => ({
haloRange: enemy.haloRange ?? 0,
haloSquare: enemy.haloSquare ?? false,
hpBuff: enemy.hpBuff ?? 0,
atkBuff: enemy.atkBuff ?? 0,
defBuff: enemy.defBuff ?? 0
})
color: '#E6E099'
}
)
);
// 26 - 支援
manager.registerSpecial(
26,
defineNonePropertySpecial(26, {
getSpecialName: () => '支援',
getDescription: () =>
'当周围一圈的怪物受到攻击时将上前支援,并组成小队战斗。',
fromLegacyEnemy: () => {}
// 24 - 支援
map.set(
24,
defineNonePropertySpecial(24, {
name: '支援',
desc: '当周围一圈的怪物受到攻击时将上前支援,并组成小队战斗。',
color: '#77C0B6'
})
);
// 27 - 捕捉
manager.registerSpecial(
27,
// 25 - 捕捉
map.set(
25,
defineNonePropertySpecial(27, {
getSpecialName: () => '捕捉',
getDescription: () => '当走到怪物十字范围内时会进行强制战斗。',
fromLegacyEnemy: () => {}
name: '捕捉',
desc: '当走到怪物十字范围内时会进行强制战斗。',
color: '#C0DDBB'
})
);
// TODO: 26 - 特殊光环
// 重要!!!下面这一行的注释不要删
//#endregion
return map;
}