|
@@ -0,0 +1,202 @@
|
|
1
|
+
|
|
2
|
+import Two from "two.js";
|
|
3
|
+
|
|
4
|
+const colorList = ['#f5222d', '#d4380d', '#d46b08', '#d48806', '#d4b106', '#7cb305', '#389e0d', '#08979c', '#096dd9', '#531dab']
|
|
5
|
+
|
|
6
|
+export default function game({ el, onError, onSuccess }) {
|
|
7
|
+
|
|
8
|
+ // 是否游戏结束
|
|
9
|
+ let isFinished = false;
|
|
10
|
+
|
|
11
|
+ // 是否失败
|
|
12
|
+ // eslint-disable-next-line no-unused-vars
|
|
13
|
+ let isError = false;
|
|
14
|
+
|
|
15
|
+ // 每个区域的旋转角度, 10 个轿厢, 分 20 等份
|
|
16
|
+ const perAngle = 2 * Math.PI / 20 // 分20等份
|
|
17
|
+
|
|
18
|
+ // 大转盘半径
|
|
19
|
+ const raduis = 150;
|
|
20
|
+
|
|
21
|
+ // 初始化
|
|
22
|
+ const two = new Two({
|
|
23
|
+ fullscreen: true,
|
|
24
|
+ autostart: true
|
|
25
|
+ }).appendTo(el);
|
|
26
|
+
|
|
27
|
+ // 转盘中心坐标
|
|
28
|
+ const center = { x: two.width / 2, y: two.height / 2 }
|
|
29
|
+
|
|
30
|
+ // 轿厢 - 子弹
|
|
31
|
+ const bullets = []
|
|
32
|
+
|
|
33
|
+ // 子弹是否在射击状态, 如果是, 不能进行其他操作
|
|
34
|
+ let isShooting = false
|
|
35
|
+
|
|
36
|
+ // 子弹飞行速度, 因为是向上飞, y 值是不端减小的过程
|
|
37
|
+ const speed = -0.01;
|
|
38
|
+
|
|
39
|
+ // 目标轿厢列表, 该列表主要用来判断子弹是否击中目标
|
|
40
|
+ const cageList = [];
|
|
41
|
+
|
|
42
|
+ // 轿厢的初始旋转弧度
|
|
43
|
+ let rotateAngle = 0
|
|
44
|
+
|
|
45
|
+ // 轿厢的旋转速度 - 单位弧度
|
|
46
|
+ const rotateSpeed = 0.01
|
|
47
|
+
|
|
48
|
+ // 目标轿厢与子弹轿厢的映射字典
|
|
49
|
+ const mntMap = {}
|
|
50
|
+
|
|
51
|
+ // 绘制转盘
|
|
52
|
+ const drawRoundAbout = () => {
|
|
53
|
+ const { x, y } = center
|
|
54
|
+
|
|
55
|
+ // 绘制扇形区域
|
|
56
|
+ // 该区域对游戏实际上没有用, 主要是梳理逻辑
|
|
57
|
+ const arcList = Array(10).fill().map((_, inx) => {
|
|
58
|
+ const startAngle = 2 * inx * perAngle
|
|
59
|
+ const endAngle = startAngle + perAngle
|
|
60
|
+ const arc = two.makeArcSegment(x, y, 0, raduis, startAngle, endAngle)
|
|
61
|
+ arc.fill = colorList[inx]
|
|
62
|
+ arc.noStroke()
|
|
63
|
+
|
|
64
|
+ // 绘制目标轿厢
|
|
65
|
+ // 1、计算扇形边上的中心坐标
|
|
66
|
+ const cx = x + Math.cos(startAngle + perAngle / 2) * raduis
|
|
67
|
+ const cy = y + Math.sin(startAngle + perAngle / 2) * raduis
|
|
68
|
+
|
|
69
|
+ // 2、轿厢的宽度, 等于圆周长的 1/20
|
|
70
|
+ const w = Math.PI * 2 * raduis / 20 // 周长 / 20
|
|
71
|
+ const rect = two.makeRectangle(cx, cy, w, w);
|
|
72
|
+ rect.id = `mnt-${inx}`
|
|
73
|
+ rect.fill = colorList[inx]
|
|
74
|
+ rect.noStroke()
|
|
75
|
+ rect.__$angle = startAngle // 轿厢的初始弧度
|
|
76
|
+
|
|
77
|
+ cageList.push(rect)
|
|
78
|
+
|
|
79
|
+ return arc
|
|
80
|
+ })
|
|
81
|
+
|
|
82
|
+ return arcList
|
|
83
|
+ }
|
|
84
|
+ const roundAbout = drawRoundAbout()
|
|
85
|
+
|
|
86
|
+ // 旋转
|
|
87
|
+ const rotate = () => {
|
|
88
|
+ rotateAngle += rotateSpeed
|
|
89
|
+ roundAbout.forEach((x, inx) => {
|
|
90
|
+ x.rotation = rotateAngle
|
|
91
|
+
|
|
92
|
+ // 计算旋转后的轿厢坐标
|
|
93
|
+ const cage = cageList[inx]
|
|
94
|
+ const angle = cage.__$angle + rotateAngle + perAngle / 2
|
|
95
|
+ const cx = center.x + Math.cos(angle) * raduis
|
|
96
|
+ const cy = center.y + Math.sin(angle) * raduis
|
|
97
|
+ cage.position = new Two.Vector(cx, cy)
|
|
98
|
+
|
|
99
|
+ // 如果有对应的挂载物
|
|
100
|
+ if (mntMap[cage.id]) {
|
|
101
|
+ mntMap[cage.id].position = cage.position
|
|
102
|
+ }
|
|
103
|
+ })
|
|
104
|
+ }
|
|
105
|
+
|
|
106
|
+ // 绘制子弹
|
|
107
|
+ const drawBullets = (x, y) => {
|
|
108
|
+ const w = 20, h = 20;
|
|
109
|
+
|
|
110
|
+ const list = Array(10).fill().map((_, inx) => {
|
|
111
|
+ const rect = two.makeRectangle(x, y, w, h)
|
|
112
|
+ rect.id = `bullet-${inx}`
|
|
113
|
+ rect.fill = 'black'
|
|
114
|
+ rect.noStroke()
|
|
115
|
+
|
|
116
|
+ return rect
|
|
117
|
+ })
|
|
118
|
+
|
|
119
|
+ bullets.push(...list)
|
|
120
|
+ }
|
|
121
|
+ drawBullets(center.x, center.y + 400) // 400 是距离转盘中心的长度
|
|
122
|
+
|
|
123
|
+ // 待发射子弹
|
|
124
|
+ const clip = bullets.slice()
|
|
125
|
+
|
|
126
|
+ // 挂载到轮盘
|
|
127
|
+ const mountToCage = (targ, cage) => {
|
|
128
|
+ targ.position = cage.position
|
|
129
|
+ // 写入映射表
|
|
130
|
+ mntMap[cage.id] = targ
|
|
131
|
+ }
|
|
132
|
+
|
|
133
|
+ // 当前第几个子弹
|
|
134
|
+ let currentBullet = null;
|
|
135
|
+ // 射击
|
|
136
|
+ const shooting = () => {
|
|
137
|
+ isShooting = true
|
|
138
|
+ const { left, width, top } = currentBullet.getBoundingClientRect()
|
|
139
|
+ currentBullet.position = new Two.Vector(left + width / 2, top + speed)
|
|
140
|
+
|
|
141
|
+ const hitted = isHit(currentBullet)
|
|
142
|
+ if (hitted === 'error') {
|
|
143
|
+ isError = true
|
|
144
|
+ isFinished = true
|
|
145
|
+ onError()
|
|
146
|
+ return
|
|
147
|
+ }
|
|
148
|
+
|
|
149
|
+ if (hitted) {
|
|
150
|
+ mountToCage(currentBullet, hitted)
|
|
151
|
+ currentBullet = null;
|
|
152
|
+ isShooting = false;
|
|
153
|
+
|
|
154
|
+ if (!clip.length) {
|
|
155
|
+ onSuccess()
|
|
156
|
+ isFinished = true
|
|
157
|
+ }
|
|
158
|
+ }
|
|
159
|
+ }
|
|
160
|
+
|
|
161
|
+ // 是否击中
|
|
162
|
+ // 如果击中, 则返回目标轿厢
|
|
163
|
+ const isHit = (bullet) => {
|
|
164
|
+ const rect2 = bullet.getBoundingClientRect();
|
|
165
|
+ const x = rect2.left + rect2.width / 2
|
|
166
|
+ const y = rect2.top + rect2.height / 2
|
|
167
|
+
|
|
168
|
+ if (y < (center.y + raduis)) {
|
|
169
|
+ return 'error';
|
|
170
|
+ }
|
|
171
|
+
|
|
172
|
+ const cage = cageList.filter((it) => {
|
|
173
|
+ const rect1 = it.getBoundingClientRect();
|
|
174
|
+ return x >= rect1.left && x <= rect1.right &&
|
|
175
|
+ y >= rect1.top && y <= rect1.bottom;
|
|
176
|
+ })[0]
|
|
177
|
+
|
|
178
|
+ if (!cage) return false;
|
|
179
|
+
|
|
180
|
+ return !cage ? false : cage
|
|
181
|
+ }
|
|
182
|
+
|
|
183
|
+ // 重复绘制内容
|
|
184
|
+ two.bind('update', () => {
|
|
185
|
+ if (!isFinished) {
|
|
186
|
+ rotate()
|
|
187
|
+
|
|
188
|
+ if (currentBullet) {
|
|
189
|
+ shooting()
|
|
190
|
+ }
|
|
191
|
+ }
|
|
192
|
+ })
|
|
193
|
+
|
|
194
|
+ // 绑定 dom click 事件 触发子弹发射
|
|
195
|
+ el.addEventListener('click', () => {
|
|
196
|
+ if (!isFinished) {
|
|
197
|
+ if (clip.length && !isShooting) {
|
|
198
|
+ currentBullet = clip.pop()
|
|
199
|
+ }
|
|
200
|
+ }
|
|
201
|
+ })
|
|
202
|
+}
|