feat(skill): add ultimate skill camera and shake effects

add camera focus and screen shake effects for ultimate skills that start with "max" prefix,
add CameraFX utility class for targeted camera zoom and reset functionality
This commit is contained in:
panFD
2026-09-13 15:55:33 +08:00
parent ed9232cf0b
commit 5f511ddcb4
3 changed files with 114 additions and 0 deletions

View File

@@ -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;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "82e59b46-a184-4a0f-af21-8bbb6a014e6b",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -17,6 +17,8 @@ import { MissionEconomy } from "../map/MissionEconomy";
import { mLogger } from "../common/Logger"; import { mLogger } from "../common/Logger";
import { buffManager } from "./BuffManager"; import { buffManager } from "./BuffManager";
import { BuffList } from "../common/config/BuffSet"; 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); 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 中的普通技能 // 因为 castSkill 只由 update 循环调用,处理的必然是 heroAttrs.skills 中的普通技能
// 特殊触发技能(call/dead/atking等)走的是 forceCastTriggerSkill,不会调用 castSkill // 特殊触发技能(call/dead/atking等)走的是 forceCastTriggerSkill,不会调用 castSkill
// 召唤类技能(summon_count>1)不再计入普攻链,避免触发 atking 附加技能 // 召唤类技能(summon_count>1)不再计入普攻链,避免触发 atking 附加技能
@@ -445,6 +459,12 @@ export class SCastSystem extends ecs.ComblockSystem implements ecs.ISystemUpdate
}, i * FightSet.SUMMON_INTERVAL); }, i * FightSet.SUMMON_INTERVAL);
} }
}, delay); }, delay);
// 3. 命中瞬间震屏:与技能实体生成时机对齐(前摇结束时)
if (isMaxSkill) {
ScreenShake.shake(8, 0.3);
}
heroAttrs.triggerSkillCD(s_uuid); heroAttrs.triggerSkillCD(s_uuid);
} }