├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── bin └── rpg-cli ├── config ├── box.json ├── creature.json ├── door.json ├── group.json ├── item.json ├── names │ ├── armor-names.json │ ├── box-names.json │ ├── consumable-names.json │ ├── creature-names.json │ ├── door-names.json │ ├── path-names.json │ ├── place-names.json │ └── weapon-names.json ├── path.json ├── place.json ├── player.json ├── race.json └── symbols.json ├── package.json └── src ├── actions.js ├── box.js ├── cli.js ├── commands ├── attack.js ├── boxes.js ├── character.js ├── creatures.js ├── debug.js ├── doors.js ├── drop.js ├── explore.js ├── group.js ├── help.js ├── inventory.js ├── leave.js ├── move.js ├── open.js ├── pickup.js ├── quit.js ├── start.js ├── stop.js └── templates │ ├── attack.js │ ├── boxes.js │ ├── character.js │ ├── creatures.js │ ├── doors.js │ ├── drop.js │ ├── inventory.js │ ├── leave.js │ ├── openBox.js │ ├── openDoor.js │ ├── pickup.js │ └── where.js ├── creature.js ├── door.js ├── group.js ├── index.js ├── item.js ├── name.js ├── parser.js ├── path.js ├── place.js ├── player.js ├── properties.js ├── race.js ├── rpg.js ├── test └── properties.js ├── uuid.js └── world.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ], 5 | "plugins": [ 6 | "transform-object-assign" 7 | ] 8 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.idea 3 | /browse.VC.db 4 | /.vscode 5 | /lib 6 | /test 7 | /rpg.db 8 | /fix_toni_and_force-push.sh 9 | npm-debug.log 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.idea 3 | /browse.VC.db 4 | /src 5 | /test 6 | /rpg.db 7 | /fix_toni_and_force-push.sh 8 | /npm-debug.log 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Toni Feistauer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rpg-library 2 | 3 | A `nodejs` library for text-based role-playing games. work in progress ❤️ 4 | 5 | 6 | # License 7 | 8 | The MIT License (MIT) 9 | 10 | Copyright (c) 2016 Toni Feistauer 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | SOFTWARE. 29 | -------------------------------------------------------------------------------- /bin/rpg-cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli'); // no ES6 here 4 | -------------------------------------------------------------------------------- /config/box.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "name", 3 | "open": false, 4 | "dexterity": 0, 5 | "items": [] 6 | } 7 | -------------------------------------------------------------------------------- /config/creature.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "John Doe", 3 | "properties": { 4 | "health": 100, 5 | "rank": 1, 6 | "attack": 0, 7 | "defense": 0, 8 | "aggression": 0 9 | }, 10 | "items": [] 11 | } 12 | -------------------------------------------------------------------------------- /config/door.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "name", 3 | "open": false, 4 | "path": false 5 | } -------------------------------------------------------------------------------- /config/group.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "name", 3 | "members": [] 4 | } -------------------------------------------------------------------------------- /config/item.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": [ 3 | { 4 | "type": "weapon", 5 | "name": "weapon", 6 | "collectible": true, 7 | "properties": { 8 | "rank": 0, 9 | "health": 0, 10 | "slots": 0, 11 | "attack": 0, 12 | "defense": 0, 13 | "speed": 0, 14 | "dexterity": 0, 15 | "time": 0 16 | } 17 | }, 18 | { 19 | "type": "armor", 20 | "name": "armor", 21 | "collectible": true, 22 | "properties": { 23 | "rank": 0, 24 | "health": 0, 25 | "slots": 0, 26 | "attack": 0, 27 | "defense": 0, 28 | "speed": 0, 29 | "dexterity": 0, 30 | "time": 0 31 | } 32 | }, 33 | { 34 | "type": "consumable", 35 | "name": "consumable", 36 | "collectible": true, 37 | "properties": { 38 | "rank": 0, 39 | "health": 0, 40 | "slots": 0, 41 | "attack": 0, 42 | "defense": 0, 43 | "dexterity": 0, 44 | "speed": 0, 45 | "time": 0 46 | } 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /config/names/armor-names.json: -------------------------------------------------------------------------------- 1 | { 2 | "pre1": [ 3 | "mighty", 4 | "rusty", 5 | "broken", 6 | "magical", 7 | "horrible", 8 | "violent", 9 | "funny", 10 | "secret", 11 | "bohemian", 12 | "Bernie's" 13 | ], 14 | "pre2": [ 15 | "shiny", 16 | "black", 17 | "white", 18 | "red", 19 | "blue", 20 | "yellow", 21 | "green", 22 | "pink", 23 | "purple", 24 | "leather", 25 | "iron", 26 | "steel", 27 | "wooden", 28 | "golden", 29 | "cotton", 30 | "golden", 31 | "silver", 32 | "copper" 33 | ], 34 | "main": [ 35 | "cap", 36 | "helmet", 37 | "shoes", 38 | "gloves", 39 | "vest", 40 | "trousers", 41 | "shield", 42 | "ring", 43 | "crown" 44 | ], 45 | "post": [ 46 | "of the sky", 47 | "from hell", 48 | "of death", 49 | "of war", 50 | "of love", 51 | "of blood", 52 | "of guts", 53 | "of rage", 54 | "of souls", 55 | "of doom™", 56 | "of quake™", 57 | "of light", 58 | "of darkness", 59 | "of your mother", 60 | "of the elves", 61 | "from africa", 62 | "with spikes" 63 | ] 64 | } -------------------------------------------------------------------------------- /config/names/box-names.json: -------------------------------------------------------------------------------- 1 | { 2 | "pre1": [ 3 | "small", 4 | "big", 5 | "tiny", 6 | "strange", 7 | "bohemian" 8 | ], 9 | "pre2": [ 10 | "hidden", 11 | "closed", 12 | "covered", 13 | "dirty", 14 | "broken", 15 | "golden", 16 | "silver", 17 | "copper" 18 | ], 19 | "main": [ 20 | "shelf", 21 | "box", 22 | "crate", 23 | "chest", 24 | "bag", 25 | "sack", 26 | "closet", 27 | "cabinet", 28 | "closet", 29 | "drawer" 30 | ], 31 | "post": [ 32 | "", 33 | "", 34 | "", 35 | "", 36 | "", 37 | "", 38 | "", 39 | "", 40 | "", 41 | "", 42 | "(it's a trap)", 43 | "of emptiness" 44 | ] 45 | } -------------------------------------------------------------------------------- /config/names/consumable-names.json: -------------------------------------------------------------------------------- 1 | { 2 | "pre1": [ 3 | "slimy", 4 | "old", 5 | "magical", 6 | "horrible", 7 | "violent", 8 | "dripping", 9 | "trippy", 10 | "smelly", 11 | "secret" 12 | ], 13 | "pre2": [ 14 | "red", 15 | "blue", 16 | "yellow", 17 | "green", 18 | "pink", 19 | "purple" 20 | ], 21 | "main": [ 22 | "potion", 23 | "apple", 24 | "drink", 25 | "meal", 26 | "cheese", 27 | "bread", 28 | "plant", 29 | "flower" 30 | ], 31 | "post": [ 32 | "of the sky", 33 | "from hell", 34 | "of doom", 35 | "of light", 36 | "of your mother", 37 | "of the elves", 38 | "of cannabis", 39 | "from africa" 40 | ] 41 | } -------------------------------------------------------------------------------- /config/names/creature-names.json: -------------------------------------------------------------------------------- 1 | { 2 | "pre1": [ 3 | "little", 4 | "small", 5 | "brutal", 6 | "Sir", 7 | "Lord", 8 | "dead", 9 | "mighty", 10 | "Farmer", 11 | "King" 12 | ], 13 | "pre2": [ 14 | "John", 15 | "Gelfri", 16 | "Eschast", 17 | "Wunnio", 18 | "Gaido", 19 | "Hrido", 20 | "Gebo", 21 | "Araerct", 22 | "Geri", 23 | "Mari", 24 | "Rido", 25 | "Thomas", 26 | "Charly", 27 | "Toni", 28 | "Bastian", 29 | "Daniel" 30 | ], 31 | "main": [ 32 | "McJohnson", 33 | "Uzgakh", 34 | "Gradbold", 35 | "Azog", 36 | "Grodush", 37 | "Ugluk", 38 | "Bulug", 39 | "Lurtzog", 40 | "Shuradb", 41 | "Oldolg", 42 | "Lagduf", 43 | "Murphy", 44 | "O'Sullivan", 45 | "Wallace", 46 | "Ackland", 47 | "Adams", 48 | "Allington", 49 | "Ashton", 50 | "Almond", 51 | "Badham", 52 | "Bancroft", 53 | "Bail", 54 | "Barrymore", 55 | "Baskin", 56 | "Beecroft", 57 | "Benett", 58 | "Bloomfield", 59 | "Bostwick", 60 | "Bosworth", 61 | "Boyle", 62 | "Bradshaw", 63 | "Bridges", 64 | "Broderick", 65 | "Browning", 66 | "Burton", 67 | "Callahan", 68 | "Carmody", 69 | "Catrall", 70 | "Chamberlain", 71 | "Clancy", 72 | "Combe", 73 | "Cort", 74 | "Corraface", 75 | "Corey", 76 | "Crichton", 77 | "Cromwell", 78 | "Darabont", 79 | "Davonport", 80 | "Dennehy", 81 | "Densham", 82 | "Devaney", 83 | "Donovan", 84 | "Dunn", 85 | "Edgecomb", 86 | "Eliot", 87 | "Farnsworth", 88 | "Fairchild", 89 | "Featherstone", 90 | "Forsythe", 91 | "Gady", 92 | "Gage", 93 | "Gallagher", 94 | "Gayheart", 95 | "Grantham", 96 | "Graves", 97 | "Graysmark", 98 | "Grieben", 99 | "Haddington", 100 | "Haddock", 101 | "Hamlin", 102 | "Hannigan", 103 | "Hardin", 104 | "Harrington", 105 | "Hathaway", 106 | "Hartshorn", 107 | "Hawn", 108 | "Hayman", 109 | "Henderson", 110 | "Hennessy", 111 | "Hensley", 112 | "Hiliard", 113 | "Hoskins", 114 | "Huxley", 115 | "Jones", 116 | "Kendall", 117 | "Kinmont", 118 | "Kirkwood", 119 | "Kober", 120 | "Lancaster", 121 | "Lankford", 122 | "Lansbury", 123 | "Leachman", 124 | "Leech", 125 | "Lester", 126 | "Lewis", 127 | "Lineback", 128 | "Lithgow", 129 | "Lockhart", 130 | "Locklear", 131 | "Lockwood", 132 | "Longfellow", 133 | "Lorring", 134 | "Madigan", 135 | "Malfoy", 136 | "Marley", 137 | "Marshal", 138 | "Mayhew", 139 | "McGowan", 140 | "McLeod", 141 | "Millington", 142 | "Mills", 143 | "Monroe", 144 | "Morriss", 145 | "Musgrave", 146 | "Neil", 147 | "Neville", 148 | "Nicksay", 149 | "Onnington", 150 | "Patton", 151 | "Payne", 152 | "Perlman", 153 | "Poe", 154 | "Prentiss", 155 | "Preston", 156 | "Primes", 157 | "Prinsloo", 158 | "Roades", 159 | "Robinson", 160 | "Reacock", 161 | "Ross", 162 | "Rushton", 163 | "Satchmore", 164 | "Shepherd", 165 | "Simpson", 166 | "Stanton", 167 | "Sterling", 168 | "Thackeray", 169 | "Thuringer", 170 | "Ward", 171 | "Warriner", 172 | "Warrington", 173 | "Whitman", 174 | "Willoughby", 175 | "Wiltshire", 176 | "Winfield", 177 | "Winston", 178 | "Wyler" 179 | ], 180 | "post": [ 181 | "from Johnsonville", 182 | "from Jena", 183 | "the butcher", 184 | "the unicorn", 185 | "the 3rd", 186 | "the " 187 | ] 188 | } 189 | -------------------------------------------------------------------------------- /config/names/door-names.json: -------------------------------------------------------------------------------- 1 | { 2 | "pre1": [ 3 | "small", 4 | "big", 5 | "tiny", 6 | "strange", 7 | "dirty", 8 | "humble", 9 | "little", 10 | "red", 11 | "violet", 12 | "ruined", 13 | "shattered", 14 | "demolished", 15 | "damaged" 16 | ], 17 | "pre2": [ 18 | "magic", 19 | "simple", 20 | "disgusting", 21 | "nasty", 22 | "wicked", 23 | "enchanted", 24 | "ugly", 25 | "dinky", 26 | "ordinary", 27 | "destroyed" 28 | ], 29 | "main": [ 30 | "door", 31 | "trapdoor", 32 | "hatch", 33 | "hatchway", 34 | "window", 35 | "gate", 36 | "gateway", 37 | "port", 38 | "vent" 39 | ], 40 | "post": [] 41 | } -------------------------------------------------------------------------------- /config/names/path-names.json: -------------------------------------------------------------------------------- 1 | { 2 | "pre1": [ 3 | "muddy", 4 | "dirty", 5 | "nice", 6 | "rainy", 7 | "wet", 8 | "curvy", 9 | "twisted", 10 | "crazy" 11 | ], 12 | "pre2": [ 13 | "stone", 14 | "grass", 15 | "dirt", 16 | "little", 17 | "serpentine", 18 | "winding" 19 | ], 20 | "main": [ 21 | "way", 22 | "street", 23 | "path", 24 | "road", 25 | "route", 26 | "track", 27 | "lane", 28 | "course", 29 | "avenue", 30 | "parkway", 31 | "alley", 32 | "catwalk" 33 | ], 34 | "post": [ 35 | "over a hill", 36 | "through the mountains", 37 | "through a forrest", 38 | "through a dark forrest", 39 | "through a field", 40 | "through a mine", 41 | "over mountains", 42 | "over bridges" 43 | ] 44 | } -------------------------------------------------------------------------------- /config/names/place-names.json: -------------------------------------------------------------------------------- 1 | { 2 | "pre1": [ 3 | "small", 4 | "big", 5 | "tiny", 6 | "strange", 7 | "dirty", 8 | "humble", 9 | "little", 10 | "red", 11 | "violet", 12 | "ruined", 13 | "shattered", 14 | "demolished", 15 | "damaged" 16 | ], 17 | "pre2": [ 18 | "magic", 19 | "simple", 20 | "disgusting", 21 | "nasty", 22 | "wicked", 23 | "enchanted", 24 | "ugly", 25 | "dinky", 26 | "ordinary", 27 | "destroyed" 28 | ], 29 | "main": [ 30 | "castle", 31 | "house", 32 | "hut", 33 | "cave", 34 | "chateau", 35 | "manor", 36 | "foxhole", 37 | "booth", 38 | "dungeon", 39 | "mushroom", 40 | "tower", 41 | "fort", 42 | "tomb", 43 | "pyramid", 44 | "obelisk", 45 | "memorial", 46 | "monument", 47 | "grave" 48 | ], 49 | "post": [ 50 | "of the king", 51 | "where the orcs live", 52 | "of your mother", 53 | "in the woods", 54 | "made of ivory", 55 | "called Greenflower", 56 | "on the hill", 57 | "of Toser", 58 | "called Durdnee", 59 | "in Jena" 60 | ] 61 | } -------------------------------------------------------------------------------- /config/names/weapon-names.json: -------------------------------------------------------------------------------- 1 | { 2 | "pre1": [ 3 | "mighty", 4 | "rusty", 5 | "broken", 6 | "magical", 7 | "horrible", 8 | "violent", 9 | "two-handed", 10 | "secret", 11 | "bohemian", 12 | "Zaphod's", 13 | "God's", 14 | "Devil's" 15 | ], 16 | "pre2": [ 17 | "shiny", 18 | "big", 19 | "small", 20 | "tiny", 21 | "black", 22 | "white", 23 | "red", 24 | "blue", 25 | "yellow", 26 | "green", 27 | "pink", 28 | "purple", 29 | "leather", 30 | "iron", 31 | "steel", 32 | "wooden", 33 | "monster", 34 | "golden", 35 | "silver", 36 | "copper" 37 | ], 38 | "main": [ 39 | "sword", 40 | "axe", 41 | "dagger", 42 | "mace", 43 | "spear", 44 | "club", 45 | "whip", 46 | "hammer", 47 | "knife", 48 | "staff", 49 | "dildo" 50 | ], 51 | "post": [ 52 | "of the sky", 53 | "from hell", 54 | "of death", 55 | "of war", 56 | "of love", 57 | "of blood", 58 | "of guts", 59 | "of rage", 60 | "of souls", 61 | "of doom™", 62 | "of quake™", 63 | "of light", 64 | "of darkness", 65 | "of your mother", 66 | "of the elves", 67 | "from africa" 68 | ] 69 | } -------------------------------------------------------------------------------- /config/path.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "name", 3 | "distance": 0, 4 | "places": [] 5 | } -------------------------------------------------------------------------------- /config/place.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "name", 3 | "location": false, 4 | "boxes": [], 5 | "groups": [], 6 | "doors": [], 7 | "creatures": [], 8 | "explored": false 9 | } -------------------------------------------------------------------------------- /config/player.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "John Doe", 3 | "properties": { 4 | "health": 100, 5 | "rank": 1, 6 | "attack": 0, 7 | "defense": 0, 8 | "dexterity": 0, 9 | "speed": 4, 10 | "slots": 30 11 | }, 12 | "items": [], 13 | "weapon": null, 14 | "armor": null 15 | } 16 | -------------------------------------------------------------------------------- /config/race.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": [ 3 | { 4 | "type": "cat", 5 | "properties": { 6 | "attack": 5, 7 | "defense": 10, 8 | "dexterity": 8, 9 | "speed": 8 10 | } 11 | }, 12 | { 13 | "type": "dog", 14 | "properties": { 15 | "attack": 12, 16 | "defense": 6, 17 | "dexterity": 4, 18 | "speed": 6 19 | } 20 | }, 21 | { 22 | "type": "human", 23 | "properties": { 24 | "attack": 7, 25 | "defense": 7, 26 | "dexterity": 7, 27 | "speed": 6 28 | } 29 | }, 30 | { 31 | "type": "orc", 32 | "properties": { 33 | "attack": 14, 34 | "defense": 7, 35 | "dexterity": 3, 36 | "speed": 4 37 | } 38 | }, 39 | { 40 | "type": "butterfly", 41 | "properties": { 42 | "attack": 3, 43 | "defense": 5, 44 | "dexterity": 10, 45 | "speed": 7 46 | } 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /config/symbols.json: -------------------------------------------------------------------------------- 1 | { 2 | "race": { 3 | "cat": "😼", 4 | "dog": "🐶", 5 | "human": "😏", 6 | "orc": "👿", 7 | "butterfly": "Ƹ̵̡Ӝ̵̨̄Ʒ", 8 | "snail": "🐌" 9 | }, 10 | 11 | "itemType": { 12 | "weapon": "🗡", 13 | "armor": "🛡", 14 | "consumable": "🍴", 15 | "unknown": "❓" 16 | }, 17 | 18 | "propertie": { 19 | "rank": "🎖", 20 | "slots": "👜", 21 | "health": "♥", 22 | "attack": "👊", 23 | "defense": "✋", 24 | "dexterity": "👌", 25 | "speed": "👣", 26 | "time": "⏳" 27 | }, 28 | 29 | "state": { 30 | "open": "🔓", 31 | "close": "🔒", 32 | "locked": "🚷", 33 | "walk": "🚶", 34 | "dead": "☠" 35 | }, 36 | 37 | "presentational": { 38 | "door": "🚪", 39 | "clock": "🕙", 40 | "redDown": "🔻" 41 | } 42 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rpg-library", 3 | "version": "0.0.9", 4 | "description": "A nodejs framework for text based role-playing games", 5 | "main": "lib/rpg.js", 6 | "bin": { 7 | "rpg-cli": "bin/rpg-cli" 8 | }, 9 | "scripts": { 10 | "cli": "npm run build && node bin/rpg-cli", 11 | "start": "npm run build && node lib/index.js", 12 | "build": "babel src -d lib", 13 | "buildTest": "babel src/test -d test", 14 | "test": "npm run build && npm run buildTest && mocha", 15 | "prepublish": "npm run build" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/toser/node-rpg-library.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/toser/node-rpg-library.git/issues" 23 | }, 24 | "author": "Toni Feistauer", 25 | "license": "MIT", 26 | "dependencies": { 27 | "helptos": "^1.0.10", 28 | "lodash": "^4.15.0", 29 | "nedb": "^1.8.0", 30 | "random-tools": "0.0.5" 31 | }, 32 | "devDependencies": { 33 | "babel-cli": "^6.6.5", 34 | "babel-plugin-transform-object-assign": "^6.5.0", 35 | "babel-preset-es2015": "^6.6.0", 36 | "chai": "^3.5.0", 37 | "mocha": "^2.4.5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/actions.js: -------------------------------------------------------------------------------- 1 | import * as path from './path'; 2 | import * as box from './box'; 3 | import * as creature from './creature'; 4 | import {newLocation} from './world'; 5 | import {randomInt} from 'random-tools'; 6 | 7 | 8 | /** 9 | * transfer an item from one thing to another 10 | * e.g. box to player, player to player, creature to player 11 | * 12 | * @param from 13 | * @param to 14 | * @param id 15 | * @returns {boolean} 16 | */ 17 | export const itemTransfer = (from, to, id) => { 18 | 19 | const foundItem = from.items.list('id', id); 20 | 21 | // [from] has item 22 | if(!foundItem.length){ 23 | return { 24 | success: false, 25 | error: 'item unavailable' 26 | }; 27 | } 28 | 29 | // get first found item 30 | const item = foundItem[0]; 31 | 32 | // check if [from] has open property 33 | // if from.open is false return false 34 | if('open' in from && 35 | !from.open.get()) { 36 | 37 | return { 38 | success: false, 39 | error: 'from open' 40 | }; 41 | } 42 | 43 | // check if [to] has open property 44 | // if to.open is false return false 45 | if('open' in to && 46 | !to.open.get()) { 47 | 48 | return { 49 | success: false, 50 | error: 'to open' 51 | }; 52 | } 53 | 54 | // check if [to] and [item] has rank property 55 | // if [to]s rank is not high enougth return a fail 56 | if('rank' in to && 57 | 'rank' in item && 58 | to.rank.get() < item.rank.get()) { 59 | 60 | return { 61 | success: false, 62 | error: 'rank' 63 | }; 64 | } 65 | 66 | // check if [to] and [item] has slots property 67 | // if [to] has not enougth slots free return a fail 68 | if('slots' in to && 69 | 'slots' in item && 70 | to.slots.free() < item.slots.get()) { 71 | 72 | return { 73 | success: false, 74 | error: 'slots' 75 | }; 76 | } 77 | 78 | // add item to [to]s item list 79 | to.items.add(item); 80 | 81 | // check if [to] has the item now 82 | // then remove it from [from]s item list 83 | if(to.items.list('id', id).length) { 84 | from.items.remove(id); 85 | return { 86 | success: true 87 | }; 88 | } 89 | else { 90 | return { 91 | success: false, 92 | error: 'unknown' 93 | }; 94 | } 95 | }; 96 | 97 | /** 98 | * get average dexterity of items in box 99 | * when player has higher dexterity as 75% of average item dexterity 100 | */ 101 | export const openBox = (box, player) => { 102 | 103 | const items = box.summary.items.get(), 104 | boxDexterity = (items.reduce((dex, item) => dex + item.dexterity, 0) / items.length) * .95; 105 | let canOpen = player.dexterity.get() >= boxDexterity; 106 | 107 | // always open when box is already open 108 | if(box.open.get()) { 109 | canOpen = true; 110 | } 111 | 112 | box.open.set(canOpen); 113 | 114 | if(canOpen) { 115 | return { 116 | success: true 117 | }; 118 | } 119 | else { 120 | return { 121 | success: false, 122 | error: 'dexterity' 123 | } 124 | } 125 | }; 126 | 127 | 128 | export const openDoor = ({door, player, place}) => { 129 | 130 | let canOpen = true; // ToDo: add conditions here 131 | 132 | if(door.open.get()) { 133 | canOpen = true; 134 | } 135 | 136 | door.open.set(canOpen); 137 | 138 | if(!door.path.get()){ 139 | door.path.set(path.createPath({ 140 | currentPlace: place 141 | })); 142 | } 143 | 144 | if(canOpen) { 145 | return { 146 | success: true 147 | }; 148 | } 149 | else { 150 | return { 151 | success: false, 152 | error: 'unknown' 153 | } 154 | } 155 | }; 156 | 157 | export const leave = ({door, group, place, world}) => { 158 | 159 | const foundPlace = door.path.get() 160 | .places.list() 161 | .filter(x => x.name.get() !== place.name.get()); 162 | 163 | const numberOfBoxes = randomInt(7, 1); 164 | const numberOfCreatures = randomInt(6, 1); 165 | 166 | const newPlace = foundPlace[0]; 167 | 168 | if(!newPlace.location.get()) { 169 | newPlace.location.set(newLocation(world, place.location.get())); 170 | } 171 | 172 | const location = newPlace.location.get(); 173 | 174 | if(!newPlace.boxes.list().length) { 175 | newPlace.boxes.add(box.createBoxes({ 176 | average: group.info.average() 177 | }, numberOfBoxes)); 178 | } 179 | 180 | if(!newPlace.creatures.list().length) { 181 | newPlace.creatures.add(creature.createCreatures({ 182 | average: group.info.average() 183 | }, numberOfCreatures)); 184 | } 185 | 186 | newPlace.groups.add(group); 187 | newPlace.location.set(location); 188 | world.places[location] = newPlace; 189 | world.currentPlace = location; 190 | 191 | place.groups.remove(group.id.get()); 192 | 193 | return { 194 | success: true 195 | }; 196 | }; 197 | 198 | export const attack = ({attacker, defender, weapon, armor}) => { 199 | 200 | let power = attacker.attack.get(), 201 | defense = defender.defense.get(); 202 | 203 | if (weapon) { 204 | power += weapon.attack.get(); 205 | } 206 | 207 | if (armor) { 208 | defense += armor.defense.get(); 209 | } 210 | 211 | console.log('power', power); 212 | console.log('defense', defense); 213 | 214 | if (power > defense) { 215 | defender.health.down(power - defense); 216 | 217 | return { 218 | success: true, 219 | data: { 220 | power, 221 | defense 222 | } 223 | }; 224 | } else { 225 | if (armor) { 226 | attacker.health.down(armor.attack.get()); 227 | } 228 | 229 | return { 230 | success: false, 231 | data: { 232 | power, 233 | defense 234 | }, 235 | error: 'defended' 236 | } 237 | } 238 | }; 239 | -------------------------------------------------------------------------------- /src/box.js: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import {getConfig, copyObject} from 'helptos'; 3 | import {randomInt} from 'random-tools'; 4 | import {createName} from './name'; 5 | import * as properties from './properties'; 6 | import * as item from './item'; 7 | 8 | const config = getConfig('../config/box.json', __dirname); 9 | const boxNames = getConfig('../config/names/box-names.json', __dirname); 10 | 11 | const name = state => Object.assign({}, properties.mixed('name', state)); 12 | const open = state => Object.assign({}, properties.boolean('open', state)); 13 | 14 | const items = state => Object.assign({}, 15 | // get default list functionality 16 | properties.list( 17 | 'items', 18 | state, 19 | state, 20 | [ 21 | item.types.WEAPON, 22 | item.types.ARMOR, 23 | item.types.CONSUMABLE 24 | ] 25 | ) 26 | ); 27 | 28 | const summary = state => ({ 29 | 30 | get: () => { 31 | 32 | const box = state.element; 33 | 34 | return { 35 | name: box.name.get(), 36 | open: box.open.get(), 37 | items: box.items.list().map(item => item.summary.get()) 38 | }; 39 | }, 40 | short: () => { 41 | 42 | const box = state.element; 43 | 44 | return { 45 | name: box.name.get(), 46 | open: box.open.get() 47 | }; 48 | }, 49 | items: { 50 | get: () => state.element.items.list().map(item => item.summary.get()), 51 | short: () => state.element.items.list().map(item => item.summary.short()) 52 | } 53 | 54 | }); 55 | 56 | const newBox = (boxName) => { 57 | 58 | let state = copyObject(config); 59 | 60 | state.name = boxName; 61 | 62 | state.element = { 63 | name: name(state), 64 | open: open(state), 65 | items: items(state), 66 | summary: summary(state), 67 | event: new EventEmitter() 68 | }; 69 | 70 | return state.element; 71 | }; 72 | 73 | export const createBox = ({average}) => { 74 | 75 | const weaponsCount = randomInt(2, 0), 76 | armorCount = randomInt(2, 0), 77 | consumableCount = randomInt(3, 1); 78 | 79 | let box = newBox(createName(boxNames)), 80 | items = [], 81 | i; 82 | 83 | for (i = 0; i < weaponsCount; i++) { 84 | 85 | items.push(item.createWeapon({ 86 | rank: average.rank, 87 | slots: 6, 88 | attack: average.attack, 89 | defense: 0, 90 | dexterity: average.dexterity, 91 | speed: -5 92 | })); 93 | } 94 | 95 | for (i = 0; i < armorCount; i++) { 96 | 97 | items.push(item.createArmor({ 98 | rank: average.rank, 99 | slots: 4, 100 | attack: 2, 101 | defense: average.defense, 102 | dexterity: average.dexterity, 103 | speed: -3 104 | })); 105 | } 106 | 107 | for (i = 0; i < consumableCount; i++) { 108 | 109 | items.push(item.createConsumable({ 110 | rank: average.rank, 111 | slots: 2, 112 | attack: average.attack, 113 | defense: average.defense, 114 | dexterity: average.dexterity, 115 | speed: average.speed, 116 | health: average.health 117 | })); 118 | } 119 | 120 | items.forEach(item => { 121 | box.items.add(item); 122 | }); 123 | 124 | return box; 125 | }; 126 | 127 | export const createBoxes = (options, count) => { 128 | 129 | let i = 0, 130 | boxes = []; 131 | 132 | for (i; i < count; i++) { 133 | boxes.push(createBox(options)); 134 | } 135 | 136 | return boxes; 137 | }; 138 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | 2 | import * as RPG from './rpg'; 3 | 4 | // initialized 5 | 6 | const rpg = RPG.createRPG(), 7 | user = 'player', 8 | indicateUserInput = () => { 9 | process.stdout.write('> '); 10 | }; 11 | 12 | // hooking up the stdin for the user commands 13 | let stdin = process.openStdin(); 14 | console.log('initialized. write your commands, e.g. "help" (confirm by pressing Enter):'); 15 | indicateUserInput(); 16 | stdin.addListener('data', (data) => { 17 | try { 18 | rpg.parse(user, data.toString().trim(), console.log, indicateUserInput); 19 | } catch(error) { 20 | console.log('error:', error.stack); 21 | process.stdout.write('> '); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /src/commands/attack.js: -------------------------------------------------------------------------------- 1 | 2 | import {attack} from '../actions'; 3 | import * as template from './templates/attack'; 4 | 5 | export const cmdRegExp = /^(attack|a) (\S[ \S]*\S) *(>) *(\S[ \S]*\S)$/; 6 | 7 | export const run = (player, command, world) => { 8 | 9 | if (!world.currentPlace) { 10 | return [ `world is not initialized .. yet.` ]; 11 | } else if (world.getPlayers(player).length > 0) { 12 | 13 | const matches = cmdRegExp.exec(command), 14 | activePlayer = world.getPlayers(player)[0], 15 | place = world.places[world.currentPlace], 16 | weaponName = matches[2], 17 | creatureName = matches[4]; 18 | let weapon, 19 | creature, 20 | armor; 21 | 22 | weapon = activePlayer.items.listWeapon('name', weaponName); 23 | 24 | if(!weapon.length) { 25 | weapon = false; 26 | creature = place.creatures.list('name', weaponName); 27 | if(!creature.length) { 28 | return template.fail({ 29 | playerName: player, 30 | weaponName 31 | }, 'not found'); 32 | } 33 | } else { 34 | weapon = weapon[0]; 35 | creature = place.creatures.list('name', creatureName); 36 | if(!creature.length) { 37 | return template.fail({ 38 | playerName: player, 39 | creatureName, 40 | placeName: place.name.get() 41 | }, 'not found creature'); 42 | } 43 | creature = creature[0]; 44 | } 45 | 46 | armor = creature.items.listArmor(); 47 | 48 | if (armor.length === 1) { 49 | armor = armor[0]; 50 | } else if (armor.length > 1) { 51 | armor = armor[Math.round(Math.random() * (armor.length - 1))]; 52 | } else { 53 | armor = false; 54 | } 55 | 56 | const fight = attack({ 57 | attacker: activePlayer, 58 | defender: creature, 59 | weapon, 60 | armor 61 | }); 62 | 63 | if (fight.success) { 64 | return template.success({ 65 | attacker: activePlayer.summary.get(), 66 | defender: creature.summary.get(), 67 | power: fight.data.power, 68 | defense: fight.data.defense, 69 | weapon: weapon.summary.get(), 70 | armor: armor.summary.get() 71 | }); 72 | } 73 | 74 | return template.fail({ 75 | attacker: activePlayer.summary.get(), 76 | defender: creature.summary.get(), 77 | power: fight.data.power, 78 | defense: fight.data.defense, 79 | weapon: weapon.summary.get(), 80 | armor: armor.summary.get() 81 | }, 'defended'); 82 | 83 | 84 | } else { 85 | return [ `${player} is not a registered player.` ]; 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /src/commands/boxes.js: -------------------------------------------------------------------------------- 1 | 2 | import * as template from './templates/boxes'; 3 | 4 | export const cmdRegExp = /^(boxes|box|b) *(\S[ \S]+\S){0,1}$/; 5 | 6 | export const run = (player, command, world) => { 7 | if (!world.currentPlace) { 8 | return [ `world is not initialized .. yet.` ]; 9 | } else if (world.getPlayers(player).length > 0) { 10 | 11 | const place = world.places[world.currentPlace]; 12 | 13 | if (place.explored.get()) { 14 | return template.success({ 15 | place: place.summary.get(), 16 | boxes: place.summary.boxes.get() 17 | }); 18 | } 19 | return template.fail({ 20 | place: place.summary.get() 21 | }); 22 | 23 | } else { 24 | return [ `${player} is not a registered player.` ]; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/commands/character.js: -------------------------------------------------------------------------------- 1 | import * as player from '../player'; 2 | import * as race from '../race'; 3 | import * as template from './templates/character'; 4 | 5 | const characters = race.getRaces().map(x => player.createPlayer(x, x).summary.get()); 6 | 7 | export const cmdRegExp = /^(character|char) *(\S+){0,1}$/; // " *" vs ( \S+) :-( 8 | 9 | export const run = (playerName, command, world) => { 10 | if (!world.playerGroup) { 11 | return [ `no group for players started .. yet.` ]; 12 | } else if (world.getPlayers(playerName).length > 0) { 13 | return [ `character "${playerName}" already exists!` ]; 14 | } else { 15 | let matches = cmdRegExp.exec(command), 16 | selectedRace = matches[2] ? matches[2].trim() : null; 17 | if (!race || race.getRaces().indexOf(selectedRace) === -1){ // no or wrong race chosen 18 | 19 | return template.fail({ 20 | characters: characters, 21 | playerName: playerName 22 | }); 23 | } 24 | 25 | let character = player.createPlayer(playerName, selectedRace); 26 | world.playerGroup.members.add(character); 27 | 28 | return template.success({ 29 | character: character.summary.get(), 30 | group: world.playerGroup.summary.get() 31 | }); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/commands/creatures.js: -------------------------------------------------------------------------------- 1 | 2 | import * as template from './templates/creatures'; 3 | 4 | export const cmdRegExp = /^(who is here|who|creatures) *(\S[ \S]+\S){0,1}$/; 5 | 6 | export const run = (player, command, world) => { 7 | if (!world.currentPlace) { 8 | return [ `world is not initialized .. yet.` ]; 9 | } else if (world.getPlayers(player).length > 0) { 10 | 11 | const place = world.places[world.currentPlace]; 12 | 13 | if (place.explored.get()) { 14 | return template.success({ 15 | place: place.summary.get(), 16 | creatures: place.summary.creatures.long() 17 | }); 18 | } 19 | return template.fail({ 20 | place: place.summary.get() 21 | }); 22 | 23 | } else { 24 | return [ `${player} is not a registered player.` ]; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/commands/debug.js: -------------------------------------------------------------------------------- 1 | 2 | export const cmdRegExp = /^(debug)$/; 3 | 4 | export const run = (player, command, world) => { 5 | return [ { action : 'debug' } ]; 6 | }; 7 | -------------------------------------------------------------------------------- /src/commands/doors.js: -------------------------------------------------------------------------------- 1 | 2 | import * as template from './templates/doors'; 3 | 4 | export const cmdRegExp = /^(doors|door|d)$/; 5 | 6 | export const run = (player, command, world) => { 7 | if (!world.currentPlace) { 8 | return [ `world is not initialized .. yet.` ]; 9 | } else if (world.getPlayers(player).length > 0) { 10 | 11 | const place = world.places[world.currentPlace], 12 | group = world.playerGroup; 13 | 14 | if (place.explored.get()) { 15 | return template.success({ 16 | place: place.summary.get(), 17 | doors: place.summary.doors.get(), 18 | speed: group.info.average('speed') 19 | }); 20 | } 21 | return template.fail({ 22 | place: place.summary.get() 23 | }); 24 | 25 | } else { 26 | return [ `${player} is not a registered player.` ]; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/commands/drop.js: -------------------------------------------------------------------------------- 1 | 2 | import {itemTransfer} from '../actions'; 3 | import * as template from './templates/drop'; 4 | 5 | export const cmdRegExp = /^(drop) (\S[ \S]*\S) (>) (\S[ \S]*\S)$/; 6 | 7 | export const run = (player, command, world) => { 8 | 9 | if (!world.currentPlace) { 10 | return [ `world is not initialized .. yet.` ]; 11 | } else if (world.getPlayers(player).length > 0) { 12 | 13 | const matches = cmdRegExp.exec(command), 14 | activePlayer = world.getPlayers(player)[0], 15 | place = world.places[world.currentPlace], 16 | to = matches[4], 17 | item = matches[2]; 18 | 19 | const foundBox = place.boxes.list('name', to); 20 | 21 | if(!foundBox.length) { 22 | 23 | return template.fail({ 24 | toName: to, 25 | place: place.summary.get(), 26 | from: activePlayer.summary.get() 27 | }, 'box unavailable'); 28 | } 29 | 30 | const foundItem = activePlayer.items.list('name', item); 31 | 32 | if(!foundItem.length) { 33 | 34 | return template.fail({ 35 | from: activePlayer.summary.get(), 36 | to: foundBox[0].summary.get(), 37 | itemName: item 38 | }, 'item unavailable'); 39 | } 40 | 41 | const itemDroped = itemTransfer(activePlayer, foundBox[0], foundItem[0].id.get()); 42 | 43 | if(itemDroped.success) { 44 | 45 | return template.success({ 46 | from: activePlayer.summary.get(), 47 | to: foundBox[0].summary.get(), 48 | item: foundItem[0].summary.get(), 49 | place: place 50 | }); 51 | } 52 | 53 | return template.fail({ 54 | from: activePlayer.summary.get(), 55 | to: foundBox[0].summary.get(), 56 | item: foundItem[0].summary.get(), 57 | place: place 58 | }, itemDroped.error); 59 | 60 | } else { 61 | return [ `${player} is not a registered player.` ]; 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/commands/explore.js: -------------------------------------------------------------------------------- 1 | 2 | import * as template from './templates/where'; 3 | 4 | export const cmdRegExp = /^(explore)$/; 5 | 6 | export const run = (player, command, world) => { 7 | if (!world.currentPlace) { 8 | return [ `world is not initialized .. yet.` ]; 9 | } else if (world.getPlayers(player).length > 0) { 10 | 11 | const activePlayer = world.getPlayers(player)[0], 12 | place = world.places[world.currentPlace]; 13 | 14 | place.explored.set(true); 15 | 16 | return template.success({ 17 | player: activePlayer.summary.get(), 18 | weapons: activePlayer.summary.items.short(), 19 | group: world.playerGroup.summary.get(), 20 | place: world.places[world.currentPlace].summary.get() 21 | }); 22 | 23 | //return [ world.places[world.currentPlace].summary.get() ]; // this should never fail 24 | } else { 25 | return [ `${player} is not a registered player.` ]; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/commands/group.js: -------------------------------------------------------------------------------- 1 | 2 | import * as Group from '../group'; 3 | 4 | export const cmdRegExp = /^(group|grp|g) *(\S[ \S]+\S){0,1}$/; 5 | 6 | export const run = (player, command, world) => { 7 | if (world.playerGroup) { 8 | return [ `group already exists! Named "${world.playerGroup.name.get()}".` ]; 9 | } 10 | 11 | let matches = cmdRegExp.exec(command), 12 | group = matches[2] ? matches[2].trim() : null; 13 | if (!group) // no group-name given 14 | return [ `name your group, ${player}!` ]; 15 | world.playerGroup = Group.createGroup(group); 16 | return [ `${player} started the group "${group}"! Let's see what adventure may lie upon you...` ]; 17 | }; 18 | -------------------------------------------------------------------------------- /src/commands/help.js: -------------------------------------------------------------------------------- 1 | 2 | export const cmdRegExp = /^(help|h)$/; 3 | 4 | export const run = (player, command, world) => { 5 | return [ '>>> usage:', // TODO: *prefix* for all commands is `...` ? 6 | '1. `group `', 7 | '2. `character `', 8 | '3. `start`', 9 | 'from there on: use `where`, `inventory`, `pickup`, `move`, `stop`' ]; 10 | }; 11 | -------------------------------------------------------------------------------- /src/commands/inventory.js: -------------------------------------------------------------------------------- 1 | 2 | import * as template from './templates/inventory'; 3 | 4 | export const cmdRegExp = /^(inventory|inv|i) *(\S[ \S]+\S){0,1}$/; 5 | 6 | export const run = (player, command, world) => { 7 | if (world.getPlayers(player).length > 0) { 8 | let players = world.getPlayers(player), 9 | output = []; 10 | 11 | 12 | 13 | 14 | players.forEach((e) => { 15 | output.push(player); 16 | output.push(e.summary.items.get()); 17 | }); 18 | return output; // this should never fail 19 | } else { 20 | return [ `${player} is not a registered player.` ]; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/commands/leave.js: -------------------------------------------------------------------------------- 1 | 2 | import {leave} from '../actions'; 3 | import {randomInt} from 'random-tools'; 4 | import * as template from './templates/leave'; 5 | 6 | export const cmdRegExp = /^(leave|l) (\S[ \S]+\S)+$/; 7 | 8 | export const run = (player, command, world) => { 9 | 10 | if (!world.currentPlace) { 11 | return [ `world is not initialized .. yet.` ]; 12 | } else if (world.getPlayers(player).length > 0) { 13 | 14 | const matches = cmdRegExp.exec(command), 15 | place = world.places[world.currentPlace], 16 | group = world.playerGroup, 17 | groupSum = group.summary.get(), 18 | name = matches[2].trim(), 19 | doors = place.doors.list('name', name); 20 | 21 | if (!doors.length) { 22 | return `${player} could not find ${name}.`; 23 | } 24 | 25 | let door = doors[0]; 26 | 27 | if(!door.open.get()) { 28 | return `${name} ist not open yet.`; 29 | } 30 | 31 | const /*interruptions = template.interrupt({ 32 | group: groupSum, 33 | member: groupSum.members[Math.floor(Math.random() * groupSum.members.length)].name 34 | }, Math.ceil(Math.random() * 5)),*/ 35 | foundPlace = door.path.get() 36 | .places.list() 37 | .filter(x => x.name.get() !== place.name.get()); 38 | 39 | let speed = group.info.average('speed'), 40 | delay = (door.path.get().distance.get() / speed) * 60 * 60 * 1000; 41 | 42 | //console.log(JSON.stringify(interruptions , null, 2)); 43 | 44 | return [ 45 | { action: 'disable' }, 46 | { action: 'message', delay: 500, text: template.start({ 47 | group: group.summary.get(), 48 | place: place.summary.get() 49 | }) }, 50 | { action: 'delay', 51 | delay: delay, 52 | callback: () => { 53 | leave({ 54 | world, 55 | group, 56 | place, 57 | door 58 | }); 59 | } 60 | }, 61 | { action: 'message', delay: delay + 500, text: template.success({ 62 | group: group.summary.get(), 63 | place: foundPlace[0].summary.get() 64 | }) }, 65 | { action: 'enable', delay: delay + 500 } 66 | ]; 67 | 68 | } else { 69 | return [ `${player} is not a registered player.` ]; 70 | } 71 | }; -------------------------------------------------------------------------------- /src/commands/move.js: -------------------------------------------------------------------------------- 1 | 2 | export const cmdRegExp = /^(move|move to|m) *(east|west|north|south|left|right|up|down){0,1}$/; 3 | 4 | export const run = (player, command, world) => { 5 | return [ `${player} moves (like Jagger?) *TODO*` ]; 6 | }; 7 | -------------------------------------------------------------------------------- /src/commands/open.js: -------------------------------------------------------------------------------- 1 | 2 | import {openBox, openDoor} from '../actions'; 3 | import * as templateBox from './templates/openBox'; 4 | import * as templateDoor from './templates/openDoor'; 5 | 6 | export const cmdRegExp = /^(open|o) (\S[ \S]+\S)+$/; 7 | 8 | export const run = (player, command, world) => { 9 | 10 | if (!world.currentPlace) { 11 | return [ `world is not initialized .. yet.` ]; 12 | } else if (world.getPlayers(player).length > 0) { 13 | 14 | const matches = cmdRegExp.exec(command), 15 | place = world.places[world.currentPlace], 16 | activePlayer = world.getPlayers(player)[0], 17 | name = matches[2].trim(), 18 | group = world.playerGroup, 19 | foundBox = place.boxes.list('name', name), 20 | foundDoor = place.doors.list('name', name); 21 | 22 | if(foundBox.length) { 23 | return openBoxAction({ 24 | box: foundBox[0], 25 | activePlayer, 26 | place, 27 | name 28 | }); 29 | } 30 | 31 | if(foundDoor.length) { 32 | return openDoorAction({ 33 | door: foundDoor[0], 34 | activePlayer, 35 | place, 36 | name, 37 | group 38 | }); 39 | } 40 | 41 | return templateBox.fail({ 42 | player: activePlayer.summary.get(), 43 | boxName: name, 44 | place: place.summary.get() 45 | }, 'availability') 46 | + '\n' + 47 | templateDoor.fail({ 48 | player: activePlayer.summary.get(), 49 | doorName: name, 50 | place: place.summary.get() 51 | }, 'availability'); 52 | 53 | } else { 54 | return [ `${player} is not a registered player.` ]; 55 | } 56 | }; 57 | 58 | 59 | function openBoxAction({box, activePlayer, place, name}) { 60 | 61 | const boxOpened = openBox(box, activePlayer); 62 | 63 | if(boxOpened.success) { 64 | return templateBox.success({ 65 | player: activePlayer.summary.get(), 66 | box: box.summary.short(), 67 | items: box.summary.items.get() 68 | }); 69 | } 70 | return templateBox.fail({ 71 | player: activePlayer.summary.get(), 72 | boxName: name, 73 | place: place.summary.get() 74 | }, boxOpened.error); 75 | } 76 | 77 | function openDoorAction({door, activePlayer, place, name, group}) { 78 | 79 | const doorOpened = openDoor({ 80 | door, 81 | player: activePlayer, 82 | place 83 | }); 84 | 85 | if(doorOpened.success) { 86 | 87 | const speed = group.info.average('speed'), 88 | time = Math.round((door.path.get().distance.get() / speed) * 60); 89 | 90 | return templateDoor.success({ 91 | player: activePlayer.summary.get(), 92 | door: door.summary.get(), 93 | time: time, 94 | place: place.summary.get() 95 | }); 96 | } 97 | return templateDoor.fail({ 98 | player: activePlayer.summary.get(), 99 | doorName: name, 100 | place: place.summary.get() 101 | }, doorOpened.error); 102 | } 103 | -------------------------------------------------------------------------------- /src/commands/pickup.js: -------------------------------------------------------------------------------- 1 | 2 | import {itemTransfer} from '../actions'; 3 | import * as template from './templates/pickup'; 4 | 5 | export const cmdRegExp = /^(pick|pickup|pick up) (\S[ \S]*\S) (>) (\S[ \S]*\S)$/; 6 | 7 | export const run = (player, command, world) => { 8 | 9 | if (!world.currentPlace) { 10 | return [ `world is not initialized .. yet.` ]; 11 | } else if (world.getPlayers(player).length > 0) { 12 | 13 | const matches = cmdRegExp.exec(command), 14 | activePlayer = world.getPlayers(player)[0], 15 | place = world.places[world.currentPlace], 16 | from = matches[2], 17 | item = matches[4]; 18 | 19 | const foundBox = place.boxes.list('name', from); 20 | 21 | if(!foundBox.length) { 22 | 23 | return template.fail({ 24 | fromName: from, 25 | place: place.summary.get(), 26 | to: activePlayer.summary.get() 27 | }, 'box unavailable'); 28 | } 29 | 30 | const foundItem = foundBox[0].items.list('name', item); 31 | 32 | if(!foundItem.length) { 33 | 34 | return template.fail({ 35 | from: foundBox[0].summary.get(), 36 | to: activePlayer.summary.get(), 37 | itemName: item 38 | }, 'item unavailable'); 39 | } 40 | 41 | const itemPicked = itemTransfer(foundBox[0], activePlayer, foundItem[0].id.get()); 42 | 43 | 44 | if(itemPicked.success) { 45 | 46 | return template.success({ 47 | from: foundBox[0].summary.get(), 48 | to: activePlayer.summary.get(), 49 | item: foundItem[0].summary.get(), 50 | place: place 51 | }); 52 | } 53 | 54 | return template.fail({ 55 | from: foundBox[0].summary.get(), 56 | to: activePlayer.summary.get(), 57 | item: foundItem[0].summary.get(), 58 | place: place 59 | }, itemPicked.error); 60 | 61 | } else { 62 | return [ `${player} is not a registered player.` ]; 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /src/commands/quit.js: -------------------------------------------------------------------------------- 1 | 2 | export const cmdRegExp = /^(exit|quit|q)$/; 3 | 4 | export const run = (player, command, world) => { 5 | setTimeout(() => { process.exit(0); }, 500); //TODO: save the database and wait for a clean exit (do not exit the whole process :-/) 6 | return [ { action : 'quit' } ]; 7 | }; 8 | -------------------------------------------------------------------------------- /src/commands/start.js: -------------------------------------------------------------------------------- 1 | 2 | export const cmdRegExp = /^(start)$/; 3 | 4 | export const run = (player, command, world) => { 5 | if (!world.playerGroup) { 6 | return [ `no group exists!` ]; 7 | } else if (world.getPlayers().length === 0) { 8 | return [ `no character in group!` ]; 9 | } 10 | world.init(); 11 | return [ `Good luck, have fun...` ]; 12 | }; 13 | -------------------------------------------------------------------------------- /src/commands/stop.js: -------------------------------------------------------------------------------- 1 | 2 | export const cmdRegExp = /^(stop)$/; 3 | 4 | export const run = (player, command, world) => { 5 | if (!world.playerGroup) { 6 | return [ `no group started .. yet.` ]; 7 | } else if (world.getPlayers(player).length > 0) { 8 | world.reset(); 9 | return [ `${player} stopped the game.` ]; // this should never fail 10 | } else { 11 | return [ `${player} is not a registered player.` ]; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/commands/templates/attack.js: -------------------------------------------------------------------------------- 1 | 2 | import {getConfig} from 'helptos'; 3 | 4 | const symbols = getConfig('../../../config/symbols.json', __dirname); 5 | 6 | export function success(data) { 7 | 8 | let constent = `${data.attacker.name} attacks ${data.defender.name} with ${data.weapon.name} 9 | ${symbols.presentational.redDown}${data.power - data.defense} ${data.defender.name} ${symbols.propertie.health}${data.defender.health}`; 10 | 11 | return constent; 12 | } 13 | 14 | export function fail(data, type) { 15 | 16 | switch(type) { 17 | case 'not found': 18 | return `${data.playerName} could not find ${data.weaponName}.`; 19 | case 'not found creature': 20 | return `${data.playerName} could not find ${data.creatureName} in ${data.placeName}.`; 21 | case 'defended': 22 | return `${data.defender.name} defended the attack. `; 23 | default: 24 | return `FEHL -,-`; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/templates/boxes.js: -------------------------------------------------------------------------------- 1 | import {getConfig} from 'helptos'; 2 | 3 | const symbols = getConfig('../../../config/symbols.json', __dirname); 4 | 5 | export function success(data) { 6 | 7 | const boxList = data.boxes.map(box => { 8 | let openState = symbols.state.close, 9 | content = ''; 10 | if(box.open) { 11 | openState = symbols.state.open; 12 | content = '\n ' + box.items.map(item => { 13 | return symbols.itemType[item.type]; 14 | }).join(''); 15 | } 16 | return `${openState} "${box.name}"${content}`; 17 | }).join('\n\n'); 18 | 19 | return `Boxes in ${data.place.name}:\n\n${boxList}`; 20 | } 21 | 22 | export function fail(data) { 23 | return `${data.place.name} is not explored yet.\n(Use "where are we" to explore)`; 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/templates/character.js: -------------------------------------------------------------------------------- 1 | 2 | import {getConfig} from 'helptos'; 3 | 4 | const symbols = getConfig('../../../config/symbols.json', __dirname); 5 | 6 | export function success(data) { 7 | return `Character ${symbols.race[data.character.type]} ${data.character.name} joined ${data.group.name}, may glory be with you!`; 8 | } 9 | 10 | export function fail(data) { 11 | 12 | const characters = data.characters.map(character => { 13 | return `${symbols.race[character.type]} "${character.type}"\n ${symbols.propertie.slots}${character.slots} ${symbols.propertie.attack}${character.attack} ${symbols.propertie.defense}${character.defense} ${symbols.propertie.dexterity}${character.dexterity} ${symbols.propertie.speed}${character.speed}`; 14 | }).join('\n\n'); 15 | 16 | return `${data.playerName}, choose a race:\n\n${characters}`; 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/templates/creatures.js: -------------------------------------------------------------------------------- 1 | import {getConfig} from 'helptos'; 2 | 3 | const symbols = getConfig('../../../config/symbols.json', __dirname); 4 | 5 | export function success(data) { 6 | 7 | const creatureList = data.creatures.map(creature => { 8 | let dead = '', 9 | content = ''; 10 | 11 | if (creature.dead) { 12 | dead = `${symbols.state.dead} `; 13 | 14 | if (creature.weapon.length) { 15 | const weapons = creature.weapon.map(weapon => `${symbols.itemType.weapon} ${weapon.name} ${symbols.propertie.rank}${weapon.rank} ${symbols.propertie.attack}${weapon.attack}`); 16 | content += `${weapons.join('\n')}\n`; 17 | } 18 | if (creature.armor.length) { 19 | const armors = creature.armor.map(armor => `${symbols.itemType.armor} ${armor.name} ${symbols.propertie.rank}${armor.rank} ${symbols.propertie.defense}${armor.defense}`); 20 | content += `${armors.join('\n')}\n`; 21 | } 22 | if (creature.consumable.length) { 23 | const consumables = creature.consumable.map(consumable => `${symbols.itemType.consumable} ${consumable.name} ${symbols.propertie.rank}${consumable.rank}`); 24 | content += `${consumables.join('\n')}\n`; 25 | } 26 | } else { 27 | content += `${symbols.propertie.rank} ${creature.rank} ${symbols.propertie.health} ${creature.health} ${symbols.propertie.attack} ${creature.attack} ${symbols.propertie.defense} ${creature.defense}\n`; 28 | } 29 | 30 | return `${dead}${symbols.race[creature.type]} ${creature.name} \n${content}`; 31 | }).join('\n'); 32 | 33 | return `Creatures in ${data.place.name}:\n\n${creatureList}\n`; 34 | } 35 | 36 | export function fail(data) { 37 | return `${data.place.name} is not explored yet.\n(Use "where are we" to explore)`; 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/templates/doors.js: -------------------------------------------------------------------------------- 1 | import {getConfig} from 'helptos'; 2 | 3 | const symbols = getConfig('../../../config/symbols.json', __dirname); 4 | 5 | export function success(data) { 6 | 7 | const doorList = data.doors.map(door => { 8 | let openState = symbols.state.close, 9 | content = ''; 10 | if(door.open) { 11 | const pathTo = door.path.places.filter(x => x !== data.place.name)[0]; 12 | const time = Math.round((door.path.distance / data.speed) * 60); 13 | openState = symbols.state.open; 14 | content = `\n ${door.path.name} > ${pathTo}\n ${symbols.state.walk} ${door.path.distance}km\n ${symbols.presentational.clock} ${time}min`; 15 | } 16 | return `${openState} "${door.name}"${content}`; 17 | }).join('\n\n'); 18 | 19 | return `Doors in ${data.place.name}:\n\n${doorList}`; 20 | } 21 | 22 | export function fail(data) { 23 | return `${data.place.name} is not explored yet.\n(Use "where are we" to explore)`; 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/templates/drop.js: -------------------------------------------------------------------------------- 1 | 2 | import {getConfig} from 'helptos'; 3 | 4 | const symbols = getConfig('../../../config/symbols.json', __dirname); 5 | 6 | export function success(data) { 7 | return `${data.from.name} dropped ${data.item.name} to ${data.to.name}`; 8 | } 9 | 10 | export function fail(data, type) { 11 | 12 | switch(type) { 13 | case 'box unavailable': 14 | return `${data.from.name} could not find ${data.toName} at ${data.place.name}.`; 15 | case 'item unavailable': 16 | return `${data.from.name} don't have ${data.itemName} in the inventory.`; 17 | case 'from open': 18 | return `${data.from.name} is not open yet.`; 19 | case 'to open': 20 | return `${data.to.name} is not open yet.`; 21 | case 'rank': 22 | return `The rank of ${data.to.name} (${symbols.propertie.rank}${data.to.rank}) is not high enough to drop ${data.item.name} (${symbols.propertie.rank}${data.item.rank})`; 23 | case 'slots': 24 | return `${data.to.name} (${symbols.propertie.slots}${data.to.slots}) has not enough slots free to drop ${data.item.name} (${symbols.propertie.slots}${data.to.slots})`; 25 | default: 26 | return `${data.from.name} can not drop ${data.item.name} into ${data.to.name}`; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/commands/templates/inventory.js: -------------------------------------------------------------------------------- 1 | 2 | import {getConfig} from 'helptos'; 3 | 4 | const symbols = getConfig('../../../config/symbols.json', __dirname); 5 | 6 | export function success(data) { 7 | 8 | } 9 | 10 | export function fail(data, type) { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/commands/templates/leave.js: -------------------------------------------------------------------------------- 1 | import {getConfig} from 'helptos'; 2 | 3 | const symbols = getConfig('../../../config/symbols.json', __dirname); 4 | 5 | function interruptionMsg(data) { 6 | 7 | return [ 8 | `` 9 | ] 10 | }; 11 | 12 | export function start(data) { 13 | return `${data.group.name} leaves the ${data.place.name}.`; 14 | } 15 | 16 | export function success(data) { 17 | return `${data.group.name} arrived in a ${data.place.name}. Let's see what we will find in here.`; 18 | } 19 | 20 | export function interrupt(data, count) { 21 | 22 | if (!count) { 23 | return `${data.member}`; 24 | } 25 | 26 | let messages = []; 27 | 28 | for (let i = 0; i < count; i++) { 29 | const member = groupSum.members[Math.floor(Math.random() * groupSum.members.length)].name 30 | 31 | 32 | } 33 | 34 | 35 | return messages[0]; 36 | } 37 | -------------------------------------------------------------------------------- /src/commands/templates/openBox.js: -------------------------------------------------------------------------------- 1 | 2 | import {getConfig} from 'helptos'; 3 | 4 | const symbols = getConfig('../../../config/symbols.json', __dirname); 5 | 6 | export function success(data) { 7 | const pre = `${data.player.name} opened ${data.box.name}. It contains:\n\n`, 8 | content = data.items.map(item => { 9 | const base = `${symbols.itemType[item.type]} "${item.name}" ${symbols.propertie.rank}${item.rank} ${symbols.propertie.slots}${item.slots}\n ${symbols.propertie.attack}${item.attack} ${symbols.propertie.defense}${item.defense} ${symbols.propertie.dexterity}${item.dexterity}`; 10 | let consume = ''; 11 | 12 | if (item.type === 'consumable') { 13 | consume = ` ${symbols.propertie.health}${item.health} ${symbols.propertie.time}${item.time}sec`; 14 | } 15 | 16 | return `${base}${consume}`; 17 | }).join('\n\n'); 18 | 19 | return `${pre}${content}`; 20 | } 21 | 22 | export function fail(data, type) { 23 | 24 | switch(type) { 25 | case 'availability': 26 | return `${data.player.name} could not find a box called "${data.boxName}" at ${data.place.name}.`; 27 | case 'dexterity': 28 | return `${data.player.name} is not dexterous enough to open ${data.boxName}.`; 29 | default: 30 | return `${data.player.name} can't open ${data.boxName}`; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/templates/openDoor.js: -------------------------------------------------------------------------------- 1 | 2 | import {getConfig} from 'helptos'; 3 | 4 | const symbols = getConfig('../../../config/symbols.json', __dirname); 5 | 6 | export function success(data) { 7 | 8 | const pathTo = data.door.path.places.filter(x => x !== data.place.name)[0]; 9 | 10 | return `${data.player.name} opened the ${data.door.name} and sees:\nA ${data.door.path.distance}km long ${data.door.path.name} to a ${pathTo}.\nThe jurney will take ${data.time} Minutes.`; 11 | } 12 | 13 | export function fail(data, type) { 14 | 15 | switch(type) { 16 | case 'availability': 17 | return `${data.player.name} could not find a door called "${data.doorName}" at ${data.place.name}.`; 18 | case 'dexterity': 19 | return `${data.player.name} is not dexterous enough to open ${data.doorName}.`; 20 | default: 21 | return `${data.player.name} can't open ${data.doorName}`; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/commands/templates/pickup.js: -------------------------------------------------------------------------------- 1 | 2 | import {getConfig} from 'helptos'; 3 | 4 | const symbols = getConfig('../../../config/symbols.json', __dirname); 5 | 6 | export function success(data) { 7 | return `${data.to.name} picked up ${data.item.name} from ${data.from.name}` 8 | } 9 | 10 | export function fail(data, type) { 11 | 12 | switch(type) { 13 | case 'box unavailable': 14 | return `${data.to.name} could not find ${data.fromName} at ${data.place.name}.`; 15 | case 'item unavailable': 16 | return `${data.to.name} could not find ${data.itemName} in ${data.from.name}.`; 17 | case 'from open': 18 | return `${data.from.name} is not open yet.`; 19 | case 'to open': 20 | return `${data.to.name} is not open yet.`; 21 | case 'rank': 22 | return `The rank of ${data.to.name} (${symbols.propertie.rank}${data.to.rank}) is not high enough to pick up ${data.item.name} (${symbols.propertie.rank}${data.item.rank})`; 23 | case 'slots': 24 | return `${data.to.name} (${symbols.propertie.slots}${data.to.slots}) has not enough slots free to pick up ${data.item.name} (${symbols.propertie.slots}${data.item.slots})`; 25 | default: 26 | return `${data.to.name} can not pick up ${data.item.name} from ${data.from.name}`; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/commands/templates/where.js: -------------------------------------------------------------------------------- 1 | 2 | export function success(data) { 3 | 4 | const numGroups = data.place.groups.length, 5 | numBoxes = data.place.boxes.length, 6 | numDoors = data.place.doors.length, 7 | weapons = data.weapons.filter(x => x.type === 'weapon'); 8 | 9 | // default texts 10 | let textWeapon = `only armed with fists and a threatening expression in the face.`, 11 | textGroups = `we are alone in this place. You can pack away your weapons and take a deep breath. It is save in here.`, 12 | textBoxes = `There is nothing to find in here. No chests, boxes or anything else.`, 13 | textDoors = ``, 14 | textCreatures = `CREATURES: ${data.place.creatures.map(c => c.name).join(', ')}`; 15 | 16 | 17 | 18 | if(weapons.length) { 19 | textWeapon = `ready to defend the group with the ${weapons[0].name}.`; 20 | } 21 | 22 | if(numGroups === 2) { 23 | textGroups = `be quiet now. I see one other group in the back of ${data.place.name}. Keep your weapons ready. We might need to fight.`; 24 | } 25 | else if(numGroups > 2){ 26 | textGroups = `be quiet now. I see ${numGroups} other group in here. Keep your weapons ready. We might need to fight.`; 27 | } 28 | 29 | if(numBoxes === 1) { 30 | textBoxes = `I've found a ${data.place.boxes[0].name}. Maybe we find something useful in it.` 31 | } 32 | else if(numBoxes > 1) { 33 | const boxList = data.place.boxes.reduce((list, item) => `${list} a ${item.name},`, '').slice(0, -1).trim(); 34 | textBoxes = `I've found:\n${boxList}.\nMaybe we find something useful in there.` 35 | } 36 | 37 | if(numDoors === 1) { 38 | textDoors = `There is one way out of this place. You see that ${data.place.doors[0].name}?`; 39 | } 40 | else if(numDoors > 1) { 41 | const doorList = data.place.doors.reduce((list, item) => `${list} a ${item.name},`, '').slice(0, -1).trim(); 42 | textDoors = `There are several ways out of this place:\n${doorList}.`; 43 | } 44 | 45 | let textIntro = `${data.player.name} slowly inspects the ${data.place.name}. Sneaking around and looking in all corners of this place, ${textWeapon} 46 | After a few moments ${data.player.name} turns to ${data.group.name}, and says: "Fellows, ${textGroups}`; 47 | 48 | return [ 49 | { action:'disable' }, 50 | { action:'message', delay:500, text:`${textIntro}` }, 51 | { action:'message', delay:1500, text: `${textCreatures}`}, 52 | { action:'message', delay:2000, text:`${textBoxes}` }, 53 | { action:'message', delay:4000, text:`${textDoors}` }, 54 | { action:'enable', delay:4200 } 55 | ]; 56 | } -------------------------------------------------------------------------------- /src/creature.js: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import {getConfig, copyObject, getFirstByName} from 'helptos'; 3 | import {randomInt} from 'random-tools'; 4 | import {createName} from './name'; 5 | import * as properties from './properties'; 6 | import * as race from './race'; 7 | import * as item from './item'; 8 | import * as actions from './actions'; 9 | 10 | function createItems(creature) { 11 | 12 | const summary = creature.summary.get(), 13 | weaponsCount = randomInt(2, 1), 14 | armorCount = randomInt(2, 0), 15 | consumableCount = randomInt(2, 0); 16 | 17 | let items = [], 18 | i; 19 | 20 | for (i = 0; i < weaponsCount; i++) { 21 | 22 | items.push(item.createWeapon({ 23 | rank: summary.rank, 24 | slots: 6, 25 | attack: summary.attack, 26 | defense: 0, 27 | dexterity: summary.dexterity, 28 | speed: -5 29 | })); 30 | } 31 | 32 | for (i = 0; i < armorCount; i++) { 33 | 34 | items.push(item.createArmor({ 35 | rank: summary.rank, 36 | slots: 4, 37 | attack: 2, 38 | defense: summary.defense, 39 | dexterity: summary.dexterity, 40 | speed: -3 41 | })); 42 | } 43 | 44 | for (i = 0; i < consumableCount; i++) { 45 | 46 | items.push(item.createConsumable({ 47 | rank: summary.rank, 48 | slots: 2, 49 | attack: summary.attack, 50 | defense: summary.defense, 51 | dexterity: summary.dexterity, 52 | speed: summary.speed, 53 | health: summary.health 54 | })); 55 | } 56 | 57 | return items; 58 | } 59 | 60 | const config = getConfig('../config/creature.json', __dirname); 61 | const creatureNames = getConfig('../config/names/creature-names.json', __dirname); 62 | 63 | 64 | const name = (state) => Object.assign({}, properties.mixed('name', state)), 65 | type = (state) => Object.assign({}, properties.fixed('type', state)), 66 | health = (state) => Object.assign({}, properties.numericalPositive('health', state.properties, state)), 67 | rank = (state) => Object.assign({}, properties.numericalPositive('rank', state.properties, state)), 68 | attack = (state) => Object.assign({}, properties.numerical('attack', state.properties, state)), 69 | defense = (state) => Object.assign({}, properties.numerical('defense', state.properties, state)), 70 | aggression = (state) => Object.assign({}, properties.numericalPositive('aggression', state.properties, state)); 71 | 72 | /** 73 | * anything you can do with inventory items 74 | * 75 | * @param state 76 | */ 77 | const items = (state) => Object.assign({}, 78 | // get default list functionality 79 | properties.list( 80 | 'items', 81 | state, 82 | state, 83 | [ 84 | item.types.WEAPON, 85 | item.types.ARMOR, 86 | item.types.CONSUMABLE 87 | ] 88 | ) 89 | ); 90 | 91 | const summary = state => ({ 92 | 93 | get: () => { 94 | 95 | const creature = state.element; 96 | 97 | return { 98 | dead: isDead(creature), 99 | name: creature.name.get(), 100 | type: creature.type.get(), 101 | rank: creature.rank.get(), 102 | health: creature.health.get(), 103 | attack: creature.attack.get(), 104 | defense: creature.defense.get(), 105 | aggression: creature.aggression.get() 106 | }; 107 | }, 108 | short: () => { 109 | 110 | const creature = state.element; 111 | 112 | return { 113 | dead: isDead(creature), 114 | name: creature.name.get(), 115 | type: creature.type.get(), 116 | rank: creature.rank.get() 117 | }; 118 | }, 119 | long: () => { 120 | 121 | const creature = state.element; 122 | 123 | return { 124 | dead: isDead(creature), 125 | name: creature.name.get(), 126 | type: creature.type.get(), 127 | rank: creature.rank.get(), 128 | health: creature.health.get(), 129 | attack: creature.attack.get(), 130 | defense: creature.defense.get(), 131 | aggression: creature.aggression.get(), 132 | weapon: creature.items.listWeapon().map(item => item.summary.get()), 133 | armor: creature.items.listArmor().map(item => item.summary.get()), 134 | consumable: creature.items.listConsumable().map(item => item.summary.get()) 135 | }; 136 | }, 137 | items: { 138 | get: () => { 139 | 140 | const creature = state.element; 141 | 142 | return creature.items.list().map(item => item.summary.get()); 143 | }, 144 | short: () => { 145 | 146 | const creature = state.element; 147 | 148 | return creature.items.list().map(item => item.summary.short()); 149 | } 150 | } 151 | 152 | }); 153 | 154 | const newCreature = (creatureName, creatureRace) => { 155 | 156 | let state = copyObject(config); 157 | const raceState = race.getRace(creatureRace); 158 | 159 | state.name = creatureName; 160 | state.type = raceState.type; 161 | state.properties = Object.assign(state.properties, raceState.properties); 162 | 163 | state.element = { 164 | name: name(state), 165 | type: type(state), 166 | health: health(state), 167 | rank: rank(state), 168 | attack: attack(state), 169 | defense: defense(state), 170 | aggression: aggression(state), 171 | items: items(state), 172 | summary: summary(state), 173 | event: new EventEmitter() 174 | }; 175 | 176 | return state.element; 177 | }; 178 | 179 | export const createCreature = (average) => { 180 | 181 | const races = race.getRaces(), 182 | creatureName = createName(creatureNames), 183 | creatureRace = races[randomInt(races.length - 1, 0)], 184 | creature = newCreature(creatureName, creatureRace); 185 | 186 | creature.health.up(randomInt(30, -40)); 187 | creature.rank.up(randomInt(average.rank + 2, average.rank - 3)); 188 | creature.aggression.up(randomInt(100, 0)); 189 | creature.items.add(createItems(creature)); 190 | 191 | return creature; 192 | }; 193 | 194 | export const createCreatures = ({average}, numberOfCreatures) => { 195 | 196 | const creatures = []; 197 | 198 | for (let i = 0; i < numberOfCreatures; i++) { 199 | creatures.push(createCreature(average)); 200 | } 201 | 202 | return creatures; 203 | }; 204 | 205 | export const activate = (creature, group) => { 206 | 207 | console.log('aggr', creature.aggression.get()); 208 | console.log('group', group.name.get()); 209 | 210 | if (creature.aggression.get() < 50) { 211 | return; 212 | } 213 | 214 | const players = group.members.list(); 215 | console.log('group', group.members.list().map(x => x.name.get())); 216 | const activePlayer = group.members.list()[Math.floor(Math.random() * players.length)]; 217 | console.log('active player', activePlayer.name.get()); 218 | const delay = (activePlayer.dexterity.get() / 100) 219 | * (10 * (100 - creature.aggression.get())); 220 | 221 | console.log('delay', delay); 222 | 223 | setTimeout(() => { 224 | fight(creature, activePlayer); 225 | }, delay * 1000); 226 | 227 | // console.log('activePlayer', activePlayer.name.get()); 228 | 229 | return creature; 230 | }; 231 | 232 | export const fight = (creature, activePlayer) => { 233 | console.log('attack -> ', creature.name.get(), '->', activePlayer.name.get()); 234 | }; 235 | 236 | export const activateMap = group => creature => activate(creature, group); 237 | export const isAggrassive = creature => creature.aggression.get() > 50; 238 | export const isDead = creature => creature.health.get() <= 0; 239 | -------------------------------------------------------------------------------- /src/door.js: -------------------------------------------------------------------------------- 1 | import {getConfig, copyObject} from 'helptos'; 2 | import {randomInt} from 'random-tools'; 3 | import * as properties from './properties'; 4 | import {createName} from './name'; 5 | 6 | const config = getConfig('../config/door.json', __dirname); 7 | const doorNames = getConfig('../config/names/door-names.json', __dirname); 8 | 9 | const name = state => Object.assign({}, properties.mixed('name', state)); 10 | const open = state => Object.assign({}, properties.boolean('open', state)); 11 | const path = state => Object.assign({}, properties.mixed('path', state)); 12 | 13 | const summary = state => ({ 14 | 15 | get: () => { 16 | const door = state.element; 17 | let path = door.path.get(); 18 | 19 | if (path) { 20 | path = door.path.get().summary.get(); 21 | } 22 | 23 | return { 24 | name: door.name.get(), 25 | open: door.open.get(), 26 | path: path 27 | }; 28 | }, 29 | short: () => { 30 | const door = state.element; 31 | 32 | return { 33 | name: door.name.get(), 34 | open: door.open.get() 35 | }; 36 | } 37 | }); 38 | 39 | const newDoor = (doorName) => { 40 | 41 | let state = copyObject(config); 42 | 43 | state.name = doorName; 44 | 45 | state.element = { 46 | name: name(state), 47 | open: open(state), 48 | path: path(state), 49 | summary: summary(state) 50 | }; 51 | 52 | return state.element; 53 | }; 54 | 55 | export const createDoor = ({name = createName(doorNames)}) => { 56 | 57 | let door = newDoor(name); 58 | 59 | return door; 60 | }; 61 | 62 | export const createDoors = (options, count) => { 63 | 64 | let i = 0, 65 | doors = []; 66 | 67 | for (i; i < count; i++) { 68 | doors.push(createDoor(options)); 69 | } 70 | 71 | return doors; 72 | }; 73 | -------------------------------------------------------------------------------- /src/group.js: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import {getConfig, copyObject, getFirstByType} from 'helptos'; 3 | import * as properties from './properties'; 4 | import * as uuid from './uuid'; 5 | 6 | const config = getConfig('../config/group.json', __dirname); 7 | const playerPropertyConfig = getConfig('../config/player.json', __dirname).properties; 8 | 9 | 10 | const getPropertiesFromAllMembers = (members, baseProperties) => { 11 | 12 | return members.reduce((output, member) => { 13 | 14 | output = Object.keys(output).reduce((out, key) => { 15 | 16 | // for the first member create array for each property 17 | if(!(out[key] instanceof Array)){ 18 | out[key] = []; 19 | } 20 | 21 | out[key].push(member[key].get()); 22 | return out; 23 | }, output); 24 | 25 | return output; 26 | 27 | }, baseProperties); 28 | }; 29 | 30 | const name = state => Object.assign({}, properties.mixed('name', state)); 31 | const id = state => Object.assign({}, properties.fixed('id', state)); 32 | 33 | const members = state => Object.assign({}, 34 | // get default list functionality 35 | properties.list( 36 | 'members', 37 | state 38 | ) 39 | ); 40 | 41 | /** 42 | * get infos from group by its member infos (eg. average from properties) 43 | */ 44 | const info = state => ({ 45 | 46 | average: (property = false) => { 47 | 48 | if(!state.element.members.list().length){ 49 | return new Error(`the group ${state.element.name.get()} has no members.`); 50 | } 51 | 52 | const baseProperties = copyObject(playerPropertyConfig), 53 | members = state.element.members.list(), 54 | allMembersProperties = getPropertiesFromAllMembers(members, baseProperties), 55 | average = Object.keys(allMembersProperties).reduce((output, key) => { 56 | output[key] = Math.round(allMembersProperties[key].reduce((sum,val) => { 57 | return sum + val 58 | },0) / members.length); 59 | return output; 60 | }, {}); 61 | 62 | return property ? average[property] : average; 63 | }, 64 | min: (property = false) => { 65 | 66 | if(!state.element.members.list().length){ 67 | return new Error(`the group ${state.element.name.get()} has no members.`); 68 | } 69 | 70 | const baseProperties = copyObject(playerPropertyConfig), 71 | members = state.element.members.list(), 72 | allMembersProperties = getPropertiesFromAllMembers(members, baseProperties), 73 | min = Object.keys(allMembersProperties).reduce((output, key) => { 74 | output[key] = Math.min.apply(this, allMembersProperties[key]); 75 | return output; 76 | }, {}); 77 | 78 | return property ? min[property] : min; 79 | }, 80 | max: (property = false) => { 81 | 82 | if(!state.element.members.list().length){ 83 | return new Error(`the group ${state.element.name.get()} has no members.`); 84 | } 85 | 86 | const baseProperties = copyObject(playerPropertyConfig), 87 | group = state.element, 88 | allMembersProperties = getPropertiesFromAllMembers(group.members.list(), baseProperties), 89 | max = Object.keys(allMembersProperties).reduce((output, key) => { 90 | output[key] = Math.max.apply(this, allMembersProperties[key]); 91 | return output; 92 | }, {}); 93 | 94 | return property ? max[property] : max; 95 | } 96 | }); 97 | 98 | const summary = state => ({ 99 | 100 | get: () => { 101 | 102 | const group = state.element; 103 | 104 | return { 105 | name: group.name.get(), 106 | members: group.members.list().map(member => member.summary.short()) 107 | }; 108 | }, 109 | short: () => { 110 | 111 | const group = state.element; 112 | 113 | return { 114 | name: group.name.get() 115 | }; 116 | }, 117 | members: { 118 | get: () => { 119 | return state.element.members.list().map(member => member.summary.get()); 120 | }, 121 | short: () => { 122 | return state.element.members.list().map(member => member.summary.short()); 123 | }, 124 | long: () => { 125 | return state.element.members.list().map(member => member.summary.long()); 126 | } 127 | } 128 | 129 | }); 130 | 131 | 132 | const newGroup = (groupName) => { 133 | 134 | let state = copyObject(config); 135 | 136 | state.name = groupName; 137 | state.id = uuid.generate(); 138 | 139 | state.element = { 140 | name: name(state), 141 | id: id(state), 142 | members: members(state), 143 | summary: summary(state), 144 | info: info(state), 145 | event: new EventEmitter() 146 | }; 147 | 148 | return state.element; 149 | }; 150 | 151 | export const createGroup = (groupName, creatureGroup) => { 152 | 153 | return newGroup(groupName); 154 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as player from './player'; 2 | import * as creature from './creature'; 3 | import * as group from './group'; 4 | import * as box from './box'; 5 | import * as item from './item'; 6 | import * as place from './place'; 7 | import * as door from './door'; 8 | import * as path from './path'; 9 | import * as actions from './actions'; 10 | 11 | 12 | let attacker = player.createPlayer('toser', 'cat'), 13 | g = group.createGroup('ASD'); 14 | 15 | g.members.add(attacker); 16 | 17 | let weapon = item.createWeapon(g.info.average()), 18 | defender = creature.createCreature(g.info.average()), 19 | armor = defender.items.listArmor(); 20 | 21 | if (armor) { 22 | armor = armor[0]; 23 | } 24 | 25 | console.log('ARMOR', armor); 26 | 27 | console.log(attacker.name.get(), attacker.health.get()); 28 | console.log(defender.name.get(), defender.health.get()); 29 | console.log(JSON.stringify(weapon.summary.get(), null, 2)); 30 | console.log(JSON.stringify(armor.summary.get(), null, 2)); 31 | 32 | actions.attack({ 33 | attacker, 34 | defender, 35 | weapon, 36 | armor 37 | }); 38 | 39 | console.log(attacker.name.get(), attacker.health.get()); 40 | console.log(defender.name.get(), defender.health.get()); 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/item.js: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import {getConfig, copyObject, getFirstByType} from 'helptos'; 3 | import {WeightedSelection, randomInt} from 'random-tools'; 4 | import {createName} from './name'; 5 | import * as properties from './properties'; 6 | import * as uuid from './uuid'; 7 | 8 | const config = getConfig('../config/item.json', __dirname); 9 | const weaponNames = getConfig('../config/names/weapon-names.json', __dirname); 10 | const armorNames = getConfig('../config/names/armor-names.json', __dirname); 11 | const consumableNames = getConfig('../config/names/consumable-names.json', __dirname); 12 | 13 | // assign numerical getter and setter to properties 14 | const name = (state) => Object.assign({}, properties.mixed('name', state)), 15 | id = (state) => Object.assign({}, properties.fixed('id', state)), 16 | type = (state) => Object.assign({}, properties.fixed('type', state)), 17 | collectible = (state) => Object.assign({}, properties.boolean('name', state)), 18 | rank = (state) => Object.assign({}, properties.numericalPositive('rank', state.properties, state)), 19 | slots = (state) => Object.assign({}, properties.numericalPositive('slots', state.properties, state)), 20 | health = (state) => Object.assign({}, properties.numerical('health', state.properties, state)), 21 | attack = (state) => Object.assign({}, properties.numerical('attack', state.properties, state)), 22 | defense = (state) => Object.assign({}, properties.numerical('defense', state.properties, state)), 23 | dexterity = (state) => Object.assign({}, properties.numerical('dexterity', state.properties, state)), 24 | speed = (state) => Object.assign({}, properties.numerical('speed', state.properties, state)), 25 | time = (state) => Object.assign({}, properties.numericalPositive('time', state.properties, state)); 26 | 27 | const summary = state => ({ 28 | 29 | get: () => { 30 | 31 | const item = state.element; 32 | 33 | return { 34 | name: item.name.get(), 35 | type: item.type.get(), 36 | slots: item.slots.get(), 37 | rank: item.rank.get(), 38 | health: item.health.get(), 39 | attack: item.attack.get(), 40 | defense: item.defense.get(), 41 | dexterity: item.dexterity.get(), 42 | speed: item.speed.get(), 43 | time: item.time.get() 44 | }; 45 | }, 46 | long: () => { 47 | 48 | const item = state.element; 49 | 50 | return { 51 | name: item.name.get(), 52 | id: item.id.get(), 53 | type: item.type.get(), 54 | slots: item.slots.get(), 55 | rank: item.rank.get(), 56 | health: item.health.get(), 57 | attack: item.attack.get(), 58 | defense: item.defense.get(), 59 | dexterity: item.dexterity.get(), 60 | speed: item.speed.get(), 61 | time: item.time.get() 62 | }; 63 | }, 64 | short: () => { 65 | 66 | const item = state.element; 67 | 68 | return { 69 | name: item.name.get(), 70 | type: item.type.get(), 71 | rank: item.rank.get() 72 | }; 73 | } 74 | }); 75 | 76 | // creates a new item 77 | const newItem = (itemName, itemType) => { 78 | 79 | // get template for item type 80 | let state = getFirstByType(copyObject(config).templates, itemType); 81 | 82 | state.name = itemName; 83 | state.id = uuid.generate(); 84 | state.element = { 85 | name: name(state), 86 | id: id(state), 87 | type: type(state), 88 | rank: rank(state), 89 | slots: slots(state), 90 | health: health(state), 91 | attack: attack(state), 92 | defense: defense(state), 93 | dexterity: dexterity(state), 94 | speed: speed(state), 95 | time: time(state), 96 | collectible: collectible(state), 97 | summary: summary(state), 98 | event: new EventEmitter() 99 | }; 100 | 101 | return state.element; 102 | }; 103 | 104 | export const createWeapon = (averages) => { 105 | 106 | let weapon = newItem(createName(weaponNames), types.WEAPON); 107 | 108 | // Todo: get the ranges around averages via a config or something 109 | weapon 110 | .rank.up(randomInt(averages.rank + 1, averages.rank - 2)) 111 | .slots.up(randomInt(averages.slots + 4, averages.slots - 4)) 112 | .attack.up(randomInt(averages.attack + 10, averages.attack - 10)) 113 | .defense.up(randomInt(averages.defense + 2, averages.defense - 2)) 114 | .speed.up(randomInt(averages.speed + 2, averages.speed - 10)) 115 | .dexterity.up(randomInt(averages.dexterity + 5, averages.dexterity - 5)); 116 | 117 | return weapon; 118 | }; 119 | 120 | export const createArmor = (averages) => { 121 | 122 | let armor = newItem(createName(armorNames), types.ARMOR); 123 | 124 | // Todo: get the ranges around averages via a config or something 125 | armor 126 | .rank.up(randomInt(averages.rank + 1, averages.rank - 2)) 127 | .slots.up(randomInt(averages.slots + 3, averages.slots - 3)) 128 | .attack.up(randomInt(averages.attack + 2, averages.attack - 2)) 129 | .defense.up(randomInt(averages.defense + 10, averages.defense - 5)) 130 | .speed.up(randomInt(averages.speed + 2, averages.speed - 8)) 131 | .dexterity.up(randomInt(averages.dexterity + 5, averages.dexterity - 5)); 132 | 133 | return armor; 134 | }; 135 | 136 | export const createConsumable = (averages) => { 137 | 138 | const effect = new WeightedSelection({ 139 | 'attack': 1, 140 | 'defense': 1, 141 | 'speed': 1, 142 | 'dexterity': 1 143 | }).random(); 144 | 145 | let consumable = newItem(createName(consumableNames), types.CONSUMABLE); 146 | 147 | // Todo: get the ranges around averages via a config or something 148 | consumable 149 | .rank.up(randomInt(averages.rank + 1, averages.rank - 3)) 150 | .slots.up(randomInt(averages.slots + 2, averages.slots - 1)) 151 | .health.up(randomInt(60, 10)) 152 | .time.up(randomInt(120, 30)) 153 | [effect].up(randomInt(averages[effect] * 2, parseInt(averages[effect] * .5))); 154 | 155 | return consumable; 156 | }; 157 | 158 | 159 | // item types 160 | export const types = { 161 | WEAPON: 'weapon', 162 | ARMOR: 'armor', 163 | CONSUMABLE: 'consumable' 164 | }; -------------------------------------------------------------------------------- /src/name.js: -------------------------------------------------------------------------------- 1 | import {WeightedSelection, randomInt} from 'random-tools'; 2 | import {assign} from 'lodash'; 3 | 4 | 5 | function getWeightedSelection (weight) { 6 | 7 | if (weight === true) { 8 | return () => 1; 9 | } 10 | 11 | if (weight === false) { 12 | return () => 0; 13 | } 14 | 15 | if (typeof weight === 'string' && 16 | weight.split('/').length >= 2) { 17 | 18 | const weightArr = weight.split('/'), 19 | selection = new WeightedSelection({ 20 | '1': weightArr[0], 21 | '0': weightArr[1] 22 | }); 23 | return () => selection.random(); 24 | } 25 | 26 | return () => 0; 27 | } 28 | 29 | 30 | export const createName = (names, _weight) => { 31 | 32 | const defaultWeight = { 33 | pre1: '1/2', 34 | pre2: '2/1', 35 | main: true, 36 | post: '1/2' 37 | }, 38 | weight = assign({}, defaultWeight, _weight), 39 | randomPre1 = getWeightedSelection(weight.pre1), 40 | randomPre2 = getWeightedSelection(weight.pre2), 41 | randomMain = getWeightedSelection(weight.main), 42 | randomPost = getWeightedSelection(weight.post); 43 | 44 | let name = ''; 45 | 46 | if (names.pre1.length && !!parseInt(randomPre1())) { 47 | name += names.pre1[randomInt(names.pre1.length - 1)] + ' '; 48 | } 49 | 50 | if (names.pre2.length && !!parseInt(randomPre2())) { 51 | name += names.pre2[randomInt(names.pre2.length - 1)] + ' '; 52 | } 53 | 54 | if (names.main.length && !!parseInt(randomMain())) { 55 | name += names.main[randomInt(names.main.length - 1)] + ' '; 56 | } 57 | 58 | if (names.post.length && !!parseInt(randomPost())) { 59 | name += names.post[randomInt(names.post.length - 1)] + ' '; 60 | } 61 | 62 | return name.trim() 63 | }; -------------------------------------------------------------------------------- /src/parser.js: -------------------------------------------------------------------------------- 1 | 2 | import {EOL} from 'os'; 3 | import {chain} from 'lodash'; 4 | import * as Help from './commands/help'; 5 | import * as Quit from './commands/quit'; 6 | import * as Debug from './commands/debug'; 7 | import * as Start from './commands/start'; 8 | import * as Stop from './commands/stop'; 9 | import * as Group from './commands/group'; 10 | import * as Player from './commands/character'; 11 | import * as Where from './commands/where'; 12 | import * as Boxes from './commands/boxes'; 13 | import * as Doors from './commands/doors'; 14 | import * as Creatures from './commands/creatures'; 15 | import * as Move from './commands/move'; 16 | import * as Pick from './commands/pickup'; 17 | import * as Drop from './commands/drop'; 18 | import * as Inventory from './commands/inventory'; 19 | import * as Open from './commands/open'; 20 | import * as Leave from './commands/leave'; 21 | import * as Attack from './commands/attack'; 22 | 23 | const commands = [ 24 | // 25 | Help, 26 | Quit, 27 | Debug, 28 | Start, 29 | Stop, 30 | // 31 | Group, 32 | Player, 33 | Where, 34 | Boxes, 35 | Doors, 36 | Creatures, 37 | // 38 | Move, 39 | Leave, 40 | Open, 41 | Pick, 42 | Drop, 43 | Attack, 44 | Inventory 45 | ]; 46 | 47 | let debug = false, 48 | disabled = false; // disable parsing 49 | 50 | function isSimpleString(action) { 51 | return typeof action === 'string'; 52 | } 53 | 54 | function actionIs(type) { 55 | return action => { 56 | return typeof action === 'object' && 57 | 'action' in action && 58 | action.action === type; 59 | } 60 | } 61 | 62 | function commandMatches(command) { 63 | return c => !!c.cmdRegExp && c.cmdRegExp.test(command); 64 | } 65 | 66 | function runCommand(player, command, world) { 67 | return c => c.run(player, command, world); 68 | } 69 | 70 | /** 71 | * parses a given command and executes the according effects in the given world 72 | * @param {String} command 73 | * @param {Object} world 74 | * @return {Array} array of strings containing response messages 75 | */ 76 | export let parse = (player, command, world, write, indicateUserInput) => { 77 | 78 | if (disabled) 79 | return false; // proceed? 80 | 81 | let proceed = true; 82 | 83 | const actions = chain(commands) 84 | .filter(commandMatches(command)) 85 | .flatMap(runCommand(player, command, world)) 86 | .value(); 87 | 88 | // string 89 | const simpleStrings = actions.filter(isSimpleString); 90 | if(simpleStrings.length) { 91 | write(simpleStrings.join(EOL)); 92 | } 93 | 94 | // message 95 | actions.filter(actionIs('message')) 96 | .forEach(action => { 97 | let delay = action.delay; 98 | if (debug) { 99 | delay = 0; 100 | } 101 | setTimeout(() => { 102 | write(action.text); 103 | }, delay || 0); 104 | }); 105 | 106 | // delay 107 | actions.filter(actionIs('delay')) 108 | .forEach(action => { 109 | let delay = action.delay; 110 | if (debug) { 111 | delay = 0; 112 | } 113 | setTimeout(() => { 114 | action.callback(); 115 | }, delay || 0); 116 | }); 117 | 118 | // disable 119 | actions.filter(actionIs('disable')) 120 | .forEach(action => { 121 | let delay = action.delay; 122 | if (debug) { 123 | delay = 0; 124 | } 125 | setTimeout(() => { 126 | disabled = true; 127 | if (debug) 128 | write('DISABLED'); 129 | }, delay || 0); 130 | proceed = false; 131 | }); 132 | 133 | // enable 134 | actions.filter(actionIs('enable')) 135 | .forEach(action => { 136 | let delay = action.delay; 137 | if (debug) { 138 | delay = 0; 139 | } 140 | setTimeout(() => { 141 | disabled = false; 142 | if (debug) 143 | write('ENABLED'); 144 | if (indicateUserInput) 145 | indicateUserInput(); // in CLI show '>' 146 | }, delay || 0); 147 | proceed = false; 148 | }); 149 | 150 | // quit 151 | actions.filter(actionIs('quit')) 152 | .forEach(action => { 153 | write('bye'); 154 | proceed = false; 155 | }); 156 | 157 | // debug 158 | actions.filter(actionIs('debug')) 159 | .forEach(action => { 160 | debug = !debug; 161 | write('DEBUG', debug ? 'ON' : 'OFF'); 162 | }); 163 | 164 | // fallback 165 | chain(actions) 166 | .reject(action => { 167 | return isSimpleString(action) || 168 | actionIs('message')(action) || 169 | actionIs('delay')(action) || 170 | actionIs('disable')(action) || 171 | actionIs('enable')(action) || 172 | actionIs('quit')(action) || 173 | actionIs('debug')(action); 174 | }) 175 | .value() 176 | .forEach(action => { 177 | write(JSON.stringify(action, null, 2)); 178 | }); 179 | 180 | // command not found 181 | if(!actions.length) { 182 | write('unknown command :('); 183 | } 184 | 185 | if (indicateUserInput && proceed) { 186 | indicateUserInput(); // in CLI show '>' 187 | } 188 | }; 189 | -------------------------------------------------------------------------------- /src/path.js: -------------------------------------------------------------------------------- 1 | import {getConfig, copyObject} from 'helptos'; 2 | import {randomInt} from 'random-tools'; 3 | import {createName} from './name'; 4 | import * as place from './place'; 5 | import * as properties from './properties'; 6 | 7 | const config = getConfig('../config/path.json', __dirname); 8 | const pathNames = getConfig('../config/names/path-names.json', __dirname); 9 | 10 | const name = state => Object.assign({}, properties.mixed('name', state)); 11 | const distance = state => Object.assign({}, properties.numericalPositive('distance', state)); 12 | 13 | const places = state => Object.assign({}, 14 | // get default list functionality 15 | properties.list( 16 | 'places', 17 | state 18 | ) 19 | ); 20 | 21 | const summary = state => ({ 22 | 23 | get: () => { 24 | const path = state.element; 25 | 26 | return { 27 | name: path.name.get(), 28 | distance: path.distance.get(), 29 | places: path.places.list().map(place => place.summary.short()) 30 | }; 31 | }, 32 | short: () => { 33 | const path = state.element; 34 | 35 | return { 36 | name: state.element.name.get(), 37 | distance: path.distance.get(), 38 | }; 39 | }, 40 | places: { 41 | get: () => state.element.places.list().map(place => place.summary.get()), 42 | short: () => state.element.places.list().map(place => place.summary.short()) 43 | } 44 | }); 45 | 46 | 47 | const newPath = (name_in) => { 48 | 49 | let state = copyObject(config); 50 | 51 | state.name = name_in; 52 | 53 | state.element = { 54 | name: name(state), 55 | distance: distance(state), 56 | places: places(state), 57 | summary: summary(state) 58 | }; 59 | 60 | return state.element; 61 | }; 62 | 63 | export const createPath = ({currentPlace, name = createName(pathNames)}) => { 64 | 65 | let path = newPath(name), 66 | newPlace = place.createPlace({}), 67 | newPlaceDoor = newPlace.doors.list()[0], 68 | pathDistance = randomInt(3000, 200) / 1000; 69 | 70 | path.distance.up(pathDistance); 71 | path.places.add(currentPlace); 72 | path.places.add(newPlace); 73 | newPlaceDoor.path.set(path); 74 | 75 | return path; 76 | } 77 | -------------------------------------------------------------------------------- /src/place.js: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import {getConfig, copyObject} from 'helptos'; 3 | import {randomInt} from 'random-tools'; 4 | import * as properties from './properties'; 5 | import * as box from './box'; 6 | import * as door from './door'; 7 | import * as creature from './creature'; 8 | import {createName} from './name'; 9 | 10 | const config = getConfig('../config/place.json', __dirname); 11 | const placeNames = getConfig('../config/names/place-names.json', __dirname); 12 | 13 | const name = state => Object.assign({}, properties.mixed('name', state, state)); 14 | const location = state => Object.assign({}, properties.mixed('location', state, state)); 15 | const explored = state => Object.assign({}, properties.boolean('explored', state, state)); 16 | 17 | const boxes = state => Object.assign({}, 18 | // get default list functionality 19 | properties.list( 20 | 'boxes', 21 | state 22 | ) 23 | ); 24 | 25 | const doors = state => Object.assign({}, 26 | // get default list functionality 27 | properties.list( 28 | 'doors', 29 | state 30 | ) 31 | ); 32 | 33 | const groups = state => Object.assign({}, 34 | // get default list functionality 35 | properties.list( 36 | 'groups', 37 | state 38 | ) 39 | ); 40 | 41 | const creatures = state => Object.assign({}, 42 | // get default list functionality 43 | properties.list( 44 | 'creatures', 45 | state 46 | ) 47 | ); 48 | 49 | const summary = state => ({ 50 | 51 | get: () => { 52 | const place = state.element; 53 | 54 | return { 55 | name: place.name.get(), 56 | location: place.location.get(), 57 | groups: place.groups.list().map(group => group.summary.short()), 58 | boxes: place.boxes.list().map(box => box.summary.short()), 59 | doors: place.doors.list().map(door => door.summary.short()), 60 | creatures: place.creatures.list().map(creature => creature.summary.short()), 61 | }; 62 | }, 63 | short: () => state.element.name.get(), 64 | boxes: { 65 | get: () => state.element.boxes.list().map(box => box.summary.get()), 66 | short: () => state.element.boxes.list().map(box => box.summary.short()) 67 | }, 68 | doors: { 69 | get: () => state.element.doors.list().map(door => door.summary.get()), 70 | short: () => state.element.doors.list().map(door => door.summary.short()) 71 | }, 72 | groups: { 73 | get: () => state.element.groups.list().map(group => group.summary.get()), 74 | short: () => state.element.groups.list().map(group => group.summary.short()) 75 | }, 76 | creatures: { 77 | long: () => state.element.creatures.list().map(creature => creature.summary.long()), 78 | get: () => state.element.creatures.list().map(creature => creature.summary.get()), 79 | short: () => state.element.groups.list().map(creature => creature.summary.short()) 80 | } 81 | }); 82 | 83 | const newPlace = (state_in) => { 84 | 85 | let state = Object.assign(copyObject(config), state_in); 86 | 87 | state.element = { 88 | name: name(state), 89 | location: location(state), 90 | explored: explored(state), 91 | boxes: boxes(state), 92 | doors: doors(state), 93 | groups: groups(state), 94 | creatures: creatures(state), 95 | summary: summary(state), 96 | event: new EventEmitter() 97 | }; 98 | 99 | return state.element; 100 | }; 101 | 102 | export const createPlace = ({ group, path, name}) => { 103 | 104 | let place = newPlace({ 105 | name: name || createName(placeNames) 106 | }), 107 | numberOfBoxes = randomInt(10, 1), 108 | numberOfEnemyGroups = randomInt(4, 1), 109 | numberOfDoors = randomInt(5, 2), 110 | numberOfCreatures = randomInt(4, 1); 111 | 112 | place.doors.add(door.createDoors({}, numberOfDoors)); 113 | 114 | if(group) { 115 | place.boxes.add(box.createBoxes({average: group.info.average()}, numberOfBoxes)) 116 | .creatures.add(creature.createCreatures({average: group.info.average()}, numberOfCreatures)) 117 | .groups.add(group); 118 | 119 | place.creatures 120 | .list() 121 | .filter(creature.isAggrassive) 122 | .map(creature.activateMap(group)); 123 | } 124 | 125 | if(path && place.doors.list().length) { 126 | place.doors.list()[0].path.add(path); 127 | } 128 | 129 | return place; 130 | } 131 | -------------------------------------------------------------------------------- /src/player.js: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from 'events'; 2 | import {getConfig, copyObject, getFirstByName} from 'helptos'; 3 | import * as properties from './properties'; 4 | import * as race from './race'; 5 | import * as item from './item'; 6 | 7 | const config = getConfig('../config/player.json', __dirname); 8 | 9 | const name = (state) => properties.mixed('name', state), 10 | type = (state) => properties.fixed('type', state), 11 | health = (state) => properties.numericalPositive('health', state.properties, state), 12 | rank = (state) => properties.numericalPositive('rank', state.properties, state), 13 | attack = (state) => properties.numerical('attack', state.properties, state), 14 | defense = (state) => properties.numerical('defense', state.properties, state), 15 | dexterity = (state) => properties.numerical('dexterity', state.properties, state), 16 | speed = (state) => properties.numerical('speed', state.properties, state), 17 | weapon = (state) => properties.mixed('weapon', state), 18 | armor = (state) => properties.mixed('armor', state); 19 | 20 | /** 21 | * anything you can do with your inventory slots 22 | * 23 | * @param state 24 | */ 25 | const slots = (state) => Object.assign({ 26 | 27 | filled: () => { 28 | 29 | return state.element.items.list().reduce((filled, item) => { 30 | return filled + item.slots.get(); 31 | }, 0); 32 | }, 33 | 34 | /** 35 | * get number of free slots 36 | * @returns {int} 37 | */ 38 | free: () => { 39 | 40 | return state.element.slots.get() - state.element.slots.filled() 41 | } 42 | 43 | }, properties.numerical('slots', state.properties, state)); 44 | 45 | /** 46 | * anything you can do with inventory items 47 | * 48 | * @param state 49 | */ 50 | const items = (state) => Object.assign({}, 51 | // get default list functionality 52 | properties.list( 53 | 'items', 54 | state, 55 | state, 56 | [ 57 | item.types.WEAPON, 58 | item.types.ARMOR, 59 | item.types.CONSUMABLE 60 | ] 61 | ), 62 | { 63 | /** 64 | * override add item default functionality 65 | * 66 | * @param item 67 | * @returns {{name, type, health, rank, attack, defense, dexterity, speed, slots, items, event: *, describe: state.element.describe}|*} 68 | */ 69 | add: (item) => { 70 | 71 | if(item.collectible.get() && 72 | state.element.rank.get() >= item.rank.get() && 73 | state.element.slots.free() >= item.slots.get()){ 74 | 75 | state.items.push(item); 76 | } 77 | 78 | return state.element; 79 | } 80 | } 81 | ); 82 | 83 | const summary = state => ({ 84 | 85 | 86 | get: () => { 87 | 88 | const player = state.element; 89 | 90 | return { 91 | name: player.name.get(), 92 | type: player.type.get(), 93 | rank: player.rank.get(), 94 | slots: player.slots.free(), 95 | health: player.health.get(), 96 | attack: player.attack.get(), 97 | defense: player.defense.get(), 98 | dexterity: player.dexterity.get(), 99 | speed: player.speed.get() 100 | }; 101 | }, 102 | short: () => { 103 | 104 | const player = state.element; 105 | 106 | return { 107 | name: player.name.get(), 108 | type: player.type.get(), 109 | rank: player.rank.get() 110 | }; 111 | }, 112 | long: () => { 113 | 114 | const player = state.element; 115 | 116 | return { 117 | name: player.name.get(), 118 | type: player.type.get(), 119 | rank: player.rank.get(), 120 | slots: player.slots.free(), 121 | health: player.health.get(), 122 | attack: player.attack.get(), 123 | defense: player.defense.get(), 124 | dexterity: player.dexterity.get(), 125 | speed: player.speed.get(), 126 | weapon: player.items.listWeapon().map(item => item.summary.short()), 127 | armor: player.items.listArmor().map(item => item.summary.short()), 128 | consumable: player.items.listConsumable().map(item => item.summary.short()) 129 | }; 130 | }, 131 | items: { 132 | get: () => { 133 | 134 | const player = state.element; 135 | 136 | return player.items.list().map(item => item.summary.get()); 137 | }, 138 | short: () => { 139 | 140 | const player = state.element; 141 | 142 | return player.items.list().map(item => item.summary.short()); 143 | } 144 | } 145 | 146 | }); 147 | 148 | const newPlayer = (playerName, playerRace) => { 149 | 150 | let state = copyObject(config); 151 | const raceState = race.getRace(playerRace); 152 | 153 | state.name = playerName; 154 | state.type = raceState.type; 155 | state.properties = Object.assign(state.properties, raceState.properties); 156 | 157 | state.element = { 158 | name: name(state), 159 | type: type(state), 160 | health: health(state), 161 | rank: rank(state), 162 | attack: attack(state), 163 | defense: defense(state), 164 | dexterity: dexterity(state), 165 | speed: speed(state), 166 | slots: slots(state), 167 | items: items(state), 168 | weapon: weapon(state), 169 | armor: armor(state), 170 | summary: summary(state), 171 | event: new EventEmitter() 172 | }; 173 | 174 | return state.element; 175 | }; 176 | 177 | export const createPlayer = (playerName, playerRace) => { 178 | 179 | return newPlayer(playerName, playerRace); 180 | }; -------------------------------------------------------------------------------- /src/properties.js: -------------------------------------------------------------------------------- 1 | import {capitalize} from 'helptos'; 2 | 3 | // ------------------------- 4 | // property function collections 5 | // ------------------------- 6 | 7 | /** 8 | * basic getter and setter for numerical properties 9 | * 10 | * @param property 11 | * @param parent 12 | * @param state 13 | */ 14 | export const numerical = (property, parent, state = parent) => ({ 15 | get: () => parent[property], 16 | up: (val = 1) => { 17 | parent[property] = parent[property] + val; 18 | return state.element; 19 | }, 20 | down: (val = 1) => { 21 | parent[property] = parent[property] - val; 22 | return state.element; 23 | } 24 | }); 25 | 26 | /** 27 | * basic getter and setter for numerical properties 28 | * 29 | * @param property 30 | * @param parent 31 | * @param state 32 | */ 33 | export const numericalPositive = (property, parent, state = parent) => ({ 34 | get: () => parent[property], 35 | up: (val = 1) => { 36 | parent[property] = parent[property] + val; 37 | if (parent[property] < 0) { 38 | parent[property] = 0; 39 | } 40 | return state.element; 41 | }, 42 | down: (val = 1) => { 43 | parent[property] = parent[property] - val; 44 | if (parent[property] < 0) { 45 | parent[property] = 0; 46 | } 47 | return state.element; 48 | } 49 | }); 50 | 51 | /** 52 | * get and set for mixed values 53 | * 54 | * @param property 55 | * @param parent 56 | * @param state 57 | */ 58 | export const mixed = (property, parent, state = parent) => ({ 59 | get: () => parent[property], 60 | set: (val) => { 61 | if (val) { 62 | parent[property] = val; 63 | } 64 | return state.element; 65 | } 66 | }); 67 | 68 | /** 69 | * get and set for boolean values 70 | * 71 | * @param property 72 | * @param parent 73 | * @param state 74 | */ 75 | export const boolean = (property, parent, state = parent) => ({ 76 | get: () => parent[property], 77 | set: (val) => { 78 | if (val) { 79 | parent[property] = !!val; 80 | } 81 | return state.element; 82 | } 83 | }); 84 | 85 | /** 86 | * get protected field 87 | * 88 | * @param property 89 | * @param parent 90 | * @param state 91 | */ 92 | export const fixed = (property, parent, state = parent) => ({ 93 | get: () => parent[property] 94 | }); 95 | 96 | 97 | /** 98 | * list default functions 99 | * 100 | * you can add default filters as own functions by passing an array of type filters 101 | * e.g.: 102 | * typeFilters = ['weapon'] 103 | * will create a function listWeapon() 104 | * that will return a a filtered list of elements with type==='weapon' 105 | * 106 | * 107 | * @param property 108 | * @param parent 109 | * @param state 110 | * @param typeFilters 111 | * @returns {{list: (function())}} 112 | */ 113 | export const list = (property, parent, state = parent, typeFilters = []) => { 114 | 115 | let obj = { 116 | list: getList(parent[property]), 117 | add: addToList(parent[property], state), 118 | remove: removeFromList(parent[property], state) 119 | }; 120 | 121 | typeFilters.forEach(filter => { 122 | obj[`list${capitalize(filter)}`] = getList(parent[property], filter); 123 | }); 124 | 125 | return obj; 126 | }; 127 | 128 | 129 | // ------------------------- 130 | // single property functions 131 | // ------------------------- 132 | 133 | /** 134 | * add element to array 135 | */ 136 | export const addToList = (arr, state) => { 137 | 138 | return (element) => { 139 | 140 | if(element.constructor === Array) { 141 | element.forEach(function (item) { 142 | arr.push(item); 143 | }); 144 | } 145 | else { 146 | arr.push(element); 147 | } 148 | 149 | return state.element; 150 | }; 151 | }; 152 | 153 | /** 154 | * remove element from array by name 155 | * 156 | * @param state 157 | * @param arr 158 | * @param callback 159 | * @returns {function(elementName)} 160 | */ 161 | export const removeFromList = (arr, state) => { 162 | 163 | return (id) => { 164 | 165 | let index = arr.map(i => i.id.get()).indexOf(id); 166 | 167 | if(index !== -1) { 168 | arr.splice(index, 1); 169 | } 170 | 171 | return state.element; 172 | } 173 | }; 174 | 175 | /** 176 | * get a list of elements (array) 177 | * set type to filter the list by type property 178 | * 179 | * @param arr 180 | * @param type 181 | * @returns {function} 182 | */ 183 | export const getList = (arr, type) => { 184 | 185 | return (key, value) => { // or filter by key:value pair 186 | if (type) { 187 | return arr.filter(item => item.type.get() === type); 188 | } else if (key && (value || value === 0)) { 189 | return arr.filter(item => key in item ? item[key].get() === value : false); 190 | } else { 191 | return arr; 192 | } 193 | } 194 | }; 195 | -------------------------------------------------------------------------------- /src/race.js: -------------------------------------------------------------------------------- 1 | import {getConfig, getFirstByType, copyObject} from 'helptos'; 2 | 3 | 4 | let config = getConfig('../config/race.json', __dirname); 5 | 6 | export let getRace = type => getFirstByType(copyObject(config).templates, type); 7 | 8 | export let getRaces = () => Object.keys(config.templates).map((e) => { return config.templates[e].type }) 9 | -------------------------------------------------------------------------------- /src/rpg.js: -------------------------------------------------------------------------------- 1 | 2 | // this should be the entry point for node-rpg in the near future 3 | // an thus replace index.js 4 | // soon... 5 | 6 | import {getConfig, getFirstByType, copyObject} from 'helptos'; 7 | import * as Parser from './parser'; 8 | import * as World from './world'; 9 | 10 | const newRPG = (databaseFile) => { 11 | 12 | let state = {}; 13 | 14 | state.world = World.createWorld(databaseFile); 15 | state.rpg = { 16 | parse : (player, command, write, indicateUserInput) => { return Parser.parse(player, command, state.world, write, indicateUserInput); } 17 | }; 18 | 19 | return state.rpg; 20 | }; 21 | 22 | export const createRPG = (databaseFile) => { 23 | 24 | let rpg = newRPG(databaseFile); 25 | 26 | return rpg; 27 | } 28 | -------------------------------------------------------------------------------- /src/test/properties.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {numerical} from '../lib/properties'; 3 | 4 | describe('properties', () => { 5 | 6 | describe('numerical', () => { 7 | 8 | describe('get', () => { 9 | 10 | /** 11 | * ToDo: move this to a mock 12 | */ 13 | const state = { 14 | health: 100, 15 | subElement: { 16 | attack: 30 17 | } 18 | }; 19 | 20 | it('should return the correct value for a property', () => { 21 | const health = numerical('health', state).get(); 22 | const attack = numerical('attack', state.subElement, state).get(); 23 | expect(health).to.equal(100); 24 | expect(attack).to.equal(30); 25 | }); 26 | 27 | it('should return undefined if property is not present', () => { 28 | const age = numerical('age', state).get(); 29 | const gender = numerical('gender', state.subElement, state).get(); 30 | expect(age).to.equal(undefined); 31 | expect(gender).to.equal(undefined); 32 | }); 33 | 34 | }); 35 | }); 36 | }); -------------------------------------------------------------------------------- /src/uuid.js: -------------------------------------------------------------------------------- 1 | // immediately-invoked 2 | export const generate = function () { 3 | let uuid = 0; 4 | return () => uuid++; 5 | }(); -------------------------------------------------------------------------------- /src/world.js: -------------------------------------------------------------------------------- 1 | 2 | import {getConfig, getFirstByType, copyObject} from 'helptos'; 3 | import {randomInt} from 'random-tools'; 4 | import Database from 'nedb'; 5 | 6 | import * as Place from './place'; 7 | import * as Box from './box'; 8 | 9 | const newWorld = (databaseFile) => { 10 | 11 | let state = {}; 12 | 13 | state.database = new Database({ filename: databaseFile || 'rpg.db', autoload: true }); 14 | state.world = { 15 | playerGroup: null, 16 | places: {}, 17 | currentPlace: null, 18 | getPlayers: n => { return (state.world.playerGroup ? state.world.playerGroup.members.list('name', n) : []); }, 19 | init: () => { 20 | let spawnLocation = newLocation(state.world); 21 | state.world.places[spawnLocation] = Place.createPlace({ group : state.world.playerGroup }); 22 | state.world.currentPlace = spawnLocation; 23 | state.world.places[spawnLocation].location.set(spawnLocation); 24 | }, 25 | reset: () => { 26 | state.world.playerGroup = null; 27 | state.world.places = []; 28 | state.world.currentPlace = null; 29 | } 30 | }; 31 | 32 | return state.world; 33 | }; 34 | 35 | export const createWorld = (databaseFile) => { 36 | 37 | return newWorld(databaseFile); 38 | } 39 | 40 | // ToDo: we should do this better 41 | export const newLocation = (world, location) => { 42 | 43 | if(!location) { 44 | return '0,0'; 45 | } 46 | 47 | const currX = parseInt(location.split(',')[0]), 48 | currY = parseInt(location.split(',')[1]), 49 | x = randomInt(currX+1, currX-1), 50 | y = randomInt(currY+1, currY-1); 51 | 52 | if(!world.places[`${x},${y}`]) { 53 | return `${x},${y}`; 54 | } 55 | 56 | return newLocation(world, `${x},${y}`); 57 | }; 58 | --------------------------------------------------------------------------------