├── favicon.ico ├── images ├── bg_001.jpg ├── logo.png ├── button_01.png ├── pic_death.jpg ├── pic_wagons.jpg ├── pic_welcome.jpg ├── map_texture_001.png ├── pic_bandit_meet.jpg ├── pic_overweight.jpg ├── map-marker-player.png ├── ui-stat-indikator.png └── town_marker_green_001.png ├── js ├── data │ ├── GameConstants.js │ ├── StartWorldState.js │ ├── DeathRules.js │ ├── DeathDialog.js │ ├── CaravanConstants.js │ ├── ShopEvents.js │ ├── DropDialogs.js │ ├── TownDialog.js │ ├── RandomEvents.js │ ├── BanditEvents.js │ └── BanditDialogs.js ├── plugins │ ├── DropPlugin.js │ ├── DeathCheck.js │ ├── RandomEventPlugin.js │ ├── CorePlugin.js │ ├── Map2DPlugin.js │ ├── WorldViewPlugin.js │ ├── BanditPlugin.js │ └── ShopPlugin.js ├── world │ ├── WorldState.js │ └── WorldUtils.js ├── build_one_js.bat ├── Utils.js ├── Game.js └── DialogWindow.js ├── licenses.txt ├── README.md ├── css └── style.css └── index.html /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kvisaz/nukecaravan/HEAD/favicon.ico -------------------------------------------------------------------------------- /images/bg_001.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kvisaz/nukecaravan/HEAD/images/bg_001.jpg -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kvisaz/nukecaravan/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/button_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kvisaz/nukecaravan/HEAD/images/button_01.png -------------------------------------------------------------------------------- /images/pic_death.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kvisaz/nukecaravan/HEAD/images/pic_death.jpg -------------------------------------------------------------------------------- /images/pic_wagons.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kvisaz/nukecaravan/HEAD/images/pic_wagons.jpg -------------------------------------------------------------------------------- /images/pic_welcome.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kvisaz/nukecaravan/HEAD/images/pic_welcome.jpg -------------------------------------------------------------------------------- /images/map_texture_001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kvisaz/nukecaravan/HEAD/images/map_texture_001.png -------------------------------------------------------------------------------- /images/pic_bandit_meet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kvisaz/nukecaravan/HEAD/images/pic_bandit_meet.jpg -------------------------------------------------------------------------------- /images/pic_overweight.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kvisaz/nukecaravan/HEAD/images/pic_overweight.jpg -------------------------------------------------------------------------------- /images/map-marker-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kvisaz/nukecaravan/HEAD/images/map-marker-player.png -------------------------------------------------------------------------------- /images/ui-stat-indikator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kvisaz/nukecaravan/HEAD/images/ui-stat-indikator.png -------------------------------------------------------------------------------- /images/town_marker_green_001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kvisaz/nukecaravan/HEAD/images/town_marker_green_001.png -------------------------------------------------------------------------------- /js/data/GameConstants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Константы по времени игры 3 | */ 4 | 5 | var GameConstants = { 6 | STEP_IN_MS: 20, // интервал между обновлениями игры 7 | DAY_IN_MS: 2000, // 2000 // день каравана в наших миллисекундах 8 | MAX_LOG_MESSAGES: 10, // максимум сообщений в логе 9 | }; -------------------------------------------------------------------------------- /licenses.txt: -------------------------------------------------------------------------------- 1 | CC0 Creative Commons 2 | 3 | Бесплатно для коммерческого использования 4 | Указание авторства не требуется 5 | 6 | All code and images are public domain. 7 | photos images - from http://pixabay.com 8 | wasteland sprites - from http://opengameart.org/content/dungeon-crawl-32x32-tiles 9 | some code and idea from this tutorial - http://gamedevacademy.org/js13kgames-tutorial/ -------------------------------------------------------------------------------- /js/data/StartWorldState.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Стартовые параметры мира / каравана 3 | */ 4 | 5 | var StartWorldState = { 6 | crew: 8, // число людей в команде 7 | oxen: 4, // число тягловой силы, помимо людей 8 | food: 300, // еда на людей 9 | firepower: 4, // единиц оружия 10 | money: 1000, // денег в караване 11 | cargo: 250, // товары для продажи в городе 12 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuke caravan 2 | Nuke Caravan sandbox game in pure JavaScript / HTML / Css 3 | *** 4 | Игра про караван на чистом JavaScript / HTML / Css. 5 | + [Описание на Хабре](https://habrahabr.ru/post/336724/). 6 | + [Живое демо](http://skachat-besplatno.ru/gamelab/zoolander/). 7 | *** 8 | Последние исправления 9 | 10 | 21.09.2017 - размер лога теперь ограничен константой в GameConstants. Причина: тормоза и OutOfMemoryException у манчкинов, играющих слишком долго при бесконечном размере лога. 11 | 12 | 21.09.2017 - исправлен баг бесконечного цикла при забитии браминов, когда заканчивается еда, в Core Plugin 13 | -------------------------------------------------------------------------------- /js/data/DeathRules.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Правила смерти 3 | * по 1 параметру 4 | * 5 | * param - строковое название численного параметра в WorldState 6 | * death - граница смерти 7 | * alive - любое значение параметра, при котором жив (служит для определения пересечения границы, 8 | * когда нет точно совпадения 9 | * text - сообщение для лога 10 | */ 11 | 12 | var DeathRules = [ 13 | { 14 | param: 'food', 15 | death: 0, 16 | live: 1, 17 | text: 'Ваш караван погиб от голода' 18 | }, 19 | 20 | { 21 | param: 'crew', 22 | death: 0, 23 | live: 1, 24 | text: 'В караване не осталось живых людей' 25 | } 26 | ]; 27 | -------------------------------------------------------------------------------- /js/data/DeathDialog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Диалоги для смерти и рестарта. 3 | */ 4 | var DeathDialogs = { 5 | "start": { 6 | icon: "images/pic_death.jpg", 7 | title: "Погибший в пустоши", 8 | desc: "", 9 | desc_action: function (world, rule) { 10 | var desc = " Причина смерти: "+rule.text+". Вы сумели пройти "+Math.floor(world.distance) + " миль и накопить "+Math.floor(world.money) + " денег. "; 11 | desc += "Может быть, следующим караванщикам повезет больше?" 12 | return desc; 13 | }, 14 | choices:[ 15 | { 16 | text: 'Начать новую игру', 17 | action: function () { return "stop"; } 18 | } 19 | ] 20 | }, 21 | }; -------------------------------------------------------------------------------- /js/plugins/DropPlugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Модуль интерфейса для дропа, если есть перевес 3 | * 4 | * - каждый модуль интерфейса должен содержать функцию init(world, game) 5 | * - в листенерах при изменении параметров world должен вызываться game.onWorldUpdate 6 | */ 7 | 8 | var DropPlugin = {}; 9 | 10 | DropPlugin.init = function (world) { 11 | this.world = world; 12 | }; 13 | 14 | DropPlugin.onDialogClose = function () { 15 | this.world.uiLock = false; // снимаем захват с действий пользователя 16 | this.world.stop = false; // продолжаем путешествие 17 | }; 18 | 19 | DropPlugin.update = function () { 20 | // если стоим или нет перевеса - ничего не делаем 21 | if(this.world.stop || !hasCaravanOverweight(this.world)) return; 22 | 23 | // Перевес! стопим караван 24 | this.world.uiLock = true; // стопим захват с действий пользователя 25 | this.world.stop = true; // стопим путешествие 26 | DialogWindow.show(DropDialogs, this.world, null, this); // показываем диалог 27 | }; 28 | 29 | Game.addPlugin(DropPlugin); -------------------------------------------------------------------------------- /js/world/WorldState.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Дата класс для хранения состояния мира игры 3 | * */ 4 | function WorldState(stats) { 5 | this.day = 0; // текущий день, с десятичными долям 6 | this.crew = stats.crew; // количество людей 7 | this.oxen = stats.oxen; // количество быков 8 | this.food = stats.food; // запасы еды 9 | this.firepower = stats.firepower; // единиц оружия 10 | this.cargo = stats.cargo; // товаров для торговли 11 | this.money = stats.money; //деньги 12 | 13 | // лог событий, содержит день, описание и характеристику 14 | // { day: 1, message: "Хорошо покушали", goodness: Goodness.positive} 15 | this.log = []; 16 | 17 | // координаты каравана, пункта отправления и назначения 18 | this.caravan = { x: 0, y: 0}; 19 | this.from = {x: 0, y: 0}; 20 | this.to = {x: 0, y: 0}; 21 | 22 | this.distance = 0; // сколько всего пройдено 23 | 24 | this.gameover = false; // gameover 25 | this.stop = false; // маркер для обозначения того, что караван стоит 26 | this.uiLock = false; // маркер для блокировки интерфейса 27 | } -------------------------------------------------------------------------------- /js/data/CaravanConstants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Базовые константы каравана 3 | * для настроек событий и встреч - смотри соответствующие константы 4 | */ 5 | var Caravan = { 6 | WEIGHT_PER_PERSON: 30, // вес, который может нести один человек 7 | WEIGHT_PER_OX: 200, // вес на одного быка / средство передвижения 8 | 9 | FOOD_WEIGHT: 0.6, // вес 1 порции еды 10 | FIREPOWER_WEIGHT: 5, // вес 1 единицы оружия, примерно возьмем за основу вес калашаникова с 2 рожками 11 | 12 | FOOD_PER_PERSON: 3, // порций еды на 1 человека в день 13 | 14 | MEAT_PER_OX: 40, // порций еды из 1 брамина при автопоедании 15 | 16 | FULL_SPEED: 55, // максимальная скорость каравана в день, 17 | SLOW_SPEED: 38, // минимальная скорость каравана в день, 18 | 19 | CARGO_PRICE: 2, // за сколько продаем 1 единицу товара (cargo) 20 | CARGO_PER_OX: 150, // сколько берем товаров, в пересчете на вола, в новом городе 21 | CARGO_BUY_PRICE: 1, // за сколько покупаем 1 единицу товара (cargo) 22 | 23 | TOUCH_DISTANCE: 10, // расстояние до точки прибытия, при котором срабатывает прибытие 24 | 25 | }; -------------------------------------------------------------------------------- /js/build_one_js.bat: -------------------------------------------------------------------------------- 1 | del build.js 2 | 3 | type .\Utils.js >> .\build.js 4 | type .\DialogWindow.js >> .\build.js 5 | 6 | type .\world\WorldState.js >> .\build.js 7 | type .\world\WorldUtils.js >> .\build.js 8 | 9 | type .\data\StartWorldState.js >> .\build.js 10 | type .\data\GameConstants.js >> .\build.js 11 | type .\data\CaravanConstants.js >> .\build.js 12 | type .\data\BanditEvents.js >> .\build.js 13 | type .\data\BanditDialogs.js >> .\build.js 14 | type .\data\RandomEvents.js >> .\build.js 15 | type .\data\ShopEvents.js >> .\build.js 16 | type .\data\DeathRules.js >> .\build.js 17 | type .\data\DeathDialog.js >> .\build.js 18 | type .\data\DropDialogs.js >> .\build.js 19 | type .\data\TownDialog.js >> .\build.js 20 | 21 | type .\Game.js >> .\build.js 22 | 23 | type .\plugins\CorePlugin.js >> .\build.js 24 | type .\plugins\Map2DPlugin.js >> .\build.js 25 | type .\plugins\RandomEventPlugin.js >> .\build.js 26 | type .\plugins\ShopPlugin.js >> .\build.js 27 | type .\plugins\BanditPlugin.js >> .\build.js 28 | type .\plugins\DeathCheck.js >> .\build.js 29 | type .\plugins\DropPlugin.js >> .\build.js 30 | type .\plugins\WorldViewPlugin.js >> .\build.js -------------------------------------------------------------------------------- /js/Utils.js: -------------------------------------------------------------------------------- 1 | String.prototype.withArg = function (arg1, arg2) { 2 | var str = this.replace("$1", arg1); 3 | if (arg2) { 4 | str = str.replace("$2", arg2); 5 | } 6 | return str; 7 | }; 8 | 9 | Array.prototype.getRandom = function () { 10 | // Math.random() will never be 1, nor should it. 11 | return this[Math.floor(Math.random() * this.length)]; 12 | }; 13 | 14 | // функция для взятия описания по числу от 0 до 1 15 | // описания в массиве идут от 0 до 1, описаний может быть сколько угодно 16 | Array.prototype.getByDegree = function (number) { 17 | if (number > 1) number = 1; 18 | var maxI = this.length - 1; 19 | return this[Math.floor(maxI * number)]; 20 | }; 21 | 22 | // функция проверки для выпадения случаного события с вероятностью от 0 до 1 23 | function checkProbability(probability) { 24 | return Math.random() <= probability; 25 | } 26 | 27 | // функция для проверки выпадения случайного события на текущем шаге игры 28 | function checkEventForStep(dayProbability) { 29 | var probability = dayProbability * GameConstants.STEP_IN_MS / GameConstants.DAY_IN_MS; 30 | return checkProbability(probability); 31 | } -------------------------------------------------------------------------------- /js/Game.js: -------------------------------------------------------------------------------- 1 | var Game = { 2 | plugins: [], // генераторы событий 3 | }; 4 | 5 | Game.init = function () { 6 | // создаем мир по стартовому состоянию 7 | // все редактируемые переменные - в директории data 8 | this.world = new WorldState(StartWorldState); 9 | 10 | var i; 11 | for (i = 0; i < this.plugins.length; i++) { 12 | this.plugins[i].init(this.world); 13 | } 14 | }; 15 | 16 | // добавление плагинов 17 | Game.addPlugin = function (plugin) { 18 | this.plugins.push(plugin); 19 | }; 20 | 21 | // игровой цикл 22 | Game.update = function () { 23 | if (this.world.gameover) return; // никаких действий 24 | var i; 25 | for (i = 0; i < this.plugins.length; i++) { 26 | this.plugins[i].update(); 27 | } 28 | }; 29 | 30 | 31 | // запуск цикла игры, использую setInterval для совместимости со старым Safari 32 | // bind позволяет привязать this объекта 33 | // так как по дефолту setInterval передает в функцию this от window 34 | Game.resume = function () { 35 | this.interval = setInterval(this.update.bind(this), GameConstants.STEP_IN_MS); 36 | }; 37 | 38 | Game.stop = function () { 39 | clearInterval(this.interval); 40 | }; 41 | 42 | Game.restart = function () { 43 | this.init(); 44 | this.resume(); 45 | }; -------------------------------------------------------------------------------- /js/plugins/DeathCheck.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Проверяет условия смерти 3 | * по DeathRules 4 | * если смерть 5 | * - устанавливает world.gameover = true 6 | * - устанавливает world.stop = true 7 | */ 8 | 9 | DeathCheck = {}; 10 | 11 | DeathCheck.init = function (world) { 12 | this.world = world; 13 | this.rules = DeathRules; 14 | }; 15 | 16 | DeathCheck.update = function () { 17 | if (this.world.gameover) return; // если уже мертвы, проверять бесполезно 18 | 19 | // проверка условий по массиву DeathRules 20 | var i, rule, sign; 21 | for (i = 0; i < this.rules.length; i++) { 22 | rule = this.rules[i]; 23 | sign = (rule.live - rule.death) / Math.abs(rule.live - rule.death); 24 | if (this.world[rule.param] == rule.death || this.world[rule.param] * sign <= rule.death) { 25 | this.onDeath(this.world, rule); 26 | break; 27 | } 28 | } 29 | }; 30 | 31 | DeathCheck.onDeath = function (world, rule) { 32 | Game.stop(); 33 | addLogMessage(world, Goodness.negative, rule.text); 34 | world.gameover = true; 35 | world.stop = true; 36 | DialogWindow.show(DeathDialogs, world, rule, this); 37 | }; 38 | 39 | DeathCheck.onDialogClose = function () { 40 | Game.restart(); 41 | }; 42 | 43 | Game.addPlugin(DeathCheck); -------------------------------------------------------------------------------- /js/plugins/RandomEventPlugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Плагин рандомных событий 3 | * - основного геймплея в оригинале игры с караваном 4 | */ 5 | RandomEventPlugin = {}; 6 | 7 | RandomEventPlugin.init = function (world) { 8 | this.world = world; 9 | this.events = RandomEvents; 10 | }; 11 | 12 | RandomEventPlugin.update = function () { 13 | if (this.world.stop) return; // если стоим на месте - рандомных событий нет 14 | // проверка на выпадение события вообще 15 | if(!checkEventForStep(RandomEventConstants.EVENT_PROBABILITY)) return; 16 | 17 | var event = this.events.getRandom(); 18 | var valueChange = event.value; 19 | 20 | valueChange = Math.floor(Math.random() * valueChange); // случайные значения изменений 21 | 22 | if (valueChange == 0) return; // если случайное значение выпало ноль - никаких изменений, событие отменяется 23 | 24 | // если выпало отрицательное значение, а параметр уже нулевой - ничего не происходит 25 | if (valueChange < 0 && this.world[event.stat] <= 0) return; 26 | 27 | // отрицательные значения не могут быть по модулю больше текущего параметра 28 | if (valueChange < 0 && Math.abs(valueChange) > this.world[event.stat]) { 29 | valueChange = Math.floor(this.world[event.stat]); 30 | } 31 | 32 | this.world[event.stat] += valueChange; 33 | var message = event.text.withArg(Math.abs(valueChange)); 34 | addLogMessage(this.world, event.goodness, message); 35 | }; 36 | 37 | Game.addPlugin(RandomEventPlugin); -------------------------------------------------------------------------------- /js/data/ShopEvents.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Константы для настройки магазинов 3 | */ 4 | var ShopEventConstants = { 5 | SHOP_DISTANCE_MIN: 100, // минимальное расстояние между магазинами 6 | SHOP_PROBABILITY: 0.5, // шанс встретить магазин или караван в день 7 | SHOP_NO_MONEY_MESSAGE: 'Кончились деньги и вы пошли дальше...', // сообщение о нехватке денег 8 | SHOP_BUY_MESSAGE: 'Получено: ', // сообщение о нехватке денег 9 | SHOP_HINT: 'Можно купить: ', // сообщение о нехватке денег 10 | SHOP_EXIT: 'Пойти дальше', // сообщение о нехватке денег 11 | SHOP_PIC: "images/pic_welcome.jpg", // относительный index.html путь к картинке 12 | }; 13 | 14 | // описания возможных магазинов 15 | var Shops = [{ 16 | text: 'Вы нашли магазин в этой жуткой пустоши', 17 | products: [ 18 | {item: 'food', text: 'Еда', qty: 20, price: 50}, 19 | {item: 'oxen', text: 'Брамины', qty: 1, price: 200}, 20 | {item: 'firepower', text: 'Оружие', qty: 2, price: 50}, 21 | ] 22 | }, 23 | { 24 | text: 'Вы встретили другой караван!', 25 | products: [ 26 | {item: 'food', text: 'Еда', qty: 30, price: 50}, 27 | {item: 'oxen', text: 'Брамины',qty: 1, price: 200}, 28 | {item: 'firepower', text: 'Оружие',qty: 2, price: 20}, 29 | {item: 'crew', text: 'Наемники',qty: 1, price: 200} 30 | ] 31 | }, 32 | { 33 | text: 'Следопыты встретили охотников', 34 | products: [ 35 | {item: 'food', text: 'Еда', qty: 20, price: 60}, 36 | {item: 'oxen', text: 'Брамины', qty: 1, price: 300}, 37 | {item: 'firepower', text: 'Оружие', qty: 2, price: 80}, 38 | {item: 'crew', text: 'Охотники', qty: 1, price: 300} 39 | ] 40 | }, 41 | ]; -------------------------------------------------------------------------------- /js/data/DropDialogs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Константы для плагина сброса лишнего груза 3 | */ 4 | 5 | var DropDialogs = { 6 | "start": { 7 | icon: "images/pic_overweight.jpg", 8 | exit: false, 9 | title: "Перевес", 10 | desc_action: function (world) { 11 | var desc = "Караван перегружен и не может двигаться"; 12 | addLogMessage(world, Goodness.negative, desc); // логируем 13 | return desc; 14 | }, 15 | choices: [ 16 | { 17 | text: "Сбросить 100 единиц груза", 18 | action: function (world) { 19 | world.cargo = Math.max(0, world.cargo - 100); 20 | var next = hasCaravanOverweight(world) ? "start" : "stop"; 21 | return next; 22 | } 23 | }, 24 | { 25 | text: "Сбросить 10 единиц груза", 26 | action: function (world) { 27 | world.cargo = Math.max(0, world.cargo - 10); 28 | var next = hasCaravanOverweight(world) ? "start" : "stop"; 29 | return next; 30 | } 31 | }, { 32 | text: "Сбросить 10 единиц еды", 33 | action: function (world) { 34 | world.food = Math.max(0, world.food - 10); 35 | var next = hasCaravanOverweight(world) ? "start" : "stop"; 36 | return next; 37 | } 38 | }, { 39 | text: "Сбросить 1 единицу оружия", 40 | action: function (world) { 41 | world.firepower = Math.max(0, world.firepower - 1); 42 | var next = hasCaravanOverweight(world) ? "start" : "stop"; 43 | return next; 44 | } 45 | } 46 | ] 47 | } 48 | }; -------------------------------------------------------------------------------- /js/data/TownDialog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Диалоги для городов 3 | */ 4 | var TownDialogs = { 5 | "start": { 6 | icon: "images/pic_wagons.jpg", 7 | title: "Вы прибыли в город", 8 | desc: "", 9 | desc_action: function (world, revisit) { 10 | if( revisit ) { 11 | return "Вы уже торговали в этом городе. Надо идти в другой."; 12 | } 13 | var desc = "Вы входите на местный рынок. "; 14 | var sell = sellCargo(world); 15 | if (sell.money > 0) { 16 | desc += "Продано $1 товаров на сумму $$2. ".withArg(sell.cargo, sell.money); 17 | } 18 | else { 19 | desc += "Товаров нет, поэтому ничего продать не удалось. "; 20 | } 21 | addLogMessage(world, Goodness.neutral, desc); 22 | var sellMessage; 23 | 24 | var buy = buyCargo(world); 25 | if (buy.money > 0) { 26 | sellMessage = "Куплено $1 товаров на сумму $$2. ".withArg(buy.cargo, buy.money); 27 | } 28 | else { 29 | sellMessage = "Купить ничего не удалось: не хватает денег или быков. "; 30 | } 31 | addLogMessage(world, Goodness.neutral, sellMessage); 32 | desc += sellMessage; 33 | 34 | var income = sell.money - buy.money; 35 | var signStr = income >= 0 ? "+" : "-"; 36 | var goodness = income > 0 ? Goodness.positive : Goodness.negative; 37 | var incomeMessage = "Прибыль от посещения города: "+signStr+"$" + Math.abs(income); 38 | addLogMessage(world, goodness, incomeMessage); 39 | 40 | desc += incomeMessage; 41 | return desc; 42 | }, 43 | choices: [ 44 | { 45 | text: 'Выйти из города', 46 | action: function () { 47 | return "stop"; 48 | } 49 | } 50 | ] 51 | }, 52 | }; -------------------------------------------------------------------------------- /js/world/WorldUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Функции для вычисления данных по параметрам мира 3 | */ 4 | 5 | // детекция, что точка 1 находится рядом с точкой 2 6 | // nearDistance - расстояние срабатывания 7 | function areNearPoints(point1, point2, nearDistance) { 8 | return getDistance(point1, point2) <= nearDistance; 9 | } 10 | 11 | // расстояние между двумя точками (объектами с полями x и y) 12 | function getDistance(point1, point2) { 13 | return Math.sqrt(Math.pow(point1.x - point2.x, 2) + Math.pow(point1.y - point2.y, 2)); 14 | } 15 | 16 | // максимальный вес, который может нести караван 17 | function getCaravanMaxWeight(world) { 18 | return world.oxen * Caravan.WEIGHT_PER_OX + world.crew * Caravan.WEIGHT_PER_PERSON; 19 | } 20 | 21 | // текущий вес, который тащит караван 22 | function getCaravanWeight(world) { 23 | return world.food * Caravan.FOOD_WEIGHT 24 | + world.firepower * Caravan.FIREPOWER_WEIGHT 25 | + world.cargo; 26 | } 27 | 28 | // не перегружен ли караван 29 | function hasCaravanOverweight(world) { 30 | return getCaravanWeight(world) > getCaravanMaxWeight(world); 31 | } 32 | 33 | // Награда за прибытие в город - премия за сохраненный груз 34 | function sellCargo(world) { 35 | var cargo = world.cargo; 36 | var money = cargo * Caravan.CARGO_PRICE; 37 | world.money += money; 38 | world.cargo = 0; 39 | return {money: money, cargo: cargo}; 40 | } 41 | 42 | // Покупка груза, учитывает вес уже купленного и наличие денег 43 | function buyCargo(world) { 44 | var cargoMax = world.money / Caravan.CARGO_BUY_PRICE; 45 | var newCargo = world.oxen * Caravan.CARGO_PER_OX - world.cargo; // сколько можем купить 46 | newCargo = Math.min(cargoMax, newCargo); // вычисляем адекватную нагрузку по кошельку 47 | var money = newCargo * Caravan.CARGO_BUY_PRICE; 48 | world.cargo += newCargo; 49 | world.money -= money; 50 | return {money: money, cargo: newCargo}; 51 | } 52 | 53 | // добавляем сообщение в лог 54 | function addLogMessage(world, goodness, message) { 55 | world.log.push({ 56 | day: world.day, 57 | message: message, 58 | goodness: goodness 59 | }); 60 | 61 | // если лог превысил указанный размер, удаляем старые сообщения 62 | if(Object.keys(world.log).length > GameConstants.MAX_LOG_MESSAGES){ 63 | world.log.shift(); 64 | } 65 | } 66 | 67 | /** 68 | * Тип события для лога - положительный, отрицательный, нейтральный 69 | */ 70 | var Goodness = { 71 | positive: 'positive', 72 | negative: 'negative', 73 | neutral: 'neutral' 74 | }; -------------------------------------------------------------------------------- /js/plugins/CorePlugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Core Plugin - базовые события 3 | * - изменение дня 4 | * - потребление пищи 5 | * - перемещение к цели 6 | */ 7 | 8 | CorePlugin = {}; 9 | 10 | CorePlugin.init = function (world) { 11 | this.world = world; 12 | this.time = 0; // общее время с начала игры, в миллисекундах 13 | this.dayDelta = GameConstants.STEP_IN_MS / GameConstants.DAY_IN_MS; // сколько дней в одном шаге игру 14 | this.lastDay = -1; // отслеживаем наступление нового дня 15 | this.speedDelta = Caravan.FULL_SPEED - Caravan.SLOW_SPEED; // разница между полной и минимальной скоростью 16 | }; 17 | 18 | CorePlugin.update = function () { 19 | if (this.world.stop) return; // если стоим - никаких изменений 20 | this.time += GameConstants.STEP_IN_MS; // увеличение времени 21 | this.world.day = Math.ceil(this.time / GameConstants.DAY_IN_MS); // текущий день, целый 22 | 23 | // Движение каравана в зависимости от того, сколько дней прошло 24 | this.updateDistance(this.dayDelta, this.world); 25 | 26 | // события связанные с наступлением нового дня 27 | if (this.lastDay < this.world.day) { 28 | this.consumeFood(this.world); 29 | this.lastDay = this.world.day; 30 | } 31 | }; 32 | 33 | // еда выдается один раз в день 34 | CorePlugin.consumeFood = function (world) { 35 | var needFood = world.crew * Caravan.FOOD_PER_PERSON; 36 | var eated = Math.min(needFood, world.food); // съесть можем не больше того, что имеем 37 | world.food -= eated; // съедаем запасы еды 38 | 39 | if (world.food == 0) { 40 | // автопоедание быков при минимальных запасах еды - временный фикс 41 | if (world.oxen > 0) { 42 | world.food += Caravan.MEAT_PER_OX; 43 | world.oxen--; 44 | addLogMessage(world, Goodness.negative, "Кончились запасы еды. 1 брамин забит на мясо.") 45 | } 46 | } 47 | }; 48 | 49 | // обновить пройденный путь в зависимости от потраченного времени в днях 50 | CorePlugin.updateDistance = function (dayDelta, world) { 51 | var maxWeight = getCaravanMaxWeight(world); 52 | var weight = getCaravanWeight(world); 53 | 54 | // при перевесе - Caravan.SLOW_SPEED 55 | // при 0 весе - Caravan.FULL_SPEED 56 | var speed = Caravan.SLOW_SPEED + (this.speedDelta) * Math.max(0, 1 - weight / maxWeight); 57 | 58 | // расстояние, которое может пройти караван при такой скорости 59 | var distanceDelta = speed * dayDelta; 60 | 61 | // вычисляем расстояние до цели 62 | var dx = world.to.x - world.caravan.x; 63 | var dy = world.to.y - world.caravan.y; 64 | 65 | // если мы находимся около цели - останавливаемся 66 | if (areNearPoints(world.caravan, world.to, Caravan.TOUCH_DISTANCE)) { 67 | world.stop = true; 68 | return; 69 | } 70 | 71 | // до цели еще далеко - рассчитываем угол перемещения 72 | // и получаем смещение по координатам 73 | var angle = Math.atan2(dy, dx); 74 | world.caravan.x += Math.cos(angle) * distanceDelta; 75 | world.caravan.y += Math.sin(angle) * distanceDelta; 76 | world.distance += distanceDelta; 77 | }; 78 | 79 | Game.addPlugin(CorePlugin); -------------------------------------------------------------------------------- /js/plugins/Map2DPlugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Map2D Plugin * 3 | * - при клике на городе на карте - отправляет караван туда 4 | * - в update проверяет прибытие в город 5 | */ 6 | Map2DPlugin = { 7 | // чтобы не гонять DOM каждый раз - гоняем только когда обновляются координаты игрока 8 | // для этогоп делаем проверку через это поле 9 | lastPlayerPosition: {x: 0, y: 0}, 10 | // маркер "мы в городе" - соответствует "открыт диалог города" 11 | inTown: false, 12 | // последний посещенный город 13 | lastTown: { x: -1, y: -1 }, 14 | }; 15 | 16 | Map2DPlugin.init = function (world) { 17 | this.world = world; 18 | 19 | // элементы для отображения карты 20 | this.view = {}; 21 | this.view.player = document.getElementById('map-player'); // маркер игрока 22 | 23 | // добавляем в них города - пока два 24 | this.view.towns = document.getElementsByClassName('town'); 25 | 26 | // вешаем на города обработчики кликов, чтобы отправлять туда караван 27 | var i, map2dPlugin = this; 28 | for (i = 0; i < this.view.towns.length; i++) { 29 | this.view.towns[i].addEventListener("click", function (e) { 30 | if (world.uiLock) return; // если какой-то плагин перехватил работу с пользователем, то есть открыто модальное окно, не реагируем на действия пользователя 31 | var element = e.target || e.srcElement; 32 | world.from = {x: world.caravan.x, y: world.caravan.y}; 33 | world.to = {x: element.offsetLeft, y: element.offsetTop}; 34 | world.stop = false; 35 | map2dPlugin.inTown = false; // все, покидаем город 36 | 37 | addLogMessage(world, Goodness.positive, "Путешествие через пустыню начинается!"); 38 | }); 39 | } 40 | 41 | // если найдены города на карте, помещаем игрока в первый попавшийся 42 | if (this.view.towns.length > 0) { 43 | world.caravan.x = this.view.towns[0].offsetLeft; 44 | world.caravan.y = this.view.towns[0].offsetTop; 45 | // запоминаем его как последний, чтобы не торговать в нем же при быстром возвращении 46 | this.lastTown = {x: world.caravan.x, y: world.caravan.y }; 47 | world.stop = true; // чтобы не двигался 48 | this.movePlayerViewTo(world.caravan.x, world.caravan.y); 49 | } 50 | }; 51 | 52 | Map2DPlugin.update = function () { 53 | if (this.inTown) return; // если открыт диалог города - ничего не делаем 54 | 55 | 56 | // обновляем DOM только когда есть изменения в координатах 57 | if (this.lastPlayerPosition.x != this.world.caravan.x || 58 | this.lastPlayerPosition.y != this.world.caravan.y) { 59 | this.movePlayerViewTo(this.world.caravan.x, this.world.caravan.y); 60 | this.lastPlayerPosition.x = this.world.caravan.x; 61 | this.lastPlayerPosition.y = this.world.caravan.y; 62 | } 63 | 64 | // проверяем достижение города на остановках 65 | if (this.world.stop && this.isAboutTarget(this.world)) { 66 | this.inTown = true; 67 | this.world.uiLock = true; // маркируем интерфейс как блокированный 68 | addLogMessage(this.world, Goodness.positive, "Вы достигли города!"); 69 | // проверка что мы были в этом городе 70 | var revisit = this.world.to.x === this.lastTown.x && this.world.to.y === this.lastTown.y; 71 | // запоминаем последений посещенный город 72 | this.lastTown = { x: this.world.to.x, y: this.world.to.y}; 73 | DialogWindow.show(TownDialogs, this.world, revisit, this); 74 | } 75 | }; 76 | 77 | // проверка, что координаты каравана около заданной цели 78 | Map2DPlugin.isAboutTarget = function (world) { 79 | return areNearPoints(world.caravan, world.to, Caravan.TOUCH_DISTANCE); 80 | }; 81 | 82 | Map2DPlugin.movePlayerViewTo = function (x, y) { 83 | this.view.player.style.left = x + "px"; // сдвигаем маркер на карте 84 | this.view.player.style.top = y + "px"; // сдвигаем маркер на карте 85 | }; 86 | 87 | Map2DPlugin.onDialogClose = function () { 88 | // запоминаем этот город, как последний, чтобы не было чита с автоторговлей 89 | this.world.uiLock = false; 90 | }; 91 | 92 | Game.addPlugin(Map2DPlugin); -------------------------------------------------------------------------------- /js/data/RandomEvents.js: -------------------------------------------------------------------------------- 1 | var RandomEventConstants = { 2 | EVENT_PROBABILITY: 1, // 3 // примерное число событий в день, реально будет колебаться около этого значения 3 | }; 4 | 5 | /* 6 | * Набор рандомных событий, происходят по генератору, правил пока никаких 7 | * type - один из вариантов Goodness 8 | * (позитивное, нейтральное, отрицательное изменение) 9 | * 10 | * $1 - используется для указание реального значения параметра 11 | * */ 12 | var RandomEvents = [ 13 | { 14 | goodness: Goodness.negative, 15 | stat: 'crew', 16 | value: -4, 17 | text: 'На караван напал смертокогть! Людей: -$1' 18 | }, 19 | { 20 | goodness: Goodness.negative, 21 | stat: 'crew', 22 | value: -3, 23 | text: 'Радиоактивная буря убила часть команды. Людей: -$1' 24 | }, 25 | { 26 | goodness: Goodness.positive, 27 | stat: 'crew', 28 | value: 2, 29 | text: 'Вы встретили одиноких путников, которые с радостью хотят присоединиться к вам. Людей: +$1' 30 | }, 31 | 32 | // food states --------------------------- 33 | { 34 | goodness: Goodness.negative, 35 | stat: 'food', 36 | value: -10, 37 | text: 'Кротокрысы на привале сожрали часть еды. Пропало пищи: -$1' 38 | }, 39 | 40 | { 41 | goodness: Goodness.negative, 42 | stat: 'food', 43 | value: -15, 44 | text: 'Радиоактивные осадки испортили часть запасов. Пищи: -$1' 45 | }, 46 | 47 | { 48 | goodness: Goodness.negative, 49 | stat: 'food', 50 | value: -5, 51 | text: 'В запасах еды завелись черви. Пропало пищи: -$1' 52 | }, 53 | 54 | { 55 | goodness: Goodness.positive, 56 | stat: 'food', 57 | value: 20, 58 | text: 'Следопыты нашли съедобный кактус. Запасы пищи: +$1' 59 | }, 60 | 61 | { 62 | goodness: Goodness.positive, 63 | stat: 'food', 64 | value: 20, 65 | text: 'Ваши люди подстрелили нападающих кротокрысов. Запасы пищи: +$1' 66 | }, 67 | 68 | { 69 | goodness: Goodness.positive, 70 | stat: 'food', 71 | value: 30, 72 | text: 'Атака гекконов успешно отражена. Запасы еды: +$1' 73 | }, 74 | 75 | { 76 | goodness: Goodness.positive, 77 | stat: 'food', 78 | value: 10, 79 | text: 'В руинах дома следопыты нашли довоенные консервы. Запасы еды: +$1' 80 | }, 81 | 82 | { 83 | goodness: Goodness.positive, 84 | stat: 'food', 85 | value: 5, 86 | text: 'На дороге найдены хорошие довоенные кожаные сапоги. Запасы еды: +$1' 87 | }, 88 | 89 | // money states --------------------------- 90 | { 91 | goodness: Goodness.negative, 92 | stat: 'money', 93 | value: -50, 94 | text: 'Воры выследили ваш караван. Денег: -$1' 95 | }, 96 | 97 | { 98 | goodness: Goodness.positive, 99 | stat: 'money', 100 | value: 15, 101 | text: 'У дороги найден мертвый путешественник. На теле найдены монеты. Денег: +$1' 102 | }, 103 | 104 | { 105 | goodness: Goodness.positive, 106 | stat: 'money', 107 | value: 5, 108 | text: 'Встречные охотники купили у вас товары. Денег: +$1' 109 | }, 110 | 111 | { 112 | goodness: Goodness.positive, 113 | stat: 'money', 114 | value: 5, 115 | text: 'Вы поймали вора, затаившегося у дороги, и отняли у него часть добычи! Денег: +$1' 116 | }, 117 | 118 | { 119 | goodness: Goodness.positive, 120 | stat: 'money', 121 | value: 12, 122 | text: 'Следопыты нашли и раскопали свежую могилу. Денег: +$1' 123 | }, 124 | 125 | // Волы ------------------------- 126 | 127 | { 128 | goodness: Goodness.negative, 129 | stat: 'oxen', 130 | value: -1, 131 | text: 'Радиоактивные гекконы напали на ваших быков. Браминов: -$1' 132 | }, 133 | 134 | { 135 | goodness: Goodness.positive, 136 | stat: 'oxen', 137 | value: 1, 138 | text: 'Найден одичалый брамин. Браминов: +$1' 139 | }, 140 | ]; -------------------------------------------------------------------------------- /js/plugins/WorldViewPlugin.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Функция для отображения текущего состояния мира 3 | * и лога событий 4 | * */ 5 | var WorldView = { 6 | // модель для хранения состояния View: предыдущие отображаемые значения 7 | // чтобы не дергать UI каждый раз, только при изменениях 8 | viewModel: { 9 | day: 0, 10 | crew: 0, 11 | oxen: 0, 12 | food: 0, 13 | firepower: 0, 14 | cargo: 0, 15 | money: 0, 16 | lastMessage: "", // обновление лога мониторим по последнему сообщению, так как размер лога теперь ограничен 17 | distance: 0, 18 | weight: 0, 19 | maxWeight: 0, 20 | } 21 | }; 22 | 23 | WorldView.init = function (world) { 24 | this.world = world; 25 | 26 | this.UI_DAY_TEXT = "День"; 27 | 28 | // элементы DOM находим сразу и запоминаем 29 | this.view = {}; 30 | this.view.distance = document.getElementById('game-stat-distance'); 31 | this.view.days = document.getElementById('game-stat-day'); 32 | this.view.crew = document.getElementById('game-stat-crew'); 33 | this.view.oxen = document.getElementById('game-stat-oxen'); 34 | this.view.food = document.getElementById('game-stat-food'); 35 | this.view.money = document.getElementById('game-stat-money'); 36 | this.view.firepower = document.getElementById('game-stat-firepower'); 37 | this.view.cargo = document.getElementById('game-stat-cargo'); 38 | this.view.log = document.getElementById('game-log'); 39 | this.view.weightBarText = document.getElementById('game-weight-bartext'); 40 | this.view.weightBarFill = document.getElementById('game-weight-barfill'); 41 | this.view.weight = document.getElementById('game-stat-cargo'); 42 | }; 43 | 44 | // Обновляем параметры по текущему состоянию мира 45 | // если какой-то параметр не менялся - обновления для него не происходит 46 | WorldView.update = function () { 47 | var world = this.world; 48 | if(this.viewModel.distance != world.distance){ 49 | this.view.distance.innerHTML = Math.floor(world.distance); 50 | this.viewModel.distance = world.distance; 51 | } 52 | 53 | if(this.viewModel.day != world.day){ 54 | this.view.days.innerHTML = Math.ceil(world.day); 55 | this.viewModel.day = world.day; 56 | } 57 | 58 | if(this.viewModel.crew != world.crew){ 59 | this.view.crew.innerHTML = world.crew; 60 | this.viewModel.crew = world.crew; 61 | } 62 | 63 | if(this.viewModel.oxen != world.oxen){ 64 | this.view.oxen.innerHTML = world.oxen; 65 | this.viewModel.oxen = world.oxen; 66 | } 67 | 68 | if(this.viewModel.food != world.food){ 69 | this.view.food.innerHTML = Math.ceil(world.food); 70 | this.viewModel.food = world.food; 71 | } 72 | 73 | if(this.viewModel.money != world.money){ 74 | this.view.money.innerHTML = Math.ceil(world.money); 75 | this.viewModel.money = world.money; 76 | } 77 | 78 | if(this.viewModel.firepower != world.firepower){ 79 | this.view.firepower.innerHTML = Math.ceil(world.firepower); 80 | this.viewModel.firepower = world.firepower; 81 | } 82 | 83 | if(this.viewModel.cargo != world.cargo){ 84 | this.view.cargo.innerHTML = Math.ceil(world.cargo); 85 | this.viewModel.cargo = world.cargo; 86 | } 87 | 88 | var lastMessage = world.log[world.log.length-1]; 89 | if (this.viewModel.lastMessage != lastMessage) { 90 | this.refreshLog(world.log); 91 | this.viewModel.lastMessage = lastMessage; 92 | } 93 | 94 | var weight = getCaravanWeight(world); 95 | var maxWeight = getCaravanMaxWeight(world); 96 | if(weight!=this.viewModel.weight || maxWeight!=this.viewModel.maxWeight){ 97 | var percent = Math.ceil(100*(Math.min(1, weight / maxWeight))); 98 | this.view.weightBarFill.style.width = percent+"%"; 99 | this.view.weightBarText.innerHTML = "общий вес "+Math.ceil(weight) + " / максимальный вес " + Math.ceil(maxWeight); 100 | this.viewModel.weight = weight; 101 | this.viewModel.maxWeight = maxWeight; 102 | } 103 | }; 104 | 105 | WorldView.refreshLog = function (log) { 106 | var messageLog = "", index; 107 | // лог показываем снизу вверх 108 | for (index = log.length - 1; index >= 0; index--) { 109 | messageLog += this.formatMessage(log[index]); 110 | } 111 | this.view.log.innerHTML = messageLog; 112 | }; 113 | 114 | WorldView.formatMessage = function (message) { 115 | var messageClass = 'log-message-'+message.goodness; 116 | var formatted = '
'; 117 | return formatted; 118 | }; 119 | 120 | Game.addPlugin(WorldView); -------------------------------------------------------------------------------- /js/plugins/BanditPlugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Столкновения с бандитами 3 | * 4 | * 5 | - встречи делятся на два этапа 6 | - приблизиться // возможны разные варианты, смотри ниже 7 | - бежать // вас атакуют в любом случае 8 | 9 | - у бандитов есть параметры 10 | - описание банды 11 | - число оружия 12 | - число человек 13 | - голод 0..1 14 | 15 | - бандиты нападают всегда, если у вас меньше оружия и людей (на 1 человека нападут всегда) 16 | 17 | - бандиты могут захотеть примкнуть к вам, если у вас столько же оружия или больше, 18 | есть еда и они голодны 19 | - цена снижается 20 | 21 | - бандитов можно перекупить, если у вас есть деньги 22 | - цена зависит от количества стволов и человек 23 | - цена высокая 24 | - вы не получаете денег 25 | * 26 | */ 27 | 28 | BanditPlugin = {}; 29 | 30 | BanditPlugin.init = function (world) { 31 | this.world = world; 32 | this.dialogView = DialogWindow; 33 | this.lastMeet = {x: -1, y: -1}; // координаты предыдущего стычки - чтобы не слишком часто 34 | this.lastTown = {x: -1, y: -1}; // координаты предыдущего города - чтобы не встречать караван в том же сегменте 35 | }; 36 | 37 | BanditPlugin.update = function () { 38 | var world = this.world; 39 | // если стоим на месте - бандиты не появляются 40 | if (world.stop || world.gameover) return; 41 | // проверка на выпадение события вообще 42 | // я использую стоп-условие, так как оно позволяет избегать лесенки c if 43 | // но вы можете использовать классический блок if 44 | 45 | // проверяем, не были ли выхода из города - если да, то запоминаем его 46 | if(this.lastTown.x != world.from.x || this.lastTown.y != world.from.y) 47 | { 48 | this.lastTown = { x: world.from.x, y: world.from.y }; 49 | this.lastMeet = { x: world.from.x, y: world.from.y }; 50 | } 51 | 52 | // проверяем расстояние между последней стычкой и текущими координатами 53 | var prevShopDistance = getDistance(world.caravan, this.lastMeet); 54 | if (prevShopDistance < BanditConstants.DISTANCE_MIN) return; 55 | 56 | if (!checkEventForStep(BanditConstants.EVENT_PROBABILITY)) return; 57 | 58 | // ну, понеслась! 59 | // караван останавливается 60 | world.stop = true; 61 | // флаг для блокировки UI в других плагинах включается 62 | world.uiLock = true; 63 | // генерируется случайная банда 64 | var bandits = BanditEvents.getRandom(); 65 | // она голодная по рандому от 0 до 1, 0 - самый сильный, "смертельный", голод 66 | bandits.hunger = Math.random(); 67 | // количество денег у бандитов - это явно функция от количества стволов 68 | bandits.money = bandits.firepower * BanditConstants.GOLD_PER_FIREPOWER; 69 | 70 | // цена найма бандитов за 1 человека 71 | bandits.price = BanditConstants.HIRE_PRICE_PER_PERSON; 72 | // коээффициент лута и потерь (будет менять от разных факторов) 73 | bandits.lootK = 1; 74 | // показываем окно с первым диалогом 75 | // this.showDialog("start"); 76 | this.dialogView.show(BanditDialogs, world, bandits, this); 77 | }; 78 | 79 | // отправляемся дальше 80 | BanditPlugin.onDialogClose = function () { 81 | this.world.uiLock = false; // снимаем захват с действий пользователя 82 | this.world.stop = false; // продолжаем путешествие 83 | }; 84 | 85 | /* Вычисление ущерба для команды от открытого сражения с бандитами 86 | * 1. ущерб - число погибших в команде 87 | * 2. ущерб не может быть больше команды, естественно 88 | * 3. ущерб растет в зависимости от силы оружия бандитов 89 | * 4. ущерб уменьшается при накапливании оружия в караване, но не уходит в ноль 90 | * 5. ущерб имеет рандомный разброс 91 | * */ 92 | BanditPlugin.getDamage = function (world, bandits) { 93 | // перевес каравана по оружию, минимум 0 94 | var caravanOverpowered = Math.max(0, world.firepower - bandits.firepower); 95 | // по мере возрастания caravanOverpowered - caravanOverPowerK будет стремиться от 1 к нулю, 96 | // не уходя в него полностью. 97 | // получаем коэффицинт от 1 до 0.01, уменьшающий дамаг 98 | var caravanOverPowerK = 1 / Math.sqrt(caravanOverpowered + 1); 99 | // таки в среднем baseDamage будет колебаться около bandits.firepower 100 | var baseDamage = bandits.firepower * 2 * Math.random(); 101 | // получаем уменьшающийся с прокачкой дамаг. Иногда даже будет вылетать ноль 102 | var damage = Math.round(baseDamage * caravanOverPowerK); 103 | // не может погибнуть больше, чем в команде 104 | damage = Math.min(damage, world.crew); 105 | return damage; 106 | }; 107 | 108 | /* 109 | * Вычисляем, сколько бандитов могут наняться к вам 110 | * */ 111 | BanditPlugin.getMaxHire = function (world, bandits) { 112 | // вычисляем по своему кошельку и их цене, или берем всех, если бандиты бесплатные 113 | var max = bandits.price > 0 ? Math.floor(world.money / bandits.price) : bandits.crew; 114 | return max; 115 | }; 116 | 117 | Game.addPlugin(BanditPlugin); -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | border: 0; 5 | } 6 | 7 | body { 8 | font-family: Verdana, Arial, sans-serif; 9 | background-color: #000; 10 | } 11 | 12 | .hidden { display: none; } 13 | .endfloat { float: none; clear: both; display: block; height: 0; } 14 | 15 | #w { 16 | width: 960px; 17 | margin: 0 auto; 18 | background: url("../images/bg_001.jpg") center; 19 | background-size: cover; 20 | 21 | border: 4px solid #3c2b21; 22 | border-top: none; 23 | border-radius: 0 0 12px 12px; 24 | 25 | position: relative; 26 | } 27 | 28 | #logo { 29 | display: block; 30 | float: left; 31 | width: 345px; 32 | height: 58px; 33 | margin-left: -12px; 34 | } 35 | 36 | a img { 37 | border: none; 38 | } 39 | 40 | #game { 41 | margin: 0px 24px 24px 24px; 42 | } 43 | 44 | .frame_border { 45 | margin: 0 auto; 46 | border: 8px inset #3c2b21; 47 | border-radius: 8px; 48 | } 49 | 50 | #game-map2d { 51 | width: 896px; 52 | height: 512px; 53 | background: url("../images/map_texture_001.png"); 54 | position: relative; 55 | } 56 | 57 | .town { 58 | width: 64px; 59 | height: 64px; 60 | background: url("../images/town_marker_green_001.png"); 61 | position: absolute; 62 | } 63 | 64 | #map-player { 65 | width: 64px; 66 | height: 64px; 67 | background: url("../images/map-marker-player.png") center no-repeat; 68 | position: absolute; 69 | } 70 | 71 | #game-weight-bar { 72 | float: right; 73 | width: 430px; 74 | height: 32px; 75 | margin: 10px 24px 0 0; 76 | background: #0c0906; 77 | border-radius: 8px; 78 | border: #3c2b21 inset 4px; 79 | overflow: hidden; 80 | } 81 | 82 | #game-weight-barfill { 83 | height: 100%; 84 | width: 50%; 85 | background: #484411; 86 | } 87 | 88 | #game-weight-bartext{ 89 | width: 100%; 90 | height: 100%; 91 | text-align: center; 92 | color: #90902c; 93 | font-size: 14px; 94 | line-height: 32px; 95 | margin-top: -32px; 96 | } 97 | 98 | #game-stats { 99 | margin: 6px auto 0 auto; 100 | width: 912px; 101 | } 102 | 103 | #game-stats div { 104 | float: left; 105 | width: 93px; 106 | height: 76px; 107 | margin-left: 18px; 108 | background: url("../images/ui-stat-indikator.png") top center no-repeat; 109 | text-align: center; 110 | } 111 | 112 | #game-stats span { 113 | display: block; 114 | height: 44px; 115 | } 116 | 117 | .game-stat-value { 118 | margin: 10px auto 0; 119 | font-size: 16px; 120 | color: black; 121 | } 122 | 123 | .game-stat-desc { 124 | margin: -8px auto 0; 125 | font-size: 16px; 126 | color: #cbcbcb; 127 | text-transform: uppercase; 128 | line-height: 16px; 129 | } 130 | 131 | #game-log { 132 | background: #130F09; 133 | padding: 12px; 134 | line-height: 150%; 135 | font-size: 16px; 136 | height: 200px; 137 | overflow: auto; 138 | border: #336626 solid 1px; 139 | border-radius: 8px; 140 | } 141 | 142 | .log-message-positive { 143 | color: #58a638; 144 | } 145 | 146 | .log-message-negative { 147 | color: #FF6633; 148 | } 149 | 150 | .log-message-neutral { 151 | color: #90908F; 152 | } 153 | 154 | #dialog { 155 | position: absolute; 156 | top: 140px; 157 | left: 50%; 158 | margin-left: -220px; 159 | width: 440px; 160 | 161 | border: 4px outset #3c2b21; 162 | border-radius: 12px; 163 | background: url("../images/bg_001.jpg"); 164 | 165 | padding: 16px 16px 4px 16px; 166 | text-align: center; 167 | } 168 | 169 | #dialog div, #dialog img, #dialog button { 170 | text-align: center; 171 | margin: 0 auto 8px auto; 172 | } 173 | 174 | #dialog-icon { 175 | display: block; 176 | border-radius: 8px; 177 | border: 6px inset #3c2b21; 178 | } 179 | 180 | #dialog-title{ 181 | font-size: 24px; 182 | margin-bottom: 12px; 183 | color: #cbcbcb; 184 | } 185 | 186 | #dialog-hint { 187 | font-size: 14px; 188 | color: #cbcbcb; 189 | padding: 4px; 190 | border-radius: 2px; 191 | background: rgba(11,11,11,0.6); 192 | } 193 | 194 | .dialog-choice { 195 | display: block; 196 | color: #cbcbcb; 197 | border: 2px outset #3c2b21; 198 | border-radius: 8px; 199 | background: #130F09; 200 | width: 300px; 201 | padding: 8px; 202 | } 203 | 204 | .red-button { 205 | margin: 0 auto; 206 | border-radius: 12px; 207 | background: url("../images/button_01.png") top left no-repeat; 208 | height: 49px; 209 | width: 200px; 210 | color: #cbcbcb; 211 | font-weight: bold; 212 | font-size: 16px; 213 | text-transform: uppercase; 214 | outline: none; 215 | } 216 | 217 | .red-button, .dialog-choice{ 218 | box-shadow: 0 4px rgba(22, 22, 22, 0.5); 219 | } 220 | 221 | .red-button:active, .dialog-choice:active { 222 | transform: translateY(4px); 223 | box-shadow: 0 0px rgba(22, 22, 22, 0.5); 224 | } -------------------------------------------------------------------------------- /js/plugins/ShopPlugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Плагин магазина 3 | * основная концепция - минимальная связь с другими классами 4 | * 5 | * 1. модуль вызывается из Game 6 | * 2. модуль имеет функцию отображения своего интерфейса 7 | * 3. модуль меняет состояние мира. 8 | * 9 | */ 10 | 11 | var ShopPlugin = {}; 12 | 13 | ShopPlugin.init = function (world) { 14 | this.world = world; 15 | this.shops = Shops; // возможные случаи магазинов, основы для генерация конкретной встречи 16 | 17 | this.lastShop = {x: -1, y: -1}; // координаты предыдущего магазина - чтобы не слишком часто 18 | this.lastTown = {x: -1, y: -1}; // координаты предыдущего города - чтобы не встречать караван в том же сегменте 19 | this.products = []; // продукты в конкретном магазине, генерируем 20 | }; 21 | 22 | ShopPlugin.update = function () { 23 | var world = this.world; 24 | if (world.stop) return; // если стоим - никаких новых магазинов 25 | 26 | // проверяем, не были ли выхода из города - если да, то запоминаем его 27 | if(this.lastTown.x != world.from.x || this.lastTown.y != world.from.y) 28 | { 29 | this.lastTown = { x: world.from.x, y: world.from.y }; 30 | this.lastShop = { x: world.from.x, y: world.from.y }; 31 | } 32 | 33 | // проверяем расстояние до предыдущего магазина, чтобы не частили 34 | var prevShopDistance = getDistance(world.caravan, this.lastShop); 35 | if (prevShopDistance < ShopEventConstants.SHOP_DISTANCE_MIN) return; 36 | 37 | // проверка на выпадение случайного магазина 38 | if (!checkEventForStep(ShopEventConstants.SHOP_PROBABILITY)) return; 39 | 40 | // стоп-условия выполнились 41 | world.stop = true; // караван остановился 42 | world.uiLock = true; // обозначаем, что действия пользователя теперь исключительно наши, пример: чтобы караван случайно не пошел по карте, если кликнем по ней при работе с магазином 43 | 44 | this.lastShop = { x: world.caravan.x, y: world.caravan.y }; // запоминаем магазинчик 45 | this.show(this.shops.getRandom()); // показываем магазин 46 | }; 47 | 48 | ShopPlugin.show = function (shop) { 49 | // добавляем сообщение о магазине в лог 50 | addLogMessage(this.world, Goodness.neutral, shop.text); 51 | // создаем набор продуктов по ассортименту данного магазина 52 | this.products = this.generateProducts(shop); 53 | // Создаем объект для отображения 1 диалога 54 | var ShopDialog = { 55 | start: { 56 | icon: ShopEventConstants.SHOP_PIC, // пока у магазина никакой иконки 57 | title: shop.text, // заголовок 58 | desc: ShopEventConstants.SHOP_HINT, // описание 59 | choices: [], // выбор продуктов и 60 | } 61 | }; 62 | 63 | // генерируем набор кнопок для продуктов 64 | var shopPlugin = this; 65 | var buttonText; // временная переменная для создания текста на кнопке продукта 66 | this.products.forEach(function (product) { 67 | buttonText = product.text + ' [' + product.qty + '] за $' + product.price; 68 | ShopDialog.start.choices.push({ 69 | text: buttonText, 70 | action: function () { 71 | if (product.price > shopPlugin.world.money) { 72 | addLogMessage(shopPlugin.world, Goodness.negative, ShopEventConstants.SHOP_NO_MONEY_MESSAGE); 73 | return "stop"; 74 | } 75 | shopPlugin.buy(product); 76 | return "start"; 77 | } 78 | }); 79 | }); 80 | 81 | // и добавляем кнопку для просто выхода 82 | ShopDialog.start.choices.push({ 83 | text: ShopEventConstants.SHOP_EXIT, 84 | action: function () { return "stop";} 85 | }); 86 | 87 | DialogWindow.show(ShopDialog, null, null, this); 88 | }; 89 | 90 | // Обязательная функция при использовании диалогов - коллбэк, вызываемый при закрытии 91 | ShopPlugin.onDialogClose = function () { 92 | this.world.uiLock = false; // снимаем захват с действий пользователя 93 | this.world.stop = false; // продолжаем путешествие 94 | }; 95 | 96 | // генерируем набор продуктов на основе базового 97 | ShopPlugin.generateProducts = function (shop) { 98 | var PRODUCTS_AMOUNT = 4; 99 | var numProds = Math.ceil(Math.random() * PRODUCTS_AMOUNT); 100 | var products = []; 101 | var j, priceFactor; 102 | 103 | for (var i = 0; i < numProds; i++) { 104 | j = Math.floor(Math.random() * shop.products.length); // берем случайный продукт из набора 105 | priceFactor = 0.7 + 0.6 * Math.random(); // //multiply price by random factor +-30% 106 | products.push({ 107 | item: shop.products[j].item, 108 | text: shop.products[j].text, 109 | qty: shop.products[j].qty, 110 | price: Math.round(shop.products[j].price * priceFactor) 111 | }); 112 | } 113 | return products; 114 | }; 115 | 116 | ShopPlugin.buy = function (product) { 117 | var world = this.world; 118 | world.money -= product.price; 119 | world[product.item] += product.qty; 120 | addLogMessage(world, Goodness.positive, ShopEventConstants.SHOP_BUY_MESSAGE + ' ' + product.text + ' +' + product.qty); 121 | }; 122 | 123 | Game.addPlugin(ShopPlugin); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
13 |
14 |