Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
P
paopao
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
cocktail-party
paopao
Commits
f2fef882
提交
f2fef882
authored
3月 23, 2026
作者:
lidongxu
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
大屏碎裂效果完美版本
上级
16dd650a
隐藏空白字符变更
内嵌
并排
正在显示
2 个修改的文件
包含
193 行增加
和
247 行删除
+193
-247
main.js
big-screen/src/main.js
+15
-18
explosion.js
big-screen/src/renderer/explosion.js
+178
-229
没有找到文件。
big-screen/src/main.js
浏览文件 @
f2fef882
...
@@ -9,7 +9,7 @@ import { drawBubbleGrid } from './renderer/bubbleGrid.js'
...
@@ -9,7 +9,7 @@ import { drawBubbleGrid } from './renderer/bubbleGrid.js'
import
{
drawBubble3D
,
BUBBLE_RADIUS
}
from
'./renderer/bubble.js'
import
{
drawBubble3D
,
BUBBLE_RADIUS
}
from
'./renderer/bubble.js'
import
{
drawShooter
}
from
'./renderer/shooter.js'
import
{
drawShooter
}
from
'./renderer/shooter.js'
import
{
drawGameInfo
,
drawTeamResultOverlay
}
from
'./renderer/gameinfo.js'
import
{
drawGameInfo
,
drawTeamResultOverlay
}
from
'./renderer/gameinfo.js'
import
{
updateAndDrawExplosions
,
appendExplosionsFromState
,
Explosion
,
setExplosionQuality
}
from
'./renderer/explosion.js'
import
{
detectAndCreateBursts
,
updateAndDrawBursts
,
clearPrevGrid
}
from
'./renderer/explosion.js'
import
{
drawIdleScreen
}
from
'./renderer/idleScreen.js'
import
{
drawIdleScreen
}
from
'./renderer/idleScreen.js'
import
{
SCREEN_WIDTH
,
SCREEN_HEIGHT
}
from
'./constants.js'
import
{
SCREEN_WIDTH
,
SCREEN_HEIGHT
}
from
'./constants.js'
...
@@ -19,8 +19,8 @@ const ctx = canvas.getContext('2d')
...
@@ -19,8 +19,8 @@ const ctx = canvas.getContext('2d')
const
SCREEN_NAME
=
import
.
meta
.
env
.
VITE_SCREEN_NAME
||
'big-screen-1'
const
SCREEN_NAME
=
import
.
meta
.
env
.
VITE_SCREEN_NAME
||
'big-screen-1'
/** 每个玩家独立的
爆炸列表:Map<playerId, Explosion
[]> */
/** 每个玩家独立的
碎裂效果列表:Map<playerId, BubbleBurst
[]> */
const
player
Explosion
s
=
new
Map
()
const
player
Burst
s
=
new
Map
()
let
frameCount
=
0
let
frameCount
=
0
...
@@ -42,9 +42,9 @@ function applyScaler(playerCount = 1) {
...
@@ -42,9 +42,9 @@ function applyScaler(playerCount = 1) {
function
renderPlayer
(
state
,
offsetX
,
roomId
)
{
function
renderPlayer
(
state
,
offsetX
,
roomId
)
{
const
pid
=
state
.
playerId
??
1
const
pid
=
state
.
playerId
??
1
// 初始化该玩家的
爆炸
列表
// 初始化该玩家的
碎裂效果
列表
if
(
!
player
Explosions
.
has
(
pid
))
playerExplosion
s
.
set
(
pid
,
[])
if
(
!
player
Bursts
.
has
(
pid
))
playerBurst
s
.
set
(
pid
,
[])
const
explosions
=
playerExplosion
s
.
get
(
pid
)
const
bursts
=
playerBurst
s
.
get
(
pid
)
ctx
.
save
()
ctx
.
save
()
ctx
.
translate
(
offsetX
,
0
)
ctx
.
translate
(
offsetX
,
0
)
...
@@ -63,12 +63,11 @@ function renderPlayer(state, offsetX, roomId) {
...
@@ -63,12 +63,11 @@ function renderPlayer(state, offsetX, roomId) {
}
}
}
}
// 爆炸特效:消费后立即清空,避免下一帧重复添加
// 碎裂特效:通过对比前后帧grid自动检测消失的球
if
(
state
.
explosions
&&
state
.
explosions
.
length
)
{
if
(
state
.
grid
)
{
appendExplosionsFromState
(
explosions
,
state
.
explosions
)
detectAndCreateBursts
(
bursts
,
pid
,
state
.
grid
,
state
.
pushAnimOffsetY
??
0
)
state
.
explosions
=
[]
}
}
updateAndDraw
Explosions
(
ctx
,
explosion
s
)
updateAndDraw
Bursts
(
ctx
,
burst
s
)
// 射击器
// 射击器
if
(
state
.
shooter
)
{
if
(
state
.
shooter
)
{
...
@@ -196,15 +195,15 @@ function loop() {
...
@@ -196,15 +195,15 @@ function loop() {
const
teamBStates
=
states
.
filter
(
s
=>
getPlayerTeam
(
s
.
playerId
??
1
)
===
'B'
)
const
teamBStates
=
states
.
filter
(
s
=>
getPlayerTeam
(
s
.
playerId
??
1
)
===
'B'
)
const
totalSlots
=
Math
.
max
(
teamAStates
.
length
+
teamBStates
.
length
,
1
)
const
totalSlots
=
Math
.
max
(
teamAStates
.
length
+
teamBStates
.
length
,
1
)
// 人数变化时重新计算缩放
和爆炸质量
// 人数变化时重新计算缩放
if
(
totalSlots
!==
_lastPlayerCount
)
{
if
(
totalSlots
!==
_lastPlayerCount
)
{
_lastPlayerCount
=
totalSlots
_lastPlayerCount
=
totalSlots
applyScaler
(
totalSlots
)
applyScaler
(
totalSlots
)
setExplosionQuality
(
totalSlots
)
// 根据人数调整爆炸效果质量
// 清理消失玩家的碎裂列表和grid快照
// 清理消失玩家的爆炸列表
for
(
const
pid
of
playerBursts
.
keys
())
{
for
(
const
pid
of
playerExplosions
.
keys
())
{
if
(
!
states
.
find
(
s
=>
(
s
.
playerId
??
1
)
===
pid
))
{
if
(
!
states
.
find
(
s
=>
(
s
.
playerId
??
1
)
===
pid
))
{
playerExplosions
.
delete
(
pid
)
playerBursts
.
delete
(
pid
)
clearPrevGrid
(
pid
)
}
}
}
}
}
}
...
@@ -250,11 +249,9 @@ function loop() {
...
@@ -250,11 +249,9 @@ function loop() {
// ── 检测是否所有玩家都结束,显示队伍比分 ─────────────────────────────
// ── 检测是否所有玩家都结束,显示队伍比分 ─────────────────────────────
const
allGameOver
=
states
.
every
(
s
=>
s
.
isGameOver
)
const
allGameOver
=
states
.
every
(
s
=>
s
.
isGameOver
)
console
.
log
(
'[BigScreen] 游戏状态检查'
,
{
allGameOver
,
statesCount
:
states
.
length
,
totalSlots
,
canvasWidth
:
canvas
.
width
,
SCREEN_WIDTH
})
if
(
allGameOver
&&
states
.
length
>
1
)
{
if
(
allGameOver
&&
states
.
length
>
1
)
{
// 全屏显示队伍比分(传入实际的大屏宽度)
// 全屏显示队伍比分(传入实际的大屏宽度)
const
totalWidth
=
SCREEN_WIDTH
*
totalSlots
const
totalWidth
=
SCREEN_WIDTH
*
totalSlots
console
.
log
(
'[BigScreen] 显示队伍比分'
,
{
totalSlots
,
totalWidth
,
canvasWidth
:
canvas
.
width
,
states
:
states
.
length
})
ctx
.
save
()
ctx
.
save
()
ctx
.
setTransform
(
1
,
0
,
0
,
1
,
0
,
0
)
ctx
.
setTransform
(
1
,
0
,
0
,
1
,
0
,
0
)
drawTeamResultOverlay
(
ctx
,
states
,
getPlayerTeam
,
totalWidth
)
drawTeamResultOverlay
(
ctx
,
states
,
getPlayerTeam
,
totalWidth
)
...
...
big-screen/src/renderer/explosion.js
浏览文件 @
f2fef882
/**
/**
* 爆炸粒子特效 —— 与 minigame-1/js/effects/explosion.js 1:1 复刻
* 泡泡碎裂特效(大屏独立实现)
* 去除 wx 依赖,使用浏览器 Canvas 2D API
*
* 优化:批量渲染 + 动态质量调整,支持4人同屏
* 原理:大屏通过对比前后两帧的 grid 数据,检测哪些泡泡消失了,
* 在消失位置播放一次碎裂动画。完全不依赖小游戏传来的 explosions 数据。
*
* 动画过程:闪光 → 碎片四散 → 渐隐消失,只播放一次。
*/
*/
import
{
BUBBLE_RADIUS
,
BUBBLE_COLORS
}
from
'./bubble.js'
import
{
BUBBLE_RADIUS
,
BUBBLE_COLORS
,
gridToScreen
}
from
'./bubble.js'
// ─── 全局配置:根据同屏玩家数动态调整质量 ─────────────────────────────────────
const
QUALITY_CONFIG
=
{
// 单人/双人 - 高质量
low
:
{
circleCount
:
12
,
glintCount
:
5
,
sparkCount
:
5
,
ringCount
:
2
,
maxLifeBase
:
25
},
// 三人 - 中等质量
medium
:
{
circleCount
:
8
,
glintCount
:
3
,
sparkCount
:
3
,
ringCount
:
2
,
maxLifeBase
:
20
},
// 四人 - 性能优先
high
:
{
circleCount
:
6
,
glintCount
:
2
,
sparkCount
:
2
,
ringCount
:
1
,
maxLifeBase
:
18
}
}
let
currentQuality
=
'medium'
// ─── 碎片粒子 ─────────────────────────────────────────────────────────────────
/**
class
Shard
{
* 设置爆炸质量等级(根据同屏玩家数调用)
constructor
(
x
,
y
,
color
,
angle
,
speed
,
size
)
{
* @param {number} playerCount 当前游戏人数 1-4
*/
export
function
setExplosionQuality
(
playerCount
)
{
if
(
playerCount
<=
2
)
currentQuality
=
'low'
else
if
(
playerCount
===
3
)
currentQuality
=
'medium'
else
currentQuality
=
'high'
}
// ─── 圆形粒子 ─────────────────────────────────────────────────────────────────
class
CircleParticle
{
constructor
(
x
,
y
,
colorHex
,
vx
,
vy
,
radius
,
maxLife
)
{
this
.
x
=
x
this
.
x
=
x
this
.
y
=
y
this
.
y
=
y
this
.
color
=
colorHex
this
.
color
=
color
this
.
vx
=
vx
this
.
size
=
size
this
.
vy
=
vy
this
.
radius
=
radius
this
.
alpha
=
1
this
.
alpha
=
1
this
.
life
=
0
this
.
life
=
0
this
.
maxLife
=
maxLife
this
.
maxLife
=
80
+
Math
.
floor
(
Math
.
random
()
*
30
)
// 80-110帧 ≈ 1.3-1.8秒
this
.
alive
=
true
this
.
rotation
=
Math
.
random
()
*
Math
.
PI
*
2
this
.
rotSpeed
=
(
Math
.
random
()
-
0.5
)
*
0.1
this
.
gravity
=
0.04
this
.
vx
=
Math
.
cos
(
angle
)
*
speed
this
.
vy
=
Math
.
sin
(
angle
)
*
speed
}
}
update
()
{
update
()
{
this
.
vx
*=
0.92
this
.
life
++
this
.
vy
=
this
.
vy
*
0.92
+
0.2
this
.
vx
*=
0.99
this
.
vy
*=
0.99
this
.
vy
+=
this
.
gravity
this
.
x
+=
this
.
vx
this
.
x
+=
this
.
vx
this
.
y
+=
this
.
vy
this
.
y
+=
this
.
vy
this
.
life
++
this
.
rotation
+=
this
.
rotSpeed
this
.
alpha
=
Math
.
max
(
0
,
1
-
this
.
life
/
this
.
maxLife
)
this
.
size
*=
0.997
this
.
radius
=
Math
.
max
(
0.5
,
this
.
radius
*
0.97
)
const
t
=
this
.
life
/
this
.
maxLife
if
(
this
.
life
>=
this
.
maxLife
)
this
.
alive
=
false
this
.
alpha
=
t
<
0.85
?
1
-
t
*
0.2
:
Math
.
max
(
0
,
(
1
-
t
)
/
0.15
)
}
}
}
// ─── 火花线条粒子 ─────────────────────────────────────────────────────────────
class
SparkParticle
{
get
alive
()
{
constructor
(
x
,
y
,
colorHex
,
vx
,
vy
)
{
return
this
.
life
<
this
.
maxLife
this
.
x
=
x
this
.
y
=
y
this
.
color
=
colorHex
this
.
vx
=
vx
this
.
vy
=
vy
this
.
prevX
=
x
this
.
prevY
=
y
this
.
alpha
=
1
this
.
life
=
0
this
.
maxLife
=
18
+
Math
.
floor
(
Math
.
random
()
*
14
)
this
.
alive
=
true
this
.
isSpark
=
true
}
}
update
()
{
render
(
ctx
)
{
this
.
prevX
=
this
.
x
if
(
!
this
.
alive
||
this
.
alpha
<=
0
)
return
this
.
prevY
=
this
.
y
ctx
.
globalAlpha
=
this
.
alpha
this
.
vx
*=
0.88
ctx
.
fillStyle
=
this
.
color
this
.
vy
=
this
.
vy
*
0.88
+
0.15
ctx
.
save
()
this
.
x
+=
this
.
vx
ctx
.
translate
(
this
.
x
,
this
.
y
)
this
.
y
+=
this
.
vy
ctx
.
rotate
(
this
.
rotation
)
this
.
life
++
ctx
.
beginPath
()
this
.
alpha
=
Math
.
max
(
0
,
1
-
this
.
life
/
this
.
maxLife
)
ctx
.
moveTo
(
-
this
.
size
,
-
this
.
size
*
0.6
)
if
(
this
.
life
>=
this
.
maxLife
)
this
.
alive
=
false
ctx
.
lineTo
(
this
.
size
*
0.8
,
-
this
.
size
*
0.4
)
ctx
.
lineTo
(
this
.
size
*
0.5
,
this
.
size
*
0.7
)
ctx
.
lineTo
(
-
this
.
size
*
0.7
,
this
.
size
*
0.5
)
ctx
.
closePath
()
ctx
.
fill
()
ctx
.
restore
()
}
}
}
}
// ─── 冲击波圆环 ───────────────────────────────────────────────────────────────
// ─── 冲击波圆环 ───────────────────────────────────────────────────────────────
class
Shock
Ring
{
class
Ring
{
constructor
(
x
,
y
,
color
Hex
,
delay
=
0
)
{
constructor
(
x
,
y
,
color
)
{
this
.
x
=
x
this
.
x
=
x
this
.
y
=
y
this
.
y
=
y
this
.
color
=
colorHex
this
.
color
=
color
this
.
r
=
BUBBLE_RADIUS
*
0.2
this
.
r
=
BUBBLE_RADIUS
*
0.3
this
.
maxR
=
BUBBLE_RADIUS
*
2.4
this
.
maxR
=
BUBBLE_RADIUS
*
2.2
this
.
life
=
-
delay
this
.
life
=
0
this
.
maxLife
=
24
this
.
maxLife
=
40
this
.
alpha
=
0
}
this
.
alive
=
true
get
alive
()
{
return
this
.
life
<
this
.
maxLife
}
}
update
()
{
update
()
{
this
.
life
++
this
.
life
++
if
(
this
.
life
<=
0
)
return
const
t
=
this
.
life
/
this
.
maxLife
const
t
=
this
.
life
/
this
.
maxLife
this
.
r
=
BUBBLE_RADIUS
*
0.2
+
(
this
.
maxR
-
BUBBLE_RADIUS
*
0.2
)
*
t
this
.
r
=
BUBBLE_RADIUS
*
0.3
+
(
this
.
maxR
-
BUBBLE_RADIUS
*
0.3
)
*
t
this
.
alpha
=
0.9
*
(
1
-
t
)
this
.
alpha
=
0.6
*
(
1
-
t
)
if
(
this
.
life
>=
this
.
maxLife
)
this
.
alive
=
false
}
render
(
ctx
)
{
if
(
!
this
.
alive
||
this
.
alpha
<=
0
)
return
ctx
.
globalAlpha
=
this
.
alpha
ctx
.
strokeStyle
=
this
.
color
ctx
.
lineWidth
=
2
ctx
.
beginPath
()
ctx
.
arc
(
this
.
x
,
this
.
y
,
this
.
r
,
0
,
Math
.
PI
*
2
)
ctx
.
stroke
()
}
}
}
}
// ─── 单颗泡泡
爆炸 ─────
────────────────────────────────────────────────────────
// ─── 单颗泡泡
碎裂效果
────────────────────────────────────────────────────────
export
class
Explosion
{
class
BubbleBurst
{
/**
constructor
(
x
,
y
,
colorIdx
)
{
* @param {number} x 爆炸中心 x
* @param {number} y 爆炸中心 y
* @param {number|string} color 颜色索引 1-9 或直接传 '#rrggbb' 十六进制字符串
* @param {boolean} isFloating 是否为悬空掉落(较小效果)
*/
constructor
(
x
,
y
,
color
,
isFloating
=
false
)
{
this
.
alive
=
true
this
.
alive
=
true
this
.
particles
=
[]
this
.
shards
=
[]
this
.
rings
=
[]
this
.
ring
=
null
this
.
_x
=
x
this
.
_y
=
y
const
colorHex
=
typeof
color
===
'string'
&&
color
.
startsWith
(
'#'
)
const
colorHex
=
BUBBLE_COLORS
[
colorIdx
]
||
'#ffffff'
?
color
:
(
BUBBLE_COLORS
[
color
]
||
'#ffffff'
)
const
R
=
BUBBLE_RADIUS
const
R
=
BUBBLE_RADIUS
const
cfg
=
QUALITY_CONFIG
[
currentQuality
]
this
.
flashLife
=
isFloating
?
1
:
1
// 只闪一下
this
.
flashAlpha
=
1.0
this
.
_x
=
x
this
.
ring
=
new
Ring
(
x
,
y
,
colorHex
)
this
.
_y
=
y
this
.
_colorHex
=
colorHex
// ── 圆形粒子
const
circleCount
=
isFloating
?
Math
.
floor
(
cfg
.
circleCount
*
0.6
)
:
cfg
.
circleCount
for
(
let
i
=
0
;
i
<
circleCount
;
i
++
)
{
const
angle
=
(
i
/
circleCount
)
*
Math
.
PI
*
2
+
(
Math
.
random
()
-
0.5
)
*
0.8
const
speed
=
isFloating
?
(
1.5
+
Math
.
random
()
*
3
)
:
(
2.5
+
Math
.
random
()
*
5
)
const
vx
=
Math
.
cos
(
angle
)
*
speed
const
vy
=
isFloating
?
(
Math
.
sin
(
angle
)
*
speed
*
0.4
+
1.5
)
:
Math
.
sin
(
angle
)
*
speed
const
radius
=
R
*
(
isFloating
?
0.2
:
0.18
+
Math
.
random
()
*
0.28
)
const
maxLife
=
cfg
.
maxLifeBase
+
Math
.
floor
(
Math
.
random
()
*
20
)
this
.
particles
.
push
(
new
CircleParticle
(
x
,
y
,
colorHex
,
vx
,
vy
,
radius
,
maxLife
))
}
// ── 白色 & 黄色小圆粒子(高光碎片)
// 碎片:10个不规则碎块
const
glintCount
=
isFloating
?
Math
.
floor
(
cfg
.
glintCount
*
0.4
)
:
cfg
.
glintCount
const
shardCount
=
10
for
(
let
i
=
0
;
i
<
glintCount
;
i
++
)
{
for
(
let
i
=
0
;
i
<
shardCount
;
i
++
)
{
const
angle
=
Math
.
random
()
*
Math
.
PI
*
2
const
angle
=
(
i
/
shardCount
)
*
Math
.
PI
*
2
+
(
Math
.
random
()
-
0.5
)
*
0.5
const
speed
=
3
+
Math
.
random
()
*
4
const
speed
=
0.6
+
Math
.
random
()
*
1.2
const
glintColor
=
Math
.
random
()
<
0.5
?
'#ffffff'
:
'#ffe566'
const
size
=
R
*
(
0.18
+
Math
.
random
()
*
0.22
)
this
.
particles
.
push
(
this
.
shards
.
push
(
new
Shard
(
x
,
y
,
colorHex
,
angle
,
speed
,
size
))
new
CircleParticle
(
x
,
y
,
glintColor
,
Math
.
cos
(
angle
)
*
speed
,
Math
.
sin
(
angle
)
*
speed
,
R
*
0.12
,
18
+
Math
.
floor
(
Math
.
random
()
*
12
))
)
}
}
// ── 火花线条粒子
// 额外3个白色高光碎片
const
sparkCount
=
isFloating
?
Math
.
floor
(
cfg
.
sparkCount
*
0.4
)
:
cfg
.
sparkCount
for
(
let
i
=
0
;
i
<
3
;
i
++
)
{
for
(
let
i
=
0
;
i
<
sparkCount
;
i
++
)
{
const
angle
=
Math
.
random
()
*
Math
.
PI
*
2
const
angle
=
Math
.
random
()
*
Math
.
PI
*
2
const
speed
=
5
+
Math
.
random
()
*
7
const
speed
=
0.8
+
Math
.
random
()
*
1.5
const
sparkColor
=
i
%
2
===
0
?
'#ffffff'
:
colorHex
const
size
=
R
*
0.1
this
.
particles
.
push
(
this
.
shards
.
push
(
new
Shard
(
x
,
y
,
'#ffffff'
,
angle
,
speed
,
size
))
new
SparkParticle
(
x
,
y
,
sparkColor
,
Math
.
cos
(
angle
)
*
speed
,
Math
.
sin
(
angle
)
*
speed
)
)
}
// ── 冲击波圆环
if
(
!
isFloating
)
{
this
.
rings
.
push
(
new
ShockRing
(
x
,
y
,
colorHex
,
0
))
if
(
cfg
.
ringCount
>=
2
)
{
this
.
rings
.
push
(
new
ShockRing
(
x
,
y
,
'#ffffff'
,
5
))
}
}
}
}
}
update
()
{
update
()
{
if
(
this
.
flashLife
>
0
)
this
.
flashLife
--
if
(
this
.
flashAlpha
>
0
)
{
this
.
flashAlpha
-=
0.05
// 约20帧(0.33秒)渐隐
}
for
(
const
p
of
this
.
particle
s
)
{
for
(
const
s
of
this
.
shard
s
)
{
if
(
p
.
alive
)
p
.
update
()
if
(
s
.
alive
)
s
.
update
()
}
}
for
(
const
r
of
this
.
rings
)
{
if
(
r
.
alive
)
r
.
update
()
if
(
this
.
ring
&&
this
.
ring
.
alive
)
{
this
.
ring
.
update
()
}
}
if
(
const
shardsAlive
=
this
.
shards
.
some
(
s
=>
s
.
alive
)
this
.
flashLife
<=
0
&&
const
ringAlive
=
this
.
ring
&&
this
.
ring
.
alive
this
.
particles
.
every
(
p
=>
!
p
.
alive
)
&&
if
(
!
shardsAlive
&&
!
ringAlive
&&
this
.
flashAlpha
<=
0
)
{
this
.
rings
.
every
(
r
=>
!
r
.
alive
)
)
{
this
.
alive
=
false
this
.
alive
=
false
}
}
}
}
render
(
ctx
)
{
render
(
ctx
)
{
// ── 闪光光晕(简单实心圆,性能更好)
if
(
this
.
flashAlpha
>
0
)
{
if
(
this
.
flashLife
>
0
)
{
const
t
=
this
.
flashLife
/
10
ctx
.
save
()
ctx
.
save
()
ctx
.
globalAlpha
=
t
*
0.6
ctx
.
globalAlpha
=
t
his
.
flashAlpha
*
0.7
ctx
.
fillStyle
=
'#ffffff'
ctx
.
fillStyle
=
'#ffffff'
ctx
.
beginPath
()
ctx
.
beginPath
()
ctx
.
arc
(
this
.
_x
,
this
.
_y
,
BUBBLE_RADIUS
*
0.8
,
0
,
Math
.
PI
*
2
)
ctx
.
arc
(
this
.
_x
,
this
.
_y
,
BUBBLE_RADIUS
*
1.3
,
0
,
Math
.
PI
*
2
)
ctx
.
fill
()
ctx
.
globalAlpha
=
t
*
0.4
ctx
.
fillStyle
=
this
.
_colorHex
ctx
.
beginPath
()
ctx
.
arc
(
this
.
_x
,
this
.
_y
,
BUBBLE_RADIUS
*
1.4
,
0
,
Math
.
PI
*
2
)
ctx
.
fill
()
ctx
.
fill
()
ctx
.
restore
()
ctx
.
restore
()
}
}
// ── 冲击波圆环(批量渲染)
ctx
.
save
()
ctx
.
save
()
ctx
.
lineWidth
=
2.5
if
(
this
.
ring
&&
this
.
ring
.
alive
)
{
for
(
const
r
of
this
.
rings
)
{
this
.
ring
.
render
(
ctx
)
if
(
!
r
.
alive
||
r
.
alpha
<=
0
||
r
.
life
<=
0
)
continue
ctx
.
globalAlpha
=
r
.
alpha
ctx
.
strokeStyle
=
r
.
color
ctx
.
beginPath
()
ctx
.
arc
(
r
.
x
,
r
.
y
,
r
.
r
,
0
,
Math
.
PI
*
2
)
ctx
.
stroke
()
}
}
ctx
.
restore
()
ctx
.
restore
()
// ── 粒子批量渲染(圆形粒子 + 火花线条)
ctx
.
save
()
ctx
.
save
()
for
(
const
p
of
this
.
particles
)
{
for
(
const
s
of
this
.
shards
)
{
if
(
!
p
.
alive
||
p
.
alpha
<=
0
)
continue
if
(
s
.
alive
)
s
.
render
(
ctx
)
ctx
.
globalAlpha
=
p
.
alpha
if
(
p
.
isSpark
)
{
ctx
.
strokeStyle
=
p
.
color
ctx
.
lineWidth
=
1.8
ctx
.
lineCap
=
'round'
ctx
.
beginPath
()
ctx
.
moveTo
(
p
.
x
,
p
.
y
)
ctx
.
lineTo
(
p
.
prevX
,
p
.
prevY
)
ctx
.
stroke
()
}
else
{
ctx
.
fillStyle
=
p
.
color
ctx
.
beginPath
()
ctx
.
arc
(
p
.
x
,
p
.
y
,
p
.
radius
,
0
,
Math
.
PI
*
2
)
ctx
.
fill
()
}
}
}
ctx
.
restore
()
ctx
.
restore
()
ctx
.
globalAlpha
=
1
}
}
}
}
// ─── 工具函数(供 main.js 调用)──────────────────────────────────────────────
// ─── 对外接口 ─────────────────────────────────────────────────────────────────
/** 每个玩家的上一帧 grid 快照 + 总球数:Map<playerId, { grid, totalCount }> */
const
prevSnapshots
=
new
Map
()
/**
/**
*
更新并绘制爆炸列表,移除已结束的实例
*
统计 grid 中泡泡总数
*/
*/
export
function
updateAndDrawExplosions
(
ctx
,
explosionList
)
{
function
countBubbles
(
grid
)
{
if
(
!
explosionList
||
!
explosionList
.
length
)
return
let
count
=
0
for
(
let
i
=
explosionList
.
length
-
1
;
i
>=
0
;
i
--
)
{
for
(
const
row
of
grid
)
{
const
e
=
explosionList
[
i
]
if
(
!
row
)
continue
e
.
update
()
for
(
const
c
of
row
)
{
e
.
render
(
ctx
)
if
(
c
>
0
)
count
++
if
(
!
e
.
alive
)
explosionList
.
splice
(
i
,
1
)
}
}
}
return
count
}
}
/** 单玩家最大爆炸实例数 */
const
MAX_EXPLOSIONS_PER_PLAYER
=
50
/**
/**
* 根据状态中的新爆炸事件追加 Explosion 实例
* 通过对比前后两帧 grid,检测消失的泡泡并创建碎裂效果
* 小游戏端已保证每个爆炸只发送一次(待发送队列机制),这里直接添加即可
*
* 处理 pushDown 场景:消除后可能触发补行,grid 顶部插入新行导致行号整体偏移。
* 通过计算行偏移量(新增行数)来对齐对比,确保消除+补行同帧发生时碎裂效果不丢失。
*/
*/
export
function
appendExplosionsFromState
(
explosionList
,
newExplosions
)
{
export
function
detectAndCreateBursts
(
burstList
,
playerId
,
grid
,
pushAnimOffsetY
=
0
)
{
if
(
!
newExplosions
||
!
newExplosions
.
length
)
return
if
(
!
grid
||
!
grid
.
length
)
return
const
prev
=
prevSnapshots
.
get
(
playerId
)
const
currCount
=
countBubbles
(
grid
)
if
(
prev
)
{
const
prevGrid
=
prev
.
grid
// 计算行偏移:新grid比旧grid多出的行数(pushDown插入的新行数)
// pushDown 总是在顶部插入偶数行(通常是2行),所以偏移量 = grid.length - prevGrid.length
const
rowOffset
=
Math
.
max
(
0
,
grid
.
length
-
prevGrid
.
length
)
// 逐格对比(旧grid的 row 对应新grid的 row+rowOffset)
for
(
let
prevRow
=
0
;
prevRow
<
prevGrid
.
length
;
prevRow
++
)
{
const
prevRowArr
=
prevGrid
[
prevRow
]
const
currRow
=
prevRow
+
rowOffset
const
currRowArr
=
grid
[
currRow
]
if
(
!
prevRowArr
)
continue
for
(
let
col
=
0
;
col
<
prevRowArr
.
length
;
col
++
)
{
const
prevColor
=
prevRowArr
[
col
]
const
currColor
=
(
currRowArr
&&
currRowArr
[
col
])
||
0
if
(
prevColor
>
0
&&
currColor
===
0
)
{
// 球消失了,用新的行号计算屏幕位置(因为球已经被推到新位置了)
const
{
x
,
y
}
=
gridToScreen
(
currRow
,
col
)
burstList
.
push
(
new
BubbleBurst
(
x
,
y
+
(
pushAnimOffsetY
||
0
),
prevColor
))
}
}
}
}
for
(
const
{
x
,
y
,
colorHex
,
color
}
of
newExplosions
)
{
// 保存当前帧快照
if
(
explosionList
.
length
>=
MAX_EXPLOSIONS_PER_PLAYER
)
break
prevSnapshots
.
set
(
playerId
,
{
const
c
=
colorHex
||
color
||
1
grid
:
grid
.
map
(
row
=>
row
?
[...
row
]
:
[]),
explosionList
.
push
(
new
Explosion
(
x
,
y
,
c
,
false
))
totalCount
:
currCount
})
}
/**
* 更新并绘制碎裂效果列表
*/
export
function
updateAndDrawBursts
(
ctx
,
burstList
)
{
if
(
!
burstList
||
!
burstList
.
length
)
return
for
(
let
i
=
burstList
.
length
-
1
;
i
>=
0
;
i
--
)
{
const
b
=
burstList
[
i
]
b
.
update
()
b
.
render
(
ctx
)
if
(
!
b
.
alive
)
burstList
.
splice
(
i
,
1
)
}
}
}
}
/**
* 清除某个玩家的 grid 快照(玩家离开时调用)
*/
export
function
clearPrevGrid
(
playerId
)
{
prevSnapshots
.
delete
(
playerId
)
}
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论