Initial commit

This commit is contained in:
NGQwMzBkYmY3 2021-10-22 11:56:15 +08:00
parent f552161e5b
commit cbb8612357
70 changed files with 1869 additions and 6 deletions

15
.babelrc Normal file
View File

@ -0,0 +1,15 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"browsers": [
"ios >= 9",
"android >= 4"
]
}
}
]
]
}

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
node_modules
.idea/
npm-debug.log
yarn.lock
.DS_Store

20
LICENSE
View File

@ -1,9 +1,21 @@
MIT License
Copyright (c) <year> <copyright holders>
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:
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 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.
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.

134
README.md
View File

@ -1,3 +1,133 @@
# towergame
[![](http://www.writebug.com/myres/static/uploads/2021/10/22/9c92c17a3018530ae6c17b70c748ed01.writebug)](LICENSE)
盖楼游戏 html5 canvas tower building game
<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 http://git.writebug.com/NGQwMzBkYmY3/towergame.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.

BIN
assets/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
assets/bgm.mp3 Normal file

Binary file not shown.

BIN
assets/bgm.ogg Normal file

Binary file not shown.

BIN
assets/block-perfect.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
assets/block-rope.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
assets/block.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/c1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
assets/c2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
assets/c3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
assets/c4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
assets/c5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
assets/c6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
assets/c7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
assets/c8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
assets/drop-perfect.mp3 Normal file

Binary file not shown.

BIN
assets/drop-perfect.ogg Normal file

Binary file not shown.

BIN
assets/drop.mp3 Normal file

Binary file not shown.

BIN
assets/drop.ogg Normal file

Binary file not shown.

BIN
assets/f1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
assets/f2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
assets/f3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
assets/f4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/f5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/f6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
assets/f7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
assets/game-over.mp3 Normal file

Binary file not shown.

BIN
assets/game-over.ogg Normal file

Binary file not shown.

BIN
assets/heart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
assets/hook.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
assets/main-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
assets/main-index-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
assets/main-index-start.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
assets/main-index-title.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
assets/main-loading.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
assets/main-modal-bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
assets/main-modal-over.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/main-share-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
assets/rope.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

BIN
assets/rotate.mp3 Normal file

Binary file not shown.

BIN
assets/rotate.ogg Normal file

Binary file not shown.

BIN
assets/score.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
assets/tutorial-arrow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/tutorial.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
assets/wenxue.eot Normal file

Binary file not shown.

1
assets/wenxue.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
assets/wenxue.ttf Normal file

Binary file not shown.

BIN
assets/wenxue.woff Normal file

Binary file not shown.

2
assets/zepto-1.1.6.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/main.js vendored Normal file

File diff suppressed because one or more lines are too long

571
index.html Normal file
View File

@ -0,0 +1,571 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0,maximum-scale=1.0, user-scalable=no,minimal-ui">
<title>Tower</title>
<style>
* {
margin: 0;
padding: 0
}
img {
width: 100%
}
html {
background: #FFF;
height: 100%
}
body {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
margin: 0 auto;
text-align: center;
width: 100%;
height: 100%;
background: #F95240 url(./assets/main-bg.png)
}
@media screen and (min-height: 560px) {
html {
font-size: 100px
}
}
@media screen and (min-height: 640px) {
html {
font-size: 112.5px
}
}
@media screen and (min-height: 720px) {
html {
font-size: 125px
}
}
@media screen and (min-height: 800px) {
html {
font-size: 137.5px
}
}
@media screen and (min-height: 880px) {
html {
font-size: 150px
}
}
@media screen and (min-height: 960px) {
html {
font-size: 162.5px
}
}
@media screen and (min-height: 1040px) {
html {
font-size: 180px
}
}
@media screen and (min-height: 1200px) {
html {
font-size: 200px
}
}
html {
font-size: 17.6vh
}
#canvas {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
a {
text-decoration: none
}
li, ul, ol {
list-style-type: none;
padding: 0;
margin: 0
}
.hide {
display: none
}
.clear {
clear: both
}
.loading {
background-color: #F05A50;
height: 100%;
width: 100%;
}
.loading .main {
width: 60%;
margin: 0 auto;
color: #FFF
}
.loading .main img {
width: 60%;
margin: 1rem auto 0
}
.loading .main .title {
font-size: .3rem
}
.loading .main .text {
font-size: .15rem
}
.loading .main .bar {
height: .12rem;
width: 100%;
border: 3px solid #FFF;
border-radius: .6rem;
margin: .1rem 0;
}
.loading .main .bar .sub {
height: .1rem;
width: 98%;
margin: .008rem auto 0;
}
.loading .main .bar .percent {
height: 100%;
width: 0;
background-color: #FFF;
border-radius: .6rem;
}
.loading .logo {
position: absolute;
bottom: .3rem;
left: 0;
right: 0
}
.loading .logo img {
width: 1rem
}
.content {
height: 100vh;
margin: 0 auto;
position: relative;
}
.landing .title {
width: 60%;
}
.landing .logo {
width: 30%;
position: absolute;
right: .2rem;
top: .2rem;
}
.landing .action-2 {
position: absolute;
bottom: .2rem;
width: 100%;
}
.landing .start {
width: 65%;
}
.slideTop {
-webkit-animation: st 1s ease-in-out;
animation: st 1s ease-in-out;
}
@-webkit-keyframes st {
0% {
transform: translateZ(0)
}
100% {
transform: translate3d(0, -100%, 0)
}
}
@keyframes st {
0% {
transform: translateZ(0)
}
100% {
transform: translate3d(0, -100%, 0)
}
}
.slideBottom {
-webkit-animation: sb 1s ease-in-out;
animation: sb 1s ease-in-out;
}
@-webkit-keyframes sb {
0% {
transform: translateZ(0)
}
100% {
transform: translate3d(0, 200%, 0)
}
}
@keyframes sb {
0% {
transform: translateZ(0)
}
100% {
transform: translate3d(0, 200%, 0)
}
}
.swing {
-webkit-animation: sw 2s ease-in-out alternate infinite;
animation: sw 2s ease-in-out alternate infinite;
}
@-webkit-keyframes sw {
0% {
transform: rotate(5deg);
transform-origin: top center;
}
100% {
transform: rotate(-5deg);
transform-origin: top center;
}
}
@keyframes sw {
0% {
transform: rotate(5deg);
transform-origin: top center;
}
100% {
transform: rotate(-5deg);
transform-origin: top center;
}
}
.modal .mask {
background-color: #000;
opacity: .6;
position: fixed;
height: 100%;
width: 100%;
top: 0;
left: 0;
}
.modal .modal-content {
position: fixed;
height: 100%;
width: 90%;
margin-top: .3rem;
top: 0;
}
.modal .main {
width: 85%;
margin: 0 auto;
}
.modal .container {
position: relative
}
.modal .bg {
width: 100%;
position: absolute;
top: 0;
left: 0
}
.modal .modal-main {
width: 100%;
position: absolute;
top: 0;
left: 0;
margin-top: -0.4rem;
}
.modal .over-img {
width: 45%;
margin: .8rem auto 0
}
.modal .over-score {
margin-top: -0.2rem;
font-size: .5rem;
color: #FF735C;
text-shadow: -2px -2px 0 #FFF, 2px -2px 0 #FFF, -2px 2px 0 #FFF, 2px 2px 0 #FFF;
}
.modal .tip {
font-size: .16rem;
color: #9B724E;
}
.modal .over-button-b {
width: 70%;
margin: 0.1rem auto 0
}
.wxShare {
background: #000;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 11;
opacity: .9
}
.wxShare img {
width: 50%;
float: right;
margin: 10px 10px 0 0
}
@font-face {
font-family: 'wenxue';
src: url('./assets/wenxue.eot');
src: url('./assets/wenxue.eot'),
url('./assets/wenxue.woff'),
url('./assets/wenxue.ttf'),
url('./assets/wenxue.svg');
}
.font-wenxue {
font-family: 'wenxue';
}
</style>
</head>
<body>
<canvas id="canvas" class="hide"></canvas>
<div class="content">
<div class="loading">
<div class="main"><img
src="./assets/main-loading.gif">
<div class="progress">
<div class="title font-wenxue">0%</div>
<div class="bar">
<div class="sub">
<div class="percent"></div>
</div>
</div>
<div class="text">加载中</div>
</div>
</div>
</div>
<div class="landing hide">
<div class="action-1">
<img
src="./assets/main-index-title.png"
class="title swing">
</div>
<div class="action-2"><img id="start"
src="./assets/main-index-start.png"
class="start"></div>
</div>
<div id="modal" class="modal hide">
<div class="mask"></div>
<div class="js-modal-content modal-content">
<div class="main">
<div class="container"><img
src="./assets/main-modal-bg.png"
class="bg">
<div class="modal-main">
<div id="over-modal" class="hide js-modal-card"><img
src="./assets/main-modal-over.png"
class="over-img">
<div id="score" class="over-score font-wenxue"></div>
<div id="over-zero" class="hide">
<div class="tip"><p>再来一次吧!</p>
<img
src="./assets/main-modal-again-b.png"
class="over-button-b js-reload"><img
src="./assets/main-modal-invite-b.png"
class="over-button-b js-invite"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="wxShare hide">
<img src="./assets/main-share-icon.png">
</div>
</div>
<script src="./dist/main.js"></script>
<script src="./assets/zepto-1.1.6.min.js"></script>
<script>
var domReady, loadFinish, canvasReady, loadError, gameStart, game, score, successCount
// init window height and width
var gameWidth = window.innerWidth
var gameHeight = window.innerHeight
var ratio = 1.5
if (gameHeight / gameWidth < ratio) {
gameWidth = Math.ceil(gameHeight / ratio)
}
$('.content').css({ "height": gameHeight + "px", "width": gameWidth + "px" })
$('.js-modal-content').css({ "width": gameWidth + "px" })
// loading animation
function hideLoading() {
if (domReady && canvasReady) {
$('#canvas').show()
loadFinish = true
setTimeout(function () {
$('.loading').hide()
$('.landing').show()
}, 1000)
}
}
function updateLoading(status) {
var success = status.success
var total = status.total
var failed = status.failed
if (failed > 0 && !loadError) {
loadError = true
alert('加载失败 请刷新后重试')
return
}
var percent = parseInt((success / total) * 100);
if (percent === 100 && !canvasReady) {
canvasReady = true
hideLoading()
}
percent = percent > 98 ? 98 : percent
percent = percent + '%'
$('.loading .title').text(percent);
$('.loading .percent').css({
'width': percent
})
}
function overShowOver() {
$('#modal').show()
$('#over-modal').show()
$('#over-zero').show()
}
// game customization options
const option = {
width: gameWidth,
height: gameHeight,
canvasId: 'canvas',
soundOn: true,
setGameScore: function (s) {
score = s
},
setGameSuccess: function (s) {
successCount = s
},
setGameFailed: function (f) {
$('#score').text(score)
if (f >= 3) overShowOver()
}
}
// game init with option
function gameReady() {
game = TowerGame(option)
game.load(function () {
game.playBgm()
game.init()
}, updateLoading)
}
var isWechat = navigator.userAgent.toLowerCase().indexOf("micromessenger") !== -1
if (isWechat) {
document.addEventListener("WeixinJSBridgeReady", gameReady, false)
} else {
gameReady()
}
function indexHide() {
$('.landing .action-1').addClass('slideTop')
$('.landing .action-2').addClass('slideBottom')
setTimeout(function () {
$('.landing').hide()
}, 950)
}
// click event
$('#start').on('click', function () {
if (gameStart) return
gameStart = true
indexHide()
setTimeout(game.start, 400)
})
$('.js-reload').on('click', function () {
window.location.href = window.location.href + '?s=' + (+new Date())
})
$('.js-invite').on('click', function () {
$('.wxShare').show()
})
$('.wxShare').on('click', function () {
$('.wxShare').hide()
})
// listener
window.addEventListener('load', function () {
domReady = true
hideLoading()
}, false);
</script>
<script>
(function (i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
i[r] = i[r] || function () {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date();
a = s.createElement(o),
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m)
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
ga('create', 'UA-46444752-20', 'auto');
ga('send', 'pageview');
var _hmt = _hmt || [];
(function () {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?c1b044f909411ac4213045f0478e96fc";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
</body>
</html>

17
index.js Normal file
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)
})

32
package.json Normal file
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",
"webpack": "^4.0.1",
"webpack-cli": "^2.0.9"
},
"dependencies": {
"cooljs": "^1.0.2",
"express": "^4.16.3",
"opn": "^5.3.0"
}
}

124
src/animateFuncs.js Normal file
View File

@ -0,0 +1,124 @@
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: '层',
size: engine.width * 0.06,
x: (engine.width * 0.24) + threeFiguresOffset,
y: engine.width * 0.12,
textAlign: 'left'
})
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
src/background.js Normal file
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
src/block.js Normal file
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
src/cloud.js Normal file
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
src/constant.js Normal file
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
src/flight.js Normal file
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
src/hook.js Normal file
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
src/index.js Normal file
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
src/line.js Normal file
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
src/tutorial.js Normal file
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)
}

177
src/utils.js Normal file
View File

@ -0,0 +1,177 @@
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
} = option
const { ctx } = engine
const fontName = 'wenxue'
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 = `${fontSize}px ${fontName}`
ctx.strokeText(string, x, y)
ctx.fillText(string, x, y)
ctx.restore()
}