diff --git a/assets/script/game/common/CameraFX.ts b/assets/script/game/common/CameraFX.ts new file mode 100644 index 00000000..05cca910 --- /dev/null +++ b/assets/script/game/common/CameraFX.ts @@ -0,0 +1,85 @@ +/** + * @file CameraFX.ts + * @description 大招镜头聚焦工具(表现层) + * + * 项目内场景 Camera 从未启用(MapViewScene.camera 可能为 null), + * 与 ScreenShake 同理改为操作地图根节点 smc.map.MapView.node: + * 以施法者位置为锚点做 scale + 位移 tween,实现"画面瞬间拉近再回弹"的镜头突进感, + * UI 层(oops.gui 挂在独立根节点)天然不受影响。 + * + * 与 ScreenShake 的区别:本工具是"有明确锚点的缩放聚焦", + * ScreenShake 是"无锚点的随机抖动",两者可叠加(大招命中瞬间聚焦回弹 + 震屏)。 + * + * 用法: + * CameraFX.focusOn(heroView.node, 1.2, 0.5); + */ +import { Node, tween, Tween, v3, Vec3, view } from "cc"; +import { smc } from "./SingletonModuleComp"; + +export class CameraFX { + /** 根节点基准状态(首次聚焦时快照:位置 + 缩放,用于回弹复位,防多次触发漂移) */ + private static basePos: Vec3 | null = null; + private static baseScale: Vec3 | null = null; + + /** + * 镜头聚焦:画面瞬间向锚点拉近,随后回弹复位 + * @param anchor 聚焦锚点节点(通常是施法者节点),以其世界坐标为画面中心 + * @param zoom 拉近倍率(建议 1.15~1.25,过大容易出戏) + * @param holdTime 聚焦保持到回弹完成的总时长(秒,建议 0.4~0.6) + */ + static focusOn(anchor: Node | null | undefined, zoom: number = 1.2, holdTime: number = 0.5) { + const root = smc.map?.MapView?.node; + if (!root || !root.isValid || !anchor || !anchor.isValid) return; + + if (!this.basePos) { + this.basePos = root.position.clone(); + this.baseScale = root.scale.clone(); + } + + // 打断进行中的聚焦(新大招覆盖旧大招,避免多英雄连续放大时 tween 叠加失控) + Tween.stopAllByTarget(root); + + // 锚点世界坐标 -> 根节点本地坐标,作为缩放后的画面中心 + const worldPos = anchor.worldPosition; + const localPos = v3(); + root.inverseTransformPoint(localPos, worldPos); + + const designSize = view.getDesignResolutionSize(); + const focusScale = this.baseScale.x * zoom; + + // 缩放会围绕根节点原点(0,0)放大画面,需反向平移让锚点保持在屏幕中心附近: + // 目标位置 = 基准位置 - 锚点本地坐标 * (zoom - 1) * 基准缩放 + // 同时按设计分辨率的一半作为屏幕中心基准,避免锚点在屏幕边缘时拉出黑边 + const offsetX = localPos.x * (zoom - 1) * this.baseScale.x; + const offsetY = localPos.y * (zoom - 1) * this.baseScale.y; + const clampX = designSize.width * 0.25; // 平移量限幅:最多偏移 1/4 屏,防边缘英雄把画面拉飞 + const clampY = designSize.height * 0.25; + const targetPos = v3( + this.basePos.x - Math.max(-clampX, Math.min(clampX, offsetX)), + this.basePos.y - Math.max(-clampY, Math.min(clampY, offsetY)), + this.basePos.z + ); + const focusScaleV3 = v3(focusScale, focusScale, this.baseScale.z); + + // 三段式节奏:快速突进(0.12s) -> 短暂停留 -> 慢速回弹(holdTime) + tween(root) + .to(0.12, { position: targetPos, scale: focusScaleV3 }, { easing: "sineOut" }) + .delay(Math.max(0.05, holdTime * 0.3)) + .to(holdTime, { position: this.basePos, scale: this.baseScale }, { easing: "sineInOut" }) + .start(); + } + + /** + * 复位镜头状态(关卡切换/战斗结束时调用,防止状态残留) + */ + static reset() { + const root = smc.map?.MapView?.node; + if (root && root.isValid && this.basePos && this.baseScale) { + Tween.stopAllByTarget(root); + root.setPosition(this.basePos); + root.setScale(this.baseScale); + } + this.basePos = null; + this.baseScale = null; + } +} diff --git a/assets/script/game/common/CameraFX.ts.meta b/assets/script/game/common/CameraFX.ts.meta new file mode 100644 index 00000000..e6800f94 --- /dev/null +++ b/assets/script/game/common/CameraFX.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "82e59b46-a184-4a0f-af21-8bbb6a014e6b", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/script/game/hero/SCastSystem.ts b/assets/script/game/hero/SCastSystem.ts index 1fd4cabb..18c2d335 100644 --- a/assets/script/game/hero/SCastSystem.ts +++ b/assets/script/game/hero/SCastSystem.ts @@ -17,6 +17,8 @@ import { MissionEconomy } from "../map/MissionEconomy"; import { mLogger } from "../common/Logger"; import { buffManager } from "./BuffManager"; import { BuffList } from "../common/config/BuffSet"; +import { CameraFX } from "../common/CameraFX"; +import { ScreenShake } from "../common/ScreenShake"; /** * ==================== 自动施法系统 ==================== @@ -409,6 +411,18 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate //播放角色攻击动画 heroView.playSkillAnm(config.act); + // 大招(主动技)专属镜头表现:判定依据为施法动画为 max 开头(普攻为 "atk") + // 镜头聚焦拉近 + 技能名横幅 + 命中瞬间震屏,三段式叠加增强打击感 + const isMaxSkill = config.act.startsWith("max"); + if (isMaxSkill) { + // 1. 镜头聚焦:画面瞬间向施法者拉近再回弹 + CameraFX.focusOn(heroView.node, 1.2, 0.5); + // 2. 技能名横幅:与触发技路径表现统一(castSkill 此前未调用) + let bannerVal = ""; + if (castTimes > 1) bannerVal = "*" + castTimes.toString(); + heroView.skill_name(bannerVal, s_uuid); + } + // 因为 castSkill 只由 update 循环调用,处理的必然是 heroAttrs.skills 中的普通技能 // 特殊触发技能(call/dead/atking等)走的是 forceCastTriggerSkill,不会调用 castSkill // 召唤类技能(summon_count>1)不再计入普攻链,避免触发 atking 附加技能 @@ -445,6 +459,12 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate }, i * FightSet.SUMMON_INTERVAL); } }, delay); + + // 3. 命中瞬间震屏:与技能实体生成时机对齐(前摇结束时) + if (isMaxSkill) { + ScreenShake.shake(8, 0.3); + } + heroAttrs.triggerSkillCD(s_uuid); }