/* * Copyright (c) 2011. * * Author: oldj * Blog: http://oldj.net/ * */ var _TD = { a: [], retina: window.devicePixelRatio || 1, init: function (td_board, is_debug) { delete this.init; // 一旦初始化运行,即删除这个入口引用,防止初始化方法被再次调用 var i, TD = { version: "0.1.17", // 版本命名规范参考:http://semver.org/ is_debug: !!is_debug, is_paused: true, width: 16, // 横向多少个格子 height: 16, // 纵向多少个格子 show_monster_life: true, // 是否显示怪物的生命值 fps: 0, exp_fps: 24, // 期望的 fps exp_fps_half: 12, exp_fps_quarter: 6, exp_fps_eighth: 4, stage_data: {}, defaultSettings: function () { return { step_time: 36, // 每一次 step 循环之间相隔多少毫秒 grid_size: 32 * _TD.retina, // px padding: 10 * _TD.retina, // px global_speed: 0.1 // 全局速度系数 }; }, /** * 初始化 * @param ob_board */ init: function (ob_board/*, ob_info*/) { this.obj_board = TD.lang.$e(ob_board); this.canvas = this.obj_board.getElementsByTagName("canvas")[0]; //this.obj_info = TD.lang.$e(ob_info); if (!this.canvas.getContext) return; // 不支持 canvas this.ctx = this.canvas.getContext("2d"); // 让 canvas 可聚焦以确保能接收键盘/鼠标交互 try { this.canvas.tabIndex = 0; this.canvas.style.outline = 'none'; } catch (e) {} this.monster_type_count = TD.getDefaultMonsterAttributes(); // 一共有多少种怪物 this.iframe = 0; // 当前播放到第几帧了 this.last_iframe_time = (new Date()).getTime(); this.fps = 0; this.start(); }, /** * 开始游戏,或重新开始游戏 */ start: function () { clearTimeout(this._st); TD.log("Start!"); var _this = this; this._exp_fps_0 = this.exp_fps - 0.4; // 下限 this._exp_fps_1 = this.exp_fps + 0.4; // 上限 this.mode = "normal"; // mode 分为 normail(普通模式)及 build(建造模式)两种 this.eventManager.clear(); // 清除事件管理器中监听的事件 this.lang.mix(this, this.defaultSettings()); this.stage = new TD.Stage("stage-main", TD.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 (e) { var xy = _this.getEventXY.call(_this, e); _this.hover(xy[0], xy[1]); }; this.canvas.onclick = function (e) { var xy = _this.getEventXY.call(_this, e); _this.click(xy[0], xy[1]); }; this.is_paused = false; this.stage.start(); // 记录游戏开始时间(用于计算时长并在结束时上报) this.startedAt = (new Date()).getTime(); this.step(); return this; }, /** * 作弊方法 * @param cheat_code * * 用例: * 1、增加 100 万金钱:javascript:_TD.cheat="money+";void(0); * 2、难度增倍:javascript:_TD.cheat="difficulty+";void(0); * 3、难度减半:javascript:_TD.cheat="difficulty-";void(0); * 4、生命值恢复:javascript:_TD.cheat="life+";void(0); * 5、生命值降为最低:javascript:_TD.cheat="life-";void(0); */ checkCheat: function (cheat_code) { switch (cheat_code) { case "money+": this.money += 1000000; 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); break; } }, /** * 主循环方法 */ step: function () { if (this.is_debug && _TD && _TD.cheat) { // 检查作弊代码 this.checkCheat(_TD.cheat); _TD.cheat = ""; } if (this.is_paused) return; this.iframe++; // 当前总第多少帧 if (this.iframe % 50 == 0) { // 计算 fps var t = (new Date()).getTime(), step_time = this.step_time; this.fps = Math.round(500000 / (t - this.last_iframe_time)) / 10; this.last_iframe_time = t; // 动态调整 step_time ,保证 fps 恒定为 24 左右 if (this.fps < this._exp_fps_0 && step_time > 1) { step_time--; } else if (this.fps > this._exp_fps_1) { step_time++; } // if (step_time != this.step_time) // TD.log("FPS: " + this.fps + ", Step Time: " + step_time); this.step_time = step_time; } if (this.iframe % 2400 == 0) TD.gc(); // 每隔一段时间自动回收垃圾 this.stage.step(); this.stage.render(); var _this = this; this._st = setTimeout(function () { _this.step(); }, this.step_time); }, /** * 取得事件相对于 canvas 左上角的坐标 * @param e */ getEventXY: function (e) { // Use bounding client rect to correctly map event coordinates to canvas, // this handles iframe offsets, CSS scaling, and scrolling more reliably. try { var rect = this.canvas.getBoundingClientRect(); var x = (e.clientX - rect.left); var y = (e.clientY - rect.top); // map to canvas pixel coordinates in case canvas is scaled via CSS var scaleX = this.canvas.width / rect.width; var scaleY = this.canvas.height / rect.height; return [Math.round(x * scaleX), Math.round(y * scaleY)]; } catch (err) { // fallback to old method if something goes wrong var wra = TD.lang.$e("wrapper"), x = e.clientX - wra.offsetLeft - this.canvas.offsetLeft + Math.max(document.documentElement.scrollLeft, document.body.scrollLeft), y = e.clientY - wra.offsetTop - this.canvas.offsetTop + Math.max(document.documentElement.scrollTop, document.body.scrollTop); return [x * _TD.retina, y * _TD.retina]; } }, /** * 鼠标移到指定位置事件 * @param x * @param y */ hover: function (x, y) { this.eventManager.hover(x, y); }, /** * 点击事件 * @param x * @param y */ click: function (x, y) { this.eventManager.click(x, y); }, /** * 是否将 canvas 中的鼠标指针变为手的形状 * @param v {Boolean} */ mouseHand: function (v) { this.canvas.style.cursor = v ? "pointer" : "default"; }, /** * 显示调试信息,只在 is_debug 为 true 的情况下有效 * @param txt */ log: function (txt) { this.is_debug && window.console && console.log && console.log(txt); }, /** * 回收内存 * 注意:CollectGarbage 只在 IE 下有效 */ gc: function () { if (window.CollectGarbage) { CollectGarbage(); setTimeout(CollectGarbage, 1); } } }; for (i = 0; this.a[i]; i++) { // 依次执行添加到列表中的函数 this.a[i](TD); } delete this.a; // Expose simple save/load helpers to window so parent can request state from iframe try { window.__TD_INSTANCE = TD; window.__TD_getState = function () { try { var scene = TD.stage && TD.stage.current_act && TD.stage.current_act.current_scene; var map = (TD.stage && TD.stage.map) || (scene && scene.map) || null; var buildings = []; if (map && map.buildings && map.buildings.length) { for (var i = 0; i < map.buildings.length; i++) { var b = map.buildings[i]; if (!b || !b.grid) continue; buildings.push({ type: b.type, mx: b.grid.mx, my: b.grid.my, level: b.level, money: b.money }); } } return { money: TD.money, life: TD.life, score: TD.score, wave: (scene && scene.wave) || 0, buildings: buildings }; } catch (e) { console.error('__TD_getState error', e); return null; } }; window.__TD_loadState = function (s) { try { if (!s) return; var scene = TD.stage && TD.stage.current_act && TD.stage.current_act.current_scene; var map = (TD.stage && TD.stage.map) || (scene && scene.map) || null; if (!map) return; // remove existing buildings for (var gi = 0; gi < map.grids.length; gi++) { var g = map.grids[gi]; if (g && g.building) g.removeBuilding(); } // add saved buildings if (s.buildings && s.buildings.length) { for (var j = 0; j < s.buildings.length; j++) { var bi = s.buildings[j]; var grid = map.getGrid(bi.mx, bi.my); if (grid) grid.addBuilding(bi.type); var b = grid && grid.building; if (b) { if (typeof bi.level !== 'undefined') b.level = bi.level; if (typeof bi.money !== 'undefined') b.money = bi.money; b.updateBtnDesc && b.updateBtnDesc(); } } } if (typeof s.money !== 'undefined') TD.money = s.money; if (typeof s.life !== 'undefined') TD.life = s.life; if (typeof s.score !== 'undefined') TD.score = s.score; if (typeof s.wave !== 'undefined' && scene) scene.wave = s.wave; } catch (e) { console.error('__TD_loadState error', e); } }; } catch (e) { console.warn('expose TD api failed', e); } TD.init(td_board); } }; /* * Copyright (c) 2011. * * Author: oldj * Blog: http://oldj.net/ * * Last Update: 2011/1/10 5:22:52 */ // _TD.a.push begin _TD.a.push(function (TD) { TD.lang = { /** * document.getElementById 方法的简写 * @param el_id {String} */ $e: function (el_id) { return document.getElementById(el_id); }, /** * 创建一个 DOM 元素 * @param tag_name {String} * @param attributes {Object} * @param parent_node {HTMLElement} * @return {HTMLElement} */ $c: function (tag_name, attributes, parent_node) { var el = document.createElement(tag_name); attributes = attributes || {}; for (var k in attributes) { if (attributes.hasOwnProperty(k)) { el.setAttribute(k, attributes[k]); } } if (parent_node) parent_node.appendChild(el); return el; }, /** * 从字符串 s 左边截取n个字符 * 如果包含汉字,则汉字按两个字符计算 * @param s {String} 输入的字符串 * @param n {Number} */ strLeft: function (s, n) { var s2 = s.slice(0, n), i = s2.replace(/[^\x00-\xff]/g, "**").length; if (i <= n) return s2; i -= s2.length; switch (i) { case 0: return s2; case n: return s.slice(0, n >> 1); default: var k = n - i, s3 = s.slice(k, n), j = s3.replace(/[\x00-\xff]/g, "").length; return j ? s.slice(0, k) + this.arguments.callee(s3, j) : s.slice(0, k); } }, /** * 取得一个字符串的字节长度 * 汉字等字符长度算2,英文、数字等算1 * @param s {String} */ strLen2: function (s) { return s.replace(/[^\x00-\xff]/g, "**").length; }, /** * 对一个数组的每一个元素执行指定方法 * @param list {Array} * @param f {Function} */ each: function (list, f) { if (Array.prototype.forEach) { list.forEach(f); } else { for (var i = 0, l = list.length; i < l; i++) { f(list[i]); } } }, /** * 对一个数组的每一项依次执行指定方法,直到某一项的返回值为 true * 返回第一个令 f 值为 true 的元素,如没有元素令 f 值为 true,则 * 返回 null * @param list {Array} * @param f {Function} * @return {Object} */ any: function (list, f) { for (var i = 0, l = list.length; i < l; i++) { if (f(list[i])) return list[i]; } return null; }, /** * 依次弹出列表中的元素,并对其进行操作 * 注意,执行完毕之后原数组将被清空 * 类似于 each,不同的是这个函数执行完后原数组将被清空 * @param list {Array} * @param f {Function} */ shift: function (list, f) { while (list[0]) { f(list.shift()); // f.apply(list.shift(), args); } }, /** * 传入一个数组,将其随机排序并返回 * 返回的是一个新的数组,原数组不变 * @param list {Array} * @return {Array} */ rndSort: function (list) { var a = list.concat(); return a.sort(function () { return Math.random() - 0.5; }); }, _rndRGB2: function (v) { var s = v.toString(16); return s.length == 2 ? s : ("0" + s); }, /** * 随机生成一个 RGB 颜色 */ rndRGB: function () { var r = Math.floor(Math.random() * 256), g = Math.floor(Math.random() * 256), b = Math.floor(Math.random() * 256); return "#" + this._rndRGB2(r) + this._rndRGB2(g) + this._rndRGB2(b); }, /** * 将一个 rgb 色彩字符串转化为一个数组 * eg: '#ffffff' => [255, 255, 255] * @param rgb_str {String} rgb色彩字符串,类似于“#f8c693” */ rgb2Arr: function (rgb_str) { if (rgb_str.length != 7) return [0, 0, 0]; var r = rgb_str.substr(1, 2), g = rgb_str.substr(3, 2), b = rgb_str.substr(3, 2); return [parseInt(r, 16), parseInt(g, 16), parseInt(b, 16)]; }, /** * 生成一个长度为 n 的随机字符串 * * @param [n] {Number} */ rndStr: function (n) { n = n || 16; var chars = "1234567890abcdefghijklmnopqrstuvwxyz", a = [], i, chars_len = chars.length, r; for (i = 0; i < n; i++) { r = Math.floor(Math.random() * chars_len); a.push(chars.substr(r, 1)); } return a.join(""); }, /** * 空函数,一般用于占位 */ nullFunc: function () { }, /** * 判断两个数组是否相等 * * @param arr1 {Array} * @param arr2 {Array} */ arrayEqual: function (arr1, arr2) { var i, l = arr1.length; if (l != arr2.length) return false; for (i = 0; i < l; i++) { if (arr1[i] != arr2[i]) return false; } return true; }, /** * 将所有 s 的属性复制给 r * @param r {Object} * @param s {Object} * @param [is_overwrite] {Boolean} 如指定为 false ,则不覆盖已有的值,其它值 * 包括 undefined ,都表示 s 中的同名属性将覆盖 r 中的值 */ mix: function (r, s, is_overwrite) { if (!s || !r) return r; for (var p in s) { if (s.hasOwnProperty(p) && (is_overwrite !== false || !(p in r))) { r[p] = s[p]; } } return r; } }; }); // _TD.a.push end /* * Copyright (c) 2011. * * Author: oldj * Blog: http://oldj.net/ * * Last Update: 2011/1/10 5:22:52 */ // _TD.a.push begin _TD.a.push(function (TD) { /** * 事件管理器 */ TD.eventManager = { ex: -1, // 事件坐标 x ey: -1, // 事件坐标 y _registers: {}, // 注册监听事件的元素 // 目前支持的事件类型 ontypes: [ "enter", // 鼠标移入 "hover", // 鼠标在元素上,相当于 onmouseover "out", // 鼠标移出 "click" // 鼠标点击 ], // 当前事件类型 current_type: "hover", /** * 根据事件坐标,判断事件是否在元素上 * @param el {Element} Element 元素 * @return {Boolean} */ isOn: function (el) { return (this.ex != -1 && this.ey != -1 && this.ex > el.x && this.ex < el.x2 && this.ey > el.y && this.ey < el.y2); }, /** * 根据元素名、事件名,生成一个字符串标识,用于注册事件监听 * @param el {Element} * @param evt_type {String} * @return evt_name {String} 字符串标识 */ _mkElEvtName: function (el, evt_type) { return el.id + "::_evt_::" + evt_type; }, /** * 为元素注册事件监听 * 现在的实现比较简单,如果一个元素对某个事件多次注册监听,后面的监听将会覆盖前面的 * * @param el {Element} * @param evt_type {String} * @param f {Function} */ on: function (el, evt_type, f) { this._registers[this._mkElEvtName(el, evt_type)] = [el, evt_type, f]; }, /** * 移除元素对指定事件的监听 * @param el {Element} * @param evt_type {String} */ removeEventListener: function (el, evt_type) { var en = this._mkElEvtName(el, evt_type); delete this._registers[en]; }, /** * 清除所有监听事件 */ clear: function () { delete this._registers; this._registers = {}; //this.elements = []; }, /** * 主循环方法 */ step: function () { if (!this.current_type) return; // 没有事件被触发 var k, a, el, et, f, //en, j, _this = this, ontypes_len = this.ontypes.length, is_evt_on, // reg_length = 0, to_del_el = []; //var m = TD.stage.current_act.current_scene.map; //TD.log([m.is_hover, this.isOn(m)]); // 遍历当前注册的事件 for (k in this._registers) { // reg_length ++; if (!this._registers.hasOwnProperty(k)) continue; a = this._registers[k]; el = a[0]; // 事件对应的元素 et = a[1]; // 事件类型 f = a[2]; // 事件处理函数 if (!el.is_valid) { to_del_el.push(el); continue; } if (!el.is_visiable) continue; // 不可见元素不响应事件 is_evt_on = this.isOn(el); // 事件是否发生在元素上 if (this.current_type != "click") { // enter / out / hover 事件 if (et == "hover" && el.is_hover && is_evt_on) { // 普通的 hover f(); this.current_type = "hover"; } else if (et == "enter" && !el.is_hover && is_evt_on) { // enter 事件 el.is_hover = true; f(); this.current_type = "enter"; } else if (et == "out" && el.is_hover && !is_evt_on) { // out 事件 el.is_hover = false; f(); this.current_type = "out"; // } else { // 事件与当前元素无关 // continue; } } else { // click 事件 if (is_evt_on && et == "click") f(); } } // 删除指定元素列表的事件 TD.lang.each(to_del_el, function (obj) { for (j = 0; j < ontypes_len; j++) _this.removeEventListener(obj, _this.ontypes[j]); }); // TD.log(reg_length); this.current_type = ""; }, /** * 鼠标在元素上 * @param ex {Number} * @param ey {Number} */ hover: function (ex, ey) { // 如果还有 click 事件未处理则退出,点击事件具有更高的优先级 if (this.current_type == "click") return; this.current_type = "hover"; this.ex = ex; this.ey = ey; }, /** * 点击事件 * @param ex {Number} * @param ey {Number} */ click: function (ex, ey) { this.current_type = "click"; this.ex = ex; this.ey = ey; } }; }); // _TD.a.push end /* * Copyright (c) 2011. * * Author: oldj * Blog: http://oldj.net/ * * Last Update: 2011/1/10 5:22:52 */ // _TD.a.push begin _TD.a.push(function (TD) { /** * 舞台类 * @param id {String} 舞台ID * @param cfg {Object} 配置 */ TD.Stage = function (id, cfg) { this.id = id || ("stage-" + TD.lang.rndStr()); this.cfg = cfg || {}; this.width = this.cfg.width || 640; this.height = this.cfg.height || 540; /** * mode 有以下状态: * "normal": 普通状态 * "build": 建造模式 */ this.mode = "normal"; /* * state 有以下几种状态: * 0: 等待中 * 1: 运行中 * 2: 暂停 * 3: 已结束 */ this.state = 0; this.acts = []; this.current_act = null; this._step2 = TD.lang.nullFunc; this._init(); }; TD.Stage.prototype = { _init: function () { if (typeof this.cfg.init == "function") { this.cfg.init.call(this); } if (typeof this.cfg.step2 == "function") { this._step2 = this.cfg.step2; } }, start: function () { this.state = 1; TD.lang.each(this.acts, function (obj) { obj.start(); }); }, pause: function () { this.state = 2; }, gameover: function () { //this.pause(); this.current_act.gameover(); }, /** * 清除本 stage 所有物品 */ clear: function () { this.state = 3; TD.lang.each(this.acts, function (obj) { obj.clear(); }); // delete this; }, /** * 主循环函数 */ step: function () { if (this.state != 1 || !this.current_act) return; TD.eventManager.step(); this.current_act.step(); this._step2(); }, /** * 绘制函数 */ render: function () { if (this.state == 0 || this.state == 3 || !this.current_act) return; this.current_act.render(); }, addAct: function (act) { this.acts.push(act); }, addElement: function (el, step_level, render_level) { if (this.current_act) this.current_act.addElement(el, step_level, render_level); } }; }); // _TD.a.push end // _TD.a.push begin _TD.a.push(function (TD) { TD.Act = function (stage, id) { this.stage = stage; this.id = id || ("act-" + TD.lang.rndStr()); /* * state 有以下几种状态: * 0: 等待中 * 1: 运行中 * 2: 暂停 * 3: 已结束 */ this.state = 0; this.scenes = []; this.end_queue = []; // 本 act 结束后要执行的队列,添加时请保证里面全是函数 this.current_scene = null; this._init(); }; TD.Act.prototype = { _init: function () { this.stage.addAct(this); }, /* * 开始当前 act */ start: function () { if (this.stage.current_act && this.stage.current_act.state != 3) { // queue... this.state = 0; this.stage.current_act.queue(this.start); return; } // start this.state = 1; this.stage.current_act = this; TD.lang.each(this.scenes, function (obj) { obj.start(); }); }, pause: function () { this.state = 2; }, end: function () { this.state = 3; var f; while (f = this.end_queue.shift()) { f(); } this.stage.current_act = null; }, queue: function (f) { this.end_queue.push(f); }, clear: function () { this.state = 3; TD.lang.each(this.scenes, function (obj) { obj.clear(); }); // delete this; }, step: function () { if (this.state != 1 || !this.current_scene) return; this.current_scene.step(); }, render: function () { if (this.state == 0 || this.state == 3 || !this.current_scene) return; this.current_scene.render(); }, addScene: function (scene) { this.scenes.push(scene); }, addElement: function (el, step_level, render_level) { if (this.current_scene) this.current_scene.addElement(el, step_level, render_level); }, gameover: function () { //this.is_paused = true; //this.is_gameover = true; this.current_scene.gameover(); } }; }); // _TD.a.push end // _TD.a.push begin _TD.a.push(function (TD) { TD.Scene = function (act, id) { this.act = act; this.stage = act.stage; this.is_gameover = false; this.id = id || ("scene-" + TD.lang.rndStr()); /* * state 有以下几种状态: * 0: 等待中 * 1: 运行中 * 2: 暂停 * 3: 已结束 */ this.state = 0; this.end_queue = []; // 本 scene 结束后要执行的队列,添加时请保证里面全是函数 this._step_elements = [ // step 共分为 3 层 [], // 0 [], // 1 默认 [] // 2 ]; this._render_elements = [ // 渲染共分为 10 层 [], // 0 背景 1 背景图片 [], // 1 背景 2 [], // 2 背景 3 地图、格子 [], // 3 地面 1 一般建筑 [], // 4 地面 2 人物、NPC等 [], // 5 地面 3 [], // 6 天空 1 子弹等 [], // 7 天空 2 主地图外边的遮罩,panel [], // 8 天空 3 [] // 9 系统特殊操作,如选中高亮,提示、文字遮盖等 ]; this._init(); }; TD.Scene.prototype = { _init: function () { this.act.addScene(this); this.wave = 0; // 第几波 }, start: function () { if (this.act.current_scene && this.act.current_scene != this && this.act.current_scene.state != 3) { // queue... this.state = 0; this.act.current_scene.queue(this.start); return; } // start this.state = 1; this.act.current_scene = this; }, pause: function () { this.state = 2; }, end: function () { this.state = 3; var f; while (f = this.end_queue.shift()) { f(); } this.clear(); this.act.current_scene = null; }, /** * 清空场景 */ clear: function () { // 清空本 scene 中引用的所有对象以回收内存 TD.lang.shift(this._step_elements, function (obj) { TD.lang.shift(obj, function (obj2) { // element //delete this.scene; obj2.del(); // delete this; }); // delete this; }); TD.lang.shift(this._render_elements, function (obj) { TD.lang.shift(obj, function (obj2) { // element //delete this.scene; obj2.del(); // delete this; }); // delete this; }); // delete this; }, queue: function (f) { this.end_queue.push(f); }, gameover: function () { if (this.is_gameover) return; this.pause(); this.is_gameover = true; }, step: function () { if (this.state != 1) return; if (TD.life <= 0) { TD.life = 0; this.gameover(); } var i, a; for (i = 0; i < 3; i++) { a = []; var level_elements = this._step_elements[i]; TD.lang.shift(level_elements, function (obj) { if (obj.is_valid) { if (!obj.is_paused) obj.step(); a.push(obj); } else { setTimeout(function () { obj = null; }, 500); // 一会儿之后将这个对象彻底删除以收回内存 } }); this._step_elements[i] = a; } }, render: function () { if (this.state == 0 || this.state == 3) return; var i, a, ctx = TD.ctx; ctx.clearRect(0, 0, this.stage.width, this.stage.height); for (i = 0; i < 10; i++) { a = []; var level_elements = this._render_elements[i]; TD.lang.shift(level_elements, function (obj) { if (obj.is_valid) { if (obj.is_visiable) obj.render(); a.push(obj); } }); this._render_elements[i] = a; } if (this.is_gameover) { this.panel.gameover_obj.show(); } }, addElement: function (el, step_level, render_level) { //TD.log([step_level, render_level]); step_level = step_level || el.step_level || 1; render_level = render_level || el.render_level; this._step_elements[step_level].push(el); this._render_elements[render_level].push(el); el.scene = this; el.step_level = step_level; el.render_level = render_level; } }; }); // _TD.a.push end /* * Copyright (c) 2011. * * Author: oldj * Blog: http://oldj.net/ * * * 本文件定义了 Element 类,这个类是游戏中所有元素的基类, * 包括地图、格子、怪物、建筑、子弹、气球提示等都基于这个类 * */ // _TD.a.push begin _TD.a.push(function (TD) { /** * Element 是游戏中所有可控制元素的基类 * @param id {String} 给这个元素一个唯一的不重复的 ID,如果不指定则随机生成 * @param cfg {Object} 元素的配置信息 */ TD.Element = function (id, cfg) { this.id = id || ("el-" + TD.lang.rndStr()); this.cfg = cfg || {}; this.is_valid = true; this.is_visiable = typeof cfg.is_visiable != "undefined" ? cfg.is_visiable : true; this.is_paused = false; this.is_hover = false; 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 = cfg.step_level || 1; this.render_level = cfg.render_level; this.on_events = cfg.on_events || []; this._init(); }; TD.Element.prototype = { _init: function () { var _this = this, i, en, len; // 监听指定事件 for (i = 0, len = this.on_events.length; i < len; i++) { en = this.on_events[i]; switch (en) { // 鼠标进入元素 case "enter": this.on("enter", function () { _this.onEnter(); }); break; // 鼠标移出元素 case "out": this.on("out", function () { _this.onOut(); }); break; // 鼠标在元素上,相当于 DOM 中的 onmouseover case "hover": this.on("hover", function () { _this.onHover(); }); break; // 鼠标点击了元素 case "click": this.on("click", function () { _this.onClick(); }); break; } } 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 = false; }, pause: function () { this.is_paused = true; }, hide: function () { this.is_visiable = false; this.onOut(); }, show: function () { this.is_visiable = true; }, /** * 删除本元素 */ del: function () { this.is_valid = false; }, /** * 绑定指定类型的事件 * @param evt_type {String} 事件类型 * @param f {Function} 处理方法 */ on: function (evt_type, f) { TD.eventManager.on(this, evt_type, f); }, // 下面几个方法默认为空,实例中按需要重载 onEnter: TD.lang.nullFunc, onOut: TD.lang.nullFunc, onHover: TD.lang.nullFunc, onClick: TD.lang.nullFunc, step: TD.lang.nullFunc, render: TD.lang.nullFunc, /** * 将当前 element 加入到场景 scene 中 * 在加入本 element 之前,先加入 pre_add_list 中的element * @param scene * @param step_level {Number} * @param render_level {Number} * @param pre_add_list {Array} Optional [element1, element2, ...] */ addToScene: function (scene, step_level, render_level, pre_add_list) { this.scene = scene; if (isNaN(step_level)) return; this.step_level = step_level || this.step_level; this.render_level = render_level || this.render_level; if (pre_add_list) { TD.lang.each(pre_add_list, function (obj) { scene.addElement(obj, step_level, render_level); }); } scene.addElement(this, step_level, render_level); } }; }); // _TD.a.push end /* * Copyright (c) 2011. * * Author: oldj * Blog: http://oldj.net/ * * Last Update: 2011/1/10 5:22:52 */ // _TD.a.push begin _TD.a.push(function (TD) { var _default_wait_clearInvalidElements = 20; // map 对象的属性、方法。注意属性中不要有数组、对象等 // 引用属性,否则多个实例的相关属性会发生冲突 var map_obj = { _init: function (cfg) { cfg = cfg || {}; this.grid_x = cfg.grid_x || 10; this.grid_y = cfg.grid_y || 10; this.x = cfg.x || 0; this.y = cfg.y || 0; this.width = this.grid_x * TD.grid_size; this.height = this.grid_y * TD.grid_size; this.x2 = this.x + this.width; this.y2 = this.y + this.height; this.grids = []; this.entrance = this.exit = null; this.buildings = []; this.monsters = []; this.bullets = []; this.scene = cfg.scene; this.is_main_map = !!cfg.is_main_map; this.select_hl = TD.MapSelectHighLight(this.id + "-hl", { map: this }); this.select_hl.addToScene(this.scene, 1, 9); this.selected_building = null; this._wait_clearInvalidElements = _default_wait_clearInvalidElements; this._wait_add_monsters = 0; this._wait_add_monsters_arr = []; if (this.is_main_map) { this.mmm = new MainMapMask(this.id + "-mmm", { map: this }); this.mmm.addToScene(this.scene, 1, 7); } // 下面添加相应的格子 var i, l = this.grid_x * this.grid_y, grid_data = cfg["grid_data"] || [], d, grid; for (i = 0; i < l; i++) { d = grid_data[i] || {}; d.mx = i % this.grid_x; d.my = Math.floor(i / this.grid_x); d.map = this; d.step_level = this.step_level; d.render_level = this.render_level; grid = new TD.Grid(this.id + "-grid-" + d.mx + "-" + d.my, d); this.grids.push(grid); } if (cfg.entrance && cfg.exit && !TD.lang.arrayEqual(cfg.entrance, cfg.exit)) { this.entrance = this.getGrid(cfg.entrance[0], cfg.entrance[1]); this.entrance.is_entrance = true; this.exit = this.getGrid(cfg.exit[0], cfg.exit[1]); this.exit.is_exit = true; } var _this = this; if (cfg.grids_cfg) { TD.lang.each(cfg.grids_cfg, function (obj) { var grid = _this.getGrid(obj.pos[0], obj.pos[1]); if (!grid) return; if (!isNaN(obj.passable_flag)) grid.passable_flag = obj.passable_flag; if (!isNaN(obj.build_flag)) grid.build_flag = obj.build_flag; if (obj.building) { grid.addBuilding(obj.building); } }); } }, /** * 检查地图中是否有武器(具备攻击性的建筑) * 因为第一波怪物只有在地图上有了第一件武器后才会出现 */ checkHasWeapon: function () { this.has_weapon = (this.anyBuilding(function (obj) { return obj.is_weapon; }) != null); }, /** * 取得指定位置的格子对象 * @param mx {Number} 地图上的坐标 x * @param my {Number} 地图上的坐标 y */ getGrid: function (mx, my) { var p = my * this.grid_x + mx; return this.grids[p]; }, anyMonster: function (f) { return TD.lang.any(this.monsters, f); }, anyBuilding: function (f) { return TD.lang.any(this.buildings, f); }, anyBullet: function (f) { return TD.lang.any(this.bullets, f); }, eachBuilding: function (f) { TD.lang.each(this.buildings, f); }, eachMonster: function (f) { TD.lang.each(this.monsters, f); }, eachBullet: function (f) { TD.lang.each(this.bullets, f); }, /** * 预建设 * @param building_type {String} */ preBuild: function (building_type) { TD.mode = "build"; if (this.pre_building) { this.pre_building.remove(); } this.pre_building = new TD.Building(this.id + "-" + "pre-building-" + TD.lang.rndStr(), { type: building_type, map: this, is_pre_building: true }); this.scene.addElement(this.pre_building, 1, this.render_level + 1); //this.show_all_ranges = true; }, /** * 退出预建设状态 */ cancelPreBuild: function () { TD.mode = "normal"; if (this.pre_building) { this.pre_building.remove(); } //this.show_all_ranges = false; }, /** * 清除地图上无效的元素 */ clearInvalidElements: function () { if (this._wait_clearInvalidElements > 0) { this._wait_clearInvalidElements--; return; } this._wait_clearInvalidElements = _default_wait_clearInvalidElements; var a = []; TD.lang.shift(this.buildings, function (obj) { if (obj.is_valid) a.push(obj); }); this.buildings = a; a = []; TD.lang.shift(this.monsters, function (obj) { if (obj.is_valid) a.push(obj); }); this.monsters = a; a = []; TD.lang.shift(this.bullets, function (obj) { if (obj.is_valid) a.push(obj); }); this.bullets = a; }, /** * 在地图的入口处添加一个怪物 * @param monster 可以是数字,也可以是 monster 对象 */ addMonster: function (monster) { if (!this.entrance) return; if (typeof monster == "number") { monster = new TD.Monster(null, { idx: monster, difficulty: TD.difficulty, step_level: this.step_level, render_level: this.render_level + 2 }); } this.entrance.addMonster(monster); }, /** * 在地图的入口处添加 n 个怪物 * @param n * @param monster */ addMonsters: function (n, monster) { this._wait_add_monsters = n; this._wait_add_monsters_objidx = monster; }, /** * arr 的格式形如: * [[1, 0], [2, 5], [3, 6], [10, 4]...] */ addMonsters2: function (arr) { this._wait_add_monsters_arr = arr; }, /** * 检查地图的指定格子是否可通过 * @param mx {Number} * @param my {Number} */ checkPassable: function (mx, my) { var grid = this.getGrid(mx, my); return (grid != null && grid.passable_flag == 1 && grid.build_flag != 2); }, step: function () { this.clearInvalidElements(); if (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 a = this._wait_add_monsters_arr.shift(); this.addMonsters(a[0], a[1]); } }, render: function () { var ctx = TD.ctx; ctx.strokeStyle = "#99a"; ctx.lineWidth = _TD.retina; ctx.beginPath(); ctx.strokeRect(this.x + 0.5, this.y + 0.5, this.width, this.height); ctx.closePath(); ctx.stroke(); }, /** * 鼠标移出地图事件 */ onOut: function () { if (this.is_main_map && this.pre_building) this.pre_building.hide(); } }; /** * @param id {String} 配置对象 * @param cfg {Object} 配置对象 * 至少需要包含以下项: * { * grid_x: 宽度(格子), * grid_y: 高度(格子), * scene: 属于哪个场景, * } */ TD.Map = function (id, cfg) { // map 目前只需要监听 out 事件 // 虽然只需要监听 out 事件,但同时也需要监听 enter ,因为如果 // 没有 enter ,out 将永远不会被触发 cfg.on_events = ["enter", "out"]; var map = new TD.Element(id, cfg); TD.lang.mix(map, map_obj); map._init(cfg); return map; }; /** * 地图选中元素高亮边框对象 */ var map_selecthl_obj = { _init: function (cfg) { this.map = cfg.map; this.width = TD.grid_size + 2; this.height = TD.grid_size + 2; this.is_visiable = false; }, show: function (grid) { this.x = grid.x; this.y = grid.y; this.is_visiable = true; }, render: function () { var ctx = TD.ctx; ctx.lineWidth = 2; ctx.strokeStyle = "#f93"; ctx.beginPath(); ctx.strokeRect(this.x, this.y, this.width - 1, this.height - 1); ctx.closePath(); ctx.stroke(); } }; /** * 地图选中的高亮框 * @param id {String} 至少需要包含 * @param cfg {Object} 至少需要包含 * { * map: map 对象 * } */ TD.MapSelectHighLight = function (id, cfg) { var map_selecthl = new TD.Element(id, cfg); TD.lang.mix(map_selecthl, map_selecthl_obj); map_selecthl._init(cfg); return map_selecthl; }; var mmm_obj = { _init: function (cfg) { this.map = cfg.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 ctx = TD.ctx; /*ctx.clearRect(0, 0, this.x1, this.h); ctx.clearRect(0, 0, this.w, this.y1); ctx.clearRect(0, this.y2, this.w, this.h2); ctx.clearRect(this.x2, 0, this.w2, this.h2);*/ ctx.fillStyle = "#fff"; ctx.beginPath(); ctx.fillRect(0, 0, this.x1, this.h); ctx.fillRect(0, 0, this.w, this.y1); ctx.fillRect(0, this.y2, this.w, this.h2); ctx.fillRect(this.x2, 0, this.w2, this.h); ctx.closePath(); ctx.fill(); } }; /** * 主地图外边的遮罩,用于遮住超出地图的射程等 */ function MainMapMask(id, cfg) { var mmm = new TD.Element(id, cfg); TD.lang.mix(mmm, mmm_obj); mmm._init(cfg); return mmm; } }); // _TD.a.push end /* * Copyright (c) 2011. * * Author: oldj * Blog: http://oldj.net/ * * Last Update: 2011/1/10 5:22:52 */ // _TD.a.push begin _TD.a.push(function (TD) { // grid 对象的属性、方法。注意属性中不要有数组、对象等 // 引用属性,否则多个实例的相关属性会发生冲突 var grid_obj = { _init: function (cfg) { cfg = cfg || {}; this.map = cfg.map; this.scene = this.map.scene; this.mx = cfg.mx; // 在 map 中的格子坐标 this.my = cfg.my; this.width = TD.grid_size; this.height = TD.grid_size; this.is_entrance = this.is_exit = false; this.passable_flag = 1; // 0: 不可通过; 1: 可通过 this.build_flag = 1;// 0: 不可修建; 1: 可修建; 2: 已修建 this.building = null; this.caculatePos(); }, /** * 根据 map 位置及本 grid 的 (mx, my) ,计算格子的位置 */ caculatePos: function () { this.x = this.map.x + this.mx * TD.grid_size; this.y = this.map.y + this.my * TD.grid_size; this.x2 = this.x + TD.grid_size; this.y2 = this.y + TD.grid_size; this.cx = Math.floor(this.x + TD.grid_size / 2); this.cy = Math.floor(this.y + TD.grid_size / 2); }, /** * 检查如果在当前格子建东西,是否会导致起点与终点被阻塞 */ checkBlock: function () { if (this.is_entrance || this.is_exit) { this._block_msg = TD._t("entrance_or_exit_be_blocked"); return true; } var is_blocked, _this = this, fw = new TD.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 (x, y) { return !(x == _this.mx && y == _this.my) && _this.map.checkPassable(x, y); } ); is_blocked = fw.is_blocked; if (!is_blocked) { is_blocked = !!this.map.anyMonster(function (obj) { return obj.chkIfBlocked(_this.mx, _this.my); }); if (is_blocked) this._block_msg = TD._t("monster_be_blocked"); } else { this._block_msg = TD._t("blocked"); } return is_blocked; }, /** * 购买建筑 * @param building_type {String} */ buyBuilding: function (building_type) { var cost = TD.getDefaultBuildingAttributes(building_type).cost || 0; if (TD.money >= cost) { TD.money -= cost; this.addBuilding(building_type); } else { TD.log(TD._t("not_enough_money", [cost])); this.scene.panel.balloontip.msg(TD._t("not_enough_money", [cost]), this); } }, /** * 在当前格子添加指定类型的建筑 * @param building_type {String} */ addBuilding: function (building_type) { if (this.building) { // 如果当前格子已经有建筑,先将其移除 this.removeBuilding(); } var building = new TD.Building("building-" + building_type + "-" + TD.lang.rndStr(), { type: building_type, step_level: this.step_level, render_level: this.render_level }); building.locate(this); this.scene.addElement(building, this.step_level, this.render_level + 1); this.map.buildings.push(building); this.building = building; this.build_flag = 2; this.map.checkHasWeapon(); if (this.map.pre_building) this.map.pre_building.hide(); }, /** * 移除当前格子的建筑 */ removeBuilding: function () { if (this.build_flag == 2) this.build_flag = 1; if (this.building) this.building.remove(); this.building = null; }, /** * 在当前建筑添加一个怪物 * @param monster */ addMonster: function (monster) { monster.beAddToGrid(this); this.map.monsters.push(monster); monster.start(); }, /** * 高亮当前格子 * @param show {Boolean} */ hightLight: function (show) { this.map.select_hl[show ? "show" : "hide"](this); }, render: function () { var ctx = TD.ctx, px = this.x + 0.5, py = this.y + 0.5; //if (this.map.is_main_map) { //ctx.drawImage(this.map.res, //0, 0, 32, 32, this.x, this.y, 32, 32 //); //} if (this.is_hover) { ctx.fillStyle = "rgba(255, 255, 200, 0.2)"; ctx.beginPath(); ctx.fillRect(px, py, this.width, this.height); ctx.closePath(); ctx.fill(); } if (this.passable_flag == 0) { // 不可通过 ctx.fillStyle = "#fcc"; ctx.beginPath(); ctx.fillRect(px, py, this.width, this.height); ctx.closePath(); ctx.fill(); } /** * 画入口及出口 */ if (this.is_entrance || this.is_exit) { ctx.lineWidth = 1; ctx.fillStyle = "#ccc"; ctx.beginPath(); ctx.fillRect(px, py, this.width, this.height); ctx.closePath(); ctx.fill(); ctx.strokeStyle = "#666"; ctx.fillStyle = this.is_entrance ? "#fff" : "#666"; ctx.beginPath(); ctx.arc(this.cx, this.cy, TD.grid_size * 0.325, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke(); } ctx.strokeStyle = "#eee"; ctx.lineWidth = 1; ctx.beginPath(); ctx.strokeRect(px, py, this.width, this.height); ctx.closePath(); ctx.stroke(); }, /** * 鼠标进入当前格子事件 */ onEnter: function () { if (this.map.is_main_map && TD.mode == "build") { if (this.build_flag == 1) { this.map.pre_building.show(); this.map.pre_building.locate(this); } else { this.map.pre_building.hide(); } } else if (this.map.is_main_map) { var msg = ""; if (this.is_entrance) { msg = TD._t("entrance"); } else if (this.is_exit) { msg = TD._t("exit"); } else if (this.passable_flag == 0) { msg = TD._t("_cant_pass"); } else if (this.build_flag == 0) { msg = TD._t("_cant_build"); } if (msg) { this.scene.panel.balloontip.msg(msg, this); } } }, /** * 鼠标移出当前格子事件 */ onOut: function () { // 如果当前气球提示指向本格子,将其隐藏 if (this.scene.panel.balloontip.el == this) { this.scene.panel.balloontip.hide(); } }, /** * 鼠标点击了当前格子事件 */ onClick: function () { if (this.scene.state != 1) return; if (TD.mode == "build" && this.map.is_main_map && !this.building) { // 如果处于建设模式下,并且点击在主地图的空格子上,则尝试建设指定建筑 if (this.checkBlock()) { // 起点与终点之间被阻塞,不能修建 this.scene.panel.balloontip.msg(this._block_msg, this); } else { // 购买建筑 this.buyBuilding(this.map.pre_building.type); } } else if (!this.building && this.map.selected_building) { // 取消选中建筑 this.map.selected_building.toggleSelected(); this.map.selected_building = null; } } }; /** * @param id {String} * @param cfg {object} 配置对象 * 至少需要包含以下项: * { * mx: 在 map 格子中的横向坐标, * my: 在 map 格子中的纵向坐标, * map: 属于哪个 map, * } */ TD.Grid = function (id, cfg) { cfg.on_events = ["enter", "out", "click"]; var grid = new TD.Element(id, cfg); TD.lang.mix(grid, grid_obj); grid._init(cfg); return grid; }; }); // _TD.a.push end /* * Copyright (c) 2011. * * Author: oldj * Blog: http://oldj.net/ * * Last Update: 2011/1/10 5:22:52 */ // _TD.a.push begin _TD.a.push(function (TD) { // building 对象的属性、方法。注意属性中不要有数组、对象等 // 引用属性,否则多个实例的相关属性会发生冲突 var building_obj = { _init: function (cfg) { this.is_selected = false; this.level = 0; this.killed = 0; // 当前建筑杀死了多少怪物 this.target = null; cfg = cfg || {}; this.map = cfg.map || null; this.grid = cfg.grid || null; /** * 子弹类型,可以有以下类型: * 1:普通子弹 * 2:激光类,发射后马上命中,暂未实现 * 3:导弹类,击中后会爆炸,带来面攻击,暂未实现 */ this.bullet_type = cfg.bullet_type || 1; /** * type 可能的值有: * "wall": 墙壁,没有攻击性 * "cannon": 炮台 * "LMG": 轻机枪 * "HMG": 重机枪 * "laser_gun": 激光枪 * */ this.type = cfg.type; this.speed = cfg.speed; this.bullet_speed = cfg.bullet_speed; this.is_pre_building = !!cfg.is_pre_building; this.blink = this.is_pre_building; this.wait_blink = this._default_wait_blink = 20; this.is_weapon = (this.type != "wall"); // 墙等不可攻击的建筑此项为 false ,其余武器此项为 true var o = TD.getDefaultBuildingAttributes(this.type); TD.lang.mix(this, o); this.range_px = this.range * TD.grid_size; this.money = this.cost; // 购买、升级本建筑已花费的钱 this.caculatePos(); }, /** * 升级本建筑需要的花费 */ getUpgradeCost: function () { return Math.floor(this.money * 0.75); }, /** * 出售本建筑能得到多少钱 */ getSellMoney: function () { return Math.floor(this.money * 0.5) || 1; }, /** * 切换选中 / 未选中状态 */ toggleSelected: function () { this.is_selected = !this.is_selected; this.grid.hightLight(this.is_selected); // 高亮 var _this = this; if (this.is_selected) { // 如果当前建筑被选中 this.map.eachBuilding(function (obj) { obj.is_selected = obj == _this; }); // 取消另一个地图中选中建筑的选中状态 ( this.map.is_main_map ? this.scene.panel_map : this.scene.map ).eachBuilding(function (obj) { obj.is_selected = false; obj.grid.hightLight(false); }); this.map.selected_building = this; if (!this.map.is_main_map) { // 在面版地图中选中了建筑,进入建筑模式 this.scene.map.preBuild(this.type); } else { // 取消建筑模式 this.scene.map.cancelPreBuild(); } } else { // 如果当前建筑切换为未选中状态 if (this.map.selected_building == this) this.map.selected_building = null; if (!this.map.is_main_map) { // 取消建筑模式 this.scene.map.cancelPreBuild(); } } // 如果是选中 / 取消选中主地图上的建筑,显示 / 隐藏对应的操作按钮 if (this.map.is_main_map) { if (this.map.selected_building) { this.scene.panel.btn_upgrade.show(); this.scene.panel.btn_sell.show(); this.updateBtnDesc(); } else { this.scene.panel.btn_upgrade.hide(); this.scene.panel.btn_sell.hide(); } } }, /** * 生成、更新升级按钮的说明文字 */ updateBtnDesc: function () { this.scene.panel.btn_upgrade.desc = TD._t( "upgrade", [ TD._t("building_name_" + this.type), this.level + 1, this.getUpgradeCost() ]); this.scene.panel.btn_sell.desc = TD._t( "sell", [ TD._t("building_name_" + this.type), this.getSellMoney() ]); }, /** * 将本建筑放置到一个格子中 * @param grid {Element} 指定格子 */ locate: function (grid) { this.grid = grid; this.map = grid.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 + 0.5; this.py = this.y + 0.5; this.wait_blink = this._default_wait_blink; this._fire_wait = Math.floor(Math.max(2 / (this.speed * TD.global_speed), 1)); this._fire_wait2 = this._fire_wait; }, /** * 将本建筑彻底删除 */ remove: function () { // TD.log("remove building #" + this.id + "."); if (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) return; var cx = this.cx, cy = this.cy, range2 = Math.pow(this.range_px, 2); // 如果当前建筑有目标,并且目标还是有效的,并且目标仍在射程内 if (this.target && this.target.is_valid && Math.pow(this.target.cx - cx, 2) + Math.pow(this.target.cy - cy, 2) <= range2) return; // 在进入射程的怪物中寻找新的目标 this.target = TD.lang.any( TD.lang.rndSort(this.map.monsters), // 将怪物随机排序 function (obj) { return Math.pow(obj.cx - cx, 2) + Math.pow(obj.cy - cy, 2) <= range2; }); }, /** * 取得目标的坐标(相对于地图左上角) */ getTargetPosition: function () { if (!this.target) { // 以 entrance 为目标 var grid = this.map.is_main_map ? this.map.entrance : this.grid; return [grid.cx, grid.cy]; } return [this.target.cx, this.target.cy]; }, /** * 向自己的目标开火 */ fire: function () { if (!this.target || !this.target.is_valid) return; if (this.type == "laser_gun") { // 如果是激光枪,目标立刻被击中 this.target.beHit(this, this.damage); return; } var muzzle = this.muzzle || [this.cx, this.cy], // 炮口的位置 cx = muzzle[0], cy = muzzle[1]; new TD.Bullet(null, { building: this, damage: this.damage, target: this.target, speed: this.bullet_speed, x: cx, y: cy }); }, tryToFire: function () { if (!this.is_weapon || !this.target) return; // 动态计算发射间隔,实时支持倍速模式 var base_fire_wait = Math.floor(Math.max(2 / this.speed, 1)); var current_fire_wait = Math.max(Math.floor(base_fire_wait / TD.global_speed), 1); // 如果速度倍数改变,更新发射间隔 if (current_fire_wait !== this._fire_wait2) { this._fire_wait2 = current_fire_wait; } this._fire_wait--; if (this._fire_wait > 0) { // return; } else if (this._fire_wait < 0) { this._fire_wait = this._fire_wait2; } else { this.fire(); } }, _upgrade2: function (k) { if (!this._upgrade_records[k]) this._upgrade_records[k] = this[k]; var v = this._upgrade_records[k], mk = "max_" + k, uk = "_upgrade_rule_" + k, uf = this[uk] || TD.default_upgrade_rule; if (!v || isNaN(v)) return; v = uf(this.level, v); if (this[mk] && !isNaN(this[mk]) && this[mk] < v) v = this[mk]; this._upgrade_records[k] = v; this[k] = Math.floor(v); }, /** * 升级建筑 */ upgrade: function () { if (!this._upgrade_records) this._upgrade_records = {}; var attrs = [ // 可升级的变量 "damage", "range", "speed", "life", "shield" ], i, l = attrs.length; for (i = 0; i < l; i++) this._upgrade2(attrs[i]); this.level++; this.range_px = this.range * TD.grid_size; }, tryToUpgrade: function (btn) { var cost = this.getUpgradeCost(), msg = ""; if (cost > TD.money) { msg = TD._t("not_enough_money", [cost]); } else { TD.money -= cost; this.money += cost; this.upgrade(); msg = TD._t("upgrade_success", [ TD._t("building_name_" + this.type), this.level, this.getUpgradeCost() ]); } this.updateBtnDesc(); this.scene.panel.balloontip.msg(msg, btn); }, tryToSell: function () { if (!this.is_valid) return; TD.money += this.getSellMoney(); this.grid.removeBuilding(); this.is_valid = false; 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 () { if (this.blink) { this.wait_blink--; if (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) return; var ctx = TD.ctx; TD.renderBuilding(this); if ( 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 ) { // 画射程 ctx.lineWidth = _TD.retina; ctx.fillStyle = "rgba(187, 141, 32, 0.15)"; ctx.strokeStyle = "#bb8d20"; ctx.beginPath(); ctx.arc(this.cx, this.cy, this.range_px, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke(); } if (this.type == "laser_gun" && this.target && this.target.is_valid) { // 画激光 ctx.lineWidth = 3 * _TD.retina; ctx.strokeStyle = "rgba(50, 50, 200, 0.5)"; ctx.beginPath(); ctx.moveTo(this.cx, this.cy); ctx.lineTo(this.target.cx, this.target.cy); ctx.closePath(); ctx.stroke(); ctx.lineWidth = _TD.retina; ctx.strokeStyle = "rgba(150, 150, 255, 0.5)"; ctx.beginPath(); ctx.lineTo(this.cx, this.cy); ctx.closePath(); ctx.stroke(); } }, onEnter: function () { if (this.is_pre_building) return; var msg = "建筑工事"; if (this.map.is_main_map) { 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]); } else { msg = TD._t("building_intro_" + this.type, [TD.getDefaultBuildingAttributes(this.type).cost]); } this.scene.panel.balloontip.msg(msg, this.grid); }, onOut: function () { if (this.scene.panel.balloontip.el == this.grid) { this.scene.panel.balloontip.hide(); } }, onClick: function () { if (this.is_pre_building || this.scene.state != 1) return; this.toggleSelected(); } }; /** * @param id {String} * @param cfg {object} 配置对象 * 至少需要包含以下项: * { * type: 建筑类型,可选的值有 * "wall" * "cannon" * "LMG" * "HMG" * "laser_gun" * } */ TD.Building = function (id, cfg) { cfg.on_events = ["enter", "out", "click"]; var building = new TD.Element(id, cfg); TD.lang.mix(building, building_obj); building._init(cfg); return building; }; // bullet 对象的属性、方法。注意属性中不要有数组、对象等 // 引用属性,否则多个实例的相关属性会发生冲突 var bullet_obj = { _init: function (cfg) { cfg = cfg || {}; this.speed = cfg.speed; this.damage = cfg.damage; this.target = cfg.target; this.cx = cfg.x; this.cy = cfg.y; this.r = cfg.r || Math.max(Math.log(this.damage), 2); if (this.r < 1) this.r = 1; if (this.r > 6) this.r = 6; this.building = cfg.building || null; this.map = cfg.map || this.building.map; this.type = cfg.type || 1; this.color = cfg.color || "#000"; this.map.bullets.push(this); this.addToScene(this.map.scene, 1, 6); if (this.type == 1) { this.caculate(); } }, /** * 计算子弹的一些数值 */ caculate: function () { var sx, sy, c, tx = this.target.cx, ty = this.target.cy, speed; sx = tx - this.cx; sy = ty - this.cy; c = Math.sqrt(Math.pow(sx, 2) + Math.pow(sy, 2)); speed = 20 * this.speed * TD.global_speed; this.vx = sx * speed / c; this.vy = sy * speed / c; }, /** * 检查当前子弹是否已超出地图范围 */ checkOutOfMap: function () { this.is_valid = !( this.cx < this.map.x || this.cx > this.map.x2 || this.cy < this.map.y || this.cy > this.map.y2 ); return !this.is_valid; }, /** * 检查当前子弹是否击中了怪物 */ checkHit: function () { var cx = this.cx, cy = this.cy, r = this.r * _TD.retina, monster = this.map.anyMonster(function (obj) { return Math.pow(obj.cx - cx, 2) + Math.pow(obj.cy - cy, 2) <= Math.pow(obj.r + r, 2) * 2; }); if (monster) { // 击中的怪物 monster.beHit(this.building, this.damage); this.is_valid = false; // 子弹小爆炸效果 TD.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: 0.2 }); return true; } return false; }, step: function () { if (this.checkOutOfMap() || this.checkHit()) return; this.cx += this.vx; this.cy += this.vy; }, render: function () { var ctx = TD.ctx; ctx.fillStyle = this.color; ctx.beginPath(); ctx.arc(this.cx, this.cy, this.r, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); } }; /** * @param id {String} 配置对象 * @param cfg {Object} 配置对象 * 至少需要包含以下项: * { * x: 子弹发出的位置 * y: 子弹发出的位置 * speed: * damage: * target: 目标,一个 monster 对象 * building: 所属的建筑 * } * 子弹类型,可以有以下类型: * 1:普通子弹 * 2:激光类,发射后马上命中 * 3:导弹类,击中后会爆炸,带来面攻击 */ TD.Bullet = function (id, cfg) { var bullet = new TD.Element(id, cfg); TD.lang.mix(bullet, bullet_obj); bullet._init(cfg); return bullet; }; }); // _TD.a.push end /* * Copyright (c) 2011. * * Author: oldj * Blog: http://oldj.net/ * * Last Update: 2011/1/10 5:22:52 */ // _TD.a.push begin _TD.a.push(function (TD) { // monster 对象的属性、方法。注意属性中不要有数组、对象等 // 引用属性,否则多个实例的相关属性会发生冲突 var monster_obj = { _init: function (cfg) { cfg = cfg || {}; this.is_monster = true; this.idx = cfg.idx || 1; this.difficulty = cfg.difficulty || 1.0; var attr = TD.getDefaultMonsterAttributes(this.idx); this.speed = Math.floor( (attr.speed + this.difficulty / 2) * (Math.random() * 0.5 + 0.75) ); if (this.speed < 1) this.speed = 1; if (this.speed > cfg.max_speed) this.speed = cfg.max_speed; this.life = this.life0 = Math.floor( attr.life * (this.difficulty + 1) * (Math.random() + 0.5) * 0.5 ); if (this.life < 1) this.life = this.life0 = 1; this.shield = Math.floor(attr.shield + this.difficulty / 2); if (this.shield < 0) this.shield = 0; this.damage = Math.floor( (attr.damage || 1) * (Math.random() * 0.5 + 0.75) ); if (this.damage < 1) this.damage = 1; this.money = attr.money || Math.floor( Math.sqrt((this.speed + this.life) * (this.shield + 1) * this.damage) ); if (this.money < 1) this.money = 1; this.color = attr.color || TD.lang.rndRGB(); this.r = Math.floor(this.damage * 1.2) * _TD.retina; if (this.r < (4 * _TD.retina)) this.r = 4 * _TD.retina; if (this.r > TD.grid_size / 2 - (4 * _TD.retina)) this.r = TD.grid_size / 2 - (4 * _TD.retina); this.render = attr.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 = false; // 前进的道路是否被阻塞了 }, caculatePos: function () { // if (!this.map) return; var r = this.r; this.x = this.cx - r; this.y = this.cy - r; this.x2 = this.cx + r; this.y2 = this.cy + r; }, /** * 怪物被击中 * @param building {Element} 对应的建筑(武器) * @param damage {Number} 本次攻击的原始伤害值 */ beHit: function (building, damage) { if (!this.is_valid) return; var min_damage = Math.ceil(damage * 0.1); damage -= this.shield; if (damage <= min_damage) damage = min_damage; this.life -= damage; TD.score += Math.floor(Math.sqrt(damage)); if (this.life <= 0) { this.beKilled(building); } var balloontip = this.scene.panel.balloontip; if (balloontip.el == this) { balloontip.text = TD._t("monster_info", [this.life, this.shield, this.speed, this.damage]); } }, /** * 怪物被杀死 * @param building {Element} 对应的建筑(武器) */ beKilled: function (building) { if (!this.is_valid) return; this.life = 0; this.is_valid = false; TD.money += this.money; building.killed++; TD.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 _this = this; var fw = new TD.FindWay( this.map.grid_x, this.map.grid_y, this.grid.mx, this.grid.my, this.map.exit.mx, this.map.exit.my, function (x, y) { return _this.map.checkPassable(x, y); } ); this.way = fw.way; //delete fw; }, /** * 检查是否已到达终点 */ checkFinish: function () { if (this.grid && this.map && this.grid == this.map.exit) { TD.life -= this.damage; TD.wave_damage += this.damage; if (TD.life <= 0) { TD.life = 0; TD.stage.gameover(); } else { this.pause(); this.del(); } } }, beAddToGrid: function (grid) { this.grid = grid; this.map = grid.map; this.cx = grid.cx; this.cy = grid.cy; this.grid.scene.addElement(this); }, /** * 取得朝向 * 即下一个格子在当前格子的哪边 * 0:上;1:右;2:下;3:左 */ getToward: function () { if (!this.grid || !this.next_grid) return; if (this.grid.my < this.next_grid.my) { this.toward = 0; } else if (this.grid.mx < this.next_grid.mx) { this.toward = 1; } else if (this.grid.my > this.next_grid.my) { this.toward = 2; } else if (this.grid.mx > this.next_grid.mx) { this.toward = 3; } }, /** * 取得要去的下一个格子 */ getNextGrid: function () { if (this.way.length == 0 || Math.random() < 0.1 // 有 1/10 的概率自动重新寻路 ) { this.findWay(); } var next_grid = this.way.shift(); if (next_grid && !this.map.checkPassable(next_grid[0], next_grid[1])) { this.findWay(); next_grid = this.way.shift(); } if (!next_grid) { return; } this.next_grid = this.map.getGrid(next_grid[0], next_grid[1]); // this.getToward(); // 在这个版本中暂时没有用 }, /** * 检查假如在地图 (x, y) 的位置修建建筑,是否会阻塞当前怪物 * @param mx {Number} 地图的 x 坐标 * @param my {Number} 地图的 y 坐标 * @return {Boolean} */ chkIfBlocked: function (mx, my) { var _this = this, fw = new TD.FindWay( this.map.grid_x, this.map.grid_y, this.grid.mx, this.grid.my, this.map.exit.mx, this.map.exit.my, function (x, y) { return !(x == mx && y == my) && _this.map.checkPassable(x, y); } ); return fw.is_blocked; }, /** * 怪物前进的道路被阻塞(被建筑包围了) */ beBlocked: function () { if (this.is_blocked) return; this.is_blocked = true; TD.log("monster be blocked!"); }, step: function () { if (!this.is_valid || this.is_paused || !this.grid) return; if (!this.next_grid) { this.getNextGrid(); /** * 如果依旧找不着下一步可去的格子,说明当前怪物被阻塞了 */ if (!this.next_grid) { this.beBlocked(); return; } } if (this.cx == this.next_grid.cx && this.cy == this.next_grid.cy) { this.arrive(); } else { // 移动到 next grid var dpx = this.next_grid.cx - this.cx, dpy = this.next_grid.cy - this.cy, sx = dpx < 0 ? -1 : 1, sy = dpy < 0 ? -1 : 1, speed = this.speed * TD.global_speed; if (Math.abs(dpx) < speed && Math.abs(dpy) < speed) { this.cx = this.next_grid.cx; this.cy = this.next_grid.cy; this._dx = speed - Math.abs(dpx); this._dy = speed - Math.abs(dpy); } else { this.cx += dpx == 0 ? 0 : sx * (speed + this._dx); this.cy += dpy == 0 ? 0 : sy * (speed + this._dy); this._dx = 0; this._dy = 0; } } this.caculatePos(); }, onEnter: function () { var msg, balloontip = this.scene.panel.balloontip; if (balloontip.el == this) { balloontip.hide(); balloontip.el = null; } else { msg = TD._t("monster_info", [this.life, this.shield, this.speed, this.damage]); balloontip.msg(msg, this); } }, onOut: function () { // if (this.scene.panel.balloontip.el == this) { // this.scene.panel.balloontip.hide(); // } } }; /** * @param id {String} * @param cfg {Object} 配置对象 * 至少需要包含以下项: * { * life: 怪物的生命值 * shield: 怪物的防御值 * speed: 怪物的速度 * } */ TD.Monster = function (id, cfg) { cfg.on_events = ["enter", "out"]; var monster = new TD.Element(id, cfg); TD.lang.mix(monster, monster_obj); monster._init(cfg); return monster; }; /** * 怪物死亡时的爆炸效果对象 */ var explode_obj = { _init: function (cfg) { cfg = cfg || {}; var rgb = TD.lang.rgb2Arr(cfg.color); this.cx = cfg.cx; this.cy = cfg.cy; this.r = cfg.r * _TD.retina; this.step_level = cfg.step_level; this.render_level = cfg.render_level; this.rgb_r = rgb[0]; this.rgb_g = rgb[1]; this.rgb_b = rgb[2]; this.rgb_a = 1; this.wait = this.wait0 = TD.exp_fps * (cfg.time || 1); cfg.scene.addElement(this); }, step: function () { if (!this.is_valid) return; this.wait--; this.r++; this.is_valid = this.wait > 0; this.rgb_a = this.wait / this.wait0; }, render: function () { var ctx = TD.ctx; ctx.fillStyle = "rgba(" + this.rgb_r + "," + this.rgb_g + "," + this.rgb_b + "," + this.rgb_a + ")"; ctx.beginPath(); ctx.arc(this.cx, this.cy, this.r, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); } }; /** * @param id {String} * @param cfg {Object} 配置对象 * { * // 至少需要包含以下项: * cx: 中心 x 坐标 * cy: 中心 y 坐标 * r: 半径 * color: RGB色彩,形如“#f98723” * scene: Scene 对象 * step_level: * render_level: * * // 以下项可选: * time: 持续时间,默认为 1,单位大致为秒(根据渲染情况而定,不是很精确) * } */ TD.Explode = function (id, cfg) { // cfg.on_events = ["enter", "out"]; var explode = new TD.Element(id, cfg); TD.lang.mix(explode, explode_obj); explode._init(cfg); return explode; }; }); // _TD.a.push end /* * Copyright (c) 2011. * * Author: oldj * Blog: http://oldj.net/ * * Last Update: 2011/1/10 5:22:52 */ // _TD.a.push begin _TD.a.push(function (TD) { // panel 对象的属性、方法。注意属性中不要有数组、对象等 // 引用属性,否则多个实例的相关属性会发生冲突 var panel_obj = { _init: function (cfg) { cfg = cfg || {}; this.x = cfg.x; this.y = cfg.y; this.scene = cfg.scene; this.map = cfg.main_map; // make panel map var panel_map = new TD.Map("panel-map", TD.lang.mix({ x: this.x + cfg.map.x, y: this.y + cfg.map.y, scene: this.scene, step_level: this.step_level, render_level: this.render_level }, cfg.map, false)); this.addToScene(this.scene, 1, 7); panel_map.addToScene(this.scene, 1, 7, panel_map.grids); this.scene.panel_map = panel_map; this.gameover_obj = new TD.GameOver("panel-gameover", { panel: this, scene: this.scene, step_level: this.step_level, is_visiable: false, x: 0, y: 0, width: this.scene.stage.width, height: this.scene.stage.height, render_level: 9 }); this.balloontip = new TD.BalloonTip("panel-balloon-tip", { scene: this.scene, step_level: this.step_level, render_level: 9 }); this.balloontip.addToScene(this.scene, 1, 9); // make buttons // 暂停按钮 this.btn_pause = new TD.Button("panel-btn-pause", { scene: this.scene, x: this.x, y: this.y + 260 * _TD.retina, text: TD._t("button_pause_text"), //desc: TD._t("button_pause_desc_0"), step_level: this.step_level, render_level: this.render_level + 1, onClick: function () { if (this.scene.state == 1) { this.scene.pause(); this.text = TD._t("button_continue_text"); this.scene.panel.btn_upgrade.hide(); this.scene.panel.btn_sell.hide(); this.scene.panel.btn_restart.show(); //this.desc = TD._t("button_pause_desc_1"); } else if (this.scene.state == 2) { this.scene.start(); this.text = TD._t("button_pause_text"); this.scene.panel.btn_restart.hide(); if (this.scene.map.selected_building) { this.scene.panel.btn_upgrade.show(); this.scene.panel.btn_sell.show(); } //this.desc = TD._t("button_pause_desc_0"); } } }); // 重新开始按钮 this.btn_restart = new TD.Button("panel-btn-restart", { scene: this.scene, x: this.x, y: this.y + 300 * _TD.retina, is_visiable: false, text: TD._t("button_restart_text"), step_level: this.step_level, render_level: this.render_level + 1, onClick: function () { setTimeout(function () { TD.stage.clear(); TD.is_paused = true; TD.start(); TD.mouseHand(false); }, 0); } }); // 建筑升级按钮 this.btn_upgrade = new TD.Button("panel-btn-upgrade", { scene: this.scene, x: this.x, y: this.y + 300 * _TD.retina, is_visiable: false, text: TD._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 TD.Button("panel-btn-sell", { scene: this.scene, x: this.x, y: this.y + 340 * _TD.retina, is_visiable: false, text: TD._t("button_sell_text"), step_level: this.step_level, render_level: this.render_level + 1, onClick: function () { this.scene.map.selected_building.tryToSell(this); } }); // 速度控制滑动条 this.speed_slider = new TD.Slider("panel-speed-slider", { scene: this.scene, x: this.x, y: this.y + 400 * _TD.retina, width: 80 * _TD.retina, height: 10 * _TD.retina, min: 0, max: 4, value: 0, // 默认 1x (2^0) step_level: this.step_level, render_level: this.render_level + 1, onChange: function (value) { // value: 0->1x, 1->2x, 2->4x, 3->8x, 4->16x var multiplier = Math.pow(2, value); // 2^value TD.global_speed = 0.1 * multiplier; } }); }, step: function () { if (TD.life_recover) { this._life_recover = this._life_recover2 = TD.life_recover; this._life_recover_wait = this._life_recover_wait2 = TD.exp_fps * 3; TD.life_recover = 0; } if (this._life_recover && (TD.iframe % TD.exp_fps_eighth == 0)) { TD.life ++; this._life_recover --; } }, render: function () { // 画状态文字 var ctx = TD.ctx; ctx.textAlign = "left"; ctx.textBaseline = "top"; ctx.fillStyle = "#000"; ctx.font = "normal " + (12 * _TD.retina) + "px 'Courier New'"; ctx.beginPath(); ctx.fillText(TD._t("panel_money_title") + TD.money, this.x, this.y); ctx.fillText(TD._t("panel_score_title") + TD.score, this.x, this.y + 20 * _TD.retina); ctx.fillText(TD._t("panel_life_title") + TD.life, this.x, this.y + 40 * _TD.retina); ctx.fillText(TD._t("panel_building_title") + this.map.buildings.length, this.x, this.y + 60 * _TD.retina); ctx.fillText(TD._t("panel_monster_title") + this.map.monsters.length, this.x, this.y + 80 * _TD.retina); ctx.fillText(TD._t("wave_info", [this.scene.wave]), this.x, this.y + 210 * _TD.retina); ctx.closePath(); if (this._life_recover_wait) { // 画生命恢复提示 var a = this._life_recover_wait / this._life_recover_wait2; ctx.fillStyle = "rgba(255, 0, 0, " + a + ")"; ctx.font = "bold " + (12 * _TD.retina) + "px 'Verdana'"; ctx.beginPath(); ctx.fillText("+" + this._life_recover2, this.x + 60 * _TD.retina, this.y + 40 * _TD.retina); ctx.closePath(); this._life_recover_wait --; } // 在右下角画版本信息 ctx.textAlign = "right"; ctx.fillStyle = "#666"; ctx.font = "normal " + (12 * _TD.retina) + "px 'Courier New'"; ctx.beginPath(); ctx.fillText("version: " + TD.version + " | oldj.net", TD.stage.width - TD.padding, TD.stage.height - TD.padding * 2); ctx.closePath(); // 在左下角画FPS信息 ctx.textAlign = "left"; ctx.fillStyle = "#666"; ctx.font = "normal " + (12 * _TD.retina) + "px 'Courier New'"; ctx.beginPath(); ctx.fillText("FPS: " + TD.fps, TD.padding, TD.stage.height - TD.padding * 2); ctx.closePath(); } }; /** * @param id {String} * @param cfg {Object} 配置对象 * 至少需要包含以下项: * { * life: 怪物的生命值 * shield: 怪物的防御值 * speed: 怪物的速度 * } */ TD.Panel = function (id, cfg) { var panel = new TD.Element(id, cfg); TD.lang.mix(panel, panel_obj); panel._init(cfg); return panel; }; // balloon tip对象的属性、方法。注意属性中不要有数组、对象等 // 引用属性,否则多个实例的相关属性会发生冲突 var balloontip_obj = { _init: function (cfg) { cfg = cfg || {}; this.scene = cfg.scene; }, caculatePos: function () { var el = this.el; this.x = el.cx + 0.5; this.y = el.cy + 0.5; if (this.x + this.width > this.scene.stage.width - TD.padding) { this.x = this.x - this.width; } this.px = this.x + 5 * _TD.retina; this.py = this.y + 4 * _TD.retina; }, msg: function (txt, el) { this.text = txt; var ctx = TD.ctx; ctx.font = "normal " + (12 * _TD.retina) + "px 'Courier New'"; this.width = Math.max( ctx.measureText(txt).width + 10 * _TD.retina, TD.lang.strLen2(txt) * 6 + 10 * _TD.retina ); this.height = 20 * _TD.retina; if (el && el.cx && el.cy) { this.el = el; this.caculatePos(); this.show(); } }, step: function () { if (!this.el || !this.el.is_valid) { this.hide(); return; } if (this.el.is_monster) { // monster 会移动,所以需要重新计算 tip 的位置 this.caculatePos(); } }, render: function () { if (!this.el) return; var ctx = TD.ctx; ctx.lineWidth = _TD.retina; ctx.fillStyle = "rgba(255, 255, 0, 0.5)"; ctx.strokeStyle = "rgba(222, 222, 0, 0.9)"; ctx.beginPath(); ctx.rect(this.x, this.y, this.width, this.height); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.textAlign = "left"; ctx.textBaseline = "top"; ctx.fillStyle = "#000"; ctx.font = "normal " + (12 * _TD.retina) + "px 'Courier New'"; ctx.beginPath(); ctx.fillText(this.text, this.px, this.py); ctx.closePath(); } }; /** * @param id {String} * @param cfg {Object} 配置对象 * 至少需要包含以下项: * { * scene: scene * } */ TD.BalloonTip = function (id, cfg) { var balloontip = new TD.Element(id, cfg); TD.lang.mix(balloontip, balloontip_obj); balloontip._init(cfg); return balloontip; }; // button 对象的属性、方法。注意属性中不要有数组、对象等 // 引用属性,否则多个实例的相关属性会发生冲突 var button_obj = { _init: function (cfg) { cfg = cfg || {}; this.text = cfg.text; this.onClick = cfg.onClick || TD.lang.nullFunc; this.x = cfg.x; this.y = cfg.y; this.width = cfg.width || 80 * _TD.retina; this.height = cfg.height || 30 * _TD.retina; this.font_x = this.x + 8 * _TD.retina; this.font_y = this.y + 9 * _TD.retina; this.scene = cfg.scene; this.desc = cfg.desc || ""; this.addToScene(this.scene, this.step_level, this.render_level); this.caculatePos(); }, onEnter: function () { TD.mouseHand(true); if (this.desc) { this.scene.panel.balloontip.msg(this.desc, this); } }, onOut: function () { TD.mouseHand(false); if (this.scene.panel.balloontip.el == this) { this.scene.panel.balloontip.hide(); } }, render: function () { var ctx = TD.ctx; ctx.lineWidth = 2 * _TD.retina; ctx.fillStyle = this.is_hover ? "#eee" : "#ccc"; ctx.strokeStyle = "#999"; ctx.beginPath(); ctx.rect(this.x, this.y, this.width, this.height); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.textAlign = "left"; ctx.textBaseline = "top"; ctx.fillStyle = "#000"; ctx.font = "normal " + (12 * _TD.retina) + "px 'Courier New'"; ctx.beginPath(); ctx.fillText(this.text, this.font_x, this.font_y); ctx.closePath(); ctx.fill(); } }; /** * @param id {String} * @param cfg {Object} 配置对象 * 至少需要包含以下项: * { * x: * y: * text: * onClick: function * sence: * } */ TD.Button = function (id, cfg) { cfg.on_events = ["enter", "out", "click"]; var button = new TD.Element(id, cfg); TD.lang.mix(button, button_obj); button._init(cfg); return button; }; // gameover 对象的属性、方法。注意属性中不要有数组、对象等 // 引用属性,否则多个实例的相关属性会发生冲突 var gameover_obj = { _init: function (cfg) { this.panel = cfg.panel; this.scene = cfg.scene; this._score_submitted = false; // 添加标志,确保只提交一次成绩 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 ctx = TD.ctx; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillStyle = "#ccc"; ctx.font = "bold 62px 'Verdana'"; ctx.beginPath(); ctx.fillText("GAME OVER", this.width / 2, this.height / 2); ctx.closePath(); // 尝试将成绩通过 postMessage 通知父窗口(若被嵌入在 iframe 中) // 仅在第一次渲染时发送,防止重复提交 if (!this._score_submitted) { this._score_submitted = true; try { var timeSeconds = 0; if (typeof TD.startedAt === 'number') { timeSeconds = Math.round(((new Date()).getTime() - TD.startedAt) / 1000); } var wave = (this.scene && typeof this.scene.wave === 'number') ? this.scene.wave : (TD && TD.stage && TD.stage.current_scene ? TD.stage.current_scene.wave : 0); var score = typeof TD.score === 'number' ? TD.score : 0; if (window && window.parent && window !== window.parent) { window.parent.postMessage({ type: 'tower-defense:complete', wave: wave, score: score, timeSeconds: timeSeconds }, '*'); } } catch (e) { // 忽略 postMessage 错误 console.error('Failed to send tower defense score:', e); } } ctx.fillStyle = "#f00"; ctx.font = "bold 60px 'Verdana'"; ctx.beginPath(); ctx.fillText("GAME OVER", this.width / 2, this.height / 2); ctx.closePath(); } }; /** * @param id {String} * @param cfg {Object} 配置对象 * 至少需要包含以下项: * { * panel: * scene: * } */ TD.GameOver = function (id, cfg) { var obj = new TD.Element(id, cfg); TD.lang.mix(obj, gameover_obj); obj._init(cfg); return obj; }; /** * 恢复 n 点生命值 * @param n */ TD.recover = function (n) { // TD.life += n; TD.life_recover = n; TD.log("life recover: " + n); }; // 滑动条控件 var slider_obj = { _init: function (cfg) { cfg = cfg || {}; this.x = cfg.x || 0; this.y = cfg.y || 0; this.width = cfg.width || 80 * _TD.retina; this.height = cfg.height || 10 * _TD.retina; this.min = cfg.min || 0; this.max = cfg.max || 10; this.value = cfg.value || 0; this.onChange = cfg.onChange || function(){}; this.scene = cfg.scene; this.is_hover = false; this.addToScene(this.scene, this.step_level, this.render_level); }, step: function () { // 检查鼠标是否在滑动条上 if (TD.eventManager.isOn(this)) { this.is_hover = true; } }, render: function () { var ctx = TD.ctx; var track_y = this.y + this.height / 2; // 绘制轨道 ctx.fillStyle = "#ddd"; ctx.fillRect(this.x, track_y - 2, this.width, 4); // 绘制已填充部分 var fill_width = (this.value - this.min) / (this.max - this.min) * this.width; ctx.fillStyle = "#4a90e2"; ctx.fillRect(this.x, track_y - 2, fill_width, 4); // 绘制滑块 var knob_x = this.x + fill_width; ctx.fillStyle = this.is_hover ? "#2563eb" : "#4a90e2"; ctx.beginPath(); ctx.arc(knob_x, track_y, 6 * _TD.retina, 0, Math.PI * 2); ctx.fill(); // 绘制标签 ctx.textAlign = "center"; ctx.textBaseline = "top"; ctx.fillStyle = "#666"; ctx.font = "normal " + (10 * _TD.retina) + "px 'Arial'"; // 显示速度标签 var labels = ["1x", "2x", "4x", "8x", "16x"]; for (var i = 0; i <= this.max; i++) { var label_x = this.x + (i / this.max) * this.width; ctx.fillText(labels[i] || (Math.pow(2, i) + "x"), label_x, track_y + 12); // 绘制刻度线 ctx.strokeStyle = "#ccc"; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(label_x, track_y - 5); ctx.lineTo(label_x, track_y + 5); ctx.stroke(); } }, onEnter: function () { this.is_hover = true; TD.mouseHand(true); }, onOut: function () { this.is_hover = false; TD.mouseHand(false); }, onClick: function () { // 点击滑动条时更新值 if (TD.eventManager.ex !== undefined && TD.eventManager.ey !== undefined) { var relative_x = TD.eventManager.ex - this.x; var new_value = Math.round((relative_x / this.width) * (this.max - this.min) + this.min); new_value = Math.max(this.min, Math.min(this.max, new_value)); if (new_value !== this.value) { this.value = new_value; this.onChange(this.value); } } } }; TD.Slider = function (id, cfg) { cfg.on_events = ["enter", "out", "click"]; var slider = new TD.Element(id, cfg); TD.lang.mix(slider, slider_obj); slider._init(cfg); return slider; }; }); // _TD.a.push end /* * Copyright (c) 2011. * * Author: oldj * Blog: http://oldj.net/ * * 默认关卡 */ // _TD.a.push begin _TD.a.push(function (TD) { // main stage 初始化方法 var _stage_main_init = function () { var act = new TD.Act(this, "act-1"), scene = new TD.Scene(act, "scene-1"), cfg = TD.getDefaultStageData("scene_endless"); this.config = cfg.config; TD.life = this.config.life; TD.money = this.config.money; TD.score = this.config.score; TD.difficulty = this.config.difficulty; TD.wave_damage = this.config.wave_damage; // make map var map = new TD.Map("main-map", TD.lang.mix({ scene: scene, is_main_map: true, step_level: 1, render_level: 2 }, cfg.map)); map.addToScene(scene, 1, 2, map.grids); scene.map = map; // make panel scene.panel = new TD.Panel("panel", TD.lang.mix({ scene: scene, main_map: map, step_level: 1, render_level: 7 }, cfg.panel)); this.newWave = cfg.newWave; this.map = map; this.wait_new_wave = this.config.wait_new_wave; }, _stage_main_step2 = function () { //TD.log(this.current_act.current_scene.wave); var scene = this.current_act.current_scene, wave = scene.wave; if ((wave == 0 && !this.map.has_weapon) || scene.state != 1) { return; } if (this.map.monsters.length == 0) { if (wave > 0 && this.wait_new_wave == this.config.wait_new_wave - 1) { // 一波怪物刚刚走完 // 奖励生命值 var wave_reward = 0; if (wave % 10 == 0) { wave_reward = 10; } else if (wave % 5 == 0) { wave_reward = 5; } if (TD.life + wave_reward > 100) { wave_reward = 100 - TD.life; } if (wave_reward > 0) { TD.recover(wave_reward); } } if (this.wait_new_wave > 0) { this.wait_new_wave--; return; } this.wait_new_wave = this.config.wait_new_wave; wave++; scene.wave = wave; this.newWave({ map: this.map, wave: wave }); } }; TD.getDefaultStageData = function (k) { var data = { stage_main: { width: 900 * _TD.retina, // px (增加到 900 以容纳更大的棋盘和控制面板) height: 580 * _TD.retina, init: _stage_main_init, step2: _stage_main_step2 }, scene_endless: { // scene 1 map: { grid_x: 21, grid_y: 17, x: TD.padding, y: TD.padding, entrance: [0, 0], exit: [20, 16], grids_cfg: [ { pos: [3, 3], //building: "cannon", passable_flag: 0 }, { pos: [7, 15], build_flag: 0 }, { pos: [4, 12], building: "wall" }, { pos: [4, 13], building: "wall" //}, { //pos: [11, 9], //building: "cannon" //}, { //pos: [5, 2], //building: "HMG" //}, { //pos: [14, 9], //building: "LMG" //}, { //pos: [3, 14], //building: "LMG" } ] }, panel: { x: TD.padding * 2 + TD.grid_size * 21, y: TD.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: true, wait_new_wave: TD.exp_fps * 3, // 经过多少 step 后再开始新的一波 difficulty: 1.0, // 难度系数 wave: 0, max_wave: -1, wave_damage: 0, // 当前一波怪物造成了多少点生命值的伤害 max_monsters_per_wave: 100, // 每一波最多多少怪物 money: 500, score: 0, // 开局时的积分 life: 100, waves: [ // 这儿只定义了前 10 波怪物,从第 11 波开始自动生成 [], // 第一个参数是没有用的(第 0 波) // 第一波 [ [1, 0] // 1 个 0 类怪物 ], // 第二波 [ [1, 0], // 1 个 0 类怪物 [1, 1] // 1 个 1 类怪物 ], // wave 3 [ [2, 0], // 2 个 0 类怪物 [1, 1] // 1 个 1 类怪物 ], // wave 4 [ [2, 0], [1, 1] ], // wave 5 [ [3, 0], [2, 1] ], // wave 6 [ [4, 0], [2, 1] ], // wave 7 [ [5, 0], [3, 1], [1, 2] ], // wave 8 [ [6, 0], [4, 1], [1, 2] ], // wave 9 [ [7, 0], [3, 1], [2, 2] ], // wave 10 [ [8, 0], [4, 1], [3, 2] ] ] }, /** * 生成第 n 波怪物的方法 */ newWave: function (cfg) { cfg = cfg || {}; var map = cfg.map, wave = cfg.wave || 1, //difficulty = TD.difficulty || 1.0, wave_damage = TD.wave_damage || 0; // 自动调整难度系数 if (wave == 1) { //pass } else if (wave_damage == 0) { // 没有造成伤害 if (wave < 5) { TD.difficulty *= 1.05; } else if (TD.difficulty > 30) { TD.difficulty *= 1.1; } else { TD.difficulty *= 1.2; } } else if (TD.wave_damage >= 50) { TD.difficulty *= 0.6; } else if (TD.wave_damage >= 30) { TD.difficulty *= 0.7; } else if (TD.wave_damage >= 20) { TD.difficulty *= 0.8; } else if (TD.wave_damage >= 10) { TD.difficulty *= 0.9; } else { // 造成了 10 点以内的伤害 if (wave >= 10) TD.difficulty *= 1.05; } if (TD.difficulty < 1) TD.difficulty = 1; TD.log("wave " + wave + ", last wave damage = " + wave_damage + ", difficulty = " + TD.difficulty); //map.addMonsters(100, 7); //map.addMonsters2([[10, 7], [5, 0], [5, 5]]); // var wave_data = this.config.waves[wave] || // 自动生成怪物 TD.makeMonsters(Math.min( Math.floor(Math.pow(wave, 1.1)), this.config.max_monsters_per_wave )); map.addMonsters2(wave_data); TD.wave_damage = 0; } } // end of scene_endless }; return data[k] || {}; }; }); // _TD.a.push end /* * Copyright (c) 2011. * * Author: oldj * Blog: http://oldj.net/ * * 本文件定义了建筑的参数、属性 */ // _TD.a.push begin _TD.a.push(function (TD) { /** * 默认的升级规则 * @param old_level {Number} * @param old_value {Number} * @return new_value {Number} */ TD.default_upgrade_rule = function (old_level, old_value) { return old_value * 1.2; }; /** * 取得建筑的默认属性 * @param building_type {String} 建筑类型 */ TD.getDefaultBuildingAttributes = function (building_type) { var building_attributes = { // 路障 "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 (old_level, old_value) { return old_value * (old_level <= 10 ? 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 (old_level, old_value) { return old_value * 1.3; } }, // 激光枪 "laser_gun": { damage: 25, range: 6, max_range: 10, speed: 20, // bullet_speed: 10, // laser_gun 的 bullet_speed 属性没有用 life: 100, shield: 100, cost: 2000 } }; return building_attributes[building_type] || {}; }; }); // _TD.a.push end /* * Copyright (c) 2011. * * Author: oldj * Blog: http://oldj.net/ * * 本文件定义了怪物默认属性及渲染方法 */ // _TD.a.push begin _TD.a.push(function (TD) { /** * 默认的怪物渲染方法 */ function defaultMonsterRender() { if (!this.is_valid || !this.grid) return; var ctx = TD.ctx; // 画一个圆代表怪物 ctx.strokeStyle = "#000"; ctx.lineWidth = 1; ctx.fillStyle = this.color; ctx.beginPath(); ctx.arc(this.cx, this.cy, this.r, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke(); // 画怪物的生命值 if (TD.show_monster_life) { var s = Math.floor(TD.grid_size / 4), l = s * 2 - 2 * _TD.retina; ctx.fillStyle = "#000"; ctx.beginPath(); ctx.fillRect(this.cx - s, this.cy - this.r - 6, s * 2, 4 * _TD.retina); ctx.closePath(); ctx.fillStyle = "#f00"; ctx.beginPath(); ctx.fillRect(this.cx - s + _TD.retina, this.cy - this.r - (6 - _TD.retina), this.life * l / this.life0, 2 * _TD.retina); ctx.closePath(); } } /** * 取得怪物的默认属性 * @param [monster_idx] {Number} 怪物的类型 * @return attributes {Object} */ TD.getDefaultMonsterAttributes = function (monster_idx) { var monster_attributes = [ { // idx: 0 name: "monster 1", desc: "最弱小的怪物", speed: 3, max_speed: 10, life: 50, damage: 1, // 到达终点后会带来多少点伤害(1 ~ 10) shield: 0, money: 5 // 消灭本怪物后可得多少金钱(可选) }, { // idx: 1 name: "monster 2", desc: "稍强一些的小怪", speed: 6, max_speed: 20, life: 50, damage: 2, // 到达终点后会带来多少点伤害(1 ~ 10) shield: 1 }, { // idx: 2 name: "monster speed", desc: "速度较快的小怪", speed: 12, max_speed: 30, life: 50, damage: 3, // 到达终点后会带来多少点伤害(1 ~ 10) shield: 1 }, { // idx: 3 name: "monster life", desc: "生命值很强的小怪", speed: 5, max_speed: 10, life: 500, damage: 3, // 到达终点后会带来多少点伤害(1 ~ 10) shield: 1 }, { // idx: 4 name: "monster shield", desc: "防御很强的小怪", speed: 5, max_speed: 10, life: 50, damage: 3, // 到达终点后会带来多少点伤害(1 ~ 10) shield: 20 }, { // idx: 5 name: "monster damage", desc: "伤害值很大的小怪", speed: 7, max_speed: 14, life: 50, damage: 10, // 到达终点后会带来多少点伤害(1 ~ 10) shield: 2 }, { // idx: 6 name: "monster speed-life", desc: "速度、生命都较高的怪物", speed: 15, max_speed: 30, life: 100, damage: 3, // 到达终点后会带来多少点伤害(1 ~ 10) shield: 3 }, { // idx: 7 name: "monster speed-2", desc: "速度很快的怪物", speed: 30, max_speed: 40, life: 30, damage: 4, // 到达终点后会带来多少点伤害(1 ~ 10) shield: 1 }, { // idx: 8 name: "monster shield-life", desc: "防御很强、生命值很高的怪物", speed: 3, max_speed: 10, life: 300, damage: 5, // 到达终点后会带来多少点伤害(1 ~ 10) shield: 15 } ]; if (typeof monster_idx == "undefined") { // 如果只传了一个参数,则只返回共定义了多少种怪物(供 td.js 中使用) return monster_attributes.length; } var attr = monster_attributes[monster_idx] || monster_attributes[0], attr2 = {}; TD.lang.mix(attr2, attr); if (!attr2.render) { // 如果没有指定当前怪物的渲染方法 attr2.render = defaultMonsterRender } return attr2; }; /** * 生成一个怪物列表, * 包含 n 个怪物 * 怪物类型在 range 中指定,如未指定,则为随机 */ TD.makeMonsters = function (n, range) { var a = [], count = 0, i, c, d, r, l = TD.monster_type_count; if (!range) { range = []; for (i = 0; i < l; i++) { range.push(i); } } while (count < n) { d = n - count; c = Math.min( Math.floor(Math.random() * d) + 1, 3 // 同一类型的怪物一次最多出现 3 个,防止某一波中怪出大量高防御或高速度的怪 ); r = Math.floor(Math.random() * l); a.push([c, range[r]]); count += c; } return a; }; }); // _TD.a.push end /* * Copyright (c) 2011. * * Author: oldj * Blog: http://oldj.net/ * * Last Update: 2011/1/10 5:22:52 */ // _TD.a.push begin _TD.a.push(function (TD) { function lineTo2(ctx, x0, y0, x1, y1, len) { var x2, y2, a, b, p, xt, a2, b2, c2; if (x0 == x1) { x2 = x0; y2 = y1 > y0 ? y0 + len : y0 - len; } else if (y0 == y1) { y2 = y0; x2 = x1 > x0 ? x0 + len : x0 - len; } else { // 解一元二次方程 a = (y0 - y1) / (x0 - x1); b = y0 - x0 * a; a2 = a * a + 1; b2 = 2 * (a * (b - y0) - x0); c2 = Math.pow(b - y0, 2) + x0 * x0 - Math.pow(len, 2); p = Math.pow(b2, 2) - 4 * a2 * c2; if (p < 0) { // TD.log("ERROR: [a, b, len] = [" + ([a, b, len]).join(", ") + "]"); return [0, 0]; } p = Math.sqrt(p); xt = (-b2 + p) / (2 * a2); if ((x1 - x0 > 0 && xt - x0 > 0) || (x1 - x0 < 0 && xt - x0 < 0)) { x2 = xt; y2 = a * x2 + b; } else { x2 = (-b2 - p) / (2 * a2); y2 = a * x2 + b; } } ctx.lineCap = "round"; ctx.moveTo(x0, y0); ctx.lineTo(x2, y2); return [x2, y2]; } var renderFunctions = { "cannon": function (b, ctx, map, gs, gs2) { var target_position = b.getTargetPosition(); ctx.fillStyle = "#393"; ctx.strokeStyle = "#000"; ctx.beginPath(); ctx.lineWidth = _TD.retina; ctx.arc(b.cx, b.cy, gs2 - 5, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.lineWidth = 3 * _TD.retina; ctx.beginPath(); ctx.moveTo(b.cx, b.cy); b.muzzle = lineTo2(ctx, b.cx, b.cy, target_position[0], target_position[1], gs2); ctx.closePath(); // ctx.fill(); ctx.stroke(); ctx.lineWidth = _TD.retina; ctx.fillStyle = "#060"; ctx.beginPath(); ctx.arc(b.cx, b.cy, 7 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#cec"; ctx.beginPath(); ctx.arc(b.cx + 2, b.cy - 2, 3 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); }, "LMG": function (b, ctx, map, gs, gs2) { var target_position = b.getTargetPosition(); ctx.fillStyle = "#36f"; ctx.strokeStyle = "#000"; ctx.beginPath(); ctx.lineWidth = _TD.retina; ctx.arc(b.cx, b.cy, 7 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.lineWidth = 2 * _TD.retina; ctx.beginPath(); ctx.moveTo(b.cx, b.cy); b.muzzle = lineTo2(ctx, b.cx, b.cy, target_position[0], target_position[1], gs2); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.lineWidth = _TD.retina; ctx.fillStyle = "#66c"; ctx.beginPath(); ctx.arc(b.cx, b.cy, 5 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#ccf"; ctx.beginPath(); ctx.arc(b.cx + 1, b.cy - 1, 2 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); }, "HMG": function (b, ctx, map, gs, gs2) { var target_position = b.getTargetPosition(); ctx.fillStyle = "#933"; ctx.strokeStyle = "#000"; ctx.beginPath(); ctx.lineWidth = _TD.retina; ctx.arc(b.cx, b.cy, gs2 - 2, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.lineWidth = 5 * _TD.retina; ctx.beginPath(); ctx.moveTo(b.cx, b.cy); b.muzzle = lineTo2(ctx, b.cx, b.cy, target_position[0], target_position[1], gs2); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.lineWidth = _TD.retina; ctx.fillStyle = "#630"; ctx.beginPath(); ctx.arc(b.cx, b.cy, gs2 - 5 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#960"; ctx.beginPath(); ctx.arc(b.cx + 1, b.cy - 1, 8 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.fillStyle = "#fcc"; ctx.beginPath(); ctx.arc(b.cx + 3, b.cy - 3, 4 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); }, "wall": function (b, ctx, map, gs, gs2) { ctx.lineWidth = _TD.retina; ctx.fillStyle = "#666"; ctx.strokeStyle = "#000"; ctx.fillRect(b.cx - gs2 + 1, b.cy - gs2 + 1, gs - 1, gs - 1); ctx.beginPath(); ctx.moveTo(b.cx - gs2 + 0.5, b.cy - gs2 + 0.5); ctx.lineTo(b.cx - gs2 + 0.5, b.cy + gs2 + 0.5); ctx.lineTo(b.cx + gs2 + 0.5, b.cy + gs2 + 0.5); ctx.lineTo(b.cx + gs2 + 0.5, b.cy - gs2 + 0.5); ctx.lineTo(b.cx - gs2 + 0.5, b.cy - gs2 + 0.5); ctx.moveTo(b.cx - gs2 + 0.5, b.cy + gs2 + 0.5); ctx.lineTo(b.cx + gs2 + 0.5, b.cy - gs2 + 0.5); ctx.moveTo(b.cx - gs2 + 0.5, b.cy - gs2 + 0.5); ctx.lineTo(b.cx + gs2 + 0.5, b.cy + gs2 + 0.5); ctx.closePath(); ctx.stroke(); }, "laser_gun": function (b, ctx/*, map, gs, gs2*/) { // var target_position = b.getTargetPosition(); ctx.fillStyle = "#f00"; ctx.strokeStyle = "#000"; ctx.beginPath(); ctx.lineWidth = _TD.retina; // ctx.arc(b.cx, b.cy, gs2 - 5, 0, Math.PI * 2, true); ctx.moveTo(b.cx, b.cy - 10 * _TD.retina); ctx.lineTo(b.cx - 8.66 * _TD.retina, b.cy + 5 * _TD.retina); ctx.lineTo(b.cx + 8.66 * _TD.retina, b.cy + 5 * _TD.retina); ctx.lineTo(b.cx, b.cy - 10 * _TD.retina); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#60f"; ctx.beginPath(); ctx.arc(b.cx, b.cy, 7 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.stroke(); ctx.fillStyle = "#000"; ctx.beginPath(); ctx.arc(b.cx, b.cy, 3 * _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.fillStyle = "#666"; ctx.beginPath(); ctx.arc(b.cx + 1, b.cy - 1, _TD.retina, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.lineWidth = 3 * _TD.retina; ctx.beginPath(); ctx.moveTo(b.cx, b.cy); // b.muzzle = lineTo2(ctx, b.cx, b.cy, target_position[0], target_position[1], gs2); ctx.closePath(); ctx.fill(); ctx.stroke(); } }; TD.renderBuilding = function (building) { var ctx = TD.ctx, map = building.map, gs = TD.grid_size, gs2 = TD.grid_size / 2; (renderFunctions[building.type] || renderFunctions["wall"])( building, ctx, map, gs, gs2 ); } }); // _TD.a.push end /* * Copyright (c) 2011. * * Author: oldj * Blog: http://oldj.net/ * * Last Update: 2011/1/10 5:22:52 */ // _TD.a.push begin _TD.a.push(function (TD) { TD._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" }; TD._t = TD.translate = function (k, args) { args = (typeof args == "object" && args.constructor == Array) ? args : []; var msg = this._msg_texts[k] || this._msg_texts["_"], i, l = args.length; for (i = 0; i < l; i++) { msg = msg.replace("${" + i + "}", args[i]); } return msg; }; }); // _TD.a.push end /* * Copyright (c) 2011. * * Author: oldj * Blog: http://oldj.net/ * * Last Update: 2011/1/10 5:22:52 */ // _TD.a.push begin _TD.a.push(function (TD) { /** * 使用 A* 算法(Dijkstra算法?)寻找从 (x1, y1) 到 (x2, y2) 最短的路线 * */ TD.FindWay = function (w, h, x1, y1, x2, y2, f_passable) { this.m = []; this.w = w; this.h = h; this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2; this.way = []; this.len = this.w * this.h; this.is_blocked = this.is_arrived = false; this.fPassable = typeof f_passable == "function" ? f_passable : function () { return true; }; this._init(); }; TD.FindWay.prototype = { _init: function () { if (this.x1 == this.x2 && this.y1 == this.y2) { // 如果输入的坐标已经是终点了 this.is_arrived = true; this.way = [ [this.x1, this.y1] ]; return; } for (var i = 0; i < this.len; i++) this.m[i] = -2; // -2 表示未探索过,-1 表示不可到达 this.x = this.x1; this.y = this.y1; this.distance = 0; this.current = [ [this.x, this.y] ]; // 当前一步探索的格子 this.setVal(this.x, this.y, 0); while (this.next()) { } }, getVal: function (x, y) { var p = y * this.w + x; return p < this.len ? this.m[p] : -1; }, setVal: function (x, y, v) { var p = y * this.w + x; if (p > this.len) return false; this.m[p] = v; }, /** * 得到指定坐标的邻居,即从指定坐标出发,1 步之内可以到达的格子 * 目前返回的是指定格子的上、下、左、右四个邻格 * @param x {Number} * @param y {Number} */ getNeighborsOf: function (x, y) { var nbs = []; if (y > 0) nbs.push([x, y - 1]); if (x < this.w - 1) nbs.push([x + 1, y]); if (y < this.h - 1) nbs.push([x, y + 1]); if (x > 0) nbs.push([x - 1, y]); return nbs; }, /** * 取得当前一步可到达的 n 个格子的所有邻格 */ getAllNeighbors: function () { var nbs = [], nb1, i, c, l = this.current.length; for (i = 0; i < l; i++) { c = this.current[i]; nb1 = this.getNeighborsOf(c[0], c[1]); nbs = nbs.concat(nb1); } return nbs; }, /** * 从终点倒推,寻找从起点到终点最近的路径 * 此处的实现是,从终点开始,从当前格子的邻格中寻找值最低(且大于 0)的格子, * 直到到达起点。 * 这个实现需要反复地寻找邻格,有时邻格中有多个格子的值都为最低,这时就从中 * 随机选取一个。还有一种实现方式是在一开始的遍历中,给每一个到达过的格子添加 * 一个值,指向它的来时的格子(父格子)。 */ findWay: function () { var x = this.x2, y = this.y2, nb, max_len = this.len, nbs_len, nbs, i, l, v, min_v = -1, closest_nbs; while ((x != this.x1 || y != this.y1) && min_v != 0 && this.way.length < max_len) { this.way.unshift([x, y]); nbs = this.getNeighborsOf(x, y); nbs_len = nbs.length; closest_nbs = []; // 在邻格中寻找最小的 v min_v = -1; for (i = 0; i < nbs_len; i++) { v = this.getVal(nbs[i][0], nbs[i][1]); if (v < 0) continue; if (min_v < 0 || min_v > v) min_v = v; } // 找出所有 v 最小的邻格 for (i = 0; i < nbs_len; i++) { nb = nbs[i]; if (min_v == this.getVal(nb[0], nb[1])) { closest_nbs.push(nb); } } // 从 v 最小的邻格中随机选取一个作为当前格子 l = closest_nbs.length; i = l > 1 ? Math.floor(Math.random() * l) : 0; nb = closest_nbs[i]; x = nb[0]; y = nb[1]; } }, /** * 到达终点 */ arrive: function () { this.current = []; this.is_arrived = true; this.findWay(); }, /** * 道路被阻塞 */ blocked: function () { this.current = []; this.is_blocked = true; }, /** * 下一次迭代 * @return {Boolean} 如果返回值为 true ,表示未到达终点,并且道路 * 未被阻塞,可以继续迭代;否则表示不必继续迭代 */ next: function () { var neighbors = this.getAllNeighbors(), nb, l = neighbors.length, valid_neighbors = [], x, y, i, v; this.distance++; for (i = 0; i < l; i++) { nb = neighbors[i]; x = nb[0]; y = nb[1]; if (this.getVal(x, y) != -2) continue; // 当前格子已探索过 //grid = this.map.getGrid(x, y); //if (!grid) continue; if (this.fPassable(x, y)) { // 可通过 /** * 从起点到当前格子的耗费 * 这儿只是简单地把从起点到当前格子需要走几步作为耗费 * 比较复杂的情况下,可能还需要考虑不同的路面耗费也会不同, * 比如沼泽地的耗费比平地要多。不过现在的版本中路况没有这么复杂, * 先不考虑。 */ v = this.distance; valid_neighbors.push(nb); } else { // 不可通过或有建筑挡着 v = -1; } this.setVal(x, y, v); if (x == this.x2 && y == this.y2) { this.arrive(); return false; } } if (valid_neighbors.length == 0) { this.blocked(); return false } this.current = valid_neighbors; return true; } }; }); // _TD.a.push end