PassCardManager.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. import { _decorator, Component, Node, Label, Sprite, Button, resources, SpriteFrame, tween, Vec3, UIOpacity, UITransform, Color } from 'cc';
  2. const { ccclass, property } = _decorator;
  3. /**
  4. * 通行证数据接口
  5. */
  6. interface PassCardData {
  7. name: string; // 人物姓名
  8. room: string; // 房间号
  9. id: string; // 身份ID
  10. reason: string; // 原因
  11. characterId: number; // 角色ID,用于加载头像
  12. }
  13. @ccclass('PassCardManager')
  14. export class PassCardManager extends Component {
  15. @property({
  16. type: Node,
  17. tooltip: '通行证面板节点'
  18. })
  19. passCardPanel: Node = null;
  20. @property({
  21. type: Label,
  22. tooltip: '姓名标签'
  23. })
  24. nameLabel: Label = null;
  25. @property({
  26. type: Label,
  27. tooltip: '房间号标签'
  28. })
  29. roomLabel: Label = null;
  30. @property({
  31. type: Label,
  32. tooltip: '身份ID标签'
  33. })
  34. idLabel: Label = null;
  35. @property({
  36. type: Label,
  37. tooltip: '原因标签'
  38. })
  39. reasonLabel: Label = null;
  40. @property({
  41. type: Sprite,
  42. tooltip: '头像显示组件'
  43. })
  44. avatarSprite: Sprite = null;
  45. @property({
  46. type: Button,
  47. tooltip: '关闭按钮'
  48. })
  49. closeButton: Button = null;
  50. @property({
  51. tooltip: '通行证面板动画时长(秒)',
  52. range: [0.1, 2.0, 0.1]
  53. })
  54. animDuration: number = 0.5;
  55. @property({
  56. type: Node,
  57. tooltip: '卡片阴影节点(可选)'
  58. })
  59. cardShadow: Node = null;
  60. // 当前通行证数据
  61. private currentPassData: PassCardData = null;
  62. // 用于动画控制的UIOpacity组件
  63. private passCardOpacity: UIOpacity = null;
  64. private shadowOpacity: UIOpacity = null;
  65. start() {
  66. // 初始隐藏通行证面板
  67. if (this.passCardPanel) {
  68. this.passCardPanel.active = false;
  69. // 确保面板有UIOpacity组件
  70. this.passCardOpacity = this.passCardPanel.getComponent(UIOpacity);
  71. if (!this.passCardOpacity) {
  72. this.passCardOpacity = this.passCardPanel.addComponent(UIOpacity);
  73. }
  74. // 如果有阴影节点,初始化它
  75. if (this.cardShadow) {
  76. this.shadowOpacity = this.cardShadow.getComponent(UIOpacity);
  77. if (!this.shadowOpacity) {
  78. this.shadowOpacity = this.cardShadow.addComponent(UIOpacity);
  79. }
  80. this.cardShadow.active = false;
  81. } else {
  82. // 自动创建阴影节点
  83. this.createShadowNode();
  84. }
  85. }
  86. // 注册关闭按钮点击事件
  87. if (this.closeButton) {
  88. // 先移除可能已存在的事件监听(防止重复注册)
  89. this.closeButton.node.off(Button.EventType.CLICK, this.onCloseButtonClick, this);
  90. // 重新注册点击事件
  91. this.closeButton.node.on(Button.EventType.CLICK, this.onCloseButtonClick, this);
  92. console.log('关闭按钮事件已注册');
  93. } else {
  94. console.error('关闭按钮未设置');
  95. }
  96. }
  97. /**
  98. * 创建卡片阴影节点
  99. */
  100. private createShadowNode(): void {
  101. if (!this.passCardPanel || this.cardShadow) return;
  102. // 创建阴影节点
  103. this.cardShadow = new Node('CardShadow');
  104. this.passCardPanel.parent.insertChild(this.cardShadow, this.passCardPanel.getSiblingIndex());
  105. // 设置阴影大小
  106. const cardTransform = this.passCardPanel.getComponent(UITransform);
  107. if (cardTransform) {
  108. const shadowTransform = this.cardShadow.addComponent(UITransform);
  109. shadowTransform.width = cardTransform.width * 0.9;
  110. shadowTransform.height = cardTransform.height * 0.9;
  111. }
  112. // 添加阴影图像
  113. const shadowSprite = this.cardShadow.addComponent(Sprite);
  114. shadowSprite.color = new Color(0, 0, 0, 180);
  115. // 添加透明度控制
  116. this.shadowOpacity = this.cardShadow.addComponent(UIOpacity);
  117. this.shadowOpacity.opacity = 0;
  118. this.cardShadow.active = false;
  119. }
  120. /**
  121. * 关闭按钮点击事件处理
  122. */
  123. private onCloseButtonClick(): void {
  124. console.log('关闭按钮被点击');
  125. this.hidePassCard();
  126. }
  127. /**
  128. * 显示通行证
  129. * @param passData 通行证数据
  130. */
  131. public showPassCard(passData: PassCardData): void {
  132. console.log('PassCardManager.showPassCard 被调用:', passData);
  133. if (!this.passCardPanel) {
  134. console.error('通行证面板未设置');
  135. return;
  136. }
  137. // 保存当前数据
  138. this.currentPassData = passData;
  139. // 更新UI显示
  140. this.updatePassCardUI();
  141. // 显示面板并播放动画
  142. this.passCardPanel.active = true;
  143. this.playShowAnimation();
  144. console.log('通行证面板已显示');
  145. // 确保阴影也显示
  146. if (this.cardShadow) {
  147. this.cardShadow.active = true;
  148. }
  149. }
  150. /**
  151. * 播放显示动画
  152. */
  153. private playShowAnimation(): void {
  154. // 重置面板的初始状态
  155. this.passCardPanel.setScale(new Vec3(0.6, 0.6, 1));
  156. this.passCardPanel.setPosition(new Vec3(200, -100, 0)); // 从更远的地方传递过来
  157. this.passCardPanel.setRotationFromEuler(new Vec3(0, 0, -15)); // 初始旋转角度
  158. // 设置初始透明度
  159. if (this.passCardOpacity) {
  160. this.passCardOpacity.opacity = 0;
  161. }
  162. // 设置阴影初始状态
  163. if (this.cardShadow && this.shadowOpacity) {
  164. this.cardShadow.setPosition(new Vec3(210, -105, 0)); // 稍微偏移
  165. this.cardShadow.setScale(new Vec3(0.6, 0.6, 1));
  166. this.cardShadow.setRotationFromEuler(new Vec3(0, 0, -15));
  167. this.shadowOpacity.opacity = 0;
  168. }
  169. // 创建动画序列 - 模拟传递过来的感觉
  170. tween(this.passCardPanel)
  171. // 第一阶段:接近动作,带有轻微旋转
  172. .to(this.animDuration * 0.7, {
  173. scale: new Vec3(1.05, 1.05, 1),
  174. position: new Vec3(10, 5, 0),
  175. eulerAngles: new Vec3(0, 0, 5) // 轻微旋转到反方向
  176. }, {
  177. easing: 'quartOut'
  178. })
  179. // 第二阶段:最终位置,带有轻微的回弹效果
  180. .to(this.animDuration * 0.3, {
  181. scale: new Vec3(1, 1, 1),
  182. position: new Vec3(0, 0, 0),
  183. eulerAngles: new Vec3(0, 0, 0)
  184. }, {
  185. easing: 'backOut'
  186. })
  187. // 第三阶段:添加轻微的抖动效果,模拟卡片放在桌面上的感觉
  188. .call(() => {
  189. this.addShakeEffect();
  190. })
  191. .start();
  192. // 为阴影创建跟随动画
  193. if (this.cardShadow) {
  194. tween(this.cardShadow)
  195. // 第一阶段
  196. .to(this.animDuration * 0.7, {
  197. scale: new Vec3(1.05, 1.05, 1),
  198. position: new Vec3(15, 2, 0), // 稍微偏移以产生阴影效果
  199. eulerAngles: new Vec3(0, 0, 5)
  200. }, {
  201. easing: 'quartOut'
  202. })
  203. // 第二阶段
  204. .to(this.animDuration * 0.3, {
  205. scale: new Vec3(1, 1, 1),
  206. position: new Vec3(5, -3, 0), // 保持偏移
  207. eulerAngles: new Vec3(0, 0, 0)
  208. }, {
  209. easing: 'backOut'
  210. })
  211. .start();
  212. // 阴影透明度动画
  213. if (this.shadowOpacity) {
  214. tween(this.shadowOpacity)
  215. .delay(this.animDuration * 0.2)
  216. .to(this.animDuration * 0.5, { opacity: 100 }) // 半透明阴影
  217. .start();
  218. }
  219. }
  220. // 单独为透明度创建动画
  221. if (this.passCardOpacity) {
  222. tween(this.passCardOpacity)
  223. .to(this.animDuration * 0.5, { opacity: 255 })
  224. .start();
  225. }
  226. }
  227. /**
  228. * 添加轻微的抖动效果
  229. */
  230. private addShakeEffect(): void {
  231. // 创建一个轻微的抖动效果,模拟卡片被放置的震感
  232. const originalPos = new Vec3(0, 0, 0);
  233. const shakeAmount = 1.5; // 抖动幅度
  234. tween(this.passCardPanel)
  235. .to(0.05, { position: new Vec3(shakeAmount, 0, 0) })
  236. .to(0.05, { position: new Vec3(-shakeAmount, 0, 0) })
  237. .to(0.05, { position: new Vec3(shakeAmount * 0.5, 0, 0) })
  238. .to(0.05, { position: new Vec3(-shakeAmount * 0.5, 0, 0) })
  239. .to(0.05, { position: originalPos })
  240. .start();
  241. // 同时给阴影添加抖动效果
  242. if (this.cardShadow) {
  243. const shadowOriginalPos = new Vec3(5, -3, 0); // 阴影的原始位置
  244. tween(this.cardShadow)
  245. .to(0.05, { position: new Vec3(shadowOriginalPos.x + shakeAmount, shadowOriginalPos.y, 0) })
  246. .to(0.05, { position: new Vec3(shadowOriginalPos.x - shakeAmount, shadowOriginalPos.y, 0) })
  247. .to(0.05, { position: new Vec3(shadowOriginalPos.x + shakeAmount * 0.5, shadowOriginalPos.y, 0) })
  248. .to(0.05, { position: new Vec3(shadowOriginalPos.x - shakeAmount * 0.5, shadowOriginalPos.y, 0) })
  249. .to(0.05, { position: shadowOriginalPos })
  250. .start();
  251. }
  252. }
  253. /**
  254. * 隐藏通行证
  255. */
  256. public hidePassCard(): void {
  257. console.log('hidePassCard 被调用');
  258. if (this.passCardPanel) {
  259. // 播放隐藏动画
  260. this.playHideAnimation(() => {
  261. this.passCardPanel.active = false;
  262. console.log('通行证面板已隐藏');
  263. });
  264. } else {
  265. console.error('通行证面板未设置,无法隐藏');
  266. }
  267. }
  268. /**
  269. * 播放隐藏动画
  270. * @param callback 动画完成回调
  271. */
  272. private playHideAnimation(callback?: Function): void {
  273. // 创建移动和缩放动画 - 模拟被人收回的感觉
  274. tween(this.passCardPanel)
  275. // 首先上抬一点,像是被拿起
  276. .to(this.animDuration * 0.2, {
  277. position: new Vec3(0, 10, 0),
  278. eulerAngles: new Vec3(0, 0, -5)
  279. }, {
  280. easing: 'sineOut'
  281. })
  282. // 然后向远处移动,同时旋转
  283. .to(this.animDuration * 0.6, {
  284. scale: new Vec3(0.7, 0.7, 1),
  285. position: new Vec3(-180, 50, 0),
  286. eulerAngles: new Vec3(0, 0, -15)
  287. }, {
  288. easing: 'quartIn'
  289. })
  290. .call(() => {
  291. if (callback) callback();
  292. })
  293. .start();
  294. // 阴影的隐藏动画
  295. if (this.cardShadow) {
  296. tween(this.cardShadow)
  297. // 首先上抬一点,跟随卡片
  298. .to(this.animDuration * 0.2, {
  299. position: new Vec3(5, 7, 0), // 保持偏移
  300. eulerAngles: new Vec3(0, 0, -5)
  301. }, {
  302. easing: 'sineOut'
  303. })
  304. // 然后向远处移动
  305. .to(this.animDuration * 0.6, {
  306. scale: new Vec3(0.7, 0.7, 1),
  307. position: new Vec3(-175, 47, 0), // 保持偏移
  308. eulerAngles: new Vec3(0, 0, -15)
  309. }, {
  310. easing: 'quartIn'
  311. })
  312. .call(() => {
  313. if (this.cardShadow) {
  314. this.cardShadow.active = false;
  315. }
  316. })
  317. .start();
  318. // 阴影透明度动画
  319. if (this.shadowOpacity) {
  320. tween(this.shadowOpacity)
  321. .to(this.animDuration * 0.5, { opacity: 0 })
  322. .start();
  323. }
  324. }
  325. // 单独为透明度创建动画,稍微延迟开始
  326. if (this.passCardOpacity) {
  327. tween(this.passCardOpacity)
  328. .delay(this.animDuration * 0.1)
  329. .to(this.animDuration * 0.7, { opacity: 0 })
  330. .start();
  331. }
  332. }
  333. /**
  334. * 更新通行证UI
  335. */
  336. private updatePassCardUI(): void {
  337. console.log('更新通行证UI');
  338. if (!this.currentPassData) {
  339. console.error('没有通行证数据');
  340. return;
  341. }
  342. // 更新文本内容
  343. if (this.nameLabel) {
  344. this.nameLabel.string = this.currentPassData.name || '';
  345. console.log('设置姓名:', this.currentPassData.name);
  346. } else {
  347. console.error('姓名标签未设置');
  348. }
  349. if (this.roomLabel) {
  350. this.roomLabel.string = this.currentPassData.room || '';
  351. console.log('设置房间号:', this.currentPassData.room);
  352. } else {
  353. console.error('房间号标签未设置');
  354. }
  355. if (this.idLabel) {
  356. this.idLabel.string = this.currentPassData.id || '';
  357. console.log('设置ID:', this.currentPassData.id);
  358. } else {
  359. console.error('ID标签未设置');
  360. }
  361. if (this.reasonLabel) {
  362. this.reasonLabel.string = this.currentPassData.reason || '';
  363. console.log('设置原因:', this.currentPassData.reason);
  364. } else {
  365. console.error('原因标签未设置');
  366. }
  367. // 加载并显示头像
  368. this.loadAvatar(this.currentPassData.characterId);
  369. }
  370. /**
  371. * 加载角色头像
  372. * @param characterId 角色ID
  373. */
  374. private loadAvatar(characterId: number): void {
  375. console.log('加载头像:', characterId);
  376. if (!this.avatarSprite) {
  377. console.error('头像Sprite组件未设置');
  378. return;
  379. }
  380. if (characterId <= 0) {
  381. console.error('无效的角色ID:', characterId);
  382. return;
  383. }
  384. // 构建头像资源路径,加上spriteFrame后缀以直接获取SpriteFrame子资源
  385. const avatarPath = `avatars/${characterId}/avatar_${characterId}_5/spriteFrame`;
  386. console.log('尝试加载头像路径:', avatarPath);
  387. // 加载头像资源
  388. resources.load(avatarPath, SpriteFrame, (err, spriteFrame) => {
  389. if (err) {
  390. console.error(`加载头像失败: ${avatarPath}`, err);
  391. // 如果加载失败,尝试其他路径
  392. this.loadFallbackAvatar(characterId);
  393. } else {
  394. // 设置头像
  395. this.avatarSprite.spriteFrame = spriteFrame;
  396. console.log('头像加载成功: ' + avatarPath);
  397. }
  398. });
  399. }
  400. /**
  401. * 尝试加载备用头像
  402. * @param characterId 角色ID
  403. */
  404. private loadFallbackAvatar(characterId: number): void {
  405. console.log('尝试加载备用头像:', characterId);
  406. // 尝试加载备用路径
  407. const fallbackPath = `avatars/${characterId}/avatar_${characterId}_1/spriteFrame`;
  408. resources.load(fallbackPath, SpriteFrame, (err, spriteFrame) => {
  409. if (err) {
  410. console.error(`加载备用头像失败: ${fallbackPath}`);
  411. } else {
  412. this.avatarSprite.spriteFrame = spriteFrame;
  413. console.log('备用头像加载成功: ' + fallbackPath);
  414. }
  415. });
  416. }
  417. onDestroy() {
  418. // 清理事件监听
  419. if (this.closeButton) {
  420. this.closeButton.node.off(Button.EventType.CLICK, this.onCloseButtonClick, this);
  421. }
  422. }
  423. }