diff --git a/2001/ze_hr_dead_center/car_gas.js b/2001/ze_hr_dead_center/car_gas.js new file mode 100644 index 0000000..649482c --- /dev/null +++ b/2001/ze_hr_dead_center/car_gas.js @@ -0,0 +1,187 @@ +import { Instance, PointTemplate, Entity } from "cs_script/point_script"; + +/** + * 油桶收集脚本 + * 此脚本由皮皮猫233编写 + * 2026/2/23 + */ + +let gasNumber = 0; +let gasBgm = 1; +const gasMaker = new Map(); + +Instance.OnScriptInput("Spawn", () => { + const makers = Instance.FindEntitiesByName("scavenge_gascans_spawn_cs2"); + const makers1st = Instance.FindEntitiesByName("scavenge_gascans_spawn_1st_floor_cs2"); + if (!makers || !makers1st) return; + for (const maker of makers) { + const entities = ForspawnAtMaker(maker); + if (!entities) return; + for (const entity of entities) { + if (entity.GetEntityName() === "item_gascan_atrium_phy") { + gasMaker.set(entity, { maker: maker, floor: 2 }); + } + } + } + for (const maker of makers1st) { + const entities = ForspawnAtMaker(maker); + if (!entities) return; + for (const entity of entities) { + if (entity.GetEntityName() === "item_gascan_atrium_phy") { + gasMaker.set(entity, { maker: maker, floor: 1 }); + } + } + } +}); + +Instance.OnScriptInput("Respawn", (inputData) => { + Respawn(inputData.caller); +}); + +Instance.OnScriptInput("Add", (inputData) => { + Respawn(inputData.activator); + gasNumber ++; + if (gasNumber < 26) { + Instance.ServerCommand("say 当前油量" + gasNumber + "/26"); + if (gasBgm === 1) { + if (gasNumber >= 10) { + Instance.EntFireAtName({ name: "bgm_zombat_fadeout", input: "FireUser1" }); + gasBgm = 2; + } + } else if (gasBgm === 2) { + if (gasNumber > 21) { + Instance.EntFireAtName({ name: "bgm_final", input: "StartSound" }); + gasBgm = 3; + } + } + } else { + Instance.EntFireAtName({ name: "relay_car_escape", input: "Trigger" }); + } +}); + +// 回合重启时重置数量 +Instance.OnRoundStart(() => { + gasNumber = 0; + gasMaker.clear(); + thinkQueue.length = 0; + gasBgm = 1; +}); + +/** + * 重复刷新油桶 + * @param {Entity|undefined} phy + * @returns + */ +function Respawn(phy) { + if (!phy || !phy.IsValid()) return; + if (gasMaker.has(phy)) { + const value = gasMaker.get(phy); + const maker = value.maker; + const floor = value.floor; + gasMaker.delete(phy); + // 判断油桶所在的不同楼层决定刷新时间 + if (floor === 2) { + // 延迟30秒后刷新 + Delay(30, () => { + const entities = ForspawnAtMaker(maker); + if (!entities) return; + for (const entity of entities) { + if (entity.GetEntityName() === "item_gascan_atrium_phy") { + gasMaker.set(entity, { maker: maker, floor: 2 }); + } + } + }); + } else { + // 延迟60秒后刷新 + Delay(60, () => { + const entities = ForspawnAtMaker(maker); + if (!entities) return; + for (const entity of entities) { + if (entity.GetEntityName() === "item_gascan_atrium_phy") { + gasMaker.set(entity, { maker: maker, floor: 1 }); + } + } + }); + } + + } +} + +/** + * 将模板生成到某个生成器的位置 + * @param {Entity} maker + */ +function ForspawnAtMaker(maker) { + const temp = /** @type {PointTemplate} */ (Instance.FindEntityByName("item_gascan_atrium_temp")); + return temp.ForceSpawn(maker.GetAbsOrigin(), maker.GetAbsAngles()); +} + +/** @type {{ time: number, callback: () => void }[]} */ +const thinkQueue = []; + +/** + * 延迟执行函数 + * @param {number} delaySeconds 延迟的秒数 + * @param {() => void} callback 回调函数 + */ +function Delay(delaySeconds, callback) { + const executeTime = Instance.GetGameTime() + delaySeconds; + QueueThink(executeTime, callback); +} + +/** + * 异步延迟函数,返回Promise + * @param {number} delaySeconds 延迟的秒数 + * @returns {Promise} + */ +function DelayAsync(delaySeconds) { + return new Promise((resolve) => { + Delay(delaySeconds, resolve); + }); +} + +/** + * 将think任务加入队列 + * @param {number} time 执行时间 + * @param {() => void} callback 回调函数 + */ +function QueueThink(time, callback) { + // 查找插入位置(按时间排序) + let insertIndex = 0; + for (let i = thinkQueue.length - 1; i >= 0; i--) { + if (thinkQueue[i].time <= time) { + insertIndex = i + 1; + break; + } + } + + // 插入到合适位置 + thinkQueue.splice(insertIndex, 0, { time, callback }); + + // 如果新任务是最早的,则更新think + if (insertIndex === 0) { + Instance.SetNextThink(time); + } +} + +/** + * Think循环处理函数 + */ +function RunThinkQueue() { + const currentTime = Instance.GetGameTime(); + + // 执行所有到期的任务 + while (thinkQueue.length > 0 && thinkQueue[0].time <= currentTime) { + const task = thinkQueue.shift(); + if (!task) continue; + task.callback(); + } + + // 更新下一次think + if (thinkQueue.length > 0) { + Instance.SetNextThink(thinkQueue[0].time); + } +} + +// 设置Think循环 +Instance.SetThink(RunThinkQueue); \ No newline at end of file diff --git a/2001/ze_hr_dead_center/deinfect.js b/2001/ze_hr_dead_center/deinfect.js new file mode 100644 index 0000000..650df1b --- /dev/null +++ b/2001/ze_hr_dead_center/deinfect.js @@ -0,0 +1,150 @@ +import { Instance, Entity } from "cs_script/point_script"; + +/** + * 防化脚本 + * 此脚本用于实现类似求生之路中的感染者攻击而非ze模式中的直接感染 + * 此脚本为针对风云社参数适配后的版本,对比原版,将僵尸低血量时的1000血降为600血 + * 此脚本由皮皮猫233编写 + * 2026/3/6 + */ + +let deinfectSwitch = false; +let currentHandler = createDamageHandler(false); + +// @ts-ignore +Instance.OnModifyPlayerDamage((event) => currentHandler(event)); + +Instance.OnScriptInput("Enable", () => { + deinfectSwitch = true; + currentHandler = createDamageHandler(true); + ZombieLowHP(); +}); + +Instance.OnScriptInput("Disable", () => { + deinfectSwitch = false; + currentHandler = createDamageHandler(false); +}); + +Instance.OnRoundStart(() => { + deinfectSwitch = false; + currentHandler = createDamageHandler(false); + thinkQueue.length = 0; +}); + +/** + * 创建一个可以切换的处理器 + * @param {boolean} enabled + * @returns + */ +function createDamageHandler(enabled) { + if (!enabled) { + return () => {}; + } + return (/** @type {import("cs_script/point_script").ModifyPlayerDamageEvent} */ event) => { + const attacker = event.attacker; + if (!attacker || !attacker.IsValid() || attacker.GetClassName() !== "player" || attacker.GetTeamNumber() !== 2) return; + + const player = event.player; + if (!player || !player.IsValid() || player.GetTeamNumber() === 2) return; + + player.TakeDamage({ damage: 40, damageTypes: 512 }); + return { abort: true }; + }; +} + +/** + * 设置僵尸低血量 + */ +function ZombieLowHP() { + if (!deinfectSwitch) return; + const players = Instance.FindEntitiesByClass("player"); + for (const player of players) { + if (!player || !player.IsValid() || player.GetTeamNumber() !== 2) continue; + SetLowHP(player); + } + // 1秒后再次检测 + Delay(1, ZombieLowHP); +} + +/** + * 设置低血量 + * @param {Entity} player + */ +function SetLowHP(player) { + if (player.GetMaxHealth() > 100) { + Delay(0.5, () => { + player.SetMaxHealth(1); + player.SetHealth(600); + }); + } +} + +/** @type {{ time: number, callback: () => void }[]} */ +const thinkQueue = []; + +/** + * 延迟执行函数 + * @param {number} delaySeconds 延迟的秒数 + * @param {() => void} callback 回调函数 + */ +function Delay(delaySeconds, callback) { + const executeTime = Instance.GetGameTime() + delaySeconds; + QueueThink(executeTime, callback); +} + +/** + * 异步延迟函数,返回Promise + * @param {number} delaySeconds 延迟的秒数 + * @returns {Promise} + */ +function DelayAsync(delaySeconds) { + return new Promise((resolve) => { + Delay(delaySeconds, resolve); + }); +} + +/** + * 将think任务加入队列 + * @param {number} time 执行时间 + * @param {() => void} callback 回调函数 + */ +function QueueThink(time, callback) { + // 查找插入位置(按时间排序) + let insertIndex = 0; + for (let i = thinkQueue.length - 1; i >= 0; i--) { + if (thinkQueue[i].time <= time) { + insertIndex = i + 1; + break; + } + } + + // 插入到合适位置 + thinkQueue.splice(insertIndex, 0, { time, callback }); + + // 如果新任务是最早的,则更新think + if (insertIndex === 0) { + Instance.SetNextThink(time); + } +} + +/** + * Think循环处理函数 + */ +function RunThinkQueue() { + const currentTime = Instance.GetGameTime(); + + // 执行所有到期的任务 + while (thinkQueue.length > 0 && thinkQueue[0].time <= currentTime) { + const task = thinkQueue.shift(); + if (!task) continue; + task.callback(); + } + + // 更新下一次think + if (thinkQueue.length > 0) { + Instance.SetNextThink(thinkQueue[0].time); + } +} + +// 设置Think循环 +Instance.SetThink(RunThinkQueue); \ No newline at end of file diff --git a/2001/ze_hr_dead_center/dynamic_light_fix.js b/2001/ze_hr_dead_center/dynamic_light_fix.js new file mode 100644 index 0000000..5ded774 --- /dev/null +++ b/2001/ze_hr_dead_center/dynamic_light_fix.js @@ -0,0 +1,94 @@ +import { Instance } from "cs_script/point_script"; + +/** + * 动态光修复脚本 + * 此脚本用于修复使用vpk分关时出现的动态光失效问题 + * 此脚本由皮皮猫233编写 + * 2025/11/20 + */ + +Instance.OnScriptInput("LightFix", (inputData) => { + const lights = Instance.FindEntitiesByClass("light_omni2").concat(Instance.FindEntitiesByClass("light_barn")); + for (const light of lights) { + if (!light || !light.IsValid()) continue; + const currentPosition = light.GetAbsOrigin(); + + // 对灯光实体进行一段位移以修复动态光功能 + light.Teleport({ position: { x: currentPosition.x, y: currentPosition.y, z: currentPosition.z + 1 } }); + + // 等待1tick时间 + Delay(1 / 64, () => { + light.Teleport({ position: currentPosition }); + }) + } +}); + +/** @type {{ time: number, callback: () => void }[]} */ +const thinkQueue = []; + +/** + * 延迟执行函数 + * @param {number} delaySeconds 延迟的秒数 + * @param {() => void} callback 回调函数 + */ +function Delay(delaySeconds, callback) { + const executeTime = Instance.GetGameTime() + delaySeconds; + QueueThink(executeTime, callback); +} + +/** + * 异步延迟函数,返回Promise + * @param {number} delaySeconds 延迟的秒数 + * @returns {Promise} + */ +function DelayAsync(delaySeconds) { + return new Promise((resolve) => { + Delay(delaySeconds, resolve); + }); +} + +/** + * 将think任务加入队列 + * @param {number} time 执行时间 + * @param {() => void} callback 回调函数 + */ +function QueueThink(time, callback) { + // 查找插入位置(按时间排序) + let insertIndex = 0; + for (let i = thinkQueue.length - 1; i >= 0; i--) { + if (thinkQueue[i].time <= time) { + insertIndex = i + 1; + break; + } + } + + // 插入到合适位置 + thinkQueue.splice(insertIndex, 0, { time, callback }); + + // 如果新任务是最早的,则更新think + if (insertIndex === 0) { + Instance.SetNextThink(time); + } +} + +/** + * Think循环处理函数 + */ +function RunThinkQueue() { + const currentTime = Instance.GetGameTime(); + + // 执行所有到期的任务 + while (thinkQueue.length > 0 && thinkQueue[0].time <= currentTime) { + const task = thinkQueue.shift(); + if (!task) continue; + task.callback(); + } + + // 更新下一次think + if (thinkQueue.length > 0) { + Instance.SetNextThink(thinkQueue[0].time); + } +} + +// 设置Think循环 +Instance.SetThink(RunThinkQueue); \ No newline at end of file diff --git a/2001/ze_hr_dead_center/item_effect.js b/2001/ze_hr_dead_center/item_effect.js new file mode 100644 index 0000000..8197ee6 --- /dev/null +++ b/2001/ze_hr_dead_center/item_effect.js @@ -0,0 +1,45 @@ +import { Instance, PointTemplate } from "cs_script/point_script"; + +/** + * 道具效果生成脚本 + * 此脚本由皮皮猫233编写 + * 2026/2/9 + */ + +Instance.OnScriptInput("Explode", (inputData) => { + const caller = inputData.caller; + if (!caller || !caller.IsValid()) return; + const entityName = caller.GetEntityName(); + const position = caller.GetAbsOrigin(); + const angles = caller.GetAbsAngles(); + angles.pitch = 0; + angles.roll = 0; + Instance.EntFireAtTarget({ target: caller, input: "KillHierarchy" }); + + if (entityName.includes("anecanister")) Explode("anecanister", position, angles); + else if (entityName.includes("gascan")) Explode("gascan", position, angles); + else if (entityName.includes("oxygentank")) Explode("oxygentank", position, angles); +}); + +/** + * 爆炸函数 + * @param {string} type + * @param {import("cs_script/point_script").Vector} position + * @param {import("cs_script/point_script").QAngle} angles + */ +function Explode(type, position, angles) { + const explodeTemp = /** @type {PointTemplate} */ (Instance.FindEntityByName("item_" + type + "_effect_temp")); + if (!explodeTemp || !explodeTemp.IsValid()) return; + const entities = explodeTemp.ForceSpawn(position, angles); + if (!entities) return; + for (const entity of entities) { + Instance.EntFireAtTarget({ target: entity, input: "Kill", delay: 5 }); + if (entity.GetClassName() === "env_explosion") { + Instance.EntFireAtTarget({ target: entity, input: "Explode" }); + } else if (entity.GetClassName() === "env_shake") { + Instance.EntFireAtTarget({ target: entity, input: "StartShake" }); + } else if (entity.GetClassName() === "point_soundevent") { + Instance.EntFireAtTarget({ target: entity, input: "StopSound", delay: 4.9 }); + } + } +} \ No newline at end of file diff --git a/2001/ze_hr_dead_center/phy_pick.js b/2001/ze_hr_dead_center/phy_pick.js new file mode 100644 index 0000000..ab003ec --- /dev/null +++ b/2001/ze_hr_dead_center/phy_pick.js @@ -0,0 +1,208 @@ +import { Instance, Entity, CSPlayerPawn, CSWeaponType, CSGearSlot, CSWeaponAttackType } from "cs_script/point_script"; + +/** + * 可拾取道具系统 + * 该脚本用于还原求生之路中的可拾取道具(例如油桶等) + * 此脚本由皮皮猫233编写 + * 2026/2/10 + */ + +const playerState = new Map(); +const pickedPhy = new Set(); + +Instance.OnScriptInput("Use", (inputData) => { + const player = /** @type {CSPlayerPawn} */ (inputData.activator); + if (!player || !player.IsValid() || player.GetTeamNumber() !== 3) return; + + const phy = inputData.caller?.GetParent(); + if (!phy || !phy.IsValid()) return; + + // 判断是否已拾取物品 + if (playerState.has(player)) { + // 判断此物品是否属于该玩家 + if (playerState.get(player).phy === phy) { + Drop(player, phy); + } + } else if (!pickedPhy.has(phy)) { + // 判断物品是否已被其他人拾取 + Pickup(player, phy); + } +}); + +// 监听玩家挥刀 +Instance.OnKnifeAttack((event) => { + if (!event.weapon) return; + + // 判断该玩家是否为道具拾取者 + const player = event.weapon.GetOwner(); + if (!player || !player.IsValid() || !playerState.has(player)) return; + + const phy = playerState.get(player).phy; + if (!phy || !phy.IsValid()) return; + + if (event.attackType === CSWeaponAttackType.PRIMARY) + Throw(player, phy); +}); + +// 回合重启时重置状态 +Instance.OnRoundStart(() => { + playerState.clear(); + pickedPhy.clear(); +}); + +// 利用Think循环轮询每个拾取道具的玩家的当前武器状态 +Instance.SetThink(() => { + // 检查是否存在需要检查的状态 + if (playerState.size === 0) return; + + const ignorePhys = Instance.FindEntitiesByClass("prop_physics").concat(Instance.FindEntitiesByClass("func_button")); + + playerState.forEach((value, key) => { + if (key.GetTeamNumber() === 3 && IsKnife(key)) { + // 判断道具与玩家是否有效 + if (value.phy && key && value.phy.IsValid() && key.IsValid()) { + const angles = { pitch: 0, yaw: key.GetEyeAngles().yaw, roll: 0 }; + const currentPosition = key.GetEyePosition(); + const position = { x: currentPosition.x, y: currentPosition.y, z: currentPosition.z - 20 }; + + const forward = getForward(angles); + const traceEnd = vectorAdd(position, vectorScale(forward, 40)); + + // 计算玩家视线位置离墙体的距离,防止道具卡进后室 + const result = Instance.TraceLine({ + start: position, + end: traceEnd, + ignoreEntity: ignorePhys, + ignorePlayers: true + }); + if (result.didHit) { + value.phy.Teleport({ + position: { + x: result.end.x + result.normal.x * 10, + y: result.end.y + result.normal.y * 10, + z: result.end.z + }, + angles: { pitch: 0, yaw: angles.yaw + 90, roll: 0 } + }); + } else { + value.phy.Teleport({ + position: { + x: position.x + forward.x * 30, + y: position.y + forward.y * 30, + z: position.z + }, + angles: { pitch: 0, yaw: angles.yaw + 90, roll: 0 } + }); + } + } else { + // 清理玩家状态 + playerState.delete(key); + pickedPhy.delete(value.phy); + } + } else + Drop(key, value.phy); + }); + + Instance.SetNextThink(Instance.GetGameTime()); +}); + +/** + * 拾取函数 + * @param {CSPlayerPawn} player - 玩家实体 + * @param {Entity} phy - 道具实体 + */ +function Pickup(player, phy) { + + // 拾取时将当前武器切换至匕首 + const knife = player.FindWeaponBySlot(CSGearSlot.KNIFE); + if (knife && knife.IsValid()) + player.SwitchToWeapon(knife); + else return; + + Instance.EntFireAtTarget({ target: phy, input: "SetDamageFilter", value: "god" }); + Instance.EntFireAtTarget({ target: phy, input: "DisableMotion" }); + + // 初始化当前玩家状态 + playerState.set(player, { + phy: phy + }); + pickedPhy.add(phy); + + Instance.SetNextThink(Instance.GetGameTime()); +} + +/** + * 丢弃函数 + * @param {CSPlayerPawn} player - 玩家实体 + * @param {Entity} phy - 道具实体 + */ +function Drop(player, phy) { + Instance.EntFireAtTarget({ target: phy, input: "SetDamageFilter", value: "", delay: 0.5 }); + Instance.EntFireAtTarget({ target: phy, input: "EnableMotion" }); + + // 清理玩家状态 + playerState.delete(player); + pickedPhy.delete(phy); +} + +/** + * 投掷函数 + * @param {CSPlayerPawn} player - 玩家实体 + * @param {Entity} phy - 道具实体 + */ +function Throw(player, phy) { + Drop(player, phy); + + const forward = getForward(player.GetEyeAngles()); + phy.Teleport({ velocity: { + x: forward.x * 500, + y: forward.y * 500, + z: forward.z * 500 + }}); +} + +/** + * 判断玩家当前手持武器状态 + * @param {CSPlayerPawn} player - 玩家实体 + * @returns {boolean} + */ +function IsKnife(player) { + const weaponData = player.GetActiveWeapon()?.GetData(); + return weaponData?.GetType() === CSWeaponType.KNIFE; +} + +/** + * 获取向前向量 + * @param {import("cs_script/point_script").QAngle} angles - 角度 + * @returns {import("cs_script/point_script").Vector} - 向前向量 + */ +function getForward(angles) { + const pitchRadians = (angles.pitch * Math.PI) / 180; + const yawRadians = (angles.yaw * Math.PI) / 180; + const hScale = Math.cos(pitchRadians); + return { + x: Math.cos(yawRadians) * hScale, + y: Math.sin(yawRadians) * hScale, + z: -Math.sin(pitchRadians), + }; +} + +/** + * 向量加法 + * @param {import("cs_script/point_script").Vector} vec1 + * @param {import("cs_script/point_script").Vector} vec2 + * @returns {import("cs_script/point_script").Vector} + */ +function vectorAdd(vec1, vec2) { + return { x: vec1.x + vec2.x, y: vec1.y + vec2.y, z: vec1.z + vec2.z }; +} + +/** + * 向量缩放 + * @param {import("cs_script/point_script").Vector} vec + * @param {number} scale + * @returns {import("cs_script/point_script").Vector} + */ +function vectorScale(vec, scale) { + return { x: vec.x * scale, y: vec.y * scale, z: vec.z * scale }; +} \ No newline at end of file diff --git a/2001/ze_hr_dead_center/save_data.js b/2001/ze_hr_dead_center/save_data.js new file mode 100644 index 0000000..fdcd8dd --- /dev/null +++ b/2001/ze_hr_dead_center/save_data.js @@ -0,0 +1,189 @@ +import { CSPlayerPawn, Instance, CSGearSlot } from "cs_script/point_script"; + +/** + * 数据存读取脚本 + * 此脚本用于实现回合结算后仍能恢复连关数据 + * 此脚本由皮皮猫233编写 + * 2026/3/6 + */ + +let continuousSwitch = false; +const playerData = new Map(); + +Instance.OnScriptInput("SaveData", () => { + Instance.ServerCommand("say **正在保存连关数据**"); + + // 启用针对风云社的开局防尸变参数配置实体 + Instance.EntFireAtName({ name: "stage_continuous_init_zr_off_relay_fys", input: "Enable" }); + + // 清除上次的存储内容 + playerData.clear(); + + const players = /** @type {CSPlayerPawn[]} */ (Instance.FindEntitiesByClass("player")); + for (const player of players) { + if (!player || !player.IsValid() || player.GetTeamNumber() !== 3) continue; + const health = player.GetHealth(); + if (!(health > 0)) continue; + const armor = player.GetArmor(); + const weapons = FindWeapons(player); + playerData.set(player, { armor: armor, health: health, weapons: weapons }); + } +}); + +Instance.OnScriptInput("ReadData", () => { + + // 检查是否开启连关 + if (!continuousSwitch) return; + if (!playerData.size) return; + + // 尸变发生后延迟1秒将所有玩家变为人类 + Delay(1, () => { + Instance.ServerCommand("c_revive @t"); + }); + + // 延迟2秒后读取数据 + Delay(2, () => { + Instance.ServerCommand("say **正在读取连关数据**"); + const players = /** @type {CSPlayerPawn[]} */ (Instance.FindEntitiesByClass("player")); + for (const player of players) { + if (!player || !player.IsValid()) continue; + + // 检查是否被存储为上局存活的玩家 + if (playerData.has(player)) { + const properties = playerData.get(player); + player.SetArmor(properties.armor); + player.SetHealth(properties.health); + GiveWeapons(player, properties.weapons); + } else if (player.GetTeamNumber() === 3) { + player.Kill(); + } + } + playerData.clear(); + }); +}); + +Instance.OnScriptInput("Enable", () => { + continuousSwitch = true; +}); + +Instance.OnScriptInput("Disable", () => { + continuousSwitch = false; +}); + +Instance.OnRoundEnd((event) => { + if (event.winningTeam === 2) { + if (continuousSwitch) { + Instance.EntFireAtName({ name: "level_counter", input: "SetValue", value: 1 }); + + // 关闭针对风云社的开局防尸变参数配置实体 + Instance.EntFireAtName({ name: "stage_continuous_init_zr_off_relay_fys", input: "Disable" }); + } + playerData.clear(); + } +}); + +/** + * 获取当前玩家的全部武器 + * @param {CSPlayerPawn} player + */ +function FindWeapons(player) { + const weaponSlots = [CSGearSlot.RIFLE, CSGearSlot.PISTOL] + const weaponNames = ["weapon_healthshot", "weapon_molotov", "weapon_flashbang", "weapon_incgrenade", "weapon_hegrenade", "weapon_smokegrenade", "weapon_decoy", "weapon_taser"] + const weapons = []; + for (const weaponSlot of weaponSlots) { + const weapon = player.FindWeaponBySlot(weaponSlot); + if (!weapon || !weapon.IsValid()) continue; + const name = weapon.GetClassName(); + weapons.push(name); + } + for (const weaponName of weaponNames) { + const weapon = player.FindWeapon(weaponName); + if (!weapon || !weapon.IsValid()) continue; + weapons.push(weaponName); + } + return weapons; +} + +/** + * 给予玩家多种武器 + * @param {CSPlayerPawn} player + * @param {string[]} weapons + */ +function GiveWeapons(player, weapons) { + for (const weapon of weapons) { + player.DestroyWeapons(); + Delay(1, () => { + player.GiveNamedItem(weapon); + }); + } +} + +/** @type {{ time: number, callback: () => void }[]} */ +const thinkQueue = []; + +/** + * 延迟执行函数 + * @param {number} delaySeconds 延迟的秒数 + * @param {() => void} callback 回调函数 + */ +function Delay(delaySeconds, callback) { + const executeTime = Instance.GetGameTime() + delaySeconds; + QueueThink(executeTime, callback); +} + +/** + * 异步延迟函数,返回Promise + * @param {number} delaySeconds 延迟的秒数 + * @returns {Promise} + */ +function DelayAsync(delaySeconds) { + return new Promise((resolve) => { + Delay(delaySeconds, resolve); + }); +} + +/** + * 将think任务加入队列 + * @param {number} time 执行时间 + * @param {() => void} callback 回调函数 + */ +function QueueThink(time, callback) { + // 查找插入位置(按时间排序) + let insertIndex = 0; + for (let i = thinkQueue.length - 1; i >= 0; i--) { + if (thinkQueue[i].time <= time) { + insertIndex = i + 1; + break; + } + } + + // 插入到合适位置 + thinkQueue.splice(insertIndex, 0, { time, callback }); + + // 如果新任务是最早的,则更新think + if (insertIndex === 0) { + Instance.SetNextThink(time); + } +} + +/** + * Think循环处理函数 + */ +function RunThinkQueue() { + const currentTime = Instance.GetGameTime(); + + // 执行所有到期的任务 + while (thinkQueue.length > 0 && thinkQueue[0].time <= currentTime) { + const task = thinkQueue.shift(); + if (!task) continue; + task.callback(); + } + + // 更新下一次think + if (thinkQueue.length > 0) { + Instance.SetNextThink(thinkQueue[0].time); + } +} + +// 设置Think循环 +Instance.SetThink(RunThinkQueue); \ No newline at end of file diff --git a/2001/ze_hr_dead_center/sound_control.js b/2001/ze_hr_dead_center/sound_control.js new file mode 100644 index 0000000..fbce6c2 --- /dev/null +++ b/2001/ze_hr_dead_center/sound_control.js @@ -0,0 +1,239 @@ +import { Instance } from "cs_script/point_script"; + +/** + * 音频控制脚本 + * 此脚本用于控制一些特殊音频的播放 + * 此脚本大部分由DeepSeek编写 + * 2026/2/25 + */ + +/** + * ============================================================================ + * 常量定义 + * ============================================================================ + */ + +/** 主旋律变体数据结构 */ +/** + * @typedef {Object} Variant + * @property {string|string[]} first - 第一阶段音频事件名(可能为数组,表示随机池) + * @property {string[]} a - A 阶段音频事件名池 + * @property {string|string[]} b - B 阶段音频事件名(可能为数组) + */ + +/** 所有主旋律变体(编号 1~11) */ +/** @type {Record} */ +const VARIANTS = { + 1: { first: "drums01c", a: ["drums01b", "drums01d"], b: "drums01c" }, + 2: { first: "drums02c", a: ["drums02d"], b: "drums02c" }, + 3: { first: "drums3c", a: ["drums3d", "drums3f"], b: "drums3c" }, + 4: { first: "drums03a", a: ["drums03b"], b: "drums03a" }, + 5: { first: ["drums5b", "drums5d"], a: ["drums5c", "drums5e"], b: ["drums5b", "drums5d"] }, + 6: { first: ["drums08a", "drums08b"], a: ["druns08e", "drums08f"], b: ["drums08a", "drums08b"] }, + 7: { first: ["drums7a", "drums7c"], a: ["drums7b"], b: ["drums7a", "drums7c"] }, + 8: { first: "drums8b", a: ["drums8c"], b: "drums8b" }, + 9: { first: "drums09c", a: ["drums09d"], b: "drums09c" }, + 10: { first: "drums10b", a: ["drums10c"], b: "drums10b" }, + 11: { first: "drums11c", a: ["drums11d"], b: "drums11c" }, +}; + +/** 商场特色层 Intro 池(也作为 B 段池) */ +/** @type {string[]} */ +const MALL_INTRO = [ + "banjo_01a_02", "banjo_01a_03", "banjo_01a_04", "banjo_01a_05", "banjo_01a_06", + "banjo_01b_01", "banjo_01b_03", "banjo_01b_04" +]; + +/** 商场特色层 A 段池 */ +/** @type {string[]} */ +const MALL_A = [ + "banjo_02_01", "banjo_02_02", "banjo_02_03", "banjo_02_04", "banjo_02_05", + "banjo_02_06", "banjo_02_07", "banjo_02_08", "banjo_02_09", "banjo_02_10", + "banjo_02_13", "banjo_02_14", "banjo_02_15" +]; + +/** 商场特色层 B 段池(复用 Intro 池) */ +const MALL_B = MALL_INTRO; + +/** + * ============================================================================ + * 状态变量 + * ============================================================================ + */ + +/** @type {boolean} 是否正在播放尸潮音乐 */ +let active = false; + +/** @type {number} 当前主旋律变体编号 */ +let currentVariantId = 1; + +/** @type {Variant} 当前主旋律变体数据 */ +let currentVariant = VARIANTS[1]; + +/** @type {'FIRST'|'A'|'B'} 当前主旋律阶段 */ +let mainPhase = 'FIRST'; + +/** @type {'INTRO'|'A'|'B'} 商场特色层当前阶段(仅在 Mall 模式使用) */ +let mallPhase = 'INTRO'; + +/** @type {boolean} 是否启用了商场特色层 */ +let mallEnabled = false; + +/** + * ============================================================================ + * 对外接口(输入事件处理) + * ============================================================================ + */ + +/** 启动普通尸潮音乐(仅主旋律 + 引子) */ +Instance.OnScriptInput("StartSoundZombat", () => { + stopAll(); + active = true; + mallEnabled = false; + + // 播放引子(实体已预设好音频事件) + Instance.EntFireAtName({ name: "bgm_zombat_intro", input: "StartSound" }); + + // 主旋律从变体1的FIRST阶段开始 + playMain(1, 'FIRST'); +}); + +/** 启动商场特色尸潮音乐(主旋律 + 引子 + 商场特色层) */ +Instance.OnScriptInput("StartSoundZombatMall", () => { + stopAll(); + active = true; + mallEnabled = true; + + // 引子 + Instance.EntFireAtName({ name: "bgm_zombat_intro", input: "StartSound" }); + + // 主旋律从变体1的FIRST阶段开始 + playMain(1, 'FIRST'); + + // 商场特色层从 Intro 阶段开始(随机选一个) + playMall('INTRO'); +}); + +/** 停止所有音乐 */ +Instance.OnScriptInput("StopSoundZombat", () => { + stopAll(); +}); + +/** 主旋律一个片段播放完毕 */ +Instance.OnScriptInput("ZombatMainFinish", () => { + if (!active) return; + + // 50% 概率切换变体 + const shouldSwitch = Math.random() < 0.5; + if (shouldSwitch) { + // 随机选择新变体(1~11) + const newVariantId = Math.floor(Math.random() * 11) + 1; + playMain(newVariantId, 'FIRST'); + } else { + // 继续当前变体的下一个阶段 + if (mainPhase === 'FIRST') { + playMain(currentVariantId, 'A'); + } else if (mainPhase === 'A') { + playMain(currentVariantId, 'B'); + } else { // 'B' + playMain(currentVariantId, 'A'); + } + } +}); + +/** 商场特色层一个片段播放完毕 */ +Instance.OnScriptInput("ZombatMallFinish", () => { + if (!active || !mallEnabled) return; + + if (mallPhase === 'INTRO') { + playMall('A'); + } else if (mallPhase === 'A') { + playMall('B'); + } else { // 'B' + playMall('A'); + } +}); + +/** 新回合开始:重置所有状态 */ +Instance.OnRoundStart(() => { + stopAll(); + currentVariantId = 1; + currentVariant = VARIANTS[1]; + mainPhase = 'FIRST'; + mallPhase = 'INTRO'; +}); + +/** + * ============================================================================ + * 内部辅助函数 + * ============================================================================ + */ + +/** + * 停止所有音频实体,并重置活动标志 + */ +function stopAll() { + Instance.EntFireAtName({ name: "bgm_zombat_intro", input: "StopSound" }); + Instance.EntFireAtName({ name: "bgm_zombat_main", input: "StopSound" }); + Instance.EntFireAtName({ name: "bgm_zombat_mall", input: "StopSound" }); + active = false; + mallEnabled = false; +} + +/** + * 从可能为数组或字符串的项中随机选择一个 + * @param {string|string[]} item - 单个字符串或字符串数组 + * @returns {string} 随机选取的字符串 + */ +function randomFrom(item) { + return Array.isArray(item) ? item[Math.floor(Math.random() * item.length)] : item; +} + +/** + * 播放主旋律的指定阶段 + * @param {number} variantId - 变体编号(1~11) + * @param {'FIRST'|'A'|'B'} phase - 要播放的阶段 + */ +function playMain(variantId, phase) { + const variant = VARIANTS[variantId]; + let file; + if (phase === 'FIRST') { + file = randomFrom(variant.first); + } else if (phase === 'A') { + file = randomFrom(variant.a); + } else { // 'B' + file = randomFrom(variant.b); + } + Instance.EntFireAtName({ name: "bgm_zombat_main", input: "SetSoundEventName", value: file }); + Instance.EntFireAtName({ name: "bgm_zombat_main", input: "StartSound" }); + currentVariantId = variantId; + currentVariant = variant; + mainPhase = phase; +} + +/** + * 播放商场特色层的指定阶段 + * @param {'INTRO'|'A'|'B'} phase - 要播放的阶段 + */ +function playMall(phase) { + let file; + if (phase === 'INTRO') { + file = randomFrom(MALL_INTRO); + } else if (phase === 'A') { + file = randomFrom(MALL_A); + } else { // 'B' + file = randomFrom(MALL_B); + } + Instance.EntFireAtName({ name: "bgm_zombat_mall", input: "SetSoundEventName", value: file }); + Instance.EntFireAtName({ name: "bgm_zombat_mall", input: "StartSound" }); + mallPhase = phase; +} + +Instance.OnRoundEnd((event) => { + Instance.EntFireAtName({ name: "bgm_env", input: "PauseSound" }); + if (event.winningTeam === 2) { + stopAll(); + Instance.EntFireAtName({ name: "bgm_*", input: "StopSound" }); + Instance.EntFireAtName({ name: "bgm_fail", input: "StartSound" }); + } +}); \ No newline at end of file diff --git a/2001/ze_hr_dead_center/vpk_load_detect.js b/2001/ze_hr_dead_center/vpk_load_detect.js new file mode 100644 index 0000000..aac5bb8 --- /dev/null +++ b/2001/ze_hr_dead_center/vpk_load_detect.js @@ -0,0 +1,328 @@ +import { Instance } from "cs_script/point_script"; + +/** + * vpk地图热加载检测脚本 + * 此脚本用于检测vpk加载是否完全,防止出现玩家集体掉入虚空的问题 + * 此脚本由皮皮猫233编写 + * 2026/2/22 + */ + +/** + * 检测位置对象 + * @typedef {Object} DetectPosition + * @property {Object.} start 起始位置 + * @property {Object.} end 结束位置 + */ + +const stageList = ["hotel", "mall", "streets", "atrium"]; +/** @type {DetectPosition} */ +const detectPosition = { + start: { + hotel: { x: 616, y: 5720, z: 2425 }, + streets: { x: 2368, y: 5184, z: 449 }, + mall: { x: 6824, y: -1424, z: 25 }, + atrium: { x: -2256, y: -5008, z: 537 }, + }, + end: { + hotel: { x: 616, y: 5720, z: 2823 }, + streets: { x: 2368, y: 5184, z: 447 }, + mall: { x: 6824, y: -1424, z: 23 }, + atrium: { x: -2256, y: -5008, z: 535 }, + } +}; +let loadDetectedTimes = 0; +let currentLoadStage = /** @type {undefined|string|null} */ (null); +let unloadDetectedTimes = 0; +const unloadQueue = /** @type {Array} */ ([]); +let currentUnloadStage = /** @type {undefined|string|null} */ (null); +let enableChatCommand = false; +let autoReloadSwitch = false; + +Instance.OnScriptInput("LoadStageHotel", () => { + StartLoadStage("hotel"); +}); + +Instance.OnScriptInput("LoadStageStreets", () => { + StartLoadStage("streets"); +}); + +Instance.OnScriptInput("LoadStageMall", () => { + StartLoadStage("mall"); +}); + +Instance.OnScriptInput("LoadStageAtrium", () => { + StartLoadStage("atrium"); +}); + +Instance.OnPlayerChat((event) => { + if (autoReloadSwitch === true && event.text === "!关闭自动重载") { + Delay(1, () => { + Instance.ServerCommand("say **已关闭自动重载功能**"); + }); + autoReloadSwitch = false; + } + if (!enableChatCommand) return; + if (event.text === "!开启自动重载") { + Delay(1, () => { + Instance.ServerCommand("say **已开启自动重载功能**"); + }); + autoReloadSwitch = true; + enableChatCommand = false; + } +}); + +Instance.OnRoundStart(() => { + loadDetectedTimes = 0; + currentLoadStage = null; + unloadDetectedTimes = 0; + unloadQueue.length = 0; + currentUnloadStage = null; + thinkQueue.length = 0; + enableChatCommand = false; +}); + +Instance.OnRoundEnd(() => { + loadDetectedTimes = 0; + currentLoadStage = null; + unloadDetectedTimes = 0; + unloadQueue.length = 0; + currentUnloadStage = null; + thinkQueue.length = 0; + enableChatCommand = false; +}); + +/** + * 开始加载关卡 + * @param {string} stage + */ +function StartLoadStage(stage) { + currentLoadStage = stage; + UnloadOtherStages(stage); +} + +/** + * 关卡加载检测 + * @param {string|null|undefined} stage + */ +function LoadStage(stage) { + if (!stage || stage !== currentLoadStage) return; + + // 检测实体是否成功加载 + const detectedEntity = Instance.FindEntityByName("stage_" + stage + "_auto_relay_cs2"); + if (detectedEntity && detectedEntity.IsValid()) { + + // 检测世界碰撞是否加载 + const allEntities = Instance.FindEntitiesByName(""); + const result = Instance.TraceLine({ + start: detectPosition.start[stage], + end: detectPosition.end[stage], + ignoreEntity: allEntities, + ignorePlayers: true + }); + if (result.didHit && result.hitEntity?.IsWorld()) { + + // 检测次数少于1则直接初始化对应关卡 + if (loadDetectedTimes === 0) { + Instance.EntFireAtName({ name: "stage_" + stage + "_init_relay", input: "Trigger" }); + } else { + Delay(5, () => { + Instance.EntFireAtName({ name: "stage_" + stage + "_init_relay", input: "Trigger" }); + }); + } + loadDetectedTimes = 0; + currentLoadStage = null; + } else ReloadStage(stage); + } else ReloadStage(stage); +} + +/** + * 延迟1秒后重新检测 + * @param {string} stage + */ +function ReloadStage(stage) { + // 第一次重检测时触发加载命令 + if (loadDetectedTimes === 0) { + Instance.ServerCommand("say **正在加载地图,加载时间可能较长,请耐心等待**"); + Instance.EntFireAtName({ name: "LoadVpk_loadunload_*_" + currentLoadStage, input: "StartSpawnGroupLoad" }); + } + Delay(1, () => LoadStage(stage)); + if (loadDetectedTimes < 30) { + loadDetectedTimes ++; + } else { + if (autoReloadSwitch) { + loadDetectedTimes = 0; + Instance.ServerCommand('say **正在尝试重新加载**'); + } else { + loadDetectedTimes = 25; + Instance.ServerCommand('say **当前关卡加载时间过长,请耐心等待或输入"!开启自动重载"开启自动重载功能**'); + enableChatCommand = true; + } + } +} + +/** + * 卸载其他关卡 + * @param {string} stage + */ +function UnloadOtherStages(stage) { + const allStages = new Set(stageList); + allStages.delete(stage); + const stages = Array.from(allStages); + + // 清空队列并添加新任务 + unloadQueue.length = 0; + unloadQueue.push(...stages); + + // 重置状态 + currentUnloadStage = null; + unloadDetectedTimes = 0; + + UnloadNextStage(); +} + +/** + * 卸载下一个关卡 + */ +function UnloadNextStage() { + + // 卸载完毕后开始加载关卡 + if (unloadQueue.length === 0) { + currentUnloadStage = null; + // 假设直接跳过卸载流程则直接加载关卡 + if (unloadDetectedTimes === 0) { + LoadStage(currentLoadStage); + } else { + Delay(5, () => { + LoadStage(currentLoadStage); + }); + } + return; + } + + // 获取并移除队列中的第一个关卡 + currentUnloadStage = /** @type {string} */ (unloadQueue.shift()); + unloadDetectedTimes = 0; + + // 检测卸载是否完成 + CheckUnloadStage(currentUnloadStage); + return; +} + +/** + * 检测关卡是否已卸载 + * @param {string} stage 关卡名称 + */ +function CheckUnloadStage(stage) { + + // 如果当前卸载的关卡已经改变,则停止检测 + if (currentUnloadStage !== stage) { + return; + } + + const detectedEntity = Instance.FindEntityByName("stage_" + stage + "_auto_relay_cs2"); + + // 检测实体是否卸载 + if (!detectedEntity) { + const allEntities = Instance.FindEntitiesByName(""); + const result = Instance.TraceLine({ + start: detectPosition.start[stage], + end: detectPosition.end[stage], + ignoreEntity: allEntities, + ignorePlayers: true + }); + + // 检测碰撞是否成功卸载 + if (!result.didHit) { + UnloadNextStage(); + + } else RetryUnloadStage(stage); + } else RetryUnloadStage(stage); +} + +/** + * 重新检测卸载状态 + * @param {string} stage 关卡名称 + */ +function RetryUnloadStage(stage) { + + // 第一次重卸载检测时执行卸载命令 + if (unloadDetectedTimes === 0) { + Instance.EntFireAtName({ name: "LoadVpk_loadunload_*_" + currentUnloadStage, input: "StartSpawnGroupUnload" }); + } + + unloadDetectedTimes ++; + if (unloadDetectedTimes >= 10) { + Instance.ServerCommand("say **部分关卡卸载超时,正在跳过卸载该关卡**"); + UnloadNextStage(); + } else Delay(1, () => CheckUnloadStage(stage)); +} + +/** @type {{ time: number, callback: () => void }[]} */ +const thinkQueue = []; + +/** + * 延迟执行函数 + * @param {number} delaySeconds 延迟的秒数 + * @param {() => void} callback 回调函数 + */ +function Delay(delaySeconds, callback) { + const executeTime = Instance.GetGameTime() + delaySeconds; + QueueThink(executeTime, callback); +} + +/** + * 异步延迟函数,返回Promise + * @param {number} delaySeconds 延迟的秒数 + * @returns {Promise} + */ +function DelayAsync(delaySeconds) { + return new Promise((resolve) => { + Delay(delaySeconds, resolve); + }); +} + +/** + * 将think任务加入队列 + * @param {number} time 执行时间 + * @param {() => void} callback 回调函数 + */ +function QueueThink(time, callback) { + // 查找插入位置(按时间排序) + let insertIndex = 0; + for (let i = thinkQueue.length - 1; i >= 0; i--) { + if (thinkQueue[i].time <= time) { + insertIndex = i + 1; + break; + } + } + + // 插入到合适位置 + thinkQueue.splice(insertIndex, 0, { time, callback }); + + // 如果新任务是最早的,则更新think + if (insertIndex === 0) { + Instance.SetNextThink(time); + } +} + +/** + * Think循环处理函数 + */ +function RunThinkQueue() { + const currentTime = Instance.GetGameTime(); + + // 执行所有到期的任务 + while (thinkQueue.length > 0 && thinkQueue[0].time <= currentTime) { + const task = thinkQueue.shift(); + if (!task) continue; + task.callback(); + } + + // 更新下一次think + if (thinkQueue.length > 0) { + Instance.SetNextThink(thinkQueue[0].time); + } +} + +// 设置Think循环 +Instance.SetThink(RunThinkQueue); \ No newline at end of file