Compare commits
4 Commits
e194132731
...
8d61c67c1d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d61c67c1d | ||
|
|
cede98eab9 | ||
|
|
f03db06c3c | ||
|
|
3893ec33d7 |
@@ -46,6 +46,7 @@ import { Tooltip } from "../skill/Tooltip";
|
||||
import { CardInitCoins } from "../common/config/CardSet";
|
||||
import { Timer } from "db://oops-framework/core/common/timer/Timer";
|
||||
import { FieldSkillType } from "../common/config/SkillSet";
|
||||
import { spawningEngine } from "./RogueConfig";
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
/** 任务(关卡)生命周期阶段 */
|
||||
@@ -118,8 +119,8 @@ export class MissionComp extends CCComp {
|
||||
|
||||
// ======================== 运行时状态 ========================
|
||||
|
||||
/** 战斗倒计时(秒) */
|
||||
FightTime:number = FightSet.FiIGHT_TIME
|
||||
/** 战斗已耗时(秒),正向计时 */
|
||||
clearTime:number = 0
|
||||
/** 剩余复活次数 */
|
||||
revive_times: number = 1;
|
||||
/** 掉落奖励列表 */
|
||||
@@ -234,14 +235,7 @@ export class MissionComp extends CCComp {
|
||||
this.syncMonsterSpawnState(dt)
|
||||
if(smc.mission.stop_mon_action) return
|
||||
smc.vmdata.mission_data.fight_time+=dt
|
||||
if (!this.isBossWave) {
|
||||
this.FightTime-=dt
|
||||
if (this.FightTime <= 0) {
|
||||
// 时间到了,自动结束战斗进入准备阶段
|
||||
this.FightTime = FightSet.FiIGHT_TIME;
|
||||
oops.message.dispatchEvent("TimeUpAdvanceWave");
|
||||
}
|
||||
}
|
||||
this.clearTime += dt
|
||||
this.update_time();
|
||||
}
|
||||
}
|
||||
@@ -250,29 +244,11 @@ export class MissionComp extends CCComp {
|
||||
|
||||
/** 更新时间/波数显示(仅在秒数变化时更新以减少 Label 操作) */
|
||||
update_time(){
|
||||
if (this.isBossWave) {
|
||||
const str = "∞";
|
||||
if (str != this.lastTimeStr) {
|
||||
if (this.time_node && this.time_node.isValid) {
|
||||
const timeChild = this.time_node.getChildByName("time");
|
||||
if (timeChild) {
|
||||
const label = timeChild.getComponent(Label);
|
||||
if (label) label.string = str;
|
||||
}
|
||||
}
|
||||
this.lastTimeStr = str;
|
||||
this.lastTimeSecond = -1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const time = Math.max(0, this.FightTime);
|
||||
const remainSecond = Math.floor(time);
|
||||
const remainSecond = Math.floor(this.clearTime);
|
||||
if (remainSecond === this.lastTimeSecond) return;
|
||||
this.lastTimeSecond = remainSecond;
|
||||
let m = Math.floor(remainSecond / 60);
|
||||
let s = remainSecond % 60;
|
||||
const wave = Math.max(1, this.currentWave || smc.vmdata.mission_data.level || 1);
|
||||
let str = `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
||||
if(str != this.lastTimeStr){
|
||||
if (this.time_node && this.time_node.isValid) {
|
||||
@@ -422,7 +398,7 @@ export class MissionComp extends CCComp {
|
||||
const label = phaseNode.getComponent(Label);
|
||||
if (label) {
|
||||
const wave = Math.max(1, this.currentWave || (smc.vmdata && smc.vmdata.mission_data ? smc.vmdata.mission_data.level : 1) || 1);
|
||||
label.string = `第 ${wave}/20 波`;
|
||||
label.string = `第 ${wave}/30 波`;
|
||||
}
|
||||
|
||||
// 阶段切换动感表现:只在进入战斗阶段跳动一下,让流程充满心流体验
|
||||
@@ -497,8 +473,8 @@ export class MissionComp extends CCComp {
|
||||
smc.vmdata.scores.wave_all_alive_count++;
|
||||
}
|
||||
|
||||
// 【评分系统 - 战绩分】判断是否通过最后一关(第20回合)
|
||||
if (this.currentWave === 20) {
|
||||
// 【评分系统 - 战绩分】判断是否通过最后一关(第30回合)
|
||||
if (this.currentWave === 30) {
|
||||
smc.vmdata.scores.passed_wave_20 = true;
|
||||
}
|
||||
}
|
||||
@@ -721,13 +697,13 @@ export class MissionComp extends CCComp {
|
||||
smc.mission.stop_spawn_mon = false;
|
||||
smc.vmdata.mission_data.in_fight=false
|
||||
smc.vmdata.mission_data.fight_time=0
|
||||
this.clearTime = 0
|
||||
smc.vmdata.mission_data.mon_num=0
|
||||
smc.vmdata.mission_data.level = 1
|
||||
smc.vmdata.mission_data.mon_max = Math.max(1, Math.floor(this.maxMonsterCount))
|
||||
this.currentPhase = MissionPhase.None;
|
||||
this.currentWave = 1;
|
||||
this.isBossWave = false;
|
||||
this.FightTime=FightSet.FiIGHT_TIME
|
||||
this.rewards=[]
|
||||
this.revive_times = 1;
|
||||
this.lastTimeStr = "";
|
||||
@@ -744,6 +720,8 @@ export class MissionComp extends CCComp {
|
||||
this.monsterCountSyncTimer = 0;
|
||||
this.lastPrepareCoinWave = 0;
|
||||
|
||||
spawningEngine.reset();
|
||||
|
||||
// 重置所有的战局得分数据,防止上一局的数据污染
|
||||
smc.resetScores();
|
||||
|
||||
@@ -786,7 +764,7 @@ export class MissionComp extends CCComp {
|
||||
smc.vmdata.mission_data.level = wave;
|
||||
this.grantPrepareCoinByWave(wave);
|
||||
this.lastTimeSecond = -1;
|
||||
this.FightTime = FightSet.FiIGHT_TIME;
|
||||
this.clearTime = 0;
|
||||
this.update_time();
|
||||
|
||||
// 检查并推送卡池升级事件
|
||||
@@ -877,8 +855,17 @@ export class MissionComp extends CCComp {
|
||||
|
||||
// 怪物全灭检测:如果战斗阶段场上没有任何活着的怪物,直接结束战斗进入下一波的准备阶段
|
||||
if (monsterCount === 0 && smc.mission.play && !smc.mission.pause && this.currentPhase === MissionPhase.Battle) {
|
||||
this.FightTime = FightSet.FiIGHT_TIME;
|
||||
oops.message.dispatchEvent("TimeUpAdvanceWave");
|
||||
let heroesAliveRatio = heroCount / 6.0; // 假设最大 6 个站位,或者直接基于存活数算比例
|
||||
// 如果能获取当前已部署英雄数最好,这里简化处理,大于 4 个就算高存活
|
||||
heroesAliveRatio = Math.min(1.0, heroCount / 4.0);
|
||||
spawningEngine.updateAdaptive(heroesAliveRatio, this.clearTime);
|
||||
|
||||
if (this.currentWave >= 30) {
|
||||
// 30 波通关
|
||||
this.open_Victory(null, false);
|
||||
} else {
|
||||
oops.message.dispatchEvent("TimeUpAdvanceWave");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ import { HeroInfo, HType } from "../common/config/heroSet";
|
||||
import { smc } from "../common/SingletonModuleComp";
|
||||
import { GameEvent } from "../common/config/GameEvent";
|
||||
import {BoxSet, FacSet } from "../common/config/GameSet";
|
||||
import { MonList, MonType, SpawnPowerBias, StageBossGrow, StageGrow, UpType, WaveSlotConfig, DefaultWaveSlot, IWaveSlot } from "./RogueConfig";
|
||||
import { spawningEngine, GeneratedMonster, AffixType, MonType, MonList } from "./RogueConfig";
|
||||
import { HeroAttrsComp } from "../hero/HeroAttrsComp";
|
||||
import { MonMoveComp } from "../hero/MonMoveComp";
|
||||
const { ccclass, property } = _decorator;
|
||||
@@ -94,6 +94,10 @@ export class MissionMonCompComp extends CCComp {
|
||||
private waveTargetCount: number = 0;
|
||||
/** 当前波已生成的怪物数量 */
|
||||
private waveSpawnedCount: number = 0;
|
||||
/** 等待生成的怪物队列(由新肉鸽引擎提供) */
|
||||
private pendingMonsters: GeneratedMonster[] = [];
|
||||
/** 增量刷怪计时器 */
|
||||
private spawnTimer: number = 0;
|
||||
|
||||
// ======================== 生命周期 ========================
|
||||
|
||||
@@ -108,14 +112,35 @@ export class MissionMonCompComp extends CCComp {
|
||||
* 帧更新:
|
||||
* 1. 检查游戏是否运行中。
|
||||
* 2. 处理插队刷怪队列。
|
||||
* 3. 逐步从 pendingMonsters 队列中生成怪物(受 stop_spawn_mon 限制)。
|
||||
*/
|
||||
protected update(dt: number): void {
|
||||
if(!smc.mission.play) return
|
||||
if(smc.mission.pause) return
|
||||
if(smc.mission.stop_mon_action) return;
|
||||
if(!smc.mission.in_fight) return;
|
||||
if(smc.mission.stop_spawn_mon) return;
|
||||
|
||||
this.updateSpecialQueue(dt);
|
||||
|
||||
if(smc.mission.stop_spawn_mon) return;
|
||||
|
||||
// 逐步刷怪逻辑
|
||||
if (this.pendingMonsters.length > 0) {
|
||||
this.spawnTimer += dt;
|
||||
// 控制刷怪速率:例如每 0.2 秒刷 1-2 只
|
||||
if (this.spawnTimer > 0.2) {
|
||||
this.spawnTimer = 0;
|
||||
// 一次出 2 只,加快进度
|
||||
for (let i = 0; i < 2; i++) {
|
||||
if (this.pendingMonsters.length === 0) break;
|
||||
const monData = this.pendingMonsters.shift()!;
|
||||
const lane = this.pickBalancedLane();
|
||||
this.addMonsterAt(lane, this.laneIndices[lane], monData);
|
||||
this.laneIndices[lane]++;
|
||||
this.waveSpawnedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 事件处理 ========================
|
||||
@@ -152,15 +177,16 @@ export class MissionMonCompComp extends CCComp {
|
||||
this.waveTargetCount = 0
|
||||
this.waveSpawnedCount = 0
|
||||
this.MonQueue = []
|
||||
this.pendingMonsters = []
|
||||
this.spawnTimer = 0
|
||||
this.laneIndices = [0, 0, 0];
|
||||
|
||||
let hasBoss = false;
|
||||
const config = WaveSlotConfig[this.currentWave] || DefaultWaveSlot;
|
||||
for (const slot of config) {
|
||||
if (slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss || slot.type === MonType.FlyBoss) {
|
||||
hasBoss = true;
|
||||
}
|
||||
}
|
||||
// 预生成第一波数据以获取数量和 Boss 信息
|
||||
const monsters = spawningEngine.generateWave(this.currentWave);
|
||||
this.pendingMonsters = monsters;
|
||||
this.waveTargetCount = monsters.length;
|
||||
let hasBoss = monsters.some(m => m.isBoss);
|
||||
|
||||
oops.message.dispatchEvent(GameEvent.NewWave, {
|
||||
wave: this.currentWave,
|
||||
total: this.waveTargetCount,
|
||||
@@ -199,13 +225,22 @@ export class MissionMonCompComp extends CCComp {
|
||||
this.queueTimer = 0;
|
||||
|
||||
const isBoss = MonList[MonType.MeleeBoss].includes(item.uuid) ||
|
||||
MonList[MonType.LongBoss].includes(item.uuid) ||
|
||||
(MonList[MonType.FlyBoss] && MonList[MonType.FlyBoss].includes(item.uuid));
|
||||
MonList[MonType.LongBoss].includes(item.uuid);
|
||||
|
||||
const upType = this.getRandomUpType();
|
||||
const lane = item.flyLane !== undefined && item.flyLane >= 0 && item.flyLane <= 2 ? item.flyLane : this.pickBalancedLane();
|
||||
|
||||
this.addMonsterAt(lane, this.laneIndices[lane], item.uuid, isBoss, upType, Math.max(1, Number(item.level ?? 1)));
|
||||
// 构造一个模拟的 GeneratedMonster 数据传递给 addMonsterAt
|
||||
const base = HeroInfo[item.uuid];
|
||||
const monData: GeneratedMonster = {
|
||||
uuid: item.uuid,
|
||||
type: MonType.Melee, // 简化的兜底,真实逻辑依赖 heroSet 配置
|
||||
hp: base ? base.hp : 100,
|
||||
ap: base ? base.ap : 10,
|
||||
affixes: [],
|
||||
isBoss: isBoss,
|
||||
spawnIndex: 0
|
||||
};
|
||||
this.addMonsterAt(lane, this.laneIndices[lane], monData, item.level);
|
||||
this.laneIndices[lane]++;
|
||||
}
|
||||
|
||||
@@ -214,25 +249,21 @@ export class MissionMonCompComp extends CCComp {
|
||||
/**
|
||||
* 开始下一波:
|
||||
* 1. 波数 +1 并更新全局数据。
|
||||
* 2. 重置槽位并根据配置生成本波所有怪物。
|
||||
* 3. 分发 NewWave 事件。
|
||||
* 2. 分发 NewWave 事件(实际的生成在 resetSlotSpawnData 中触发)。
|
||||
*/
|
||||
private onTimeUpAdvanceWave() {
|
||||
this.currentWave += 1;
|
||||
smc.vmdata.mission_data.level = this.currentWave;
|
||||
|
||||
// 检查本波是否有 Boss
|
||||
let hasBoss = false;
|
||||
const config = WaveSlotConfig[this.currentWave] || DefaultWaveSlot;
|
||||
for (const slot of config) {
|
||||
if (slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss || slot.type === MonType.FlyBoss) {
|
||||
hasBoss = true;
|
||||
}
|
||||
}
|
||||
// 预生成新一波数据以获取数量和 Boss 信息
|
||||
const monsters = spawningEngine.generateWave(this.currentWave);
|
||||
this.pendingMonsters = monsters;
|
||||
this.waveTargetCount = monsters.length;
|
||||
let hasBoss = monsters.some(m => m.isBoss);
|
||||
|
||||
oops.message.dispatchEvent(GameEvent.NewWave, {
|
||||
wave: this.currentWave,
|
||||
total: this.waveTargetCount, // 此时还是上一波的怪物数量,但可以不传或后续修正
|
||||
total: this.waveTargetCount,
|
||||
bossWave: hasBoss,
|
||||
});
|
||||
}
|
||||
@@ -241,60 +272,12 @@ export class MissionMonCompComp extends CCComp {
|
||||
this.resetSlotSpawnData(this.currentWave);
|
||||
}
|
||||
|
||||
/** 获取当前阶段(stage = wave - 1,用于属性成长计算) */
|
||||
private getCurrentStage(): number {
|
||||
return Math.max(0, this.currentWave - 1);
|
||||
}
|
||||
|
||||
// ======================== 随机选取 ========================
|
||||
|
||||
/** 随机选取一种成长类型 */
|
||||
private getRandomUpType(): UpType {
|
||||
const keys = Object.keys(StageGrow).map(v => Number(v) as UpType);
|
||||
const index = Math.floor(Math.random() * keys.length);
|
||||
return keys[index] ?? UpType.AP1_HP1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据怪物类型从对应池中随机选取 UUID。
|
||||
* @param monType 怪物类型(MonType 枚举值)
|
||||
* @returns 怪物 UUID
|
||||
*/
|
||||
private getRandomUuidByType(monType: number): number {
|
||||
const pool = (MonList as any)[monType] || MonList[MonType.Melee];
|
||||
if (!pool || pool.length === 0) return 6001;
|
||||
const index = Math.floor(Math.random() * pool.length);
|
||||
return pool[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算怪物属性成长值对。
|
||||
* Boss 在普通成长基础上叠加 StageBossGrow。
|
||||
*
|
||||
* @param upType 成长类型
|
||||
* @param isBoss 是否为 Boss
|
||||
* @returns [AP 成长值, HP 成长值]
|
||||
*/
|
||||
private resolveGrowPair(upType: UpType, isBoss: boolean): [number, number] {
|
||||
const grow = StageGrow[upType] || StageGrow[UpType.AP1_HP1];
|
||||
if (!isBoss) return [grow[0], grow[1]];
|
||||
const bossGrow = StageBossGrow[upType] || StageBossGrow[UpType.AP1_HP1];
|
||||
return [grow[0] + bossGrow[0], grow[1] + bossGrow[1]];
|
||||
}
|
||||
|
||||
/** 获取全局刷怪强度系数 */
|
||||
private getSpawnPowerBias(): number {
|
||||
return SpawnPowerBias;
|
||||
}
|
||||
|
||||
// ======================== 槽位管理 ========================
|
||||
|
||||
/**
|
||||
* 重新分配并生成本波所有怪物:
|
||||
* 重新分配本波所有怪物状态:
|
||||
* 1. 清理上一波残留怪物。
|
||||
* 2. 读取波次配置。
|
||||
* 3. 依据配置和 flyLane 属性,为每只怪物分配自增索引。
|
||||
* 4. 立即实例化所有怪物。
|
||||
* 2. pendingMonsters 已在 onTimeUpAdvanceWave / fight_ready 中准备好,只需重置 laneIndices 即可。
|
||||
*
|
||||
* @param wave 当前波数
|
||||
*/
|
||||
@@ -307,43 +290,9 @@ export class MissionMonCompComp extends CCComp {
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 读取波次配置
|
||||
const config: IWaveSlot[] = WaveSlotConfig[wave] || DefaultWaveSlot;
|
||||
|
||||
// 3. 重置排号索引
|
||||
// 2. 重置排号索引
|
||||
this.laneIndices = [0, 0, 0];
|
||||
|
||||
let allMons: any[] = [];
|
||||
|
||||
// 解析配置
|
||||
for (const slot of config) {
|
||||
const isBoss = slot.type === MonType.MeleeBoss || slot.type === MonType.LongBoss || slot.type === MonType.FlyBoss;
|
||||
|
||||
for (let i = 0; i < slot.count; i++) {
|
||||
const uuid = this.getRandomUuidByType(slot.type);
|
||||
const upType = this.getRandomUpType();
|
||||
// 优先使用配置的 lane,否则均衡分配
|
||||
let lane = slot.flyLane !== undefined ? slot.flyLane : this.pickBalancedLane();
|
||||
lane = Math.max(0, Math.min(2, lane));
|
||||
|
||||
const req = { uuid, isBoss, upType, monLv: wave, lane };
|
||||
allMons.push(req);
|
||||
// 提前累加 laneIndices,以便本波内的均衡分配能正确计算
|
||||
this.laneIndices[lane]++;
|
||||
}
|
||||
}
|
||||
|
||||
this.waveTargetCount = allMons.length;
|
||||
this.waveSpawnedCount = 0;
|
||||
|
||||
// 由于上面循环中已经累加了 laneIndices,这里需要重置,以便下面真正生成时再累加(或者直接利用 allMons 生成)
|
||||
this.laneIndices = [0, 0, 0];
|
||||
|
||||
// 4. 立即生成本波所有怪物
|
||||
for (const req of allMons) {
|
||||
this.addMonsterAt(req.lane, this.laneIndices[req.lane], req.uuid, req.isBoss, req.upType, req.monLv);
|
||||
this.laneIndices[req.lane]++;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 怪物生成 ========================
|
||||
@@ -353,17 +302,13 @@ export class MissionMonCompComp extends CCComp {
|
||||
*
|
||||
* @param laneIndex 三路索引 (0 上, 1 中, 2 下)
|
||||
* @param monIndex 该路级的第几个怪 (0, 1, 2...)
|
||||
* @param uuid 怪物 UUID
|
||||
* @param isBoss 是否为 Boss
|
||||
* @param upType 属性成长类型
|
||||
* @param monLv 怪物等级
|
||||
* @param monData 新引擎生成的怪物数据 (含 uuid, hp, ap, affixes 等)
|
||||
* @param monLv 怪物等级 (仅对旧有的 level 参数做兼容,实际属性由 monData 决定)
|
||||
*/
|
||||
private addMonsterAt(
|
||||
laneIndex: number,
|
||||
monIndex: number,
|
||||
uuid: number = 1001,
|
||||
isBoss: boolean = false,
|
||||
upType: UpType = UpType.AP1_HP1,
|
||||
monData: GeneratedMonster,
|
||||
monLv: number = 1
|
||||
) {
|
||||
let mon = ecs.getEntity<Monster>(Monster);
|
||||
@@ -371,11 +316,11 @@ export class MissionMonCompComp extends CCComp {
|
||||
|
||||
// 计算坐标
|
||||
const spawnX = MissionMonCompComp.MON_SPAWN_START_X + monIndex * MissionMonCompComp.MON_SPAWN_GAP_X;
|
||||
const landingY = BoxSet.GAME_LINE + MissionMonCompComp.LANE_Y_OFFSETS[laneIndex] + (isBoss ? 6 : 0);
|
||||
const landingY = BoxSet.GAME_LINE + MissionMonCompComp.LANE_Y_OFFSETS[laneIndex] + (monData.isBoss ? 6 : 0);
|
||||
const spawnPos: Vec3 = v3(spawnX, landingY + MissionMonCompComp.MON_DROP_HEIGHT, 0);
|
||||
this.globalSpawnOrder = (this.globalSpawnOrder + 1) % 999;
|
||||
|
||||
mon.load(spawnPos, scale, uuid, isBoss, landingY, monLv, laneIndex);
|
||||
mon.load(spawnPos, scale, monData.uuid, monData.isBoss, landingY, monLv, laneIndex);
|
||||
|
||||
// 设置渲染排序
|
||||
const move = mon.get(MonMoveComp);
|
||||
@@ -383,16 +328,15 @@ export class MissionMonCompComp extends CCComp {
|
||||
move.spawnOrder = this.globalSpawnOrder;
|
||||
}
|
||||
|
||||
// 计算最终属性
|
||||
// 应用新引擎计算好的最终属性和词缀
|
||||
const model = mon.get(HeroAttrsComp);
|
||||
const base = HeroInfo[uuid];
|
||||
if (!model || !base) return;
|
||||
const stage = this.getCurrentStage();
|
||||
const grow = this.resolveGrowPair(upType, isBoss);
|
||||
const bias = Math.max(0.1, this.getSpawnPowerBias());
|
||||
model.ap = Math.max(1, Math.floor((base.ap + stage * grow[0]) * bias));
|
||||
model.hp_max = Math.max(1, Math.floor((base.hp + stage * grow[1]) * bias));
|
||||
if (!model) return;
|
||||
model.ap = monData.ap;
|
||||
model.hp_max = monData.hp;
|
||||
model.hp = model.hp_max;
|
||||
|
||||
// 将词缀记录到属性组件上,供战斗层使用
|
||||
(model as any).affixes = monData.affixes || [];
|
||||
}
|
||||
|
||||
/** ECS 组件移除时触发(当前不销毁节点) */
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
9
assets/script/game/map/gdd.meta
Normal file
9
assets/script/game/map/gdd.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "f7d12276-41a4-4460-83a1-6053a86a7c0d",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
assets/script/game/map/gdd/reviews.meta
Normal file
9
assets/script/game/map/gdd/reviews.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "0431b4de-106c-454e-b811-40c5a5dfa45e",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# Review Log: rogue-spawning.md
|
||||
|
||||
## Review — 2026-05-15 — Verdict: MAJOR REVISION NEEDED
|
||||
Scope signal: XL
|
||||
Specialists: game-designer, systems-designer, qa-lead, level-designer, creative-director
|
||||
Blocking items: 10 | Recommended: 9 | Nice-to-have: 7
|
||||
Summary: 首次评审发现 10 个阻塞项,核心问题包括自适应因子双重叠加(已修复为仅作用于属性)、玩家幻想与自动战斗架构矛盾(已重写为阵容搭配)、蓝图模板池为空(已添加 15 个示例)、填充算法未定义(已添加伪代码)、词缀概率超标(已 clamp)、怪物引入时间线过于后置(已重构 Assassin→T3/Summoner→T5/Splitter→T6)。所有阻塞项已在本次会话中修复。
|
||||
Prior verdict resolved: First review
|
||||
|
||||
### Blocking Items Resolved
|
||||
1. AF 双重叠加 → F2 移除 AF,仅 F3 保留
|
||||
2. 玩家幻想矛盾 → 重写为阵容搭配幻想
|
||||
3. 填充算法未定义 → 添加规则 5a 伪代码
|
||||
4. 蓝图模板为空 → 添加规则 5b 共 15 个示例模板
|
||||
5. 词缀概率 >100% → 添加 min(, 1.0) clamp
|
||||
6. Summoner 互斥 → 互斥组增加 Summoner 词缀×Summoner 类型
|
||||
7. BOSS 槽位不强制 → 添加 mandatory_slots 规则
|
||||
8. 不可测试 AC → AC4/5/10/11 重写为统计可验证
|
||||
9. 缺失 AC → 添加 15 条新 AC
|
||||
10. 怪物时间线 → Assassin→T3, Summoner→T5, Splitter→T6
|
||||
|
||||
### Outstanding Recommended Items (未在本次修复)
|
||||
- ELITE 模板 0.8× 悖论性更简单
|
||||
- Bomber 爆炸随 AP 词缀缩放
|
||||
- REST 波密度 33% 偏高
|
||||
- W12→W15 MajorBoss 跳跃缺乏铺垫
|
||||
- W16-W21 六波无 Boss
|
||||
- 后期预算产生 30-50+ 怪物
|
||||
- 英雄战力曲线未验证
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"importer": "text",
|
||||
"imported": true,
|
||||
"uuid": "e1cc9fc8-bffd-4459-bf43-43e1a1077211",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
551
assets/script/game/map/gdd/rogue-spawning.md
Normal file
551
assets/script/game/map/gdd/rogue-spawning.md
Normal file
@@ -0,0 +1,551 @@
|
||||
# Rogue Spawning System (肉鸽刷怪系统)
|
||||
|
||||
> **Status**: Designed (pending review)
|
||||
> **Author**: walkpan + agents
|
||||
> **Last Updated**: 2026-05-15
|
||||
> **Implements Pillar**: Roguelike replayability & strategic depth
|
||||
|
||||
## Overview
|
||||
|
||||
肉鸽刷怪系统是控制每局游戏怪物生成的核心引擎。它从固定 30 波硬编码配置演进为**蓝图模板 + 权重填充 + 自适应微调**的三层程序化生成架构。主线 30 波分为 10 个难度阶梯(每档 3 波),每档内遵循"恢复→攀升→高潮"的心流节奏。10 种怪物类型配合 8 种词缀修饰,使同一基础怪物产生多种变体。通关后玩家可选进入无限模式,以分层推进方式无限挑战。自适应难度引擎追踪玩家表现,在 ±15% 范围内微调强度,确保始终处于心流通道内。
|
||||
|
||||
## Player Fantasy
|
||||
|
||||
本系统是玩家**间接体验**的基础设施——玩家不直接操作刷怪系统,而是通过英雄阵容选择和升级策略来应对它创造的变化。英雄根据类型和站位自动攻击,玩家的核心操控权在于**战前构建**而非**战中目标选择**。
|
||||
|
||||
**核心幻想:每次开局都是全新的挑战**
|
||||
|
||||
- "我的阵容能应对任何组合"——每波的怪物构成和词缀变体都不同,玩家的阵容选择、站位配置和升级路线决定了能否通关
|
||||
- "不确定性带来的是刺激而非焦虑"——自适应难度确保不会出现过难或过易的无聊区间
|
||||
- "通关后的无限挑战才是真正的考验"——无限模式是高手的舞台,每次突破极限都有成就感
|
||||
- 心流节奏让玩家在紧张和放松之间交替,避免疲劳
|
||||
|
||||
**锚定时刻**:波间准备阶段,看到下一波怪物预览的瞬间,快速判断威胁构成(有自爆怪吗?有辅助治疗吗?有刺客突后排吗?),据此调整英雄站位、技能升级优先级和装备选择。决策发生在**构建层面**而非战斗层面。
|
||||
|
||||
## Detailed Design
|
||||
|
||||
### Core Rules
|
||||
|
||||
**规则 1:三层生成架构**
|
||||
|
||||
每一波的怪物配置由三层依次决定:
|
||||
|
||||
1. **蓝图模板层**:从模板池中抽取一个蓝图,确定该波的类型(REST/NORMAL/MIXED/ELITE/BOSS)和怪物槽位骨架
|
||||
2. **权重填充层**:根据蓝图中的槽位定义,从怪物池中按权重随机抽取具体怪物,填充数量
|
||||
3. **自适应微调层**:根据玩家历史表现,对难度预算进行 ±15% 浮动调整
|
||||
|
||||
**规则 2:10 阶梯 × 3 波结构**
|
||||
|
||||
主线 30 波分为 10 个阶梯(Tier),每个 Tier 3 波:
|
||||
|
||||
| Tier | 波次 | 主题 | 新引入元素 | Boss |
|
||||
|------|------|------|-----------|------|
|
||||
| 1 | W1-3 | 入门教程 | Melee | - |
|
||||
| 2 | W4-6 | 远程登场 | Long, Heavy | W6 MiniBoss |
|
||||
| 3 | W7-9 | 刺客登场 | Assassin(突后排威胁) | - |
|
||||
| 4 | W10-12 | 辅助登场 | Support + 近远程刺协同 | W12 MiniBoss |
|
||||
| 5 | W13-15 | 中期 Boss | Bomber, Summoner + 词缀系统首次出现 | **W15 MajorBoss** |
|
||||
| 6 | W16-18 | 分裂时代 | Splitter(死亡触发型) | - |
|
||||
| 7 | W19-21 | 精英时代 | 词缀大量出现(全种类) | W21 MiniBoss |
|
||||
| 8 | W22-24 | 全兵种混合 | 所有类型协同 | - |
|
||||
| 9 | W25-27 | 终极压力 | 极限数量 | W27 MiniBoss |
|
||||
| 10 | W28-30 | 最终 Boss | 全词缀组合 | **W30 FinalBoss** |
|
||||
|
||||
**规则 3:档内三拍节奏**
|
||||
|
||||
每个 Tier 内 3 波遵循固定节奏模式:
|
||||
|
||||
- **Wave 1(恢复)**:从 REST 类模板池抽取,低难度预算,少量弱怪
|
||||
- **Wave 2(攀升)**:从 NORMAL/MIXED/ELITE 类模板池抽取,中等难度预算
|
||||
- **Wave 3(高潮)**:Boss 档位从 BOSS 类模板抽取;非 Boss 档位从 MIXED 类模板抽取
|
||||
|
||||
例外:Tier 1 Wave 1 为教程波,固定使用特殊教程模板(1 个 Melee 怪)。
|
||||
|
||||
**规则 4:难度预算制**
|
||||
|
||||
每波有一个难度预算值,每只怪物"花费"一定预算:
|
||||
|
||||
```
|
||||
wave_budget = base_budget[tier] × template_modifier × adaptive_factor
|
||||
```
|
||||
|
||||
怪物成本:
|
||||
|
||||
| 类型 | 预算成本 |
|
||||
|------|---------|
|
||||
| Melee | 30 |
|
||||
| Heavy | 50 |
|
||||
| Long | 40 |
|
||||
| Support | 50 |
|
||||
| Bomber | 35 |
|
||||
| Summoner | 60 |
|
||||
| Assassin | 45 |
|
||||
| Splitter | 55 |
|
||||
| Boss (Melee/Long) | 200 |
|
||||
|
||||
词缀额外成本:
|
||||
|
||||
| 词缀 | 额外成本 |
|
||||
|------|---------|
|
||||
| Elite | +20 |
|
||||
| Berserk | +15 |
|
||||
| Shield | +25 |
|
||||
| Regen | +20 |
|
||||
| Swift | +10 |
|
||||
| Giant | +30 |
|
||||
| Chain | +20 |
|
||||
| Summoner | +25 |
|
||||
|
||||
**规则 5:蓝图模板池**
|
||||
|
||||
模板按类型分类,每个模板定义:
|
||||
- `id`:唯一标识
|
||||
- `type`:REST / NORMAL / MIXED / ELITE / BOSS
|
||||
- `tier_min`:首次可出现的最低档位
|
||||
- `slots`:怪物槽位列表(类型池、数量范围、权重)
|
||||
- `allow_affix`:是否允许词缀修饰
|
||||
- `mandatory_slots`:必须填充的槽位(BOSS 类型必须包含至少 1 个 Boss 类型槽位)
|
||||
|
||||
**Boss 槽位强制规则**:BOSS 类型模板的 `mandatory_slots` 必须包含至少 1 个 Boss 类型怪物(MeleeBoss 或 LongBoss)。填充算法必须优先放置强制槽位,确保 Boss 波次始终包含 Boss 怪物。
|
||||
|
||||
**规则 5a:权重填充算法**
|
||||
|
||||
```
|
||||
function fill_wave(template, budget, tier, adaptive_factor):
|
||||
monsters = []
|
||||
|
||||
// Step 1: 放置强制槽位(Boss 等)
|
||||
for slot in template.mandatory_slots:
|
||||
monster_type = weighted_random(slot.type_pool, slot.weights)
|
||||
monster_cost = base_cost[monster_type] + max_affix_cost(tier, slot)
|
||||
if monster_cost <= budget:
|
||||
monsters.append(create_monster(monster_type, tier))
|
||||
budget -= monster_cost
|
||||
|
||||
// Step 2: 填充可选槽位,从高权重到低权重
|
||||
remaining_slots = shuffle_by_weight(template.slots - mandatory_slots)
|
||||
for slot in remaining_slots:
|
||||
monster_type = weighted_random(slot.type_pool, slot.weights)
|
||||
monster_cost = base_cost[monster_type] + estimated_affix_cost(tier)
|
||||
while budget >= monster_cost and count(monster_type) < slot.max_count:
|
||||
monsters.append(create_monster(monster_type, tier))
|
||||
budget -= monster_cost
|
||||
|
||||
// Step 3: 预算不足时保底
|
||||
if len(monsters) == 0:
|
||||
monsters.append(create_monster(Melee, tier)) // 忽略预算
|
||||
|
||||
// Step 4: 预算利用率检查
|
||||
utilization = total_cost(monsters) / original_budget
|
||||
if utilization < 0.5: // 利用了不到一半预算,尝试补充
|
||||
fill_remaining(budget, monsters, tier)
|
||||
|
||||
return monsters
|
||||
```
|
||||
|
||||
填充策略说明:
|
||||
- 强制槽位优先放置,不参与预算竞争
|
||||
- 可选槽位按权重随机,每个槽位尽量填满 max_count
|
||||
- 预算利用率目标 ≥ 70%(不足时触发补充)
|
||||
- 保底规则:即使预算耗尽也至少生成 1 个 Melee
|
||||
|
||||
**规则 5b:示例模板**
|
||||
|
||||
每种类型至少定义 3 个模板以验证预算系统可行性:
|
||||
|
||||
**REST 类型(tier_min=1):**
|
||||
| ID | 槽位配置 | 说明 |
|
||||
|----|---------|------|
|
||||
| REST_01 | 1-2 × Melee(权重10) | 纯近战休息波 |
|
||||
| REST_02 | 1 × Melee(8) + 1 × Long(4) | 轻混合休息波 |
|
||||
| REST_03 | 2 × 随机(权重均分) | 随机轻波 |
|
||||
|
||||
**NORMAL 类型(tier_min=1):**
|
||||
| ID | 槽位配置 | 说明 |
|
||||
|----|---------|------|
|
||||
| NORM_01 | 3-4 × Melee(10) | 近战群 |
|
||||
| NORM_02 | 2 × Melee(6) + 2 × Long(6) | 标准混合 |
|
||||
| NORM_03 | 2 × Melee(4) + 1 × Long(6) + 1 × Heavy(4) | 重型混合 |
|
||||
|
||||
**MIXED 类型(tier_min=3):**
|
||||
| ID | 槽位配置 | 说明 |
|
||||
|----|---------|------|
|
||||
| MIX_01 | 2 × Melee(4) + 2 × Long(4) + 1 × Assassin(4) | 刺客突袭 |
|
||||
| MIX_02 | 1 × Heavy(6) + 2 × Long(6) + 1 × Support(3) | 远程压制 |
|
||||
| MIX_03 | 2 × Melee(4) + 1 × Long(4) + 1 × Assassin(4) + 1 × Bomber(3) | 多线威胁 |
|
||||
|
||||
**ELITE 类型(tier_min=5, allow_affix=true):**
|
||||
| ID | 槽位配置 | 说明 |
|
||||
|----|---------|------|
|
||||
| ELIT_01 | 2-3 × 随机(权重均分, 必带词缀) | 精英小队 |
|
||||
| ELIT_02 | 1 × Heavy(必带词缀) + 1 × Support(必带词缀) | 重装精英 |
|
||||
| ELIT_03 | 2 × Assassin(必带词缀) + 1 × Long(必带词缀) | 精锐突袭 |
|
||||
|
||||
**BOSS 类型(tier_min=2, mandatory_slots 包含 Boss):**
|
||||
| ID | 槽位配置 | 说明 |
|
||||
|----|---------|------|
|
||||
| BOSS_01 | 1 × MeleeBoss(强制) + 2-3 × Melee | 近战 Boss + 小怪 |
|
||||
| BOSS_02 | 1 × LongBoss(强制) + 1-2 × Support | 远程 Boss + 治疗 |
|
||||
| BOSS_03 | 1 × MeleeBoss 或 LongBoss(强制) + 1 × Assassin + 1 × Bomber | Boss + 精锐护卫 |
|
||||
|
||||
**规则 6:词缀系统**
|
||||
|
||||
词缀是附着在基础怪物上的属性修饰,提供变体丰富度:
|
||||
|
||||
| 词缀 | 效果 | 首次出现 Tier |
|
||||
|------|------|-------------|
|
||||
| Elite | +50% HP, +30% AP | T5 |
|
||||
| Berserk | 攻速 ×1.5 | T5 |
|
||||
| Shield | 开局带伤害吸收盾(20% HP) | T6 |
|
||||
| Regen | 每秒回复 2% HP | T7 |
|
||||
| Swift | 移速 ×2 | T7 |
|
||||
| Giant | ×2 体型, +100% HP, +50% AP | T8 |
|
||||
| Chain | 攻击附带 50% 溅射伤害 | T9 |
|
||||
| Summoner | 每 8 秒召唤 1 个小怪(Melee,1 级属性) | T10 |
|
||||
|
||||
词缀触发概率:
|
||||
|
||||
```
|
||||
affix_chance = min(base_affix_chance[tier] × role_multiplier, 1.0)
|
||||
|
||||
base_affix_chance:
|
||||
T1-T4: 0%
|
||||
T5: 10%, T6: 15%, T7: 25%, T8: 30%, T9: 40%, T10: 50%
|
||||
|
||||
role_multiplier:
|
||||
普通怪: 1.0x
|
||||
MiniBoss: 2.0x
|
||||
MajorBoss/FinalBoss: 3.0x
|
||||
```
|
||||
|
||||
> **注**:概率上限为 1.0(100%)。例如 T10 FinalBoss 的原始概率为 50%×3.0=150%,经 clamp 后为 100%(必出词缀)。每个词缀槽位独立判定概率,不超过角色类型的叠加上限。
|
||||
|
||||
词缀叠加上限:
|
||||
- 普通怪:最多 1 个
|
||||
- MiniBoss:最多 2 个
|
||||
- MajorBoss/FinalBoss:最多 3 个
|
||||
|
||||
互斥组:
|
||||
- Giant × Swift(体型矛盾)
|
||||
- Regen × Shield(防止过肉)
|
||||
- Summoner(词缀)× Summoner(类型)(防止双倍召唤叠加)
|
||||
|
||||
词缀属性加成采用**加法叠加**:
|
||||
```
|
||||
affix_hp_multiplier = 1.0 + Σ(各词缀 HP 加成百分比)
|
||||
affix_ap_multiplier = 1.0 + Σ(各词缀 AP 加成百分比)
|
||||
|
||||
示例:Elite(+50% HP, +30% AP) + Giant(+100% HP, +50% AP)
|
||||
→ affix_hp_multiplier = 1.0 + 0.5 + 1.0 = 2.5
|
||||
→ affix_ap_multiplier = 1.0 + 0.3 + 0.5 = 1.8
|
||||
```
|
||||
词缀乘数范围:1.0(无词缀)到 2.5(Elite+Giant HP 加满,加法叠加最大值)。
|
||||
|
||||
**规则 7:怪物类型定义**
|
||||
|
||||
| 类型 | 基础 HP | 基础 AP | 行为特征 |
|
||||
|------|---------|---------|---------|
|
||||
| Melee | 120 | 12 | 标准近战攻击,中速移动 |
|
||||
| Heavy | 350 | 30 | 极慢移动,高血量肉盾 |
|
||||
| Long | 80 | 45 | 远程攻击,低血量 |
|
||||
| Support | 80 | 20 | 治疗/增益其他怪,优先击杀目标 |
|
||||
| Bomber | 60 | 80 | 低血量,死亡时对周围造成 AP×200% 的 AOE 伤害 |
|
||||
| Summoner | 100 | 15 | 每 8 秒召唤 1 个小怪(Melee 1 级属性) |
|
||||
| Assassin | 90 | 55 | 快速突进到后排攻击英雄 |
|
||||
| Splitter | 150 | 20 | 死亡时分裂为 2 个小体(50% HP/AP) |
|
||||
| MeleeBoss | 1500 | 20 | Boss 级近战,带特殊技能 |
|
||||
| LongBoss | 350 | 30 | Boss 级远程,带特殊技能 |
|
||||
|
||||
### States and Transitions
|
||||
|
||||
**波次生成状态机:**
|
||||
|
||||
```
|
||||
INIT → SELECT_TEMPLATE → FILL_MONSTERS → APPLY_AFFIXES → APPLY_GROWTH → APPLY_ADAPTIVE → SPAWN_WAVE → EVALUATE_PERFORMANCE → (下一波)
|
||||
```
|
||||
|
||||
| 状态 | 输入 | 输出 | 转换条件 |
|
||||
|------|------|------|---------|
|
||||
| INIT | 当前 wave_number, tier | tier, wave_position_in_tier | 自动 |
|
||||
| SELECT_TEMPLATE | tier, wave_position, is_boss_wave | blueprint_template | 根据 REST/NORMAL/BOSS 规则选模板 |
|
||||
| FILL_MONSTERS | template, difficulty_budget | monster_list (types + counts) | 权重随机,总成本 ≤ budget |
|
||||
| APPLY_AFFIXES | monster_list, tier | affixed_monster_list | 按概率和规则添加词缀 |
|
||||
| APPLY_GROWTH | affixed_monster_list, tier | scaled_monsters | 属性 × tier_multiplier |
|
||||
| APPLY_ADAPTIVE | scaled_monsters, adaptive_factor | final_monsters | 属性 × adaptive_factor |
|
||||
| SPAWN_WAVE | final_monsters | (游戏内生成) | 按顺序排列怪物 |
|
||||
| EVALUATE_PERFORMANCE | 战斗结果 | updated adaptive_factor | 计算新自适应系数 |
|
||||
|
||||
### Interactions with Other Systems
|
||||
|
||||
| 系统 | 数据流方向 | 接口 |
|
||||
|------|-----------|------|
|
||||
| 英雄系统 | ← 接收 | 英雄存活率、队伍 DPS、通关时间 |
|
||||
| 战斗系统 | → 发送 | 最终怪物列表(类型、属性、词缀、位置) |
|
||||
| 怪物配置 | ← 接收 | 怪物基础属性、行为定义 |
|
||||
| 成长/升级系统 | ← 接收 | 玩家当前英雄等级和装备强度(自适应难度参考) |
|
||||
| UI 系统 | → 发送 | 当前波次、档位、怪物预览信息 |
|
||||
| 分数系统 | → 发送 | 击杀统计、Boss 击杀数据 |
|
||||
| 无限模式入口 | ← 接收 | 玩家选择是否进入无限模式 |
|
||||
|
||||
## Formulas
|
||||
|
||||
### F1: 阶梯属性倍率
|
||||
|
||||
```
|
||||
tier_multiplier = tier_multiplier_table[tier]
|
||||
```
|
||||
|
||||
| Variable | Symbol | Type | Range | Description |
|
||||
|----------|--------|------|-------|-------------|
|
||||
| tier | T | int | 1-10 (主线), 11+ (无限) | 当前难度阶梯 |
|
||||
| tier_multiplier | TM | float | 1.0-5.5 | 属性倍率 |
|
||||
|
||||
**Tier Multiplier 表:**
|
||||
|
||||
| Tier | Multiplier | Tier | Multiplier |
|
||||
|------|-----------|------|-----------|
|
||||
| T1 | 1.0x | T6 | 2.8x |
|
||||
| T2 | 1.3x | T7 | 3.3x |
|
||||
| T3 | 1.6x | T8 | 3.9x |
|
||||
| T4 | 1.9x | T9 | 4.6x |
|
||||
| T5 | 2.3x | T10 | 5.5x |
|
||||
|
||||
**无限模式扩展:** T(n) = T(n-1) × 1.2(T11=6.6x, T12=7.9x, ...)
|
||||
|
||||
**Output Range:** 1.0x (T1) to 5.5x (T10 主线), 无限模式无限递增
|
||||
**Example:** 基础 Melee 怪在 T5 的属性 = 120×2.3=276 HP, 12×2.3=27.6 AP
|
||||
|
||||
### F2: 难度预算
|
||||
|
||||
```
|
||||
wave_budget = base_budget[tier] × template_modifier[type]
|
||||
```
|
||||
|
||||
| Variable | Symbol | Type | Range | Description |
|
||||
|----------|--------|------|-------|-------------|
|
||||
| base_budget | BB | int | 100-1050 | 档位基础预算 |
|
||||
| template_modifier | TMOD | float | 0.5-1.5 | 模板类型修正 |
|
||||
|
||||
**Base Budget 表:**
|
||||
|
||||
| Tier | Budget | Tier | Budget |
|
||||
|------|--------|------|--------|
|
||||
| T1 | 100 | T6 | 440 |
|
||||
| T2 | 150 | T7 | 560 |
|
||||
| T3 | 200 | T8 | 700 |
|
||||
| T4 | 260 | T9 | 860 |
|
||||
| T5 | 340 | T10 | 1050 |
|
||||
|
||||
**Template Modifier:**
|
||||
- REST: 0.5x
|
||||
- NORMAL: 1.0x
|
||||
- MIXED: 1.2x
|
||||
- ELITE: 0.8x
|
||||
- BOSS: 1.5x
|
||||
|
||||
**Output Range:** 50 (T1 REST) to 1575 (T10 BOSS)
|
||||
**Example:** T5 NORMAL 波 → budget = 340 × 1.0 = 340 → 可生成约 8 个 Melee 或 4 个 Melee + 2 个 Long + 1 个 Support
|
||||
|
||||
### F3: 怪物最终属性
|
||||
|
||||
```
|
||||
final_hp = base_hp × tier_multiplier × affix_hp_multiplier × adaptive_factor
|
||||
final_ap = base_ap × tier_multiplier × affix_ap_multiplier × adaptive_factor
|
||||
```
|
||||
|
||||
| Variable | Symbol | Type | Range | Description |
|
||||
|----------|--------|------|-------|-------------|
|
||||
| base_hp/base_ap | BH/BA | int | 60-1500 | 怪物基础属性 |
|
||||
| affix_hp/ap_multiplier | AHM/AAM | float | 1.0-2.5 | 词缀属性倍率(加法叠加,Elite+Giant 最大 2.5) |
|
||||
| adaptive_factor | AF | float | 0.85-1.15 | 自适应系数 |
|
||||
|
||||
**Output Range:** HP 51 (Bomber, T1, no affix, AF=0.85) to 19,031+ (MeleeBoss, T15 无限, Elite+Giant, AF=1.15)
|
||||
**Example:** Melee 怪在 T7, 带 Elite 词缀, AF=1.0 → HP=120×3.3×1.5×1.0=594, AP=12×3.3×1.3×1.0=51.5
|
||||
|
||||
### F4: 自适应难度系数
|
||||
|
||||
> **设计说明**:自适应系数 (AF) 仅作用于怪物属性(F3),不作用于波次预算(F2)。这意味着自适应难度调整的是"每只怪物的强弱"而非"怪物的数量",实际效果接近 ±15%,而非双重叠加。
|
||||
|
||||
```
|
||||
adaptive_factor = clamp(adaptive_factor + delta, 0.85, 1.15)
|
||||
|
||||
delta calculation per wave:
|
||||
if heroes_alive_ratio >= 0.8 AND clear_time < target_time:
|
||||
delta = +0.03 // 玩家太强,提高怪物属性
|
||||
elif heroes_alive_ratio <= 0.3:
|
||||
delta = -0.03 // 快团灭,降低怪物属性
|
||||
else:
|
||||
delta = 0 // 维持
|
||||
```
|
||||
|
||||
| Variable | Symbol | Type | Range | Description |
|
||||
|----------|--------|------|-------|-------------|
|
||||
| heroes_alive_ratio | HAR | float | 0.0-1.0 | 存活英雄比例 |
|
||||
| clear_time | CT | float | 3.0-60.0 | 通关时间(秒) |
|
||||
| target_time | TT | float | 15.0 | 目标通关时间 |
|
||||
| delta | D | float | -0.03 to +0.03 | 每波调整量 |
|
||||
| adaptive_factor | AF | float | 0.85-1.15 | 最终自适应系数 |
|
||||
|
||||
**Output Range:** 0.85-1.15 (硬限制)
|
||||
**Example:** 连续 3 波全英雄存活且 5 秒内通关 → AF = 1.0 + 0.03×3 = 1.09
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- **If 难度预算不足以生成任何怪物**:保底生成 1 个 Melee 怪(忽略预算限制),确保每波至少有内容。
|
||||
- **If 权重填充结果为空(所有怪物类型被 tier_min 过滤)**:回退到该 Tier 最低限制的类型池(Melee 总是可用)。
|
||||
- **If 两个互斥词缀同时被选中**:按词缀优先级(Shield > Regen > Giant > Swift)保留优先级更高的,丢弃另一个。
|
||||
- **If 怪物最终 HP 或 AP 低于 1**:clamp 到最小值 1。怪物不能是 0 血或 0 攻击。
|
||||
- **If 自适应系数连续 5 波向同一方向调整(持续加难或降难)**:强制一次反向 +0.01 微调,防止极端漂移。
|
||||
- **If 无限模式中 tier_multiplier 导致怪物 HP > 100,000**:启用对数压缩公式 `display_hp = log10(actual_hp) × scaling_factor`,避免 UI 数字溢出。实际伤害计算仍用真实值。
|
||||
- **If Bomber 怪被 AOE 同时击杀多个**:爆炸伤害不叠加,每个爆炸独立计算但不会叠加超过总伤害的 300%。
|
||||
- **If Splitter 分裂出的子体再次被击杀**:子体不再分裂(分裂深度限制为 1 级)。
|
||||
- **If Summoner 召唤的小怪存活超过 30 秒**:小怪自动消散,防止场上怪物无限积累。
|
||||
- **If 玩家在 Boss 波前有 0 个存活英雄(理论上不应发生)**:不生成该波怪物,触发游戏结束结算。
|
||||
- **If 同一模板被连续抽取 3 次**:强制下一次从不同类型模板池中抽取,避免体验单调。
|
||||
|
||||
## Dependencies
|
||||
|
||||
| 依赖系统 | 方向 | 性质 | 数据接口 |
|
||||
|----------|------|------|---------|
|
||||
| **英雄系统** (heroSet.ts) | 上游(硬依赖) | 读取英雄属性、存活状态 | `heroes_alive_ratio`, `team_dps`, `clear_time` |
|
||||
| **战斗系统** | 下游(硬依赖) | 发送怪物列表 | `final_monster_list[]` 含 type, hp, ap, affixes, position |
|
||||
| **怪物配置** (HeroAttrs.ts 怪物部分) | 上游(硬依赖) | 读取怪物基础属性 | `base_hp`, `base_ap`, `speed`, `skills` |
|
||||
| **成长/升级系统** | 上游(软依赖) | 了解玩家当前强度 | `hero_levels[]`, `equipment_power` |
|
||||
| **UI 系统** | 下游(软依赖) | 发送波次信息 | `wave_number`, `tier`, `monster_preview[]` |
|
||||
| **分数系统** | 下游(软依赖) | 发送击杀统计 | `boss_kills`, `elite_kills`, `total_kills` |
|
||||
| **无限模式入口** | 上游(软依赖) | 接收玩家选择 | `enter_infinite: boolean` |
|
||||
|
||||
**硬依赖说明**:英雄系统、战斗系统、怪物配置缺失时刷怪系统无法运行。
|
||||
**软依赖说明**:成长系统和 UI 系统缺失时刷怪系统可工作,但自适应难度和波次预览功能降级。
|
||||
|
||||
## Tuning Knobs
|
||||
|
||||
所有设计者可在不修改代码的情况下调整的配置值:
|
||||
|
||||
| 旋钮 | 当前值 | 安全范围 | 超出后果 |
|
||||
|------|--------|---------|---------|
|
||||
| `tier_multiplier_table[T1-T10]` | 1.0-5.5 | 0.5-10.0 | 过低→后期无挑战;过高→后期不可打 |
|
||||
| `base_budget[T1-T10]` | 100-1050 | 50-2000 | 过低→怪物太少无聊;过高→怪物太多卡顿 |
|
||||
| `template_modifier[REST]` | 0.5x | 0.3-0.8 | 过低→恢复波太弱无意义;过高→节奏混乱 |
|
||||
| `template_modifier[BOSS]` | 1.5x | 1.0-2.5 | 过低→Boss 无感;过高→Boss 波必团灭 |
|
||||
| `adaptive_factor_min` | 0.85 | 0.7-0.95 | 过低→自适应降难太明显 |
|
||||
| `adaptive_factor_max` | 1.15 | 1.05-1.3 | 过高→自适应加难太明显 |
|
||||
| `adaptive_delta_per_wave` | 0.03 | 0.01-0.08 | 过低→反应迟钝;过高→体验不稳定 |
|
||||
| `target_clear_time` | 15.0s | 5.0-30.0 | 影响自适应判断阈值 |
|
||||
| `base_affix_chance[T5-T10]` | 10%-50% | 0%-80% | 过低→词缀系统无意义;过高→全是精英怪 |
|
||||
| `bomber_explosion_multiplier` | 200% | 100%-400% | 爆炸伤害占 AP 的比例 |
|
||||
| `splitter_child_hp_ratio` | 50% | 25%-75% | 分裂子体 HP 占母体比例 |
|
||||
| `splitter_child_ap_ratio` | 50% | 25%-75% | 分裂子体 AP 占母体比例 |
|
||||
| `summoner_spawn_interval` | 8s | 4-15s | 召唤间隔 |
|
||||
| `summoner_max_minions` | 3 | 1-5 | 场上最多同时存在的小怪数 |
|
||||
| `minion_lifetime` | 30s | 10-60s | 小怪存活上限 |
|
||||
| `infinite_tier_growth_rate` | 1.2x | 1.1-1.5 | 无限模式每档递增率 |
|
||||
| `monster_types_available[T1-T10]` | 见规则2 | - | 每档可用怪物类型集合 |
|
||||
| `affix_mutual_exclusion` | Giant×Swift, Regen×Shield | - | 互斥词缀对 |
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### 核心波次生成
|
||||
|
||||
- **GIVEN** 游戏开始(adaptive_factor=1.0),**WHEN** 进入 W1,**THEN** 生成 1 个 Melee 怪(T1, 无词缀),HP=120, AP=12。
|
||||
- **GIVEN** 当前 Tier 2 W2(攀升波),**WHEN** 模板选取完成,**THEN** 模板类型为 NORMAL 或 MIXED,且模板的怪物槽位池中包含 Long 类型(cost=40)。运行 100 次抽取,Long 类型出现在槽位池中的比例为 100%。
|
||||
- **GIVEN** W6(Boss 波),**WHEN** 模板选取完成,**THEN** 模板类型为 BOSS,mandatory_slots 包含 Boss 类型,至少生成 1 个 MeleeBoss + 0-3 个普通怪。
|
||||
- **GIVEN** Tier 3 W1(REST 波),**WHEN** 模板选取完成,**THEN** 模板类型为 REST,template_modifier=0.5x。
|
||||
- **GIVEN** Tier 4 W3(非 Boss 高潮波),**WHEN** 模板选取完成,**THEN** 模板类型为 MIXED,template_modifier=1.2x。
|
||||
|
||||
### 预算系统
|
||||
|
||||
- **GIVEN** Tier 5 NORMAL 波(budget=340, AF 不影响预算),**WHEN** 填充怪物完成,**THEN** 所有怪物成本总和在 340 × 0.7 到 340 × 1.0 之间(即 238-340),且至少生成 3 只怪物。
|
||||
- **GIVEN** 预算不足以生成任何怪物,**WHEN** 填充阶段,**THEN** 保底生成 1 个 Melee 怪(忽略预算限制)。
|
||||
|
||||
### 词缀系统
|
||||
|
||||
- **GIVEN** Tier 5 正常波(10+ 只怪),**WHEN** 运行 1000 次统计测试,**THEN** 词缀触发率落在 8%-12% 区间(90% 置信度),且所有触发的词缀类型均为 Elite 或 Berserk。单次运行中无词缀怪物比例在 20%-50% 区间内。
|
||||
- **GIVEN** Tier 10 FinalBoss(W30),**WHEN** 词缀应用完成,**THEN** FinalBoss 拥有 2-3 个词缀,且任意两个词缀的组合不属于互斥组(Giant+Swift, Regen+Shield, Summoner类型+Summoner词缀)。运行 100 次模拟,0 次出现互斥词缀同时生效。
|
||||
- **GIVEN** Tier 7 正常波,**WHEN** 词缀应用完成,**THEN** 每只普通怪最多拥有 1 个词缀。运行 1000 次模拟,0 次出现普通怪拥有 >1 个词缀的情况。
|
||||
- **GIVEN** Tier 7 W21 MiniBoss,**WHEN** 词缀应用完成,**THEN** MiniBoss 拥有 0-2 个词缀,0 次超过 2 个词缀。
|
||||
- **GIVEN** 两个互斥词缀(Giant + Swift)同时被选中,**WHEN** 词缀应用阶段,**THEN** 保留优先级更高的词缀(Giant),丢弃 Swift。
|
||||
- **GIVEN** 两个互斥词缀(Regen + Shield)同时被选中,**WHEN** 词缀应用阶段,**THEN** 保留优先级更高的词缀(Shield),丢弃 Regen。
|
||||
|
||||
### 自适应难度
|
||||
|
||||
- **GIVEN** adaptive_factor 初始值为 1.0,且连续 3 波全英雄存活(heroes_alive_ratio ≥ 0.8)且通关时间 < 15s,**WHEN** 自适应计算完成,**THEN** adaptive_factor = 1.0 + 0.03 × 3 = 1.09。
|
||||
- **GIVEN** adaptive_factor 初始值为 1.0,且连续 2 波仅 1 英雄存活(heroes_alive_ratio ≤ 0.3),**WHEN** 自适应计算完成,**THEN** adaptive_factor = 1.0 - 0.03 × 2 = 0.94。
|
||||
- **GIVEN** adaptive_factor 连续 5 波递减(每次 delta=-0.03),当前值为 0.85(clamp 下限),**WHEN** 第 6 波计算 delta 仍为 -0.03,**THEN** 实际施加的 delta 被覆盖为 +0.01(反向微调),adaptive_factor 变为 0.86。
|
||||
|
||||
### 怪物行为
|
||||
|
||||
- **GIVEN** Bomber 怪(final_ap=X)被击杀,**WHEN** 死亡触发,**THEN** 以 Bomber 死亡位置为中心,对 AOE_RADIUS 范围内的所有英雄造成 X × 200% 的伤害。
|
||||
- **GIVEN** 3 个 Bomber 怪(final_ap=80)在 AOE 范围内被同时击杀,**WHEN** 爆炸触发,**THEN** 单个英雄受到的总伤害 ≤ 80 × 200% × 300% = 480。
|
||||
- **GIVEN** Splitter 怪(HP=150, AP=20)被击杀,**WHEN** 死亡触发,**THEN** 生成 2 个小体,每个小体 HP=75, AP=10。
|
||||
- **GIVEN** Splitter 小体被击杀,**WHEN** 死亡触发,**THEN** 不生成任何子体(分裂深度限制为 1 级)。
|
||||
- **GIVEN** Summoner 怪存活且场上 0 个小怪,**WHEN** 经过 8 秒,**THEN** 召唤 1 个 Melee 小怪(T1 属性)。
|
||||
- **GIVEN** Summoner 怪存活且场上已有 3 个小怪,**WHEN** 经过 8 秒,**THEN** 不召唤新的小怪。
|
||||
- **GIVEN** Summoner 召唤的小怪存活 30 秒,**WHEN** 计时器触发,**THEN** 小怪自动消散(从场上移除)。
|
||||
|
||||
### 属性计算
|
||||
|
||||
- **GIVEN** Tier 7 Melee 怪,带 Elite 词缀(+50% HP, +30% AP),AF=1.0,**WHEN** 属性计算完成,**THEN** HP = 120 × 3.3 × 1.5 × 1.0 = 594,AP = 12 × 3.3 × 1.3 × 1.0 = 51.48。
|
||||
- **GIVEN** 任何怪物,**WHEN** 最终属性计算完成(base × tier_multiplier × affix_multiplier × adaptive_factor),**THEN** final_hp ≥ 1 且 final_ap ≥ 1。
|
||||
|
||||
### 无限模式与模板多样性
|
||||
|
||||
- **GIVEN** 主线 W30 通关,**WHEN** 玩家选择进入无限模式,**THEN** 从 T11 开始,每层 4 波(REST→NORMAL→MIXED→BOSS),属性倍率 = tier_multiplier_table[T10] × infinite_tier_growth_rate = 5.5 × 1.2 = 6.6x。
|
||||
- **GIVEN** 无限模式 T12,**WHEN** 属性倍率计算,**THEN** tier_multiplier = 6.6 × 1.2 = 7.92x。
|
||||
- **GIVEN** 同一模板(id 相同)被连续抽取 3 次,**WHEN** 第 4 波模板选取,**THEN** 选出的模板类型与连续 3 次的类型不同(如连续 3 次 NORMAL 则强制从 REST/MIXED/ELITE/BOSS 池中抽取)。
|
||||
|
||||
### 游戏结束条件
|
||||
|
||||
- **GIVEN** 当前波次为 Boss 波且 heroes_alive_count = 0,**WHEN** 波次生成流程启动,**THEN** 不生成任何怪物,触发游戏结束结算流程。
|
||||
|
||||
## Visual/Audio Requirements
|
||||
|
||||
### 词缀视觉差异化
|
||||
|
||||
每种词缀需要对应的视觉标识,让玩家一眼识别威胁类型:
|
||||
|
||||
| 词缀 | 视觉效果 | 颜色/标识 |
|
||||
|------|---------|----------|
|
||||
| Elite | 怪物周围光圈 | 金色光环 |
|
||||
| Berserk | 怪物身体发红,攻击动画加速 | 红色脉动 |
|
||||
| Shield | 怪物面前半透明护盾 | 蓝色半透明盾 |
|
||||
| Regen | 怪物脚下绿色光环,回复时绿色数字飘出 | 绿色粒子 |
|
||||
| Swift | 怪物移动时带残影拖尾 | 白色拖尾 |
|
||||
| Giant | 怪物体型 ×2 | - |
|
||||
| Chain | 攻击时连电弧特效 | 紫色电弧 |
|
||||
| Summoner | 召唤时脚下法阵特效 | 紫色法阵 |
|
||||
|
||||
### Boss 视觉需求
|
||||
|
||||
- Boss 生成时全屏闪白 + 震屏效果
|
||||
- Boss 血条 UI 单独显示在屏幕顶部
|
||||
- MajorBoss/FinalBoss 有独特的出场动画(2 秒)
|
||||
|
||||
### 新怪物类型视觉
|
||||
|
||||
- **Bomber**:怪物身上带红色炸弹标识,死亡时爆炸粒子特效(橙色+火焰)
|
||||
- **Summoner**:带法杖的怪物,召唤时法杖发光
|
||||
- **Assassin**:瘦长体型,移动时带残影,突进时有速度线特效
|
||||
- **Splitter**:体型略大于普通怪,死亡时分裂动画(绿色分裂粒子)
|
||||
- **Heavy**:体型 ×1.5,移动缓慢,脚步有灰尘特效
|
||||
|
||||
### Audio 需求
|
||||
|
||||
- Boss 出场:低频鼓声 + 金属碰撞音效
|
||||
- Bomber 爆炸:爆炸音效(带屏幕震动)
|
||||
- 自爆怪预警音:Bomber 血量低于 30% 时发出嘀嗒声
|
||||
- 词缀怪物:Elite 怪发出低沉吼叫,Swift 怪有疾风声
|
||||
|
||||
## Open Questions
|
||||
|
||||
| # | 问题 | 负责人 | 目标解决日期 | 备注 |
|
||||
|---|------|--------|-------------|------|
|
||||
| 1 | 无限模式 Mega Boss 的特殊机制是什么? | 策划 | 待定 | 每 3 层出现的 Mega Boss 需要独特玩法,不能只是数值翻倍 |
|
||||
| 2 | Summoner 召唤的小怪是否影响分数统计? | 策划 | 待定 | 如果计入击杀数可能刷分,需确认规则 |
|
||||
| 3 | 自适应难度是否对玩家可见? | 策划+UI | 待定 | 部分 Roguelike 游戏会显示难度等级作为反馈 |
|
||||
| 4 | 蓝图模板池具体需要多少个模板? | 策划 | 待定 | 目前每类定义了 3 个示例模板,正式版每类需要 5-8 个 |
|
||||
| 5 | 新怪物类型(Bomber/Summoner/Assassin/Splitter/Heavy)的 UUID 编号段? | 开发 | 待定 | 需与 HeroAttrs.ts 中的编号体系对齐,需独立怪物定义文档(UUID、AI 行为树、资源规范) |
|
||||
| 6 | 飞行怪移除后,现有 Fly/FlyBoss 的 UUID 是否回收? | 开发 | 待定 | 影响资源管理 |
|
||||
| 7 | **英雄战力曲线验证** — 怪物缩放(5.5×)需对照玩家 DPS 曲线校准 | 策划 | 待定 | 缺少英雄成长 GDD,所有平衡数值未验证。需定义:T1/T5/T10 预期团队 DPS、每波怪物预期存活时间 |
|
||||
| 8 | **Bomber 爆炸 AOE 半径** — AC 引用了 AOE_RADIUS 但未在调参旋钮中定义 | 策划 | 待定 | 需添加到 Tuning Knobs 表 |
|
||||
| 9 | **MajorBoss vs MiniBoss 机械区别** — 当前仅数值差异,是否有特殊技能/阶段转换? | 策划 | 待定 | W15 MajorBoss 需要与非 MajorBoss 的体验差异 |
|
||||
11
assets/script/game/map/gdd/rogue-spawning.md.meta
Normal file
11
assets/script/game/map/gdd/rogue-spawning.md.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"importer": "text",
|
||||
"imported": true,
|
||||
"uuid": "fce5dc7c-74cc-4f86-a314-996d2d8650b5",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
7
assets/script/game/map/gdd/systems-index.md
Normal file
7
assets/script/game/map/gdd/systems-index.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Systems Index
|
||||
|
||||
Design order: Foundation → Core → Feature → Presentation → Polish
|
||||
|
||||
| System | Design Order | GDD File | Status | Dependencies |
|
||||
|--------|-------------|----------|--------|-------------|
|
||||
| Rogue Spawning (肉鸽刷怪) | Core | [rogue-spawning.md](rogue-spawning.md) | Revised (pending re-review) | 英雄系统, 战斗系统, 怪物配置 (硬) |
|
||||
11
assets/script/game/map/gdd/systems-index.md.meta
Normal file
11
assets/script/game/map/gdd/systems-index.md.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"ver": "1.0.1",
|
||||
"importer": "text",
|
||||
"imported": true,
|
||||
"uuid": "c7049b13-8ae2-45e8-a8de-d760eef08b8e",
|
||||
"files": [
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
Reference in New Issue
Block a user