|
@@ -2,7 +2,40 @@ import Two from "two.js";
|
2
|
2
|
|
3
|
3
|
const colorList = ['#f5222d', '#d4380d', '#d46b08', '#d48806', '#d4b106', '#7cb305', '#389e0d', '#08979c', '#096dd9', '#531dab']
|
4
|
4
|
|
5
|
|
-export default function game({ el, center, raduis, onError, onSuccess, onBingo }) {
|
|
5
|
+import backImage from './assets/BackImage/framebg.jpg'
|
|
6
|
+import wheelImage from './assets/RoundaboutImage/2-09.png';
|
|
7
|
+import cageImage from './assets/RoundaboutImage/2-13.png';
|
|
8
|
+import pedestalImage from './assets/RoundaboutImage/2-14.png'
|
|
9
|
+import countImage from './assets/RoundaboutImage/计数.png'
|
|
10
|
+import ropeImage1 from './assets/RoundaboutImage/2-15.png'
|
|
11
|
+import ropeImage2 from './assets/RoundaboutImage/2-16.png'
|
|
12
|
+import ropeImage3 from './assets/RoundaboutImage/2-17.png'
|
|
13
|
+import ropeImage4 from './assets/RoundaboutImage/2-18.png'
|
|
14
|
+import ropeImage5 from './assets/RoundaboutImage/2-19.png'
|
|
15
|
+import ropeImage6 from './assets/RoundaboutImage/2-20.png'
|
|
16
|
+import ropeImage7 from './assets/RoundaboutImage/2-21.png'
|
|
17
|
+import ropeImage8 from './assets/RoundaboutImage/2-22.png'
|
|
18
|
+import ropeImage9 from './assets/RoundaboutImage/2-23.png'
|
|
19
|
+
|
|
20
|
+const designWidth = 640; // 设计稿宽度
|
|
21
|
+const desginRadius = 246; // 设计稿轮盘半径
|
|
22
|
+const pedestalDistance = 424; // 底座与轮盘圆心距离
|
|
23
|
+const primaryColor = '#F19805'; // 设计稿主题色
|
|
24
|
+
|
|
25
|
+const scale = window.screen.width / designWidth;
|
|
26
|
+const ropeImageList = [
|
|
27
|
+ ropeImage1,
|
|
28
|
+ ropeImage2,
|
|
29
|
+ ropeImage3,
|
|
30
|
+ ropeImage4,
|
|
31
|
+ ropeImage5,
|
|
32
|
+ ropeImage6,
|
|
33
|
+ ropeImage7,
|
|
34
|
+ ropeImage8,
|
|
35
|
+ ropeImage9,
|
|
36
|
+];
|
|
37
|
+
|
|
38
|
+export default function game({ el, center, onError, onSuccess, onBingo }) {
|
6
|
39
|
|
7
|
40
|
// 是否游戏结束
|
8
|
41
|
let isFinished = false;
|
|
@@ -11,11 +44,16 @@ export default function game({ el, center, raduis, onError, onSuccess, onBingo }
|
11
|
44
|
// eslint-disable-next-line no-unused-vars
|
12
|
45
|
let isError = false;
|
13
|
46
|
|
14
|
|
- // 每个区域的旋转角度, 10 个轿厢, 分 20 等份
|
15
|
|
- const perAngle = 2 * Math.PI / 20 // 分20等份
|
|
47
|
+ // 每个区域的旋转角度, 8 个轿厢, 分 16 等份
|
|
48
|
+ const cageNum = 8;
|
|
49
|
+ const perAngle = Math.PI / cageNum // 2 * Math.PI / ( cageNum * 2 ) 分成 轿厢 * 2 的分数, 最终效果是每隔一个,显示一个轿厢
|
|
50
|
+ const offsetAngle = perAngle / 2
|
16
|
51
|
|
17
|
52
|
// 大转盘半径
|
18
|
|
- // const raduis = 150;
|
|
53
|
+ const raduis = desginRadius * scale;
|
|
54
|
+
|
|
55
|
+ // 轿厢宽度 = 大轮盘的周长 / 间隔份数
|
|
56
|
+ const cageRadius = Math.PI * 2 * raduis / (cageNum * 2)
|
19
|
57
|
|
20
|
58
|
// 转盘中心坐标
|
21
|
59
|
// const center;
|
|
@@ -27,7 +65,7 @@ export default function game({ el, center, raduis, onError, onSuccess, onBingo }
|
27
|
65
|
let isShooting = false
|
28
|
66
|
|
29
|
67
|
// 子弹飞行速度, 因为是向上飞, y 值是不端减小的过程
|
30
|
|
- const speed = -0.01;
|
|
68
|
+ const speed = -5;
|
31
|
69
|
|
32
|
70
|
// 目标轿厢列表, 该列表主要用来判断子弹是否击中目标
|
33
|
71
|
const cageList = [];
|
|
@@ -47,17 +85,30 @@ export default function game({ el, center, raduis, onError, onSuccess, onBingo }
|
47
|
85
|
autostart: true
|
48
|
86
|
}).appendTo(el);
|
49
|
87
|
|
|
88
|
+ // 绘制背景
|
|
89
|
+ makeBg({two, center});
|
|
90
|
+
|
50
|
91
|
// 绘制转盘
|
|
92
|
+ const wheelArc = two.makeCircle(center.x, center.y, 0);
|
51
|
93
|
const drawRoundAbout = () => {
|
52
|
94
|
const { x, y } = center
|
53
|
95
|
|
54
|
|
- // 绘制扇形区域
|
55
|
|
- // 该区域对游戏实际上没有用, 主要是梳理逻辑
|
56
|
|
- const arcList = Array(10).fill().map((_, inx) => {
|
57
|
|
- const startAngle = 2 * inx * perAngle
|
|
96
|
+ // 绘制图片转盘
|
|
97
|
+ const wheelTexture = two.makeTexture(wheelImage);
|
|
98
|
+ wheelArc.noStroke();
|
|
99
|
+ wheelArc.fill = wheelTexture;
|
|
100
|
+ wheelTexture.bind('load', () => {
|
|
101
|
+ wheelArc.radius = wheelTexture.image.naturalWidth / 2;
|
|
102
|
+ wheelArc.scale = scale;
|
|
103
|
+ })
|
|
104
|
+
|
|
105
|
+ // 绘制辅助扇形区域
|
|
106
|
+ const arcList = Array(cageNum).fill().map((_, inx) => {
|
|
107
|
+ const startAngle = 2 * inx * perAngle - offsetAngle; // 实际效果图是圆形有固定大小的, 不能直接从 0° 开始, 需要从 0 - offset 开始
|
58
|
108
|
const endAngle = startAngle + perAngle
|
59
|
109
|
const arc = two.makeArcSegment(x, y, 0, raduis, startAngle, endAngle)
|
60
|
|
- arc.fill = colorList[inx]
|
|
110
|
+ // arc.fill = colorList[inx] // 取消这个注释, 有助于梳理逻辑
|
|
111
|
+ arc.noFill()
|
61
|
112
|
arc.noStroke()
|
62
|
113
|
|
63
|
114
|
// 绘制目标轿厢
|
|
@@ -65,12 +116,14 @@ export default function game({ el, center, raduis, onError, onSuccess, onBingo }
|
65
|
116
|
const cx = x + Math.cos(startAngle + perAngle / 2) * raduis
|
66
|
117
|
const cy = y + Math.sin(startAngle + perAngle / 2) * raduis
|
67
|
118
|
|
68
|
|
- // 2、轿厢的宽度, 等于圆周长的 1/20
|
69
|
|
- const w = Math.PI * 2 * raduis / 20 // 周长 / 20
|
70
|
|
- const rect = two.makeRectangle(cx, cy, w, w);
|
|
119
|
+ // 2、轿厢
|
|
120
|
+ const rect = two.makeCircle(cx, cy, cageRadius / 2);
|
71
|
121
|
rect.id = `mnt-${inx}`
|
72
|
|
- rect.fill = colorList[inx]
|
73
|
|
- rect.noStroke()
|
|
122
|
+ // rect.fill = colorList[inx] // 取消这个注释, 有助于梳理逻辑
|
|
123
|
+ rect.noFill()
|
|
124
|
+ rect.stroke = primaryColor
|
|
125
|
+ rect.linewidth = 2
|
|
126
|
+ rect.dashes = [4]
|
74
|
127
|
rect.__$angle = startAngle // 轿厢的初始弧度
|
75
|
128
|
|
76
|
129
|
cageList.push(rect)
|
|
@@ -85,6 +138,7 @@ export default function game({ el, center, raduis, onError, onSuccess, onBingo }
|
85
|
138
|
// 旋转
|
86
|
139
|
const rotate = () => {
|
87
|
140
|
rotateAngle += rotateSpeed
|
|
141
|
+ wheelArc.rotation = rotateAngle
|
88
|
142
|
roundAbout.forEach((x, inx) => {
|
89
|
143
|
x.rotation = rotateAngle
|
90
|
144
|
|
|
@@ -102,25 +156,81 @@ export default function game({ el, center, raduis, onError, onSuccess, onBingo }
|
102
|
156
|
})
|
103
|
157
|
}
|
104
|
158
|
|
|
159
|
+ let ropeStart = null;
|
|
160
|
+ let ropeStop = null;
|
|
161
|
+
|
|
162
|
+ // 绘制底座
|
|
163
|
+ const drawPedestal = (x, y) => {
|
|
164
|
+ //
|
|
165
|
+ const pedestalTexture = two.makeTexture(pedestalImage);
|
|
166
|
+ const pedestalBox = two.makeRectangle(x, y, 0, 0);
|
|
167
|
+ pedestalBox.noStroke();
|
|
168
|
+ pedestalBox.fill = pedestalTexture;
|
|
169
|
+ pedestalTexture.bind('load', () => {
|
|
170
|
+ const { naturalWidth, naturalHeight } = pedestalTexture.image
|
|
171
|
+ pedestalBox.width = naturalWidth
|
|
172
|
+ pedestalBox.height = naturalHeight
|
|
173
|
+ pedestalBox.scale = scale
|
|
174
|
+ })
|
|
175
|
+
|
|
176
|
+ //
|
|
177
|
+ const ropeSequence = two.makeImageSequence(ropeImageList, x, y, 40); // 40 是不断调式出来的频率, 需要跟 speed 匹配
|
|
178
|
+ ropeSequence.scale = new Two.Vector(scale, scale);
|
|
179
|
+
|
|
180
|
+ ropeStart = () => ropeSequence.play();
|
|
181
|
+ ropeStop = () => ropeSequence.stop();
|
|
182
|
+
|
|
183
|
+ return pedestalBox;
|
|
184
|
+ }
|
|
185
|
+ const pedestalBox = drawPedestal(center.x, center.y + pedestalDistance * scale)
|
|
186
|
+
|
105
|
187
|
// 绘制子弹
|
106
|
188
|
const drawBullets = (x, y) => {
|
107
|
|
- const w = 20, h = 20;
|
|
189
|
+ const cageTexture = two.makeTexture(cageImage);
|
108
|
190
|
|
109
|
|
- const list = Array(10).fill().map((_, inx) => {
|
110
|
|
- const rect = two.makeRectangle(x, y, w, h)
|
111
|
|
- rect.id = `bullet-${inx}`
|
112
|
|
- rect.fill = 'black'
|
113
|
|
- rect.noStroke()
|
|
191
|
+ const list = Array(cageNum).fill().map((_, inx) => {
|
|
192
|
+ const arc = two.makeCircle(x, y, cageRadius / 2)
|
|
193
|
+ arc.id = `bullet-${inx}`
|
|
194
|
+ // arc.fill = 'black'
|
|
195
|
+ arc.fill = cageTexture
|
|
196
|
+ arc.noStroke()
|
|
197
|
+ arc.visible = inx === 0; // 第一个显示, 其余默认不显示
|
114
|
198
|
|
115
|
|
- return rect
|
|
199
|
+ return arc
|
|
200
|
+ })
|
|
201
|
+
|
|
202
|
+ cageTexture.bind('load', () => {
|
|
203
|
+ const r = cageTexture.image.naturalWidth / 2;
|
|
204
|
+ list.forEach(it => {
|
|
205
|
+ it.radius = r;
|
|
206
|
+ it.scale = scale;
|
|
207
|
+ });
|
116
|
208
|
})
|
117
|
209
|
|
118
|
210
|
bullets.push(...list)
|
119
|
211
|
}
|
120
|
|
- drawBullets(center.x, center.y + 400) // 400 是距离转盘中心的长度
|
121
|
|
-
|
|
212
|
+ drawBullets(center.x, center.y + pedestalDistance * scale)
|
122
|
213
|
// 待发射子弹
|
123
|
214
|
const clip = bullets.slice()
|
|
215
|
+
|
|
216
|
+ // 绘制计数器
|
|
217
|
+ const drawCounter = (x, y) => {
|
|
218
|
+ const counterTexture = two.makeTexture(countImage);
|
|
219
|
+ const counterBox = two.makeRectangle(x, y, 0, 0);
|
|
220
|
+ counterBox.noStroke();
|
|
221
|
+ counterBox.fill = counterTexture;
|
|
222
|
+ counterTexture.bind('load', () => {
|
|
223
|
+ const { naturalWidth, naturalHeight } = counterTexture.image
|
|
224
|
+ counterBox.width = naturalWidth
|
|
225
|
+ counterBox.height = naturalHeight
|
|
226
|
+ counterBox.scale = scale
|
|
227
|
+ })
|
|
228
|
+ // 初始值是弹夹中子弹数量
|
|
229
|
+ const counter = two.makeText(clip.length, x, y, { fill: primaryColor, weight: 700 })
|
|
230
|
+
|
|
231
|
+ return counter
|
|
232
|
+ }
|
|
233
|
+ const counter = drawCounter(pedestalBox.position.x + (224 * scale), pedestalBox.position.y); // 224 是设计稿上底座与计数器之间距离
|
124
|
234
|
|
125
|
235
|
// 挂载到轮盘
|
126
|
236
|
const mountToCage = (targ, cage) => {
|
|
@@ -129,17 +239,17 @@ export default function game({ el, center, raduis, onError, onSuccess, onBingo }
|
129
|
239
|
mntMap[cage.id] = targ
|
130
|
240
|
}
|
131
|
241
|
|
132
|
|
- // 当前第几个子弹
|
|
242
|
+ // 当前子弹
|
133
|
243
|
let currentBullet = null;
|
134
|
244
|
// 射击
|
135
|
245
|
const shooting = () => {
|
136
|
|
- isShooting = true
|
137
|
|
- const { top } = currentBullet.getBoundingClientRect()
|
138
|
|
- currentBullet.position = new Two.Vector(currentBullet.position.x, top + speed)
|
|
246
|
+ const { top, height } = currentBullet.getBoundingClientRect()
|
|
247
|
+ currentBullet.position = new Two.Vector(currentBullet.position.x, top + height / 2 + speed)
|
139
|
248
|
|
140
|
249
|
// 是否击中
|
141
|
250
|
const hitted = isHit(currentBullet)
|
142
|
251
|
if (hitted === 'error') {
|
|
252
|
+ ropeStop();
|
143
|
253
|
const t = setTimeout(() => {
|
144
|
254
|
isError = true
|
145
|
255
|
isFinished = true
|
|
@@ -152,12 +262,21 @@ export default function game({ el, center, raduis, onError, onSuccess, onBingo }
|
152
|
262
|
// 击中之后, 需要挂载轿厢到对应的位置
|
153
|
263
|
if (hitted) {
|
154
|
264
|
mountToCage(currentBullet, hitted)
|
155
|
|
- currentBullet = null;
|
|
265
|
+ // currentBullet = null;
|
|
266
|
+ currentBullet = clip.shift();
|
|
267
|
+ if (currentBullet) {
|
|
268
|
+ currentBullet.visible = true
|
|
269
|
+ counter.value = clip.length + 1
|
|
270
|
+ } else {
|
|
271
|
+ isFinished = true
|
|
272
|
+ counter.value = 0
|
|
273
|
+ }
|
|
274
|
+
|
156
|
275
|
isShooting = false;
|
|
276
|
+ ropeStop();
|
157
|
277
|
|
158
|
278
|
const t = setTimeout(() => {
|
159
|
|
- if (!clip.length) {
|
160
|
|
- isFinished = true
|
|
279
|
+ if (isFinished) {
|
161
|
280
|
onSuccess()
|
162
|
281
|
} else {
|
163
|
282
|
onBingo()
|
|
@@ -197,8 +316,8 @@ export default function game({ el, center, raduis, onError, onSuccess, onBingo }
|
197
|
316
|
two.bind('update', () => {
|
198
|
317
|
if (!isFinished) {
|
199
|
318
|
rotate()
|
200
|
|
-
|
201
|
|
- if (currentBullet) {
|
|
319
|
+
|
|
320
|
+ if (isShooting) {
|
202
|
321
|
shooting()
|
203
|
322
|
}
|
204
|
323
|
}
|
|
@@ -206,16 +325,43 @@ export default function game({ el, center, raduis, onError, onSuccess, onBingo }
|
206
|
325
|
|
207
|
326
|
// 绑定 dom click 事件 触发子弹发射
|
208
|
327
|
el.addEventListener('click', () => {
|
209
|
|
- if (!isFinished) {
|
210
|
|
- if (clip.length && !isShooting) {
|
211
|
|
- currentBullet = clip.pop()
|
|
328
|
+ if (!isFinished && !isShooting) {
|
|
329
|
+ isShooting = true
|
|
330
|
+ // 启动绳子效果
|
|
331
|
+ ropeStart()
|
|
332
|
+ if (!currentBullet) {
|
|
333
|
+ currentBullet = clip.shift()
|
|
334
|
+ currentBullet.visible = true
|
212
|
335
|
}
|
213
|
336
|
}
|
214
|
337
|
})
|
215
|
338
|
|
216
|
|
- return () => {
|
217
|
|
- two.unbind('update');
|
218
|
|
- two.pause();
|
219
|
|
- el.removeChild(two.renderer.domElement);
|
|
339
|
+ return {
|
|
340
|
+ start: () => {
|
|
341
|
+ isFinished = false;
|
|
342
|
+ },
|
|
343
|
+ stop: () => {
|
|
344
|
+ isFinished = true;
|
|
345
|
+ },
|
|
346
|
+ destroy: () => {
|
|
347
|
+
|
|
348
|
+ two.unbind('update');
|
|
349
|
+ two.pause();
|
|
350
|
+ el.removeChild(two.renderer.domElement);
|
|
351
|
+ }
|
220
|
352
|
}
|
221
|
353
|
}
|
|
354
|
+
|
|
355
|
+function makeBg({two, center}) {
|
|
356
|
+ const bgTexture = two.makeTexture(backImage);
|
|
357
|
+
|
|
358
|
+ const bgBox = two.makeRectangle(center.x, center.y, 0, 0);
|
|
359
|
+ bgBox.noStroke();
|
|
360
|
+ bgBox.fill = bgTexture;
|
|
361
|
+
|
|
362
|
+ bgTexture.bind('load', () => {
|
|
363
|
+ bgBox.width = bgTexture.image.naturalWidth;
|
|
364
|
+ bgBox.height = bgTexture.image.naturalHeight;
|
|
365
|
+ bgBox.scale = scale;
|
|
366
|
+ })
|
|
367
|
+}
|