Compare commits
2 Commits
a94381b19f
...
08fd5cb092
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08fd5cb092 | ||
|
|
ff2c98a296 |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -36,56 +36,56 @@ interface EquipRawData {
|
||||
/** 装备卡原始数据表 */
|
||||
const EquipRawList: EquipRawData[] = [
|
||||
// ==================== wave 1 档(基础强度) ====================
|
||||
{ uuid: 8701, wave: SKILL_CARD_WAVES[0], icon: "Stat_PotionBoost", name: "战后恢复", info: "战斗结束生命回复量+10%", field: [7001] },
|
||||
{ uuid: 8702, wave: SKILL_CARD_WAVES[0], icon: "Stat_Attack_03", name: "攻击加成", info: "英雄攻击力+10%", field: [7002] },
|
||||
{ uuid: 8703, wave: SKILL_CARD_WAVES[0], icon: "Stat_Stun_01", name: "击晕加成", info: "英雄击晕概率+10%", field: [7003] },
|
||||
{ uuid: 8704, wave: SKILL_CARD_WAVES[0], icon: "Stat_CriticalChance_02", name: "暴击加成", info: "英雄暴击率+10%", field: [7004] },
|
||||
{ uuid: 8705, wave: SKILL_CARD_WAVES[0], icon: "Stat_Critical_01", name: "暴伤加成", info: "英雄暴击伤害+20%", field: [7005] },
|
||||
{ uuid: 8706, wave: SKILL_CARD_WAVES[0], icon: "Stat_AttackSpeed_02", name: "攻速加成", info: "英雄攻击速度+10%", field: [7006] },
|
||||
{ uuid: 8707, wave: SKILL_CARD_WAVES[0], icon: "Stat_Hp_02", name: "生命加成", info: "英雄最大生命+10%", field: [7007] },
|
||||
{ uuid: 8708, wave: SKILL_CARD_WAVES[0], icon: "Stat_CriticalComboChance", name: "风怒加成", info: "英雄风怒概率+10%", field: [7008] },
|
||||
{ uuid: 8709, wave: SKILL_CARD_WAVES[0], icon: "Stat_Tripleshot", name: "穿刺加成", info: "英雄穿刺概率+10%", field: [7009] },
|
||||
{ uuid: 8710, wave: SKILL_CARD_WAVES[0], icon: "Stat_InventorySlotIncrease", name: "金币收益", info: "每回合金币收益+1", field: [7010] },
|
||||
{ uuid: 8711, wave: SKILL_CARD_WAVES[0], icon: "Stat_GoldGainIncrease_01", name: "出售强化", info: "卖出英雄金币+1", field: [7011] },
|
||||
{ uuid: 8701, wave: SKILL_CARD_WAVES[0], icon: "Helmet-FA_Helmet_021_Gray", name: "战后恢复", info: "战斗结束生命回复量+10%", field: [7001] },
|
||||
{ uuid: 8702, wave: SKILL_CARD_WAVES[0], icon: "Sword-FA_WP_Main_Sword_003_GrayGold", name: "攻击加成", info: "英雄攻击力+10%", field: [7002] },
|
||||
{ uuid: 8703, wave: SKILL_CARD_WAVES[0], icon: "Blunt-FA_WP_Main_Blunt_002_Wood", name: "击晕加成", info: "英雄击晕概率+10%", field: [7003] },
|
||||
{ uuid: 8704, wave: SKILL_CARD_WAVES[0], icon: "Axe-FA_WP_Main_Axe_001_WoodGray", name: "暴击加成", info: "英雄暴击率+10%", field: [7004] },
|
||||
{ uuid: 8705, wave: SKILL_CARD_WAVES[0], icon: "Bow-FA_WP_Main_Bow_003_WoodGreen", name: "暴伤加成", info: "英雄暴击伤害+20%", field: [7005] },
|
||||
{ uuid: 8706, wave: SKILL_CARD_WAVES[0], icon: "Crossbow-FA_WP_Main_Crossbow_001_WoodWhite", name: "攻速加成", info: "英雄攻击速度+10%", field: [7006] },
|
||||
{ uuid: 8707, wave: SKILL_CARD_WAVES[0], icon: "Chest-FA_Chest_014_Gray", name: "生命加成", info: "英雄最大生命+10%", field: [7007] },
|
||||
{ uuid: 8708, wave: SKILL_CARD_WAVES[0], icon: "Wand-FA_WP_Main_Wand_001_GrayGreen", name: "风怒加成", info: "英雄风怒概率+10%", field: [7008] },
|
||||
{ uuid: 8709, wave: SKILL_CARD_WAVES[0], icon: "Spear-FA_WP_Main_Spear_004_WoodBlue", name: "穿刺加成", info: "英雄穿刺概率+10%", field: [7009] },
|
||||
{ uuid: 8710, wave: SKILL_CARD_WAVES[0], icon: "Shield-FA_WP_Sub_Shield_007_GoldGray", name: "金币收益", info: "每回合金币收益+1", field: [7010] },
|
||||
{ uuid: 8711, wave: SKILL_CARD_WAVES[0], icon: "Staff-FA_WP_Main_Staff_003_WoodSkyblue", name: "出售强化", info: "卖出英雄金币+1", field: [7011] },
|
||||
// 多词条装备
|
||||
{ uuid: 8720, wave: SKILL_CARD_WAVES[0], icon: "Stat_Attack_03", name: "勇者护符", info: "英雄攻击力+10%, 暴击率+10%, 暴击伤害+20%", field: [7002, 7004, 7005] },
|
||||
{ uuid: 8720, wave: SKILL_CARD_WAVES[0], icon: "Shield-FA_WP_Sub_Shield_007_GoldGray", name: "勇者护符", info: "英雄攻击力+10%, 暴击率+10%, 暴击伤害+20%", field: [7002, 7004, 7005] },
|
||||
|
||||
// ==================== wave 5 档(强度 ×2) ====================
|
||||
{ uuid: 8751, wave: SKILL_CARD_WAVES[1], icon: "Stat_PotionBoost", name: "战后恢复+", info: "战斗结束生命回复量+20%", field: [7201] },
|
||||
{ uuid: 8752, wave: SKILL_CARD_WAVES[1], icon: "Stat_Attack_03", name: "攻击加成+", info: "英雄攻击力+20%", field: [7202] },
|
||||
{ uuid: 8753, wave: SKILL_CARD_WAVES[1], icon: "Stat_Stun_01", name: "击晕加成+", info: "英雄击晕概率+20%", field: [7203] },
|
||||
{ uuid: 8754, wave: SKILL_CARD_WAVES[1], icon: "Stat_CriticalChance_02", name: "暴击加成+", info: "英雄暴击率+20%", field: [7204] },
|
||||
{ uuid: 8755, wave: SKILL_CARD_WAVES[1], icon: "Stat_Critical_01", name: "暴伤加成+", info: "英雄暴击伤害+40%", field: [7205] },
|
||||
{ uuid: 8756, wave: SKILL_CARD_WAVES[1], icon: "Stat_AttackSpeed_02", name: "攻速加成+", info: "英雄攻击速度+20%", field: [7206] },
|
||||
{ uuid: 8757, wave: SKILL_CARD_WAVES[1], icon: "Stat_Hp_02", name: "生命加成+", info: "英雄最大生命+20%", field: [7207] },
|
||||
{ uuid: 8758, wave: SKILL_CARD_WAVES[1], icon: "Stat_CriticalComboChance", name: "风怒加成+", info: "英雄风怒概率+20%", field: [7208] },
|
||||
{ uuid: 8759, wave: SKILL_CARD_WAVES[1], icon: "Stat_Tripleshot", name: "穿刺加成+", info: "英雄穿刺概率+20%", field: [7209] },
|
||||
{ uuid: 8760, wave: SKILL_CARD_WAVES[1], icon: "Stat_InventorySlotIncrease", name: "金币收益+", info: "每回合金币收益+2", field: [7210] },
|
||||
{ uuid: 8761, wave: SKILL_CARD_WAVES[1], icon: "Stat_KeyCapacityIncrease", name: "购买优惠+", info: "购买卡牌费用-2金币", field: [7212] },
|
||||
{ uuid: 8762, wave: SKILL_CARD_WAVES[1], icon: "Stat_RandomBonus", name: "刷新优惠", info: "刷新卡牌费用-1金币", field: [7213] },
|
||||
{ uuid: 8751, wave: SKILL_CARD_WAVES[1], icon: "Helmet-FA_Helmet_022_DarkBlue", name: "战后恢复+", info: "战斗结束生命回复量+20%", field: [7201] },
|
||||
{ uuid: 8752, wave: SKILL_CARD_WAVES[1], icon: "Sword-FA_WP_Main_Sword_007_Blue", name: "攻击加成+", info: "英雄攻击力+20%", field: [7202] },
|
||||
{ uuid: 8753, wave: SKILL_CARD_WAVES[1], icon: "Blunt-FA_WP_Main_Blunt_004_Gray", name: "击晕加成+", info: "英雄击晕概率+20%", field: [7203] },
|
||||
{ uuid: 8754, wave: SKILL_CARD_WAVES[1], icon: "Axe-FA_WP_Main_Axe_006_WoodBronze", name: "暴击加成+", info: "英雄暴击率+20%", field: [7204] },
|
||||
{ uuid: 8755, wave: SKILL_CARD_WAVES[1], icon: "Bow-FA_WP_Main_Bow_010_Blue", name: "暴伤加成+", info: "英雄暴击伤害+40%", field: [7205] },
|
||||
{ uuid: 8756, wave: SKILL_CARD_WAVES[1], icon: "Crossbow-FA_WP_Main_Crossbow_004_Gray", name: "攻速加成+", info: "英雄攻击速度+20%", field: [7206] },
|
||||
{ uuid: 8757, wave: SKILL_CARD_WAVES[1], icon: "Chest-FA_Chest_014_BlueGold", name: "生命加成+", info: "英雄最大生命+20%", field: [7207] },
|
||||
{ uuid: 8758, wave: SKILL_CARD_WAVES[1], icon: "Wand-FA_WP_Main_Wand_003_BlueWood", name: "风怒加成+", info: "英雄风怒概率+20%", field: [7208] },
|
||||
{ uuid: 8759, wave: SKILL_CARD_WAVES[1], icon: "Spear-FA_WP_Main_Spear_007_GraySkyblue", name: "穿刺加成+", info: "英雄穿刺概率+20%", field: [7209] },
|
||||
{ uuid: 8760, wave: SKILL_CARD_WAVES[1], icon: "Shield-FA_WP_Sub_Shield_008_BlueGold", name: "金币收益+", info: "每回合金币收益+2", field: [7210] },
|
||||
{ uuid: 8761, wave: SKILL_CARD_WAVES[1], icon: "Shield-FA_WP_Sub_Shield_008_BlueGold", name: "购买优惠+", info: "购买卡牌费用-2金币", field: [7212] },
|
||||
{ uuid: 8762, wave: SKILL_CARD_WAVES[1], icon: "Staff-FA_WP_Main_Staff_007_WoodGray", name: "刷新优惠", info: "刷新卡牌费用-1金币", field: [7213] },
|
||||
// 多词条装备
|
||||
{ uuid: 8770, wave: SKILL_CARD_WAVES[1], icon: "Stat_Hp_02", name: "守卫护甲", info: "英雄最大生命+20%, 战后生命回复+20%, 攻速+20%", field: [7207, 7201, 7206] },
|
||||
{ uuid: 8770, wave: SKILL_CARD_WAVES[1], icon: "Chest-FA_Chest_014_BlueGold", name: "守卫护甲", info: "英雄最大生命+20%, 战后生命回复+20%, 攻速+20%", field: [7207, 7201, 7206] },
|
||||
|
||||
// ==================== wave 8 档(强度 ×3) ====================
|
||||
{ uuid: 8801, wave: SKILL_CARD_WAVES[2], icon: "Stat_PotionBoost", name: "战后恢复++", info: "战斗结束生命回复量+30%", field: [7401] },
|
||||
{ uuid: 8802, wave: SKILL_CARD_WAVES[2], icon: "Stat_Attack_03", name: "攻击加成++", info: "英雄攻击力+30%", field: [7402] },
|
||||
{ uuid: 8803, wave: SKILL_CARD_WAVES[2], icon: "Stat_Stun_01", name: "击晕加成++", info: "英雄击晕概率+30%", field: [7403] },
|
||||
{ uuid: 8804, wave: SKILL_CARD_WAVES[2], icon: "Stat_CriticalChance_02", name: "暴击加成++", info: "英雄暴击率+30%", field: [7404] },
|
||||
{ uuid: 8805, wave: SKILL_CARD_WAVES[2], icon: "Stat_Critical_01", name: "暴伤加成++", info: "英雄暴击伤害+60%", field: [7405] },
|
||||
{ uuid: 8806, wave: SKILL_CARD_WAVES[2], icon: "Stat_AttackSpeed_02", name: "攻速加成++", info: "英雄攻击速度+30%", field: [7406] },
|
||||
{ uuid: 8807, wave: SKILL_CARD_WAVES[2], icon: "Stat_Hp_02", name: "生命加成++", info: "英雄最大生命+30%", field: [7407] },
|
||||
{ uuid: 8808, wave: SKILL_CARD_WAVES[2], icon: "Stat_CriticalComboChance", name: "风怒加成++", info: "英雄风怒概率+30%", field: [7408] },
|
||||
{ uuid: 8809, wave: SKILL_CARD_WAVES[2], icon: "Stat_Tripleshot", name: "穿刺加成++", info: "英雄穿刺概率+30%", field: [7409] },
|
||||
{ uuid: 8810, wave: SKILL_CARD_WAVES[2], icon: "Stat_InventorySlotIncrease", name: "金币收益++", info: "每回合金币收益+3", field: [7410] },
|
||||
{ uuid: 8801, wave: SKILL_CARD_WAVES[2], icon: "Helmet-FA_Helmet_021_RedGold", name: "战后恢复++", info: "战斗结束生命回复量+30%", field: [7401] },
|
||||
{ uuid: 8802, wave: SKILL_CARD_WAVES[2], icon: "Sword-FA_WP_Main_Sword_001_Gold", name: "攻击加成++", info: "英雄攻击力+30%", field: [7402] },
|
||||
{ uuid: 8803, wave: SKILL_CARD_WAVES[2], icon: "Blunt-FA_WP_Main_Blunt_006_OrangeGray", name: "击晕加成++", info: "英雄击晕概率+30%", field: [7403] },
|
||||
{ uuid: 8804, wave: SKILL_CARD_WAVES[2], icon: "Axe-FA_WP_Main_Axe_012_WoodGold", name: "暴击加成++", info: "英雄暴击率+30%", field: [7404] },
|
||||
{ uuid: 8805, wave: SKILL_CARD_WAVES[2], icon: "Bow-FA_WP_Main_Bow_010_Gold", name: "暴伤加成++", info: "英雄暴击伤害+60%", field: [7405] },
|
||||
{ uuid: 8806, wave: SKILL_CARD_WAVES[2], icon: "Crossbow-FA_WP_Main_Crossbow_005_WoodGold", name: "攻速加成++", info: "英雄攻击速度+30%", field: [7406] },
|
||||
{ uuid: 8807, wave: SKILL_CARD_WAVES[2], icon: "Chest-FA_Chest_015_RedGold", name: "生命加成++", info: "英雄最大生命+30%", field: [7407] },
|
||||
{ uuid: 8808, wave: SKILL_CARD_WAVES[2], icon: "Wand-FA_WP_Main_Wand_004_WoodGold", name: "风怒加成++", info: "英雄风怒概率+30%", field: [7408] },
|
||||
{ uuid: 8809, wave: SKILL_CARD_WAVES[2], icon: "Spear-FA_WP_Main_Spear_008_GrayGold", name: "穿刺加成++", info: "英雄穿刺概率+30%", field: [7409] },
|
||||
{ uuid: 8810, wave: SKILL_CARD_WAVES[2], icon: "Shield-FA_WP_Sub_Shield_009_RedGold", name: "金币收益++", info: "每回合金币收益+3", field: [7410] },
|
||||
// 触发次数强化
|
||||
{ uuid: 8811, wave: SKILL_CARD_WAVES[2], icon: "Stat_UnitSummonIncrease_02", name: "召唤强化++", info: "召唤触发技能次数+1", field: [7014] },
|
||||
{ uuid: 8812, wave: SKILL_CARD_WAVES[2], icon: "Stat_PoisonChanceIncrease", name: "死亡强化++", info: "死亡触发技能次数+1", field: [7015] },
|
||||
{ uuid: 8813, wave: SKILL_CARD_WAVES[2], icon: "Stat_AttackRangeIncrease_01", name: "开场强化++", info: "战斗开始触发技能次数+1", field: [7016] },
|
||||
{ uuid: 8814, wave: SKILL_CARD_WAVES[2], icon: "Stat_UnitSummonIncrease_01", name: "结束强化++", info: "战斗结束触发技能次数+1", field: [7017] },
|
||||
{ uuid: 8815, wave: SKILL_CARD_WAVES[2], icon: "Stat_Attack_03", name: "攻击强化++", info: "攻击触发技能次数+1", field: [7018] },
|
||||
{ uuid: 8816, wave: SKILL_CARD_WAVES[2], icon: "Stat_Armor_01", name: "受击强化++", info: "被攻击触发技能次数+1", field: [7019] },
|
||||
{ uuid: 8811, wave: SKILL_CARD_WAVES[2], icon: "Wand-FA_WP_Main_Wand_004_WoodGold", name: "召唤强化++", info: "召唤触发技能次数+1", field: [7014] },
|
||||
{ uuid: 8812, wave: SKILL_CARD_WAVES[2], icon: "Staff-FA_WP_Main_Staff_005_WoodGold", name: "死亡强化++", info: "死亡触发技能次数+1", field: [7015] },
|
||||
{ uuid: 8813, wave: SKILL_CARD_WAVES[2], icon: "Spear-FA_WP_Main_Spear_008_GrayGold", name: "开场强化++", info: "战斗开始触发技能次数+1", field: [7016] },
|
||||
{ uuid: 8814, wave: SKILL_CARD_WAVES[2], icon: "Blunt-FA_WP_Main_Blunt_006_OrangeGray", name: "结束强化++", info: "战斗结束触发技能次数+1", field: [7017] },
|
||||
{ uuid: 8815, wave: SKILL_CARD_WAVES[2], icon: "Sword-FA_WP_Main_Sword_001_Gold", name: "攻击强化++", info: "攻击触发技能次数+1", field: [7018] },
|
||||
{ uuid: 8816, wave: SKILL_CARD_WAVES[2], icon: "Shield-FA_WP_Sub_Shield_009_RedGold", name: "受击强化++", info: "被攻击触发技能次数+1", field: [7019] },
|
||||
// 多词条装备
|
||||
{ uuid: 8820, wave: SKILL_CARD_WAVES[2], icon: "Stat_Attack_03", name: "神圣之刃", info: "英雄攻击力+30%, 暴击率+30%, 暴击伤害+60%, 攻速+30%", field: [7402, 7404, 7405, 7406] },
|
||||
{ uuid: 8820, wave: SKILL_CARD_WAVES[2], icon: "Sword-FA_WP_Main_Sword_001_Gold", name: "神圣之刃", info: "英雄攻击力+30%, 暴击率+30%, 暴击伤害+60%, 攻速+30%", field: [7402, 7404, 7405, 7406] },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,24 +33,24 @@ interface ItemCardRaw {
|
||||
|
||||
const ItemCardData: ItemCardRaw[] = [
|
||||
// ==================== 一次性回血(计时性 HoT,5s) ====================
|
||||
{ uuid: 9101, skill: 6502, icon: "Stat_PotionBoost", name: "再生药水", info: "全体友方每秒回复生命,持续 5 秒", cost: 0, weight: 20 },
|
||||
{ uuid: 9101, skill: 6502, icon: "Sub_Item-FA_WP_Sub_Potion_001_Green", name: "再生药水", info: "全体友方每秒回复生命,持续 5 秒", cost: 0, weight: 20 },
|
||||
|
||||
// ==================== 计时性强化(统一 5s) ====================
|
||||
{ uuid: 9102, skill: 6503, icon: "Stat_Attack_03", name: "攻击药水", info: "全体友方攻击力提升 50%,持续 5 秒", cost: 0, weight: 15 },
|
||||
{ uuid: 9103, skill: 6504, icon: "Stat_CriticalChance_02", name: "暴击药水", info: "全体友方暴击率提升 30%,持续 5 秒", cost: 0, weight: 15 },
|
||||
{ uuid: 9104, skill: 6505, icon: "Stat_Stun_01", name: "击退药水", info: "全体友方击退概率提升 30%,持续 5 秒", cost: 0, weight: 12 },
|
||||
{ uuid: 9105, skill: 6506, icon: "Stat_Tripleshot", name: "穿透药水", info: "全体友方穿透概率提升 30%,持续 5 秒", cost: 0, weight: 12 },
|
||||
{ uuid: 9106, skill: 6507, icon: "Stat_AttackSpeed_02", name: "攻速药水", info: "全体友方攻击速度提升 40%,持续 5 秒", cost: 0, weight: 15 },
|
||||
{ uuid: 9107, skill: 6508, icon: "Stat_CriticalComboChance", name: "风怒药水", info: "全体友方风怒概率提升 20%,持续 5 秒", cost: 0, weight: 12 },
|
||||
{ uuid: 9108, skill: 6509, icon: "Stat_Stun_01", name: "击晕药水", info: "全体友方击晕概率提升 30%,持续 5 秒", cost: 0, weight: 12 },
|
||||
{ uuid: 9109, skill: 6510, icon: "Stat_Armor_01", name: "击晕抗性药水", info: "全体友方击晕抗性提升 50%,持续 5 秒", cost: 4, weight: 10 },
|
||||
{ uuid: 9110, skill: 6511, icon: "Stat_Freeze", name: "冰冻抗性药水", info: "全体友方冰冻抗性提升 50%,持续 5 秒", cost: 4, weight: 10 },
|
||||
{ uuid: 9102, skill: 6503, icon: "Sub_Item-FA_WP_Sub_Potion_001_Red", name: "攻击药水", info: "全体友方攻击力提升 50%,持续 5 秒", cost: 0, weight: 15 },
|
||||
{ uuid: 9103, skill: 6504, icon: "Sub_Item-FA_WP_Sub_Potion_001_Yellow", name: "暴击药水", info: "全体友方暴击率提升 30%,持续 5 秒", cost: 0, weight: 15 },
|
||||
{ uuid: 9104, skill: 6505, icon: "Sub_Item-FA_WP_Sub_Potion_001_Purple", name: "击退药水", info: "全体友方击退概率提升 30%,持续 5 秒", cost: 0, weight: 12 },
|
||||
{ uuid: 9105, skill: 6506, icon: "Sub_Item-FA_WP_Sub_Potion_001_Blue", name: "穿透药水", info: "全体友方穿透概率提升 30%,持续 5 秒", cost: 0, weight: 12 },
|
||||
{ uuid: 9106, skill: 6507, icon: "Sub_Item-FA_WP_Sub_Potion_001_Red", name: "攻速药水", info: "全体友方攻击速度提升 40%,持续 5 秒", cost: 0, weight: 15 },
|
||||
{ uuid: 9107, skill: 6508, icon: "Sub_Item-FA_WP_Sub_Potion_001_Purple", name: "风怒药水", info: "全体友方风怒概率提升 20%,持续 5 秒", cost: 0, weight: 12 },
|
||||
{ uuid: 9108, skill: 6509, icon: "Sub_Item-FA_WP_Sub_Potion_001_Purple", name: "击晕药水", info: "全体友方击晕概率提升 30%,持续 5 秒", cost: 0, weight: 12 },
|
||||
{ uuid: 9109, skill: 6510, icon: "Sub_Item-FA_WP_Sub_Potion_001_Blue", name: "击晕抗性药水", info: "全体友方击晕抗性提升 50%,持续 5 秒", cost: 4, weight: 10 },
|
||||
{ uuid: 9110, skill: 6511, icon: "Sub_Item-FA_WP_Sub_Potion_001_Blue", name: "冰冻抗性药水", info: "全体友方冰冻抗性提升 50%,持续 5 秒", cost: 4, weight: 10 },
|
||||
|
||||
// ==================== 高级档(buff_value 覆写,复用同一技能底座) ====================
|
||||
{ uuid: 9201, skill: 6502, icon: "Stat_PotionBoost", name: "高级再生药水", info: "全体友方每秒回复 25 生命,持续 5 秒", cost: 0, weight: 8, overrides: { buff_value: 25 } },
|
||||
{ uuid: 9202, skill: 6503, icon: "Stat_Attack_03", name: "高级攻击药水", info: "全体友方攻击力提升 80%,持续 5 秒", cost: 0, weight: 8, overrides: { buff_value: 80 } },
|
||||
{ uuid: 9203, skill: 6504, icon: "Stat_CriticalChance_02", name: "高级暴击药水", info: "全体友方暴击率提升 50%,持续 5 秒", cost: 0, weight: 8, overrides: { buff_value: 50 } },
|
||||
{ uuid: 9206, skill: 6507, icon: "Stat_AttackSpeed_02", name: "高级攻速药水", info: "全体友方攻击速度提升 70%,持续 5 秒", cost: 0, weight: 8, overrides: { buff_value: 70 } },
|
||||
{ uuid: 9201, skill: 6502, icon: "Sub_Item-FA_WP_Sub_Potion_002_Green", name: "高级再生药水", info: "全体友方每秒回复 25 生命,持续 5 秒", cost: 0, weight: 8, overrides: { buff_value: 25 } },
|
||||
{ uuid: 9202, skill: 6503, icon: "Sub_Item-FA_WP_Sub_Potion_002_Red", name: "高级攻击药水", info: "全体友方攻击力提升 80%,持续 5 秒", cost: 0, weight: 8, overrides: { buff_value: 80 } },
|
||||
{ uuid: 9203, skill: 6504, icon: "Sub_Item-FA_WP_Sub_Potion_002_Yellow", name: "高级暴击药水", info: "全体友方暴击率提升 50%,持续 5 秒", cost: 0, weight: 8, overrides: { buff_value: 50 } },
|
||||
{ uuid: 9206, skill: 6507, icon: "Sub_Item-FA_WP_Sub_Potion_002_Red", name: "高级攻速药水", info: "全体友方攻击速度提升 70%,持续 5 秒", cost: 0, weight: 8, overrides: { buff_value: 70 } },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,18 +38,18 @@ const SkillCardData: SkillCardRaw[] = [
|
||||
// --- 范围攻击卡(ap 递增,间隔缩短) ---
|
||||
{ uuid: 8261, skill: 6201, card_lv: 2, icon: "Stat_LightningDamag", name: "雷墙+", info: "召唤雷墙阻挡敌人,有概率击晕", is_inst: false, t_times: 3, t_inv: 5, trigger_type: CardTriggerType.Interval, overrides: { ap: 150 } },
|
||||
{ uuid: 8262, skill: 6202, card_lv: 2, icon: "Stat_FireDamage", name: "火墙+", info: "召唤火墙阻挡敌人,有概率击晕", is_inst: false, t_times: 3, t_inv: 5, trigger_type: CardTriggerType.Interval, overrides: { ap: 150 } },
|
||||
{ uuid: 8263, skill: 6203, card_lv: 2, icon: "Stat_Stun_01", name: "飓风+", info: "召唤飓风攻击敌人,有概率击晕", is_inst: false, t_times: 3, t_inv: 5, trigger_type: CardTriggerType.Interval, overrides: { ap: 150 } },
|
||||
{ uuid: 8264, skill: 6204, card_lv: 2, icon: "Stat_Mana", name: "水墙+", info: "召唤水墙阻挡敌人,有概率击晕", is_inst: false, t_times: 3, t_inv: 5, trigger_type: CardTriggerType.Interval, overrides: { ap: 150 } },
|
||||
{ uuid: 8265, skill: 6205, card_lv: 2, icon: "Stat_Stun_01", name: "风墙+", info: "召唤风墙困住敌人,有概率击晕", is_inst: false, t_times: 3, t_inv: 5, trigger_type: CardTriggerType.Interval, overrides: { ap: 150 } },
|
||||
{ uuid: 8266, skill: 6206, card_lv: 2, icon: "Stat_Tripleshot", name: "陨石术+", info: "召唤陨石范围攻击敌人,有概率击晕", is_inst: false, t_times: 3, t_inv: 5, trigger_type: CardTriggerType.Interval, overrides: { ap: 150 } },
|
||||
{ uuid: 8263, skill: 6203, card_lv: 2, icon: "Stat_NatureDamage", name: "飓风+", info: "召唤飓风攻击敌人,有概率击晕", is_inst: false, t_times: 3, t_inv: 5, trigger_type: CardTriggerType.Interval, overrides: { ap: 150 } },
|
||||
{ uuid: 8264, skill: 6204, card_lv: 2, icon: "Stat_WaterDamage", name: "水墙+", info: "召唤水墙阻挡敌人,有概率击晕", is_inst: false, t_times: 3, t_inv: 5, trigger_type: CardTriggerType.Interval, overrides: { ap: 150 } },
|
||||
{ uuid: 8265, skill: 6205, card_lv: 2, icon: "Stat_NatureDamage", name: "风墙+", info: "召唤风墙困住敌人,有概率击晕", is_inst: false, t_times: 3, t_inv: 5, trigger_type: CardTriggerType.Interval, overrides: { ap: 150 } },
|
||||
{ uuid: 8266, skill: 6206, card_lv: 2, icon: "Stat_SplashDamage", name: "陨石术+", info: "召唤陨石范围攻击敌人,有概率击晕", is_inst: false, t_times: 3, t_inv: 5, trigger_type: CardTriggerType.Interval, overrides: { ap: 150 } },
|
||||
|
||||
// ==================== card_lv 3 档(强度 ×3) ====================
|
||||
{ uuid: 8361, skill: 6201, card_lv: 3, icon: "Stat_LightningDamag", name: "雷墙++", info: "召唤雷墙阻挡敌人,有概率击晕", is_inst: false, t_times: 4, t_inv: 4, trigger_type: CardTriggerType.Interval, overrides: { ap: 250 } },
|
||||
{ uuid: 8362, skill: 6202, card_lv: 3, icon: "Stat_FireDamage", name: "火墙++", info: "召唤火墙阻挡敌人,有概率击晕", is_inst: false, t_times: 4, t_inv: 4, trigger_type: CardTriggerType.Interval, overrides: { ap: 250 } },
|
||||
{ uuid: 8363, skill: 6203, card_lv: 3, icon: "Stat_Stun_01", name: "飓风++", info: "召唤飓风攻击敌人,有概率击晕", is_inst: false, t_times: 4, t_inv: 4, trigger_type: CardTriggerType.Interval, overrides: { ap: 250 } },
|
||||
{ uuid: 8364, skill: 6204, card_lv: 3, icon: "Stat_Mana", name: "水墙++", info: "召唤水墙阻挡敌人,有概率击晕", is_inst: false, t_times: 4, t_inv: 4, trigger_type: CardTriggerType.Interval, overrides: { ap: 250 } },
|
||||
{ uuid: 8365, skill: 6205, card_lv: 3, icon: "Stat_Stun_01", name: "风墙++", info: "召唤风墙困住敌人,有概率击晕", is_inst: false, t_times: 4, t_inv: 4, trigger_type: CardTriggerType.Interval, overrides: { ap: 250 } },
|
||||
{ uuid: 8366, skill: 6206, card_lv: 3, icon: "Stat_Tripleshot", name: "陨石术++", info: "召唤陨石范围攻击敌人,有概率击晕", is_inst: false, t_times: 4, t_inv: 4, trigger_type: CardTriggerType.Interval, overrides: { ap: 250 } },
|
||||
{ uuid: 8363, skill: 6203, card_lv: 3, icon: "Stat_NatureDamage", name: "飓风++", info: "召唤飓风攻击敌人,有概率击晕", is_inst: false, t_times: 4, t_inv: 4, trigger_type: CardTriggerType.Interval, overrides: { ap: 250 } },
|
||||
{ uuid: 8364, skill: 6204, card_lv: 3, icon: "Stat_WaterDamage", name: "水墙++", info: "召唤水墙阻挡敌人,有概率击晕", is_inst: false, t_times: 4, t_inv: 4, trigger_type: CardTriggerType.Interval, overrides: { ap: 250 } },
|
||||
{ uuid: 8365, skill: 6205, card_lv: 3, icon: "Stat_NatureDamage", name: "风墙++", info: "召唤风墙困住敌人,有概率击晕", is_inst: false, t_times: 4, t_inv: 4, trigger_type: CardTriggerType.Interval, overrides: { ap: 250 } },
|
||||
{ uuid: 8366, skill: 6206, card_lv: 3, icon: "Stat_SplashDamage", name: "陨石术++", info: "召唤陨石范围攻击敌人,有概率击晕", is_inst: false, t_times: 4, t_inv: 4, trigger_type: CardTriggerType.Interval, overrides: { ap: 250 } },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.bak
|
||||
@@ -1 +0,0 @@
|
||||
.tmp/
|
||||
@@ -1,25 +0,0 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { buildSkillDesc } from '../src/shared/desc/buildSkillDesc';
|
||||
import { RecordValue } from '../src/io/recordValue';
|
||||
|
||||
const skillSet: Record<number, RecordValue> = {
|
||||
6301: { kind: 'obj', props: { name: { kind: 'str', value: '护盾' }, kind: { kind: 'enumRef', qualifier: 'SkillKind', member: 'Shield' }, ap: { kind: 'num', value: 4 } } },
|
||||
};
|
||||
const fieldSet: Record<number, RecordValue> = {
|
||||
7015: { kind: 'obj', props: { name: { kind: 'str', value: '死亡强化' }, info: { kind: 'str', value: '死亡触发技能次数+1' } } },
|
||||
};
|
||||
const hero: RecordValue = { kind: 'obj', props: {
|
||||
atked: { kind: 'arr', items: [{ kind: 'obj', props: { s_uuid: { kind: 'num', value: 6301 }, t_num: { kind: 'num', value: 3 }, overrides: { kind: 'obj', props: { ap: { kind: 'num', value: 4 } } } } }] },
|
||||
field: { kind: 'arr', items: [{ kind: 'num', value: 7015 }] },
|
||||
} };
|
||||
|
||||
test('renders atked trigger with shield effect', () => {
|
||||
const out = buildSkillDesc(hero, skillSet, fieldSet);
|
||||
assert.match(out, /受击3次:护盾/);
|
||||
assert.match(out, /护盾4次/);
|
||||
});
|
||||
test('renders field aura', () => {
|
||||
const out = buildSkillDesc(hero, skillSet, fieldSet);
|
||||
assert.match(out, /场上存活:死亡强化 死亡触发技能次数\+1/);
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
// 测试夹具:镜像真实 heroSet.ts 片段(speed 表达式 + 枚举引用 + 触发槽 + 驻场 + 复活)
|
||||
export const HeroInfo: Record<number, any> = {
|
||||
5011:{uuid:5011,name:"小铁卫",path:"hk1", fac:FacSet.HERO,pool_lv:1,lv:1,type:HType.Melee,hp:400,ap:20,
|
||||
skills:{6001:{uuid:6001,lv:1,cd:AtkSpeedSet[AtkSpeedLv.Slow3].cd,ccd:0}},
|
||||
atked:[{s_uuid:6301,t_num:3,overrides:{TGroup:TGroup.Self,ap:4}}],
|
||||
field:[7015],
|
||||
revive:{s_uuid:6501,r_num:1,upr:0.3},
|
||||
info:"每受击3次为自身添加4层护盾"},
|
||||
6001:{uuid:6001,name:"兽人战士",path:"m1", fac:FacSet.MON,lv:1,type:HType.Melee,hp:220,ap:10,speed:70,
|
||||
skills:{6005:{uuid:6005,lv:1,cd:AtkSpeedSet[AtkSpeedLv.VerySlow1].cd,ccd:0}},info:"基础近战位怪"},
|
||||
};
|
||||
|
||||
export const HeroList: number[] = [5011];
|
||||
@@ -1,16 +0,0 @@
|
||||
// 测试夹具:镜像真实 SkillSet.ts 片段(含 SkillSet + FieldSkillSet)
|
||||
export const SkillSet: Record<number, any> = {
|
||||
6001:{uuid:6001,name:"火球",sp_name:"atk_1",icon:"Stat_Attack_01",TGroup:TGroup.Enemy,act:"atk",
|
||||
DTType:DTType.single,ap:100,hit_count:1,hitcd:0.2,speed:720,with:0,ready:0.2,
|
||||
IType:IType.Melee,RType:RType.bezier,EType:EType.collision,info:"造成攻击力100%的伤害"},
|
||||
6301:{uuid:6301,name:"护盾",sp_name:"buff_wind",icon:"Stat_Defense",TGroup:TGroup.Self,act:"atk",
|
||||
DTType:DTType.single,kind:SkillKind.Shield,ap:3,hit_count:1,hitcd:0.2,speed:720,with:0,ready:0.2,
|
||||
IType:IType.support,RType:RType.fixed,EType:EType.animationEnd,info:"添加护盾"},
|
||||
6501:{uuid:6501,name:"复活",sp_name:"buff_wind",icon:"Stat_HolyDamage",TGroup:TGroup.Self,act:"atk",
|
||||
DTType:DTType.single,kind:SkillKind.Support,ap:50,hit_count:3,hitcd:0.2,speed:720,with:0,ready:0.2,
|
||||
IType:IType.support,RType:RType.fixed,EType:EType.animationEnd,info:"复活百分比"},
|
||||
};
|
||||
|
||||
export const FieldSkillSet: Record<number, any> = {
|
||||
7015:{uuid:7015,name:"死亡强化",icon:"Stat_PoisonChanceIncrease",type:FieldSkillType.DeadCount,value:1,info:"死亡触发技能次数+1"},
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import * as ts from 'typescript';
|
||||
import { parseExpression, findExportObjectLiteral } from '../src/io/parser';
|
||||
|
||||
function parse(text: string): ts.Expression {
|
||||
const src = ts.createSourceFile('x.ts', `const _ = ${text};`, ts.ScriptTarget.Latest, true);
|
||||
const decl = src.statements[0] as ts.VariableStatement;
|
||||
return decl.declarationList.declarations[0].initializer!;
|
||||
}
|
||||
|
||||
test('num / str / bool', () => {
|
||||
assert.deepEqual(parseExpression(parse('400')), { kind: 'num', value: 400 });
|
||||
assert.deepEqual(parseExpression(parse('"小铁卫"')), { kind: 'str', value: '小铁卫' });
|
||||
assert.deepEqual(parseExpression(parse('true')), { kind: 'bool', value: true });
|
||||
});
|
||||
test('enumRef: FacSet.HERO', () => {
|
||||
assert.deepEqual(parseExpression(parse('FacSet.HERO')), { kind: 'enumRef', qualifier: 'FacSet', member: 'HERO' });
|
||||
});
|
||||
test('speed: AtkSpeedSet[AtkSpeedLv.Slow3].cd', () => {
|
||||
assert.deepEqual(parseExpression(parse('AtkSpeedSet[AtkSpeedLv.Slow3].cd')), { kind: 'speed', level: 'Slow3' });
|
||||
});
|
||||
test('non-Speed two-segment access falls back to enumRef', () => {
|
||||
assert.deepEqual(parseExpression(parse('Foo.bar')), { kind: 'enumRef', qualifier: 'Foo', member: 'bar' });
|
||||
});
|
||||
test('arr', () => {
|
||||
assert.deepEqual(parseExpression(parse('[1,2,3]')), { kind: 'arr', items: [
|
||||
{ kind: 'num', value: 1 }, { kind: 'num', value: 2 }, { kind: 'num', value: 3 }] });
|
||||
});
|
||||
test('obj with numeric + identifier keys', () => {
|
||||
const v = parseExpression(parse('{6001:{uuid:6001},name:"x"}'));
|
||||
assert.equal(v.kind, 'obj');
|
||||
assert.deepEqual(v.props['6001'], { kind: 'obj', props: { uuid: { kind: 'num', value: 6001 } } });
|
||||
assert.deepEqual(v.props['name'], { kind: 'str', value: 'x' });
|
||||
});
|
||||
test('raw: unsupported expression preserved verbatim', () => {
|
||||
const v = parseExpression(parse('a + b'));
|
||||
assert.equal(v.kind, 'raw');
|
||||
assert.equal((v as any).text, 'a + b');
|
||||
});
|
||||
test('findExportObjectLiteral locates HeroInfo', () => {
|
||||
const src = ts.createSourceFile('x.ts',
|
||||
`export const HeroInfo: Record<number, any> = { 5011:{uuid:5011} };`, ts.ScriptTarget.Latest, true);
|
||||
const node = findExportObjectLiteral(src, 'HeroInfo');
|
||||
assert.ok(node);
|
||||
assert.equal(node!.properties.length, 1);
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { serializeValue, serializeEntry } from '../src/io/serializer';
|
||||
import { RecordValue } from '../src/io/recordValue';
|
||||
|
||||
test('serializeValue: num/str/bool', () => {
|
||||
assert.equal(serializeValue({ kind: 'num', value: 400 }), '400');
|
||||
assert.equal(serializeValue({ kind: 'str', value: '小铁卫' }), '"小铁卫"');
|
||||
assert.equal(serializeValue({ kind: 'bool', value: true }), 'true');
|
||||
});
|
||||
test('serializeValue: enumRef', () => {
|
||||
const v: RecordValue = { kind: 'enumRef', qualifier: 'FacSet', member: 'HERO' };
|
||||
assert.equal(serializeValue(v), 'FacSet.HERO');
|
||||
});
|
||||
test('serializeValue: speed', () => {
|
||||
const v: RecordValue = { kind: 'speed', level: 'Slow3' };
|
||||
assert.equal(serializeValue(v), 'AtkSpeedSet[AtkSpeedLv.Slow3].cd');
|
||||
});
|
||||
test('serializeValue: arr', () => {
|
||||
const v: RecordValue = { kind: 'arr', items: [{ kind: 'num', value: 1 }, { kind: 'num', value: 2 }] };
|
||||
assert.equal(serializeValue(v), '[1,2]');
|
||||
});
|
||||
test('serializeValue: obj', () => {
|
||||
const v: RecordValue = { kind: 'obj', props: { a: { kind: 'num', value: 1 }, b: { kind: 'str', value: 'x' } } };
|
||||
assert.equal(serializeValue(v), '{a:1,b:"x"}');
|
||||
});
|
||||
test('serializeValue: raw passthrough', () => {
|
||||
const v: RecordValue = { kind: 'raw', text: 'a + b' };
|
||||
assert.equal(serializeValue(v), 'a + b');
|
||||
});
|
||||
test('serializeEntry: all-scalar one line with trailing comma', () => {
|
||||
const v: RecordValue = { kind: 'obj', props: { uuid: { kind: 'num', value: 1 }, name: { kind: 'str', value: 'x' } } };
|
||||
assert.equal(serializeEntry('1', v), '1:{uuid:1,name:"x"},');
|
||||
});
|
||||
test('serializeEntry: nested on continuation lines', () => {
|
||||
const v: RecordValue = { kind: 'obj', props: {
|
||||
uuid: { kind: 'num', value: 5011 },
|
||||
skills: { kind: 'obj', props: { 6001: { kind: 'obj', props: { uuid: { kind: 'num', value: 6001 } } } } },
|
||||
} };
|
||||
const out = serializeEntry('5011', v);
|
||||
assert.match(out, /^5011:\{uuid:5011,$/m); // 首行:键 + 标量
|
||||
assert.match(out, / skills:\{6001:\{uuid:6001\}\},$/m); // 续行 8 空格
|
||||
assert.match(out, /^ \},$/m); // 闭合 4 空格
|
||||
});
|
||||
@@ -1,101 +0,0 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, copyFileSync, mkdtempSync, readdirSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { TsConfigFile } from '../src/io/TsConfigFile';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const fixture = join(here, 'fixtures', 'heroSet.sample.ts');
|
||||
|
||||
test('load + getKeys', () => {
|
||||
const f = new TsConfigFile(fixture, 'HeroInfo');
|
||||
f.load();
|
||||
assert.deepEqual(f.getKeys(), ['5011', '6001']);
|
||||
});
|
||||
test('read returns structured value with speed/enumRef preserved', () => {
|
||||
const f = new TsConfigFile(fixture, 'HeroInfo');
|
||||
f.load();
|
||||
const v = f.read('5011')!;
|
||||
assert.equal(v.kind, 'obj');
|
||||
assert.deepEqual(v.props['fac'], { kind: 'enumRef', qualifier: 'FacSet', member: 'HERO' });
|
||||
const skill = v.props['skills'].props['6001'];
|
||||
assert.deepEqual(skill.props['cd'], { kind: 'speed', level: 'Slow3' });
|
||||
});
|
||||
test('getText after load equals original file (no mutation on load)', () => {
|
||||
const f = new TsConfigFile(fixture, 'HeroInfo');
|
||||
f.load();
|
||||
assert.equal(f.getText(), readFileSync(fixture, 'utf8'));
|
||||
});
|
||||
test('read missing key returns null', () => {
|
||||
const f = new TsConfigFile(fixture, 'HeroInfo');
|
||||
f.load();
|
||||
assert.equal(f.read('9999'), null);
|
||||
});
|
||||
|
||||
function withTempFixture(): { dir: string; file: string } {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'phcfg-'));
|
||||
const file = join(dir, 'heroSet.sample.ts');
|
||||
copyFileSync(fixture, file);
|
||||
return { dir, file };
|
||||
}
|
||||
|
||||
test('patch updates one entry; reload reads new value; other entries intact', () => {
|
||||
const { file } = withTempFixture();
|
||||
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
|
||||
const v = f.read('5011')!;
|
||||
v.props['ap'] = { kind: 'num', value: 99 };
|
||||
f.patch('5011', v);
|
||||
assert.equal(f.isDirty(), true);
|
||||
assert.equal(f.save().ok, true);
|
||||
const f2 = new TsConfigFile(file, 'HeroInfo'); f2.load();
|
||||
assert.deepEqual(f2.read('5011')!.props['ap'], { kind: 'num', value: 99 });
|
||||
assert.equal(f2.read('6001')!.props['name'].value, '兽人战士'); // 另一条未破坏
|
||||
});
|
||||
|
||||
test('patch preserves speed + enumRef on reload', () => {
|
||||
const { file } = withTempFixture();
|
||||
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
|
||||
const v = f.read('5011')!;
|
||||
v.props['hp'] = { kind: 'num', value: 500 };
|
||||
f.patch('5011', v); f.save();
|
||||
const f2 = new TsConfigFile(file, 'HeroInfo'); f2.load();
|
||||
const again = f2.read('5011')!;
|
||||
assert.deepEqual(again.props['fac'], { kind: 'enumRef', qualifier: 'FacSet', member: 'HERO' });
|
||||
assert.deepEqual(again.props['skills'].props['6001'].props['cd'], { kind: 'speed', level: 'Slow3' });
|
||||
});
|
||||
|
||||
test('add appends entry readable after save+reload', () => {
|
||||
const { file } = withTempFixture();
|
||||
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
|
||||
f.add('5099', { kind: 'obj', props: { uuid: { kind: 'num', value: 5099 }, name: { kind: 'str', value: '新英雄' } } });
|
||||
f.save();
|
||||
const f2 = new TsConfigFile(file, 'HeroInfo'); f2.load();
|
||||
assert.ok(f2.getKeys().includes('5099'));
|
||||
assert.equal(f2.read('5099')!.props['name'].value, '新英雄');
|
||||
});
|
||||
|
||||
test('delete removes entry', () => {
|
||||
const { file } = withTempFixture();
|
||||
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
|
||||
f.delete('6001'); f.save();
|
||||
const f2 = new TsConfigFile(file, 'HeroInfo'); f2.load();
|
||||
assert.ok(!f2.getKeys().includes('6001'));
|
||||
assert.ok(f2.getKeys().includes('5011'));
|
||||
});
|
||||
|
||||
test('save writes a .bak backup of the pre-save file', () => {
|
||||
const { file } = withTempFixture();
|
||||
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
|
||||
const v = f.read('5011')!; v.props['hp'] = { kind: 'num', value: 1 }; f.patch('5011', v);
|
||||
f.save();
|
||||
assert.equal(readFileSync(file + '.bak', 'utf8'), readFileSync(fixture, 'utf8'));
|
||||
});
|
||||
|
||||
test('save with no edits is a no-op (ok, no .bak written)', () => {
|
||||
const { file, dir } = withTempFixture();
|
||||
const f = new TsConfigFile(file, 'HeroInfo'); f.load();
|
||||
assert.equal(f.save().ok, true);
|
||||
assert.equal(readdirSync(dir).some(p => p.endsWith('.bak')), false);
|
||||
});
|
||||
@@ -1,67 +0,0 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { validate } from '../src/shared/validation';
|
||||
import { RecordValue } from '../src/io/recordValue';
|
||||
|
||||
const ctx = {
|
||||
hasSkill: (u: number) => [6001, 6301, 6501].includes(u),
|
||||
hasField: (u: number) => [7015].includes(u),
|
||||
hasHero: (u: number) => [5011, 6001].includes(u),
|
||||
heroListKeys: new Set<string>(['5011']),
|
||||
};
|
||||
|
||||
function heroObj(extra: Record<string, RecordValue> = {}): RecordValue {
|
||||
return { kind: 'obj', props: {
|
||||
uuid: { kind: 'num', value: 5011 }, name: { kind: 'str', value: 'x' }, path: { kind: 'str', value: 'p' },
|
||||
fac: { kind: 'enumRef', qualifier: 'FacSet', member: 'HERO' }, lv: { kind: 'num', value: 1 },
|
||||
type: { kind: 'enumRef', qualifier: 'HType', member: 'Melee' }, hp: { kind: 'num', value: 400 },
|
||||
ap: { kind: 'num', value: 20 },
|
||||
skills: { kind: 'obj', props: { 6001: { kind: 'obj', props: { uuid: { kind: 'num', value: 6001 }, lv: { kind: 'num', value: 1 }, cd: { kind: 'speed', level: 'Slow3' }, ccd: { kind: 'num', value: 0 } } } } },
|
||||
info: { kind: 'str', value: 'ok' },
|
||||
...extra,
|
||||
} };
|
||||
}
|
||||
|
||||
test('valid hero → no errors', () => {
|
||||
const issues = validate('hero', new Map([['5011', heroObj()]]), ctx);
|
||||
assert.equal(issues.filter(i => i.severity === 'error').length, 0);
|
||||
});
|
||||
test('duplicate uuid → error', () => {
|
||||
// 两条记录携带相同的 uuid 字段值(Map 键不同,uuid 字段相同)
|
||||
const dup = heroObj(); // uuid=5011
|
||||
const issues = validate('hero', new Map([['5011', heroObj()], ['5012', dup]]), ctx);
|
||||
assert.ok(issues.some(i => i.severity === 'error' && i.code === 'dup-uuid'));
|
||||
});
|
||||
test('missing required field → error', () => {
|
||||
const h = heroObj(); delete h.props['name'];
|
||||
const issues = validate('hero', new Map([['5011', h]]), ctx);
|
||||
assert.ok(issues.some(i => i.code === 'missing-required' && i.fieldPath === 'name'));
|
||||
});
|
||||
test('trigger slot references missing skill → error', () => {
|
||||
const h = heroObj({ atked: { kind: 'arr', items: [{ kind: 'obj', props: {
|
||||
s_uuid: { kind: 'num', value: 9999 }, t_num: { kind: 'num', value: 3 } } }] } });
|
||||
const issues = validate('hero', new Map([['5011', h]]), ctx);
|
||||
assert.ok(issues.some(i => i.code === 'dangling-ref'));
|
||||
});
|
||||
test('field list references missing field skill → error', () => {
|
||||
const h = heroObj({ field: { kind: 'arr', items: [{ kind: 'num', value: 8888 }] } });
|
||||
const issues = validate('hero', new Map([['5011', h]]), ctx);
|
||||
assert.ok(issues.some(i => i.code === 'dangling-ref'));
|
||||
});
|
||||
test('HeroList/hero consistency: fac=HERO entry missing from HeroList → error', () => {
|
||||
const ctx2 = { ...ctx, heroListKeys: new Set<string>([]) };
|
||||
const issues = validate('hero', new Map([['5011', heroObj()]]), ctx2);
|
||||
assert.ok(issues.some(i => i.code === 'herolist-inconsistent'));
|
||||
});
|
||||
test('invalid enum value → error', () => {
|
||||
const h = heroObj({ fac: { kind: 'enumRef', qualifier: 'FacSet', member: 'NOPE' } });
|
||||
const issues = validate('hero', new Map([['5011', h]]), ctx);
|
||||
assert.ok(issues.some(i => i.code === 'bad-enum'));
|
||||
});
|
||||
test('unknown overrides key → error', () => {
|
||||
const h = heroObj({ atked: { kind: 'arr', items: [{ kind: 'obj', props: {
|
||||
s_uuid: { kind: 'num', value: 6301 }, t_num: { kind: 'num', value: 3 },
|
||||
overrides: { kind: 'obj', props: { bogus: { kind: 'num', value: 1 } } } } }] } });
|
||||
const issues = validate('hero', new Map([['5011', h]]), ctx);
|
||||
assert.ok(issues.some(i => i.code === 'bad-override-key'));
|
||||
});
|
||||
@@ -1,29 +0,0 @@
|
||||
import { build, context } from 'esbuild';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const watch = process.argv.includes('--watch');
|
||||
|
||||
const common = {
|
||||
bundle: true,
|
||||
sourcemap: false,
|
||||
logLevel: 'info',
|
||||
alias: { 'vue': 'vue/dist/vue.esm-bundler.js' },
|
||||
};
|
||||
|
||||
// 面板进程在 Cocos 的 Electron 渲染层运行,可访问 Node 内建(fs/path),但 vue 必须
|
||||
// 打进浏览器侧 IIFE。因此面板项用 platform:'browser' + 把 node: 内建标为 external,
|
||||
// 交由运行时解析;这样 esbuild 既不抱怨,又保持 plan 的"运行时读 static 文件"语义。
|
||||
const entries = [
|
||||
{ entryPoints: ['src/main/index.ts'], outfile: 'dist/main.js', platform: 'node', format: 'cjs', external: [] },
|
||||
{ entryPoints: ['src/panels/default/index.ts'], outfile: 'dist/panels/default.js', platform: 'browser', format: 'iife', external: ['node:fs', 'node:path'] },
|
||||
];
|
||||
|
||||
if (watch) {
|
||||
for (const e of entries) {
|
||||
const ctx = await context({ ...common, ...e });
|
||||
await ctx.watch();
|
||||
}
|
||||
} else {
|
||||
for (const e of entries) await build({ ...common, ...e });
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
exports.en = {
|
||||
'pixelhero-config-editor': { description: 'Hero/Skill Config Editor', title: 'Hero/Skill Config' },
|
||||
'menu': { 'panel/英雄技能配置': 'Hero/Skill Config' },
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
exports.zh = {
|
||||
'pixelhero-config-editor': { description: '英雄/技能配置编辑器', title: '英雄/技能配置' },
|
||||
'menu': { 'panel/英雄技能配置': '英雄技能配置' },
|
||||
};
|
||||
1312
extensions/pixelhero-config-editor/package-lock.json
generated
1312
extensions/pixelhero-config-editor/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"package_version": 2,
|
||||
"name": "pixelhero-config-editor",
|
||||
"version": "0.1.0",
|
||||
"description": "英雄/技能配置编辑器",
|
||||
"main": "./dist/main.js",
|
||||
"author": "pixelhero",
|
||||
"editor": ">=3.8.0",
|
||||
"scripts": {
|
||||
"build": "node esbuild.config.mjs",
|
||||
"watch": "node esbuild.config.mjs --watch",
|
||||
"test": "tsx --test __tests__/*.test.ts"
|
||||
},
|
||||
"panels": {
|
||||
"default": {
|
||||
"title": "英雄/技能配置",
|
||||
"type": "dockable",
|
||||
"main": "./dist/panels/default.js",
|
||||
"size": { "width": 960, "height": 640, "min-width": 640, "min-height": 480 }
|
||||
}
|
||||
},
|
||||
"contributions": {
|
||||
"menu": [
|
||||
{ "path": "PixelHero/英雄技能配置", "message": "open-panel" }
|
||||
],
|
||||
"messages": {
|
||||
"open-panel": { "methods": ["open-panel"] },
|
||||
"query-schema": { "methods": ["query-schema"] },
|
||||
"query-enums": { "methods": ["query-enums"] },
|
||||
"query-keys": { "methods": ["query-keys"] },
|
||||
"query-record": { "methods": ["query-record"] },
|
||||
"query-preview-desc": { "methods": ["query-preview-desc"] },
|
||||
"validate": { "methods": ["validate"] },
|
||||
"save-record": { "methods": ["save-record"] },
|
||||
"revert-record": { "methods": ["revert-record"] }
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"typescript": "^5.4.0",
|
||||
"vue": "^3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"esbuild": "^0.20.0",
|
||||
"fs-extra": "^11.0.0",
|
||||
"tsx": "^4.7.0"
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as ts from 'typescript';
|
||||
import { RecordValue } from './recordValue';
|
||||
import { findExportObjectLiteral, parseExpression } from './parser';
|
||||
import { serializeEntry } from './serializer';
|
||||
|
||||
/**
|
||||
* 以"条目级 AST 区间替换"方式读写一个 `export const <name>: Record<number,X> = {...}` 配置文件。
|
||||
* 保留符号表达式(speed)与枚举引用(enumRef),未识别表达式原样保留(raw)。
|
||||
* save() 纯逻辑:仅做 fs 读写 + 语法校验(写 .bak);asset-db 刷新由主进程在 save 成功后调用。
|
||||
*/
|
||||
export class TsConfigFile {
|
||||
private sourceText = '';
|
||||
private sourceFile!: ts.SourceFile;
|
||||
private targetNode!: ts.ObjectLiteralExpression;
|
||||
private dirty = false;
|
||||
|
||||
constructor(public readonly filePath: string, public readonly exportName: string) {}
|
||||
|
||||
load(): void {
|
||||
this.sourceText = fs.readFileSync(this.filePath, 'utf8');
|
||||
this.reparse();
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
private reparse(): void {
|
||||
// 第 4 参数 setParentNodes 必须为 true,否则 node.getStart()/getEnd() 不可用。
|
||||
this.sourceFile = ts.createSourceFile(this.filePath, this.sourceText, ts.ScriptTarget.Latest, true);
|
||||
const node = findExportObjectLiteral(this.sourceFile, this.exportName);
|
||||
if (!node) throw new Error(`export const ${this.exportName} not found or not an object literal in ${this.filePath}`);
|
||||
this.targetNode = node;
|
||||
}
|
||||
|
||||
getKeys(): string[] {
|
||||
return this.targetNode.properties.map(p => (p.name as ts.PropertyName).getText());
|
||||
}
|
||||
|
||||
read(key: string): RecordValue | null {
|
||||
const entry = this.findEntry(key);
|
||||
if (!entry) return null;
|
||||
return parseExpression(entry.initializer);
|
||||
}
|
||||
|
||||
private findEntry(key: string): ts.PropertyAssignment | undefined {
|
||||
return this.targetNode.properties.find(p => (p.name as ts.PropertyName).getText() === key) as ts.PropertyAssignment | undefined;
|
||||
}
|
||||
|
||||
getText(): string { return this.sourceText; }
|
||||
isDirty(): boolean { return this.dirty; }
|
||||
|
||||
patch(key: string, value: RecordValue): void {
|
||||
const entry = this.findEntry(key);
|
||||
const newText = serializeEntry(key, value);
|
||||
if (entry) {
|
||||
const { start, end } = this.entrySpan(entry);
|
||||
this.sourceText = this.sourceText.slice(0, start) + newText + this.sourceText.slice(end);
|
||||
} else {
|
||||
this.insertEntry(newText);
|
||||
}
|
||||
this.reparse();
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
add(key: string, value: RecordValue): void {
|
||||
if (this.findEntry(key)) throw new Error(`key ${key} already exists`);
|
||||
this.insertEntry(serializeEntry(key, value));
|
||||
this.reparse();
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
delete(key: string): void {
|
||||
const entry = this.findEntry(key);
|
||||
if (!entry) return;
|
||||
const { start, end } = this.entrySpan(entry);
|
||||
this.sourceText = this.sourceText.slice(0, start) + this.sourceText.slice(end);
|
||||
this.reparse();
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
save(): { ok: true } | { ok: false; error: string } {
|
||||
if (!this.dirty) return { ok: true };
|
||||
const check = ts.createSourceFile(this.filePath, this.sourceText, ts.ScriptTarget.Latest, true);
|
||||
if (check.parseDiagnostics.length > 0) {
|
||||
const msg = check.parseDiagnostics.map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n')).join('; ');
|
||||
return { ok: false, error: `syntax error after edit: ${msg}` };
|
||||
}
|
||||
// .bak 取自磁盘原始内容(落盘前的当前文件),保证可回滚。
|
||||
fs.writeFileSync(this.filePath + '.bak', fs.readFileSync(this.filePath));
|
||||
fs.writeFileSync(this.filePath, this.sourceText);
|
||||
this.dirty = false;
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
/** 条目文本区间:从 key 词法起始(不含前导缩进 trivia,避免吞掉上一行换行)到尾随逗号止。 */
|
||||
private entrySpan(entry: ts.PropertyAssignment): { start: number; end: number } {
|
||||
const start = entry.getStart();
|
||||
let end = entry.getEnd();
|
||||
const comma = /^\s*,/.exec(this.sourceText.slice(end));
|
||||
if (comma) end += comma[0].length;
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
/** 在闭合 `}` 前追加一条新记录(4 空格缩进)。 */
|
||||
private insertEntry(entryText: string): void {
|
||||
const closeBrace = this.targetNode.getLastToken();
|
||||
if (!closeBrace) throw new Error('object literal has no closing brace');
|
||||
const pos = closeBrace.getStart();
|
||||
this.sourceText = this.sourceText.slice(0, pos) + ` ${entryText}\n ` + this.sourceText.slice(pos);
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import * as ts from 'typescript';
|
||||
import { RecordValue } from './recordValue';
|
||||
|
||||
/** 在 SourceFile 中查找 `export const <name> = {...}`,返回其对象字面量节点。 */
|
||||
export function findExportObjectLiteral(src: ts.SourceFile, name: string): ts.ObjectLiteralExpression | null {
|
||||
let result: ts.ObjectLiteralExpression | null = null;
|
||||
const visit = (node: ts.Node) => {
|
||||
if (result) return;
|
||||
if (ts.isVariableStatement(node)) {
|
||||
for (const d of node.declarationList.declarations) {
|
||||
if (d.name.getText() === name && d.initializer && ts.isObjectLiteralExpression(d.initializer)) {
|
||||
result = d.initializer;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(src);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 识别 `AtkSpeedSet[AtkSpeedLv.<level>].cd`,否则 null。 */
|
||||
function tryParseSpeedExpr(node: ts.PropertyAccessExpression): RecordValue | null {
|
||||
if (node.name.text !== 'cd') return null;
|
||||
const inner = node.expression;
|
||||
if (!ts.isElementAccessExpression(inner)) return null;
|
||||
if (!ts.isIdentifier(inner.expression) || inner.expression.text !== 'AtkSpeedSet') return null;
|
||||
const arg = inner.argumentExpression;
|
||||
if (!ts.isPropertyAccessExpression(arg)) return null;
|
||||
if (!ts.isIdentifier(arg.expression) || arg.expression.text !== 'AtkSpeedLv') return null;
|
||||
return { kind: 'speed', level: arg.name.text };
|
||||
}
|
||||
|
||||
export function parseExpression(node: ts.Expression): RecordValue {
|
||||
if (ts.isNumericLiteral(node)) return { kind: 'num', value: Number(node.text) };
|
||||
if (ts.isStringLiteral(node)) return { kind: 'str', value: node.text };
|
||||
if (node.kind === ts.SyntaxKind.TrueKeyword) return { kind: 'bool', value: true };
|
||||
if (node.kind === ts.SyntaxKind.FalseKeyword) return { kind: 'bool', value: false };
|
||||
|
||||
if (ts.isPropertyAccessExpression(node)) {
|
||||
const speed = tryParseSpeedExpr(node);
|
||||
if (speed) return speed;
|
||||
if (ts.isIdentifier(node.expression)) {
|
||||
return { kind: 'enumRef', qualifier: node.expression.text, member: node.name.text };
|
||||
}
|
||||
return { kind: 'raw', text: node.getText() }; // 多级 a.b.c 等
|
||||
}
|
||||
|
||||
if (ts.isArrayLiteralExpression(node)) {
|
||||
return { kind: 'arr', items: node.elements.map(e => parseExpression(e)) };
|
||||
}
|
||||
|
||||
if (ts.isObjectLiteralExpression(node)) {
|
||||
const props: Record<string, RecordValue> = {};
|
||||
for (const m of node.properties) {
|
||||
if (ts.isPropertyAssignment(m)) {
|
||||
props[m.name.getText()] = parseExpression(m.initializer);
|
||||
}
|
||||
}
|
||||
return { kind: 'obj', props };
|
||||
}
|
||||
|
||||
return { kind: 'raw', text: node.getText() }; // 二元/模板/调用/其他元素访问
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
/** IO 层对配置对象字面量值的结构化表示。零依赖。 */
|
||||
export type RecordValue =
|
||||
| { kind: 'num'; value: number }
|
||||
| { kind: 'str'; value: string }
|
||||
| { kind: 'bool'; value: boolean }
|
||||
| { kind: 'enumRef'; qualifier: string; member: string } // 如 FacSet.HERO
|
||||
| { kind: 'speed'; level: string } // AtkSpeedSet[AtkSpeedLv.X].cd
|
||||
| { kind: 'obj'; props: Record<string, RecordValue> }
|
||||
| { kind: 'arr'; items: RecordValue[] }
|
||||
| { kind: 'raw'; text: string }; // 未识别表达式,原样保留
|
||||
|
||||
export function isScalar(v: RecordValue): boolean {
|
||||
return v.kind === 'num' || v.kind === 'str' || v.kind === 'bool'
|
||||
|| v.kind === 'enumRef' || v.kind === 'speed' || v.kind === 'raw';
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { RecordValue, isScalar } from './recordValue';
|
||||
|
||||
export function serializeValue(v: RecordValue): string {
|
||||
switch (v.kind) {
|
||||
case 'num': return String(v.value);
|
||||
case 'str': return JSON.stringify(v.value);
|
||||
case 'bool': return String(v.value);
|
||||
case 'enumRef': return `${v.qualifier}.${v.member}`;
|
||||
case 'speed': return `AtkSpeedSet[AtkSpeedLv.${v.level}].cd`;
|
||||
case 'arr': return `[${v.items.map(serializeValue).join(',')}]`;
|
||||
case 'obj': {
|
||||
const body = Object.entries(v.props).map(([k, val]) => `${k}:${serializeValue(val)}`).join(',');
|
||||
return `{${body}}`;
|
||||
}
|
||||
case 'raw': return v.text;
|
||||
}
|
||||
}
|
||||
|
||||
/** 序列化一条顶层记录。首行无缩进(缩进由调用处保留的 trivia 提供);嵌套字段 8 空格续行;闭合 4 空格。 */
|
||||
export function serializeEntry(key: string, obj: RecordValue): string {
|
||||
if (obj.kind !== 'obj') return `${key}:${serializeValue(obj)},`;
|
||||
const props = obj.props;
|
||||
const keys = Object.keys(props);
|
||||
const scalars = keys.filter(k => isScalar(props[k]));
|
||||
const nested = keys.filter(k => !isScalar(props[k]));
|
||||
const scalarTxt = scalars.map(k => `${k}:${serializeValue(props[k])}`).join(',');
|
||||
if (nested.length === 0) return `${key}:{${scalarTxt}},`;
|
||||
const lines = [`${key}:{${scalarTxt},`];
|
||||
for (const k of nested) lines.push(` ${k}:${serializeValue(props[k])},`);
|
||||
lines.push(' },');
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { store } from './store';
|
||||
|
||||
/**
|
||||
* Cocos Creator 3.8 扩展主进程入口。
|
||||
* 关键约定(与官方 first/message 文档一致):
|
||||
* - 处理函数必须挂在 `methods` 对象里(Cocos 读取 module.exports.methods),
|
||||
* methods 的 key 必须与 package.json 中 contributions.messages[*].methods 引用的名字一致。
|
||||
* - 生命周期钩子为 `load`/`unload`(不是 onLoad)。
|
||||
* - 处理函数直接接收消息参数(无 event),返回值即 Editor.Message.request 的 resolve 结果。
|
||||
*/
|
||||
function load() {
|
||||
store.reloadAll();
|
||||
}
|
||||
function unload() {
|
||||
// 进程级单例随扩展卸载自然销毁,无需显式清理。
|
||||
}
|
||||
|
||||
const methods: Record<string, (...args: any[]) => any> = {
|
||||
'open-panel'() {
|
||||
Editor.Panel.open('pixelhero-config-editor');
|
||||
},
|
||||
'query-schema'(id?: string) {
|
||||
return store.querySchema(id as any);
|
||||
},
|
||||
'query-enums'() {
|
||||
return store.queryEnums();
|
||||
},
|
||||
'query-keys'(id: string) {
|
||||
return store.queryKeys(id as any);
|
||||
},
|
||||
'query-record'(id: string, key: string) {
|
||||
return store.queryRecord(id as any, key);
|
||||
},
|
||||
'query-preview-desc'(hero: any) {
|
||||
return store.queryPreviewDesc(hero);
|
||||
},
|
||||
'validate'(id: string) {
|
||||
return store.validate(id as any);
|
||||
},
|
||||
'save-record'(id: string, key: string, value: any) {
|
||||
return store.saveRecord(id as any, key, value);
|
||||
},
|
||||
'revert-record'(id: string, key: string) {
|
||||
return store.revertRecord(id as any, key);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { methods, load, unload };
|
||||
@@ -1,85 +0,0 @@
|
||||
import { join } from 'node:path';
|
||||
import { TsConfigFile } from '../io/TsConfigFile';
|
||||
import { allSchemas } from '../shared/schema/registry';
|
||||
import { ENUMS } from '../shared/schema/enums';
|
||||
import { validate, ValidationContext } from '../shared/validation';
|
||||
import { buildSkillDesc } from '../shared/desc/buildSkillDesc';
|
||||
import { RecordValue } from '../io/recordValue';
|
||||
import { TableId } from '../shared/schema/types';
|
||||
|
||||
/** 游戏配置目录(相对项目根)。主进程用 Editor.Project.path 解析到项目根的 assets 配置目录。 */
|
||||
const CONFIG_DIR = join(Editor.Project.path, 'assets/script/game/common/config');
|
||||
|
||||
interface Entry { file: TsConfigFile; }
|
||||
const tables: Partial<Record<TableId, Entry>> = {};
|
||||
// skill 与 field 共享 SkillSet.ts,但用不同 exportName 的 TsConfigFile 实例
|
||||
const fileCache: Record<string, TsConfigFile> = {};
|
||||
|
||||
function getTable(id: TableId): Entry {
|
||||
if (tables[id]) return tables[id]!;
|
||||
const schema = allSchemas().find(s => s.id === id)!;
|
||||
const cacheKey = `${schema.sourceFile}:${schema.exportName}`;
|
||||
let file = fileCache[cacheKey];
|
||||
if (!file) { file = new TsConfigFile(join(CONFIG_DIR, schema.sourceFile), schema.exportName); file.load(); fileCache[cacheKey] = file; }
|
||||
const entry = { file };
|
||||
tables[id] = entry;
|
||||
return entry;
|
||||
}
|
||||
|
||||
function recordsOf(id: TableId): Map<string, RecordValue> {
|
||||
const { file } = getTable(id);
|
||||
const m = new Map<string, RecordValue>();
|
||||
for (const k of file.getKeys()) { const v = file.read(k); if (v) m.set(k, v); }
|
||||
return m;
|
||||
}
|
||||
|
||||
function buildContext(): ValidationContext {
|
||||
const hero = recordsOf('hero');
|
||||
const skill = recordsOf('skill');
|
||||
const field = recordsOf('field');
|
||||
const heroKeys = Array.from(hero.keys()).map(Number);
|
||||
const skillKeys = Array.from(skill.keys()).map(Number);
|
||||
const fieldKeys = Array.from(field.keys()).map(Number);
|
||||
// HeroList 读取(hero 表的 listExportName)
|
||||
const heroList = new TsConfigFile(join(CONFIG_DIR, 'heroSet.ts'), 'HeroList');
|
||||
heroList.load();
|
||||
const listRaw = heroList.getText();
|
||||
const heroListKeys = new Set<string>(
|
||||
(listRaw.match(/HeroList[\s\S]*?=\s*\[([^\]]*)\]/)?.[1].match(/-?\d+/g) ?? []).map(String)
|
||||
);
|
||||
return {
|
||||
hasHero: (u) => heroKeys.includes(u),
|
||||
hasSkill: (u) => skillKeys.includes(u),
|
||||
hasField: (u) => fieldKeys.includes(u),
|
||||
heroListKeys,
|
||||
};
|
||||
}
|
||||
|
||||
export const store = {
|
||||
queryEnums: () => ENUMS,
|
||||
querySchema: (id?: TableId) => id ? allSchemas().find(s => s.id === id) : allSchemas(),
|
||||
queryKeys: (id: TableId) => getTable(id).file.getKeys(),
|
||||
queryRecord: (id: TableId, key: string) => getTable(id).file.read(key),
|
||||
queryPreviewDesc: (hero: RecordValue) => {
|
||||
const skill = recordsOf('skill'); const field = recordsOf('field');
|
||||
const skillSet: Record<number, RecordValue> = {};
|
||||
for (const [k, v] of skill) skillSet[Number(k)] = v;
|
||||
const fieldSet: Record<number, RecordValue> = {};
|
||||
for (const [k, v] of field) fieldSet[Number(k)] = v;
|
||||
return buildSkillDesc(hero, skillSet, fieldSet);
|
||||
},
|
||||
validate: (id: TableId) => validate(id, recordsOf(id), buildContext()),
|
||||
saveRecord: (id: TableId, key: string, value: RecordValue) => {
|
||||
// 先在副本上试写并校验
|
||||
const { file } = getTable(id);
|
||||
file.patch(key, value);
|
||||
const issues = validate(id, recordsOf(id), buildContext()).filter(i => i.key === key && i.severity === 'error');
|
||||
if (issues.length) { file.load(); return { ok: false as const, issues }; } // 回滚内存
|
||||
const r = file.save();
|
||||
if (!r.ok) { file.load(); return { ok: false as const, issues: [] }; }
|
||||
void Editor.Message.request('asset-db', 'refresh-asset', `db://assets/script/game/common/config/${allSchemas().find(s => s.id === id)!.sourceFile}`);
|
||||
return { ok: true as const, issues: [] };
|
||||
},
|
||||
revertRecord: (id: TableId, key: string) => { getTable(id).file.load(); return getTable(id).file.read(key); },
|
||||
reloadAll: () => { for (const k of Object.keys(fileCache)) fileCache[k].load(); },
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import { createApp, defineComponent, reactive } from 'vue';
|
||||
|
||||
export const App = defineComponent({
|
||||
setup() {
|
||||
const state = reactive({ table: 'hero' as string, keys: [] as string[], picked: '' as string, detail: '' as string });
|
||||
async function load() {
|
||||
const keys = await Editor.Message.request('pixelhero-config-editor', 'query-keys', state.table);
|
||||
state.keys = keys || [];
|
||||
state.picked = ''; state.detail = '';
|
||||
}
|
||||
async function pick(k: string) {
|
||||
state.picked = k;
|
||||
const v = await Editor.Message.request('pixelhero-config-editor', 'query-record', state.table, k);
|
||||
state.detail = JSON.stringify(v, null, 2);
|
||||
}
|
||||
load();
|
||||
return { state, load, pick };
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<div class="row">
|
||||
<label>表:</label>
|
||||
<select v-model="state.table" @change="load">
|
||||
<option value="hero">英雄/怪物</option>
|
||||
<option value="skill">技能</option>
|
||||
<option value="field">驻场技能</option>
|
||||
</select>
|
||||
<span style="margin-left:12px;color:#888">共 {{ state.keys.length }} 条(端到端 IPC 已打通)</span>
|
||||
</div>
|
||||
<ul>
|
||||
<li v-for="k in state.keys" :key="k" @click="pick(k)" :style="state.picked===k ? 'font-weight:bold' : ''">{{ k }}</li>
|
||||
</ul>
|
||||
<pre v-if="state.detail" style="white-space:pre-wrap;background:var(--color-normal-fill);padding:8px">{{ state.detail }}</pre>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
export function mount(el: HTMLElement) { createApp(App).mount(el); }
|
||||
@@ -1,14 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { mount } from './app';
|
||||
|
||||
const template = readFileSync(join(__dirname, '../../../static/template/default/index.html'), 'utf-8');
|
||||
const style = readFileSync(join(__dirname, '../../../static/style/default/index.css'), 'utf-8');
|
||||
|
||||
module.exports = Editor.Panel.define({
|
||||
template,
|
||||
style,
|
||||
$: { app: '#app' },
|
||||
ready() { mount(this.$.app); },
|
||||
close() { /* Vue 卸载随面板进程退出自动清理 */ },
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import { RecordValue } from '../../io/recordValue';
|
||||
|
||||
const TRIGGER_KEYS = ['call', 'dead', 'fstart', 'fend', 'atking', 'atked'] as const;
|
||||
const TRIGGER_DESC: Record<string, string> = {
|
||||
call: '召唤时', dead: '死亡时', fstart: '战斗开始时', fend: '战斗结束时',
|
||||
field: '场上存活', atking: '攻击n次', atked: '受击n次', revive: '复活时',
|
||||
};
|
||||
|
||||
function num(v: RecordValue | undefined): number | undefined { return v && v.kind === 'num' ? v.value : undefined; }
|
||||
function str(v: RecordValue | undefined): string | undefined { return v && v.kind === 'str' ? v.value : undefined; }
|
||||
function member(v: RecordValue | undefined): string | undefined { return v && v.kind === 'enumRef' ? v.member : undefined; }
|
||||
|
||||
function buildEffect(merged: RecordValue): string {
|
||||
if (merged.kind !== 'obj') return '';
|
||||
const kind = member(merged.props['kind']);
|
||||
const ap = num(merged.props['ap']) ?? 0;
|
||||
const parts: string[] = [];
|
||||
if (kind === 'Heal') parts.push(`治疗伙伴${ap}`);
|
||||
else if (kind === 'Shield') parts.push(`护盾${ap}次`);
|
||||
else if (kind === 'Gold') parts.push(`金币+${num(merged.props['gold']) ?? 0}`);
|
||||
else if (kind === 'Support') parts.push(String(str(merged.props['info']) ?? ''));
|
||||
else { // Damage / undefined
|
||||
parts.push(`伤害${ap}%`);
|
||||
if ((num(merged.props['hit_count']) ?? 1) > 1) parts.push(`${num(merged.props['hit_count'])}段`);
|
||||
if (num(merged.props['crt'])) parts.push(`暴击+${num(merged.props['crt'])}%`);
|
||||
if (num(merged.props['stun'])) parts.push(`击晕+${num(merged.props['stun'])}%`);
|
||||
}
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
/** 合并 overrides 到 base(浅合并对象 props) */
|
||||
function merge(base: RecordValue, overrides: RecordValue | undefined): RecordValue {
|
||||
if (!overrides || overrides.kind !== 'obj' || base.kind !== 'obj') return base;
|
||||
return { kind: 'obj', props: { ...base.props, ...overrides.props } };
|
||||
}
|
||||
|
||||
export function buildSkillDesc(hero: RecordValue, skillSet: Record<number, RecordValue>, fieldSet: Record<number, RecordValue>): string {
|
||||
if (hero.kind !== 'obj') return '';
|
||||
const lines: string[] = [];
|
||||
for (const key of TRIGGER_KEYS) {
|
||||
const arr = hero.props[key];
|
||||
if (!arr || arr.kind !== 'arr') continue;
|
||||
const tpl = TRIGGER_DESC[key] ?? key;
|
||||
for (const it of arr.items) {
|
||||
if (it.kind !== 'obj') continue;
|
||||
const su = num(it.props['s_uuid']); if (su === undefined) continue;
|
||||
const base = skillSet[su]; if (!base) continue;
|
||||
const merged = merge(base, it.props['overrides']);
|
||||
const trigger = tpl.includes('n') ? tpl.replace('n', String(num(it.props['t_num']) ?? '')) : tpl;
|
||||
lines.push(`${trigger}:${str(base.props['name'])} ${buildEffect(merged)}`);
|
||||
}
|
||||
}
|
||||
const fl = hero.props['field'];
|
||||
if (fl && fl.kind === 'arr') {
|
||||
for (const it of fl.items) { const u = num(it); if (u === undefined) continue; const fs = fieldSet[u]; if (fs) lines.push(`${TRIGGER_DESC.field}:${str(fs.props['name'])} ${str(fs.props['info'])}`); }
|
||||
}
|
||||
const rv = hero.props['revive'];
|
||||
if (rv && rv.kind === 'obj') {
|
||||
const su = num(rv.props['s_uuid']); const base = su !== undefined ? skillSet[su] : undefined;
|
||||
if (base) lines.push(`${TRIGGER_DESC.revive} : ${str(base.props['name'])} ${buildEffect(merge(base, undefined))}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
export interface EnumMember { label: string; value: number | string; }
|
||||
export interface EnumDef { qualifier: string; members: EnumMember[]; }
|
||||
|
||||
export const ENUMS: Record<string, EnumDef> = {
|
||||
HType: { qualifier: 'HType', members: [
|
||||
{ label: '近战 Melee', value: 0 }, { label: '中程 Mid', value: 1 }, { label: '远程 Long', value: 2 } ] },
|
||||
FacSet: { qualifier: 'FacSet', members: [
|
||||
{ label: '英雄 HERO', value: 0 }, { label: '怪物 MON', value: 1 } ] },
|
||||
TGroup: { qualifier: 'TGroup', members: [
|
||||
{ label: '自身 Self', value: 0 }, { label: '友方含己 Ally', value: 1 },
|
||||
{ label: '友方 Team', value: 2 }, { label: '敌方 Enemy', value: 3 }, { label: '全体 All', value: 4 } ] },
|
||||
DTType: { qualifier: 'DTType', members: [
|
||||
{ label: '单体 single', value: 0 }, { label: '范围 range', value: 1 }, { label: '3x3 aoe_grid', value: 2 } ] },
|
||||
SkillKind: { qualifier: 'SkillKind', members: [
|
||||
{ label: '伤害 Damage', value: 0 }, { label: '治疗 Heal', value: 1 }, { label: '护盾 Shield', value: 2 },
|
||||
{ label: '辅助 Support', value: 3 }, { label: '金币 Gold', value: 4 } ] },
|
||||
DType: { qualifier: 'DType', members: [
|
||||
{ label: '物理 ATK', value: 0 }, { label: '冰 ICE', value: 1 }, { label: '火 FIRE', value: 2 }, { label: '风 WIND', value: 3 } ] },
|
||||
IType: { qualifier: 'IType', members: [
|
||||
{ label: '近战 Melee', value: 0 }, { label: '远程 remote', value: 1 }, { label: '辅助 support', value: 2 } ] },
|
||||
RType: { qualifier: 'RType', members: [
|
||||
{ label: '直线 linear', value: 0 }, { label: '贝塞尔 bezier', value: 1 },
|
||||
{ label: '固定起点 fixed', value: 2 }, { label: '固定终点 fixedEnd', value: 3 } ] },
|
||||
EType: { qualifier: 'EType', members: [
|
||||
{ label: '动画结束 animationEnd', value: 0 }, { label: '时间结束 timeEnd', value: 1 }, { label: '碰撞 collision', value: 2 } ] },
|
||||
FieldSkillType: { qualifier: 'FieldSkillType', members: [
|
||||
{ label: '召唤次数 SummonCount', value: 1 }, { label: '死亡次数 DeadCount', value: 2 },
|
||||
{ label: '开场次数 StartCount', value: 3 }, { label: '结束次数 EndCount', value: 4 },
|
||||
{ label: '每波金币 WaveGold', value: 5 }, { label: '卖出金币 SellGold', value: 6 },
|
||||
{ label: '战后回复 WaveHeal', value: 7 }, { label: '英雄攻击 HeroAtk', value: 8 },
|
||||
{ label: '英雄击晕 HeroStun', value: 9 }, { label: '英雄暴击 HeroCrit', value: 10 },
|
||||
{ label: '英雄暴伤 HeroCritDamage', value: 11 }, { label: '英雄攻速 HeroSpeed', value: 12 },
|
||||
{ label: '购买优惠 BuyDiscount', value: 13 }, { label: '刷新优惠 RefreshDiscount', value: 14 },
|
||||
{ label: '英雄生命 HeroHp', value: 16 }, { label: '英雄风怒 HeroWindFury', value: 17 },
|
||||
{ label: '英雄穿刺 HeroPuncture', value: 18 }, { label: '攻击次数 AtkCount', value: 19 },
|
||||
{ label: '受击次数 BeAtkCount', value: 20 } ] },
|
||||
AtkSpeedLv: { qualifier: 'AtkSpeedLv', members: [
|
||||
{ label: '极速++ VeryFast1', value: 1 }, { label: '极速+ VeryFast2', value: 2 }, { label: '极速 VeryFast3', value: 3 },
|
||||
{ label: '快速++ Fast1', value: 4 }, { label: '快速+ Fast2', value: 5 }, { label: '快速 Fast3', value: 6 },
|
||||
{ label: '中速++ Normal1', value: 7 }, { label: '中速+ Normal2', value: 8 }, { label: '中速 Normal3', value: 9 },
|
||||
{ label: '一般+ Mid1', value: 10 }, { label: '一般 Mid2', value: 11 }, { label: '一般- Mid3', value: 12 },
|
||||
{ label: '慢 Slow1', value: 13 }, { label: '慢+ Slow2', value: 14 }, { label: '慢++ Slow3', value: 15 },
|
||||
{ label: '很慢 VerySlow1', value: 16 }, { label: '很慢+ VerySlow2', value: 17 }, { label: '很慢++ VerySlow3', value: 18 } ] },
|
||||
Attrs: { qualifier: 'Attrs', members: [
|
||||
{ label: 'ap', value: 'ap' }, { label: 'hp', value: 'hp' }, { label: 'hp_max', value: 'hp_max' },
|
||||
{ label: 'critical', value: 'critical' }, { label: 'critical_damage', value: 'critical_damage' },
|
||||
{ label: 'stun_chance', value: 'stun_chance' }, { label: 'puncture_chance', value: 'puncture_chance' },
|
||||
{ label: 'wfuny', value: 'wfuny' }, { label: 'freeze_chance', value: 'freeze_chance' },
|
||||
{ label: 'knockback_chance', value: 'knockback_chance' } ] },
|
||||
};
|
||||
|
||||
/** qualifier(如 'FacSet')→ 对应 enumId(如 'FacSet')。当前两者同名。 */
|
||||
export const QUALIFIER_TO_ID: Record<string, string> = Object.fromEntries(
|
||||
Object.entries(ENUMS).map(([id, def]) => [def.qualifier, id])
|
||||
);
|
||||
@@ -1,14 +0,0 @@
|
||||
import { TableSchema } from './types';
|
||||
export const fieldSchema: TableSchema = {
|
||||
id: 'field', label: '驻场技能',
|
||||
sourceFile: 'SkillSet.ts', exportName: 'FieldSkillSet',
|
||||
idSegments: [{ label: '驻场技能', min: 7000, max: 7999 }],
|
||||
fields: [
|
||||
{ key: 'uuid', label: 'UUID', type: 'number', required: true, group: '基础' },
|
||||
{ key: 'name', label: '名称', type: 'string', required: true, group: '基础' },
|
||||
{ key: 'icon', label: '图标', type: 'string', required: true, group: '基础' },
|
||||
{ key: 'type', label: '类型', type: 'enum', enumRef: 'FieldSkillType', required: true, group: '效果' },
|
||||
{ key: 'value', label: '数值', type: 'number', required: true, group: '效果' },
|
||||
{ key: 'info', label: '描述', type: 'string', required: true, group: '基础' },
|
||||
],
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
import { TableSchema } from './types';
|
||||
export const heroSchema: TableSchema = {
|
||||
id: 'hero', label: '英雄/怪物',
|
||||
sourceFile: 'heroSet.ts', exportName: 'HeroInfo', listExportName: 'HeroList',
|
||||
idSegments: [
|
||||
{ label: '英雄', min: 5000, max: 5999 },
|
||||
{ label: '怪物', min: 6000, max: 6999 },
|
||||
],
|
||||
fields: [
|
||||
{ key: 'uuid', label: 'UUID', type: 'number', required: true, group: '基础' },
|
||||
{ key: 'name', label: '名称', type: 'string', required: true, group: '基础' },
|
||||
{ key: 'path', label: '资源路径', type: 'string', required: true, group: '基础' },
|
||||
{ key: 'icon', label: '图标', type: 'string', group: '基础' },
|
||||
{ key: 'fac', label: '阵营', type: 'enum', enumRef: 'FacSet', required: true, group: '基础' },
|
||||
{ key: 'pool_lv', label: '卡片等级', type: 'number', group: '基础' },
|
||||
{ key: 'lv', label: '英雄等级', type: 'number', required: true, default: 1, group: '基础' },
|
||||
{ key: 'type', label: '攻击定位', type: 'enum', enumRef: 'HType', required: true, group: '基础' },
|
||||
{ key: 'hp', label: '生命上限', type: 'number', required: true, group: '基础' },
|
||||
{ key: 'ap', label: '攻击力', type: 'number', required: true, group: '基础' },
|
||||
{ key: 'dis', label: '攻击距离', type: 'number', group: '基础' },
|
||||
{ key: 'speed', label: '移动速度', type: 'number', group: '基础' },
|
||||
{ key: 'skills', label: '携带技能', type: 'skillMap', required: true, group: '技能' },
|
||||
{ key: 'call', label: '召唤触发', type: 'triggerSlots', group: '触发技能' },
|
||||
{ key: 'dead', label: '死亡触发', type: 'triggerSlots', group: '触发技能' },
|
||||
{ key: 'fstart', label: '战斗开始触发', type: 'triggerSlots', group: '触发技能' },
|
||||
{ key: 'fend', label: '战斗结束触发', type: 'triggerSlots', group: '触发技能' },
|
||||
{ key: 'atking', label: '攻击触发', type: 'triggerSlots', group: '触发技能' },
|
||||
{ key: 'atked', label: '受击触发', type: 'triggerSlots', group: '触发技能' },
|
||||
{ key: 'field', label: '驻场技能', type: 'fieldList', group: '触发技能' },
|
||||
{ key: 'revive', label: '复活触发', type: 'reviveSlot', group: '触发技能' },
|
||||
{ key: 'info', label: '描述文案', type: 'string', required: true, group: '基础' },
|
||||
],
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
import { TableId, TableSchema } from './types';
|
||||
import { heroSchema } from './hero';
|
||||
import { skillSchema } from './skill';
|
||||
import { fieldSchema } from './field';
|
||||
|
||||
export const REGISTRY: Record<TableId, TableSchema> = {
|
||||
hero: heroSchema, skill: skillSchema, field: fieldSchema,
|
||||
};
|
||||
export function getSchema(id: TableId): TableSchema { return REGISTRY[id]; }
|
||||
export function allSchemas(): TableSchema[] { return Object.values(REGISTRY); }
|
||||
@@ -1,41 +0,0 @@
|
||||
import { TableSchema } from './types';
|
||||
export const skillSchema: TableSchema = {
|
||||
id: 'skill', label: '技能',
|
||||
sourceFile: 'SkillSet.ts', exportName: 'SkillSet',
|
||||
idSegments: [{ label: '技能', min: 6000, max: 6999 }],
|
||||
fields: [
|
||||
{ key: 'uuid', label: 'UUID', type: 'number', required: true, group: '基础' },
|
||||
{ key: 'name', label: '名称', type: 'string', required: true, group: '基础' },
|
||||
{ key: 'sp_name', label: '特效名', type: 'string', required: true, group: '基础' },
|
||||
{ key: 'icon', label: '图标ID', type: 'string', required: true, group: '基础' },
|
||||
{ key: 'act', label: '执行动画', type: 'string', required: true, group: '基础' },
|
||||
{ key: 'TGroup', label: '目标群体', type: 'enum', enumRef: 'TGroup', required: true, group: '目标' },
|
||||
{ key: 'DTType', label: '伤害类型', type: 'enum', enumRef: 'DTType', required: true, group: '目标' },
|
||||
{ key: 'IType', label: '技能类型', type: 'enum', enumRef: 'IType', required: true, group: '目标' },
|
||||
{ key: 'RType', label: '运行类型', type: 'enum', enumRef: 'RType', required: true, group: '目标' },
|
||||
{ key: 'EType', label: '结束条件', type: 'enum', enumRef: 'EType', required: true, group: '目标' },
|
||||
{ key: 'kind', label: '主效果', type: 'enum', enumRef: 'SkillKind', group: '目标' },
|
||||
{ key: 'ap', label: 'ap(伤害%/护盾次)', type: 'number', required: true, group: '数值' },
|
||||
{ key: 'gold', label: '金币值', type: 'number', group: '数值' },
|
||||
{ key: 'hit_count', label: '可命中次数', type: 'number', required: true, group: '数值' },
|
||||
{ key: 'hitcd', label: '伤害间隔', type: 'number', required: true, group: '数值' },
|
||||
{ key: 'speed', label: '移动速度', type: 'number', required: true, group: '数值' },
|
||||
{ key: 'ready', label: '前摇时间', type: 'number', required: true, group: '数值' },
|
||||
{ key: 'with', label: '宽度', type: 'number', default: 0, group: '数值' },
|
||||
{ key: 'readyAnm', label: '前摇动画', type: 'string', group: '动画' },
|
||||
{ key: 'endAnm', label: '结束动画', type: 'string', group: '动画' },
|
||||
{ key: 'DAnm', label: '命中动画ID', type: 'string', group: '动画' },
|
||||
{ key: 'EAnm', label: '结束动画ID', type: 'number', group: '动画' },
|
||||
{ key: 'crt', label: '额外暴击率', type: 'number', group: '高级' },
|
||||
{ key: 'stun', label: '额外击晕概率', type: 'number', group: '高级' },
|
||||
{ key: 'frz', label: '额外冰冻概率', type: 'number', group: '高级' },
|
||||
{ key: 'bck', label: '额外击退概率', type: 'number', group: '高级' },
|
||||
{ key: 'buff_type', label: 'Buff类型', type: 'enum', enumRef: 'Attrs', group: '高级' },
|
||||
{ key: 'call_hero', label: '召唤英雄', type: 'ref', refTable: 'hero', group: '高级' },
|
||||
{ key: 'time', label: '持续时间', type: 'number', group: '高级' },
|
||||
{ key: 'bezier_start_y', label: '贝塞尔起始Y', type: 'number', group: '高级' },
|
||||
{ key: 'bezier_mid_y', label: '贝塞尔中点Y', type: 'number', group: '高级' },
|
||||
{ key: 'bezier_arc', label: '贝塞尔弧度', type: 'number', group: '高级' },
|
||||
{ key: 'info', label: '描述', type: 'string', required: true, group: '基础' },
|
||||
],
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
export type TableId = 'hero' | 'skill' | 'field';
|
||||
|
||||
export type FieldType =
|
||||
| 'number' | 'string' | 'boolean'
|
||||
| 'enum' // 下拉,选项来自 enumRef
|
||||
| 'ref' // 引用另一表 uuid
|
||||
| 'speedExpr' // 攻速符号表达式
|
||||
| 'skillMap' | 'triggerSlots' | 'fieldList' | 'reviveSlot' | 'overrides';
|
||||
|
||||
export interface FieldSchema {
|
||||
key: string;
|
||||
label: string;
|
||||
type: FieldType;
|
||||
required?: boolean;
|
||||
default?: unknown;
|
||||
group?: string;
|
||||
enumRef?: string; // type='enum'
|
||||
refTable?: TableId; // type='ref'
|
||||
}
|
||||
|
||||
export interface IdSegment { label: string; min: number; max: number; }
|
||||
|
||||
export interface TableSchema {
|
||||
id: TableId;
|
||||
label: string;
|
||||
sourceFile: string; // 相对 assets/script/game/common/config/
|
||||
exportName: string; // 'HeroInfo' | 'SkillSet' | 'FieldSkillSet'
|
||||
listExportName?: string; // 'HeroList'(仅 hero)
|
||||
idSegments: IdSegment[];
|
||||
fields: FieldSchema[];
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import { RecordValue } from '../../io/recordValue';
|
||||
import { TableId } from '../schema/types';
|
||||
import { ENUMS } from '../schema/enums';
|
||||
|
||||
export type Severity = 'error' | 'warn';
|
||||
export interface Issue {
|
||||
tableId: TableId; key: string; fieldPath: string;
|
||||
severity: Severity; code: string; message: string;
|
||||
}
|
||||
export interface ValidationContext {
|
||||
hasSkill: (u: number) => boolean;
|
||||
hasField: (u: number) => boolean;
|
||||
hasHero: (u: number) => boolean;
|
||||
heroListKeys: Set<string>;
|
||||
}
|
||||
|
||||
const OVERRIDE_KEYS = new Set(['TGroup', 'ap', 'gold', 'hit_count', 'hitcd', 'crt', 'frz', 'stun', 'bck', 'buff_type', 'call_hero']);
|
||||
const TRIGGER_ARRAY_KEYS = ['call', 'dead', 'fstart', 'fend', 'atking', 'atked'];
|
||||
|
||||
function num(v: RecordValue | undefined): number | undefined {
|
||||
return v && v.kind === 'num' ? v.value : undefined;
|
||||
}
|
||||
function isKnownEnum(qualifier: string, member: string): boolean {
|
||||
const def = ENUMS[qualifier] ?? Object.values(ENUMS).find(e => e.qualifier === qualifier);
|
||||
return !!def && def.members.some(m => String(m.value) === member || m.label.split(' ').pop() === member);
|
||||
}
|
||||
|
||||
export function validate(tableId: TableId, records: Map<string, RecordValue>, ctx: ValidationContext): Issue[] {
|
||||
const issues: Issue[] = [];
|
||||
// 跟踪 uuid 字段值(而非 Map 键)—— JS Map 会折叠重复键,但两条记录可能携带相同的 uuid 字段值。
|
||||
const seenUuids = new Set<string>();
|
||||
|
||||
for (const [key, rec] of records) {
|
||||
const push = (fieldPath: string, severity: Severity, code: string, message: string) =>
|
||||
issues.push({ tableId, key, fieldPath, severity, code, message });
|
||||
|
||||
if (rec.kind !== 'obj') { push('', 'error', 'not-object', '记录不是对象'); continue; }
|
||||
const p = rec.props;
|
||||
|
||||
const uVal = num(p['uuid']);
|
||||
if (uVal !== undefined) {
|
||||
const uKey = String(uVal);
|
||||
if (seenUuids.has(uKey)) push('uuid', 'error', 'dup-uuid', `重复 uuid: ${uKey}`);
|
||||
seenUuids.add(uKey);
|
||||
}
|
||||
|
||||
// 必填(按表最小集合)
|
||||
const required: Record<TableId, string[]> = {
|
||||
hero: ['uuid', 'name', 'path', 'fac', 'lv', 'type', 'hp', 'ap', 'skills', 'info'],
|
||||
skill: ['uuid', 'name', 'sp_name', 'icon', 'act', 'TGroup', 'DTType', 'IType', 'RType', 'EType', 'ap', 'hit_count', 'hitcd', 'speed', 'ready', 'info'],
|
||||
field: ['uuid', 'name', 'icon', 'type', 'value', 'info'],
|
||||
};
|
||||
for (const f of required[tableId]) {
|
||||
if (p[f] === undefined) push(f, 'error', 'missing-required', `缺少必填字段 ${f}`);
|
||||
}
|
||||
|
||||
// 枚举值合法
|
||||
for (const [f, v] of Object.entries(p)) {
|
||||
if (v.kind === 'enumRef' && !isKnownEnum(v.qualifier, v.member)) {
|
||||
push(f, 'error', 'bad-enum', `非法枚举值 ${v.qualifier}.${v.member}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (tableId === 'hero') {
|
||||
// 触发槽数组引用 + overrides 键
|
||||
for (const tk of TRIGGER_ARRAY_KEYS) {
|
||||
const arr = p[tk];
|
||||
if (!arr || arr.kind !== 'arr') continue;
|
||||
for (const it of arr.items) {
|
||||
if (it.kind !== 'obj') continue;
|
||||
const su = num(it.props['s_uuid']);
|
||||
if (su !== undefined && !ctx.hasSkill(su)) push(tk, 'error', 'dangling-ref', `引用了不存在的技能 ${su}`);
|
||||
const ov = it.props['overrides'];
|
||||
if (ov && ov.kind === 'obj') {
|
||||
for (const k of Object.keys(ov.props)) {
|
||||
if (!OVERRIDE_KEYS.has(k)) push(`${tk}.overrides`, 'error', 'bad-override-key', `非法覆盖键 ${k}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// field 列表
|
||||
const fl = p['field'];
|
||||
if (fl && fl.kind === 'arr') {
|
||||
for (const it of fl.items) { const u = num(it); if (u !== undefined && !ctx.hasField(u)) push('field', 'error', 'dangling-ref', `引用了不存在的驻场技能 ${u}`); }
|
||||
}
|
||||
// revive
|
||||
const rv = p['revive'];
|
||||
if (rv && rv.kind === 'obj') {
|
||||
const su = num(rv.props['s_uuid']); if (su !== undefined && !ctx.hasSkill(su)) push('revive', 'error', 'dangling-ref', `引用了不存在的技能 ${su}`);
|
||||
}
|
||||
// call_hero(技能表里的 ref 也用 hasHero)
|
||||
}
|
||||
if (tableId === 'skill') {
|
||||
const ch = num(p['call_hero']); if (ch !== undefined && !ctx.hasHero(ch)) push('call_hero', 'error', 'dangling-ref', `引用了不存在的英雄 ${ch}`);
|
||||
}
|
||||
|
||||
// HeroList 一致性(仅 hero 表)
|
||||
if (tableId === 'hero') {
|
||||
const u = num(p['uuid']); const fac = p['fac'];
|
||||
const isHero = fac && fac.kind === 'enumRef' && fac.member === 'HERO';
|
||||
if (isHero && u !== undefined && !ctx.heroListKeys.has(String(u))) {
|
||||
push('uuid', 'error', 'herolist-inconsistent', `英雄 ${u} 不在 HeroList 中`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
#app { color: var(--color-font-normal); font-size: 12px; }
|
||||
.row { margin: 6px 0; }
|
||||
button { margin-right: 8px; }
|
||||
ul { list-style: none; padding: 0; max-height: 360px; overflow:auto; }
|
||||
li { padding: 3px 6px; cursor: pointer; }
|
||||
li:hover { background: var(--color-hover-bg); }
|
||||
.err { color: var(--color-warn); }
|
||||
@@ -1 +0,0 @@
|
||||
<div id="app" style="padding:12px;"></div>
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"types": ["node"],
|
||||
"lib": ["ES2020", "DOM"]
|
||||
},
|
||||
"include": ["src/**/*", "__tests__/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user