add tower builing game

This commit is contained in:
hamid zarghami
2025-12-09 10:48:48 +03:30
parent 8119203506
commit 53b9139506
79 changed files with 15363 additions and 3 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

+6 -1
View File
@@ -12,7 +12,12 @@
content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0, maximum-scale=1, user-scalable=no, minimal-ui"> content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0, maximum-scale=1, user-scalable=no, minimal-ui">
<style> <style>
body { body {
margin: 10px 0 20px 0 !important; margin: 0 !important;
padding: 0 !important;
}
.container {
padding: 0 8px !important;
} }
.heading { .heading {
+10 -2
View File
@@ -12,6 +12,10 @@ body {
body { body {
/* margin: 80px 0; */ /* margin: 80px 0; */
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
} }
.heading:after { .heading:after {
@@ -144,8 +148,10 @@ hr {
} }
.container { .container {
width: 500px; width: calc(100% - 16px);
max-width: 500px;
margin: 0 auto; margin: 0 auto;
padding: 0 8px;
} }
@-webkit-keyframes fade-in { @-webkit-keyframes fade-in {
@@ -683,8 +689,10 @@ hr {
} }
.container { .container {
width: 280px; width: calc(100% - 16px);
max-width: 280px;
margin: 0 auto; margin: 0 auto;
padding: 0 8px;
} }
.score-container, .score-container,
+15
View File
@@ -0,0 +1,15 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": [
"ios >= 9",
"android >= 4"
]
}
}
]
]
}
+6
View File
@@ -0,0 +1,6 @@
node_modules
.idea/
npm-debug.log
yarn.lock
.DS_Store
+21
View File
@@ -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.
+126
View File
@@ -0,0 +1,126 @@
[![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](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.
+122
View File
@@ -0,0 +1,122 @@
[![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+17
View File
@@ -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)
})
File diff suppressed because it is too large Load Diff
+32
View File
@@ -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
}
}
+100
View File
@@ -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)
}
+250
View File
@@ -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
// 作弊检测 超出左边或右边13
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
}
}
+53
View File
@@ -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)
}
+42
View File
@@ -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'
+82
View File
@@ -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)
}
+54
View File
@@ -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()
}
+119
View File
@@ -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
}
+40
View File
@@ -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()
}
+35
View File
@@ -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)
}
+176
View File
@@ -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()
}
+30
View File
@@ -73,6 +73,36 @@ const GamePage: FC = () => {
</div> </div>
</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>
</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