add tower builing game
|
After Width: | Height: | Size: 249 KiB |
@@ -12,7 +12,12 @@
|
||||
content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0, maximum-scale=1, user-scalable=no, minimal-ui">
|
||||
<style>
|
||||
body {
|
||||
margin: 10px 0 20px 0 !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 8px !important;
|
||||
}
|
||||
|
||||
.heading {
|
||||
|
||||
@@ -12,6 +12,10 @@ body {
|
||||
|
||||
body {
|
||||
/* margin: 80px 0; */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.heading:after {
|
||||
@@ -144,8 +148,10 @@ hr {
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 500px;
|
||||
width: calc(100% - 16px);
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
@-webkit-keyframes fade-in {
|
||||
@@ -683,8 +689,10 @@ hr {
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 280px;
|
||||
width: calc(100% - 16px);
|
||||
max-width: 280px;
|
||||
margin: 0 auto;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.score-container,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"browsers": [
|
||||
"ios >= 9",
|
||||
"android >= 4"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
.idea/
|
||||
npm-debug.log
|
||||
yarn.lock
|
||||
.DS_Store
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 BMQB, Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,126 @@
|
||||
[](LICENSE)
|
||||
|
||||
English | [简体中文](./README.zh-CN.md)
|
||||
|
||||
<h1 align="center">Tower Building Game</h1>
|
||||
<p align="center"><img src="https://o2qq673j2.qnssl.com/tower-loading.gif"/></p>
|
||||
|
||||
> a tower building game based on ES6 and Canvas (Tower Bloxx Deluxe Skyscraper)
|
||||
|
||||
## Demo
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/17680888/47480922-93a20c00-d864-11e8-8f7c-6d1d60184730.gif"/></p>
|
||||
<h2 align="center"><a href="https://iamkun.github.io/tower_game">Link to online Demo (Demo Link)</a></h2>
|
||||
<h4 align="center">Mobile Devices can scan following QR code:</h4>
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/17680888/47480646-abc55b80-d863-11e8-9337-4ea768ebe55d.png" />
|
||||
</p>
|
||||
|
||||
## Game Rule
|
||||
|
||||
The following are the default game rules:
|
||||
|
||||
- The player starts with 3 hp, indicated by the hearts in the top right corner of the screen. Every time a Tower block falls the player loses 1 hp, and the game ends when hp is depleted.
|
||||
|
||||
- The player earns 25 points for every succesful stacked block (Success). If a block is stacked pefectly (Perfect) on top of the previous one, then the player is
|
||||
rewarded with 50 points instead. Consecutive Perfects awards additional 25 points.
|
||||
|
||||
**Note: Each Success or Perfect constitutes a floor**
|
||||
|
||||
For example, the first Perfect awards 50 points. The second consecutive Perfect awards 75 points.
|
||||
The third consecutive Perfect awards 100 points. etc.
|
||||
|
||||
<p align="center">
|
||||
<img width="550" src="https://user-images.githubusercontent.com/17680888/47473105-d9021180-d843-11e8-8c19-b6b78d86cbdf.png" />
|
||||
</p>
|
||||
|
||||
## Customizing the game rules
|
||||
|
||||
```
|
||||
git clone https://github.com/iamkun/tower_game.git
|
||||
cd tower_game
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
Open `http://localhost:8082` in a web browser.
|
||||
|
||||
- To customize image and sound resource files directly replace the corresponding file under the `assets` directory.
|
||||
- To customize game rules modify the `option` object in `index.html`.
|
||||
|
||||
## Option
|
||||
|
||||
Use the following table of `option` constants to complete customization of game rules.
|
||||
|
||||
**Note: all constants are optionally included**
|
||||
|
||||
| Option | Type | Description |
|
||||
|---------|--------|-------------|
|
||||
| width | number | Width of game interface |
|
||||
| height | number | Height of game interface |
|
||||
| canvasId | string | DOM ID in Canvas |
|
||||
| soundOn | boolean | If sound is on |
|
||||
| successScore | number | Points awarded for success |
|
||||
| perfectScore | number | Additional points awarded for perfect |
|
||||
| <a href="#hookspeed">hookSpeed</a> | function | Speed of hook's movement |
|
||||
| <a href="#hookangle">hookAngle</a> | function | Angle of hook |
|
||||
| <a href="#landblockspeed">landBlockSpeed</a> | function | Speed of block sway |
|
||||
| <a href="#setgamescore">setGameScore</a> | function | hook for current score |
|
||||
| <a href="#setgamesuccess">setGameSuccess</a> | function | hook for number of current succesful game |
|
||||
| <a href="#setgamefailed">setGameFailed</a> | function | hook for number of current failed game |
|
||||
|
||||
#### hookSpeed
|
||||
|
||||
This function takes in two parameters, currentFloor and currentScore, and returns a speed value.
|
||||
```
|
||||
function(currentFloor, currentScore) {
|
||||
return number
|
||||
}
|
||||
```
|
||||
|
||||
#### hookAngle
|
||||
|
||||
This function takes in two parameters, currentFloor and currentScore, and returns a angle value.
|
||||
```
|
||||
function(currentFloor, currentScore) {
|
||||
return number
|
||||
}
|
||||
```
|
||||
|
||||
#### landBlockSpeed
|
||||
|
||||
This function takes in two parameters, currentFloor and currentScore, and returns a speed value.
|
||||
```
|
||||
function(currentFloor, currentScore) {
|
||||
return number
|
||||
}
|
||||
```
|
||||
|
||||
#### setGameScore
|
||||
|
||||
This function takes in one parameter, score, and sets currentScore to score.
|
||||
```
|
||||
function(score) {
|
||||
// your logic
|
||||
}
|
||||
```
|
||||
|
||||
#### setGameSuccess
|
||||
|
||||
This function takes in one parameter, score, and sets GameSuccess to successCount.
|
||||
```
|
||||
function(successCount) {
|
||||
// your logic
|
||||
}
|
||||
```
|
||||
|
||||
#### setGameFailed
|
||||
|
||||
This function takes in one parameter, score, and sets GameFailed to failedCount.
|
||||
```
|
||||
function(failedCount) {
|
||||
// your logic
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT license.
|
||||
@@ -0,0 +1,122 @@
|
||||
[](LICENSE)
|
||||
|
||||
[English](./README.md) | 简体中文
|
||||
|
||||
<h1 align="center">盖楼游戏</h1>
|
||||
<p align="center"><img src="https://o2qq673j2.qnssl.com/tower-loading.gif"/></p>
|
||||
|
||||
> 一个基于 Canvas 的盖楼游戏
|
||||
|
||||
> Tower Building Game (Tower Bloxx Deluxe Skyscraper)
|
||||
|
||||
## Demo 预览
|
||||
<p align="center"><img src="https://user-images.githubusercontent.com/17680888/47480922-93a20c00-d864-11e8-8f7c-6d1d60184730.gif"/></p>
|
||||
<h2 align="center"><a href="https://iamkun.github.io/tower_game">在线预览地址 (Demo Link)</a></h2>
|
||||
<h4 align="center">手机设备可以扫描下方二维码</h4>
|
||||
<p align="center">
|
||||
<img src="https://user-images.githubusercontent.com/17680888/47480646-abc55b80-d863-11e8-9337-4ea768ebe55d.png" />
|
||||
</p>
|
||||
|
||||
## Game Rule 游戏规则
|
||||
|
||||
以下为默认游戏规则,也可参照下节自定义游戏参数
|
||||
|
||||
- 每局游戏生命值为3,掉落一块楼层生命值减1,掉落3块后游戏结束,单局游戏无时间限制
|
||||
|
||||
- 成功盖楼加25分,完美盖楼加50分,连续完美盖楼额外加25分,楼层掉落扣除生命值1,单局游戏共有3次掉落机会
|
||||
|
||||
栗子:第一块完美盖楼加50分,第二块连续完美盖楼加75分,第三块连续完美盖楼加100分,依此类推……
|
||||
|
||||
<p align="center">
|
||||
<img src="https://o2qq673j2.qnssl.com/Fv7ewqHHXeAnUAlF7AI9ndQulEOC" />
|
||||
</p>
|
||||
|
||||
## Customise 自定义
|
||||
|
||||
```
|
||||
git clone https://github.com/iamkun/tower_game.git
|
||||
cd tower_game
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
打开 `http://localhost:8082`
|
||||
|
||||
- 图片、音频资源可以直接替换 `assets` 目录下对应的资源文件
|
||||
- 游戏规则可以修改 `index.html` 文件 `L480` 的 `option` 对象
|
||||
|
||||
## Option 自定义选项
|
||||
|
||||
可以使用以下 `option` 表格里的参数,完成游戏自定义,**所有参数都是非必填项**
|
||||
|
||||
| Option | Type | Description |
|
||||
|---------|--------|-------------|
|
||||
| width | number | 游戏主画面宽度 |
|
||||
| height | number | 游戏主画面高度 |
|
||||
| canvasId | string | Canvas 的 DOM ID |
|
||||
| soundOn | boolean | 是否开启声音 |
|
||||
| successScore | number | 成功盖楼分数 |
|
||||
| perfectScore | number | 完美盖楼额外奖励分数 |
|
||||
| <a href="#hookspeed">hookSpeed</a> | function | 钩子平移速度 |
|
||||
| <a href="#hookangle">hookAngle</a> | function | 钩子摆动角度 |
|
||||
| <a href="#landblockspeed">landBlockSpeed</a> | function | 下方楼房横向速度 |
|
||||
| <a href="#setgamescore">setGameScore</a> | function | 当前游戏分数hook |
|
||||
| <a href="#setgamesuccess">setGameSuccess</a> | function | 当前游戏成功次数hook |
|
||||
| <a href="#setgamefailed">setGameFailed</a> | function | 当前游戏失败次数hook |
|
||||
|
||||
#### hookSpeed
|
||||
钩子平移速度
|
||||
函数接收两个参数,当前成功楼层和当前分数,返回速度数值
|
||||
```
|
||||
function(currentFloor, currentScore) {
|
||||
return number
|
||||
}
|
||||
```
|
||||
|
||||
#### hookAngle
|
||||
钩子摆动角度
|
||||
函数接收两个参数,当前成功楼层和当前分数,返回角度数值
|
||||
```
|
||||
function(currentFloor, currentScore) {
|
||||
return number
|
||||
}
|
||||
```
|
||||
|
||||
#### landBlockSpeed
|
||||
下方楼房平移速度
|
||||
函数接收两个参数,当前成功楼层和当前分数,返回速度数值
|
||||
```
|
||||
function(currentFloor, currentScore) {
|
||||
return number
|
||||
}
|
||||
```
|
||||
|
||||
#### setGameScore
|
||||
当前游戏分数hook
|
||||
函数接收一个参数,当前游戏分数
|
||||
```
|
||||
function(score) {
|
||||
// your logic
|
||||
}
|
||||
```
|
||||
|
||||
#### setGameSuccess
|
||||
当前游戏成功次数hook
|
||||
函数接收一个参数,当前游戏成功次数
|
||||
```
|
||||
function(successCount) {
|
||||
// your logic
|
||||
}
|
||||
```
|
||||
|
||||
#### setGameFailed
|
||||
当前游戏失败次数hook
|
||||
函数接收一个参数,当前游戏失败次数
|
||||
```
|
||||
function(failedCount) {
|
||||
// your logic
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT license.
|
||||
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 273 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 928 B |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
@@ -0,0 +1,17 @@
|
||||
const express = require('express')
|
||||
const path = require('path')
|
||||
const opn = require('opn')
|
||||
|
||||
const server = express()
|
||||
const host = 'http://localhost:8082'
|
||||
server.use('/assets', express.static(path.resolve(__dirname, './assets')))
|
||||
server.use('/dist', express.static(path.resolve(__dirname, './dist')))
|
||||
|
||||
server.get('*', (req, res) => {
|
||||
res.sendFile(path.resolve(__dirname, './index.html'));
|
||||
})
|
||||
|
||||
server.listen(8082, () => {
|
||||
console.log(`server started at ${host}`)
|
||||
opn(host)
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "tower_game",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "npm run build && node index.js",
|
||||
"build": "webpack --mode production --module-bind js=babel-loader"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/bmqb/tower_game.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/bmqb/tower_game/issues"
|
||||
},
|
||||
"homepage": "https://github.com/bmqb/tower_game#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0-beta.42",
|
||||
"@babel/preset-env": "^7.0.0-beta.42",
|
||||
"babel-loader": "^8.0.0-beta",
|
||||
"express": "^4.16.3",
|
||||
"opn": "^5.3.0",
|
||||
"webpack": "^4.0.1",
|
||||
"webpack-cli": "^3.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"cooljs": "^1.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Instance } from 'cooljs'
|
||||
import { blockAction, blockPainter } from './block'
|
||||
import {
|
||||
checkMoveDown,
|
||||
getMoveDownValue,
|
||||
drawYellowString,
|
||||
getAngleBase
|
||||
} from './utils'
|
||||
import { addFlight } from './flight'
|
||||
import * as constant from './constant'
|
||||
|
||||
export const endAnimate = (engine) => {
|
||||
const gameStartNow = engine.getVariable(constant.gameStartNow)
|
||||
if (!gameStartNow) return
|
||||
const successCount = engine.getVariable(constant.successCount, 0)
|
||||
const failedCount = engine.getVariable(constant.failedCount)
|
||||
const gameScore = engine.getVariable(constant.gameScore, 0)
|
||||
const threeFiguresOffset = Number(successCount) > 99 ? engine.width * 0.1 : 0
|
||||
|
||||
drawYellowString(engine, {
|
||||
string: 'floor',
|
||||
size: engine.width * 0.06,
|
||||
x: (engine.width * 0.24) + threeFiguresOffset,
|
||||
y: engine.width * 0.12,
|
||||
textAlign: 'left',
|
||||
fontName: 'Arial',
|
||||
fontWeight: 'bold'
|
||||
})
|
||||
drawYellowString(engine, {
|
||||
string: successCount,
|
||||
size: engine.width * 0.17,
|
||||
x: (engine.width * 0.22) + threeFiguresOffset,
|
||||
y: engine.width * 0.2,
|
||||
textAlign: 'right'
|
||||
})
|
||||
const score = engine.getImg('score')
|
||||
const scoreWidth = score.width
|
||||
const scoreHeight = score.height
|
||||
const zoomedWidth = engine.width * 0.35
|
||||
const zoomedHeight = (scoreHeight * zoomedWidth) / scoreWidth
|
||||
engine.ctx.drawImage(
|
||||
score,
|
||||
engine.width * 0.61,
|
||||
engine.width * 0.038,
|
||||
zoomedWidth,
|
||||
zoomedHeight
|
||||
)
|
||||
drawYellowString(engine, {
|
||||
string: gameScore,
|
||||
size: engine.width * 0.06,
|
||||
x: engine.width * 0.9,
|
||||
y: engine.width * 0.11,
|
||||
textAlign: 'right'
|
||||
})
|
||||
const { ctx } = engine
|
||||
const heart = engine.getImg('heart')
|
||||
const heartWidth = heart.width
|
||||
const heartHeight = heart.height
|
||||
const zoomedHeartWidth = engine.width * 0.08
|
||||
const zoomedHeartHeight = (heartHeight * zoomedHeartWidth) / heartWidth
|
||||
for (let i = 1; i <= 3; i += 1) {
|
||||
ctx.save()
|
||||
if (i <= failedCount) {
|
||||
ctx.globalAlpha = 0.2
|
||||
}
|
||||
ctx.drawImage(
|
||||
heart,
|
||||
(engine.width * 0.66) + ((i - 1) * zoomedHeartWidth),
|
||||
engine.width * 0.16,
|
||||
zoomedHeartWidth,
|
||||
zoomedHeartHeight
|
||||
)
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
|
||||
export const startAnimate = (engine) => {
|
||||
const gameStartNow = engine.getVariable(constant.gameStartNow)
|
||||
if (!gameStartNow) return
|
||||
const lastBlock = engine.getInstance(`block_${engine.getVariable(constant.blockCount)}`)
|
||||
if (!lastBlock || [constant.land, constant.out].indexOf(lastBlock.status) > -1) {
|
||||
if (checkMoveDown(engine) && getMoveDownValue(engine)) return
|
||||
if (engine.checkTimeMovement(constant.hookUpMovement)) return
|
||||
const angleBase = getAngleBase(engine)
|
||||
const initialAngle = (Math.PI
|
||||
* engine.utils.random(angleBase, angleBase + 5)
|
||||
* engine.utils.randomPositiveNegative()
|
||||
) / 180
|
||||
engine.setVariable(constant.blockCount, engine.getVariable(constant.blockCount) + 1)
|
||||
engine.setVariable(constant.initialAngle, initialAngle)
|
||||
engine.setTimeMovement(constant.hookDownMovement, 500)
|
||||
const block = new Instance({
|
||||
name: `block_${engine.getVariable(constant.blockCount)}`,
|
||||
action: blockAction,
|
||||
painter: blockPainter
|
||||
})
|
||||
engine.addInstance(block)
|
||||
}
|
||||
const successCount = Number(engine.getVariable(constant.successCount, 0))
|
||||
switch (successCount) {
|
||||
case 2:
|
||||
addFlight(engine, 1, 'leftToRight')
|
||||
break
|
||||
case 6:
|
||||
addFlight(engine, 2, 'rightToLeft')
|
||||
break
|
||||
case 8:
|
||||
addFlight(engine, 3, 'leftToRight')
|
||||
break
|
||||
case 14:
|
||||
addFlight(engine, 4, 'bottomToTop')
|
||||
break
|
||||
case 18:
|
||||
addFlight(engine, 5, 'bottomToTop')
|
||||
break
|
||||
case 22:
|
||||
addFlight(engine, 6, 'bottomToTop')
|
||||
break
|
||||
case 25:
|
||||
addFlight(engine, 7, 'rightTopToLeft')
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { checkMoveDown, getMoveDownValue } from './utils'
|
||||
import * as constant from './constant'
|
||||
|
||||
export const backgroundImg = (engine) => {
|
||||
const bg = engine.getImg('background')
|
||||
const bgWidth = bg.width
|
||||
const bgHeight = bg.height
|
||||
const zoomedHeight = (bgHeight * engine.width) / bgWidth
|
||||
let offsetHeight = engine.getVariable(constant.bgImgOffset, engine.height - zoomedHeight)
|
||||
if (offsetHeight > engine.height) {
|
||||
return
|
||||
}
|
||||
engine.getTimeMovement(
|
||||
constant.moveDownMovement,
|
||||
[[offsetHeight, offsetHeight + (getMoveDownValue(engine, { pixelsPerFrame: s => s / 2 }))]],
|
||||
(value) => {
|
||||
offsetHeight = value
|
||||
},
|
||||
{
|
||||
name: 'background'
|
||||
}
|
||||
)
|
||||
engine.getTimeMovement(
|
||||
constant.bgInitMovement,
|
||||
[[offsetHeight, offsetHeight + (zoomedHeight / 4)]],
|
||||
(value) => {
|
||||
offsetHeight = value
|
||||
}
|
||||
)
|
||||
engine.setVariable(constant.bgImgOffset, offsetHeight)
|
||||
engine.setVariable(constant.lineInitialOffset, engine.height - (zoomedHeight * 0.394))
|
||||
engine.ctx.drawImage(
|
||||
bg,
|
||||
0, offsetHeight,
|
||||
engine.width, zoomedHeight
|
||||
)
|
||||
}
|
||||
|
||||
const getLinearGradientColorRgb = (colorArr, colorIndex, proportion) => {
|
||||
const currentIndex = colorIndex + 1 >= colorArr.length ? colorArr.length - 1 : colorIndex
|
||||
const colorCurrent = colorArr[currentIndex]
|
||||
const nextIndex = currentIndex + 1 >= colorArr.length - 1 ? currentIndex : currentIndex + 1
|
||||
const colorNext = colorArr[nextIndex]
|
||||
const calRgbValue = (index) => {
|
||||
const current = colorCurrent[index]
|
||||
const next = colorNext[index]
|
||||
return Math.round(current + ((next - current) * proportion))
|
||||
}
|
||||
return `rgb(${calRgbValue(0)}, ${calRgbValue(1)}, ${calRgbValue(2)})`
|
||||
}
|
||||
|
||||
export const backgroundLinearGradient = (engine) => {
|
||||
const grad = engine.ctx.createLinearGradient(0, 0, 0, engine.height)
|
||||
const colorArr = [
|
||||
[200, 255, 150],
|
||||
[105, 230, 240],
|
||||
[90, 190, 240],
|
||||
[85, 100, 190],
|
||||
[55, 20, 35],
|
||||
[75, 25, 35],
|
||||
[25, 0, 10]
|
||||
]
|
||||
const offsetHeight = engine.getVariable(constant.bgLinearGradientOffset, 0)
|
||||
if (checkMoveDown(engine)) {
|
||||
engine.setVariable(
|
||||
constant.bgLinearGradientOffset
|
||||
, offsetHeight + (getMoveDownValue(engine) * 1.5)
|
||||
)
|
||||
}
|
||||
const colorIndex = parseInt(offsetHeight / engine.height, 10)
|
||||
const calOffsetHeight = offsetHeight % engine.height
|
||||
const proportion = calOffsetHeight / engine.height
|
||||
const colorBase = getLinearGradientColorRgb(colorArr, colorIndex, proportion)
|
||||
const colorTop = getLinearGradientColorRgb(colorArr, colorIndex + 1, proportion)
|
||||
grad.addColorStop(0, colorTop)
|
||||
grad.addColorStop(1, colorBase)
|
||||
engine.ctx.fillStyle = grad
|
||||
engine.ctx.beginPath()
|
||||
engine.ctx.rect(0, 0, engine.width, engine.height)
|
||||
engine.ctx.fill()
|
||||
|
||||
// lightning
|
||||
const lightning = () => {
|
||||
engine.ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
|
||||
engine.ctx.fillRect(0, 0, engine.width, engine.height)
|
||||
}
|
||||
engine.getTimeMovement(
|
||||
constant.lightningMovement, [], () => {},
|
||||
{
|
||||
before: lightning,
|
||||
after: lightning
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const background = (engine) => {
|
||||
backgroundLinearGradient(engine)
|
||||
backgroundImg(engine)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import {
|
||||
getMoveDownValue,
|
||||
getLandBlockVelocity,
|
||||
getSwingBlockVelocity,
|
||||
touchEventHandler,
|
||||
addSuccessCount,
|
||||
addFailedCount,
|
||||
addScore
|
||||
} from './utils'
|
||||
import * as constant from './constant'
|
||||
|
||||
const checkCollision = (block, line) => {
|
||||
// 0 goon 1 drop 2 rotate left 3 rotate right 4 ok 5 perfect
|
||||
if (block.y + block.height >= line.y) {
|
||||
if (block.x < line.x - block.calWidth || block.x > line.collisionX + block.calWidth) {
|
||||
return 1
|
||||
}
|
||||
if (block.x < line.x) {
|
||||
return 2
|
||||
}
|
||||
if (block.x > line.collisionX) {
|
||||
return 3
|
||||
}
|
||||
if (block.x > line.x + (block.calWidth * 0.8) && block.x < line.x + (block.calWidth * 1.2)) {
|
||||
// -10% +10%
|
||||
return 5
|
||||
}
|
||||
return 4
|
||||
}
|
||||
return 0
|
||||
}
|
||||
const swing = (instance, engine, time) => {
|
||||
const ropeHeight = engine.getVariable(constant.ropeHeight)
|
||||
if (instance.status !== constant.swing) return
|
||||
const i = instance
|
||||
const initialAngle = engine.getVariable(constant.initialAngle)
|
||||
i.angle = initialAngle *
|
||||
getSwingBlockVelocity(engine, time)
|
||||
i.weightX = i.x +
|
||||
(Math.sin(i.angle) * ropeHeight)
|
||||
i.weightY = i.y +
|
||||
(Math.cos(i.angle) * ropeHeight)
|
||||
}
|
||||
|
||||
const checkBlockOut = (instance, engine) => {
|
||||
if (instance.status === constant.rotateLeft) {
|
||||
// 左转 要等右上角消失才算消失
|
||||
if (instance.y - instance.width >= engine.height) {
|
||||
instance.visible = false
|
||||
instance.status = constant.out
|
||||
addFailedCount(engine)
|
||||
}
|
||||
} else if (instance.y >= engine.height) {
|
||||
instance.visible = false
|
||||
instance.status = constant.out
|
||||
addFailedCount(engine)
|
||||
}
|
||||
}
|
||||
|
||||
export const blockAction = (instance, engine, time) => {
|
||||
const i = instance
|
||||
const ropeHeight = engine.getVariable(constant.ropeHeight)
|
||||
if (!i.visible) {
|
||||
return
|
||||
}
|
||||
if (!i.ready) {
|
||||
i.ready = true
|
||||
i.status = constant.swing
|
||||
instance.updateWidth(engine.getVariable(constant.blockWidth))
|
||||
instance.updateHeight(engine.getVariable(constant.blockHeight))
|
||||
instance.x = engine.width / 2
|
||||
instance.y = ropeHeight * -1.5
|
||||
}
|
||||
const line = engine.getInstance('line')
|
||||
switch (i.status) {
|
||||
case constant.swing:
|
||||
engine.getTimeMovement(
|
||||
constant.hookDownMovement,
|
||||
[[instance.y, instance.y + ropeHeight]],
|
||||
(value) => {
|
||||
instance.y = value
|
||||
},
|
||||
{
|
||||
name: 'block'
|
||||
}
|
||||
)
|
||||
swing(instance, engine, time)
|
||||
break
|
||||
case constant.beforeDrop:
|
||||
i.x = instance.weightX - instance.calWidth
|
||||
i.y = instance.weightY + (0.3 * instance.height) // add rope height
|
||||
i.rotate = 0
|
||||
i.ay = engine.pixelsPerFrame(0.0003 * engine.height) // acceleration of gravity
|
||||
i.startDropTime = time
|
||||
i.status = constant.drop
|
||||
break
|
||||
case constant.drop:
|
||||
const deltaTime = time - i.startDropTime
|
||||
i.startDropTime = time
|
||||
i.vy += i.ay * deltaTime
|
||||
i.y += (i.vy * deltaTime) + (0.5 * i.ay * (deltaTime ** 2))
|
||||
const collision = checkCollision(instance, line)
|
||||
const blockY = line.y - instance.height
|
||||
const calRotate = (ins) => {
|
||||
ins.originOutwardAngle = Math.atan(ins.height / ins.outwardOffset)
|
||||
ins.originHypotenuse = Math.sqrt((ins.height ** 2)
|
||||
+ (ins.outwardOffset ** 2))
|
||||
engine.playAudio('rotate')
|
||||
}
|
||||
switch (collision) {
|
||||
case 1:
|
||||
checkBlockOut(instance, engine)
|
||||
break
|
||||
case 2:
|
||||
i.status = constant.rotateLeft
|
||||
instance.y = blockY
|
||||
instance.outwardOffset = (line.x + instance.calWidth) - instance.x
|
||||
calRotate(instance)
|
||||
break
|
||||
case 3:
|
||||
i.status = constant.rotateRight
|
||||
instance.y = blockY
|
||||
instance.outwardOffset = (line.collisionX + instance.calWidth) - instance.x
|
||||
calRotate(instance)
|
||||
break
|
||||
case 4:
|
||||
case 5:
|
||||
i.status = constant.land
|
||||
const lastSuccessCount = engine.getVariable(constant.successCount)
|
||||
addSuccessCount(engine)
|
||||
engine.setTimeMovement(constant.moveDownMovement, 500)
|
||||
if (lastSuccessCount === 10 || lastSuccessCount === 15) {
|
||||
engine.setTimeMovement(constant.lightningMovement, 150)
|
||||
}
|
||||
instance.y = blockY
|
||||
line.y = blockY
|
||||
line.x = i.x - i.calWidth
|
||||
line.collisionX = line.x + i.width
|
||||
// 作弊检测 超出左边或右边1/3
|
||||
const cheatWidth = i.width * 0.3
|
||||
if (i.x > engine.width - (cheatWidth * 2)
|
||||
|| i.x < -cheatWidth) {
|
||||
engine.setVariable(constant.hardMode, true)
|
||||
}
|
||||
if (collision === 5) {
|
||||
instance.perfect = true
|
||||
addScore(engine, true)
|
||||
engine.playAudio('drop-perfect')
|
||||
} else {
|
||||
addScore(engine)
|
||||
engine.playAudio('drop')
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
break
|
||||
case constant.land:
|
||||
engine.getTimeMovement(
|
||||
constant.moveDownMovement,
|
||||
[[instance.y, instance.y + (getMoveDownValue(engine, { pixelsPerFrame: s => s / 2 }))]],
|
||||
(value) => {
|
||||
if (!instance.visible) return
|
||||
instance.y = value
|
||||
if (instance.y > engine.height) {
|
||||
instance.visible = false
|
||||
}
|
||||
},
|
||||
{
|
||||
name: instance.name
|
||||
}
|
||||
)
|
||||
instance.x += getLandBlockVelocity(engine, time)
|
||||
break
|
||||
case constant.rotateLeft:
|
||||
case constant.rotateRight:
|
||||
const isRight = i.status === constant.rotateRight
|
||||
const rotateSpeed = engine.pixelsPerFrame(Math.PI * 4)
|
||||
const isShouldFall = isRight ? instance.rotate > 1.3 : instance.rotate < -1.3// 75度
|
||||
const leftFix = isRight ? 1 : -1
|
||||
if (isShouldFall) {
|
||||
instance.rotate += (rotateSpeed / 8) * leftFix
|
||||
instance.y += engine.pixelsPerFrame(engine.height * 0.7)
|
||||
instance.x += engine.pixelsPerFrame(engine.width * 0.3) * leftFix
|
||||
} else {
|
||||
let rotateRatio = (instance.calWidth - instance.outwardOffset)
|
||||
/ instance.calWidth
|
||||
rotateRatio = rotateRatio > 0.5 ? rotateRatio : 0.5
|
||||
instance.rotate += rotateSpeed * rotateRatio * leftFix
|
||||
const angle = instance.originOutwardAngle + instance.rotate
|
||||
const rotateAxisX = isRight ? line.collisionX + instance.calWidth
|
||||
: line.x + instance.calWidth
|
||||
const rotateAxisY = line.y
|
||||
instance.x = rotateAxisX -
|
||||
(Math.cos(angle) * instance.originHypotenuse)
|
||||
instance.y = rotateAxisY -
|
||||
(Math.sin(angle) * instance.originHypotenuse)
|
||||
}
|
||||
checkBlockOut(instance, engine)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const drawSwingBlock = (instance, engine) => {
|
||||
const bl = engine.getImg('blockRope')
|
||||
engine.ctx.drawImage(
|
||||
bl, instance.weightX - instance.calWidth
|
||||
, instance.weightY
|
||||
, instance.width, instance.height * 1.3
|
||||
)
|
||||
const leftX = instance.weightX - instance.calWidth
|
||||
engine.debugLineY(leftX)
|
||||
}
|
||||
|
||||
const drawBlock = (instance, engine) => {
|
||||
const { perfect } = instance
|
||||
const bl = engine.getImg(perfect ? 'block-perfect' : 'block')
|
||||
engine.ctx.drawImage(bl, instance.x, instance.y, instance.width, instance.height)
|
||||
}
|
||||
|
||||
const drawRotatedBlock = (instance, engine) => {
|
||||
const { ctx } = engine
|
||||
ctx.save()
|
||||
ctx.translate(instance.x, instance.y)
|
||||
ctx.rotate(instance.rotate)
|
||||
ctx.translate(-instance.x, -instance.y)
|
||||
drawBlock(instance, engine)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
export const blockPainter = (instance, engine) => {
|
||||
const { status } = instance
|
||||
switch (status) {
|
||||
case constant.swing:
|
||||
drawSwingBlock(instance, engine)
|
||||
break
|
||||
case constant.drop:
|
||||
case constant.land:
|
||||
drawBlock(instance, engine)
|
||||
break
|
||||
case constant.rotateLeft:
|
||||
case constant.rotateRight:
|
||||
drawRotatedBlock(instance, engine)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { checkMoveDown, getMoveDownValue } from './utils'
|
||||
import * as constant from './constant'
|
||||
|
||||
const randomCloudImg = (instance) => {
|
||||
const { count } = instance
|
||||
const clouds = ['c1', 'c2', 'c3']
|
||||
const stones = ['c4', 'c5', 'c6', 'c7', 'c8']
|
||||
const randomImg = array => (array[Math.floor(Math.random() * array.length)])
|
||||
instance.imgName = count > 6 ? randomImg(stones) : randomImg(clouds)
|
||||
}
|
||||
|
||||
export const cloudAction = (instance, engine) => {
|
||||
if (!instance.ready) {
|
||||
instance.ready = true
|
||||
randomCloudImg(instance)
|
||||
instance.width = engine.getVariable(constant.cloudSize)
|
||||
instance.height = engine.getVariable(constant.cloudSize)
|
||||
const engineW = engine.width
|
||||
const engineH = engine.height
|
||||
const positionArr = [
|
||||
{ x: engineW * 0.1, y: -engineH * 0.66 },
|
||||
{ x: engineW * 0.65, y: -engineH * 0.33 },
|
||||
{ x: engineW * 0.1, y: 0 },
|
||||
{ x: engineW * 0.65, y: engineH * 0.33 }
|
||||
]
|
||||
const position = positionArr[instance.index - 1]
|
||||
instance.x = engine.utils.random(position.x, (position.x * 1.2))
|
||||
instance.originX = instance.x
|
||||
instance.ax = engine.pixelsPerFrame(instance.width * engine.utils.random(0.05, 0.08)
|
||||
* engine.utils.randomPositiveNegative())
|
||||
instance.y = engine.utils.random(position.y, (position.y * 1.2))
|
||||
}
|
||||
instance.x += instance.ax
|
||||
if (instance.x >= instance.originX + instance.width
|
||||
|| instance.x <= instance.originX - instance.width) {
|
||||
instance.ax *= -1
|
||||
}
|
||||
if (checkMoveDown(engine)) {
|
||||
instance.y += getMoveDownValue(engine) * 1.2
|
||||
}
|
||||
if (instance.y >= engine.height) {
|
||||
instance.y = -engine.height * 0.66
|
||||
instance.count += 4
|
||||
randomCloudImg(instance)
|
||||
}
|
||||
}
|
||||
|
||||
export const cloudPainter = (instance, engine) => {
|
||||
const { ctx } = engine
|
||||
const cloud = engine.getImg(instance.imgName)
|
||||
ctx.drawImage(cloud, instance.x, instance.y, instance.width, instance.height)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
export const gameStartNow = 'GAME_START_NOW'
|
||||
export const gameUserOption = 'GAME_USER_OPTION'
|
||||
export const hardMode = 'HARD_MODE'
|
||||
|
||||
export const successCount = 'SUCCESS_COUNT'
|
||||
export const failedCount = 'FAILED_COUNT'
|
||||
export const perfectCount = 'PERFECT_COUNT'
|
||||
export const gameScore = 'GAME_SCORE'
|
||||
|
||||
export const hookDown = 'HOOK_DOWN'
|
||||
export const hookUp = 'HOOK_UP'
|
||||
export const hookNormal = 'HOOK_NORMAL'
|
||||
|
||||
export const bgImgOffset = 'BACKGROUND_IMG_OFFSET_HEIGHT'
|
||||
export const lineInitialOffset = 'LINE_INITIAL_OFFSET'
|
||||
export const bgLinearGradientOffset = 'BACKGROUND_LINEAR_GRADIENT_OFFSET_HEIGHT'
|
||||
|
||||
|
||||
export const blockCount = 'BLOCK_COUNT'
|
||||
export const blockWidth = 'BLOCK_WIDTH'
|
||||
export const blockHeight = 'BLOCK_HEIGHT'
|
||||
export const cloudSize = 'CLOUD_SIZE'
|
||||
export const ropeHeight = 'ROPE_HEIGHT'
|
||||
export const flightCount = 'FLIGHT_COUNT'
|
||||
export const flightLayer = 'FLIGHT_LAYER'
|
||||
|
||||
export const rotateRight = 'ROTATE_RIGHT'
|
||||
export const rotateLeft = 'ROTATE_LEFT'
|
||||
export const swing = 'SWING'
|
||||
export const beforeDrop = 'BEFORE_DROP'
|
||||
export const drop = 'DROP'
|
||||
export const land = 'LAND'
|
||||
export const out = 'OUT'
|
||||
|
||||
export const initialAngle = 'INITIAL_ANGLE'
|
||||
|
||||
export const bgInitMovement = 'BG_INIT_MOVEMENT'
|
||||
export const hookDownMovement = 'HOOK_DOWN_MOVEMENT'
|
||||
export const hookUpMovement = 'HOOK_UP_MOVEMENT'
|
||||
export const lightningMovement = 'LIGHTNING_MOVEMENT'
|
||||
export const tutorialMovement = 'TUTORIAL_MOVEMENT'
|
||||
export const moveDownMovement = 'MOVE_DOWN_MOVEMENT'
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Instance } from 'cooljs'
|
||||
import * as constant from './constant'
|
||||
|
||||
const getActionConfig = (engine, type) => {
|
||||
const {
|
||||
width, height, utils
|
||||
} = engine
|
||||
const { random } = utils
|
||||
const size = engine.getVariable(constant.cloudSize)
|
||||
const actionTypes = {
|
||||
bottomToTop: {
|
||||
x: width * random(0.3, 0.7),
|
||||
y: height,
|
||||
vx: 0,
|
||||
vy: engine.pixelsPerFrame(height) * 0.7 * -1
|
||||
},
|
||||
leftToRight: {
|
||||
x: size * -1,
|
||||
y: height * random(0.3, 0.6),
|
||||
vx: engine.pixelsPerFrame(width) * 0.4,
|
||||
vy: engine.pixelsPerFrame(height) * 0.1 * -1
|
||||
},
|
||||
rightToLeft: {
|
||||
x: width,
|
||||
y: height * random(0.2, 0.5),
|
||||
vx: engine.pixelsPerFrame(width) * 0.4 * -1,
|
||||
vy: engine.pixelsPerFrame(height) * 0.1
|
||||
},
|
||||
rightTopToLeft: {
|
||||
x: width,
|
||||
y: 0,
|
||||
vx: engine.pixelsPerFrame(width) * 0.6 * -1,
|
||||
vy: engine.pixelsPerFrame(height) * 0.5
|
||||
}
|
||||
}
|
||||
return actionTypes[type]
|
||||
}
|
||||
|
||||
|
||||
export const flightAction = (instance, engine) => {
|
||||
const { visible, ready, type } = instance
|
||||
if (!visible) return
|
||||
const size = engine.getVariable(constant.cloudSize)
|
||||
if (!ready) {
|
||||
const action = getActionConfig(engine, type)
|
||||
instance.ready = true
|
||||
instance.width = size
|
||||
instance.height = size
|
||||
instance.x = action.x
|
||||
instance.y = action.y
|
||||
instance.vx = action.vx
|
||||
instance.vy = action.vy
|
||||
}
|
||||
instance.x += instance.vx
|
||||
instance.y += instance.vy
|
||||
if (instance.y + size < 0
|
||||
|| instance.y > engine.height
|
||||
|| instance.x + size < 0
|
||||
|| instance.x > engine.width) {
|
||||
instance.visible = false
|
||||
}
|
||||
}
|
||||
|
||||
export const flightPainter = (instance, engine) => {
|
||||
const { ctx } = engine
|
||||
const flight = engine.getImg(instance.imgName)
|
||||
ctx.drawImage(flight, instance.x, instance.y, instance.width, instance.height)
|
||||
}
|
||||
|
||||
export const addFlight = (engine, number, type) => {
|
||||
const flightCount = engine.getVariable(constant.flightCount)
|
||||
if (flightCount === number) return
|
||||
const flight = new Instance({
|
||||
name: `flight_${number}`,
|
||||
action: flightAction,
|
||||
painter: flightPainter
|
||||
})
|
||||
flight.imgName = `f${number}`
|
||||
flight.type = type
|
||||
engine.addInstance(flight, constant.flightLayer)
|
||||
engine.setVariable(constant.flightCount, number)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { getSwingBlockVelocity } from './utils'
|
||||
import * as constant from './constant'
|
||||
|
||||
export const hookAction = (instance, engine, time) => {
|
||||
const ropeHeight = engine.getVariable(constant.ropeHeight)
|
||||
if (!instance.ready) {
|
||||
instance.x = engine.width / 2
|
||||
instance.y = ropeHeight * -1.5
|
||||
instance.ready = true
|
||||
}
|
||||
engine.getTimeMovement(
|
||||
constant.hookUpMovement,
|
||||
[[instance.y, instance.y - ropeHeight]],
|
||||
(value) => {
|
||||
instance.y = value
|
||||
},
|
||||
{
|
||||
after: () => {
|
||||
instance.y = ropeHeight * -1.5
|
||||
}
|
||||
}
|
||||
)
|
||||
engine.getTimeMovement(
|
||||
constant.hookDownMovement,
|
||||
[[instance.y, instance.y + ropeHeight]],
|
||||
(value) => {
|
||||
instance.y = value
|
||||
},
|
||||
{
|
||||
name: 'hook'
|
||||
}
|
||||
)
|
||||
const initialAngle = engine.getVariable(constant.initialAngle)
|
||||
instance.angle = initialAngle *
|
||||
getSwingBlockVelocity(engine, time)
|
||||
instance.weightX = instance.x +
|
||||
(Math.sin(instance.angle) * ropeHeight)
|
||||
instance.weightY = instance.y +
|
||||
(Math.cos(instance.angle) * ropeHeight)
|
||||
}
|
||||
|
||||
export const hookPainter = (instance, engine) => {
|
||||
const { ctx } = engine
|
||||
const ropeHeight = engine.getVariable(constant.ropeHeight)
|
||||
const ropeWidth = ropeHeight * 0.1
|
||||
const hook = engine.getImg('hook')
|
||||
ctx.save()
|
||||
ctx.translate(instance.x, instance.y)
|
||||
ctx.rotate((Math.PI * 2) - instance.angle)
|
||||
ctx.translate(-instance.x, -instance.y)
|
||||
engine.ctx.drawImage(hook, instance.x - (ropeWidth / 2), instance.y, ropeWidth, ropeHeight + 5)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { Engine, Instance } from 'cooljs'
|
||||
import { touchEventHandler } from './utils'
|
||||
import { background } from './background'
|
||||
import { lineAction, linePainter } from './line'
|
||||
import { cloudAction, cloudPainter } from './cloud'
|
||||
import { hookAction, hookPainter } from './hook'
|
||||
import { tutorialAction, tutorialPainter } from './tutorial'
|
||||
import * as constant from './constant'
|
||||
import { startAnimate, endAnimate } from './animateFuncs'
|
||||
|
||||
window.TowerGame = (option = {}) => {
|
||||
const {
|
||||
width,
|
||||
height,
|
||||
canvasId,
|
||||
soundOn
|
||||
} = option
|
||||
const game = new Engine({
|
||||
canvasId,
|
||||
highResolution: true,
|
||||
width,
|
||||
height,
|
||||
soundOn
|
||||
})
|
||||
const pathGenerator = (path) => `./assets/${path}`
|
||||
|
||||
game.addImg('background', pathGenerator('background.png'))
|
||||
game.addImg('hook', pathGenerator('hook.png'))
|
||||
game.addImg('blockRope', pathGenerator('block-rope.png'))
|
||||
game.addImg('block', pathGenerator('block.png'))
|
||||
game.addImg('block-perfect', pathGenerator('block-perfect.png'))
|
||||
for (let i = 1; i <= 8; i += 1) {
|
||||
game.addImg(`c${i}`, pathGenerator(`c${i}.png`))
|
||||
}
|
||||
game.addLayer(constant.flightLayer)
|
||||
for (let i = 1; i <= 7; i += 1) {
|
||||
game.addImg(`f${i}`, pathGenerator(`f${i}.png`))
|
||||
}
|
||||
game.swapLayer(0, 1)
|
||||
game.addImg('tutorial', pathGenerator('tutorial.png'))
|
||||
game.addImg('tutorial-arrow', pathGenerator('tutorial-arrow.png'))
|
||||
game.addImg('heart', pathGenerator('heart.png'))
|
||||
game.addImg('score', pathGenerator('score.png'))
|
||||
game.addAudio('drop-perfect', pathGenerator('drop-perfect.mp3'))
|
||||
game.addAudio('drop', pathGenerator('drop.mp3'))
|
||||
game.addAudio('game-over', pathGenerator('game-over.mp3'))
|
||||
game.addAudio('rotate', pathGenerator('rotate.mp3'))
|
||||
game.addAudio('bgm', pathGenerator('bgm.mp3'))
|
||||
game.setVariable(constant.blockWidth, game.width * 0.25)
|
||||
game.setVariable(constant.blockHeight, game.getVariable(constant.blockWidth) * 0.71)
|
||||
game.setVariable(constant.cloudSize, game.width * 0.3)
|
||||
game.setVariable(constant.ropeHeight, game.height * 0.4)
|
||||
game.setVariable(constant.blockCount, 0)
|
||||
game.setVariable(constant.successCount, 0)
|
||||
game.setVariable(constant.failedCount, 0)
|
||||
game.setVariable(constant.gameScore, 0)
|
||||
game.setVariable(constant.hardMode, false)
|
||||
game.setVariable(constant.gameUserOption, option)
|
||||
for (let i = 1; i <= 4; i += 1) {
|
||||
const cloud = new Instance({
|
||||
name: `cloud_${i}`,
|
||||
action: cloudAction,
|
||||
painter: cloudPainter
|
||||
})
|
||||
cloud.index = i
|
||||
cloud.count = 5 - i
|
||||
game.addInstance(cloud)
|
||||
}
|
||||
const line = new Instance({
|
||||
name: 'line',
|
||||
action: lineAction,
|
||||
painter: linePainter
|
||||
})
|
||||
game.addInstance(line)
|
||||
const hook = new Instance({
|
||||
name: 'hook',
|
||||
action: hookAction,
|
||||
painter: hookPainter
|
||||
})
|
||||
game.addInstance(hook)
|
||||
|
||||
game.startAnimate = startAnimate
|
||||
game.endAnimate = endAnimate
|
||||
game.paintUnderInstance = background
|
||||
game.addKeyDownListener('enter', () => {
|
||||
if (game.debug) game.togglePaused()
|
||||
})
|
||||
game.touchStartListener = () => {
|
||||
touchEventHandler(game)
|
||||
}
|
||||
|
||||
game.playBgm = () => {
|
||||
game.playAudio('bgm', true)
|
||||
}
|
||||
|
||||
game.pauseBgm = () => {
|
||||
game.pauseAudio('bgm')
|
||||
}
|
||||
|
||||
game.start = () => {
|
||||
const tutorial = new Instance({
|
||||
name: 'tutorial',
|
||||
action: tutorialAction,
|
||||
painter: tutorialPainter
|
||||
})
|
||||
game.addInstance(tutorial)
|
||||
const tutorialArrow = new Instance({
|
||||
name: 'tutorial-arrow',
|
||||
action: tutorialAction,
|
||||
painter: tutorialPainter
|
||||
})
|
||||
game.addInstance(tutorialArrow)
|
||||
game.setTimeMovement(constant.bgInitMovement, 500)
|
||||
game.setTimeMovement(constant.tutorialMovement, 500)
|
||||
game.setVariable(constant.gameStartNow, true)
|
||||
}
|
||||
|
||||
return game
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { getMoveDownValue, getLandBlockVelocity } from './utils'
|
||||
import * as constant from './constant'
|
||||
|
||||
export const lineAction = (instance, engine, time) => {
|
||||
const i = instance
|
||||
if (!i.ready) {
|
||||
i.y = engine.getVariable(constant.lineInitialOffset)
|
||||
i.ready = true
|
||||
i.collisionX = engine.width - engine.getVariable(constant.blockWidth)
|
||||
}
|
||||
engine.getTimeMovement(
|
||||
constant.moveDownMovement,
|
||||
[[instance.y, instance.y + (getMoveDownValue(engine, { pixelsPerFrame: s => s / 2 }))]],
|
||||
(value) => {
|
||||
instance.y = value
|
||||
},
|
||||
{
|
||||
name: 'line'
|
||||
}
|
||||
)
|
||||
const landBlockVelocity = getLandBlockVelocity(engine, time)
|
||||
instance.x += landBlockVelocity
|
||||
instance.collisionX += landBlockVelocity
|
||||
}
|
||||
|
||||
export const linePainter = (instance, engine) => {
|
||||
const { ctx, debug } = engine
|
||||
if (!debug) {
|
||||
return
|
||||
}
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = 'red'
|
||||
ctx.moveTo(instance.x, instance.y)
|
||||
ctx.lineTo(instance.collisionX, instance.y)
|
||||
ctx.lineWidth = 1
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { getHookStatus } from './utils'
|
||||
import * as constant from './constant'
|
||||
|
||||
export const tutorialAction = (instance, engine, time) => {
|
||||
const { width, height } = engine
|
||||
const { name } = instance
|
||||
if (!instance.ready) {
|
||||
instance.ready = true
|
||||
const tutorialWidth = width * 0.2
|
||||
instance.updateWidth(tutorialWidth)
|
||||
instance.height = tutorialWidth * 0.46
|
||||
instance.x = engine.calWidth - instance.calWidth
|
||||
instance.y = height * 0.45
|
||||
if (name !== 'tutorial') {
|
||||
instance.y += instance.height * 1.2
|
||||
}
|
||||
}
|
||||
if (name !== 'tutorial') {
|
||||
instance.y += Math.cos(time / 200) * instance.height * 0.01
|
||||
}
|
||||
}
|
||||
|
||||
export const tutorialPainter = (instance, engine) => {
|
||||
if (engine.checkTimeMovement(constant.tutorialMovement)) {
|
||||
return
|
||||
}
|
||||
if (getHookStatus(engine) !== constant.hookNormal) {
|
||||
return
|
||||
}
|
||||
const { ctx } = engine
|
||||
const { name } = instance
|
||||
const t = engine.getImg(name)
|
||||
ctx.drawImage(t, instance.x, instance.y, instance.width, instance.height)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import * as constant from './constant'
|
||||
|
||||
export const checkMoveDown = engine =>
|
||||
(engine.checkTimeMovement(constant.moveDownMovement))
|
||||
|
||||
export const getMoveDownValue = (engine, store) => {
|
||||
const pixelsPerFrame = store ? store.pixelsPerFrame : engine.pixelsPerFrame.bind(engine)
|
||||
const successCount = engine.getVariable(constant.successCount)
|
||||
const calHeight = engine.getVariable(constant.blockHeight) * 2
|
||||
if (successCount <= 4) {
|
||||
return pixelsPerFrame(calHeight * 1.25)
|
||||
}
|
||||
return pixelsPerFrame(calHeight)
|
||||
}
|
||||
|
||||
export const getAngleBase = (engine) => {
|
||||
const successCount = engine.getVariable(constant.successCount)
|
||||
const gameScore = engine.getVariable(constant.gameScore)
|
||||
const { hookAngle } = engine.getVariable(constant.gameUserOption)
|
||||
if (hookAngle) {
|
||||
return hookAngle(successCount, gameScore)
|
||||
}
|
||||
if (engine.getVariable(constant.hardMode)) {
|
||||
return 90
|
||||
}
|
||||
switch (true) {
|
||||
case successCount < 10:
|
||||
return 30
|
||||
case successCount < 20:
|
||||
return 60
|
||||
default:
|
||||
return 80
|
||||
}
|
||||
}
|
||||
|
||||
export const getSwingBlockVelocity = (engine, time) => {
|
||||
const successCount = engine.getVariable(constant.successCount)
|
||||
const gameScore = engine.getVariable(constant.gameScore)
|
||||
const { hookSpeed } = engine.getVariable(constant.gameUserOption)
|
||||
if (hookSpeed) {
|
||||
return hookSpeed(successCount, gameScore)
|
||||
}
|
||||
let hard
|
||||
switch (true) {
|
||||
case successCount < 1:
|
||||
hard = 0
|
||||
break
|
||||
case successCount < 10:
|
||||
hard = 1
|
||||
break
|
||||
case successCount < 20:
|
||||
hard = 0.8
|
||||
break
|
||||
case successCount < 30:
|
||||
hard = 0.7
|
||||
break
|
||||
default:
|
||||
hard = 0.74
|
||||
break
|
||||
}
|
||||
if (engine.getVariable(constant.hardMode)) {
|
||||
hard = 1.1
|
||||
}
|
||||
return Math.sin(time / (200 / hard))
|
||||
}
|
||||
|
||||
export const getLandBlockVelocity = (engine, time) => {
|
||||
const successCount = engine.getVariable(constant.successCount)
|
||||
const gameScore = engine.getVariable(constant.gameScore)
|
||||
const { landBlockSpeed } = engine.getVariable(constant.gameUserOption)
|
||||
if (landBlockSpeed) {
|
||||
return landBlockSpeed(successCount, gameScore)
|
||||
}
|
||||
const { width } = engine
|
||||
let hard
|
||||
switch (true) {
|
||||
case successCount < 5:
|
||||
hard = 0
|
||||
break
|
||||
case successCount < 13:
|
||||
hard = 0.001
|
||||
break
|
||||
case successCount < 23:
|
||||
hard = 0.002
|
||||
break
|
||||
default:
|
||||
hard = 0.003
|
||||
break
|
||||
}
|
||||
return Math.cos(time / 200) * hard * width
|
||||
}
|
||||
|
||||
export const getHookStatus = (engine) => {
|
||||
if (engine.checkTimeMovement(constant.hookDownMovement)) {
|
||||
return constant.hookDown
|
||||
}
|
||||
if (engine.checkTimeMovement(constant.hookUpMovement)) {
|
||||
return constant.hookUp
|
||||
}
|
||||
return constant.hookNormal
|
||||
}
|
||||
|
||||
export const touchEventHandler = (engine) => {
|
||||
if (!engine.getVariable(constant.gameStartNow)) return
|
||||
if (engine.debug && engine.paused) {
|
||||
return
|
||||
}
|
||||
if (getHookStatus(engine) !== constant.hookNormal) {
|
||||
return
|
||||
}
|
||||
engine.removeInstance('tutorial')
|
||||
engine.removeInstance('tutorial-arrow')
|
||||
const b = engine.getInstance(`block_${engine.getVariable(constant.blockCount)}`)
|
||||
if (b && b.status === constant.swing) {
|
||||
engine.setTimeMovement(constant.hookUpMovement, 500)
|
||||
b.status = constant.beforeDrop
|
||||
}
|
||||
}
|
||||
|
||||
export const addSuccessCount = (engine) => {
|
||||
const { setGameSuccess } = engine.getVariable(constant.gameUserOption)
|
||||
const lastSuccessCount = engine.getVariable(constant.successCount)
|
||||
const success = lastSuccessCount + 1
|
||||
engine.setVariable(constant.successCount, success)
|
||||
if (engine.getVariable(constant.hardMode)) {
|
||||
engine.setVariable(constant.ropeHeight, engine.height * engine.utils.random(0.35, 0.55))
|
||||
}
|
||||
if (setGameSuccess) setGameSuccess(success)
|
||||
}
|
||||
|
||||
export const addFailedCount = (engine) => {
|
||||
const { setGameFailed } = engine.getVariable(constant.gameUserOption)
|
||||
const lastFailedCount = engine.getVariable(constant.failedCount)
|
||||
const failed = lastFailedCount + 1
|
||||
engine.setVariable(constant.failedCount, failed)
|
||||
engine.setVariable(constant.perfectCount, 0)
|
||||
if (setGameFailed) setGameFailed(failed)
|
||||
if (failed >= 3) {
|
||||
engine.pauseAudio('bgm')
|
||||
engine.playAudio('game-over')
|
||||
engine.setVariable(constant.gameStartNow, false)
|
||||
}
|
||||
}
|
||||
|
||||
export const addScore = (engine, isPerfect) => {
|
||||
const { setGameScore, successScore, perfectScore } = engine.getVariable(constant.gameUserOption)
|
||||
const lastPerfectCount = engine.getVariable(constant.perfectCount, 0)
|
||||
const lastGameScore = engine.getVariable(constant.gameScore)
|
||||
const perfect = isPerfect ? lastPerfectCount + 1 : 0
|
||||
const score = lastGameScore + (successScore || 25) + ((perfectScore || 25) * perfect)
|
||||
engine.setVariable(constant.gameScore, score)
|
||||
engine.setVariable(constant.perfectCount, perfect)
|
||||
if (setGameScore) setGameScore(score)
|
||||
}
|
||||
|
||||
export const drawYellowString = (engine, option) => {
|
||||
const {
|
||||
string, size, x, y, textAlign, fontName = 'wenxue', fontWeight = 'normal'
|
||||
} = option
|
||||
const { ctx } = engine
|
||||
const fontSize = size
|
||||
const lineSize = fontSize * 0.1
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, y)
|
||||
gradient.addColorStop(0, '#FAD961')
|
||||
gradient.addColorStop(1, '#F76B1C')
|
||||
ctx.fillStyle = gradient
|
||||
ctx.lineWidth = lineSize
|
||||
ctx.strokeStyle = '#FFF'
|
||||
ctx.textAlign = textAlign || 'center'
|
||||
ctx.font = `${fontWeight} ${fontSize}px ${fontName}`
|
||||
ctx.strokeText(string, x, y)
|
||||
ctx.fillText(string, x, y)
|
||||
ctx.restore()
|
||||
}
|
||||
@@ -73,6 +73,36 @@ const GamePage: FC = () => {
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{ direction: 'ltr' }}
|
||||
className='bg-white shadow-sm rounded-[10px] p-2 flex justify-between items-center cursor-pointer'
|
||||
onClick={() => {
|
||||
const currentPath = window.location.pathname
|
||||
const basePath = currentPath.split('/game')[0]
|
||||
router.push(`${basePath}/game/tower-builder`)
|
||||
}}
|
||||
>
|
||||
<div className='flex gap-3 items-center'>
|
||||
<Image
|
||||
src='/assets/images/tower_building_logo.webp'
|
||||
alt='Tower Builder'
|
||||
width={80}
|
||||
height={80}
|
||||
className='size-[80px] object-cover'
|
||||
/>
|
||||
|
||||
<div className='text-sm font-bold max-w-[92px]'>
|
||||
Tower Builder
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='h-8 bg-[#F2F2F2] rounded-lg px-3 flex gap-1 items-center justify-center'>
|
||||
<div className='text-xs'>اجرا کنید</div>
|
||||
<Play size='20' color='currentColor' />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
'use client'
|
||||
import { type FC, type ReactNode } from 'react'
|
||||
|
||||
interface TowerBuilderLayoutProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const TowerBuilderLayout: FC<TowerBuilderLayoutProps> = ({ children }) => {
|
||||
return (
|
||||
<div className='h-screen w-full p-0 m-0'>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TowerBuilderLayout
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
import { type FC } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { CloseCircle } from 'iconsax-react'
|
||||
|
||||
const TowerBuilderPage: FC = () => {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div className='fixed inset-0 w-full h-full p-0 m-0 flex items-center justify-center bg-white'>
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className='absolute top-4 left-4 z-50 bg-white rounded-full shadow-lg p-1 hover:bg-gray-100 transition-colors'
|
||||
aria-label='بستن'
|
||||
>
|
||||
<CloseCircle size='32' color='#000' variant='Bold' />
|
||||
</button>
|
||||
<iframe
|
||||
src='/tower_game-master/index.html'
|
||||
className='w-full h-full border-0'
|
||||
title='Tower Builder Game'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TowerBuilderPage
|
||||