├── 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 = '
' + this.UI_DAY_TEXT + ' ' + Math.ceil(message.day) + ': ' + message.message + '
'; 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 | Nuke Caravan 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 |
15 |
16 |
груз 344/ максимальный груз 560
17 |
18 |
19 |
20 | 21 |
22 |
23 | 0 24 | Людей 25 |
26 |
27 | 0 28 | Браминов 29 |
30 |
31 | 0 32 | Еда 33 | 34 |
35 |
36 | 0 37 | Оружие 38 |
39 |
40 | 0 41 | Груз 42 |
43 |
44 | 0 45 | Деньги 46 |
47 |
48 | 0 49 | Пройдено 50 |
51 |
52 | 0 53 | День 54 |
55 | 56 |
57 | 58 | 59 |
60 | 65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | 77 | 78 |
79 |
80 |
81 | 82 | 83 | 88 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /js/data/BanditEvents.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Условия и параметры для встреч с бандитами 3 | * логика в BanditPlugin 4 | */ 5 | var BanditConstants = { 6 | DISTANCE_MIN: 100, // минимальное расстояние между стычками и от городов 7 | EVENT_PROBABILITY: 0.1, // базовая вероятность случайного события в текущий день 8 | ATTACK_PROBABILITY: 0.75, // вероятность атаки 9 | 10 | GOLD_PER_FIREPOWER: 5, // соотношение золота к количеству стволов - для генерации золота 11 | 12 | FIGHT_DEFENSE_K: 0.5, // Понижающий коээфициент для лута и потерь при обороне 13 | RUN_DAMAGE_K: 0.7, // Понижающий коээфициент для дамага по команде при побеге 14 | 15 | // коэффициенты потерь при побеге 16 | // конкретно тут логика такая, что еду и деньги мы прячем лучше 17 | // а бедным волам-браминам тяжело уворачиваться от пуль 18 | // ну и вообще бежать с грузом 19 | RUN_CARGO_LOST_K: 0.75, // для товаров 20 | RUN_OXES_LOST_K: 0.75, // для волов 21 | RUN_FOOD_LOST_K: 0.85, // для еды 22 | RUN_GOLD_LOST_K: 0.95, // для денег 23 | 24 | 25 | // Текст для голодного найма 26 | HIRE_INFO: "Людей для найма: $1. Оружия при них: $2.", 27 | HIRE_PRICE_INFO: "Цена найма: $$1", 28 | 29 | HIRE_PRICE_PER_PERSON: 60, // базовая ставка найма бандитов за 1 голову 30 | 31 | 32 | NOTICE_MESSAGE: "Вы наткнулись на бандитов!", // сообщение о том, что вы заметили бандитов, а они вас 33 | MEET_UP_CHOICE: "Подойти", // текст на кнопке для встречи с бандитами, есть шанс разойтись миром 34 | RUN_CHOICE: "Бежать", // текст на кнопке для побега. Теперь они в любом случае попробуют вас атаковать 35 | 36 | HUNGER_CARAVAN_FOOD_PER_PERSON_THRESHOLD: 3, // число порций еды на человека в караване, с которых считается, что у вас относительно много еды 37 | HUNGER_THRESHOLD: 0.5, // начиная с этой величины и меньше бандиты считаются голодными 38 | HUNGER_DEATH_THRESHOLD: 0.2, // начиная с этой величины бандиты считаются очень голодными и готовы присоединиться бесплатно 39 | 40 | HUNGER_GANG_MESSAGE: "Ослепленные вашим оружием и воодушевленные вашими запасами еды, бандиты решили примкнуть к вам!", // сообщение, что бандиты оказались голодны и слабовооружены, поэтому хотят примкнуть к вам за дешевый прайс, может и бесплатно 41 | HUNGER_HIRE_CHOICE: "Нанять $1 человек за $$2", // вместо $1 будет вычисленное число 42 | HIRE_DECLINE_CHOICE: "Отказать", // 43 | HUNGER_ATTACK_CHOICE: "Атаковать слабаков", // Во время атаки могут погибнуть ваши люди, но вы получаете оружие и деньги 44 | 45 | ATTACK_MESSAGE: "Увидев вашу слабость, подонки нападают!", // и когда бежишь, и когда встречаешься, но слаб 46 | ATTACK_CHOICE: "Принять бой", 47 | 48 | // на случай максимального голода - бандиты просятся бесплатно 49 | HUNGER_MAX_DECLINE_MESSAGE: "Бандиты догоняют вас и просят принять к себе", 50 | HUNGER_MAX_DECLINE_DESC: "Если вы откажете, мы умрем с голоду в этой проклятой пустоши!", 51 | // варианты выбора такие же как и при первом голодном найме 52 | 53 | RUN_MESSAGE: "Вы позорно бежали, преследуемые улюлюкающими бандитами", 54 | 55 | // тексты для разных результов боя 56 | FIGHT_LOST_MESSAGE: "Поражение!", 57 | FIGHT_LOST_DESC: "Бандиты одержали победу в бою", 58 | FIGHT_WIN_MESSAGE: "Победа!", 59 | FIGHT_WIN_DESC: "Вы одержали славную победу над бесславными ублюдками", 60 | 61 | // следующие тексты могут быть и при неудачном бое, и при побеге, просто различаются количеством 62 | LOST_WEAPON_MESSAGE: "Потеряно оружия: $1", // вместо $1 будет вычисленное число 63 | LOST_CREW_MESSAGE: "Погибло ваших людей: $1", 64 | LOST_MONEY_MESSAGE: "Потеряно денег: $$1", 65 | 66 | // это тексты - для победы 67 | LOOT_TITLE: "С павших бандитов вы подняли: ", 68 | LOOT_WEAPON_MESSAGE: "оружие: $1", // вместо $1 будет вычисленное число 69 | LOOT_MONEY_MESSAGE: "деньги: $$1", 70 | 71 | HIRE_FINISHED_TEXT: "Бандиты перешли на вашу сторону. Людей: +$1, Оружия: +$2", 72 | 73 | // финальная кнопка, все закончилось 74 | EXIT_BUTTON: "Дальше" 75 | }; 76 | 77 | 78 | // Возможное вооружение бандитов 79 | // можно добавлять свои варианты, сохраняя порядок возрастания 80 | var BanditFirepowers = [ 81 | 'безоружны', 82 | 'слабо вооружены', 83 | 'вооружены', 84 | 'хорошо вооружены', 85 | 'вооружены до зубов' 86 | ]; 87 | 88 | // Возможное число бандитов 89 | // можно добавлять свои варианты, сохраняя порядок возрастания 90 | var BanditNumbers = [ 91 | 'очень мало', 92 | 'мало', 93 | 'достаточно', 94 | 'много', 95 | 'очень много' 96 | ]; 97 | 98 | // Просто дополнительное описание бандитов, берется рандомом, порядок не важен 99 | // пожалуй, важно сохранять нейтрально-оценивающий характер 100 | var BanditAtmospheric = [ 101 | 'ковыряются в зубах и смотрят на вас с плотоядным интересом', 102 | 'оценивающе смотрят на ваш груз и стволы', 103 | 'видят ваш караван', 104 | 'возбужденно перекликиваются между собой и тычут пальцами в вашу сторону', 105 | 'заметили вас и направляются к каравану' 106 | ]; 107 | 108 | 109 | // разные варианты для поражения, выбираются случайно 110 | var BanditLostMessages = [ 111 | 'Вы потерпели унизительное поражение. Ваш караван уничтожен.', 112 | 'Ваши кости остались лежать в этой пустоши, вместе с костями ваших браминов.', 113 | 'Бандиты убили всех людей в вашем караване.', 114 | 'Караван погиб в стычке с рейдерами', 115 | 'Увы, никто не узнает, где ваша могила', 116 | 'Радиоактивный ветер воет над вашим остывающим телом. Все, что у вас было - отобрали бандиты. Включая жизнь.', 117 | 'Следующий караван найдет ваши кости и поймет, что эта дорога опасна', 118 | 'Пустынные бродяги торопливо обшаривают ваши тела и скрываются вдали' 119 | ]; 120 | 121 | // разные варианты для поражения, выбираются случайно 122 | var BanditWinMessages = [ 123 | 'Вы одержали славную победу, уничтожив всех бандитов.', 124 | 'Подлые налетчики убиты в честном бою.', 125 | 'Чудом выжив, вы приходите в себя после перестрелки и понимаете, что победили в этот раз.', 126 | 'Хо-хо! Караван выстоял в этой битве. Пришла пора обшарить трупы.', 127 | 'Никто не смеет недооценивать мою мощь!', 128 | 'Хотели напасть на мою прелесссть! Ха, теперь вы мертвы. Посмотрим, что у вас в карманцах!', 129 | 'Смерть бандитам. Караванский уклад един!' 130 | ]; 131 | 132 | // Разные варианты просьб для очень голодных бандитов 133 | 134 | var BanditDeathHungerMessages = [ 135 | 'Если вы не возьмете нас, мы погибнем здесь от голода, в этой проклятой пустыне!', 136 | 'Возьмите нас с собой! Мы согласны служить за еду, только не оставляйте нас на голодную смерть.', 137 | 'Мы не видели еды уже восемь дней. Если вы откажете нам, нам придется начать есть друг друга... или умереть от голода.', 138 | 'Пожалуйста... Умоляем вас... Иначе мы умрем тут', 139 | 'Еда... Мы готовы сделать что угодно за еду', 140 | ]; 141 | 142 | /* 143 | * Возможные варианты бандитов 144 | * */ 145 | var BanditEvents = [ 146 | { 147 | text: 'Радиоактивные гули', 148 | crew: 4, 149 | firepower: 3 150 | }, { 151 | text: 'Рейдеры', 152 | crew: 6, 153 | firepower: 12 154 | }, { 155 | text: 'Бродяги', 156 | crew: 2, 157 | firepower: 1 158 | }, { 159 | text: 'Дикари', 160 | crew: 8, 161 | firepower: 4 162 | }, 163 | ]; -------------------------------------------------------------------------------- /js/DialogWindow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Простое универсальное диалоговое окно для магазинов, атак и дропа 3 | * 1. один раз за игру вызываем init для привязки к DOM-элементам 4 | * 2. в любом плагине создаем набор диалогов в формате 5 | * var SomeDialogs = { 6 | * "start": { // обязательный тег 7 | icon: "ПУТЬ К ИКОНКЕ", 8 | exit: false, // или true если надо показать кнопку закрытия диалога 9 | title: "Заголовок диалога", 10 | desc: "Описание диалога", 11 | desc_action: function (arg1, arg2) { // любой набор операций, лишь бы возвращалась строка, 12 | которая добавиться к описанию диалога}, 13 | choices: [ // массив объектов для операций выбора в формате 14 | { 15 | text: "Выбор первый", 16 | action: function(arg1, arg2) { 17 | // любой набор операций с объектами arg1 и arg2 18 | // и любыми глобальными объектами 19 | } 20 | 21 | }, 22 | ... 23 | {...} 24 | ] 25 | * } 26 | * 27 | * 3. В плагине обязательно реализуем функцию для выполнения операций ПОСЛЕ закрытия диалога 28 | * для примера 29 | SomePlugin.onDialogClose = function () { 30 | this.world.uiLock = false; // снимаем захват с действий пользователя 31 | this.world.stop = false; // продолжаем путешествие 32 | }; 33 | 34 | 4. Вызываем в плагине диалог в формате 35 | this.world.uiLock = true; // снимаем захват с действий пользователя 36 | this.world.stop = true; // продолжаем путешествие 37 | dialogView.show(SomeDialogs, arg1, arg2, this); 38 | где 39 | SomeDialogs - подготовленный как указано выше ассоциативный массив диалогов 40 | arg1, arg2 - любые объекты для операций в диалогах (обычно world и собственный дата-объект плагина) 41 | this - ссылка на сам плагин, чтобы диалог при закрытии активировал onDialogClose 42 | */ 43 | 44 | var DialogWindow = { 45 | // массив финишных тегов, которые служат маркерами для выхода 46 | // они проверяются только в случае, если в описаниях диалогов нет таких диалогов 47 | // таким образом, эти теги можно переопределять 48 | finish_tags: [ "finish", "exit", "stop"] 49 | }; 50 | 51 | DialogWindow.init = function () { 52 | // два аргумента, через которые при вызове диалога конкретны плагином, можно передавать данные для модификации 53 | this.arg1 = {}; 54 | this.arg2 = {}; 55 | // описания диалогов, в одном окне может быть куча диалогов 56 | this.dialogs = []; 57 | // коллбэки для диалогов 58 | this.dialogActions = []; 59 | // вызывающий объект 60 | this.parent = {}; 61 | 62 | // находим и сохраняем все DOM-элементы, необходимые для вывода информации 63 | this.view = {}; 64 | this.view.window = document.getElementById('dialog'); // по сути не само окно, а окно с большой тенью 65 | this.view.title = document.getElementById('dialog-title'); 66 | this.view.icon = document.getElementById('dialog-icon'); 67 | this.view.hint = document.getElementById('dialog-hint'); 68 | this.view.choices = document.getElementById('dialog-choices'); 69 | this.view.exitButton = document.getElementById('dialog-exit-button'); 70 | 71 | // Обработка кликов 72 | // класс для кнопки выбора 73 | this.CHOICE_CLASS_NAME = 'dialog-choice'; 74 | this.CHOICE_ATTRIBUTE = 'choice'; 75 | 76 | // Добавляем реакцию на клик пользователя 77 | // используется универсальный listener для всего диалога 78 | // просто отслеживаем конкретно на чем кликнули 79 | var dialogWindow = this; 80 | this.view.window.addEventListener('click', this.listener.bind(this)); 81 | }; 82 | 83 | DialogWindow.listener = function (e) { 84 | var target = e.target || e.src; 85 | // клик на кнопке. Кнопка у нас - выход 86 | if (target.tagName == 'BUTTON') { 87 | this.close(); // выход 88 | return; // обработка закончилась 89 | } 90 | // клик на каком-то из выборов 91 | if (target.tagName == 'DIV' && target.className.indexOf(this.CHOICE_CLASS_NAME) !== -1) { 92 | // получаем из атрибута номер коллбэка 93 | var choiceIndex = target.getAttribute(this.CHOICE_ATTRIBUTE); 94 | // передаем этому коллбэку аргументы диалога 95 | // и получаем тег для следующего диалога 96 | var choiceTag = this.dialogActions[choiceIndex](this.arg1, this.arg2); 97 | this.showDialog(choiceTag); 98 | return; // обработка закончилась 99 | } 100 | }; 101 | 102 | /* 103 | * При вызове диалога ему следует передать ассоциативный массив описаний диалогов 104 | * и два аргумента, которые можно использовать в коллбэках выбора в конкретном диалоге 105 | * */ 106 | DialogWindow.show = function (dialogs, arg1, arg2, parent) { 107 | this.dialogs = dialogs; 108 | this.arg1 = arg1; 109 | this.arg2 = arg2; 110 | this.parent = parent; 111 | this.showDialog("start"); 112 | this.view.window.classList.remove("hidden"); 113 | }; 114 | 115 | // прячем окно и очищаем используемые переменные 116 | DialogWindow.close = function () { 117 | this.arg1 = {}; 118 | this.arg2 = {}; 119 | this.dialogs = []; 120 | this.dialogActions = []; 121 | this.view.exitButton.classList.add("hidden"); 122 | this.view.window.classList.add("hidden"); 123 | if(typeof this.parent.onDialogClose==="function"){ 124 | this.parent.onDialogClose(); 125 | } 126 | }; 127 | 128 | /* 129 | * Показываем диалог с тегом. 130 | * */ 131 | DialogWindow.showDialog = function (dialogTag) { 132 | // если такого тега нет - проверяем выход или ошибку 133 | if (!this.dialogs.hasOwnProperty(dialogTag)) { 134 | // если команда выхода - выходим 135 | if(this.finish_tags.indexOf(dialogTag)){ 136 | this.close(); 137 | return; 138 | } 139 | // иначе сообщение об ошибке 140 | console.log("!! DialogWindow Error! Диалог с названием " + dialogTag + " не найден"); 141 | return; 142 | } 143 | var dialog = this.dialogs[dialogTag]; 144 | this.view.title.innerHTML = this.getString(dialog, "title"); // устанавливаем заголовок диалога 145 | var imageSrc = this.getString(dialog, "icon"); 146 | if(imageSrc.length>0){ 147 | this.view.icon.setAttribute("src",imageSrc ); // устанавливаем картинку 148 | this.view.icon.classList.remove("hidden"); 149 | } 150 | else { 151 | this.view.icon.classList.add("hidden"); 152 | } 153 | 154 | 155 | // Описание. С возможностью вычисляемых параметров. 156 | var description = this.getString(dialog, "desc"); // если есть базовое описание - ставим его 157 | // Вычисляем дополнительную инфу для диалога - если у него реализована функция desc_action 158 | if (typeof dialog.desc_action === "function") { 159 | description += dialog.desc_action(this.arg1, this.arg2); 160 | } 161 | this.view.hint.innerHTML = description; 162 | 163 | // Очищаем предыдущие выборы 164 | this.dialogActions = []; // очищаем массив коллбэков 165 | this.view.choices.innerHTML = ''; // очищаем видимые элементы предыдущего выбора 166 | 167 | // если есть выборы - добавляем их 168 | var choices = this.getArr(dialog, "choices"); 169 | var i, choice; 170 | for (i = 0; i < choices.length; i++) { 171 | choice = dialog.choices[i]; 172 | this.addChoice(i, choice.text); // создаем div со специальным атрибутом с номером выполняемой функции 173 | this.dialogActions[i] = choice.action; // запоминаем коллбэк под этой же функцией 174 | } 175 | 176 | if (this.getBoolean(dialog, "exit")) { 177 | this.view.exitButton.classList.remove("hidden"); 178 | } 179 | else { 180 | this.view.exitButton.classList.add("hidden"); 181 | } 182 | }; 183 | 184 | 185 | // добавляем кнопку выбора действий в окно с текстом 186 | DialogWindow.addChoice = function (index, text) { 187 | this.view.choices.innerHTML += '
' + text + '
'; 188 | }; 189 | 190 | // проверяем, есть ли поле у объекта, если нет - возвращаем пустую строку 191 | DialogWindow.getString = function (dialog, fieldName) { 192 | return dialog.hasOwnProperty(fieldName) ? dialog[fieldName] : ""; 193 | }; 194 | 195 | // проверяем, есть ли поле у объекта, если нет - возвращаем пустой массив 196 | DialogWindow.getArr = function (dialog, arrFieldName) { 197 | return dialog.hasOwnProperty(arrFieldName) ? dialog[arrFieldName] : []; 198 | }; 199 | 200 | DialogWindow.getBoolean = function (dialog, booleanFieldName) { 201 | return dialog.hasOwnProperty(booleanFieldName) ? dialog[booleanFieldName] : false; 202 | }; -------------------------------------------------------------------------------- /js/data/BanditDialogs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Описания диалогов с бандитами 3 | * вся структура и логика переходов - здесь 4 | * Это объект, используемый как ассоциативный массив 5 | * названия полей служат для идентификации диалогов 6 | */ 7 | var BanditDialogs = { 8 | /* 9 | * Заготовка для диалога, копируйте и используйте для новых вариантов 10 | * */ 11 | "none": { // 12 | icon: "images/pic_bandit_meet.jpg", // true для иконки переговоров или победы. Не обязательный параметр. 13 | exit: false, // наличие кнопки выхода, после которой мы возвращаемся в обычную игру. Необязательный параметр. 14 | title: "Заголовок окна под иконкой", 15 | desc: "Возможно многословное описание ситуации мелким шрифтом. Можно пустую строку. Можно вообще не использовать параметр", 16 | 17 | // Необязательная функция. Можно в принципе не указывать 18 | // Позволяет добавлять вычисляемые параметры после desc. 19 | // Возвращайте строку. Если нет вычислений - пишите только в desc. 20 | desc_action: function (world, bandits) { 21 | return " "; 22 | }, 23 | 24 | // массив выборов - может быть сколько угодно 25 | // может быть пустым 26 | // можно не указывать параметр вообще - в BanditPlugin есть проверка на наличие, 27 | // если не указывать - никаких выборов не будет выведено 28 | choices: [ 29 | { 30 | text: "Вариант 1", 31 | action: function (world, bandits) { 32 | return "none"; // могут быть разные вычисления и много return 33 | } 34 | }, 35 | { 36 | text: "Вариант 2", 37 | action: function (world, bandits) { 38 | return "none"; // могут быть разные вычисления и много return 39 | } 40 | } 41 | ], 42 | }, 43 | 44 | 45 | /* 46 | * Стартовый диалог 47 | * */ 48 | "start": { 49 | icon: "images/pic_bandit_meet.jpg", 50 | title: "Вы наткнулись на бандитов!", 51 | desc_action: function (world, bandits) { 52 | var desc = bandits.text + " " + BanditAtmospheric.getRandom() + ". "; 53 | desc += "Они " + BanditFirepowers.getByDegree(bandits.firepower / bandits.crew) + "."; 54 | desc += "Число людей в банде: " + BanditNumbers.getByDegree(bandits.crew / 10) + "."; 55 | addLogMessage(world, Goodness.negative, this.title); // логируем описание 56 | addLogMessage(world, Goodness.negative, desc); // логируем описание 57 | return desc; 58 | }, 59 | choices: [ 60 | { 61 | text: "Подойти", // что показывается на кнопке 62 | action: function (world, bandits) { 63 | // первоначальная идея была такая 64 | // бандиты наглеют и лезут в бой, если у них больше оружия 65 | // но решено было отказаться - так как при накоплении оружия в караване все бандиты отказываются от атаки 66 | // это нереалистично и неинтересно. Всегда есть отморозки и оружие к тому же трудно оценить точно. 67 | 68 | // if (bandits.firepower > world.firepower) return "fight"; // ................... 69 | 70 | // поэтому оставили вариант с голым рандомом 71 | if (checkProbability(BanditConstants.ATTACK_PROBABILITY)) return "fight"; 72 | 73 | // переменные для найма 74 | var maxForHire; // максимум нанимающихся 75 | var firepowerAvg = bandits.firepower / bandits.crew; // среднее количество оружия у 1 бандита 76 | 77 | // голодный найм, 78 | if (bandits.hunger < BanditConstants.HUNGER_THRESHOLD) { 79 | bandits.price = Math.floor(bandits.price * bandits.hunger); // бандиты сбрасывают цену 80 | // защита от выпадения нуля 81 | maxForHire = BanditPlugin.getMaxHire(world, bandits); 82 | 83 | bandits.hired = {}; // добавляем в бандитов инфу о цене и количестве 84 | bandits.hired.crew = bandits.crew; // голодные хотят наняться все 85 | // разумеется, нанять мы можем не больше, чем у нас есть денег 86 | // условие наличия денег проверяется перед вызовом диалога 87 | bandits.hired.crew = Math.min(maxForHire, bandits.hired.crew); 88 | 89 | // вычисляем окончательную цену 90 | bandits.hired.price = Math.floor(bandits.price * bandits.hired.crew); 91 | bandits.hired.firepower = Math.ceil(bandits.hired.crew * firepowerAvg); 92 | return "hunger_talk"; 93 | } 94 | 95 | 96 | // обычный найм, если силы равны и никто не голоден, и у вас есть деньги 97 | maxForHire = BanditPlugin.getMaxHire(world, bandits); 98 | if (maxForHire > 0) { 99 | bandits.hired = {}; // добавляем в бандитов инфу о цене и количестве 100 | bandits.hired.crew = Math.floor(Math.random() * bandits.crew * 0.5); // сытые хотят наниматься не все 101 | bandits.hired.crew = Math.min(maxForHire, bandits.hired.crew); // гарантируем, что не нанимаем больше, чем у нас есть денег 102 | // если выпал ноль 103 | if(bandits.hired.crew == 0){ 104 | return "no_hire"; 105 | } 106 | // вычисляем окончательную цену 107 | bandits.hired.price = Math.floor(bandits.price * bandits.hired.crew); 108 | bandits.hired.firepower = Math.ceil(bandits.hired.crew * firepowerAvg); 109 | return "hire_talk"; 110 | } 111 | 112 | // нет денег 113 | addLogMessage(world, Goodness.neutral, "Вы расходитесь с бандитами миром"); 114 | return "hire_talk_nomoney"; 115 | } 116 | }, 117 | { 118 | text: "Бежать", 119 | action: function (world, bandits) { 120 | return "run"; // если бежим - бандиты при любом раскладе атакуют, зато меньше потерь 121 | } 122 | } 123 | ], 124 | }, 125 | 126 | "fight": { // 127 | icon: "images/pic_bandit_meet.jpg", 128 | title: "Сражение!", 129 | desc: "Наглые бастарды атаковали ваш караван с целью наживы", 130 | choices: [ 131 | { // полновесная стычка, где побеждает тот, у кого больше стволов 132 | text: "Открыть огонь из всех стволов!", 133 | action: function (world, bandits) { 134 | var damage = BanditPlugin.getDamage(world, bandits); world.crew -= damage; 135 | addLogMessage(world, Goodness.negative, 'В яростной атаке вы потеряли ' + damage + ' человек.'); 136 | var isWin = world.crew > 0; 137 | return isWin ? "win" : "lost"; 138 | } 139 | }, 140 | { // осторожный бой, по сути активное бегство, теряем людей, но меньше, чем при обычном бое 141 | text: "Занять круговую оборону и принять бой!", 142 | action: function (world, bandits) { 143 | bandits.lootK = BanditConstants.FIGHT_DEFENSE_K; // коээфициент лута и потерь 144 | var damage = BanditPlugin.getDamage(world, bandits); 145 | damage = Math.floor(damage*bandits.lootK); 146 | world.crew -= damage; 147 | addLogMessage(world, Goodness.negative, 'В бою погибло ' + damage + ' человек.'); 148 | var isWin = world.crew > 0; 149 | return isWin ? "win" : "lost"; 150 | } 151 | }, 152 | { // Караван, который пытается сбежать... грустное, должно быть, зрелище 153 | // теряем меньше всего людей 154 | // но неизбежно теряем какой-то процент браминов/волов/передвижных средств 155 | // и еды 156 | text: "Попытаться сбежать", 157 | action: function (world, bandits) { 158 | return "run"; // или гибнем, или успешно бежим 159 | } 160 | } 161 | ], 162 | }, 163 | 164 | "hunger_talk": { // 165 | icon: "images/pic_bandit_meet.jpg", 166 | title: "Бандиты хотят присоединиться!", 167 | desc: "Восхищенные вашим оружием и едой, голодные оборванцы хотят служить в вашем караване за минимальную цену!", 168 | desc_action: function (world, bandits) { 169 | var info = " " + BanditConstants.HIRE_INFO.withArg(bandits.hired.crew, bandits.hired.firepower); 170 | info += " " + BanditConstants.HIRE_PRICE_INFO.withArg(bandits.hired.price); 171 | return info; 172 | }, 173 | 174 | // массив выборов - может быть сколько угодно 175 | choices: [ 176 | { 177 | text: "Нанять", 178 | action: function (world, bandits) { 179 | world.crew += bandits.hired.crew; 180 | world.firepower += bandits.hired.firepower; 181 | world.money -= bandits.hired.price; 182 | return "hire_success"; 183 | } 184 | }, 185 | { 186 | text: "Отказать", 187 | action: function (world, bandits) { 188 | // если бандиты очень голодны - они попросятся еще раз 189 | var isDeathHunger = bandits.hunger <= BanditConstants.HUNGER_DEATH_THRESHOLD; 190 | var nextDialog = isDeathHunger ? "hunger_death_talk" : "hire_decline" 191 | return nextDialog; 192 | } 193 | } 194 | ], 195 | }, 196 | 197 | "hire_talk": { // 198 | icon: "images/pic_bandit_meet.jpg", 199 | title: "Разговор на равных", 200 | desc: "Бандиты выказывают респект вашему вооружению. Часть из них хочет примкнуть к вашему каравану.", 201 | desc_action: function (world, bandits) { 202 | var info = " " + BanditConstants.HIRE_INFO.withArg(bandits.hired.crew, bandits.hired.firepower); 203 | info += " " + BanditConstants.HIRE_PRICE_INFO.withArg(bandits.hired.price); 204 | return info; 205 | }, 206 | 207 | choices: [ 208 | { 209 | text: "Нанять", 210 | action: function (world, bandits) { 211 | world.crew += bandits.hired.crew; 212 | world.firepower += bandits.hired.firepower; 213 | world.money -= bandits.hired.price; 214 | return "hire_success"; 215 | } 216 | }, 217 | { 218 | text: "Отказать", 219 | action: function (world, bandits) { 220 | addLogMessage(world, Goodness.neutral, "Вы расходитесь с бандитами миром"); 221 | return "hire_decline"; 222 | } 223 | } 224 | ] 225 | }, 226 | 227 | "hire_decline": { 228 | icon: "images/pic_bandit_meet.jpg", 229 | exit: true, // финал, 230 | title: "Бандиты подавлены", 231 | desc: "Они хотели бы служить у вас, но вы отказали им по своей причине. Уходя, вы слышите выстрел. Кажется, кто-то из неудачников застрелился." 232 | }, 233 | 234 | "hire_success": { 235 | icon: "images/pic_bandit_meet.jpg", 236 | exit: true, // финал 237 | title: "Переговоры прошли успешно", 238 | desc_action: function (world, bandits) { 239 | var isAll = bandits.hired.crew == bandits.crew; 240 | var message = isAll ? "К вам присоединились все бандиты. " :"К вам присоединилась часть бандитов. "; 241 | message += "Людей: +" + bandits.hired.crew + ". "; 242 | message += "Оружия: +" + bandits.hired.firepower + ". "; 243 | 244 | var priceMessage = bandits.hired.price > 0 ? "Денег: -" + bandits.hired.price : " Это не стоило вам ничего"; 245 | message += priceMessage; 246 | addLogMessage(world, Goodness.positive, message); 247 | return message; 248 | } 249 | }, 250 | 251 | "hire_talk_nomoney": { 252 | icon: "images/pic_bandit_meet.jpg", 253 | exit: true, 254 | title: "Бандиты разочарованы", 255 | desc: "Они хотели бы наняться к вам, но у вас слишком мало денег", 256 | }, 257 | 258 | "no_hire": { 259 | icon: "images/pic_bandit_meet.jpg", 260 | exit: true, 261 | title: "Разговор в пустыне", 262 | desc: "Бандиты рассказывают последние новости о том, кого ограбили и убили. Затем вы прощаетесь со странным чувством. По какой-то причине они не стали нападать. И наняться к вам тоже никто не захотел. Возможно, все дело в вашей харизме?", 263 | }, 264 | 265 | "run": { 266 | icon: "images/pic_bandit_meet.jpg", 267 | title: "Побег", 268 | exit: true, // возвращение к обычной игре 269 | desc: "Воодушевленные вашим отступлением, бандиты стреляют вам вслед и улюлюкают.", 270 | desc_action: function (world, bandits) { 271 | var damage = BanditPlugin.getDamage(world, bandits); 272 | damage = Math.ceil(damage*BanditConstants.RUN_DAMAGE_K); // как минимум 1 выживет, так как округляем вверх, и коэффициент не 1 273 | 274 | 275 | world.crew -= damage; 276 | 277 | // караван несет потери 278 | var lostOxen = Math.min(world.oxen, Math.ceil(world.oxen * BanditConstants.RUN_OXES_LOST_K)); 279 | var lostFood = Math.min(world.food, Math.ceil(world.food * BanditConstants.RUN_FOOD_LOST_K)); 280 | var lostMoney = Math.min(world.money, Math.ceil(world.money * BanditConstants.RUN_GOLD_LOST_K)); 281 | var lostCargo = Math.min(world.cargo, Math.ceil(world.cargo * BanditConstants.RUN_CARGO_LOST_K)); 282 | world.oxen -= lostOxen; 283 | world.food -= lostFood; 284 | world.money -= lostMoney; 285 | world.cargo -= lostCargo; 286 | 287 | // создаем описание 288 | var desc = " Ваши потери: люди: " + damage; 289 | desc += " / браминов: " + lostOxen + " / eда: " + lostFood + ". Денег: " + lostMoney; 290 | addLogMessage(world, Goodness.negative, desc); // логируем описание 291 | 292 | return desc; 293 | } 294 | }, 295 | 296 | "lost": { 297 | icon: "images/pic_bandit_meet.jpg", 298 | exit: true, 299 | title: "Поражение!", 300 | desc_action: function (world, bandits) { 301 | var desc = BanditLostMessages.getRandom(); 302 | addLogMessage(world, Goodness.positive, desc); 303 | return BanditLostMessages.getRandom(); 304 | } 305 | }, 306 | 307 | "win": { 308 | icon: "images/pic_bandit_meet.jpg", 309 | exit: true, 310 | title: "Победа!", 311 | desc_action: function (world, bandits) { 312 | var lootFirepower = Math.floor(bandits.lootK * bandits.firepower); 313 | var lootMoney = Math.floor(bandits.lootK * bandits.money); 314 | 315 | var desc = BanditWinMessages.getRandom(); 316 | desc += " " + BanditConstants.LOOT_TITLE; 317 | desc += " " + BanditConstants.LOOT_WEAPON_MESSAGE.withArg(lootFirepower); 318 | desc += ", " + BanditConstants.LOOT_MONEY_MESSAGE.withArg(lootMoney); 319 | 320 | world.firepower += lootFirepower; 321 | world.money += lootMoney; 322 | 323 | addLogMessage(world, Goodness.positive, desc); 324 | return desc; 325 | } 326 | }, 327 | 328 | "hunger_death_talk": { 329 | icon: "images/pic_bandit_meet.jpg", 330 | title: "Бандиты просят принять их", 331 | desc:"Они готовы служить вам бесплатно. Вот что они говорят: ", 332 | desc_action: function (world, bandits) { 333 | return '"'+BanditDeathHungerMessages.getRandom()+'"'; 334 | }, 335 | choices: [ 336 | { 337 | text:"Спасти оборванцев от голодной смерти", 338 | action: function (world, bandits) { 339 | bandits.hired = {}; 340 | bandits.hired.crew = bandits.crew; // смертельно голодные хотят наняться все 341 | bandits.hired.price = 0; // и бесплатно 342 | bandits.hired.firepower = bandits.firepower; 343 | return "hire_success"; 344 | } 345 | }, 346 | { 347 | text: "Отказать", 348 | action: function (world, bandits) { 349 | return "hire_decline"; 350 | } 351 | } 352 | ] 353 | }, 354 | }; --------------------------------------------------------------------------------