├── .gitignore ├── License.md ├── README.md ├── build ├── favicon.ico ├── readme.txt ├── td-pkg-en-min.js ├── td-pkg-en-min.js.map ├── td-pkg-zh-min.js ├── td-pkg-zh-min.js.map └── td.html ├── gulpfile.js ├── package.json ├── screenshots ├── 1.png ├── 10.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png └── 9.png ├── src ├── css │ └── c.css ├── favicon.ico ├── js │ ├── td-cfg-buildings.js │ ├── td-cfg-monsters.js │ ├── td-data-stage-1.js │ ├── td-element.js │ ├── td-event.js │ ├── td-lang.js │ ├── td-msg-en.js │ ├── td-msg-zh.js │ ├── td-obj-building.js │ ├── td-obj-grid.js │ ├── td-obj-map.js │ ├── td-obj-monster.js │ ├── td-obj-panel.js │ ├── td-render-buildings.js │ ├── td-stage.js │ ├── td-walk.js │ └── td.js └── td.html └── tools ├── compressor.py ├── countjslines.py └── deploy.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | *.pyc 4 | build/td-pkg.js 5 | src/css/c-min.css 6 | node_modules 7 | npm-debug.log 8 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 oldj 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTML5 塔防游戏 2 | 3 | 4 | * Author: oldj 5 | * Email: oldj.wu@gmail.com 6 | * Blog: [http://oldj.net/](http://oldj.net/) 7 | * Source: https://github.com/oldj/html5-tower-defense 8 | * License: MIT 9 | 10 | 11 | ## 运行 12 | 13 | 进入 `src` 或 `build` 目录,用浏览器(如 Chrome、IE9 )打开 td.html 即可运行本游戏。 14 | 15 | 或者查看[线上Demo](http://oldj.net/static/html5-tower-defense/td.html)。 16 | 17 | ## 说明 18 | 19 | 1. 本游戏完全使用 HTML5 / JavaScript / CSS 实现,没有用到 Flash、SilverLight 等技术。 20 | 2. 这一个版本没有用到图片,游戏中的所有物品都是使用 HTML5 画出来的。 21 | 3. 这一个版本部分地方为 IE9 做了专门的优化,可正常运行在 IE9 下。 22 | 23 | 24 | ## 目录 25 | 26 | /build 压缩后的可发布的文件 27 | /screenshorts 屏幕截图 28 | /src 源码 29 | /css 样式表 30 | /js JavaScripts 源文件 31 | /tools 小工具、脚本 32 | /README.md 本文件 33 | 34 | 35 | ## 作弊方法 36 | 37 | 为方便测试,本游戏内置了几个作弊方法,在命令行中执行如下命令即可: 38 | 39 | 1. 增加 100 万金钱:`_TD.cheat="money+";` 40 | 2. 难度增倍:`_TD.cheat="difficulty+";` 41 | 3. 难度减半:`_TD.cheat="difficulty-";` 42 | 4. 生命值恢复:`_TD.cheat="life+";` 43 | 5. 生命值降为最低:`_TD.cheat="life-";` 44 | 45 | 注意,以上作弊方法主要是为测试设计,正常游戏过程中请酌情使用,否则可能会降低游戏乐趣。 46 | 47 | 48 | ## 更新历史 49 | 50 | - 2015-09-06 支持 retina 显示屏。 51 | - 2011-01-01 调整参数,同时根据网友建议,新建建筑时添加检查,禁止用建筑把怪物包围起来(v0.1.14)。 52 | - 2010-12-29 根据网友建议,增加生命自动恢复功能(每隔 5 波生命恢复 5 点,每隔 10 波生命恢复 10 点)。调整参数,减小了激光枪的射程,增强了重机枪的威力(v0.1.12)。 53 | - 2010-12-18 添加新武器“激光枪”(v0.1.8.0)。 54 | - 2010-12-12 暂停图片资源版本分支的开发,继续优化、开发圈圈版(v0.1.7.0)。 55 | - 2010-11-28 第一个图片资源版本(v0.2.1.3267)。 56 | - 2010-11-23 发布 [圈圈版(v0.1.6.2970)](http://oldj.net/article/html5-td-circle-version/)。 57 | - 2010-11-14 线上发布第一个版本。 58 | - 2010-11-11 开始编写这个游戏。 59 | 60 | 61 | ## 开发计划 62 | 63 | - 添加新武器“加农炮”,特性:击中怪物时会发生爆炸,造成面攻击。 64 | - 添加关卡编辑器。 65 | - 添加保存进度的功能。 66 | -------------------------------------------------------------------------------- /build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldj/html5-tower-defense/e3e009c7673121e98d6ff02c85fb442c774b6391/build/favicon.ico -------------------------------------------------------------------------------- /build/readme.txt: -------------------------------------------------------------------------------- 1 | 说明: 2 | 3 | 如果你是从 github.com 等地方下载的本源码,则此文件夹下的文件可能不是最新的。 4 | 请转到 ../tools/ 目录,执行: 5 | 6 | python deploy.py 7 | 8 | 以便将 ../src/ 目录下的最新代码合并、压缩到本目录下。 9 | -------------------------------------------------------------------------------- /build/td-pkg-zh-min.js: -------------------------------------------------------------------------------- 1 | var _TD={a:[],retina:window.devicePixelRatio||1,init:function(t,i){delete this.init;var e,s={version:"0.1.17",is_debug:!!i,is_paused:!0,width:16,height:16,show_monster_life:!0,fps:0,exp_fps:24,exp_fps_half:12,exp_fps_quarter:6,exp_fps_eighth:4,stage_data:{},defaultSettings:function(){return{step_time:36,grid_size:32*_TD.retina,padding:10*_TD.retina,global_speed:.1}},init:function(t){this.obj_board=s.lang.$e(t),this.canvas=this.obj_board.getElementsByTagName("canvas")[0],this.canvas.getContext&&(this.ctx=this.canvas.getContext("2d"),this.monster_type_count=s.getDefaultMonsterAttributes(),this.iframe=0,this.last_iframe_time=(new Date).getTime(),this.fps=0,this.start())},start:function(){clearTimeout(this._st),s.log("Start!");var t=this;return this._exp_fps_0=this.exp_fps-.4,this._exp_fps_1=this.exp_fps+.4,this.mode="normal",this.eventManager.clear(),this.lang.mix(this,this.defaultSettings()),this.stage=new s.Stage("stage-main",s.getDefaultStageData("stage_main")),this.canvas.setAttribute("width",this.stage.width),this.canvas.setAttribute("height",this.stage.height),this.canvas.style.width=this.stage.width/_TD.retina+"px",this.canvas.style.height=this.stage.height/_TD.retina+"px",this.canvas.onmousemove=function(i){var e=t.getEventXY.call(t,i);t.hover(e[0],e[1])},this.canvas.onclick=function(i){var e=t.getEventXY.call(t,i);t.click(e[0],e[1])},this.is_paused=!1,this.stage.start(),this.step(),this},checkCheat:function(t){switch(t){case"money+":this.money+=1e6,this.log("cheat success!");break;case"life+":this.life=100,this.log("cheat success!");break;case"life-":this.life=1,this.log("cheat success!");break;case"difficulty+":this.difficulty*=2,this.log("cheat success! difficulty = "+this.difficulty);break;case"difficulty-":this.difficulty/=2,this.log("cheat success! difficulty = "+this.difficulty)}},step:function(){if(this.is_debug&&_TD&&_TD.cheat&&(this.checkCheat(_TD.cheat),_TD.cheat=""),!this.is_paused){if(this.iframe++,this.iframe%50==0){var t=(new Date).getTime(),i=this.step_time;this.fps=Math.round(5e5/(t-this.last_iframe_time))/10,this.last_iframe_time=t,this.fps1?i--:this.fps>this._exp_fps_1&&i++,this.step_time=i}this.iframe%2400==0&&s.gc(),this.stage.step(),this.stage.render();var e=this;this._st=setTimeout(function(){e.step()},this.step_time)}},getEventXY:function(t){var i=s.lang.$e("wrapper"),e=t.clientX-i.offsetLeft-this.canvas.offsetLeft+Math.max(document.documentElement.scrollLeft,document.body.scrollLeft),n=t.clientY-i.offsetTop-this.canvas.offsetTop+Math.max(document.documentElement.scrollTop,document.body.scrollTop);return[e*_TD.retina,n*_TD.retina]},hover:function(t,i){this.eventManager.hover(t,i)},click:function(t,i){this.eventManager.click(t,i)},mouseHand:function(t){this.canvas.style.cursor=t?"pointer":"default"},log:function(t){this.is_debug&&window.console&&console.log&&void 0},gc:function(){window.CollectGarbage&&(CollectGarbage(),setTimeout(CollectGarbage,1))}};for(e=0;this.a[e];e++)this.a[e](s);delete this.a,s.init(t)}};_TD.a.push(function(t){t.lang={$e:function(t){return document.getElementById(t)},$c:function(t,i,e){var s=document.createElement(t);i=i||{};for(var n in i)i.hasOwnProperty(n)&&s.setAttribute(n,i[n]);return e&&e.appendChild(s),s},strLeft:function(t,i){var e=t.slice(0,i),s=e.replace(/[^\x00-\xff]/g,"**").length;if(i>=s)return e;switch(s-=e.length){case 0:return e;case i:return t.slice(0,i>>1);default:var n=i-s,h=t.slice(n,i),a=h.replace(/[\x00-\xff]/g,"").length;return a?t.slice(0,n)+this.arguments.callee(h,a):t.slice(0,n)}},strLen2:function(t){return t.replace(/[^\x00-\xff]/g,"**").length},each:function(t,i){if(Array.prototype.forEach)t.forEach(i);else for(var e=0,s=t.length;s>e;e++)i(t[e])},any:function(t,i){for(var e=0,s=t.length;s>e;e++)if(i(t[e]))return t[e];return null},shift:function(t,i){for(;t[0];)i(t.shift())},rndSort:function(t){var i=t.concat();return i.sort(function(){return Math.random()-.5})},_rndRGB2:function(t){var i=t.toString(16);return 2==i.length?i:"0"+i},rndRGB:function(){var t=Math.floor(256*Math.random()),i=Math.floor(256*Math.random()),e=Math.floor(256*Math.random());return"#"+this._rndRGB2(t)+this._rndRGB2(i)+this._rndRGB2(e)},rgb2Arr:function(t){if(7!=t.length)return[0,0,0];var i=t.substr(1,2),e=t.substr(3,2),s=t.substr(3,2);return[parseInt(i,16),parseInt(e,16),parseInt(s,16)]},rndStr:function(t){t=t||16;var i,e,s="1234567890abcdefghijklmnopqrstuvwxyz",n=[],h=s.length;for(i=0;t>i;i++)e=Math.floor(Math.random()*h),n.push(s.substr(e,1));return n.join("")},nullFunc:function(){},arrayEqual:function(t,i){var e,s=t.length;if(s!=i.length)return!1;for(e=0;s>e;e++)if(t[e]!=i[e])return!1;return!0},mix:function(t,i,e){if(!i||!t)return t;for(var s in i)!i.hasOwnProperty(s)||e===!1&&s in t||(t[s]=i[s]);return t}}}),_TD.a.push(function(t){t.eventManager={ex:-1,ey:-1,_registers:{},ontypes:["enter","hover","out","click"],current_type:"hover",isOn:function(t){return-1!=this.ex&&-1!=this.ey&&this.ex>t.x&&this.ext.y&&this.eya;a++)r.removeEventListener(t,r.ontypes[a])}),this.current_type=""}},hover:function(t,i){"click"!=this.current_type&&(this.current_type="hover",this.ex=t,this.ey=i)},click:function(t,i){this.current_type="click",this.ex=t,this.ey=i}}}),_TD.a.push(function(t){t.Stage=function(i,e){this.id=i||"stage-"+t.lang.rndStr(),this.cfg=e||{},this.width=this.cfg.width||640,this.height=this.cfg.height||540,this.mode="normal",this.state=0,this.acts=[],this.current_act=null,this._step2=t.lang.nullFunc,this._init()},t.Stage.prototype={_init:function(){"function"==typeof this.cfg.init&&this.cfg.init.call(this),"function"==typeof this.cfg.step2&&(this._step2=this.cfg.step2)},start:function(){this.state=1,t.lang.each(this.acts,function(t){t.start()})},pause:function(){this.state=2},gameover:function(){this.current_act.gameover()},clear:function(){this.state=3,t.lang.each(this.acts,function(t){t.clear()})},step:function(){1==this.state&&this.current_act&&(t.eventManager.step(),this.current_act.step(),this._step2())},render:function(){0!=this.state&&3!=this.state&&this.current_act&&this.current_act.render()},addAct:function(t){this.acts.push(t)},addElement:function(t,i,e){this.current_act&&this.current_act.addElement(t,i,e)}}}),_TD.a.push(function(t){t.Act=function(i,e){this.stage=i,this.id=e||"act-"+t.lang.rndStr(),this.state=0,this.scenes=[],this.end_queue=[],this.current_scene=null,this._init()},t.Act.prototype={_init:function(){this.stage.addAct(this)},start:function(){return this.stage.current_act&&3!=this.stage.current_act.state?(this.state=0,void this.stage.current_act.queue(this.start)):(this.state=1,this.stage.current_act=this,void t.lang.each(this.scenes,function(t){t.start()}))},pause:function(){this.state=2},end:function(){this.state=3;for(var t;t=this.end_queue.shift();)t();this.stage.current_act=null},queue:function(t){this.end_queue.push(t)},clear:function(){this.state=3,t.lang.each(this.scenes,function(t){t.clear()})},step:function(){1==this.state&&this.current_scene&&this.current_scene.step()},render:function(){0!=this.state&&3!=this.state&&this.current_scene&&this.current_scene.render()},addScene:function(t){this.scenes.push(t)},addElement:function(t,i,e){this.current_scene&&this.current_scene.addElement(t,i,e)},gameover:function(){this.current_scene.gameover()}}}),_TD.a.push(function(t){t.Scene=function(i,e){this.act=i,this.stage=i.stage,this.is_gameover=!1,this.id=e||"scene-"+t.lang.rndStr(),this.state=0,this.end_queue=[],this._step_elements=[[],[],[]],this._render_elements=[[],[],[],[],[],[],[],[],[],[]],this._init()},t.Scene.prototype={_init:function(){this.act.addScene(this),this.wave=0},start:function(){return this.act.current_scene&&this.act.current_scene!=this&&3!=this.act.current_scene.state?(this.state=0,void this.act.current_scene.queue(this.start)):(this.state=1,void(this.act.current_scene=this))},pause:function(){this.state=2},end:function(){this.state=3;for(var t;t=this.end_queue.shift();)t();this.clear(),this.act.current_scene=null},clear:function(){t.lang.shift(this._step_elements,function(i){t.lang.shift(i,function(t){t.del()})}),t.lang.shift(this._render_elements,function(i){t.lang.shift(i,function(t){t.del()})})},queue:function(t){this.end_queue.push(t)},gameover:function(){this.is_gameover||(this.pause(),this.is_gameover=!0)},step:function(){if(1==this.state){t.life<=0&&(t.life=0,this.gameover());var i,e;for(i=0;3>i;i++){e=[];var s=this._step_elements[i];t.lang.shift(s,function(t){t.is_valid?(t.is_paused||t.step(),e.push(t)):setTimeout(function(){t=null},500)}),this._step_elements[i]=e}}},render:function(){if(0!=this.state&&3!=this.state){var i,e,s=t.ctx;for(s.clearRect(0,0,this.stage.width,this.stage.height),i=0;10>i;i++){e=[];var n=this._render_elements[i];t.lang.shift(n,function(t){t.is_valid&&(t.is_visiable&&t.render(),e.push(t))}),this._render_elements[i]=e}this.is_gameover&&this.panel.gameover_obj.show()}},addElement:function(t,i,e){i=i||t.step_level||1,e=e||t.render_level,this._step_elements[i].push(t),this._render_elements[e].push(t),t.scene=this,t.step_level=i,t.render_level=e}}}),_TD.a.push(function(t){t.Element=function(i,e){this.id=i||"el-"+t.lang.rndStr(),this.cfg=e||{},this.is_valid=!0,this.is_visiable="undefined"!=typeof e.is_visiable?e.is_visiable:!0,this.is_paused=!1,this.is_hover=!1,this.x=this.cfg.x||-1,this.y=this.cfg.y||-1,this.width=this.cfg.width||0,this.height=this.cfg.height||0,this.step_level=e.step_level||1,this.render_level=e.render_level,this.on_events=e.on_events||[],this._init()},t.Element.prototype={_init:function(){var t,i,e,s=this;for(t=0,e=this.on_events.length;e>t;t++)switch(i=this.on_events[t]){case"enter":this.on("enter",function(){s.onEnter()});break;case"out":this.on("out",function(){s.onOut()});break;case"hover":this.on("hover",function(){s.onHover()});break;case"click":this.on("click",function(){s.onClick()})}this.caculatePos()},caculatePos:function(){this.cx=this.x+this.width/2,this.cy=this.y+this.height/2,this.x2=this.x+this.width,this.y2=this.y+this.height},start:function(){this.is_paused=!1},pause:function(){this.is_paused=!0},hide:function(){this.is_visiable=!1,this.onOut()},show:function(){this.is_visiable=!0},del:function(){this.is_valid=!1},on:function(i,e){t.eventManager.on(this,i,e)},onEnter:t.lang.nullFunc,onOut:t.lang.nullFunc,onHover:t.lang.nullFunc,onClick:t.lang.nullFunc,step:t.lang.nullFunc,render:t.lang.nullFunc,addToScene:function(i,e,s,n){this.scene=i,isNaN(e)||(this.step_level=e||this.step_level,this.render_level=s||this.render_level,n&&t.lang.each(n,function(t){i.addElement(t,e,s)}),i.addElement(this,e,s))}}}),_TD.a.push(function(t){function i(i,e){var s=new t.Element(i,e);return t.lang.mix(s,h),s._init(e),s}var e=20,s={_init:function(s){s=s||{},this.grid_x=s.grid_x||10,this.grid_y=s.grid_y||10,this.x=s.x||0,this.y=s.y||0,this.width=this.grid_x*t.grid_size,this.height=this.grid_y*t.grid_size,this.x2=this.x+this.width,this.y2=this.y+this.width,this.grids=[],this.entrance=this.exit=null,this.buildings=[],this.monsters=[],this.bullets=[],this.scene=s.scene,this.is_main_map=!!s.is_main_map,this.select_hl=t.MapSelectHighLight(this.id+"-hl",{map:this}),this.select_hl.addToScene(this.scene,1,9),this.selected_building=null,this._wait_clearInvalidElements=e,this._wait_add_monsters=0,this._wait_add_monsters_arr=[],this.is_main_map&&(this.mmm=new i(this.id+"-mmm",{map:this}),this.mmm.addToScene(this.scene,1,7));var n,h,a,l=this.grid_x*this.grid_y,r=s.grid_data||[];for(n=0;l>n;n++)h=r[n]||{},h.mx=n%this.grid_x,h.my=Math.floor(n/this.grid_x),h.map=this,h.step_level=this.step_level,h.render_level=this.render_level,a=new t.Grid(this.id+"-grid-"+h.mx+"-"+h.my,h),this.grids.push(a);s.entrance&&s.exit&&!t.lang.arrayEqual(s.entrance,s.exit)&&(this.entrance=this.getGrid(s.entrance[0],s.entrance[1]),this.entrance.is_entrance=!0,this.exit=this.getGrid(s.exit[0],s.exit[1]),this.exit.is_exit=!0);var c=this;s.grids_cfg&&t.lang.each(s.grids_cfg,function(t){var i=c.getGrid(t.pos[0],t.pos[1]);i&&(isNaN(t.passable_flag)||(i.passable_flag=t.passable_flag),isNaN(t.build_flag)||(i.build_flag=t.build_flag),t.building&&i.addBuilding(t.building))})},checkHasWeapon:function(){this.has_weapon=null!=this.anyBuilding(function(t){return t.is_weapon})},getGrid:function(t,i){var e=i*this.grid_x+t;return this.grids[e]},anyMonster:function(i){return t.lang.any(this.monsters,i)},anyBuilding:function(i){return t.lang.any(this.buildings,i)},anyBullet:function(i){return t.lang.any(this.bullets,i)},eachBuilding:function(i){t.lang.each(this.buildings,i)},eachMonster:function(i){t.lang.each(this.monsters,i)},eachBullet:function(i){t.lang.each(this.bullets,i)},preBuild:function(i){t.mode="build",this.pre_building&&this.pre_building.remove(),this.pre_building=new t.Building(this.id+"-pre-building-"+t.lang.rndStr(),{type:i,map:this,is_pre_building:!0}),this.scene.addElement(this.pre_building,1,this.render_level+1)},cancelPreBuild:function(){t.mode="normal",this.pre_building&&this.pre_building.remove()},clearInvalidElements:function(){if(this._wait_clearInvalidElements>0)return void this._wait_clearInvalidElements--;this._wait_clearInvalidElements=e;var i=[];t.lang.shift(this.buildings,function(t){t.is_valid&&i.push(t)}),this.buildings=i,i=[],t.lang.shift(this.monsters,function(t){t.is_valid&&i.push(t)}),this.monsters=i,i=[],t.lang.shift(this.bullets,function(t){t.is_valid&&i.push(t)}),this.bullets=i},addMonster:function(i){this.entrance&&("number"==typeof i&&(i=new t.Monster(null,{idx:i,difficulty:t.difficulty,step_level:this.step_level,render_level:this.render_level+2})),this.entrance.addMonster(i))},addMonsters:function(t,i){this._wait_add_monsters=t,this._wait_add_monsters_objidx=i},addMonsters2:function(t){this._wait_add_monsters_arr=t},checkPassable:function(t,i){var e=this.getGrid(t,i);return null!=e&&1==e.passable_flag&&2!=e.build_flag},step:function(){if(this.clearInvalidElements(),this._wait_add_monsters>0)this.addMonster(this._wait_add_monsters_objidx),this._wait_add_monsters--;else if(this._wait_add_monsters_arr.length>0){var t=this._wait_add_monsters_arr.shift();this.addMonsters(t[0],t[1])}},render:function(){var i=t.ctx;i.strokeStyle="#99a",i.lineWidth=_TD.retina,i.beginPath(),i.strokeRect(this.x+.5,this.y+.5,this.width,this.height),i.closePath(),i.stroke()},onOut:function(){this.is_main_map&&this.pre_building&&this.pre_building.hide()}};t.Map=function(i,e){e.on_events=["enter","out"];var n=new t.Element(i,e);return t.lang.mix(n,s),n._init(e),n};var n={_init:function(i){this.map=i.map,this.width=t.grid_size+2,this.height=t.grid_size+2,this.is_visiable=!1},show:function(t){this.x=t.x,this.y=t.y,this.is_visiable=!0},render:function(){var i=t.ctx;i.lineWidth=2,i.strokeStyle="#f93",i.beginPath(),i.strokeRect(this.x,this.y,this.width-1,this.height-1),i.closePath(),i.stroke()}};t.MapSelectHighLight=function(i,e){var s=new t.Element(i,e);return t.lang.mix(s,n),s._init(e),s};var h={_init:function(t){this.map=t.map,this.x1=this.map.x,this.y1=this.map.y,this.x2=this.map.x2+1,this.y2=this.map.y2+1,this.w=this.map.scene.stage.width,this.h=this.map.scene.stage.height,this.w2=this.w-this.x2,this.h2=this.h-this.y2},render:function(){var i=t.ctx;i.fillStyle="#fff",i.beginPath(),i.fillRect(0,0,this.x1,this.h),i.fillRect(0,0,this.w,this.y1),i.fillRect(0,this.y2,this.w,this.h2),i.fillRect(this.x2,0,this.w2,this.h),i.closePath(),i.fill()}}}),_TD.a.push(function(t){var i={_init:function(i){i=i||{},this.map=i.map,this.scene=this.map.scene,this.mx=i.mx,this.my=i.my,this.width=t.grid_size,this.height=t.grid_size,this.is_entrance=this.is_exit=!1,this.passable_flag=1,this.build_flag=1,this.building=null,this.caculatePos()},caculatePos:function(){this.x=this.map.x+this.mx*t.grid_size,this.y=this.map.y+this.my*t.grid_size,this.x2=this.x+t.grid_size,this.y2=this.y+t.grid_size,this.cx=Math.floor(this.x+t.grid_size/2),this.cy=Math.floor(this.y+t.grid_size/2)},checkBlock:function(){if(this.is_entrance||this.is_exit)return this._block_msg=t._t("entrance_or_exit_be_blocked"),!0;var i,e=this,s=new t.FindWay(this.map.grid_x,this.map.grid_y,this.map.entrance.mx,this.map.entrance.my,this.map.exit.mx,this.map.exit.my,function(t,i){return!(t==e.mx&&i==e.my)&&e.map.checkPassable(t,i)});return i=s.is_blocked,i?this._block_msg=t._t("blocked"):(i=!!this.map.anyMonster(function(t){return t.chkIfBlocked(e.mx,e.my)}),i&&(this._block_msg=t._t("monster_be_blocked"))),i},buyBuilding:function(i){var e=t.getDefaultBuildingAttributes(i).cost||0;t.money>=e?(t.money-=e,this.addBuilding(i)):(t.log(t._t("not_enough_money",[e])),this.scene.panel.balloontip.msg(t._t("not_enough_money",[e]),this))},addBuilding:function(i){this.building&&this.removeBuilding();var e=new t.Building("building-"+i+"-"+t.lang.rndStr(),{type:i,step_level:this.step_level,render_level:this.render_level});e.locate(this),this.scene.addElement(e,this.step_level,this.render_level+1),this.map.buildings.push(e),this.building=e,this.build_flag=2,this.map.checkHasWeapon(),this.map.pre_building&&this.map.pre_building.hide()},removeBuilding:function(){2==this.build_flag&&(this.build_flag=1),this.building&&this.building.remove(),this.building=null},addMonster:function(t){t.beAddToGrid(this),this.map.monsters.push(t),t.start()},hightLight:function(t){this.map.select_hl[t?"show":"hide"](this)},render:function(){var i=t.ctx,e=this.x+.5,s=this.y+.5;this.is_hover&&(i.fillStyle="rgba(255, 255, 200, 0.2)",i.beginPath(),i.fillRect(e,s,this.width,this.height),i.closePath(),i.fill()),0==this.passable_flag&&(i.fillStyle="#fcc",i.beginPath(),i.fillRect(e,s,this.width,this.height),i.closePath(),i.fill()),(this.is_entrance||this.is_exit)&&(i.lineWidth=1,i.fillStyle="#ccc",i.beginPath(),i.fillRect(e,s,this.width,this.height),i.closePath(),i.fill(),i.strokeStyle="#666",i.fillStyle=this.is_entrance?"#fff":"#666",i.beginPath(),i.arc(this.cx,this.cy,.325*t.grid_size,0,2*Math.PI,!0),i.closePath(),i.fill(),i.stroke()),i.strokeStyle="#eee",i.lineWidth=1,i.beginPath(),i.strokeRect(e,s,this.width,this.height),i.closePath(),i.stroke()},onEnter:function(){if(this.map.is_main_map&&"build"==t.mode)1==this.build_flag?(this.map.pre_building.show(),this.map.pre_building.locate(this)):this.map.pre_building.hide();else if(this.map.is_main_map){var i="";this.is_entrance?i=t._t("entrance"):this.is_exit?i=t._t("exit"):0==this.passable_flag?i=t._t("_cant_pass"):0==this.build_flag&&(i=t._t("_cant_build")),i&&this.scene.panel.balloontip.msg(i,this)}},onOut:function(){this.scene.panel.balloontip.el==this&&this.scene.panel.balloontip.hide()},onClick:function(){1==this.scene.state&&("build"==t.mode&&this.map.is_main_map&&!this.building?this.checkBlock()?this.scene.panel.balloontip.msg(this._block_msg,this):this.buyBuilding(this.map.pre_building.type):!this.building&&this.map.selected_building&&(this.map.selected_building.toggleSelected(),this.map.selected_building=null))}};t.Grid=function(e,s){s.on_events=["enter","out","click"];var n=new t.Element(e,s);return t.lang.mix(n,i),n._init(s),n}}),_TD.a.push(function(t){var i={_init:function(i){this.is_selected=!1,this.level=0,this.killed=0,this.target=null,i=i||{},this.map=i.map||null,this.grid=i.grid||null,this.bullet_type=i.bullet_type||1,this.type=i.type,this.speed=i.speed,this.bullet_speed=i.bullet_speed,this.is_pre_building=!!i.is_pre_building,this.blink=this.is_pre_building,this.wait_blink=this._default_wait_blink=20,this.is_weapon="wall"!=this.type;var e=t.getDefaultBuildingAttributes(this.type);t.lang.mix(this,e),this.range_px=this.range*t.grid_size,this.money=this.cost,this.caculatePos()},getUpgradeCost:function(){return Math.floor(.75*this.money)},getSellMoney:function(){return Math.floor(.5*this.money)||1},toggleSelected:function(){this.is_selected=!this.is_selected,this.grid.hightLight(this.is_selected);var t=this;this.is_selected?(this.map.eachBuilding(function(i){i.is_selected=i==t}),(this.map.is_main_map?this.scene.panel_map:this.scene.map).eachBuilding(function(t){t.is_selected=!1,t.grid.hightLight(!1)}),this.map.selected_building=this,this.map.is_main_map?this.scene.map.cancelPreBuild():this.scene.map.preBuild(this.type)):(this.map.selected_building==this&&(this.map.selected_building=null),this.map.is_main_map||this.scene.map.cancelPreBuild()),this.map.is_main_map&&(this.map.selected_building?(this.scene.panel.btn_upgrade.show(),this.scene.panel.btn_sell.show(),this.updateBtnDesc()):(this.scene.panel.btn_upgrade.hide(),this.scene.panel.btn_sell.hide()))},updateBtnDesc:function(){this.scene.panel.btn_upgrade.desc=t._t("upgrade",[t._t("building_name_"+this.type),this.level+1,this.getUpgradeCost()]),this.scene.panel.btn_sell.desc=t._t("sell",[t._t("building_name_"+this.type),this.getSellMoney()])},locate:function(i){this.grid=i,this.map=i.map,this.cx=this.grid.cx,this.cy=this.grid.cy,this.x=this.grid.x,this.y=this.grid.y,this.x2=this.grid.x2,this.y2=this.grid.y2,this.width=this.grid.width,this.height=this.grid.height,this.px=this.x+.5,this.py=this.y+.5,this.wait_blink=this._default_wait_blink,this._fire_wait=Math.floor(Math.max(2/(this.speed*t.global_speed),1)),this._fire_wait2=this._fire_wait},remove:function(){this.grid&&this.grid.building&&this.grid.building==this&&(this.grid.building=null),this.hide(),this.del()},findTaget:function(){if(this.is_weapon&&!this.is_pre_building&&this.grid){var i=this.cx,e=this.cy,s=Math.pow(this.range_px,2);this.target&&this.target.is_valid&&Math.pow(this.target.cx-i,2)+Math.pow(this.target.cy-e,2)<=s||(this.target=t.lang.any(t.lang.rndSort(this.map.monsters),function(t){return Math.pow(t.cx-i,2)+Math.pow(t.cy-e,2)<=s}))}},getTargetPosition:function(){if(!this.target){var t=this.map.is_main_map?this.map.entrance:this.grid;return[t.cx,t.cy]}return[this.target.cx,this.target.cy]},fire:function(){if(this.target&&this.target.is_valid){if("laser_gun"==this.type)return void this.target.beHit(this,this.damage);var i=this.muzzle||[this.cx,this.cy],e=i[0],s=i[1];new t.Bullet(null,{building:this,damage:this.damage,target:this.target,speed:this.bullet_speed,x:e,y:s})}},tryToFire:function(){this.is_weapon&&this.target&&(this._fire_wait--,this._fire_wait>0||(this._fire_wait<0?this._fire_wait=this._fire_wait2:this.fire()))},_upgrade2:function(i){this._upgrade_records[i]||(this._upgrade_records[i]=this[i]);var e=this._upgrade_records[i],s="max_"+i,n="_upgrade_rule_"+i,h=this[n]||t.default_upgrade_rule;e&&!isNaN(e)&&(e=h(this.level,e),this[s]&&!isNaN(this[s])&&this[s]i;i++)this._upgrade2(e[i]);this.level++,this.range_px=this.range*t.grid_size},tryToUpgrade:function(i){var e=this.getUpgradeCost(),s="";e>t.money?s=t._t("not_enough_money",[e]):(t.money-=e,this.money+=e,this.upgrade(),s=t._t("upgrade_success",[t._t("building_name_"+this.type),this.level,this.getUpgradeCost()])),this.updateBtnDesc(),this.scene.panel.balloontip.msg(s,i)},tryToSell:function(){this.is_valid&&(t.money+=this.getSellMoney(),this.grid.removeBuilding(),this.is_valid=!1,this.map.selected_building=null,this.map.select_hl.hide(),this.map.checkHasWeapon(),this.scene.panel.btn_upgrade.hide(),this.scene.panel.btn_sell.hide(),this.scene.panel.balloontip.hide())},step:function(){this.blink&&(this.wait_blink--,this.wait_blink<-this._default_wait_blink&&(this.wait_blink=this._default_wait_blink)),this.findTaget(),this.tryToFire()},render:function(){if(this.is_visiable&&!(this.wait_blink<0)){var i=t.ctx;t.renderBuilding(this),this.map.is_main_map&&(this.is_selected||this.is_pre_building||this.map.show_all_ranges)&&this.is_weapon&&this.range>0&&this.grid&&(i.lineWidth=_TD.retina,i.fillStyle="rgba(187, 141, 32, 0.15)",i.strokeStyle="#bb8d20",i.beginPath(),i.arc(this.cx,this.cy,this.range_px,0,2*Math.PI,!0),i.closePath(),i.fill(),i.stroke()),"laser_gun"==this.type&&this.target&&this.target.is_valid&&(i.lineWidth=3*_TD.retina,i.strokeStyle="rgba(50, 50, 200, 0.5)",i.beginPath(),i.moveTo(this.cx,this.cy),i.lineTo(this.target.cx,this.target.cy),i.closePath(),i.stroke(),i.lineWidth=_TD.retina,i.strokeStyle="rgba(150, 150, 255, 0.5)",i.beginPath(),i.lineTo(this.cx,this.cy),i.closePath(),i.stroke())}},onEnter:function(){if(!this.is_pre_building){var i="建筑工事";i=this.map.is_main_map?t._t("building_info"+("wall"==this.type?"_wall":""),[t._t("building_name_"+this.type),this.level,this.damage,this.speed,this.range,this.killed]):t._t("building_intro_"+this.type,[t.getDefaultBuildingAttributes(this.type).cost]),this.scene.panel.balloontip.msg(i,this.grid)}},onOut:function(){this.scene.panel.balloontip.el==this.grid&&this.scene.panel.balloontip.hide()},onClick:function(){this.is_pre_building||1!=this.scene.state||this.toggleSelected()}};t.Building=function(e,s){s.on_events=["enter","out","click"];var n=new t.Element(e,s);return t.lang.mix(n,i),n._init(s),n};var e={_init:function(t){t=t||{},this.speed=t.speed,this.damage=t.damage,this.target=t.target,this.cx=t.x,this.cy=t.y,this.r=t.r||Math.max(Math.log(this.damage),2),this.r<1&&(this.r=1),this.r>6&&(this.r=6),this.building=t.building||null,this.map=t.map||this.building.map,this.type=t.type||1,this.color=t.color||"#000",this.map.bullets.push(this),this.addToScene(this.map.scene,1,6),1==this.type&&this.caculate()},caculate:function(){var i,e,s,n,h=this.target.cx,a=this.target.cy;i=h-this.cx,e=a-this.cy,s=Math.sqrt(Math.pow(i,2)+Math.pow(e,2)),n=20*this.speed*t.global_speed,this.vx=i*n/s,this.vy=e*n/s},checkOutOfMap:function(){return this.is_valid=!(this.cxthis.map.x2||this.cythis.map.y2),!this.is_valid},checkHit:function(){var i=this.cx,e=this.cy,s=this.r*_TD.retina,n=this.map.anyMonster(function(t){return Math.pow(t.cx-i,2)+Math.pow(t.cy-e,2)<=2*Math.pow(t.r+s,2)});return n?(n.beHit(this.building,this.damage),this.is_valid=!1,t.Explode(this.id+"-explode",{cx:this.cx,cy:this.cy,r:this.r,step_level:this.step_level,render_level:this.render_level,color:this.color,scene:this.map.scene,time:.2}),!0):!1},step:function(){this.checkOutOfMap()||this.checkHit()||(this.cx+=this.vx,this.cy+=this.vy)},render:function(){var i=t.ctx;i.fillStyle=this.color,i.beginPath(),i.arc(this.cx,this.cy,this.r,0,2*Math.PI,!0),i.closePath(),i.fill()}};t.Bullet=function(i,s){var n=new t.Element(i,s);return t.lang.mix(n,e),n._init(s),n}}),_TD.a.push(function(t){var i={_init:function(i){i=i||{},this.is_monster=!0,this.idx=i.idx||1,this.difficulty=i.difficulty||1;var e=t.getDefaultMonsterAttributes(this.idx);this.speed=Math.floor((e.speed+this.difficulty/2)*(.5*Math.random()+.75)),this.speed<1&&(this.speed=1),this.speed>i.max_speed&&(this.speed=i.max_speed),this.life=this.life0=Math.floor(e.life*(this.difficulty+1)*(Math.random()+.5)*.5),this.life<1&&(this.life=this.life0=1),this.shield=Math.floor(e.shield+this.difficulty/2),this.shield<0&&(this.shield=0),this.damage=Math.floor((e.damage||1)*(.5*Math.random()+.75)),this.damage<1&&(this.damage=1),this.money=e.money||Math.floor(Math.sqrt((this.speed+this.life)*(this.shield+1)*this.damage)),this.money<1&&(this.money=1),this.color=e.color||t.lang.rndRGB(),this.r=Math.floor(1.2*this.damage)*_TD.retina,this.r<4*_TD.retina&&(this.r=4*_TD.retina),this.r>t.grid_size/2-4*_TD.retina&&(this.r=t.grid_size/2-4*_TD.retina),this.render=e.render,this.grid=null,this.map=null,this.next_grid=null,this.way=[],this.toward=2,this._dx=0,this._dy=0,this.is_blocked=!1},caculatePos:function(){var t=this.r;this.x=this.cx-t,this.y=this.cy-t,this.x2=this.cx+t,this.y2=this.cy+t},beHit:function(i,e){if(this.is_valid){var s=Math.ceil(.1*e);e-=this.shield,s>=e&&(e=s),this.life-=e,t.score+=Math.floor(Math.sqrt(e)),this.life<=0&&this.beKilled(i);var n=this.scene.panel.balloontip;n.el==this&&(n.text=t._t("monster_info",[this.life,this.shield,this.speed,this.damage]))}},beKilled:function(i){this.is_valid&&(this.life=0,this.is_valid=!1,t.money+=this.money,i.killed++,t.Explode(this.id+"-explode",{cx:this.cx,cy:this.cy,color:this.color,r:this.r,step_level:this.step_level,render_level:this.render_level,scene:this.grid.scene}))},arrive:function(){this.grid=this.next_grid,this.next_grid=null,this.checkFinish()},findWay:function(){var i=this,e=new t.FindWay(this.map.grid_x,this.map.grid_y,this.grid.mx,this.grid.my,this.map.exit.mx,this.map.exit.my,function(t,e){return i.map.checkPassable(t,e)});this.way=e.way},checkFinish:function(){this.grid&&this.map&&this.grid==this.map.exit&&(t.life-=this.damage,t.wave_damage+=this.damage,t.life<=0?(t.life=0,t.stage.gameover()):(this.pause(),this.del()))},beAddToGrid:function(t){this.grid=t,this.map=t.map,this.cx=t.cx,this.cy=t.cy,this.grid.scene.addElement(this)},getToward:function(){this.grid&&this.next_grid&&(this.grid.mythis.next_grid.my?this.toward=2:this.grid.mx>this.next_grid.mx&&(this.toward=3))},getNextGrid:function(){(0==this.way.length||Math.random()<.1)&&this.findWay();var t=this.way.shift();t&&!this.map.checkPassable(t[0],t[1])&&(this.findWay(),t=this.way.shift()),t&&(this.next_grid=this.map.getGrid(t[0],t[1]))},chkIfBlocked:function(i,e){var s=this,n=new t.FindWay(this.map.grid_x,this.map.grid_y,this.grid.mx,this.grid.my,this.map.exit.mx,this.map.exit.my,function(t,n){return!(t==i&&n==e)&&s.map.checkPassable(t,n)});return n.is_blocked},beBlocked:function(){this.is_blocked||(this.is_blocked=!0,t.log("monster be blocked!"))},step:function(){if(this.is_valid&&!this.is_paused&&this.grid){if(!this.next_grid&&(this.getNextGrid(),!this.next_grid))return void this.beBlocked();if(this.cx==this.next_grid.cx&&this.cy==this.next_grid.cy)this.arrive();else{var i=this.next_grid.cx-this.cx,e=this.next_grid.cy-this.cy,s=0>i?-1:1,n=0>e?-1:1,h=this.speed*t.global_speed;Math.abs(i)0,this.rgb_a=this.wait/this.wait0)},render:function(){var i=t.ctx;i.fillStyle="rgba("+this.rgb_r+","+this.rgb_g+","+this.rgb_b+","+this.rgb_a+")",i.beginPath(),i.arc(this.cx,this.cy,this.r,0,2*Math.PI,!0),i.closePath(),i.fill()}};t.Explode=function(i,s){var n=new t.Element(i,s);return t.lang.mix(n,e),n._init(s),n}}),_TD.a.push(function(t){var i={_init:function(i){i=i||{},this.x=i.x,this.y=i.y,this.scene=i.scene,this.map=i.main_map;var e=new t.Map("panel-map",t.lang.mix({x:this.x+i.map.x,y:this.y+i.map.y,scene:this.scene, 2 | step_level:this.step_level,render_level:this.render_level},i.map,!1));this.addToScene(this.scene,1,7),e.addToScene(this.scene,1,7,e.grids),this.scene.panel_map=e,this.gameover_obj=new t.GameOver("panel-gameover",{panel:this,scene:this.scene,step_level:this.step_level,is_visiable:!1,x:0,y:0,width:this.scene.stage.width,height:this.scene.stage.height,render_level:9}),this.balloontip=new t.BalloonTip("panel-balloon-tip",{scene:this.scene,step_level:this.step_level,render_level:9}),this.balloontip.addToScene(this.scene,1,9),this.btn_pause=new t.Button("panel-btn-pause",{scene:this.scene,x:this.x,y:this.y+260*_TD.retina,text:t._t("button_pause_text"),step_level:this.step_level,render_level:this.render_level+1,onClick:function(){1==this.scene.state?(this.scene.pause(),this.text=t._t("button_continue_text"),this.scene.panel.btn_upgrade.hide(),this.scene.panel.btn_sell.hide(),this.scene.panel.btn_restart.show()):2==this.scene.state&&(this.scene.start(),this.text=t._t("button_pause_text"),this.scene.panel.btn_restart.hide(),this.scene.map.selected_building&&(this.scene.panel.btn_upgrade.show(),this.scene.panel.btn_sell.show()))}}),this.btn_restart=new t.Button("panel-btn-restart",{scene:this.scene,x:this.x,y:this.y+300*_TD.retina,is_visiable:!1,text:t._t("button_restart_text"),step_level:this.step_level,render_level:this.render_level+1,onClick:function(){setTimeout(function(){t.stage.clear(),t.is_paused=!0,t.start(),t.mouseHand(!1)},0)}}),this.btn_upgrade=new t.Button("panel-btn-upgrade",{scene:this.scene,x:this.x,y:this.y+300*_TD.retina,is_visiable:!1,text:t._t("button_upgrade_text"),step_level:this.step_level,render_level:this.render_level+1,onClick:function(){this.scene.map.selected_building.tryToUpgrade(this)}}),this.btn_sell=new t.Button("panel-btn-sell",{scene:this.scene,x:this.x,y:this.y+340*_TD.retina,is_visiable:!1,text:t._t("button_sell_text"),step_level:this.step_level,render_level:this.render_level+1,onClick:function(){this.scene.map.selected_building.tryToSell(this)}})},step:function(){t.life_recover&&(this._life_recover=this._life_recover2=t.life_recover,this._life_recover_wait=this._life_recover_wait2=3*t.exp_fps,t.life_recover=0),this._life_recover&&t.iframe%t.exp_fps_eighth==0&&(t.life++,this._life_recover--)},render:function(){var i=t.ctx;if(i.textAlign="left",i.textBaseline="top",i.fillStyle="#000",i.font="normal "+12*_TD.retina+"px 'Courier New'",i.beginPath(),i.fillText(t._t("panel_money_title")+t.money,this.x,this.y),i.fillText(t._t("panel_score_title")+t.score,this.x,this.y+20*_TD.retina),i.fillText(t._t("panel_life_title")+t.life,this.x,this.y+40*_TD.retina),i.fillText(t._t("panel_building_title")+this.map.buildings.length,this.x,this.y+60*_TD.retina),i.fillText(t._t("panel_monster_title")+this.map.monsters.length,this.x,this.y+80*_TD.retina),i.fillText(t._t("wave_info",[this.scene.wave]),this.x,this.y+210*_TD.retina),i.closePath(),this._life_recover_wait){var e=this._life_recover_wait/this._life_recover_wait2;i.fillStyle="rgba(255, 0, 0, "+e+")",i.font="bold "+12*_TD.retina+"px 'Verdana'",i.beginPath(),i.fillText("+"+this._life_recover2,this.x+60*_TD.retina,this.y+40*_TD.retina),i.closePath(),this._life_recover_wait--}i.textAlign="right",i.fillStyle="#666",i.font="normal "+12*_TD.retina+"px 'Courier New'",i.beginPath(),i.fillText("version: "+t.version+" | oldj.net",t.stage.width-t.padding,t.stage.height-2*t.padding),i.closePath(),i.textAlign="left",i.fillStyle="#666",i.font="normal "+12*_TD.retina+"px 'Courier New'",i.beginPath(),i.fillText("FPS: "+t.fps,t.padding,t.stage.height-2*t.padding),i.closePath()}};t.Panel=function(e,s){var n=new t.Element(e,s);return t.lang.mix(n,i),n._init(s),n};var e={_init:function(t){t=t||{},this.scene=t.scene},caculatePos:function(){var i=this.el;this.x=i.cx+.5,this.y=i.cy+.5,this.x+this.width>this.scene.stage.width-t.padding&&(this.x=this.x-this.width),this.px=this.x+5*_TD.retina,this.py=this.y+4*_TD.retina},msg:function(i,e){this.text=i;var s=t.ctx;s.font="normal "+12*_TD.retina+"px 'Courier New'",this.width=Math.max(s.measureText(i).width+10*_TD.retina,6*t.lang.strLen2(i)+10*_TD.retina),this.height=20*_TD.retina,e&&e.cx&&e.cy&&(this.el=e,this.caculatePos(),this.show())},step:function(){return this.el&&this.el.is_valid?void(this.el.is_monster&&this.caculatePos()):void this.hide()},render:function(){if(this.el){var i=t.ctx;i.lineWidth=_TD.retina,i.fillStyle="rgba(255, 255, 0, 0.5)",i.strokeStyle="rgba(222, 222, 0, 0.9)",i.beginPath(),i.rect(this.x,this.y,this.width,this.height),i.closePath(),i.fill(),i.stroke(),i.textAlign="left",i.textBaseline="top",i.fillStyle="#000",i.font="normal "+12*_TD.retina+"px 'Courier New'",i.beginPath(),i.fillText(this.text,this.px,this.py),i.closePath()}}};t.BalloonTip=function(i,s){var n=new t.Element(i,s);return t.lang.mix(n,e),n._init(s),n};var s={_init:function(i){i=i||{},this.text=i.text,this.onClick=i.onClick||t.lang.nullFunc,this.x=i.x,this.y=i.y,this.width=i.width||80*_TD.retina,this.height=i.height||30*_TD.retina,this.font_x=this.x+8*_TD.retina,this.font_y=this.y+9*_TD.retina,this.scene=i.scene,this.desc=i.desc||"",this.addToScene(this.scene,this.step_level,this.render_level),this.caculatePos()},onEnter:function(){t.mouseHand(!0),this.desc&&this.scene.panel.balloontip.msg(this.desc,this)},onOut:function(){t.mouseHand(!1),this.scene.panel.balloontip.el==this&&this.scene.panel.balloontip.hide()},render:function(){var i=t.ctx;i.lineWidth=2*_TD.retina,i.fillStyle=this.is_hover?"#eee":"#ccc",i.strokeStyle="#999",i.beginPath(),i.rect(this.x,this.y,this.width,this.height),i.closePath(),i.fill(),i.stroke(),i.textAlign="left",i.textBaseline="top",i.fillStyle="#000",i.font="normal "+12*_TD.retina+"px 'Courier New'",i.beginPath(),i.fillText(this.text,this.font_x,this.font_y),i.closePath(),i.fill()}};t.Button=function(i,e){e.on_events=["enter","out","click"];var n=new t.Element(i,e);return t.lang.mix(n,s),n._init(e),n};var n={_init:function(t){this.panel=t.panel,this.scene=t.scene,this.addToScene(this.scene,1,9)},render:function(){this.panel.btn_pause.hide(),this.panel.btn_upgrade.hide(),this.panel.btn_sell.hide(),this.panel.btn_restart.show();var i=t.ctx;i.textAlign="center",i.textBaseline="middle",i.fillStyle="#ccc",i.font="bold 62px 'Verdana'",i.beginPath(),i.fillText("GAME OVER",this.width/2,this.height/2),i.closePath(),i.fillStyle="#f00",i.font="bold 60px 'Verdana'",i.beginPath(),i.fillText("GAME OVER",this.width/2,this.height/2),i.closePath()}};t.GameOver=function(i,e){var s=new t.Element(i,e);return t.lang.mix(s,n),s._init(e),s},t.recover=function(i){t.life_recover=i,t.log("life recover: "+i)}}),_TD.a.push(function(t){var i=function(){var i=new t.Act(this,"act-1"),e=new t.Scene(i,"scene-1"),s=t.getDefaultStageData("scene_endless");this.config=s.config,t.life=this.config.life,t.money=this.config.money,t.score=this.config.score,t.difficulty=this.config.difficulty,t.wave_damage=this.config.wave_damage;var n=new t.Map("main-map",t.lang.mix({scene:e,is_main_map:!0,step_level:1,render_level:2},s.map));n.addToScene(e,1,2,n.grids),e.map=n,e.panel=new t.Panel("panel",t.lang.mix({scene:e,main_map:n,step_level:1,render_level:7},s.panel)),this.newWave=s.newWave,this.map=n,this.wait_new_wave=this.config.wait_new_wave},e=function(){var i=this.current_act.current_scene,e=i.wave;if((0!=e||this.map.has_weapon)&&1==i.state&&0==this.map.monsters.length){if(e>0&&this.wait_new_wave==this.config.wait_new_wave-1){var s=0;e%10==0?s=10:e%5==0&&(s=5),t.life+s>100&&(s=100-t.life),s>0&&t.recover(s)}if(this.wait_new_wave>0)return void this.wait_new_wave--;this.wait_new_wave=this.config.wait_new_wave,e++,i.wave=e,this.newWave({map:this.map,wave:e})}};t.getDefaultStageData=function(s){var n={stage_main:{width:640*_TD.retina,height:560*_TD.retina,init:i,step2:e},scene_endless:{map:{grid_x:16,grid_y:16,x:t.padding,y:t.padding,entrance:[0,0],exit:[15,15],grids_cfg:[{pos:[3,3],passable_flag:0},{pos:[7,15],build_flag:0},{pos:[4,12],building:"wall"},{pos:[4,13],building:"wall"}]},panel:{x:2*t.padding+16*t.grid_size,y:t.padding,map:{grid_x:3,grid_y:3,x:0,y:110*_TD.retina,grids_cfg:[{pos:[0,0],building:"cannon"},{pos:[1,0],building:"LMG"},{pos:[2,0],building:"HMG"},{pos:[0,1],building:"laser_gun"},{pos:[2,2],building:"wall"}]}},config:{endless:!0,wait_new_wave:3*t.exp_fps,difficulty:1,wave:0,max_wave:-1,wave_damage:0,max_monsters_per_wave:100,money:500,score:0,life:100,waves:[[],[[1,0]],[[1,0],[1,1]],[[2,0],[1,1]],[[2,0],[1,1]],[[3,0],[2,1]],[[4,0],[2,1]],[[5,0],[3,1],[1,2]],[[6,0],[4,1],[1,2]],[[7,0],[3,1],[2,2]],[[8,0],[4,1],[3,2]]]},newWave:function(i){i=i||{};var e=i.map,s=i.wave||1,n=t.wave_damage||0;1==s||(0==n?5>s?t.difficulty*=1.05:t.difficulty>30?t.difficulty*=1.1:t.difficulty*=1.2:t.wave_damage>=50?t.difficulty*=.6:t.wave_damage>=30?t.difficulty*=.7:t.wave_damage>=20?t.difficulty*=.8:t.wave_damage>=10?t.difficulty*=.9:s>=10&&(t.difficulty*=1.05)),t.difficulty<1&&(t.difficulty=1),t.log("wave "+s+", last wave damage = "+n+", difficulty = "+t.difficulty);var h=this.config.waves[s]||t.makeMonsters(Math.min(Math.floor(Math.pow(s,1.1)),this.config.max_monsters_per_wave));e.addMonsters2(h),t.wave_damage=0}}};return n[s]||{}}}),_TD.a.push(function(t){t.default_upgrade_rule=function(t,i){return 1.2*i},t.getDefaultBuildingAttributes=function(t){var i={wall:{damage:0,range:0,speed:0,bullet_speed:0,life:100,shield:500,cost:5},cannon:{damage:12,range:4,max_range:8,speed:2,bullet_speed:6,life:100,shield:100,cost:300,_upgrade_rule_damage:function(t,i){return i*(10>=t?1.2:1.3)}},LMG:{damage:5,range:5,max_range:10,speed:3,bullet_speed:6,life:100,shield:50,cost:100},HMG:{damage:30,range:3,max_range:5,speed:3,bullet_speed:5,life:100,shield:200,cost:800,_upgrade_rule_damage:function(t,i){return 1.3*i}},laser_gun:{damage:25,range:6,max_range:10,speed:20,life:100,shield:100,cost:2e3}};return i[t]||{}}}),_TD.a.push(function(t){function i(){if(this.is_valid&&this.grid){var i=t.ctx;if(i.strokeStyle="#000",i.lineWidth=1,i.fillStyle=this.color,i.beginPath(),i.arc(this.cx,this.cy,this.r,0,2*Math.PI,!0),i.closePath(),i.fill(),i.stroke(),t.show_monster_life){var e=Math.floor(t.grid_size/4),s=2*e-2*_TD.retina;i.fillStyle="#000",i.beginPath(),i.fillRect(this.cx-e,this.cy-this.r-6,2*e,4*_TD.retina),i.closePath(),i.fillStyle="#f00",i.beginPath(),i.fillRect(this.cx-e+_TD.retina,this.cy-this.r-(6-_TD.retina),this.life*s/this.life0,2*_TD.retina),i.closePath()}}}t.getDefaultMonsterAttributes=function(e){var s=[{name:"monster 1",desc:"最弱小的怪物",speed:3,max_speed:10,life:50,damage:1,shield:0,money:5},{name:"monster 2",desc:"稍强一些的小怪",speed:6,max_speed:20,life:50,damage:2,shield:1},{name:"monster speed",desc:"速度较快的小怪",speed:12,max_speed:30,life:50,damage:3,shield:1},{name:"monster life",desc:"生命值很强的小怪",speed:5,max_speed:10,life:500,damage:3,shield:1},{name:"monster shield",desc:"防御很强的小怪",speed:5,max_speed:10,life:50,damage:3,shield:20},{name:"monster damage",desc:"伤害值很大的小怪",speed:7,max_speed:14,life:50,damage:10,shield:2},{name:"monster speed-life",desc:"速度、生命都较高的怪物",speed:15,max_speed:30,life:100,damage:3,shield:3},{name:"monster speed-2",desc:"速度很快的怪物",speed:30,max_speed:40,life:30,damage:4,shield:1},{name:"monster shield-life",desc:"防御很强、生命值很高的怪物",speed:3,max_speed:10,life:300,damage:5,shield:15}];if("undefined"==typeof e)return s.length;var n=s[e]||s[0],h={};return t.lang.mix(h,n),h.render||(h.render=i),h},t.makeMonsters=function(i,e){var s,n,h,a,l=[],r=0,c=t.monster_type_count;if(!e)for(e=[],s=0;c>s;s++)e.push(s);for(;i>r;)h=i-r,n=Math.min(Math.floor(Math.random()*h)+1,3),a=Math.floor(Math.random()*c),l.push([n,e[a]]),r+=n;return l}}),_TD.a.push(function(t){function i(t,i,e,s,n,h){var a,l,r,c,o,_,d,u,g;if(i==s)a=i,l=n>e?e+h:e-h;else if(e==n)l=e,a=s>i?i+h:i-h;else{if(r=(e-n)/(i-s),c=e-i*r,d=r*r+1,u=2*(r*(c-e)-i),g=Math.pow(c-e,2)+i*i-Math.pow(h,2),o=Math.pow(u,2)-4*d*g,0>o)return[0,0];o=Math.sqrt(o),_=(-u+o)/(2*d),s-i>0&&_-i>0||0>s-i&&0>_-i?(a=_,l=r*a+c):(a=(-u-o)/(2*d),l=r*a+c)}return t.lineCap="round",t.moveTo(i,e),t.lineTo(a,l),[a,l]}var e={cannon:function(t,e,s,n,h){var a=t.getTargetPosition();e.fillStyle="#393",e.strokeStyle="#000",e.beginPath(),e.lineWidth=_TD.retina,e.arc(t.cx,t.cy,h-5,0,2*Math.PI,!0),e.closePath(),e.fill(),e.stroke(),e.lineWidth=3*_TD.retina,e.beginPath(),e.moveTo(t.cx,t.cy),t.muzzle=i(e,t.cx,t.cy,a[0],a[1],h),e.closePath(),e.stroke(),e.lineWidth=_TD.retina,e.fillStyle="#060",e.beginPath(),e.arc(t.cx,t.cy,7*_TD.retina,0,2*Math.PI,!0),e.closePath(),e.fill(),e.stroke(),e.fillStyle="#cec",e.beginPath(),e.arc(t.cx+2,t.cy-2,3*_TD.retina,0,2*Math.PI,!0),e.closePath(),e.fill()},LMG:function(t,e,s,n,h){var a=t.getTargetPosition();e.fillStyle="#36f",e.strokeStyle="#000",e.beginPath(),e.lineWidth=_TD.retina,e.arc(t.cx,t.cy,7*_TD.retina,0,2*Math.PI,!0),e.closePath(),e.fill(),e.stroke(),e.lineWidth=2*_TD.retina,e.beginPath(),e.moveTo(t.cx,t.cy),t.muzzle=i(e,t.cx,t.cy,a[0],a[1],h),e.closePath(),e.fill(),e.stroke(),e.lineWidth=_TD.retina,e.fillStyle="#66c",e.beginPath(),e.arc(t.cx,t.cy,5*_TD.retina,0,2*Math.PI,!0),e.closePath(),e.fill(),e.stroke(),e.fillStyle="#ccf",e.beginPath(),e.arc(t.cx+1,t.cy-1,2*_TD.retina,0,2*Math.PI,!0),e.closePath(),e.fill()},HMG:function(t,e,s,n,h){var a=t.getTargetPosition();e.fillStyle="#933",e.strokeStyle="#000",e.beginPath(),e.lineWidth=_TD.retina,e.arc(t.cx,t.cy,h-2,0,2*Math.PI,!0),e.closePath(),e.fill(),e.stroke(),e.lineWidth=5*_TD.retina,e.beginPath(),e.moveTo(t.cx,t.cy),t.muzzle=i(e,t.cx,t.cy,a[0],a[1],h),e.closePath(),e.fill(),e.stroke(),e.lineWidth=_TD.retina,e.fillStyle="#630",e.beginPath(),e.arc(t.cx,t.cy,h-5*_TD.retina,0,2*Math.PI,!0),e.closePath(),e.fill(),e.stroke(),e.fillStyle="#960",e.beginPath(),e.arc(t.cx+1,t.cy-1,8*_TD.retina,0,2*Math.PI,!0),e.closePath(),e.fill(),e.fillStyle="#fcc",e.beginPath(),e.arc(t.cx+3,t.cy-3,4*_TD.retina,0,2*Math.PI,!0),e.closePath(),e.fill()},wall:function(t,i,e,s,n){i.lineWidth=_TD.retina,i.fillStyle="#666",i.strokeStyle="#000",i.fillRect(t.cx-n+1,t.cy-n+1,s-1,s-1),i.beginPath(),i.moveTo(t.cx-n+.5,t.cy-n+.5),i.lineTo(t.cx-n+.5,t.cy+n+.5),i.lineTo(t.cx+n+.5,t.cy+n+.5),i.lineTo(t.cx+n+.5,t.cy-n+.5),i.lineTo(t.cx-n+.5,t.cy-n+.5),i.moveTo(t.cx-n+.5,t.cy+n+.5),i.lineTo(t.cx+n+.5,t.cy-n+.5),i.moveTo(t.cx-n+.5,t.cy-n+.5),i.lineTo(t.cx+n+.5,t.cy+n+.5),i.closePath(),i.stroke()},laser_gun:function(t,i){i.fillStyle="#f00",i.strokeStyle="#000",i.beginPath(),i.lineWidth=_TD.retina,i.moveTo(t.cx,t.cy-10*_TD.retina),i.lineTo(t.cx-8.66*_TD.retina,t.cy+5*_TD.retina),i.lineTo(t.cx+8.66*_TD.retina,t.cy+5*_TD.retina),i.lineTo(t.cx,t.cy-10*_TD.retina),i.closePath(),i.fill(),i.stroke(),i.fillStyle="#60f",i.beginPath(),i.arc(t.cx,t.cy,7*_TD.retina,0,2*Math.PI,!0),i.closePath(),i.fill(),i.stroke(),i.fillStyle="#000",i.beginPath(),i.arc(t.cx,t.cy,3*_TD.retina,0,2*Math.PI,!0),i.closePath(),i.fill(),i.fillStyle="#666",i.beginPath(),i.arc(t.cx+1,t.cy-1,_TD.retina,0,2*Math.PI,!0),i.closePath(),i.fill(),i.lineWidth=3*_TD.retina,i.beginPath(),i.moveTo(t.cx,t.cy),i.closePath(),i.fill(),i.stroke()}};t.renderBuilding=function(i){var s=t.ctx,n=i.map,h=t.grid_size,a=t.grid_size/2;(e[i.type]||e.wall)(i,s,n,h,a)}}),_TD.a.push(function(t){t._msg_texts={_cant_build:"不能在这儿修建",_cant_pass:"怪物不能通过这儿",entrance:"起点",exit:"终点",not_enough_money:"金钱不足,需要 $${0}!",wave_info:"第 ${0} 波",panel_money_title:"金钱: ",panel_score_title:"积分: ",panel_life_title:"生命: ",panel_building_title:"建筑: ",panel_monster_title:"怪物: ",building_name_wall:"路障",building_name_cannon:"炮台",building_name_LMG:"轻机枪",building_name_HMG:"重机枪",building_name_laser_gun:"激光炮",building_info:"${0}: 等级 ${1},攻击 ${2},速度 ${3},射程 ${4},战绩 ${5}",building_info_wall:"${0}",building_intro_wall:"路障 可以阻止怪物通过 ($${0})",building_intro_cannon:"炮台 射程、杀伤力较为平衡 ($${0})",building_intro_LMG:"轻机枪 射程较远,杀伤力一般 ($${0})",building_intro_HMG:"重机枪 快速射击,威力较大,射程一般 ($${0})",building_intro_laser_gun:"激光枪 伤害较大,命中率 100% ($${0})",click_to_build:"左键点击建造 ${0} ($${1})",upgrade:"升级 ${0} 到 ${1} 级,需花费 $${2}。",sell:"出售 ${0},可获得 $${1}",upgrade_success:"升级成功,${0} 已升级到 ${1} 级!下次升级需要 $${2}。",monster_info:"怪物: 生命 ${0},防御 ${1},速度 ${2},伤害 ${3}",button_upgrade_text:"升级",button_sell_text:"出售",button_start_text:"开始",button_restart_text:"重新开始",button_pause_text:"暂停",button_continue_text:"继续",button_pause_desc_0:"游戏暂停",button_pause_desc_1:"游戏继续",blocked:"不能在这儿修建建筑,起点与终点之间至少要有一条路可到达!",monster_be_blocked:"不能在这儿修建建筑,有怪物被围起来了!",entrance_or_exit_be_blocked:"不能在起点或终点处修建建筑!",_:"ERROR"},t._t=t.translate=function(t,i){i="object"==typeof i&&i.constructor==Array?i:[];var e,s=this._msg_texts[t]||this._msg_texts._,n=i.length;for(e=0;n>e;e++)s=s.replace("${"+e+"}",i[e]);return s}}),_TD.a.push(function(t){t.FindWay=function(t,i,e,s,n,h,a){this.m=[],this.w=t,this.h=i,this.x1=e,this.y1=s,this.x2=n,this.y2=h,this.way=[],this.len=this.w*this.h,this.is_blocked=this.is_arrived=!1,this.fPassable="function"==typeof a?a:function(){return!0},this._init()},t.FindWay.prototype={_init:function(){if(this.x1==this.x2&&this.y1==this.y2)return this.is_arrived=!0,void(this.way=[[this.x1,this.y1]]);for(var t=0;tthis.len?!1:void(this.m[s]=e)},getNeighborsOf:function(t,i){var e=[];return i>0&&e.push([t,i-1]),t0&&e.push([t-1,i]),e},getAllNeighbors:function(){var t,i,e,s=[],n=this.current.length;for(i=0;n>i;i++)e=this.current[i],t=this.getNeighborsOf(e[0],e[1]),s=s.concat(t);return s},findWay:function(){for(var t,i,e,s,n,h,a,l=this.x2,r=this.y2,c=this.len,o=-1;(l!=this.x1||r!=this.y1)&&0!=o&&this.way.lengths;s++)h=this.getVal(e[s][0],e[s][1]),0>h||(0>o||o>h)&&(o=h);for(s=0;i>s;s++)t=e[s],o==this.getVal(t[0],t[1])&&a.push(t);n=a.length,s=n>1?Math.floor(Math.random()*n):0,t=a[s],l=t[0],r=t[1]}},arrive:function(){this.current=[],this.is_arrived=!0,this.findWay()},blocked:function(){this.current=[],this.is_blocked=!0},next:function(){var t,i,e,s,n,h=this.getAllNeighbors(),a=h.length,l=[];for(this.distance++,s=0;a>s;s++)if(t=h[s],i=t[0],e=t[1],-2==this.getVal(i,e)&&(this.fPassable(i,e)?(n=this.distance,l.push(t)):n=-1,this.setVal(i,e,n),i==this.x2&&e==this.y2))return this.arrive(),!1;return 0==l.length?(this.blocked(),!1):(this.current=l,!0)}}}); 3 | //# sourceMappingURL=td-pkg-zh-min.js.map -------------------------------------------------------------------------------- /build/td.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | HTML5 Tower Defense 7 | 10 | 11 | 12 | 13 |
14 |
15 |

HTML5 塔防游戏

16 | 17 |
加载中...
18 |
19 | 抱歉,您的浏览器不支持 HTML 5 Canvas 标签,请使用 IE9 / Chrome / Opera 等浏览器浏览本页以获得最佳效果。 20 |
21 |
22 |
23 | 关于 | 24 | 源码 | 25 | oldj.net © 2010-2011 26 |
27 |
28 | 29 | 30 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * author: oldj 3 | */ 4 | 5 | var gulp = require("gulp"); 6 | var uglify = require("gulp-uglify"); 7 | var browserify = require("gulp-browserify"); 8 | var concat = require("gulp-concat"); 9 | var sourcemaps = require("gulp-sourcemaps"); 10 | var args = require("yargs").argv; 11 | 12 | var IS_DEBUG = !!args.debug; 13 | console.log("IS_DEBUG: ", IS_DEBUG); 14 | console.log("--------------------"); 15 | 16 | gulp.task("scripts", function () { 17 | function doTask(lang) { 18 | gulp.src([ 19 | "src/js/td.js" 20 | , "src/js/td-lang.js" 21 | , "src/js/td-event.js" 22 | , "src/js/td-stage.js" 23 | , "src/js/td-element.js" 24 | , "src/js/td-obj-map.js" 25 | , "src/js/td-obj-grid.js" 26 | , "src/js/td-obj-building.js" 27 | , "src/js/td-obj-monster.js" 28 | , "src/js/td-obj-panel.js" 29 | , "src/js/td-data-stage-1.js" 30 | , "src/js/td-cfg-buildings.js" 31 | , "src/js/td-cfg-monsters.js" 32 | , "src/js/td-render-buildings.js" 33 | , "src/js/td-msg-" + (lang || "") + ".js" 34 | , "src/js/td-walk.js" 35 | 36 | ]) 37 | .pipe(sourcemaps.init()) 38 | .pipe(concat("td-pkg-" + lang + "-min.js")) 39 | .pipe(uglify({ 40 | compress: { 41 | drop_console: !IS_DEBUG 42 | } 43 | })) 44 | .pipe(sourcemaps.write("./")) 45 | .pipe(gulp.dest("build")) 46 | ; 47 | } 48 | 49 | doTask("zh"); 50 | doTask("en"); 51 | }); 52 | 53 | gulp.task("default", function () { 54 | gulp.start("scripts"); 55 | gulp.watch([ 56 | "src/**/*.js" 57 | ], [ 58 | "scripts" 59 | ]); 60 | }); 61 | 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html5-tower-defense", 3 | "version": "1.0.0", 4 | "description": "HTML5 塔防游戏", 5 | "main": "td.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/oldj/html5-tower-defense.git" 12 | }, 13 | "author": "oldj", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/oldj/html5-tower-defense/issues" 17 | }, 18 | "homepage": "https://github.com/oldj/html5-tower-defense#readme", 19 | "devDependencies": { 20 | "gulp": "~3.9.0", 21 | "gulp-browserify": "~0.5.1", 22 | "gulp-concat": "~2.6.0", 23 | "gulp-sourcemaps": "^1.5.2", 24 | "gulp-uglify": "~1.4.0", 25 | "yargs": "^3.24.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldj/html5-tower-defense/e3e009c7673121e98d6ff02c85fb442c774b6391/screenshots/1.png -------------------------------------------------------------------------------- /screenshots/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldj/html5-tower-defense/e3e009c7673121e98d6ff02c85fb442c774b6391/screenshots/10.png -------------------------------------------------------------------------------- /screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldj/html5-tower-defense/e3e009c7673121e98d6ff02c85fb442c774b6391/screenshots/2.png -------------------------------------------------------------------------------- /screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldj/html5-tower-defense/e3e009c7673121e98d6ff02c85fb442c774b6391/screenshots/3.png -------------------------------------------------------------------------------- /screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldj/html5-tower-defense/e3e009c7673121e98d6ff02c85fb442c774b6391/screenshots/4.png -------------------------------------------------------------------------------- /screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldj/html5-tower-defense/e3e009c7673121e98d6ff02c85fb442c774b6391/screenshots/5.png -------------------------------------------------------------------------------- /screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldj/html5-tower-defense/e3e009c7673121e98d6ff02c85fb442c774b6391/screenshots/6.png -------------------------------------------------------------------------------- /screenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldj/html5-tower-defense/e3e009c7673121e98d6ff02c85fb442c774b6391/screenshots/7.png -------------------------------------------------------------------------------- /screenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldj/html5-tower-defense/e3e009c7673121e98d6ff02c85fb442c774b6391/screenshots/8.png -------------------------------------------------------------------------------- /screenshots/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldj/html5-tower-defense/e3e009c7673121e98d6ff02c85fb442c774b6391/screenshots/9.png -------------------------------------------------------------------------------- /src/css/c.css: -------------------------------------------------------------------------------- 1 | /** 2 | * c.css 3 | * 4 | * @save-up: [../js/td.js, ../td.html] 5 | * 6 | * last update: 2010-11-22 10:50:50 7 | */ 8 | html, body { 9 | margin: 0; 10 | padding: 0; 11 | font-size: 12px; 12 | line-height: 20px; 13 | font-family: Verdana, "Times New Roman", serif; 14 | background: #1A74BA; 15 | } 16 | h1 { 17 | padding: 0; 18 | margin: 0; 19 | line-height: 48px; 20 | font-size: 18px; 21 | font-weight: bold; 22 | font-family: Verdana, "Times New Roman", serif; 23 | letter-spacing: 0.12em; 24 | } 25 | 26 | #wrapper { 27 | margin: 0 auto; 28 | } 29 | #td-wrapper { 30 | padding: 8px 24px 60px 24px; 31 | background: #E0F4FC; 32 | } 33 | #td-board { 34 | display: none; 35 | font-size: 16px; 36 | } 37 | #td-board canvas#td-canvas { 38 | position: relative; 39 | background: #fff; 40 | border: solid 1px #cdf; 41 | } 42 | #td-loading { 43 | font-size: 18px; 44 | line-height: 48px; 45 | padding: 60px 0 120px 0; 46 | font-style: italic; 47 | } 48 | #about { 49 | color: #fff; 50 | padding: 8px 24px; 51 | } 52 | #about a { 53 | color: #fff; 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oldj/html5-tower-defense/e3e009c7673121e98d6ff02c85fb442c774b6391/src/favicon.ico -------------------------------------------------------------------------------- /src/js/td-cfg-buildings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | * 本文件定义了建筑的参数、属性 8 | */ 9 | 10 | // _TD.a.push begin 11 | _TD.a.push(function (TD) { 12 | 13 | /** 14 | * 默认的升级规则 15 | * @param old_level {Number} 16 | * @param old_value {Number} 17 | * @return new_value {Number} 18 | */ 19 | TD.default_upgrade_rule = function (old_level, old_value) { 20 | return old_value * 1.2; 21 | }; 22 | 23 | /** 24 | * 取得建筑的默认属性 25 | * @param building_type {String} 建筑类型 26 | */ 27 | TD.getDefaultBuildingAttributes = function (building_type) { 28 | 29 | var building_attributes = { 30 | // 路障 31 | "wall": { 32 | damage: 0, 33 | range: 0, 34 | speed: 0, 35 | bullet_speed: 0, 36 | life: 100, 37 | shield: 500, 38 | cost: 5 39 | }, 40 | 41 | // 炮台 42 | "cannon": { 43 | damage: 12, 44 | range: 4, 45 | max_range: 8, 46 | speed: 2, 47 | bullet_speed: 6, 48 | life: 100, 49 | shield: 100, 50 | cost: 300, 51 | _upgrade_rule_damage: function (old_level, old_value) { 52 | return old_value * (old_level <= 10 ? 1.2 : 1.3); 53 | } 54 | }, 55 | 56 | // 轻机枪 57 | "LMG": { 58 | damage: 5, 59 | range: 5, 60 | max_range: 10, 61 | speed: 3, 62 | bullet_speed: 6, 63 | life: 100, 64 | shield: 50, 65 | cost: 100 66 | }, 67 | 68 | // 重机枪 69 | "HMG": { 70 | damage: 30, 71 | range: 3, 72 | max_range: 5, 73 | speed: 3, 74 | bullet_speed: 5, 75 | life: 100, 76 | shield: 200, 77 | cost: 800, 78 | _upgrade_rule_damage: function (old_level, old_value) { 79 | return old_value * 1.3; 80 | } 81 | }, 82 | 83 | // 激光枪 84 | "laser_gun": { 85 | damage: 25, 86 | range: 6, 87 | max_range: 10, 88 | speed: 20, 89 | // bullet_speed: 10, // laser_gun 的 bullet_speed 属性没有用 90 | life: 100, 91 | shield: 100, 92 | cost: 2000 93 | } 94 | }; 95 | 96 | return building_attributes[building_type] || {}; 97 | }; 98 | 99 | }); // _TD.a.push end 100 | -------------------------------------------------------------------------------- /src/js/td-cfg-monsters.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | * 本文件定义了怪物默认属性及渲染方法 8 | */ 9 | 10 | 11 | // _TD.a.push begin 12 | _TD.a.push(function (TD) { 13 | 14 | /** 15 | * 默认的怪物渲染方法 16 | */ 17 | function defaultMonsterRender() { 18 | if (!this.is_valid || !this.grid) return; 19 | var ctx = TD.ctx; 20 | 21 | // 画一个圆代表怪物 22 | ctx.strokeStyle = "#000"; 23 | ctx.lineWidth = 1; 24 | ctx.fillStyle = this.color; 25 | ctx.beginPath(); 26 | ctx.arc(this.cx, this.cy, this.r, 0, Math.PI * 2, true); 27 | ctx.closePath(); 28 | ctx.fill(); 29 | ctx.stroke(); 30 | 31 | // 画怪物的生命值 32 | if (TD.show_monster_life) { 33 | var s = Math.floor(TD.grid_size / 4), 34 | l = s * 2 - 2 * _TD.retina; 35 | ctx.fillStyle = "#000"; 36 | ctx.beginPath(); 37 | ctx.fillRect(this.cx - s, this.cy - this.r - 6, s * 2, 4 * _TD.retina); 38 | ctx.closePath(); 39 | ctx.fillStyle = "#f00"; 40 | ctx.beginPath(); 41 | ctx.fillRect(this.cx - s + _TD.retina, this.cy - this.r - (6 - _TD.retina), this.life * l / this.life0, 2 * _TD.retina); 42 | ctx.closePath(); 43 | } 44 | } 45 | 46 | /** 47 | * 取得怪物的默认属性 48 | * @param [monster_idx] {Number} 怪物的类型 49 | * @return attributes {Object} 50 | */ 51 | TD.getDefaultMonsterAttributes = function (monster_idx) { 52 | 53 | var monster_attributes = [ 54 | { 55 | // idx: 0 56 | name: "monster 1", 57 | desc: "最弱小的怪物", 58 | speed: 3, 59 | max_speed: 10, 60 | life: 50, 61 | damage: 1, // 到达终点后会带来多少点伤害(1 ~ 10) 62 | shield: 0, 63 | money: 5 // 消灭本怪物后可得多少金钱(可选) 64 | }, 65 | { 66 | // idx: 1 67 | name: "monster 2", 68 | desc: "稍强一些的小怪", 69 | speed: 6, 70 | max_speed: 20, 71 | life: 50, 72 | damage: 2, // 到达终点后会带来多少点伤害(1 ~ 10) 73 | shield: 1 74 | }, 75 | { 76 | // idx: 2 77 | name: "monster speed", 78 | desc: "速度较快的小怪", 79 | speed: 12, 80 | max_speed: 30, 81 | life: 50, 82 | damage: 3, // 到达终点后会带来多少点伤害(1 ~ 10) 83 | shield: 1 84 | }, 85 | { 86 | // idx: 3 87 | name: "monster life", 88 | desc: "生命值很强的小怪", 89 | speed: 5, 90 | max_speed: 10, 91 | life: 500, 92 | damage: 3, // 到达终点后会带来多少点伤害(1 ~ 10) 93 | shield: 1 94 | }, 95 | { 96 | // idx: 4 97 | name: "monster shield", 98 | desc: "防御很强的小怪", 99 | speed: 5, 100 | max_speed: 10, 101 | life: 50, 102 | damage: 3, // 到达终点后会带来多少点伤害(1 ~ 10) 103 | shield: 20 104 | }, 105 | { 106 | // idx: 5 107 | name: "monster damage", 108 | desc: "伤害值很大的小怪", 109 | speed: 7, 110 | max_speed: 14, 111 | life: 50, 112 | damage: 10, // 到达终点后会带来多少点伤害(1 ~ 10) 113 | shield: 2 114 | }, 115 | { 116 | // idx: 6 117 | name: "monster speed-life", 118 | desc: "速度、生命都较高的怪物", 119 | speed: 15, 120 | max_speed: 30, 121 | life: 100, 122 | damage: 3, // 到达终点后会带来多少点伤害(1 ~ 10) 123 | shield: 3 124 | }, 125 | { 126 | // idx: 7 127 | name: "monster speed-2", 128 | desc: "速度很快的怪物", 129 | speed: 30, 130 | max_speed: 40, 131 | life: 30, 132 | damage: 4, // 到达终点后会带来多少点伤害(1 ~ 10) 133 | shield: 1 134 | }, 135 | { 136 | // idx: 8 137 | name: "monster shield-life", 138 | desc: "防御很强、生命值很高的怪物", 139 | speed: 3, 140 | max_speed: 10, 141 | life: 300, 142 | damage: 5, // 到达终点后会带来多少点伤害(1 ~ 10) 143 | shield: 15 144 | } 145 | ]; 146 | 147 | if (typeof monster_idx == "undefined") { 148 | // 如果只传了一个参数,则只返回共定义了多少种怪物(供 td.js 中使用) 149 | return monster_attributes.length; 150 | } 151 | 152 | var attr = monster_attributes[monster_idx] || monster_attributes[0], 153 | attr2 = {}; 154 | 155 | TD.lang.mix(attr2, attr); 156 | if (!attr2.render) { 157 | // 如果没有指定当前怪物的渲染方法 158 | attr2.render = defaultMonsterRender 159 | } 160 | 161 | return attr2; 162 | }; 163 | 164 | 165 | /** 166 | * 生成一个怪物列表, 167 | * 包含 n 个怪物 168 | * 怪物类型在 range 中指定,如未指定,则为随机 169 | */ 170 | TD.makeMonsters = function (n, range) { 171 | var a = [], count = 0, i, c, d, r, l = TD.monster_type_count; 172 | if (!range) { 173 | range = []; 174 | for (i = 0; i < l; i++) { 175 | range.push(i); 176 | } 177 | } 178 | 179 | while (count < n) { 180 | d = n - count; 181 | c = Math.min( 182 | Math.floor(Math.random() * d) + 1, 183 | 3 // 同一类型的怪物一次最多出现 3 个,防止某一波中怪出大量高防御或高速度的怪 184 | ); 185 | r = Math.floor(Math.random() * l); 186 | a.push([c, range[r]]); 187 | count += c; 188 | } 189 | 190 | return a; 191 | }; 192 | 193 | 194 | }); // _TD.a.push end 195 | -------------------------------------------------------------------------------- /src/js/td-data-stage-1.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | * 默认关卡 8 | */ 9 | 10 | // _TD.a.push begin 11 | _TD.a.push(function (TD) { 12 | 13 | // main stage 初始化方法 14 | var _stage_main_init = function () { 15 | var act = new TD.Act(this, "act-1"), 16 | scene = new TD.Scene(act, "scene-1"), 17 | cfg = TD.getDefaultStageData("scene_endless"); 18 | 19 | this.config = cfg.config; 20 | TD.life = this.config.life; 21 | TD.money = this.config.money; 22 | TD.score = this.config.score; 23 | TD.difficulty = this.config.difficulty; 24 | TD.wave_damage = this.config.wave_damage; 25 | 26 | // make map 27 | var map = new TD.Map("main-map", TD.lang.mix({ 28 | scene: scene, 29 | is_main_map: true, 30 | step_level: 1, 31 | render_level: 2 32 | }, cfg.map)); 33 | map.addToScene(scene, 1, 2, map.grids); 34 | scene.map = map; 35 | 36 | // make panel 37 | scene.panel = new TD.Panel("panel", TD.lang.mix({ 38 | scene: scene, 39 | main_map: map, 40 | step_level: 1, 41 | render_level: 7 42 | }, cfg.panel)); 43 | 44 | this.newWave = cfg.newWave; 45 | this.map = map; 46 | this.wait_new_wave = this.config.wait_new_wave; 47 | }, 48 | _stage_main_step2 = function () { 49 | //TD.log(this.current_act.current_scene.wave); 50 | 51 | var scene = this.current_act.current_scene, 52 | wave = scene.wave; 53 | if ((wave == 0 && !this.map.has_weapon) || scene.state != 1) { 54 | return; 55 | } 56 | 57 | if (this.map.monsters.length == 0) { 58 | if (wave > 0 && this.wait_new_wave == this.config.wait_new_wave - 1) { 59 | // 一波怪物刚刚走完 60 | // 奖励生命值 61 | 62 | var wave_reward = 0; 63 | if (wave % 10 == 0) { 64 | wave_reward = 10; 65 | } else if (wave % 5 == 0) { 66 | wave_reward = 5; 67 | } 68 | if (TD.life + wave_reward > 100) { 69 | wave_reward = 100 - TD.life; 70 | } 71 | if (wave_reward > 0) { 72 | TD.recover(wave_reward); 73 | } 74 | } 75 | 76 | if (this.wait_new_wave > 0) { 77 | this.wait_new_wave--; 78 | return; 79 | } 80 | 81 | this.wait_new_wave = this.config.wait_new_wave; 82 | wave++; 83 | scene.wave = wave; 84 | this.newWave({ 85 | map: this.map, 86 | wave: wave 87 | }); 88 | } 89 | }; 90 | 91 | TD.getDefaultStageData = function (k) { 92 | var data = { 93 | stage_main: { 94 | width: 640 * _TD.retina, // px 95 | height: 560 * _TD.retina, 96 | init: _stage_main_init, 97 | step2: _stage_main_step2 98 | }, 99 | 100 | scene_endless: { 101 | // scene 1 102 | map: { 103 | grid_x: 16, 104 | grid_y: 16, 105 | x: TD.padding, 106 | y: TD.padding, 107 | entrance: [0, 0], 108 | exit: [15, 15], 109 | grids_cfg: [ 110 | { 111 | pos: [3, 3], 112 | //building: "cannon", 113 | passable_flag: 0 114 | }, 115 | { 116 | pos: [7, 15], 117 | build_flag: 0 118 | }, 119 | { 120 | pos: [4, 12], 121 | building: "wall" 122 | }, 123 | { 124 | pos: [4, 13], 125 | building: "wall" 126 | //}, { 127 | //pos: [11, 9], 128 | //building: "cannon" 129 | //}, { 130 | //pos: [5, 2], 131 | //building: "HMG" 132 | //}, { 133 | //pos: [14, 9], 134 | //building: "LMG" 135 | //}, { 136 | //pos: [3, 14], 137 | //building: "LMG" 138 | } 139 | ] 140 | }, 141 | panel: { 142 | x: TD.padding * 2 + TD.grid_size * 16, 143 | y: TD.padding, 144 | map: { 145 | grid_x: 3, 146 | grid_y: 3, 147 | x: 0, 148 | y: 110 * _TD.retina, 149 | grids_cfg: [ 150 | { 151 | pos: [0, 0], 152 | building: "cannon" 153 | }, 154 | { 155 | pos: [1, 0], 156 | building: "LMG" 157 | }, 158 | { 159 | pos: [2, 0], 160 | building: "HMG" 161 | }, 162 | { 163 | pos: [0, 1], 164 | building: "laser_gun" 165 | }, 166 | { 167 | pos: [2, 2], 168 | building: "wall" 169 | } 170 | ] 171 | } 172 | }, 173 | config: { 174 | endless: true, 175 | wait_new_wave: TD.exp_fps * 3, // 经过多少 step 后再开始新的一波 176 | difficulty: 1.0, // 难度系数 177 | wave: 0, 178 | max_wave: -1, 179 | wave_damage: 0, // 当前一波怪物造成了多少点生命值的伤害 180 | max_monsters_per_wave: 100, // 每一波最多多少怪物 181 | money: 500, 182 | score: 0, // 开局时的积分 183 | life: 100, 184 | waves: [ // 这儿只定义了前 10 波怪物,从第 11 波开始自动生成 185 | [], 186 | // 第一个参数是没有用的(第 0 波) 187 | 188 | // 第一波 189 | [ 190 | [1, 0] // 1 个 0 类怪物 191 | ], 192 | 193 | // 第二波 194 | [ 195 | [1, 0], // 1 个 0 类怪物 196 | [1, 1] // 1 个 1 类怪物 197 | ], 198 | 199 | // wave 3 200 | [ 201 | [2, 0], // 2 个 0 类怪物 202 | [1, 1] // 1 个 1 类怪物 203 | ], 204 | 205 | // wave 4 206 | [ 207 | [2, 0], 208 | [1, 1] 209 | ], 210 | 211 | // wave 5 212 | [ 213 | [3, 0], 214 | [2, 1] 215 | ], 216 | 217 | // wave 6 218 | [ 219 | [4, 0], 220 | [2, 1] 221 | ], 222 | 223 | // wave 7 224 | [ 225 | [5, 0], 226 | [3, 1], 227 | [1, 2] 228 | ], 229 | 230 | // wave 8 231 | [ 232 | [6, 0], 233 | [4, 1], 234 | [1, 2] 235 | ], 236 | 237 | // wave 9 238 | [ 239 | [7, 0], 240 | [3, 1], 241 | [2, 2] 242 | ], 243 | 244 | // wave 10 245 | [ 246 | [8, 0], 247 | [4, 1], 248 | [3, 2] 249 | ] 250 | ] 251 | }, 252 | 253 | /** 254 | * 生成第 n 波怪物的方法 255 | */ 256 | newWave: function (cfg) { 257 | cfg = cfg || {}; 258 | var map = cfg.map, 259 | wave = cfg.wave || 1, 260 | //difficulty = TD.difficulty || 1.0, 261 | wave_damage = TD.wave_damage || 0; 262 | 263 | // 自动调整难度系数 264 | if (wave == 1) { 265 | //pass 266 | } else if (wave_damage == 0) { 267 | // 没有造成伤害 268 | if (wave < 5) { 269 | TD.difficulty *= 1.05; 270 | } else if (TD.difficulty > 30) { 271 | TD.difficulty *= 1.1; 272 | } else { 273 | TD.difficulty *= 1.2; 274 | } 275 | } else if (TD.wave_damage >= 50) { 276 | TD.difficulty *= 0.6; 277 | } else if (TD.wave_damage >= 30) { 278 | TD.difficulty *= 0.7; 279 | } else if (TD.wave_damage >= 20) { 280 | TD.difficulty *= 0.8; 281 | } else if (TD.wave_damage >= 10) { 282 | TD.difficulty *= 0.9; 283 | } else { 284 | // 造成了 10 点以内的伤害 285 | if (wave >= 10) 286 | TD.difficulty *= 1.05; 287 | } 288 | if (TD.difficulty < 1) TD.difficulty = 1; 289 | 290 | TD.log("wave " + wave + ", last wave damage = " + wave_damage + ", difficulty = " + TD.difficulty); 291 | 292 | //map.addMonsters(100, 7); 293 | //map.addMonsters2([[10, 7], [5, 0], [5, 5]]); 294 | // 295 | var wave_data = this.config.waves[wave] || 296 | // 自动生成怪物 297 | TD.makeMonsters(Math.min( 298 | Math.floor(Math.pow(wave, 1.1)), 299 | this.config.max_monsters_per_wave 300 | )); 301 | map.addMonsters2(wave_data); 302 | 303 | TD.wave_damage = 0; 304 | } 305 | } // end of scene_endless 306 | }; 307 | 308 | return data[k] || {}; 309 | }; 310 | 311 | }); // _TD.a.push end 312 | 313 | 314 | -------------------------------------------------------------------------------- /src/js/td-element.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | * 8 | * 本文件定义了 Element 类,这个类是游戏中所有元素的基类, 9 | * 包括地图、格子、怪物、建筑、子弹、气球提示等都基于这个类 10 | * 11 | */ 12 | 13 | 14 | // _TD.a.push begin 15 | _TD.a.push(function (TD) { 16 | 17 | /** 18 | * Element 是游戏中所有可控制元素的基类 19 | * @param id {String} 给这个元素一个唯一的不重复的 ID,如果不指定则随机生成 20 | * @param cfg {Object} 元素的配置信息 21 | */ 22 | TD.Element = function (id, cfg) { 23 | this.id = id || ("el-" + TD.lang.rndStr()); 24 | this.cfg = cfg || {}; 25 | 26 | this.is_valid = true; 27 | this.is_visiable = typeof cfg.is_visiable != "undefined" ? cfg.is_visiable : true; 28 | this.is_paused = false; 29 | this.is_hover = false; 30 | this.x = this.cfg.x || -1; 31 | this.y = this.cfg.y || -1; 32 | this.width = this.cfg.width || 0; 33 | this.height = this.cfg.height || 0; 34 | this.step_level = cfg.step_level || 1; 35 | this.render_level = cfg.render_level; 36 | this.on_events = cfg.on_events || []; 37 | 38 | this._init(); 39 | }; 40 | 41 | TD.Element.prototype = { 42 | _init: function () { 43 | var _this = this, 44 | i, en, len; 45 | 46 | // 监听指定事件 47 | for (i = 0, len = this.on_events.length; i < len; i++) { 48 | en = this.on_events[i]; 49 | switch (en) { 50 | 51 | // 鼠标进入元素 52 | case "enter": 53 | this.on("enter", function () { 54 | _this.onEnter(); 55 | }); 56 | break; 57 | 58 | // 鼠标移出元素 59 | case "out": 60 | this.on("out", function () { 61 | _this.onOut(); 62 | }); 63 | break; 64 | 65 | // 鼠标在元素上,相当于 DOM 中的 onmouseover 66 | case "hover": 67 | this.on("hover", function () { 68 | _this.onHover(); 69 | }); 70 | break; 71 | 72 | // 鼠标点击了元素 73 | case "click": 74 | this.on("click", function () { 75 | _this.onClick(); 76 | }); 77 | break; 78 | } 79 | } 80 | this.caculatePos(); 81 | }, 82 | /** 83 | * 重新计算元素的位置信息 84 | */ 85 | caculatePos: function () { 86 | this.cx = this.x + this.width / 2; // 中心的位置 87 | this.cy = this.y + this.height / 2; 88 | this.x2 = this.x + this.width; // 右边界 89 | this.y2 = this.y + this.height; // 下边界 90 | }, 91 | start: function () { 92 | this.is_paused = false; 93 | }, 94 | pause: function () { 95 | this.is_paused = true; 96 | }, 97 | hide: function () { 98 | this.is_visiable = false; 99 | this.onOut(); 100 | }, 101 | show: function () { 102 | this.is_visiable = true; 103 | }, 104 | /** 105 | * 删除本元素 106 | */ 107 | del: function () { 108 | this.is_valid = false; 109 | }, 110 | /** 111 | * 绑定指定类型的事件 112 | * @param evt_type {String} 事件类型 113 | * @param f {Function} 处理方法 114 | */ 115 | on: function (evt_type, f) { 116 | TD.eventManager.on(this, evt_type, f); 117 | }, 118 | 119 | // 下面几个方法默认为空,实例中按需要重载 120 | onEnter: TD.lang.nullFunc, 121 | onOut: TD.lang.nullFunc, 122 | onHover: TD.lang.nullFunc, 123 | onClick: TD.lang.nullFunc, 124 | step: TD.lang.nullFunc, 125 | render: TD.lang.nullFunc, 126 | 127 | /** 128 | * 将当前 element 加入到场景 scene 中 129 | * 在加入本 element 之前,先加入 pre_add_list 中的element 130 | * @param scene 131 | * @param step_level {Number} 132 | * @param render_level {Number} 133 | * @param pre_add_list {Array} Optional [element1, element2, ...] 134 | */ 135 | addToScene: function (scene, step_level, render_level, pre_add_list) { 136 | this.scene = scene; 137 | if (isNaN(step_level)) return; 138 | this.step_level = step_level || this.step_level; 139 | this.render_level = render_level || this.render_level; 140 | 141 | if (pre_add_list) { 142 | TD.lang.each(pre_add_list, function (obj) { 143 | scene.addElement(obj, step_level, render_level); 144 | }); 145 | } 146 | scene.addElement(this, step_level, render_level); 147 | } 148 | }; 149 | 150 | }); // _TD.a.push end 151 | 152 | -------------------------------------------------------------------------------- /src/js/td-event.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | * Last Update: 2011/1/10 5:22:52 8 | */ 9 | 10 | 11 | // _TD.a.push begin 12 | _TD.a.push(function (TD) { 13 | 14 | /** 15 | * 事件管理器 16 | */ 17 | TD.eventManager = { 18 | ex: -1, // 事件坐标 x 19 | ey: -1, // 事件坐标 y 20 | _registers: {}, // 注册监听事件的元素 21 | 22 | // 目前支持的事件类型 23 | ontypes: [ 24 | "enter", // 鼠标移入 25 | "hover", // 鼠标在元素上,相当于 onmouseover 26 | "out", // 鼠标移出 27 | "click" // 鼠标点击 28 | ], 29 | 30 | // 当前事件类型 31 | current_type: "hover", 32 | 33 | /** 34 | * 根据事件坐标,判断事件是否在元素上 35 | * @param el {Element} Element 元素 36 | * @return {Boolean} 37 | */ 38 | isOn: function (el) { 39 | return (this.ex != -1 && 40 | this.ey != -1 && 41 | this.ex > el.x && 42 | this.ex < el.x2 && 43 | this.ey > el.y && 44 | this.ey < el.y2); 45 | }, 46 | 47 | /** 48 | * 根据元素名、事件名,生成一个字符串标识,用于注册事件监听 49 | * @param el {Element} 50 | * @param evt_type {String} 51 | * @return evt_name {String} 字符串标识 52 | */ 53 | _mkElEvtName: function (el, evt_type) { 54 | return el.id + "::_evt_::" + evt_type; 55 | }, 56 | 57 | /** 58 | * 为元素注册事件监听 59 | * 现在的实现比较简单,如果一个元素对某个事件多次注册监听,后面的监听将会覆盖前面的 60 | * 61 | * @param el {Element} 62 | * @param evt_type {String} 63 | * @param f {Function} 64 | */ 65 | on: function (el, evt_type, f) { 66 | this._registers[this._mkElEvtName(el, evt_type)] = [el, evt_type, f]; 67 | }, 68 | 69 | /** 70 | * 移除元素对指定事件的监听 71 | * @param el {Element} 72 | * @param evt_type {String} 73 | */ 74 | removeEventListener: function (el, evt_type) { 75 | var en = this._mkElEvtName(el, evt_type); 76 | delete this._registers[en]; 77 | }, 78 | 79 | /** 80 | * 清除所有监听事件 81 | */ 82 | clear: function () { 83 | delete this._registers; 84 | this._registers = {}; 85 | //this.elements = []; 86 | }, 87 | 88 | /** 89 | * 主循环方法 90 | */ 91 | step: function () { 92 | if (!this.current_type) return; // 没有事件被触发 93 | 94 | var k, a, el, et, f, 95 | //en, 96 | j, 97 | _this = this, 98 | ontypes_len = this.ontypes.length, 99 | is_evt_on, 100 | // reg_length = 0, 101 | to_del_el = []; 102 | 103 | //var m = TD.stage.current_act.current_scene.map; 104 | //TD.log([m.is_hover, this.isOn(m)]); 105 | 106 | // 遍历当前注册的事件 107 | for (k in this._registers) { 108 | // reg_length ++; 109 | if (!this._registers.hasOwnProperty(k)) continue; 110 | a = this._registers[k]; 111 | el = a[0]; // 事件对应的元素 112 | et = a[1]; // 事件类型 113 | f = a[2]; // 事件处理函数 114 | if (!el.is_valid) { 115 | to_del_el.push(el); 116 | continue; 117 | } 118 | if (!el.is_visiable) continue; // 不可见元素不响应事件 119 | 120 | is_evt_on = this.isOn(el); // 事件是否发生在元素上 121 | 122 | if (this.current_type != "click") { 123 | // enter / out / hover 事件 124 | 125 | if (et == "hover" && el.is_hover && is_evt_on) { 126 | // 普通的 hover 127 | f(); 128 | this.current_type = "hover"; 129 | } else if (et == "enter" && !el.is_hover && is_evt_on) { 130 | // enter 事件 131 | el.is_hover = true; 132 | f(); 133 | this.current_type = "enter"; 134 | } else if (et == "out" && el.is_hover && !is_evt_on) { 135 | // out 事件 136 | el.is_hover = false; 137 | f(); 138 | this.current_type = "out"; 139 | // } else { 140 | // 事件与当前元素无关 141 | // continue; 142 | } 143 | 144 | } else { 145 | // click 事件 146 | if (is_evt_on && et == "click") f(); 147 | } 148 | } 149 | 150 | // 删除指定元素列表的事件 151 | TD.lang.each(to_del_el, function (obj) { 152 | for (j = 0; j < ontypes_len; j++) 153 | _this.removeEventListener(obj, _this.ontypes[j]); 154 | }); 155 | // TD.log(reg_length); 156 | this.current_type = ""; 157 | }, 158 | 159 | /** 160 | * 鼠标在元素上 161 | * @param ex {Number} 162 | * @param ey {Number} 163 | */ 164 | hover: function (ex, ey) { 165 | // 如果还有 click 事件未处理则退出,点击事件具有更高的优先级 166 | if (this.current_type == "click") return; 167 | 168 | this.current_type = "hover"; 169 | this.ex = ex; 170 | this.ey = ey; 171 | }, 172 | 173 | /** 174 | * 点击事件 175 | * @param ex {Number} 176 | * @param ey {Number} 177 | */ 178 | click: function (ex, ey) { 179 | this.current_type = "click"; 180 | this.ex = ex; 181 | this.ey = ey; 182 | } 183 | }; 184 | 185 | }); // _TD.a.push end 186 | 187 | -------------------------------------------------------------------------------- /src/js/td-lang.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | * Last Update: 2011/1/10 5:22:52 8 | */ 9 | 10 | 11 | // _TD.a.push begin 12 | _TD.a.push(function (TD) { 13 | 14 | TD.lang = { 15 | /** 16 | * document.getElementById 方法的简写 17 | * @param el_id {String} 18 | */ 19 | $e: function (el_id) { 20 | return document.getElementById(el_id); 21 | }, 22 | 23 | /** 24 | * 创建一个 DOM 元素 25 | * @param tag_name {String} 26 | * @param attributes {Object} 27 | * @param parent_node {HTMLElement} 28 | * @return {HTMLElement} 29 | */ 30 | $c: function (tag_name, attributes, parent_node) { 31 | var el = document.createElement(tag_name); 32 | attributes = attributes || {}; 33 | 34 | for (var k in attributes) { 35 | if (attributes.hasOwnProperty(k)) { 36 | el.setAttribute(k, attributes[k]); 37 | } 38 | } 39 | 40 | if (parent_node) 41 | parent_node.appendChild(el); 42 | 43 | return el; 44 | }, 45 | 46 | /** 47 | * 从字符串 s 左边截取n个字符 48 | * 如果包含汉字,则汉字按两个字符计算 49 | * @param s {String} 输入的字符串 50 | * @param n {Number} 51 | */ 52 | strLeft: function (s, n) { 53 | var s2 = s.slice(0, n), 54 | i = s2.replace(/[^\x00-\xff]/g, "**").length; 55 | if (i <= n) return s2; 56 | i -= s2.length; 57 | switch (i) { 58 | case 0: 59 | return s2; 60 | case n: 61 | return s.slice(0, n >> 1); 62 | default: 63 | var k = n - i, 64 | s3 = s.slice(k, n), 65 | j = s3.replace(/[\x00-\xff]/g, "").length; 66 | return j ? 67 | s.slice(0, k) + this.arguments.callee(s3, j) : 68 | s.slice(0, k); 69 | } 70 | }, 71 | 72 | /** 73 | * 取得一个字符串的字节长度 74 | * 汉字等字符长度算2,英文、数字等算1 75 | * @param s {String} 76 | */ 77 | strLen2: function (s) { 78 | return s.replace(/[^\x00-\xff]/g, "**").length; 79 | }, 80 | 81 | /** 82 | * 对一个数组的每一个元素执行指定方法 83 | * @param list {Array} 84 | * @param f {Function} 85 | */ 86 | each: function (list, f) { 87 | if (Array.prototype.forEach) { 88 | list.forEach(f); 89 | } else { 90 | for (var i = 0, l = list.length; i < l; i++) { 91 | f(list[i]); 92 | } 93 | } 94 | }, 95 | 96 | /** 97 | * 对一个数组的每一项依次执行指定方法,直到某一项的返回值为 true 98 | * 返回第一个令 f 值为 true 的元素,如没有元素令 f 值为 true,则 99 | * 返回 null 100 | * @param list {Array} 101 | * @param f {Function} 102 | * @return {Object} 103 | */ 104 | any: function (list, f) { 105 | for (var i = 0, l = list.length; i < l; i++) { 106 | if (f(list[i])) 107 | return list[i]; 108 | } 109 | return null; 110 | }, 111 | 112 | /** 113 | * 依次弹出列表中的元素,并对其进行操作 114 | * 注意,执行完毕之后原数组将被清空 115 | * 类似于 each,不同的是这个函数执行完后原数组将被清空 116 | * @param list {Array} 117 | * @param f {Function} 118 | */ 119 | shift: function (list, f) { 120 | while (list[0]) { 121 | f(list.shift()); 122 | // f.apply(list.shift(), args); 123 | } 124 | }, 125 | 126 | /** 127 | * 传入一个数组,将其随机排序并返回 128 | * 返回的是一个新的数组,原数组不变 129 | * @param list {Array} 130 | * @return {Array} 131 | */ 132 | rndSort: function (list) { 133 | var a = list.concat(); 134 | return a.sort(function () { 135 | return Math.random() - 0.5; 136 | }); 137 | }, 138 | 139 | _rndRGB2: function (v) { 140 | var s = v.toString(16); 141 | return s.length == 2 ? s : ("0" + s); 142 | }, 143 | /** 144 | * 随机生成一个 RGB 颜色 145 | */ 146 | rndRGB: function () { 147 | var r = Math.floor(Math.random() * 256), 148 | g = Math.floor(Math.random() * 256), 149 | b = Math.floor(Math.random() * 256); 150 | 151 | return "#" + this._rndRGB2(r) + this._rndRGB2(g) + this._rndRGB2(b); 152 | }, 153 | /** 154 | * 将一个 rgb 色彩字符串转化为一个数组 155 | * eg: '#ffffff' => [255, 255, 255] 156 | * @param rgb_str {String} rgb色彩字符串,类似于“#f8c693” 157 | */ 158 | rgb2Arr: function (rgb_str) { 159 | if (rgb_str.length != 7) return [0, 0, 0]; 160 | 161 | var r = rgb_str.substr(1, 2), 162 | g = rgb_str.substr(3, 2), 163 | b = rgb_str.substr(3, 2); 164 | 165 | return [parseInt(r, 16), parseInt(g, 16), parseInt(b, 16)]; 166 | }, 167 | 168 | /** 169 | * 生成一个长度为 n 的随机字符串 170 | * 171 | * @param [n] {Number} 172 | */ 173 | rndStr: function (n) { 174 | n = n || 16; 175 | var chars = "1234567890abcdefghijklmnopqrstuvwxyz", 176 | a = [], 177 | i, chars_len = chars.length, r; 178 | 179 | for (i = 0; i < n; i++) { 180 | r = Math.floor(Math.random() * chars_len); 181 | a.push(chars.substr(r, 1)); 182 | } 183 | return a.join(""); 184 | }, 185 | 186 | /** 187 | * 空函数,一般用于占位 188 | */ 189 | nullFunc: function () { 190 | }, 191 | 192 | /** 193 | * 判断两个数组是否相等 194 | * 195 | * @param arr1 {Array} 196 | * @param arr2 {Array} 197 | */ 198 | arrayEqual: function (arr1, arr2) { 199 | var i, l = arr1.length; 200 | 201 | if (l != arr2.length) return false; 202 | 203 | for (i = 0; i < l; i++) { 204 | if (arr1[i] != arr2[i]) return false; 205 | } 206 | 207 | return true; 208 | }, 209 | 210 | /** 211 | * 将所有 s 的属性复制给 r 212 | * @param r {Object} 213 | * @param s {Object} 214 | * @param [is_overwrite] {Boolean} 如指定为 false ,则不覆盖已有的值,其它值 215 | * 包括 undefined ,都表示 s 中的同名属性将覆盖 r 中的值 216 | */ 217 | mix: function (r, s, is_overwrite) { 218 | if (!s || !r) return r; 219 | 220 | for (var p in s) { 221 | if (s.hasOwnProperty(p) && (is_overwrite !== false || !(p in r))) { 222 | r[p] = s[p]; 223 | } 224 | } 225 | return r; 226 | } 227 | }; 228 | 229 | }); // _TD.a.push end 230 | -------------------------------------------------------------------------------- /src/js/td-msg-en.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | * Last Update: 2011/1/10 5:22:52 8 | */ 9 | 10 | 11 | // _TD.a.push begin 12 | _TD.a.push(function (TD) { 13 | 14 | TD._msg_texts = { 15 | "_cant_build": "Can't build here!", 16 | "_cant_pass": "Can't pass!", 17 | "entrance": "Entrance", 18 | "exit": "Exit", 19 | "not_enough_money": "Not enough money, need $${0}.", 20 | "wave_info": "Wave ${0}", 21 | "panel_money_title": "Money: ", 22 | "panel_score_title": "Score: ", 23 | "panel_life_title": "Life: ", 24 | "panel_building_title": "Buildings: ", 25 | "panel_monster_title": "Monsters: ", 26 | "building_name_wall": "Roadblock", 27 | "building_name_cannon": "Cannon", 28 | "building_name_LMG": "LMG", 29 | "building_name_HMG": "HMG", 30 | "building_name_laser_gun": "Laser gun", 31 | "building_info": "${0}: Level ${1}, Damage ${2}, Speed ${3}, Range ${4}, Kill ${5}", 32 | "building_info_wall": "${0}", 33 | "building_intro_wall": "Roadblock: monsters could not pass ($${0})", 34 | "building_intro_cannon": "Cannon: blance in range and damage ($${0})", 35 | "building_intro_LMG": "Light Machine Gun: longer range, normal damage ($${0})", 36 | "building_intro_HMG": "Heavy Machine Gun: fast shoot, greater damage, normal range ($${0})", 37 | "building_intro_laser_gun": "Laser gun: greater damage, 100% hit ($${0})", 38 | "click_to_build": "Left click to build ${0} ($${1})", 39 | "upgrade": "Upgrade ${0} to level ${1} , cost $${2}。", 40 | "sell": "Sell ${0} for $${1}", 41 | "upgrade_success": "Upgrade success! ${0} upgrades to level ${1}. Next upgrade will take $${2}.", 42 | "monster_info": "Monster: Life ${0}, Shield ${1}, Speed ${2}, Damage ${3}", 43 | "button_upgrade_text": "Upgrade", 44 | "button_sell_text": "Sell", 45 | "button_start_text": "Start", 46 | "button_restart_text": "Restart", 47 | "button_pause_text": "Pause", 48 | "button_continue_text": "Continue", 49 | "button_pause_desc_0": "Pause the game", 50 | "button_pause_desc_1": "Resume the game", 51 | "blocked": "Can't build here, it will block the way from entrance to exit!", 52 | "monster_be_blocked": "Can't build here, some monster will be blocked!", 53 | "entrance_or_exit_be_blocked": "Can't build on the entrance or the exit!", 54 | "_": "ERROR" 55 | }; 56 | 57 | TD._t = TD.translate = function (k, args) { 58 | args = (typeof args == "object" && args.constructor == Array) ? args : []; 59 | var msg = this._msg_texts[k] || this._msg_texts["_"], 60 | i, 61 | l = args.length; 62 | for (i = 0; i < l; i++) { 63 | msg = msg.replace("${" + i + "}", args[i]); 64 | } 65 | 66 | return msg; 67 | }; 68 | 69 | 70 | }); // _TD.a.push end 71 | -------------------------------------------------------------------------------- /src/js/td-msg-zh.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | * Last Update: 2011/1/10 5:22:52 8 | */ 9 | 10 | 11 | // _TD.a.push begin 12 | _TD.a.push(function (TD) { 13 | 14 | TD._msg_texts = { 15 | "_cant_build": "不能在这儿修建", 16 | "_cant_pass": "怪物不能通过这儿", 17 | "entrance": "起点", 18 | "exit": "终点", 19 | "not_enough_money": "金钱不足,需要 $${0}!", 20 | "wave_info": "第 ${0} 波", 21 | "panel_money_title": "金钱: ", 22 | "panel_score_title": "积分: ", 23 | "panel_life_title": "生命: ", 24 | "panel_building_title": "建筑: ", 25 | "panel_monster_title": "怪物: ", 26 | "building_name_wall": "路障", 27 | "building_name_cannon": "炮台", 28 | "building_name_LMG": "轻机枪", 29 | "building_name_HMG": "重机枪", 30 | "building_name_laser_gun": "激光炮", 31 | "building_info": "${0}: 等级 ${1},攻击 ${2},速度 ${3},射程 ${4},战绩 ${5}", 32 | "building_info_wall": "${0}", 33 | "building_intro_wall": "路障 可以阻止怪物通过 ($${0})", 34 | "building_intro_cannon": "炮台 射程、杀伤力较为平衡 ($${0})", 35 | "building_intro_LMG": "轻机枪 射程较远,杀伤力一般 ($${0})", 36 | "building_intro_HMG": "重机枪 快速射击,威力较大,射程一般 ($${0})", 37 | "building_intro_laser_gun": "激光枪 伤害较大,命中率 100% ($${0})", 38 | "click_to_build": "左键点击建造 ${0} ($${1})", 39 | "upgrade": "升级 ${0} 到 ${1} 级,需花费 $${2}。", 40 | "sell": "出售 ${0},可获得 $${1}", 41 | "upgrade_success": "升级成功,${0} 已升级到 ${1} 级!下次升级需要 $${2}。", 42 | "monster_info": "怪物: 生命 ${0},防御 ${1},速度 ${2},伤害 ${3}", 43 | "button_upgrade_text": "升级", 44 | "button_sell_text": "出售", 45 | "button_start_text": "开始", 46 | "button_restart_text": "重新开始", 47 | "button_pause_text": "暂停", 48 | "button_continue_text": "继续", 49 | "button_pause_desc_0": "游戏暂停", 50 | "button_pause_desc_1": "游戏继续", 51 | "blocked": "不能在这儿修建建筑,起点与终点之间至少要有一条路可到达!", 52 | "monster_be_blocked": "不能在这儿修建建筑,有怪物被围起来了!", 53 | "entrance_or_exit_be_blocked": "不能在起点或终点处修建建筑!", 54 | "_": "ERROR" 55 | }; 56 | 57 | TD._t = TD.translate = function (k, args) { 58 | args = (typeof args == "object" && args.constructor == Array) ? args : []; 59 | var msg = this._msg_texts[k] || this._msg_texts["_"], 60 | i, 61 | l = args.length; 62 | for (i = 0; i < l; i++) { 63 | msg = msg.replace("${" + i + "}", args[i]); 64 | } 65 | 66 | return msg; 67 | }; 68 | 69 | 70 | }); // _TD.a.push end 71 | -------------------------------------------------------------------------------- /src/js/td-obj-building.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | * Last Update: 2011/1/10 5:22:52 8 | */ 9 | 10 | 11 | // _TD.a.push begin 12 | _TD.a.push(function (TD) { 13 | 14 | // building 对象的属性、方法。注意属性中不要有数组、对象等 15 | // 引用属性,否则多个实例的相关属性会发生冲突 16 | var building_obj = { 17 | _init: function (cfg) { 18 | this.is_selected = false; 19 | this.level = 0; 20 | this.killed = 0; // 当前建筑杀死了多少怪物 21 | this.target = null; 22 | 23 | cfg = cfg || {}; 24 | this.map = cfg.map || null; 25 | this.grid = cfg.grid || null; 26 | 27 | /** 28 | * 子弹类型,可以有以下类型: 29 | * 1:普通子弹 30 | * 2:激光类,发射后马上命中,暂未实现 31 | * 3:导弹类,击中后会爆炸,带来面攻击,暂未实现 32 | */ 33 | this.bullet_type = cfg.bullet_type || 1; 34 | 35 | /** 36 | * type 可能的值有: 37 | * "wall": 墙壁,没有攻击性 38 | * "cannon": 炮台 39 | * "LMG": 轻机枪 40 | * "HMG": 重机枪 41 | * "laser_gun": 激光枪 42 | * 43 | */ 44 | this.type = cfg.type; 45 | 46 | this.speed = cfg.speed; 47 | this.bullet_speed = cfg.bullet_speed; 48 | this.is_pre_building = !!cfg.is_pre_building; 49 | this.blink = this.is_pre_building; 50 | this.wait_blink = this._default_wait_blink = 20; 51 | this.is_weapon = (this.type != "wall"); // 墙等不可攻击的建筑此项为 false ,其余武器此项为 true 52 | 53 | var o = TD.getDefaultBuildingAttributes(this.type); 54 | TD.lang.mix(this, o); 55 | this.range_px = this.range * TD.grid_size; 56 | this.money = this.cost; // 购买、升级本建筑已花费的钱 57 | 58 | this.caculatePos(); 59 | }, 60 | 61 | /** 62 | * 升级本建筑需要的花费 63 | */ 64 | getUpgradeCost: function () { 65 | return Math.floor(this.money * 0.75); 66 | }, 67 | 68 | /** 69 | * 出售本建筑能得到多少钱 70 | */ 71 | getSellMoney: function () { 72 | return Math.floor(this.money * 0.5) || 1; 73 | }, 74 | 75 | /** 76 | * 切换选中 / 未选中状态 77 | */ 78 | toggleSelected: function () { 79 | this.is_selected = !this.is_selected; 80 | this.grid.hightLight(this.is_selected); // 高亮 81 | var _this = this; 82 | 83 | if (this.is_selected) { 84 | // 如果当前建筑被选中 85 | 86 | this.map.eachBuilding(function (obj) { 87 | obj.is_selected = obj == _this; 88 | }); 89 | // 取消另一个地图中选中建筑的选中状态 90 | ( 91 | this.map.is_main_map ? this.scene.panel_map : this.scene.map 92 | ).eachBuilding(function (obj) { 93 | obj.is_selected = false; 94 | obj.grid.hightLight(false); 95 | }); 96 | this.map.selected_building = this; 97 | 98 | if (!this.map.is_main_map) { 99 | // 在面版地图中选中了建筑,进入建筑模式 100 | this.scene.map.preBuild(this.type); 101 | } else { 102 | // 取消建筑模式 103 | this.scene.map.cancelPreBuild(); 104 | } 105 | 106 | } else { 107 | // 如果当前建筑切换为未选中状态 108 | 109 | if (this.map.selected_building == this) 110 | this.map.selected_building = null; 111 | 112 | if (!this.map.is_main_map) { 113 | // 取消建筑模式 114 | this.scene.map.cancelPreBuild(); 115 | } 116 | } 117 | 118 | // 如果是选中 / 取消选中主地图上的建筑,显示 / 隐藏对应的操作按钮 119 | if (this.map.is_main_map) { 120 | if (this.map.selected_building) { 121 | this.scene.panel.btn_upgrade.show(); 122 | this.scene.panel.btn_sell.show(); 123 | this.updateBtnDesc(); 124 | } else { 125 | this.scene.panel.btn_upgrade.hide(); 126 | this.scene.panel.btn_sell.hide(); 127 | } 128 | } 129 | }, 130 | 131 | /** 132 | * 生成、更新升级按钮的说明文字 133 | */ 134 | updateBtnDesc: function () { 135 | this.scene.panel.btn_upgrade.desc = TD._t( 136 | "upgrade", [ 137 | TD._t("building_name_" + this.type), 138 | this.level + 1, 139 | this.getUpgradeCost() 140 | ]); 141 | this.scene.panel.btn_sell.desc = TD._t( 142 | "sell", [ 143 | TD._t("building_name_" + this.type), 144 | this.getSellMoney() 145 | ]); 146 | }, 147 | 148 | /** 149 | * 将本建筑放置到一个格子中 150 | * @param grid {Element} 指定格子 151 | */ 152 | locate: function (grid) { 153 | this.grid = grid; 154 | this.map = grid.map; 155 | this.cx = this.grid.cx; 156 | this.cy = this.grid.cy; 157 | this.x = this.grid.x; 158 | this.y = this.grid.y; 159 | this.x2 = this.grid.x2; 160 | this.y2 = this.grid.y2; 161 | this.width = this.grid.width; 162 | this.height = this.grid.height; 163 | 164 | this.px = this.x + 0.5; 165 | this.py = this.y + 0.5; 166 | 167 | this.wait_blink = this._default_wait_blink; 168 | this._fire_wait = Math.floor(Math.max(2 / (this.speed * TD.global_speed), 1)); 169 | this._fire_wait2 = this._fire_wait; 170 | 171 | }, 172 | 173 | /** 174 | * 将本建筑彻底删除 175 | */ 176 | remove: function () { 177 | // TD.log("remove building #" + this.id + "."); 178 | if (this.grid && this.grid.building && this.grid.building == this) 179 | this.grid.building = null; 180 | this.hide(); 181 | this.del(); 182 | }, 183 | 184 | /** 185 | * 寻找一个目标(怪物) 186 | */ 187 | findTaget: function () { 188 | if (!this.is_weapon || this.is_pre_building || !this.grid) return; 189 | 190 | var cx = this.cx, cy = this.cy, 191 | range2 = Math.pow(this.range_px, 2); 192 | 193 | // 如果当前建筑有目标,并且目标还是有效的,并且目标仍在射程内 194 | if (this.target && this.target.is_valid && 195 | Math.pow(this.target.cx - cx, 2) + Math.pow(this.target.cy - cy, 2) <= range2) 196 | return; 197 | 198 | // 在进入射程的怪物中寻找新的目标 199 | this.target = TD.lang.any( 200 | TD.lang.rndSort(this.map.monsters), // 将怪物随机排序 201 | function (obj) { 202 | return Math.pow(obj.cx - cx, 2) + Math.pow(obj.cy - cy, 2) <= range2; 203 | }); 204 | }, 205 | 206 | /** 207 | * 取得目标的坐标(相对于地图左上角) 208 | */ 209 | getTargetPosition: function () { 210 | if (!this.target) { 211 | // 以 entrance 为目标 212 | var grid = this.map.is_main_map ? this.map.entrance : this.grid; 213 | return [grid.cx, grid.cy]; 214 | } 215 | return [this.target.cx, this.target.cy]; 216 | }, 217 | 218 | /** 219 | * 向自己的目标开火 220 | */ 221 | fire: function () { 222 | if (!this.target || !this.target.is_valid) return; 223 | 224 | if (this.type == "laser_gun") { 225 | // 如果是激光枪,目标立刻被击中 226 | this.target.beHit(this, this.damage); 227 | return; 228 | } 229 | 230 | var muzzle = this.muzzle || [this.cx, this.cy], // 炮口的位置 231 | cx = muzzle[0], 232 | cy = muzzle[1]; 233 | 234 | new TD.Bullet(null, { 235 | building: this, 236 | damage: this.damage, 237 | target: this.target, 238 | speed: this.bullet_speed, 239 | x: cx, 240 | y: cy 241 | }); 242 | }, 243 | 244 | tryToFire: function () { 245 | if (!this.is_weapon || !this.target) 246 | return; 247 | 248 | this._fire_wait--; 249 | if (this._fire_wait > 0) { 250 | // return; 251 | } else if (this._fire_wait < 0) { 252 | this._fire_wait = this._fire_wait2; 253 | } else { 254 | this.fire(); 255 | } 256 | }, 257 | 258 | _upgrade2: function (k) { 259 | if (!this._upgrade_records[k]) 260 | this._upgrade_records[k] = this[k]; 261 | var v = this._upgrade_records[k], 262 | mk = "max_" + k, 263 | uk = "_upgrade_rule_" + k, 264 | uf = this[uk] || TD.default_upgrade_rule; 265 | if (!v || isNaN(v)) return; 266 | 267 | v = uf(this.level, v); 268 | if (this[mk] && !isNaN(this[mk]) && this[mk] < v) 269 | v = this[mk]; 270 | this._upgrade_records[k] = v; 271 | this[k] = Math.floor(v); 272 | }, 273 | 274 | /** 275 | * 升级建筑 276 | */ 277 | upgrade: function () { 278 | if (!this._upgrade_records) 279 | this._upgrade_records = {}; 280 | 281 | var attrs = [ 282 | // 可升级的变量 283 | "damage", "range", "speed", "life", "shield" 284 | ], i, l = attrs.length; 285 | for (i = 0; i < l; i++) 286 | this._upgrade2(attrs[i]); 287 | this.level++; 288 | this.range_px = this.range * TD.grid_size; 289 | }, 290 | 291 | tryToUpgrade: function (btn) { 292 | var cost = this.getUpgradeCost(), 293 | msg = ""; 294 | if (cost > TD.money) { 295 | msg = TD._t("not_enough_money", [cost]); 296 | } else { 297 | TD.money -= cost; 298 | this.money += cost; 299 | this.upgrade(); 300 | msg = TD._t("upgrade_success", [ 301 | TD._t("building_name_" + this.type), this.level, 302 | this.getUpgradeCost() 303 | ]); 304 | } 305 | 306 | this.updateBtnDesc(); 307 | this.scene.panel.balloontip.msg(msg, btn); 308 | }, 309 | 310 | tryToSell: function () { 311 | if (!this.is_valid) return; 312 | 313 | TD.money += this.getSellMoney(); 314 | this.grid.removeBuilding(); 315 | this.is_valid = false; 316 | this.map.selected_building = null; 317 | this.map.select_hl.hide(); 318 | this.map.checkHasWeapon(); 319 | this.scene.panel.btn_upgrade.hide(); 320 | this.scene.panel.btn_sell.hide(); 321 | this.scene.panel.balloontip.hide(); 322 | }, 323 | 324 | step: function () { 325 | if (this.blink) { 326 | this.wait_blink--; 327 | if (this.wait_blink < -this._default_wait_blink) 328 | this.wait_blink = this._default_wait_blink; 329 | } 330 | 331 | this.findTaget(); 332 | this.tryToFire(); 333 | }, 334 | 335 | render: function () { 336 | if (!this.is_visiable || this.wait_blink < 0) return; 337 | 338 | var ctx = TD.ctx; 339 | 340 | TD.renderBuilding(this); 341 | 342 | if ( 343 | this.map.is_main_map && 344 | ( 345 | this.is_selected || (this.is_pre_building) || 346 | this.map.show_all_ranges 347 | ) && 348 | this.is_weapon && this.range > 0 && this.grid 349 | ) { 350 | // 画射程 351 | ctx.lineWidth = _TD.retina; 352 | ctx.fillStyle = "rgba(187, 141, 32, 0.15)"; 353 | ctx.strokeStyle = "#bb8d20"; 354 | ctx.beginPath(); 355 | ctx.arc(this.cx, this.cy, this.range_px, 0, Math.PI * 2, true); 356 | ctx.closePath(); 357 | ctx.fill(); 358 | ctx.stroke(); 359 | } 360 | 361 | if (this.type == "laser_gun" && this.target && this.target.is_valid) { 362 | // 画激光 363 | ctx.lineWidth = 3 * _TD.retina; 364 | ctx.strokeStyle = "rgba(50, 50, 200, 0.5)"; 365 | ctx.beginPath(); 366 | ctx.moveTo(this.cx, this.cy); 367 | ctx.lineTo(this.target.cx, this.target.cy); 368 | ctx.closePath(); 369 | ctx.stroke(); 370 | ctx.lineWidth = _TD.retina; 371 | ctx.strokeStyle = "rgba(150, 150, 255, 0.5)"; 372 | ctx.beginPath(); 373 | ctx.lineTo(this.cx, this.cy); 374 | ctx.closePath(); 375 | ctx.stroke(); 376 | } 377 | }, 378 | 379 | onEnter: function () { 380 | if (this.is_pre_building) return; 381 | 382 | var msg = "建筑工事"; 383 | if (this.map.is_main_map) { 384 | msg = TD._t("building_info" + (this.type == "wall" ? "_wall" : ""), [TD._t("building_name_" + this.type), this.level, this.damage, this.speed, this.range, this.killed]); 385 | } else { 386 | msg = TD._t("building_intro_" + this.type, [TD.getDefaultBuildingAttributes(this.type).cost]); 387 | } 388 | 389 | this.scene.panel.balloontip.msg(msg, this.grid); 390 | }, 391 | 392 | onOut: function () { 393 | if (this.scene.panel.balloontip.el == this.grid) { 394 | this.scene.panel.balloontip.hide(); 395 | } 396 | }, 397 | 398 | onClick: function () { 399 | if (this.is_pre_building || this.scene.state != 1) return; 400 | this.toggleSelected(); 401 | } 402 | }; 403 | 404 | /** 405 | * @param id {String} 406 | * @param cfg {object} 配置对象 407 | * 至少需要包含以下项: 408 | * { 409 | * type: 建筑类型,可选的值有 410 | * "wall" 411 | * "cannon" 412 | * "LMG" 413 | * "HMG" 414 | * "laser_gun" 415 | * } 416 | */ 417 | TD.Building = function (id, cfg) { 418 | cfg.on_events = ["enter", "out", "click"]; 419 | var building = new TD.Element(id, cfg); 420 | TD.lang.mix(building, building_obj); 421 | building._init(cfg); 422 | 423 | return building; 424 | }; 425 | 426 | 427 | // bullet 对象的属性、方法。注意属性中不要有数组、对象等 428 | // 引用属性,否则多个实例的相关属性会发生冲突 429 | var bullet_obj = { 430 | _init: function (cfg) { 431 | cfg = cfg || {}; 432 | 433 | this.speed = cfg.speed; 434 | this.damage = cfg.damage; 435 | this.target = cfg.target; 436 | this.cx = cfg.x; 437 | this.cy = cfg.y; 438 | this.r = cfg.r || Math.max(Math.log(this.damage), 2); 439 | if (this.r < 1) this.r = 1; 440 | if (this.r > 6) this.r = 6; 441 | 442 | this.building = cfg.building || null; 443 | this.map = cfg.map || this.building.map; 444 | this.type = cfg.type || 1; 445 | this.color = cfg.color || "#000"; 446 | 447 | this.map.bullets.push(this); 448 | this.addToScene(this.map.scene, 1, 6); 449 | 450 | if (this.type == 1) { 451 | this.caculate(); 452 | } 453 | }, 454 | 455 | /** 456 | * 计算子弹的一些数值 457 | */ 458 | caculate: function () { 459 | var sx, sy, c, 460 | tx = this.target.cx, 461 | ty = this.target.cy, 462 | speed; 463 | sx = tx - this.cx; 464 | sy = ty - this.cy; 465 | c = Math.sqrt(Math.pow(sx, 2) + Math.pow(sy, 2)); 466 | speed = 20 * this.speed * TD.global_speed; 467 | this.vx = sx * speed / c; 468 | this.vy = sy * speed / c; 469 | }, 470 | 471 | /** 472 | * 检查当前子弹是否已超出地图范围 473 | */ 474 | checkOutOfMap: function () { 475 | this.is_valid = !( 476 | this.cx < this.map.x || 477 | this.cx > this.map.x2 || 478 | this.cy < this.map.y || 479 | this.cy > this.map.y2 480 | ); 481 | 482 | return !this.is_valid; 483 | }, 484 | 485 | /** 486 | * 检查当前子弹是否击中了怪物 487 | */ 488 | checkHit: function () { 489 | var cx = this.cx, 490 | cy = this.cy, 491 | r = this.r * _TD.retina, 492 | monster = this.map.anyMonster(function (obj) { 493 | return Math.pow(obj.cx - cx, 2) + Math.pow(obj.cy - cy, 2) <= Math.pow(obj.r + r, 2) * 2; 494 | }); 495 | 496 | if (monster) { 497 | // 击中的怪物 498 | monster.beHit(this.building, this.damage); 499 | this.is_valid = false; 500 | 501 | // 子弹小爆炸效果 502 | TD.Explode(this.id + "-explode", { 503 | cx: this.cx, 504 | cy: this.cy, 505 | r: this.r, 506 | step_level: this.step_level, 507 | render_level: this.render_level, 508 | color: this.color, 509 | scene: this.map.scene, 510 | time: 0.2 511 | }); 512 | 513 | return true; 514 | } 515 | return false; 516 | }, 517 | 518 | step: function () { 519 | if (this.checkOutOfMap() || this.checkHit()) return; 520 | 521 | this.cx += this.vx; 522 | this.cy += this.vy; 523 | }, 524 | 525 | render: function () { 526 | var ctx = TD.ctx; 527 | ctx.fillStyle = this.color; 528 | ctx.beginPath(); 529 | ctx.arc(this.cx, this.cy, this.r, 0, Math.PI * 2, true); 530 | ctx.closePath(); 531 | ctx.fill(); 532 | } 533 | }; 534 | 535 | /** 536 | * @param id {String} 配置对象 537 | * @param cfg {Object} 配置对象 538 | * 至少需要包含以下项: 539 | * { 540 | * x: 子弹发出的位置 541 | * y: 子弹发出的位置 542 | * speed: 543 | * damage: 544 | * target: 目标,一个 monster 对象 545 | * building: 所属的建筑 546 | * } 547 | * 子弹类型,可以有以下类型: 548 | * 1:普通子弹 549 | * 2:激光类,发射后马上命中 550 | * 3:导弹类,击中后会爆炸,带来面攻击 551 | */ 552 | TD.Bullet = function (id, cfg) { 553 | var bullet = new TD.Element(id, cfg); 554 | TD.lang.mix(bullet, bullet_obj); 555 | bullet._init(cfg); 556 | 557 | return bullet; 558 | }; 559 | 560 | }); // _TD.a.push end 561 | 562 | -------------------------------------------------------------------------------- /src/js/td-obj-grid.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | * Last Update: 2011/1/10 5:22:52 8 | */ 9 | 10 | 11 | // _TD.a.push begin 12 | _TD.a.push(function (TD) { 13 | 14 | // grid 对象的属性、方法。注意属性中不要有数组、对象等 15 | // 引用属性,否则多个实例的相关属性会发生冲突 16 | var grid_obj = { 17 | _init: function (cfg) { 18 | cfg = cfg || {}; 19 | this.map = cfg.map; 20 | this.scene = this.map.scene; 21 | this.mx = cfg.mx; // 在 map 中的格子坐标 22 | this.my = cfg.my; 23 | this.width = TD.grid_size; 24 | this.height = TD.grid_size; 25 | this.is_entrance = this.is_exit = false; 26 | this.passable_flag = 1; // 0: 不可通过; 1: 可通过 27 | this.build_flag = 1;// 0: 不可修建; 1: 可修建; 2: 已修建 28 | this.building = null; 29 | this.caculatePos(); 30 | }, 31 | 32 | /** 33 | * 根据 map 位置及本 grid 的 (mx, my) ,计算格子的位置 34 | */ 35 | caculatePos: function () { 36 | this.x = this.map.x + this.mx * TD.grid_size; 37 | this.y = this.map.y + this.my * TD.grid_size; 38 | this.x2 = this.x + TD.grid_size; 39 | this.y2 = this.y + TD.grid_size; 40 | this.cx = Math.floor(this.x + TD.grid_size / 2); 41 | this.cy = Math.floor(this.y + TD.grid_size / 2); 42 | }, 43 | 44 | /** 45 | * 检查如果在当前格子建东西,是否会导致起点与终点被阻塞 46 | */ 47 | checkBlock: function () { 48 | if (this.is_entrance || this.is_exit) { 49 | this._block_msg = TD._t("entrance_or_exit_be_blocked"); 50 | return true; 51 | } 52 | 53 | var is_blocked, 54 | _this = this, 55 | fw = new TD.FindWay( 56 | this.map.grid_x, this.map.grid_y, 57 | this.map.entrance.mx, this.map.entrance.my, 58 | this.map.exit.mx, this.map.exit.my, 59 | function (x, y) { 60 | return !(x == _this.mx && y == _this.my) && _this.map.checkPassable(x, y); 61 | } 62 | ); 63 | 64 | is_blocked = fw.is_blocked; 65 | 66 | if (!is_blocked) { 67 | is_blocked = !!this.map.anyMonster(function (obj) { 68 | return obj.chkIfBlocked(_this.mx, _this.my); 69 | }); 70 | if (is_blocked) 71 | this._block_msg = TD._t("monster_be_blocked"); 72 | } else { 73 | this._block_msg = TD._t("blocked"); 74 | } 75 | 76 | return is_blocked; 77 | }, 78 | 79 | /** 80 | * 购买建筑 81 | * @param building_type {String} 82 | */ 83 | buyBuilding: function (building_type) { 84 | var cost = TD.getDefaultBuildingAttributes(building_type).cost || 0; 85 | if (TD.money >= cost) { 86 | TD.money -= cost; 87 | this.addBuilding(building_type); 88 | } else { 89 | TD.log(TD._t("not_enough_money", [cost])); 90 | this.scene.panel.balloontip.msg(TD._t("not_enough_money", [cost]), this); 91 | } 92 | }, 93 | 94 | /** 95 | * 在当前格子添加指定类型的建筑 96 | * @param building_type {String} 97 | */ 98 | addBuilding: function (building_type) { 99 | if (this.building) { 100 | // 如果当前格子已经有建筑,先将其移除 101 | this.removeBuilding(); 102 | } 103 | 104 | var building = new TD.Building("building-" + building_type + "-" + TD.lang.rndStr(), { 105 | type: building_type, 106 | step_level: this.step_level, 107 | render_level: this.render_level 108 | }); 109 | building.locate(this); 110 | 111 | this.scene.addElement(building, this.step_level, this.render_level + 1); 112 | this.map.buildings.push(building); 113 | this.building = building; 114 | this.build_flag = 2; 115 | this.map.checkHasWeapon(); 116 | if (this.map.pre_building) 117 | this.map.pre_building.hide(); 118 | }, 119 | 120 | /** 121 | * 移除当前格子的建筑 122 | */ 123 | removeBuilding: function () { 124 | if (this.build_flag == 2) 125 | this.build_flag = 1; 126 | if (this.building) 127 | this.building.remove(); 128 | this.building = null; 129 | }, 130 | 131 | /** 132 | * 在当前建筑添加一个怪物 133 | * @param monster 134 | */ 135 | addMonster: function (monster) { 136 | monster.beAddToGrid(this); 137 | this.map.monsters.push(monster); 138 | monster.start(); 139 | }, 140 | 141 | /** 142 | * 高亮当前格子 143 | * @param show {Boolean} 144 | */ 145 | hightLight: function (show) { 146 | this.map.select_hl[show ? "show" : "hide"](this); 147 | }, 148 | 149 | render: function () { 150 | var ctx = TD.ctx, 151 | px = this.x + 0.5, 152 | py = this.y + 0.5; 153 | 154 | //if (this.map.is_main_map) { 155 | //ctx.drawImage(this.map.res, 156 | //0, 0, 32, 32, this.x, this.y, 32, 32 157 | //); 158 | //} 159 | 160 | if (this.is_hover) { 161 | ctx.fillStyle = "rgba(255, 255, 200, 0.2)"; 162 | ctx.beginPath(); 163 | ctx.fillRect(px, py, this.width, this.height); 164 | ctx.closePath(); 165 | ctx.fill(); 166 | } 167 | 168 | if (this.passable_flag == 0) { 169 | // 不可通过 170 | ctx.fillStyle = "#fcc"; 171 | ctx.beginPath(); 172 | ctx.fillRect(px, py, this.width, this.height); 173 | ctx.closePath(); 174 | ctx.fill(); 175 | } 176 | 177 | /** 178 | * 画入口及出口 179 | */ 180 | if (this.is_entrance || this.is_exit) { 181 | ctx.lineWidth = 1; 182 | ctx.fillStyle = "#ccc"; 183 | ctx.beginPath(); 184 | ctx.fillRect(px, py, this.width, this.height); 185 | ctx.closePath(); 186 | ctx.fill(); 187 | 188 | ctx.strokeStyle = "#666"; 189 | ctx.fillStyle = this.is_entrance ? "#fff" : "#666"; 190 | ctx.beginPath(); 191 | ctx.arc(this.cx, this.cy, TD.grid_size * 0.325, 0, Math.PI * 2, true); 192 | ctx.closePath(); 193 | ctx.fill(); 194 | ctx.stroke(); 195 | } 196 | 197 | ctx.strokeStyle = "#eee"; 198 | ctx.lineWidth = 1; 199 | ctx.beginPath(); 200 | ctx.strokeRect(px, py, this.width, this.height); 201 | ctx.closePath(); 202 | ctx.stroke(); 203 | }, 204 | 205 | /** 206 | * 鼠标进入当前格子事件 207 | */ 208 | onEnter: function () { 209 | if (this.map.is_main_map && TD.mode == "build") { 210 | if (this.build_flag == 1) { 211 | this.map.pre_building.show(); 212 | this.map.pre_building.locate(this); 213 | } else { 214 | this.map.pre_building.hide(); 215 | } 216 | } else if (this.map.is_main_map) { 217 | var msg = ""; 218 | if (this.is_entrance) { 219 | msg = TD._t("entrance"); 220 | } else if (this.is_exit) { 221 | msg = TD._t("exit"); 222 | } else if (this.passable_flag == 0) { 223 | msg = TD._t("_cant_pass"); 224 | } else if (this.build_flag == 0) { 225 | msg = TD._t("_cant_build"); 226 | } 227 | 228 | if (msg) { 229 | this.scene.panel.balloontip.msg(msg, this); 230 | } 231 | } 232 | }, 233 | 234 | /** 235 | * 鼠标移出当前格子事件 236 | */ 237 | onOut: function () { 238 | // 如果当前气球提示指向本格子,将其隐藏 239 | if (this.scene.panel.balloontip.el == this) { 240 | this.scene.panel.balloontip.hide(); 241 | } 242 | }, 243 | 244 | /** 245 | * 鼠标点击了当前格子事件 246 | */ 247 | onClick: function () { 248 | if (this.scene.state != 1) return; 249 | 250 | if (TD.mode == "build" && this.map.is_main_map && !this.building) { 251 | // 如果处于建设模式下,并且点击在主地图的空格子上,则尝试建设指定建筑 252 | if (this.checkBlock()) { 253 | // 起点与终点之间被阻塞,不能修建 254 | this.scene.panel.balloontip.msg(this._block_msg, this); 255 | } else { 256 | // 购买建筑 257 | this.buyBuilding(this.map.pre_building.type); 258 | } 259 | } else if (!this.building && this.map.selected_building) { 260 | // 取消选中建筑 261 | this.map.selected_building.toggleSelected(); 262 | this.map.selected_building = null; 263 | } 264 | } 265 | }; 266 | 267 | /** 268 | * @param id {String} 269 | * @param cfg {object} 配置对象 270 | * 至少需要包含以下项: 271 | * { 272 | * mx: 在 map 格子中的横向坐标, 273 | * my: 在 map 格子中的纵向坐标, 274 | * map: 属于哪个 map, 275 | * } 276 | */ 277 | TD.Grid = function (id, cfg) { 278 | cfg.on_events = ["enter", "out", "click"]; 279 | 280 | var grid = new TD.Element(id, cfg); 281 | TD.lang.mix(grid, grid_obj); 282 | grid._init(cfg); 283 | 284 | return grid; 285 | }; 286 | 287 | }); // _TD.a.push end 288 | -------------------------------------------------------------------------------- /src/js/td-obj-map.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | * Last Update: 2011/1/10 5:22:52 8 | */ 9 | 10 | 11 | // _TD.a.push begin 12 | _TD.a.push(function (TD) { 13 | 14 | var _default_wait_clearInvalidElements = 20; 15 | 16 | // map 对象的属性、方法。注意属性中不要有数组、对象等 17 | // 引用属性,否则多个实例的相关属性会发生冲突 18 | var map_obj = { 19 | _init: function (cfg) { 20 | cfg = cfg || {}; 21 | this.grid_x = cfg.grid_x || 10; 22 | this.grid_y = cfg.grid_y || 10; 23 | this.x = cfg.x || 0; 24 | this.y = cfg.y || 0; 25 | this.width = this.grid_x * TD.grid_size; 26 | this.height = this.grid_y * TD.grid_size; 27 | this.x2 = this.x + this.width; 28 | this.y2 = this.y + this.height; 29 | this.grids = []; 30 | this.entrance = this.exit = null; 31 | this.buildings = []; 32 | this.monsters = []; 33 | this.bullets = []; 34 | this.scene = cfg.scene; 35 | this.is_main_map = !!cfg.is_main_map; 36 | this.select_hl = TD.MapSelectHighLight(this.id + "-hl", { 37 | map: this 38 | }); 39 | this.select_hl.addToScene(this.scene, 1, 9); 40 | this.selected_building = null; 41 | this._wait_clearInvalidElements = _default_wait_clearInvalidElements; 42 | this._wait_add_monsters = 0; 43 | this._wait_add_monsters_arr = []; 44 | if (this.is_main_map) { 45 | this.mmm = new MainMapMask(this.id + "-mmm", { 46 | map: this 47 | }); 48 | this.mmm.addToScene(this.scene, 1, 7); 49 | 50 | } 51 | 52 | // 下面添加相应的格子 53 | var i, l = this.grid_x * this.grid_y, 54 | grid_data = cfg["grid_data"] || [], 55 | d, grid; 56 | 57 | for (i = 0; i < l; i++) { 58 | d = grid_data[i] || {}; 59 | d.mx = i % this.grid_x; 60 | d.my = Math.floor(i / this.grid_x); 61 | d.map = this; 62 | d.step_level = this.step_level; 63 | d.render_level = this.render_level; 64 | grid = new TD.Grid(this.id + "-grid-" + d.mx + "-" + d.my, d); 65 | this.grids.push(grid); 66 | } 67 | 68 | if (cfg.entrance && cfg.exit && !TD.lang.arrayEqual(cfg.entrance, cfg.exit)) { 69 | this.entrance = this.getGrid(cfg.entrance[0], cfg.entrance[1]); 70 | this.entrance.is_entrance = true; 71 | this.exit = this.getGrid(cfg.exit[0], cfg.exit[1]); 72 | this.exit.is_exit = true; 73 | } 74 | 75 | var _this = this; 76 | if (cfg.grids_cfg) { 77 | TD.lang.each(cfg.grids_cfg, function (obj) { 78 | var grid = _this.getGrid(obj.pos[0], obj.pos[1]); 79 | if (!grid) return; 80 | if (!isNaN(obj.passable_flag)) 81 | grid.passable_flag = obj.passable_flag; 82 | if (!isNaN(obj.build_flag)) 83 | grid.build_flag = obj.build_flag; 84 | if (obj.building) { 85 | grid.addBuilding(obj.building); 86 | } 87 | }); 88 | } 89 | }, 90 | 91 | /** 92 | * 检查地图中是否有武器(具备攻击性的建筑) 93 | * 因为第一波怪物只有在地图上有了第一件武器后才会出现 94 | */ 95 | checkHasWeapon: function () { 96 | this.has_weapon = (this.anyBuilding(function (obj) { 97 | return obj.is_weapon; 98 | }) != null); 99 | }, 100 | 101 | /** 102 | * 取得指定位置的格子对象 103 | * @param mx {Number} 地图上的坐标 x 104 | * @param my {Number} 地图上的坐标 y 105 | */ 106 | getGrid: function (mx, my) { 107 | var p = my * this.grid_x + mx; 108 | return this.grids[p]; 109 | }, 110 | 111 | anyMonster: function (f) { 112 | return TD.lang.any(this.monsters, f); 113 | }, 114 | anyBuilding: function (f) { 115 | return TD.lang.any(this.buildings, f); 116 | }, 117 | anyBullet: function (f) { 118 | return TD.lang.any(this.bullets, f); 119 | }, 120 | eachBuilding: function (f) { 121 | TD.lang.each(this.buildings, f); 122 | }, 123 | eachMonster: function (f) { 124 | TD.lang.each(this.monsters, f); 125 | }, 126 | eachBullet: function (f) { 127 | TD.lang.each(this.bullets, f); 128 | }, 129 | 130 | /** 131 | * 预建设 132 | * @param building_type {String} 133 | */ 134 | preBuild: function (building_type) { 135 | TD.mode = "build"; 136 | if (this.pre_building) { 137 | this.pre_building.remove(); 138 | } 139 | 140 | this.pre_building = new TD.Building(this.id + "-" + "pre-building-" + TD.lang.rndStr(), { 141 | type: building_type, 142 | map: this, 143 | is_pre_building: true 144 | }); 145 | this.scene.addElement(this.pre_building, 1, this.render_level + 1); 146 | //this.show_all_ranges = true; 147 | }, 148 | 149 | /** 150 | * 退出预建设状态 151 | */ 152 | cancelPreBuild: function () { 153 | TD.mode = "normal"; 154 | if (this.pre_building) { 155 | this.pre_building.remove(); 156 | } 157 | //this.show_all_ranges = false; 158 | }, 159 | 160 | /** 161 | * 清除地图上无效的元素 162 | */ 163 | clearInvalidElements: function () { 164 | if (this._wait_clearInvalidElements > 0) { 165 | this._wait_clearInvalidElements--; 166 | return; 167 | } 168 | this._wait_clearInvalidElements = _default_wait_clearInvalidElements; 169 | 170 | var a = []; 171 | TD.lang.shift(this.buildings, function (obj) { 172 | if (obj.is_valid) 173 | a.push(obj); 174 | }); 175 | this.buildings = a; 176 | 177 | a = []; 178 | TD.lang.shift(this.monsters, function (obj) { 179 | if (obj.is_valid) 180 | a.push(obj); 181 | }); 182 | this.monsters = a; 183 | 184 | a = []; 185 | TD.lang.shift(this.bullets, function (obj) { 186 | if (obj.is_valid) 187 | a.push(obj); 188 | }); 189 | this.bullets = a; 190 | }, 191 | 192 | /** 193 | * 在地图的入口处添加一个怪物 194 | * @param monster 可以是数字,也可以是 monster 对象 195 | */ 196 | addMonster: function (monster) { 197 | if (!this.entrance) return; 198 | if (typeof monster == "number") { 199 | monster = new TD.Monster(null, { 200 | idx: monster, 201 | difficulty: TD.difficulty, 202 | step_level: this.step_level, 203 | render_level: this.render_level + 2 204 | }); 205 | } 206 | this.entrance.addMonster(monster); 207 | }, 208 | 209 | /** 210 | * 在地图的入口处添加 n 个怪物 211 | * @param n 212 | * @param monster 213 | */ 214 | addMonsters: function (n, monster) { 215 | this._wait_add_monsters = n; 216 | this._wait_add_monsters_objidx = monster; 217 | }, 218 | 219 | /** 220 | * arr 的格式形如: 221 | * [[1, 0], [2, 5], [3, 6], [10, 4]...] 222 | */ 223 | addMonsters2: function (arr) { 224 | this._wait_add_monsters_arr = arr; 225 | }, 226 | 227 | /** 228 | * 检查地图的指定格子是否可通过 229 | * @param mx {Number} 230 | * @param my {Number} 231 | */ 232 | checkPassable: function (mx, my) { 233 | var grid = this.getGrid(mx, my); 234 | return (grid != null && grid.passable_flag == 1 && grid.build_flag != 2); 235 | }, 236 | 237 | step: function () { 238 | this.clearInvalidElements(); 239 | 240 | if (this._wait_add_monsters > 0) { 241 | this.addMonster(this._wait_add_monsters_objidx); 242 | this._wait_add_monsters--; 243 | } else if (this._wait_add_monsters_arr.length > 0) { 244 | var a = this._wait_add_monsters_arr.shift(); 245 | this.addMonsters(a[0], a[1]); 246 | } 247 | }, 248 | 249 | render: function () { 250 | var ctx = TD.ctx; 251 | ctx.strokeStyle = "#99a"; 252 | ctx.lineWidth = _TD.retina; 253 | ctx.beginPath(); 254 | ctx.strokeRect(this.x + 0.5, this.y + 0.5, this.width, this.height); 255 | ctx.closePath(); 256 | ctx.stroke(); 257 | }, 258 | 259 | /** 260 | * 鼠标移出地图事件 261 | */ 262 | onOut: function () { 263 | if (this.is_main_map && this.pre_building) 264 | this.pre_building.hide(); 265 | } 266 | }; 267 | 268 | /** 269 | * @param id {String} 配置对象 270 | * @param cfg {Object} 配置对象 271 | * 至少需要包含以下项: 272 | * { 273 | * grid_x: 宽度(格子), 274 | * grid_y: 高度(格子), 275 | * scene: 属于哪个场景, 276 | * } 277 | */ 278 | TD.Map = function (id, cfg) { 279 | // map 目前只需要监听 out 事件 280 | // 虽然只需要监听 out 事件,但同时也需要监听 enter ,因为如果 281 | // 没有 enter ,out 将永远不会被触发 282 | cfg.on_events = ["enter", "out"]; 283 | var map = new TD.Element(id, cfg); 284 | TD.lang.mix(map, map_obj); 285 | map._init(cfg); 286 | 287 | return map; 288 | }; 289 | 290 | 291 | /** 292 | * 地图选中元素高亮边框对象 293 | */ 294 | var map_selecthl_obj = { 295 | _init: function (cfg) { 296 | this.map = cfg.map; 297 | this.width = TD.grid_size + 2; 298 | this.height = TD.grid_size + 2; 299 | this.is_visiable = false; 300 | }, 301 | show: function (grid) { 302 | this.x = grid.x; 303 | this.y = grid.y; 304 | this.is_visiable = true; 305 | }, 306 | render: function () { 307 | var ctx = TD.ctx; 308 | ctx.lineWidth = 2; 309 | ctx.strokeStyle = "#f93"; 310 | ctx.beginPath(); 311 | ctx.strokeRect(this.x, this.y, this.width - 1, this.height - 1); 312 | ctx.closePath(); 313 | ctx.stroke(); 314 | } 315 | }; 316 | 317 | /** 318 | * 地图选中的高亮框 319 | * @param id {String} 至少需要包含 320 | * @param cfg {Object} 至少需要包含 321 | * { 322 | * map: map 对象 323 | * } 324 | */ 325 | TD.MapSelectHighLight = function (id, cfg) { 326 | var map_selecthl = new TD.Element(id, cfg); 327 | TD.lang.mix(map_selecthl, map_selecthl_obj); 328 | map_selecthl._init(cfg); 329 | 330 | return map_selecthl; 331 | }; 332 | 333 | 334 | var mmm_obj = { 335 | _init: function (cfg) { 336 | this.map = cfg.map; 337 | 338 | this.x1 = this.map.x; 339 | this.y1 = this.map.y; 340 | this.x2 = this.map.x2 + 1; 341 | this.y2 = this.map.y2 + 1; 342 | this.w = this.map.scene.stage.width; 343 | this.h = this.map.scene.stage.height; 344 | this.w2 = this.w - this.x2; 345 | this.h2 = this.h - this.y2; 346 | }, 347 | render: function () { 348 | var ctx = TD.ctx; 349 | /*ctx.clearRect(0, 0, this.x1, this.h); 350 | ctx.clearRect(0, 0, this.w, this.y1); 351 | ctx.clearRect(0, this.y2, this.w, this.h2); 352 | ctx.clearRect(this.x2, 0, this.w2, this.h2);*/ 353 | 354 | ctx.fillStyle = "#fff"; 355 | ctx.beginPath(); 356 | ctx.fillRect(0, 0, this.x1, this.h); 357 | ctx.fillRect(0, 0, this.w, this.y1); 358 | ctx.fillRect(0, this.y2, this.w, this.h2); 359 | ctx.fillRect(this.x2, 0, this.w2, this.h); 360 | ctx.closePath(); 361 | ctx.fill(); 362 | 363 | } 364 | }; 365 | 366 | /** 367 | * 主地图外边的遮罩,用于遮住超出地图的射程等 368 | */ 369 | function MainMapMask(id, cfg) { 370 | var mmm = new TD.Element(id, cfg); 371 | TD.lang.mix(mmm, mmm_obj); 372 | mmm._init(cfg); 373 | 374 | return mmm; 375 | } 376 | 377 | }); // _TD.a.push end 378 | 379 | -------------------------------------------------------------------------------- /src/js/td-obj-monster.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | * Last Update: 2011/1/10 5:22:52 8 | */ 9 | 10 | 11 | // _TD.a.push begin 12 | _TD.a.push(function (TD) { 13 | 14 | // monster 对象的属性、方法。注意属性中不要有数组、对象等 15 | // 引用属性,否则多个实例的相关属性会发生冲突 16 | var monster_obj = { 17 | _init: function (cfg) { 18 | cfg = cfg || {}; 19 | this.is_monster = true; 20 | this.idx = cfg.idx || 1; 21 | this.difficulty = cfg.difficulty || 1.0; 22 | var attr = TD.getDefaultMonsterAttributes(this.idx); 23 | 24 | this.speed = Math.floor( 25 | (attr.speed + this.difficulty / 2) * (Math.random() * 0.5 + 0.75) 26 | ); 27 | if (this.speed < 1) this.speed = 1; 28 | if (this.speed > cfg.max_speed) this.speed = cfg.max_speed; 29 | 30 | this.life = this.life0 = Math.floor( 31 | attr.life * (this.difficulty + 1) * (Math.random() + 0.5) * 0.5 32 | ); 33 | if (this.life < 1) this.life = this.life0 = 1; 34 | 35 | this.shield = Math.floor(attr.shield + this.difficulty / 2); 36 | if (this.shield < 0) this.shield = 0; 37 | 38 | this.damage = Math.floor( 39 | (attr.damage || 1) * (Math.random() * 0.5 + 0.75) 40 | ); 41 | if (this.damage < 1) this.damage = 1; 42 | 43 | this.money = attr.money || Math.floor( 44 | Math.sqrt((this.speed + this.life) * (this.shield + 1) * this.damage) 45 | ); 46 | if (this.money < 1) this.money = 1; 47 | 48 | this.color = attr.color || TD.lang.rndRGB(); 49 | this.r = Math.floor(this.damage * 1.2) * _TD.retina; 50 | if (this.r < (4 * _TD.retina)) this.r = 4 * _TD.retina; 51 | if (this.r > TD.grid_size / 2 - (4 * _TD.retina)) this.r = TD.grid_size / 2 - (4 * _TD.retina); 52 | this.render = attr.render; 53 | 54 | this.grid = null; // 当前格子 55 | this.map = null; 56 | this.next_grid = null; 57 | this.way = []; 58 | this.toward = 2; // 默认面朝下方 59 | this._dx = 0; 60 | this._dy = 0; 61 | 62 | this.is_blocked = false; // 前进的道路是否被阻塞了 63 | }, 64 | caculatePos: function () { 65 | // if (!this.map) return; 66 | var r = this.r; 67 | this.x = this.cx - r; 68 | this.y = this.cy - r; 69 | this.x2 = this.cx + r; 70 | this.y2 = this.cy + r; 71 | }, 72 | 73 | /** 74 | * 怪物被击中 75 | * @param building {Element} 对应的建筑(武器) 76 | * @param damage {Number} 本次攻击的原始伤害值 77 | */ 78 | beHit: function (building, damage) { 79 | if (!this.is_valid) return; 80 | var min_damage = Math.ceil(damage * 0.1); 81 | damage -= this.shield; 82 | if (damage <= min_damage) damage = min_damage; 83 | 84 | this.life -= damage; 85 | TD.score += Math.floor(Math.sqrt(damage)); 86 | if (this.life <= 0) { 87 | this.beKilled(building); 88 | } 89 | 90 | var balloontip = this.scene.panel.balloontip; 91 | if (balloontip.el == this) { 92 | balloontip.text = TD._t("monster_info", [this.life, this.shield, this.speed, this.damage]); 93 | } 94 | 95 | }, 96 | 97 | /** 98 | * 怪物被杀死 99 | * @param building {Element} 对应的建筑(武器) 100 | */ 101 | beKilled: function (building) { 102 | if (!this.is_valid) return; 103 | this.life = 0; 104 | this.is_valid = false; 105 | 106 | TD.money += this.money; 107 | building.killed++; 108 | 109 | TD.Explode(this.id + "-explode", { 110 | cx: this.cx, 111 | cy: this.cy, 112 | color: this.color, 113 | r: this.r, 114 | step_level: this.step_level, 115 | render_level: this.render_level, 116 | scene: this.grid.scene 117 | }); 118 | }, 119 | arrive: function () { 120 | this.grid = this.next_grid; 121 | this.next_grid = null; 122 | this.checkFinish(); 123 | }, 124 | findWay: function () { 125 | var _this = this; 126 | var fw = new TD.FindWay( 127 | this.map.grid_x, this.map.grid_y, 128 | this.grid.mx, this.grid.my, 129 | this.map.exit.mx, this.map.exit.my, 130 | function (x, y) { 131 | return _this.map.checkPassable(x, y); 132 | } 133 | ); 134 | this.way = fw.way; 135 | //delete fw; 136 | }, 137 | 138 | /** 139 | * 检查是否已到达终点 140 | */ 141 | checkFinish: function () { 142 | if (this.grid && this.map && this.grid == this.map.exit) { 143 | TD.life -= this.damage; 144 | TD.wave_damage += this.damage; 145 | if (TD.life <= 0) { 146 | TD.life = 0; 147 | TD.stage.gameover(); 148 | } else { 149 | this.pause(); 150 | this.del(); 151 | } 152 | } 153 | }, 154 | beAddToGrid: function (grid) { 155 | this.grid = grid; 156 | this.map = grid.map; 157 | this.cx = grid.cx; 158 | this.cy = grid.cy; 159 | 160 | this.grid.scene.addElement(this); 161 | }, 162 | 163 | /** 164 | * 取得朝向 165 | * 即下一个格子在当前格子的哪边 166 | * 0:上;1:右;2:下;3:左 167 | */ 168 | getToward: function () { 169 | if (!this.grid || !this.next_grid) return; 170 | if (this.grid.my < this.next_grid.my) { 171 | this.toward = 0; 172 | } else if (this.grid.mx < this.next_grid.mx) { 173 | this.toward = 1; 174 | } else if (this.grid.my > this.next_grid.my) { 175 | this.toward = 2; 176 | } else if (this.grid.mx > this.next_grid.mx) { 177 | this.toward = 3; 178 | } 179 | }, 180 | 181 | /** 182 | * 取得要去的下一个格子 183 | */ 184 | getNextGrid: function () { 185 | if (this.way.length == 0 || 186 | Math.random() < 0.1 // 有 1/10 的概率自动重新寻路 187 | ) { 188 | this.findWay(); 189 | } 190 | 191 | var next_grid = this.way.shift(); 192 | if (next_grid && !this.map.checkPassable(next_grid[0], next_grid[1])) { 193 | this.findWay(); 194 | next_grid = this.way.shift(); 195 | } 196 | 197 | if (!next_grid) { 198 | return; 199 | } 200 | 201 | this.next_grid = this.map.getGrid(next_grid[0], next_grid[1]); 202 | // this.getToward(); // 在这个版本中暂时没有用 203 | }, 204 | 205 | /** 206 | * 检查假如在地图 (x, y) 的位置修建建筑,是否会阻塞当前怪物 207 | * @param mx {Number} 地图的 x 坐标 208 | * @param my {Number} 地图的 y 坐标 209 | * @return {Boolean} 210 | */ 211 | chkIfBlocked: function (mx, my) { 212 | 213 | var _this = this, 214 | fw = new TD.FindWay( 215 | this.map.grid_x, this.map.grid_y, 216 | this.grid.mx, this.grid.my, 217 | this.map.exit.mx, this.map.exit.my, 218 | function (x, y) { 219 | return !(x == mx && y == my) && 220 | _this.map.checkPassable(x, y); 221 | } 222 | ); 223 | 224 | return fw.is_blocked; 225 | 226 | }, 227 | 228 | /** 229 | * 怪物前进的道路被阻塞(被建筑包围了) 230 | */ 231 | beBlocked: function () { 232 | if (this.is_blocked) return; 233 | 234 | this.is_blocked = true; 235 | TD.log("monster be blocked!"); 236 | }, 237 | 238 | step: function () { 239 | if (!this.is_valid || this.is_paused || !this.grid) return; 240 | 241 | if (!this.next_grid) { 242 | this.getNextGrid(); 243 | 244 | /** 245 | * 如果依旧找不着下一步可去的格子,说明当前怪物被阻塞了 246 | */ 247 | if (!this.next_grid) { 248 | this.beBlocked(); 249 | return; 250 | } 251 | } 252 | 253 | if (this.cx == this.next_grid.cx && this.cy == this.next_grid.cy) { 254 | this.arrive(); 255 | } else { 256 | // 移动到 next grid 257 | 258 | var dpx = this.next_grid.cx - this.cx, 259 | dpy = this.next_grid.cy - this.cy, 260 | sx = dpx < 0 ? -1 : 1, 261 | sy = dpy < 0 ? -1 : 1, 262 | speed = this.speed * TD.global_speed; 263 | 264 | if (Math.abs(dpx) < speed && Math.abs(dpy) < speed) { 265 | this.cx = this.next_grid.cx; 266 | this.cy = this.next_grid.cy; 267 | this._dx = speed - Math.abs(dpx); 268 | this._dy = speed - Math.abs(dpy); 269 | } else { 270 | this.cx += dpx == 0 ? 0 : sx * (speed + this._dx); 271 | this.cy += dpy == 0 ? 0 : sy * (speed + this._dy); 272 | this._dx = 0; 273 | this._dy = 0; 274 | } 275 | } 276 | 277 | this.caculatePos(); 278 | }, 279 | 280 | onEnter: function () { 281 | var msg, 282 | balloontip = this.scene.panel.balloontip; 283 | 284 | if (balloontip.el == this) { 285 | balloontip.hide(); 286 | balloontip.el = null; 287 | } else { 288 | msg = TD._t("monster_info", 289 | [this.life, this.shield, this.speed, this.damage]); 290 | balloontip.msg(msg, this); 291 | } 292 | }, 293 | 294 | onOut: function () { 295 | // if (this.scene.panel.balloontip.el == this) { 296 | // this.scene.panel.balloontip.hide(); 297 | // } 298 | } 299 | }; 300 | 301 | /** 302 | * @param id {String} 303 | * @param cfg {Object} 配置对象 304 | * 至少需要包含以下项: 305 | * { 306 | * life: 怪物的生命值 307 | * shield: 怪物的防御值 308 | * speed: 怪物的速度 309 | * } 310 | */ 311 | TD.Monster = function (id, cfg) { 312 | cfg.on_events = ["enter", "out"]; 313 | var monster = new TD.Element(id, cfg); 314 | TD.lang.mix(monster, monster_obj); 315 | monster._init(cfg); 316 | 317 | return monster; 318 | }; 319 | 320 | 321 | /** 322 | * 怪物死亡时的爆炸效果对象 323 | */ 324 | var explode_obj = { 325 | _init: function (cfg) { 326 | cfg = cfg || {}; 327 | 328 | var rgb = TD.lang.rgb2Arr(cfg.color); 329 | this.cx = cfg.cx; 330 | this.cy = cfg.cy; 331 | this.r = cfg.r * _TD.retina; 332 | this.step_level = cfg.step_level; 333 | this.render_level = cfg.render_level; 334 | 335 | this.rgb_r = rgb[0]; 336 | this.rgb_g = rgb[1]; 337 | this.rgb_b = rgb[2]; 338 | this.rgb_a = 1; 339 | 340 | this.wait = this.wait0 = TD.exp_fps * (cfg.time || 1); 341 | 342 | cfg.scene.addElement(this); 343 | }, 344 | step: function () { 345 | if (!this.is_valid) return; 346 | 347 | this.wait--; 348 | this.r++; 349 | 350 | this.is_valid = this.wait > 0; 351 | this.rgb_a = this.wait / this.wait0; 352 | }, 353 | render: function () { 354 | var ctx = TD.ctx; 355 | 356 | ctx.fillStyle = "rgba(" + this.rgb_r + "," + this.rgb_g + "," 357 | + this.rgb_b + "," + this.rgb_a + ")"; 358 | ctx.beginPath(); 359 | ctx.arc(this.cx, this.cy, this.r, 0, Math.PI * 2, true); 360 | ctx.closePath(); 361 | ctx.fill(); 362 | } 363 | }; 364 | 365 | /** 366 | * @param id {String} 367 | * @param cfg {Object} 配置对象 368 | * { 369 | * // 至少需要包含以下项: 370 | * cx: 中心 x 坐标 371 | * cy: 中心 y 坐标 372 | * r: 半径 373 | * color: RGB色彩,形如“#f98723” 374 | * scene: Scene 对象 375 | * step_level: 376 | * render_level: 377 | * 378 | * // 以下项可选: 379 | * time: 持续时间,默认为 1,单位大致为秒(根据渲染情况而定,不是很精确) 380 | * } 381 | */ 382 | TD.Explode = function (id, cfg) { 383 | // cfg.on_events = ["enter", "out"]; 384 | var explode = new TD.Element(id, cfg); 385 | TD.lang.mix(explode, explode_obj); 386 | explode._init(cfg); 387 | 388 | return explode; 389 | }; 390 | 391 | }); // _TD.a.push end 392 | 393 | 394 | -------------------------------------------------------------------------------- /src/js/td-obj-panel.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | * Last Update: 2011/1/10 5:22:52 8 | */ 9 | 10 | 11 | // _TD.a.push begin 12 | _TD.a.push(function (TD) { 13 | 14 | // panel 对象的属性、方法。注意属性中不要有数组、对象等 15 | // 引用属性,否则多个实例的相关属性会发生冲突 16 | var panel_obj = { 17 | _init: function (cfg) { 18 | cfg = cfg || {}; 19 | this.x = cfg.x; 20 | this.y = cfg.y; 21 | this.scene = cfg.scene; 22 | this.map = cfg.main_map; 23 | 24 | // make panel map 25 | var panel_map = new TD.Map("panel-map", TD.lang.mix({ 26 | x: this.x + cfg.map.x, 27 | y: this.y + cfg.map.y, 28 | scene: this.scene, 29 | step_level: this.step_level, 30 | render_level: this.render_level 31 | }, cfg.map, false)); 32 | 33 | this.addToScene(this.scene, 1, 7); 34 | panel_map.addToScene(this.scene, 1, 7, panel_map.grids); 35 | this.scene.panel_map = panel_map; 36 | this.gameover_obj = new TD.GameOver("panel-gameover", { 37 | panel: this, 38 | scene: this.scene, 39 | step_level: this.step_level, 40 | is_visiable: false, 41 | x: 0, 42 | y: 0, 43 | width: this.scene.stage.width, 44 | height: this.scene.stage.height, 45 | render_level: 9 46 | }); 47 | 48 | this.balloontip = new TD.BalloonTip("panel-balloon-tip", { 49 | scene: this.scene, 50 | step_level: this.step_level, 51 | render_level: 9 52 | }); 53 | this.balloontip.addToScene(this.scene, 1, 9); 54 | 55 | // make buttons 56 | // 暂停按钮 57 | this.btn_pause = new TD.Button("panel-btn-pause", { 58 | scene: this.scene, 59 | x: this.x, 60 | y: this.y + 260 * _TD.retina, 61 | text: TD._t("button_pause_text"), 62 | //desc: TD._t("button_pause_desc_0"), 63 | step_level: this.step_level, 64 | render_level: this.render_level + 1, 65 | onClick: function () { 66 | if (this.scene.state == 1) { 67 | this.scene.pause(); 68 | this.text = TD._t("button_continue_text"); 69 | this.scene.panel.btn_upgrade.hide(); 70 | this.scene.panel.btn_sell.hide(); 71 | this.scene.panel.btn_restart.show(); 72 | //this.desc = TD._t("button_pause_desc_1"); 73 | } else if (this.scene.state == 2) { 74 | this.scene.start(); 75 | this.text = TD._t("button_pause_text"); 76 | this.scene.panel.btn_restart.hide(); 77 | if (this.scene.map.selected_building) { 78 | this.scene.panel.btn_upgrade.show(); 79 | this.scene.panel.btn_sell.show(); 80 | } 81 | //this.desc = TD._t("button_pause_desc_0"); 82 | } 83 | } 84 | }); 85 | // 重新开始按钮 86 | this.btn_restart = new TD.Button("panel-btn-restart", { 87 | scene: this.scene, 88 | x: this.x, 89 | y: this.y + 300 * _TD.retina, 90 | is_visiable: false, 91 | text: TD._t("button_restart_text"), 92 | step_level: this.step_level, 93 | render_level: this.render_level + 1, 94 | onClick: function () { 95 | setTimeout(function () { 96 | TD.stage.clear(); 97 | TD.is_paused = true; 98 | TD.start(); 99 | TD.mouseHand(false); 100 | }, 0); 101 | } 102 | }); 103 | // 建筑升级按钮 104 | this.btn_upgrade = new TD.Button("panel-btn-upgrade", { 105 | scene: this.scene, 106 | x: this.x, 107 | y: this.y + 300 * _TD.retina, 108 | is_visiable: false, 109 | text: TD._t("button_upgrade_text"), 110 | step_level: this.step_level, 111 | render_level: this.render_level + 1, 112 | onClick: function () { 113 | this.scene.map.selected_building.tryToUpgrade(this); 114 | } 115 | }); 116 | // 建筑出售按钮 117 | this.btn_sell = new TD.Button("panel-btn-sell", { 118 | scene: this.scene, 119 | x: this.x, 120 | y: this.y + 340 * _TD.retina, 121 | is_visiable: false, 122 | text: TD._t("button_sell_text"), 123 | step_level: this.step_level, 124 | render_level: this.render_level + 1, 125 | onClick: function () { 126 | this.scene.map.selected_building.tryToSell(this); 127 | } 128 | }); 129 | }, 130 | step: function () { 131 | if (TD.life_recover) { 132 | this._life_recover = this._life_recover2 = TD.life_recover; 133 | this._life_recover_wait = this._life_recover_wait2 = TD.exp_fps * 3; 134 | TD.life_recover = 0; 135 | } 136 | 137 | if (this._life_recover && (TD.iframe % TD.exp_fps_eighth == 0)) { 138 | TD.life ++; 139 | this._life_recover --; 140 | } 141 | 142 | }, 143 | render: function () { 144 | // 画状态文字 145 | var ctx = TD.ctx; 146 | 147 | ctx.textAlign = "left"; 148 | ctx.textBaseline = "top"; 149 | ctx.fillStyle = "#000"; 150 | ctx.font = "normal " + (12 * _TD.retina) + "px 'Courier New'"; 151 | ctx.beginPath(); 152 | ctx.fillText(TD._t("panel_money_title") + TD.money, this.x, this.y); 153 | ctx.fillText(TD._t("panel_score_title") + TD.score, this.x, this.y + 20 * _TD.retina); 154 | ctx.fillText(TD._t("panel_life_title") + TD.life, this.x, this.y + 40 * _TD.retina); 155 | ctx.fillText(TD._t("panel_building_title") + this.map.buildings.length, 156 | this.x, this.y + 60 * _TD.retina); 157 | ctx.fillText(TD._t("panel_monster_title") + this.map.monsters.length, 158 | this.x, this.y + 80 * _TD.retina); 159 | ctx.fillText(TD._t("wave_info", [this.scene.wave]), this.x, this.y + 210 * _TD.retina); 160 | ctx.closePath(); 161 | 162 | if (this._life_recover_wait) { 163 | // 画生命恢复提示 164 | var a = this._life_recover_wait / this._life_recover_wait2; 165 | ctx.fillStyle = "rgba(255, 0, 0, " + a + ")"; 166 | ctx.font = "bold " + (12 * _TD.retina) + "px 'Verdana'"; 167 | ctx.beginPath(); 168 | ctx.fillText("+" + this._life_recover2, this.x + 60 * _TD.retina, this.y + 40 * _TD.retina); 169 | ctx.closePath(); 170 | this._life_recover_wait --; 171 | } 172 | 173 | // 在右下角画版本信息 174 | ctx.textAlign = "right"; 175 | ctx.fillStyle = "#666"; 176 | ctx.font = "normal " + (12 * _TD.retina) + "px 'Courier New'"; 177 | ctx.beginPath(); 178 | ctx.fillText("version: " + TD.version + " | oldj.net", TD.stage.width - TD.padding, 179 | TD.stage.height - TD.padding * 2); 180 | ctx.closePath(); 181 | 182 | // 在左下角画FPS信息 183 | ctx.textAlign = "left"; 184 | ctx.fillStyle = "#666"; 185 | ctx.font = "normal " + (12 * _TD.retina) + "px 'Courier New'"; 186 | ctx.beginPath(); 187 | ctx.fillText("FPS: " + TD.fps, TD.padding, TD.stage.height - TD.padding * 2); 188 | ctx.closePath(); 189 | } 190 | }; 191 | 192 | /** 193 | * @param id {String} 194 | * @param cfg {Object} 配置对象 195 | * 至少需要包含以下项: 196 | * { 197 | * life: 怪物的生命值 198 | * shield: 怪物的防御值 199 | * speed: 怪物的速度 200 | * } 201 | */ 202 | TD.Panel = function (id, cfg) { 203 | var panel = new TD.Element(id, cfg); 204 | TD.lang.mix(panel, panel_obj); 205 | panel._init(cfg); 206 | 207 | return panel; 208 | }; 209 | 210 | // balloon tip对象的属性、方法。注意属性中不要有数组、对象等 211 | // 引用属性,否则多个实例的相关属性会发生冲突 212 | var balloontip_obj = { 213 | _init: function (cfg) { 214 | cfg = cfg || {}; 215 | this.scene = cfg.scene; 216 | }, 217 | caculatePos: function () { 218 | var el = this.el; 219 | 220 | this.x = el.cx + 0.5; 221 | this.y = el.cy + 0.5; 222 | 223 | if (this.x + this.width > this.scene.stage.width - TD.padding) { 224 | this.x = this.x - this.width; 225 | } 226 | 227 | this.px = this.x + 5 * _TD.retina; 228 | this.py = this.y + 4 * _TD.retina; 229 | }, 230 | msg: function (txt, el) { 231 | this.text = txt; 232 | var ctx = TD.ctx; 233 | ctx.font = "normal " + (12 * _TD.retina) + "px 'Courier New'"; 234 | this.width = Math.max( 235 | ctx.measureText(txt).width + 10 * _TD.retina, 236 | TD.lang.strLen2(txt) * 6 + 10 * _TD.retina 237 | ); 238 | this.height = 20 * _TD.retina; 239 | 240 | if (el && el.cx && el.cy) { 241 | this.el = el; 242 | this.caculatePos(); 243 | 244 | this.show(); 245 | } 246 | }, 247 | step: function () { 248 | if (!this.el || !this.el.is_valid) { 249 | this.hide(); 250 | return; 251 | } 252 | 253 | if (this.el.is_monster) { 254 | // monster 会移动,所以需要重新计算 tip 的位置 255 | this.caculatePos(); 256 | } 257 | }, 258 | render: function () { 259 | if (!this.el) return; 260 | var ctx = TD.ctx; 261 | 262 | ctx.lineWidth = _TD.retina; 263 | ctx.fillStyle = "rgba(255, 255, 0, 0.5)"; 264 | ctx.strokeStyle = "rgba(222, 222, 0, 0.9)"; 265 | ctx.beginPath(); 266 | ctx.rect(this.x, this.y, this.width, this.height); 267 | ctx.closePath(); 268 | ctx.fill(); 269 | ctx.stroke(); 270 | 271 | ctx.textAlign = "left"; 272 | ctx.textBaseline = "top"; 273 | ctx.fillStyle = "#000"; 274 | ctx.font = "normal " + (12 * _TD.retina) + "px 'Courier New'"; 275 | ctx.beginPath(); 276 | ctx.fillText(this.text, this.px, this.py); 277 | ctx.closePath(); 278 | 279 | } 280 | }; 281 | 282 | /** 283 | * @param id {String} 284 | * @param cfg {Object} 配置对象 285 | * 至少需要包含以下项: 286 | * { 287 | * scene: scene 288 | * } 289 | */ 290 | TD.BalloonTip = function (id, cfg) { 291 | var balloontip = new TD.Element(id, cfg); 292 | TD.lang.mix(balloontip, balloontip_obj); 293 | balloontip._init(cfg); 294 | 295 | return balloontip; 296 | }; 297 | 298 | // button 对象的属性、方法。注意属性中不要有数组、对象等 299 | // 引用属性,否则多个实例的相关属性会发生冲突 300 | var button_obj = { 301 | _init: function (cfg) { 302 | cfg = cfg || {}; 303 | this.text = cfg.text; 304 | this.onClick = cfg.onClick || TD.lang.nullFunc; 305 | this.x = cfg.x; 306 | this.y = cfg.y; 307 | this.width = cfg.width || 80 * _TD.retina; 308 | this.height = cfg.height || 30 * _TD.retina; 309 | this.font_x = this.x + 8 * _TD.retina; 310 | this.font_y = this.y + 9 * _TD.retina; 311 | this.scene = cfg.scene; 312 | this.desc = cfg.desc || ""; 313 | 314 | this.addToScene(this.scene, this.step_level, this.render_level); 315 | this.caculatePos(); 316 | }, 317 | onEnter: function () { 318 | TD.mouseHand(true); 319 | if (this.desc) { 320 | this.scene.panel.balloontip.msg(this.desc, this); 321 | } 322 | }, 323 | onOut: function () { 324 | TD.mouseHand(false); 325 | if (this.scene.panel.balloontip.el == this) { 326 | this.scene.panel.balloontip.hide(); 327 | } 328 | }, 329 | render: function () { 330 | var ctx = TD.ctx; 331 | 332 | ctx.lineWidth = 2 * _TD.retina; 333 | ctx.fillStyle = this.is_hover ? "#eee" : "#ccc"; 334 | ctx.strokeStyle = "#999"; 335 | ctx.beginPath(); 336 | ctx.rect(this.x, this.y, this.width, this.height); 337 | ctx.closePath(); 338 | ctx.fill(); 339 | ctx.stroke(); 340 | 341 | ctx.textAlign = "left"; 342 | ctx.textBaseline = "top"; 343 | ctx.fillStyle = "#000"; 344 | ctx.font = "normal " + (12 * _TD.retina) + "px 'Courier New'"; 345 | ctx.beginPath(); 346 | ctx.fillText(this.text, this.font_x, this.font_y); 347 | ctx.closePath(); 348 | ctx.fill(); 349 | } 350 | }; 351 | 352 | /** 353 | * @param id {String} 354 | * @param cfg {Object} 配置对象 355 | * 至少需要包含以下项: 356 | * { 357 | * x: 358 | * y: 359 | * text: 360 | * onClick: function 361 | * sence: 362 | * } 363 | */ 364 | TD.Button = function (id, cfg) { 365 | cfg.on_events = ["enter", "out", "click"]; 366 | var button = new TD.Element(id, cfg); 367 | TD.lang.mix(button, button_obj); 368 | button._init(cfg); 369 | 370 | return button; 371 | }; 372 | 373 | 374 | // gameover 对象的属性、方法。注意属性中不要有数组、对象等 375 | // 引用属性,否则多个实例的相关属性会发生冲突 376 | var gameover_obj = { 377 | _init: function (cfg) { 378 | this.panel = cfg.panel; 379 | this.scene = cfg.scene; 380 | 381 | this.addToScene(this.scene, 1, 9); 382 | }, 383 | render: function () { 384 | 385 | this.panel.btn_pause.hide(); 386 | this.panel.btn_upgrade.hide(); 387 | this.panel.btn_sell.hide(); 388 | this.panel.btn_restart.show(); 389 | 390 | var ctx = TD.ctx; 391 | ctx.textAlign = "center"; 392 | ctx.textBaseline = "middle"; 393 | ctx.fillStyle = "#ccc"; 394 | ctx.font = "bold 62px 'Verdana'"; 395 | ctx.beginPath(); 396 | ctx.fillText("GAME OVER", this.width / 2, this.height / 2); 397 | ctx.closePath(); 398 | ctx.fillStyle = "#f00"; 399 | ctx.font = "bold 60px 'Verdana'"; 400 | ctx.beginPath(); 401 | ctx.fillText("GAME OVER", this.width / 2, this.height / 2); 402 | ctx.closePath(); 403 | 404 | } 405 | }; 406 | 407 | /** 408 | * @param id {String} 409 | * @param cfg {Object} 配置对象 410 | * 至少需要包含以下项: 411 | * { 412 | * panel: 413 | * scene: 414 | * } 415 | */ 416 | TD.GameOver = function (id, cfg) { 417 | var obj = new TD.Element(id, cfg); 418 | TD.lang.mix(obj, gameover_obj); 419 | obj._init(cfg); 420 | 421 | return obj; 422 | }; 423 | 424 | 425 | /** 426 | * 恢复 n 点生命值 427 | * @param n 428 | */ 429 | TD.recover = function (n) { 430 | // TD.life += n; 431 | TD.life_recover = n; 432 | TD.log("life recover: " + n); 433 | }; 434 | 435 | }); // _TD.a.push end 436 | 437 | -------------------------------------------------------------------------------- /src/js/td-render-buildings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | * Last Update: 2011/1/10 5:22:52 8 | */ 9 | 10 | // _TD.a.push begin 11 | _TD.a.push(function (TD) { 12 | 13 | function lineTo2(ctx, x0, y0, x1, y1, len) { 14 | var x2, y2, a, b, p, xt, 15 | a2, b2, c2; 16 | 17 | if (x0 == x1) { 18 | x2 = x0; 19 | y2 = y1 > y0 ? y0 + len : y0 - len; 20 | } else if (y0 == y1) { 21 | y2 = y0; 22 | x2 = x1 > x0 ? x0 + len : x0 - len; 23 | } else { 24 | // 解一元二次方程 25 | a = (y0 - y1) / (x0 - x1); 26 | b = y0 - x0 * a; 27 | a2 = a * a + 1; 28 | b2 = 2 * (a * (b - y0) - x0); 29 | c2 = Math.pow(b - y0, 2) + x0 * x0 - Math.pow(len, 2); 30 | p = Math.pow(b2, 2) - 4 * a2 * c2; 31 | if (p < 0) { 32 | // TD.log("ERROR: [a, b, len] = [" + ([a, b, len]).join(", ") + "]"); 33 | return [0, 0]; 34 | } 35 | p = Math.sqrt(p); 36 | xt = (-b2 + p) / (2 * a2); 37 | if ((x1 - x0 > 0 && xt - x0 > 0) || 38 | (x1 - x0 < 0 && xt - x0 < 0)) { 39 | x2 = xt; 40 | y2 = a * x2 + b; 41 | } else { 42 | x2 = (-b2 - p) / (2 * a2); 43 | y2 = a * x2 + b; 44 | } 45 | } 46 | 47 | ctx.lineCap = "round"; 48 | ctx.moveTo(x0, y0); 49 | ctx.lineTo(x2, y2); 50 | 51 | return [x2, y2]; 52 | } 53 | 54 | var renderFunctions = { 55 | "cannon": function (b, ctx, map, gs, gs2) { 56 | var target_position = b.getTargetPosition(); 57 | 58 | ctx.fillStyle = "#393"; 59 | ctx.strokeStyle = "#000"; 60 | ctx.beginPath(); 61 | ctx.lineWidth = _TD.retina; 62 | ctx.arc(b.cx, b.cy, gs2 - 5, 0, Math.PI * 2, true); 63 | ctx.closePath(); 64 | ctx.fill(); 65 | ctx.stroke(); 66 | 67 | ctx.lineWidth = 3 * _TD.retina; 68 | ctx.beginPath(); 69 | ctx.moveTo(b.cx, b.cy); 70 | b.muzzle = lineTo2(ctx, b.cx, b.cy, target_position[0], target_position[1], gs2); 71 | ctx.closePath(); 72 | // ctx.fill(); 73 | ctx.stroke(); 74 | 75 | ctx.lineWidth = _TD.retina; 76 | ctx.fillStyle = "#060"; 77 | ctx.beginPath(); 78 | ctx.arc(b.cx, b.cy, 7 * _TD.retina, 0, Math.PI * 2, true); 79 | ctx.closePath(); 80 | ctx.fill(); 81 | ctx.stroke(); 82 | 83 | ctx.fillStyle = "#cec"; 84 | ctx.beginPath(); 85 | ctx.arc(b.cx + 2, b.cy - 2, 3 * _TD.retina, 0, Math.PI * 2, true); 86 | ctx.closePath(); 87 | ctx.fill(); 88 | 89 | }, 90 | "LMG": function (b, ctx, map, gs, gs2) { 91 | var target_position = b.getTargetPosition(); 92 | 93 | ctx.fillStyle = "#36f"; 94 | ctx.strokeStyle = "#000"; 95 | ctx.beginPath(); 96 | ctx.lineWidth = _TD.retina; 97 | ctx.arc(b.cx, b.cy, 7 * _TD.retina, 0, Math.PI * 2, true); 98 | ctx.closePath(); 99 | ctx.fill(); 100 | ctx.stroke(); 101 | 102 | ctx.lineWidth = 2 * _TD.retina; 103 | ctx.beginPath(); 104 | ctx.moveTo(b.cx, b.cy); 105 | b.muzzle = lineTo2(ctx, b.cx, b.cy, target_position[0], target_position[1], gs2); 106 | ctx.closePath(); 107 | ctx.fill(); 108 | ctx.stroke(); 109 | 110 | ctx.lineWidth = _TD.retina; 111 | ctx.fillStyle = "#66c"; 112 | ctx.beginPath(); 113 | ctx.arc(b.cx, b.cy, 5 * _TD.retina, 0, Math.PI * 2, true); 114 | ctx.closePath(); 115 | ctx.fill(); 116 | ctx.stroke(); 117 | 118 | ctx.fillStyle = "#ccf"; 119 | ctx.beginPath(); 120 | ctx.arc(b.cx + 1, b.cy - 1, 2 * _TD.retina, 0, Math.PI * 2, true); 121 | ctx.closePath(); 122 | ctx.fill(); 123 | 124 | }, 125 | "HMG": function (b, ctx, map, gs, gs2) { 126 | var target_position = b.getTargetPosition(); 127 | 128 | ctx.fillStyle = "#933"; 129 | ctx.strokeStyle = "#000"; 130 | ctx.beginPath(); 131 | ctx.lineWidth = _TD.retina; 132 | ctx.arc(b.cx, b.cy, gs2 - 2, 0, Math.PI * 2, true); 133 | ctx.closePath(); 134 | ctx.fill(); 135 | ctx.stroke(); 136 | 137 | ctx.lineWidth = 5 * _TD.retina; 138 | ctx.beginPath(); 139 | ctx.moveTo(b.cx, b.cy); 140 | b.muzzle = lineTo2(ctx, b.cx, b.cy, target_position[0], target_position[1], gs2); 141 | ctx.closePath(); 142 | ctx.fill(); 143 | ctx.stroke(); 144 | 145 | ctx.lineWidth = _TD.retina; 146 | ctx.fillStyle = "#630"; 147 | ctx.beginPath(); 148 | ctx.arc(b.cx, b.cy, gs2 - 5 * _TD.retina, 0, Math.PI * 2, true); 149 | ctx.closePath(); 150 | ctx.fill(); 151 | ctx.stroke(); 152 | 153 | ctx.fillStyle = "#960"; 154 | ctx.beginPath(); 155 | ctx.arc(b.cx + 1, b.cy - 1, 8 * _TD.retina, 0, Math.PI * 2, true); 156 | ctx.closePath(); 157 | ctx.fill(); 158 | 159 | ctx.fillStyle = "#fcc"; 160 | ctx.beginPath(); 161 | ctx.arc(b.cx + 3, b.cy - 3, 4 * _TD.retina, 0, Math.PI * 2, true); 162 | ctx.closePath(); 163 | ctx.fill(); 164 | 165 | }, 166 | "wall": function (b, ctx, map, gs, gs2) { 167 | ctx.lineWidth = _TD.retina; 168 | ctx.fillStyle = "#666"; 169 | ctx.strokeStyle = "#000"; 170 | ctx.fillRect(b.cx - gs2 + 1, b.cy - gs2 + 1, gs - 1, gs - 1); 171 | ctx.beginPath(); 172 | ctx.moveTo(b.cx - gs2 + 0.5, b.cy - gs2 + 0.5); 173 | ctx.lineTo(b.cx - gs2 + 0.5, b.cy + gs2 + 0.5); 174 | ctx.lineTo(b.cx + gs2 + 0.5, b.cy + gs2 + 0.5); 175 | ctx.lineTo(b.cx + gs2 + 0.5, b.cy - gs2 + 0.5); 176 | ctx.lineTo(b.cx - gs2 + 0.5, b.cy - gs2 + 0.5); 177 | ctx.moveTo(b.cx - gs2 + 0.5, b.cy + gs2 + 0.5); 178 | ctx.lineTo(b.cx + gs2 + 0.5, b.cy - gs2 + 0.5); 179 | ctx.moveTo(b.cx - gs2 + 0.5, b.cy - gs2 + 0.5); 180 | ctx.lineTo(b.cx + gs2 + 0.5, b.cy + gs2 + 0.5); 181 | ctx.closePath(); 182 | ctx.stroke(); 183 | }, 184 | "laser_gun": function (b, ctx/*, map, gs, gs2*/) { 185 | // var target_position = b.getTargetPosition(); 186 | 187 | ctx.fillStyle = "#f00"; 188 | ctx.strokeStyle = "#000"; 189 | ctx.beginPath(); 190 | ctx.lineWidth = _TD.retina; 191 | // ctx.arc(b.cx, b.cy, gs2 - 5, 0, Math.PI * 2, true); 192 | ctx.moveTo(b.cx, b.cy - 10 * _TD.retina); 193 | ctx.lineTo(b.cx - 8.66 * _TD.retina, b.cy + 5 * _TD.retina); 194 | ctx.lineTo(b.cx + 8.66 * _TD.retina, b.cy + 5 * _TD.retina); 195 | ctx.lineTo(b.cx, b.cy - 10 * _TD.retina); 196 | ctx.closePath(); 197 | ctx.fill(); 198 | ctx.stroke(); 199 | 200 | ctx.fillStyle = "#60f"; 201 | ctx.beginPath(); 202 | ctx.arc(b.cx, b.cy, 7 * _TD.retina, 0, Math.PI * 2, true); 203 | ctx.closePath(); 204 | ctx.fill(); 205 | ctx.stroke(); 206 | 207 | ctx.fillStyle = "#000"; 208 | ctx.beginPath(); 209 | ctx.arc(b.cx, b.cy, 3 * _TD.retina, 0, Math.PI * 2, true); 210 | ctx.closePath(); 211 | ctx.fill(); 212 | 213 | ctx.fillStyle = "#666"; 214 | ctx.beginPath(); 215 | ctx.arc(b.cx + 1, b.cy - 1, _TD.retina, 0, Math.PI * 2, true); 216 | ctx.closePath(); 217 | ctx.fill(); 218 | 219 | ctx.lineWidth = 3 * _TD.retina; 220 | ctx.beginPath(); 221 | ctx.moveTo(b.cx, b.cy); 222 | // b.muzzle = lineTo2(ctx, b.cx, b.cy, target_position[0], target_position[1], gs2); 223 | ctx.closePath(); 224 | ctx.fill(); 225 | ctx.stroke(); 226 | } 227 | }; 228 | 229 | TD.renderBuilding = function (building) { 230 | var ctx = TD.ctx, 231 | map = building.map, 232 | gs = TD.grid_size, 233 | gs2 = TD.grid_size / 2; 234 | 235 | (renderFunctions[building.type] || renderFunctions["wall"])( 236 | building, ctx, map, gs, gs2 237 | ); 238 | } 239 | 240 | }); // _TD.a.push end 241 | -------------------------------------------------------------------------------- /src/js/td-stage.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | * Last Update: 2011/1/10 5:22:52 8 | */ 9 | 10 | 11 | // _TD.a.push begin 12 | _TD.a.push(function (TD) { 13 | 14 | /** 15 | * 舞台类 16 | * @param id {String} 舞台ID 17 | * @param cfg {Object} 配置 18 | */ 19 | TD.Stage = function (id, cfg) { 20 | this.id = id || ("stage-" + TD.lang.rndStr()); 21 | this.cfg = cfg || {}; 22 | this.width = this.cfg.width || 640; 23 | this.height = this.cfg.height || 540; 24 | 25 | /** 26 | * mode 有以下状态: 27 | * "normal": 普通状态 28 | * "build": 建造模式 29 | */ 30 | this.mode = "normal"; 31 | 32 | /* 33 | * state 有以下几种状态: 34 | * 0: 等待中 35 | * 1: 运行中 36 | * 2: 暂停 37 | * 3: 已结束 38 | */ 39 | this.state = 0; 40 | this.acts = []; 41 | this.current_act = null; 42 | this._step2 = TD.lang.nullFunc; 43 | 44 | this._init(); 45 | }; 46 | 47 | TD.Stage.prototype = { 48 | _init: function () { 49 | if (typeof this.cfg.init == "function") { 50 | this.cfg.init.call(this); 51 | } 52 | if (typeof this.cfg.step2 == "function") { 53 | this._step2 = this.cfg.step2; 54 | } 55 | }, 56 | start: function () { 57 | this.state = 1; 58 | TD.lang.each(this.acts, function (obj) { 59 | obj.start(); 60 | }); 61 | }, 62 | pause: function () { 63 | this.state = 2; 64 | }, 65 | gameover: function () { 66 | //this.pause(); 67 | this.current_act.gameover(); 68 | }, 69 | /** 70 | * 清除本 stage 所有物品 71 | */ 72 | clear: function () { 73 | this.state = 3; 74 | TD.lang.each(this.acts, function (obj) { 75 | obj.clear(); 76 | }); 77 | // delete this; 78 | }, 79 | /** 80 | * 主循环函数 81 | */ 82 | step: function () { 83 | if (this.state != 1 || !this.current_act) return; 84 | TD.eventManager.step(); 85 | this.current_act.step(); 86 | 87 | this._step2(); 88 | }, 89 | /** 90 | * 绘制函数 91 | */ 92 | render: function () { 93 | if (this.state == 0 || this.state == 3 || !this.current_act) return; 94 | this.current_act.render(); 95 | }, 96 | addAct: function (act) { 97 | this.acts.push(act); 98 | }, 99 | addElement: function (el, step_level, render_level) { 100 | if (this.current_act) 101 | this.current_act.addElement(el, step_level, render_level); 102 | } 103 | }; 104 | 105 | }); // _TD.a.push end 106 | 107 | 108 | // _TD.a.push begin 109 | _TD.a.push(function (TD) { 110 | 111 | TD.Act = function (stage, id) { 112 | this.stage = stage; 113 | this.id = id || ("act-" + TD.lang.rndStr()); 114 | 115 | /* 116 | * state 有以下几种状态: 117 | * 0: 等待中 118 | * 1: 运行中 119 | * 2: 暂停 120 | * 3: 已结束 121 | */ 122 | this.state = 0; 123 | this.scenes = []; 124 | this.end_queue = []; // 本 act 结束后要执行的队列,添加时请保证里面全是函数 125 | this.current_scene = null; 126 | 127 | this._init(); 128 | }; 129 | 130 | TD.Act.prototype = { 131 | _init: function () { 132 | this.stage.addAct(this); 133 | }, 134 | /* 135 | * 开始当前 act 136 | */ 137 | start: function () { 138 | if (this.stage.current_act && this.stage.current_act.state != 3) { 139 | // queue... 140 | this.state = 0; 141 | this.stage.current_act.queue(this.start); 142 | return; 143 | } 144 | // start 145 | this.state = 1; 146 | this.stage.current_act = this; 147 | TD.lang.each(this.scenes, function (obj) { 148 | obj.start(); 149 | }); 150 | }, 151 | pause: function () { 152 | this.state = 2; 153 | }, 154 | end: function () { 155 | this.state = 3; 156 | var f; 157 | while (f = this.end_queue.shift()) { 158 | f(); 159 | } 160 | this.stage.current_act = null; 161 | }, 162 | queue: function (f) { 163 | this.end_queue.push(f); 164 | }, 165 | clear: function () { 166 | this.state = 3; 167 | TD.lang.each(this.scenes, function (obj) { 168 | obj.clear(); 169 | }); 170 | // delete this; 171 | }, 172 | step: function () { 173 | if (this.state != 1 || !this.current_scene) return; 174 | this.current_scene.step(); 175 | }, 176 | render: function () { 177 | if (this.state == 0 || this.state == 3 || !this.current_scene) return; 178 | this.current_scene.render(); 179 | }, 180 | addScene: function (scene) { 181 | this.scenes.push(scene); 182 | }, 183 | addElement: function (el, step_level, render_level) { 184 | if (this.current_scene) 185 | this.current_scene.addElement(el, step_level, render_level); 186 | }, 187 | gameover: function () { 188 | //this.is_paused = true; 189 | //this.is_gameover = true; 190 | this.current_scene.gameover(); 191 | } 192 | }; 193 | 194 | }); // _TD.a.push end 195 | 196 | 197 | // _TD.a.push begin 198 | _TD.a.push(function (TD) { 199 | 200 | TD.Scene = function (act, id) { 201 | this.act = act; 202 | this.stage = act.stage; 203 | this.is_gameover = false; 204 | this.id = id || ("scene-" + TD.lang.rndStr()); 205 | /* 206 | * state 有以下几种状态: 207 | * 0: 等待中 208 | * 1: 运行中 209 | * 2: 暂停 210 | * 3: 已结束 211 | */ 212 | this.state = 0; 213 | this.end_queue = []; // 本 scene 结束后要执行的队列,添加时请保证里面全是函数 214 | this._step_elements = [ 215 | // step 共分为 3 层 216 | [], 217 | // 0 218 | [], 219 | // 1 默认 220 | [] // 2 221 | ]; 222 | this._render_elements = [ // 渲染共分为 10 层 223 | [], // 0 背景 1 背景图片 224 | [], // 1 背景 2 225 | [], // 2 背景 3 地图、格子 226 | [], // 3 地面 1 一般建筑 227 | [], // 4 地面 2 人物、NPC等 228 | [], // 5 地面 3 229 | [], // 6 天空 1 子弹等 230 | [], // 7 天空 2 主地图外边的遮罩,panel 231 | [], // 8 天空 3 232 | [] // 9 系统特殊操作,如选中高亮,提示、文字遮盖等 233 | ]; 234 | 235 | this._init(); 236 | }; 237 | 238 | TD.Scene.prototype = { 239 | _init: function () { 240 | this.act.addScene(this); 241 | this.wave = 0; // 第几波 242 | }, 243 | start: function () { 244 | if (this.act.current_scene && 245 | this.act.current_scene != this && 246 | this.act.current_scene.state != 3) { 247 | // queue... 248 | this.state = 0; 249 | this.act.current_scene.queue(this.start); 250 | return; 251 | } 252 | // start 253 | this.state = 1; 254 | this.act.current_scene = this; 255 | }, 256 | pause: function () { 257 | this.state = 2; 258 | }, 259 | end: function () { 260 | this.state = 3; 261 | var f; 262 | while (f = this.end_queue.shift()) { 263 | f(); 264 | } 265 | this.clear(); 266 | this.act.current_scene = null; 267 | }, 268 | /** 269 | * 清空场景 270 | */ 271 | clear: function () { 272 | // 清空本 scene 中引用的所有对象以回收内存 273 | TD.lang.shift(this._step_elements, function (obj) { 274 | TD.lang.shift(obj, function (obj2) { 275 | // element 276 | //delete this.scene; 277 | obj2.del(); 278 | // delete this; 279 | }); 280 | // delete this; 281 | }); 282 | TD.lang.shift(this._render_elements, function (obj) { 283 | TD.lang.shift(obj, function (obj2) { 284 | // element 285 | //delete this.scene; 286 | obj2.del(); 287 | // delete this; 288 | }); 289 | // delete this; 290 | }); 291 | // delete this; 292 | }, 293 | queue: function (f) { 294 | this.end_queue.push(f); 295 | }, 296 | gameover: function () { 297 | if (this.is_gameover) return; 298 | this.pause(); 299 | this.is_gameover = true; 300 | }, 301 | step: function () { 302 | if (this.state != 1) return; 303 | if (TD.life <= 0) { 304 | TD.life = 0; 305 | this.gameover(); 306 | } 307 | 308 | var i, a; 309 | for (i = 0; i < 3; i++) { 310 | a = []; 311 | var level_elements = this._step_elements[i]; 312 | TD.lang.shift(level_elements, function (obj) { 313 | if (obj.is_valid) { 314 | if (!obj.is_paused) 315 | obj.step(); 316 | a.push(obj); 317 | } else { 318 | setTimeout(function () { 319 | obj = null; 320 | }, 500); // 一会儿之后将这个对象彻底删除以收回内存 321 | } 322 | }); 323 | this._step_elements[i] = a; 324 | } 325 | }, 326 | render: function () { 327 | if (this.state == 0 || this.state == 3) return; 328 | var i, a, 329 | ctx = TD.ctx; 330 | 331 | ctx.clearRect(0, 0, this.stage.width, this.stage.height); 332 | 333 | for (i = 0; i < 10; i++) { 334 | a = []; 335 | var level_elements = this._render_elements[i]; 336 | TD.lang.shift(level_elements, function (obj) { 337 | if (obj.is_valid) { 338 | if (obj.is_visiable) 339 | obj.render(); 340 | a.push(obj); 341 | } 342 | }); 343 | this._render_elements[i] = a; 344 | } 345 | 346 | if (this.is_gameover) { 347 | this.panel.gameover_obj.show(); 348 | } 349 | }, 350 | addElement: function (el, step_level, render_level) { 351 | //TD.log([step_level, render_level]); 352 | step_level = step_level || el.step_level || 1; 353 | render_level = render_level || el.render_level; 354 | this._step_elements[step_level].push(el); 355 | this._render_elements[render_level].push(el); 356 | el.scene = this; 357 | el.step_level = step_level; 358 | el.render_level = render_level; 359 | } 360 | }; 361 | 362 | }); // _TD.a.push end 363 | -------------------------------------------------------------------------------- /src/js/td-walk.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | * Last Update: 2011/1/10 5:22:52 8 | */ 9 | 10 | 11 | // _TD.a.push begin 12 | _TD.a.push(function (TD) { 13 | 14 | /** 15 | * 使用 A* 算法(Dijkstra算法?)寻找从 (x1, y1) 到 (x2, y2) 最短的路线 16 | * 17 | */ 18 | TD.FindWay = function (w, h, x1, y1, x2, y2, f_passable) { 19 | this.m = []; 20 | this.w = w; 21 | this.h = h; 22 | this.x1 = x1; 23 | this.y1 = y1; 24 | this.x2 = x2; 25 | this.y2 = y2; 26 | this.way = []; 27 | this.len = this.w * this.h; 28 | this.is_blocked = this.is_arrived = false; 29 | this.fPassable = typeof f_passable == "function" ? f_passable : function () { 30 | return true; 31 | }; 32 | 33 | this._init(); 34 | }; 35 | 36 | TD.FindWay.prototype = { 37 | _init: function () { 38 | if (this.x1 == this.x2 && this.y1 == this.y2) { 39 | // 如果输入的坐标已经是终点了 40 | this.is_arrived = true; 41 | this.way = [ 42 | [this.x1, this.y1] 43 | ]; 44 | return; 45 | } 46 | 47 | for (var i = 0; i < this.len; i++) 48 | this.m[i] = -2; // -2 表示未探索过,-1 表示不可到达 49 | 50 | this.x = this.x1; 51 | this.y = this.y1; 52 | this.distance = 0; 53 | this.current = [ 54 | [this.x, this.y] 55 | ]; // 当前一步探索的格子 56 | 57 | this.setVal(this.x, this.y, 0); 58 | 59 | while (this.next()) { 60 | } 61 | }, 62 | getVal: function (x, y) { 63 | var p = y * this.w + x; 64 | return p < this.len ? this.m[p] : -1; 65 | }, 66 | setVal: function (x, y, v) { 67 | var p = y * this.w + x; 68 | if (p > this.len) return false; 69 | this.m[p] = v; 70 | }, 71 | /** 72 | * 得到指定坐标的邻居,即从指定坐标出发,1 步之内可以到达的格子 73 | * 目前返回的是指定格子的上、下、左、右四个邻格 74 | * @param x {Number} 75 | * @param y {Number} 76 | */ 77 | getNeighborsOf: function (x, y) { 78 | var nbs = []; 79 | if (y > 0) nbs.push([x, y - 1]); 80 | if (x < this.w - 1) nbs.push([x + 1, y]); 81 | if (y < this.h - 1) nbs.push([x, y + 1]); 82 | if (x > 0) nbs.push([x - 1, y]); 83 | 84 | return nbs; 85 | }, 86 | /** 87 | * 取得当前一步可到达的 n 个格子的所有邻格 88 | */ 89 | getAllNeighbors: function () { 90 | var nbs = [], nb1, i, c, l = this.current.length; 91 | for (i = 0; i < l; i++) { 92 | c = this.current[i]; 93 | nb1 = this.getNeighborsOf(c[0], c[1]); 94 | nbs = nbs.concat(nb1); 95 | } 96 | return nbs; 97 | }, 98 | /** 99 | * 从终点倒推,寻找从起点到终点最近的路径 100 | * 此处的实现是,从终点开始,从当前格子的邻格中寻找值最低(且大于 0)的格子, 101 | * 直到到达起点。 102 | * 这个实现需要反复地寻找邻格,有时邻格中有多个格子的值都为最低,这时就从中 103 | * 随机选取一个。还有一种实现方式是在一开始的遍历中,给每一个到达过的格子添加 104 | * 一个值,指向它的来时的格子(父格子)。 105 | */ 106 | findWay: function () { 107 | var x = this.x2, 108 | y = this.y2, 109 | nb, max_len = this.len, 110 | nbs_len, 111 | nbs, i, l, v, min_v = -1, 112 | closest_nbs; 113 | 114 | while ((x != this.x1 || y != this.y1) && min_v != 0 && 115 | this.way.length < max_len) { 116 | 117 | this.way.unshift([x, y]); 118 | 119 | nbs = this.getNeighborsOf(x, y); 120 | nbs_len = nbs.length; 121 | closest_nbs = []; 122 | 123 | // 在邻格中寻找最小的 v 124 | min_v = -1; 125 | for (i = 0; i < nbs_len; i++) { 126 | v = this.getVal(nbs[i][0], nbs[i][1]); 127 | if (v < 0) continue; 128 | if (min_v < 0 || min_v > v) 129 | min_v = v; 130 | } 131 | // 找出所有 v 最小的邻格 132 | for (i = 0; i < nbs_len; i++) { 133 | nb = nbs[i]; 134 | if (min_v == this.getVal(nb[0], nb[1])) { 135 | closest_nbs.push(nb); 136 | } 137 | } 138 | 139 | // 从 v 最小的邻格中随机选取一个作为当前格子 140 | l = closest_nbs.length; 141 | i = l > 1 ? Math.floor(Math.random() * l) : 0; 142 | nb = closest_nbs[i]; 143 | 144 | x = nb[0]; 145 | y = nb[1]; 146 | } 147 | }, 148 | /** 149 | * 到达终点 150 | */ 151 | arrive: function () { 152 | this.current = []; 153 | this.is_arrived = true; 154 | 155 | this.findWay(); 156 | }, 157 | /** 158 | * 道路被阻塞 159 | */ 160 | blocked: function () { 161 | this.current = []; 162 | this.is_blocked = true; 163 | }, 164 | /** 165 | * 下一次迭代 166 | * @return {Boolean} 如果返回值为 true ,表示未到达终点,并且道路 167 | * 未被阻塞,可以继续迭代;否则表示不必继续迭代 168 | */ 169 | next: function () { 170 | var neighbors = this.getAllNeighbors(), nb, 171 | l = neighbors.length, 172 | valid_neighbors = [], 173 | x, y, 174 | i, v; 175 | 176 | this.distance++; 177 | 178 | for (i = 0; i < l; i++) { 179 | nb = neighbors[i]; 180 | x = nb[0]; 181 | y = nb[1]; 182 | if (this.getVal(x, y) != -2) continue; // 当前格子已探索过 183 | //grid = this.map.getGrid(x, y); 184 | //if (!grid) continue; 185 | 186 | if (this.fPassable(x, y)) { 187 | // 可通过 188 | 189 | /** 190 | * 从起点到当前格子的耗费 191 | * 这儿只是简单地把从起点到当前格子需要走几步作为耗费 192 | * 比较复杂的情况下,可能还需要考虑不同的路面耗费也会不同, 193 | * 比如沼泽地的耗费比平地要多。不过现在的版本中路况没有这么复杂, 194 | * 先不考虑。 195 | */ 196 | v = this.distance; 197 | 198 | valid_neighbors.push(nb); 199 | } else { 200 | // 不可通过或有建筑挡着 201 | v = -1; 202 | } 203 | 204 | this.setVal(x, y, v); 205 | 206 | if (x == this.x2 && y == this.y2) { 207 | this.arrive(); 208 | return false; 209 | } 210 | } 211 | 212 | if (valid_neighbors.length == 0) { 213 | this.blocked(); 214 | return false 215 | } 216 | this.current = valid_neighbors; 217 | 218 | return true; 219 | } 220 | }; 221 | 222 | }); // _TD.a.push end 223 | 224 | 225 | -------------------------------------------------------------------------------- /src/js/td.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011. 3 | * 4 | * Author: oldj 5 | * Blog: http://oldj.net/ 6 | * 7 | */ 8 | 9 | var _TD = { 10 | a: [], 11 | retina: window.devicePixelRatio || 1, 12 | init: function (td_board, is_debug) { 13 | delete this.init; // 一旦初始化运行,即删除这个入口引用,防止初始化方法被再次调用 14 | 15 | var i, TD = { 16 | version: "0.1.17", // 版本命名规范参考:http://semver.org/ 17 | is_debug: !!is_debug, 18 | is_paused: true, 19 | width: 16, // 横向多少个格子 20 | height: 16, // 纵向多少个格子 21 | show_monster_life: true, // 是否显示怪物的生命值 22 | fps: 0, 23 | exp_fps: 24, // 期望的 fps 24 | exp_fps_half: 12, 25 | exp_fps_quarter: 6, 26 | exp_fps_eighth: 4, 27 | stage_data: {}, 28 | defaultSettings: function () { 29 | return { 30 | step_time: 36, // 每一次 step 循环之间相隔多少毫秒 31 | grid_size: 32 * _TD.retina, // px 32 | padding: 10 * _TD.retina, // px 33 | global_speed: 0.1 // 全局速度系数 34 | }; 35 | }, 36 | 37 | /** 38 | * 初始化 39 | * @param ob_board 40 | */ 41 | init: function (ob_board/*, ob_info*/) { 42 | this.obj_board = TD.lang.$e(ob_board); 43 | this.canvas = this.obj_board.getElementsByTagName("canvas")[0]; 44 | //this.obj_info = TD.lang.$e(ob_info); 45 | if (!this.canvas.getContext) return; // 不支持 canvas 46 | this.ctx = this.canvas.getContext("2d"); 47 | this.monster_type_count = TD.getDefaultMonsterAttributes(); // 一共有多少种怪物 48 | this.iframe = 0; // 当前播放到第几帧了 49 | this.last_iframe_time = (new Date()).getTime(); 50 | this.fps = 0; 51 | 52 | this.start(); 53 | }, 54 | 55 | /** 56 | * 开始游戏,或重新开始游戏 57 | */ 58 | start: function () { 59 | clearTimeout(this._st); 60 | TD.log("Start!"); 61 | var _this = this; 62 | this._exp_fps_0 = this.exp_fps - 0.4; // 下限 63 | this._exp_fps_1 = this.exp_fps + 0.4; // 上限 64 | 65 | this.mode = "normal"; // mode 分为 normail(普通模式)及 build(建造模式)两种 66 | this.eventManager.clear(); // 清除事件管理器中监听的事件 67 | this.lang.mix(this, this.defaultSettings()); 68 | this.stage = new TD.Stage("stage-main", TD.getDefaultStageData("stage_main")); 69 | 70 | this.canvas.setAttribute("width", this.stage.width); 71 | this.canvas.setAttribute("height", this.stage.height); 72 | this.canvas.style.width = (this.stage.width / _TD.retina) + "px"; 73 | this.canvas.style.height = (this.stage.height / _TD.retina) + "px"; 74 | 75 | this.canvas.onmousemove = function (e) { 76 | var xy = _this.getEventXY.call(_this, e); 77 | _this.hover(xy[0], xy[1]); 78 | }; 79 | this.canvas.onclick = function (e) { 80 | var xy = _this.getEventXY.call(_this, e); 81 | _this.click(xy[0], xy[1]); 82 | }; 83 | 84 | this.is_paused = false; 85 | this.stage.start(); 86 | this.step(); 87 | 88 | return this; 89 | }, 90 | 91 | /** 92 | * 作弊方法 93 | * @param cheat_code 94 | * 95 | * 用例: 96 | * 1、增加 100 万金钱:javascript:_TD.cheat="money+";void(0); 97 | * 2、难度增倍:javascript:_TD.cheat="difficulty+";void(0); 98 | * 3、难度减半:javascript:_TD.cheat="difficulty-";void(0); 99 | * 4、生命值恢复:javascript:_TD.cheat="life+";void(0); 100 | * 5、生命值降为最低:javascript:_TD.cheat="life-";void(0); 101 | */ 102 | checkCheat: function (cheat_code) { 103 | switch (cheat_code) { 104 | case "money+": 105 | this.money += 1000000; 106 | this.log("cheat success!"); 107 | break; 108 | case "life+": 109 | this.life = 100; 110 | this.log("cheat success!"); 111 | break; 112 | case "life-": 113 | this.life = 1; 114 | this.log("cheat success!"); 115 | break; 116 | case "difficulty+": 117 | this.difficulty *= 2; 118 | this.log("cheat success! difficulty = " + this.difficulty); 119 | break; 120 | case "difficulty-": 121 | this.difficulty /= 2; 122 | this.log("cheat success! difficulty = " + this.difficulty); 123 | break; 124 | } 125 | }, 126 | 127 | /** 128 | * 主循环方法 129 | */ 130 | step: function () { 131 | 132 | if (this.is_debug && _TD && _TD.cheat) { 133 | // 检查作弊代码 134 | this.checkCheat(_TD.cheat); 135 | _TD.cheat = ""; 136 | } 137 | 138 | if (this.is_paused) return; 139 | 140 | this.iframe++; // 当前总第多少帧 141 | if (this.iframe % 50 == 0) { 142 | // 计算 fps 143 | var t = (new Date()).getTime(), 144 | step_time = this.step_time; 145 | this.fps = Math.round(500000 / (t - this.last_iframe_time)) / 10; 146 | this.last_iframe_time = t; 147 | 148 | // 动态调整 step_time ,保证 fps 恒定为 24 左右 149 | if (this.fps < this._exp_fps_0 && step_time > 1) { 150 | step_time--; 151 | } else if (this.fps > this._exp_fps_1) { 152 | step_time++; 153 | } 154 | // if (step_time != this.step_time) 155 | // TD.log("FPS: " + this.fps + ", Step Time: " + step_time); 156 | this.step_time = step_time; 157 | } 158 | if (this.iframe % 2400 == 0) TD.gc(); // 每隔一段时间自动回收垃圾 159 | 160 | this.stage.step(); 161 | this.stage.render(); 162 | 163 | var _this = this; 164 | this._st = setTimeout(function () { 165 | _this.step(); 166 | }, this.step_time); 167 | }, 168 | 169 | /** 170 | * 取得事件相对于 canvas 左上角的坐标 171 | * @param e 172 | */ 173 | getEventXY: function (e) { 174 | var wra = TD.lang.$e("wrapper"), 175 | x = e.clientX - wra.offsetLeft - this.canvas.offsetLeft + Math.max(document.documentElement.scrollLeft, document.body.scrollLeft), 176 | y = e.clientY - wra.offsetTop - this.canvas.offsetTop + Math.max(document.documentElement.scrollTop, document.body.scrollTop); 177 | 178 | return [x * _TD.retina, y * _TD.retina]; 179 | }, 180 | 181 | /** 182 | * 鼠标移到指定位置事件 183 | * @param x 184 | * @param y 185 | */ 186 | hover: function (x, y) { 187 | this.eventManager.hover(x, y); 188 | }, 189 | 190 | /** 191 | * 点击事件 192 | * @param x 193 | * @param y 194 | */ 195 | click: function (x, y) { 196 | this.eventManager.click(x, y); 197 | }, 198 | 199 | /** 200 | * 是否将 canvas 中的鼠标指针变为手的形状 201 | * @param v {Boolean} 202 | */ 203 | mouseHand: function (v) { 204 | this.canvas.style.cursor = v ? "pointer" : "default"; 205 | }, 206 | 207 | /** 208 | * 显示调试信息,只在 is_debug 为 true 的情况下有效 209 | * @param txt 210 | */ 211 | log: function (txt) { 212 | this.is_debug && window.console && console.log && console.log(txt); 213 | }, 214 | 215 | /** 216 | * 回收内存 217 | * 注意:CollectGarbage 只在 IE 下有效 218 | */ 219 | gc: function () { 220 | if (window.CollectGarbage) { 221 | CollectGarbage(); 222 | setTimeout(CollectGarbage, 1); 223 | } 224 | } 225 | }; 226 | 227 | for (i = 0; this.a[i]; i++) { 228 | // 依次执行添加到列表中的函数 229 | this.a[i](TD); 230 | } 231 | delete this.a; 232 | 233 | TD.init(td_board); 234 | } 235 | }; 236 | -------------------------------------------------------------------------------- /src/td.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | HTML5 Tower Defense 7 | 8 | 9 | 10 | 11 |
12 |
13 |

HTML5 塔防游戏

14 | 15 |
加载中...
16 |
17 | 抱歉,您的浏览器不支持 HTML 5 Canvas 标签,请使用 IE9 / Chrome / Opera 等浏览器浏览本页以获得最佳效果。 18 |
19 |
20 |
21 | 关于 | 22 | 源码 | 23 | oldj.net © 2010-2011 24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /tools/compressor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.6 2 | # @author: allenm, oldj 3 | # 4 | # @link: https://github.com/allenm/js-css-compressor 5 | # @link: https://github.com/oldj/js-css-compressor 6 | # 7 | 8 | import httplib 9 | import urllib 10 | import sys 11 | import os 12 | 13 | # Define the parameters for the POST request and encode them in 14 | # a URL-safe format. 15 | 16 | 17 | def compressor(savename, filenames): 18 | ''' compressor and combine the javascript files. This script use the google closure REST API ''' 19 | 20 | filenames = (v.strip() for v in filenames.split(";")) 21 | code = [] 22 | for fn in filenames: 23 | if fn.startswith('http://'): 24 | # url 25 | code.append(('code_url', fn)) 26 | else: 27 | # local file 28 | if not os.path.isfile(fn): 29 | print 'ERROR: "%s" is not a valid file!' % fn 30 | return False 31 | code.append(('js_code', open(fn).read())) 32 | 33 | code.extend([ 34 | ('compilation_level', 'SIMPLE_OPTIMIZATIONS'), 35 | ('output_format', 'text'), 36 | ('output_info', 'compiled_code'), 37 | ]) 38 | 39 | params = urllib.urlencode(code) 40 | 41 | # Always use the following value for the Content-type header. 42 | headers = {'Content-type': 'application/x-www-form-urlencoded'} 43 | conn = httplib.HTTPConnection('closure-compiler.appspot.com') 44 | conn.request('POST', '/compile', params, headers) 45 | response = conn.getresponse() 46 | data = response.read() 47 | print 'DATA:' 48 | print '-' * 50 49 | print data.rstrip() 50 | conn.close() 51 | 52 | donefile = open(savename, 'w') 53 | donefile.write(data) 54 | donefile.close() 55 | 56 | print '-' * 50 57 | print '>> out: %s (%.2fK)' % (savename, len(data) / 1024.0) 58 | 59 | 60 | if __name__ == "__main__": 61 | 62 | if sys.argv.__len__() >= 3: 63 | compressor(sys.argv[1], sys.argv[2]) 64 | else: 65 | print '''This script must contain at least two parameters. 66 | The first one is the filename which you want store the data after compress, 67 | the second is the urls or filenames of javascript file which you want compress, 68 | if you have more than one file to compress, 69 | use ";" to partition them.''' 70 | -------------------------------------------------------------------------------- /tools/countjslines.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # 统计指定文件夹下的 js 一共有多少行 4 | 5 | import os 6 | from glob import glob 7 | 8 | src_folder = "../src/js" 9 | 10 | if __name__ == "__main__": 11 | print "lines: %d" % sum(len(open(fn).readlines()) for \ 12 | fn in glob(os.path.join(src_folder.replace("/", os.sep), "*.js"))) 13 | raw_input("") 14 | 15 | -------------------------------------------------------------------------------- /tools/deploy.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | u""" 4 | 发布脚本 5 | 6 | 此脚本把 src 目录下的 js 合并成一个,压缩,放到 build 目录下, 7 | 再更新 build/td.html 里 .js 文件的时间戳以防止浏览器缓存。 8 | """ 9 | 10 | import os 11 | import re 12 | import time 13 | 14 | from compressor import compressor 15 | 16 | 17 | def updateHTML(): 18 | u"更新 td.html 中 js 文件的时间戳,以防止最终访问页面时的缓存" 19 | 20 | tdh = "../build/td.html" 21 | tdh = tdh.replace("/", os.sep) 22 | html = open(tdh).read() 23 | html = re.sub(r"\.js\?fmts=[\d\.]+", ".js?fmts=%.1f" % time.time(), html) 24 | open(tdh, "w").write(html) 25 | 26 | 27 | def compress(fn): 28 | u"压缩合并后的文件,需要网络支持" 29 | 30 | print "compressing..." 31 | path, filename = os.path.split(fn) 32 | compressed_fn = os.path.join(path, filename.replace("-pkg.js", "-pkg-min.js")) 33 | compressor(compressed_fn, fn) 34 | print "compressed!" 35 | 36 | 37 | def merge(): 38 | u"合并文件" 39 | 40 | src_folder = "../src/js" 41 | files = [ 42 | "td.js", "td-lang.js", "td-event.js", 43 | "td-stage.js", "td-element.js", 44 | "td-obj-map.js", "td-obj-grid.js", 45 | "td-obj-building.js", "td-obj-monster.js", 46 | "td-obj-panel.js", "td-data-stage-1.js", 47 | "td-cfg-buildings.js", "td-cfg-monsters.js", 48 | "td-render-buildings.js", "td-msg.js", 49 | "td-walk.js", 50 | ] 51 | build_folder = "../build" 52 | build_name = "td-pkg.js" 53 | 54 | print "merging..." 55 | src_folder = src_folder.replace("/", os.sep) 56 | build_folder = build_folder.replace("/", os.sep) 57 | c = "/** %s */" % build_name 58 | 59 | for fn in files: 60 | fn = os.path.join(src_folder, fn) 61 | if os.path.isfile(fn): 62 | c = "%s\n\n%s" % (c, open(fn).read()) 63 | else: 64 | print "ERROR: '%s' is not a file!" % fn 65 | return 66 | 67 | build_path = os.path.join(build_folder, build_name) 68 | print "save to '%s'" % build_path 69 | open(build_path, "w").write(c) 70 | 71 | print "merged!" 72 | 73 | return build_path 74 | 75 | if __name__ == "__main__": 76 | fn = merge() 77 | compress(fn) 78 | updateHTML() 79 | print "done!" 80 | --------------------------------------------------------------------------------