音效修复

master
humingxing 23 hours ago
parent 07123d2527
commit d9ba6edf90
  1. 0
      assets/bundles/audio/sfx/weapon/剑攻击.mp3
  2. 0
      assets/bundles/audio/sfx/weapon/剑攻击.mp3.meta
  3. BIN
      assets/bundles/audio/sfx/weapon/法杖攻击.mp3
  4. 1
      assets/bundles/audio/sfx/weapon/法杖攻击.mp3.meta
  5. 16
      assets/bundles/first/config/Text.json
  6. 182
      assets/script/battle/audio/BattleAudio.ts
  7. 6
      assets/script/battle/presentation/SkillVisualChannel.ts
  8. 5
      assets/script/battle/skill/SkillController.ts
  9. 6
      assets/script/interface/AutoTable.ts
  10. 5
      assets/script/ui/BattleFail/UIBattleFail.ts
  11. 3
      assets/script/ui/BattleGame/BattlePeachCoinFlyPresenter.ts
  12. 4
      assets/script/ui/BattleSuccess/UIBattleSuccess.ts
  13. 2
      assets/script/ui/PopDailyShopOpen/UIPopDailyShopOpen.ts
  14. 2
      assets/script/ui/PopSelect/UIPopSelect.ts
  15. 83
      docs/需求-音效.md

@ -0,0 +1 @@
{"ver":"1.0.0","importer":"audio-clip","imported":true,"uuid":"094dbe62-cda1-4deb-97f6-22c6ec70f132","files":[".json",".mp3"],"subMetas":{},"userData":{"downloadMode":0}}

@ -123,12 +123,24 @@
"id": 2001,
"type": 10,
"level": 0,
"weight": 3
"weight": 5
},
{
"id": 2002,
"type": 11,
"level": 0,
"weight": 2
"weight": 5
},
{
"id": 3001,
"type": 12,
"level": 0,
"weight": 15
},
{
"id": 3002,
"type": 13,
"level": 0,
"weight": 5
}
]

@ -1,5 +1,6 @@
import { TeamComp, type EntityId } from 'db://assets/mx/module/battle';
import { GAudioPath } from '../../game/ConfigRes';
import { TableNames } from '../../game/ConfigTableData';
import { getTowerLoadoutService } from '../combat/TowerLoadoutService';
import { resetTangSengAudioState } from './TangSengAudioState';
@ -22,6 +23,7 @@ type ActiveSfx = { priority: SfxPri; startedAt: number };
/**
* 战斗音效:按现有 audio 资源名映射,去叠音 / 并发上限 / 优先级 / BGM 压低。
* 普攻/技能按五行 Hero.herotype、宝石 Weapon.type 解析(现表 id 101+)。
*/
export class BattleAudio {
private static _lastPlayAt = new Map<string, number>();
@ -60,10 +62,10 @@ export class BattleAudio {
/** 进战斗预载常用音效,避免首帧 async 加载导致与动画不同步 */
private static _warmUpBattleSfx(): void {
for (const name of ['刀攻击', '枪攻击', '棍攻击', '箭攻击', '孙悟空技能', '悟净技能', '李靖技能', '阎王技能', '杨戬技能音效', '金箍棒技能', '玲珑宝塔技能', '碎片掉落']) {
for (const name of ['剑攻击', '枪攻击', '棍攻击', '箭攻击','法杖攻击', '孙悟空技能', '悟净技能', '李靖技能', '阎王技能', '杨戬技能音效', '金箍棒技能', '玲珑宝塔技能', '碎片掉落']) {
void gg.audio.preloadEffect({ name, path: GAudioPath.SfxWeapon });
}
for (const name of ['怪物受击', '合成', '格子放置', '砚台出字', '开场号角', 'BOSS出场', '危险警报']) {
for (const name of ['怪物受击', '合成', '格子放置', '武器拖动', '砚台出字', '可刷新通知', 'BOSS出场', '危险警报', '结算金币效果']) {
void gg.audio.preloadEffect({ name, path: GAudioPath.SfxUi });
}
}
@ -92,23 +94,21 @@ export class BattleAudio {
static playBajiaoFanCast(): void {
}
/** 同字升级 */
/** 同级数字合成升级 */
static playUpgrade(): void {
this._play('合成', GAudioPath.SfxUi, Pri.System, 0);
}
/** 神将组合 */
/** @deprecated 多格神将组合已废弃;若残留路径触发则同「合成」 */
static playCompose(): void {
this._play('合成', GAudioPath.SfxUi, Pri.System, 0);
}
/**
* 神将合成语音(仅有资源的英雄)
* @param weapon1Id Hero.id
* @deprecated 多格神将组合已废弃,无调用时机
* @param _weapon1Id 旧 Hero.id
*/
static playComposeVoice(weapon1Id: number): void {
const name = HERO_COMPOSE_VOICE[weapon1Id];
if (name) this._play(name, GAudioPath.SfxVoice, Pri.Voice, 1000);
static playComposeVoice(_weapon1Id: number): void {
}
/** @deprecated 新产品无此音效 */
@ -200,34 +200,45 @@ export class BattleAudio {
this._play('游戏失败', GAudioPath.SfxUi, Pri.Result, 0);
}
/** 击杀武器碎片飞出 */
/** 击杀宝石掉落飞出 */
static playFragmentDrop(): void {
this._play('碎片掉落', GAudioPath.SfxWeapon, Pri.UI, 80);
}
/** 局内波次/击杀金币飞行 */
static playCoinFly(): void {
this._play('结算金币效果', GAudioPath.SfxUi, Pri.UI, 80);
}
/**
* 普攻
* 普攻(按 Hero.herotype → 现有刀/枪/棍/箭)。
* 弹道类(水弹连发等)应在真正出手时调,勿在 PresentationCue 提前播。
* @param caster 攻击实体
* @param debounceMs 去抖;连发出手用较短间隔
*/
static playWeaponAttack(caster: EntityId): void {
static playWeaponAttack(caster: EntityId, debounceMs = 150): void {
if (!this._isLocalEntity(caster)) return;
const id = getTowerLoadoutService().getLoadout(caster)?.weapon1Id;
if (id == null) return;
const name = ATTACK_SFX[id];
if (name) this._play(name, GAudioPath.SfxWeapon, Pri.Attack, 150);
const heroId = getTowerLoadoutService().getLoadout(caster)?.weapon1Id;
if (heroId == null) return;
const name = resolveAttackSfxByHeroId(heroId);
if (!name) return;
this._play(name, GAudioPath.SfxWeapon, Pri.Attack, debounceMs);
}
/**
* 英雄主动或专属武器技能
* 英雄主动或宝石武器技能
* @param caster 施法实体
* @param isEquipSkill true=专属武器技能(金箍棒/玲珑宝塔)
* @param isEquipSkill true=宝石/武器技能
*/
static playWeaponSkill(caster: EntityId, isEquipSkill = false): void {
if (!this._isLocalEntity(caster)) return;
const id = getTowerLoadoutService().getLoadout(caster)?.weapon1Id;
if (id == null) return;
const name = isEquipSkill ? EQUIP_SKILL_SFX[id] : HERO_SKILL_SFX[id];
if (name) this._play(name, GAudioPath.SfxWeapon, Pri.Combat, 0);
const loadout = getTowerLoadoutService().getLoadout(caster);
if (!loadout) return;
const name = isEquipSkill
? resolveGemSkillSfx(loadout.weaponId)
: resolveHeroSkillSfxByHeroId(loadout.weapon1Id);
if (!name) return;
this._play(name, GAudioPath.SfxWeapon, Pri.Combat, 0);
}
/**
@ -344,47 +355,100 @@ export class BattleAudio {
}
}
/** Hero.id → 普攻音效(按 Hero.weapon:1刀 2枪 3棍 4箭) */
const ATTACK_SFX: Record<number, string> = {
1: '刀攻击',
2: '枪攻击',
3: '棍攻击',
/**
* Hero.herotype → 普攻音效。
* 1金剑 2木棍 3水法杖 4火箭 5土枪
*/
const ATTACK_BY_HEROTYPE: Record<number, string> = {
1: '剑攻击',
2: '棍攻击',
3: '法杖攻击',
4: '箭攻击',
11: '棍攻击',
12: '刀攻击',
13: '棍攻击',
14: '枪攻击',
15: '枪攻击',
16: '刀攻击',
17: '枪攻击',
18: '箭攻击',
19: '刀攻击',
20: '刀攻击',
21: '箭攻击',
22: '刀攻击',
5: '枪攻击',
};
/** Hero.id → 英雄主动技能音效 */
const HERO_SKILL_SFX: Record<number, string> = {
11: '孙悟空技能',
13: '悟净技能',
17: '杨戬技能音效',
20: '阎王技能',
22: '李靖技能',
/**
* Hero.herotype → 英雄主动技能音效(复用现有神将技能资源)。
* 1金剑阵→孙悟空 2木飞棍→悟净 3水暴风雪→阎王 4火箭雨→杨戬 5土冲锋→李靖
*/
const HERO_SKILL_BY_HEROTYPE: Record<number, string> = {
1: '孙悟空技能',
2: '悟净技能',
3: '阎王技能',
4: '杨戬技能音效',
5: '李靖技能',
};
/** Hero.id → 专属武器技能音效 */
const EQUIP_SKILL_SFX: Record<number, string> = {
11: '金箍棒技能',
22: '玲珑宝塔技能',
/**
* 五行元素 → 宝石武器技能音效(Weapon.type 2金…6土 → element 1…5)。
* 金→金箍棒 木→悟净 水→阎王 火→杨戬 土→玲珑宝塔
*/
const GEM_SKILL_BY_ELEMENT: Record<number, string> = {
1: '金箍棒技能',
2: '悟净技能',
3: '阎王技能',
4: '杨戬技能音效',
5: '玲珑宝塔技能',
};
/** Hero.id → 合成语音 */
const HERO_COMPOSE_VOICE: Record<number, string> = {
11: '孙悟空合成',
14: '哪吒合成',
17: '杨戬合成',
18: '王母合成',
19: '玉帝合成',
22: '李靖合成',
};
/**
* @param heroId Hero.id(五行 101+)
* @returns 普攻资源名;无法解析时 warn 并返回空
*/
function resolveAttackSfxByHeroId(heroId: number): string {
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, heroId);
if (!hero) {
console.warn('[BattleAudio] 普攻音效缺少 Hero', heroId);
return '';
}
const herotype = Math.floor(Number(hero.herotype));
const name = ATTACK_BY_HEROTYPE[herotype];
if (!name) {
console.warn('[BattleAudio] 普攻音效未映射 herotype', herotype, 'heroId', heroId);
return '';
}
return name;
}
/**
* @param heroId Hero.id
* @returns 英雄主动技能资源名
*/
function resolveHeroSkillSfxByHeroId(heroId: number): string {
const hero = gg.data.table.getTableData<ITableHero>(TableNames.Hero, heroId);
if (!hero) {
console.warn('[BattleAudio] 英雄技能音效缺少 Hero', heroId);
return '';
}
const herotype = Math.floor(Number(hero.herotype));
const name = HERO_SKILL_BY_HEROTYPE[herotype];
if (!name) {
console.warn('[BattleAudio] 英雄技能音效未映射 herotype', herotype, 'heroId', heroId);
return '';
}
return name;
}
/**
* @param weaponId Weapon.id(宝石);0 表示未镶嵌
* @returns 宝石技能资源名
*/
function resolveGemSkillSfx(weaponId: number): string {
if (!(weaponId > 0)) {
console.warn('[BattleAudio] 宝石技能音效缺少 weaponId');
return '';
}
const weapon = gg.data.table.getTableData<ITableWeapon>(TableNames.Weapon, weaponId);
if (!weapon) {
console.warn('[BattleAudio] 宝石技能音效缺少 Weapon', weaponId);
return '';
}
// Weapon.type:1通用 2金 3木 4水 5火 6土 → element 1–5
const element = Math.floor(Number(weapon.type)) - 1;
const name = GEM_SKILL_BY_ELEMENT[element];
if (!name) {
console.warn('[BattleAudio] 宝石技能音效未映射 Weapon.type', weapon.type, 'weaponId', weaponId);
return '';
}
return name;
}

@ -12,6 +12,7 @@ import { HeroWeaponMount, WEAPON_REST_ANGLE, BOW_ATK_REST_ANGLE, BOW_RAIN_AIM_DE
import { BattleSpinePlayback } from './BattleSpinePlayback';
import { glyphSpineHasSkin } from '../view/GlyphSpineSkin';
import { ensureSpineSkeletonDataRuntime } from 'db://assets/mx/module/lang/spineRuntimePatch';
import { BattleAudio } from '../audio/BattleAudio';
import { BOW_ATK_ARROW_SPEED, SCATTER_RANGE_MUL, getBowAtkHost } from '../combat/BowAtkRuntime';
import { GBundle, GPath } from '../../game/ConfigRes';
@ -1226,6 +1227,7 @@ export class SkillVisualChannel {
host.fillScatterDegs(aimDeg, count, stepDeg, this._scatterDegs);
this._weapon.setArrowVisible(false);
const duration = Math.max(0.12, travel / BOW_ATK_ARROW_SPEED);
let launched = 0;
for (let i = 0; i < this._scatterDegs.length; i++) {
const deg = this._scatterDegs[i];
const rad = deg * Math.PI / 180;
@ -1254,7 +1256,9 @@ export class SkillVisualChannel {
pierce,
onConsume: pierce ? undefined : this._recycleBowArrow.bind(this, clone),
});
launched++;
}
if (launched > 0) BattleAudio.playWeaponAttack(caster, 40);
}
/**
@ -1467,6 +1471,7 @@ export class SkillVisualChannel {
buff2,
onLand: land.onLand,
});
BattleAudio.playWeaponAttack(caster, 40);
return true;
}
@ -1648,6 +1653,7 @@ export class SkillVisualChannel {
const duration = Math.max(0.12, dist / WATER_BALL_SPEED);
const node = this._spawnWaterBall(this._arrowFrom);
if (!node) return false;
BattleAudio.playWeaponAttack(caster, 40);
SkillTransformDriver.animateParabola({
node,
duration,

@ -378,7 +378,10 @@ export class SkillController extends Component {
}
try {
const heroId = getTowerLoadoutService().getLoadout(event.caster)?.weapon1Id;
if (heroId !== GONG_WEAPON1_ID && !(skill instanceof Gong)) {
// 12/13/17:音效在弹道真正出手时播(水弹连发等),避免开动画就响
const atkHead = Math.floor(Number(event.params?.atkTypeHead ?? event.params?.atkType) || 0);
const deferAttackSfx = atkHead === 12 || atkHead === 13 || atkHead === 17;
if (heroId !== GONG_WEAPON1_ID && !(skill instanceof Gong) && !deferAttackSfx) {
BattleAudio.playWeaponAttack(event.caster);
}
skill.onAttack(ctx);

@ -277,7 +277,11 @@ declare global {
2、木
3、水
4、火
5、土 */
5、土
10、双倍符(数字等级+1)
11、4倍符(数字等级+1)
12、随机牌(随机转换数字属性)
13、衰弱符(对方数字等级-1) */
readonly type: number;
/** 等级 */
readonly level: number;

@ -9,7 +9,8 @@ import { Auto_BattleFail } from './Auto_BattleFail';
import { BattleSuccessCoinFlyPresenter } from '../BattleSuccess/BattleSuccessCoinFlyPresenter';
import { StatisticsType, ItemNumType } from '../../game/GameData';
import { ItemData } from '../../game/ConfigProjectData';
import { UIID, GAudioPath } from '../../game/ConfigRes';
import { UIID } from '../../game/ConfigRes';
import { BattleAudio } from '../../battle/audio/BattleAudio';
import { MatchWinStats } from '../../manager/MatchWinStats';
import { RankDataManager } from '../../manager/RankDataManager';
import { VivoAd } from '../../sdk/functions/VivoAd';
@ -73,7 +74,7 @@ export class UIBattleFail extends Auto_BattleFail {
this.btnGet.active = true;
this.btnADGet.active = true;
gg.audio.playEffect({ name: '游戏失败', path: GAudioPath.SfxUi });
BattleAudio.playFail();
const starsBefore = RankDataManager.getPlayerRankStars();
MatchWinStats.record(false, gg.game.CurentBattle?.opponentRankId ?? 0);

@ -1,5 +1,6 @@
import { Label, Node, Tween, tween, UIOpacity, Vec3 } from 'cc';
import { GEvent } from 'db://assets/mx/module/event/GEvent';
import { BattleAudio } from '../../battle/audio/BattleAudio';
import { GBundle } from '../../game/ConfigRes';
import { CoinFly } from './CoinFly';
@ -129,6 +130,7 @@ export class BattlePeachCoinFlyPresenter {
this._wavePopped = 0;
this._waveSpawnFinished = false;
this._waveAwaitFly = true;
BattleAudio.playCoinFly();
this._spawnOneWaveCoin();
}
@ -277,6 +279,7 @@ export class BattlePeachCoinFlyPresenter {
*/
playKillDrop(startWorld: Readonly<Vec3>, peachCount: number): void {
if (peachCount <= 0) return;
BattleAudio.playCoinFly();
const n = this._spawnCoinFlies(startWorld.clone(), peachCount, null, true);
if (n <= 0) {
console.log('[BattlePeachCoinFly] 击杀掉桃未生成');

@ -17,7 +17,7 @@ import { BattleSuccessCoinFlyPresenter } from './BattleSuccessCoinFlyPresenter';
import { TaskType, ItemNumType } from '../../game/GameData';
import { ItemData } from '../../game/ConfigProjectData';
import MTools from 'db://assets/mx/tools/MTools';
import { GAudioPath } from '../../game/ConfigRes';
import { BattleAudio } from '../../battle/audio/BattleAudio';
import { VivoAd } from '../../sdk/functions/VivoAd';
import { IPopAnimConfig } from 'db://assets/mx/module/ui/popAnim';
import { recordKeepHeroClearFromBattle } from '../../battle/bag/KeepHeroClearRecord';
@ -165,7 +165,7 @@ export class UIBattleSuccess extends Auto_BattleSuccess {
action_param: levelId,
});
gg.audio.playEffect({ name: '游戏胜利', path: GAudioPath.SfxUi });
BattleAudio.playVictory();
MatchWinStats.record(true, gg.game.CurentBattle?.opponentRankId ?? 0);
gg.sdk.reportDY('inLevel', `${gg.game.getReportDYRankLabel()}-胜利`);

@ -531,7 +531,7 @@ export class UIPopDailyShopOpen extends Auto_PopDailyShopOpen {
this._onOneCardClosed(index);
return;
}
gg.audio.playEffect({ name: '点击抽卡', path: GAudioPath.SfxUi });
gg.audio.playEffect({ name: '点击按钮', path: GAudioPath.SfxUi });
const targetX = openFace?.isValid ? openFace.position.x : card.position.x;
const targetY = openFace?.isValid ? openFace.position.y : card.position.y;
const skew = this._ensureSkew(card);

@ -422,7 +422,7 @@ export class UIPopSelect extends Auto_PopSelect {
this._onOpenHalfDone(gen);
return;
}
gg.audio.playEffect({ name: '点击抽卡', path: GAudioPath.SfxUi });
gg.audio.playEffect({ name: '点击按钮', path: GAudioPath.SfxUi });
const hx = this._homeScale.x;
const hy = this._homeScale.y;
const hz = this._homeScale.z;

@ -1,71 +1,86 @@
# 需求:音效映射
> 日期:2026-09-08
> 日期:2026-10-16(对齐五行数字战斗)
> 资源目录:`assets/bundles/audio/`
> 播放入口:`BattleAudio`、`gg.audio.playEffect` / `playMusic`
老项目 `sfx/battle`、`sfx/wave` 已删除。无对应新资源的旧播放点不再播,不用其它音效顶替。
老项目 `sfx/battle`、`sfx/wave` 已删除。无对应新资源的旧播放点不再播。
**当前局内**见 [需求-五行合成.md](./需求-五行合成.md):同级数字合成 + 单格上阵;普攻/技能按 **Hero.herotype / Weapon.type** 复用现有 `sfx/weapon` 资源。
## 1. BGM
| 资源 | 时机 |
|------|------|
| `bgm/首页背景音` | 大厅 |
| `bgm/局内背景音` | 进战斗 |
| `bgm/局内背景音` | 进战斗(`BattleAudio.playBgmBattle`) |
## 2. UI(`sfx/ui/`)
| 资源 | 时机 |
|------|------|
| 点击按钮 | 按钮上的 `ClickButton` 组件(`gg.audio.playClick`) |
| 点击按钮 | `ClickButton`(`gg.audio.playClick`);三选一/商店翻牌 |
| 体力消耗 | 首页开战扣体力飞行 |
| 匹配完成 | 匹配页切到「站」 |
| 可刷新通知 | 召唤按钮首次变为可刷新 |
| 合成 | 同字升级 / 神将组合 |
| 合成 | 同级数字升级(`BagMerged` / `MapUpgraded` → `playUpgrade`) |
| 怪物受击 | 本机攻击命中 |
| 抽奖转盘 | 每日商店转盘每转一格 |
| 格子放置 | 字落入格子 |
| 武器拖动 | 局内拖字开始 |
| 游戏失败 | 失败结算打开 |
| 游戏胜利 | 胜利结算打开 |
| 游戏失败 | 失败结算(`BattleAudio.playFail`) |
| 游戏胜利 | 胜利结算(`BattleAudio.playVictory`) |
| 砚台出字 | 背包刷新每冒出一个字 |
| 结算金币效果 | 胜负领取金币飞行 |
| 铲子铲地 | 铲子挖地 |
| 结算金币效果 | 胜负领取金币飞行;局内波次结算金币飞行 |
| BOSS出场 | Boss 出场弹窗 |
| 危险警报 | 怪物危机提示每轮动画 |
## 3. 语音(`sfx/voice/`)
### 2.1 现有资源、当前无播放点(勿接已删玩法)
合成神将时播对应语音(仅下列有资源):孙悟空 / 哪吒 / 杨戬 / 王母 / 玉帝 / 李靖。
| 资源 | 说明 |
|------|------|
| 开场号角 | 开场表演怪已删 |
| 铲子铲地 | 开挖/铲子玩法已删 |
| `sfx/voice/*合成` | 多格神将组合已废弃 |
## 4. 武器(`sfx/weapon/`)
## 3. 武器(`sfx/weapon/`)— 五行复用
### 4.1 普攻(按 Hero.weapon)
播放:`BattleAudio.playWeaponAttack` / `playWeaponSkill`(仅本机 `Team.ownerId`)。
| weapon | 资源 | 代表 |
|--------|------|------|
| 1 | 刀攻击 | 刀、悟能、木吒、玉帝、阎王、李靖 |
| 2 | 枪攻击 | 枪、哪吒、金吒、杨戬 |
| 3 | 棍攻击 | 棍、悟空、悟净 |
| 4 | 箭攻击 | 弓、王母、嫦娥 |
- 近战等:`SkillController` PresentationCue 时播一次
- **弹道 atk_type 12/13/17**(含水弹连发):在 `SkillVisualChannel` **真正出手**时播;连发每发一次
### 4.2 英雄主动
### 3.1 普攻(`Hero.herotype`)
| Hero.id | 资源 |
|---------|------|
| 11 悟空 | 孙悟空技能 |
| 13 悟净 | 悟净技能 |
| 17 杨戬 | 杨戬技能音效 |
| 20 阎王 | 阎王技能 |
| 22 李靖 | 李靖技能 |
| herotype | 资源 |
|----------|------|
| 1 金 | 剑攻击 |
| 2 木 | 棍攻击 |
| 3 水 | 法杖攻击 |
| 4 火 | 箭攻击 |
| 5 土 | 枪攻击 |
### 4.3 专属武器技能
### 3.2 英雄主动(`Hero.herotype` → 现有技能资源)
| Skill.id | 资源 |
|----------|------|
| 20501 如意金箍棒 | 金箍棒技能 |
| 20509 玲珑宝塔 | 玲珑宝塔技能 |
| herotype | 代表技能 | 资源 |
|----------|----------|------|
| 1 金 | 剑阵 | 孙悟空技能 |
| 2 木 | 飞棍 | 悟净技能 |
| 3 水 | 暴风雪 | 阎王技能 |
| 4 火 | 箭雨 | 杨戬技能音效 |
| 5 土 | 冲锋 | 李靖技能 |
### 3.3 宝石武器技能(`Weapon.type`:2金…6土 → element 1…5)
| element | 资源 |
|---------|------|
| 1 金 | 金箍棒技能 |
| 2 木 | 悟净技能 |
| 3 水 | 阎王技能 |
| 4 火 | 杨戬技能音效 |
| 5 土 | 玲珑宝塔技能 |
### 4.4 局内掉落
### 3.4 局内掉落
| 资源 | 时机 |
|------|------|
| 碎片掉落 | 击杀后武器碎片飞出时 |
| 碎片掉落 | 击杀后宝石飞出时 |

Loading…
Cancel
Save