├── .gitignore ├── LICENSE.txt ├── README.md ├── colortest.html ├── convertAudio.py ├── convertMaps.py ├── dat2.py ├── exportImages.py ├── exportImagesPar.py ├── exportPRO.py ├── fomap.py ├── frmpixels.py ├── hex_outline.png ├── lib ├── heart.js ├── pako.min.js └── pathfinding-browser.js ├── lut ├── colorTable.json ├── color_lut.json ├── color_rgb.json ├── criticalTables.json ├── elevators.json └── intensityColorTable.js ├── maps ├── nullmap.images.json └── nullmap.json ├── mpserv.py ├── pal.py ├── parseCritTable.py ├── parseElevatorTable.py ├── play.html ├── proto.py ├── screenshot.png ├── setup.py ├── src ├── audio.ts ├── canvasrenderer.ts ├── char.ts ├── combat.ts ├── config.ts ├── criticalEffects.ts ├── critter.ts ├── data.ts ├── dis.ts ├── encounters.ts ├── events.ts ├── geometry.ts ├── idbcache.ts ├── intfile.ts ├── lighting.ts ├── lightmap.ts ├── main.ts ├── map.ts ├── net.ts ├── object.ts ├── party.ts ├── player.ts ├── pro.ts ├── renderer.ts ├── saveload.ts ├── scripting.ts ├── skillDependencies.ts ├── ui.ts ├── util.ts ├── vm.ts ├── vm_bridge.ts ├── webglrenderer.ts └── worldmap.ts ├── stitchWorldmap.py ├── tsconfig.json ├── ui.css └── wrappers └── macos ├── darkfo.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist └── darkfo ├── AppDelegate.swift ├── Base.lproj └── MainMenu.xib ├── Info.plist ├── PreferencesController.swift ├── ViewController.swift └── darkfo.entitlements /.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | !tsconfig.json 3 | *.images.txt 4 | *.map 5 | *.pyc 6 | data/ 7 | art/ 8 | .DS_Store 9 | xcuserdata 10 | contents.xcworkspacedata -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | 3 | Version 2.0, January 2004 4 | 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 16 | 17 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 18 | 19 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 20 | 21 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 22 | 23 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 24 | 25 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 26 | 27 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 28 | 29 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 30 | 31 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 32 | 33 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 34 | 35 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 36 | 37 | You must give any other recipients of the Work or Derivative Works a copy of this License; and 38 | You must cause any modified files to carry prominent notices stating that You changed the files; and 39 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 40 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 41 | 42 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 43 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 44 | 45 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 46 | 47 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 48 | 49 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 50 | 51 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 52 | 53 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DarkFO 2 | 3 | A post-nuclear RPG remake 4 | 5 | This is a modern reimplementation of the engine of the video game [Fallout 2](http://en.wikipedia.org/wiki/Fallout_2), as well as a personal research project into the feasibility of doing such. 6 | 7 | It is written primarily in TypeScript and Python, and targets a modern (HTML 5) Web browser. 8 | 9 | ## Status 10 | 11 | DarkFO is not a complete remake at this time. 12 | A lot of core functionality works, but major parts are missing or need work. 13 | 14 | If you're looking for documentation on how Fallout 2 works, or documentation on certain file formats, or 15 | just want some tools to work with them, this project will be useful to you as well. 16 | 17 | 18 | 19 | Here is a very rough list of what is known to work: 20 | 21 | - Map loading 22 | - Walking, running 23 | - Talking to NPCs 24 | - Bartering 25 | - Some quests (a lot of the scripting works, majors quests can be completed) 26 | - Some party members 27 | - Some skills (lockpicking and repair, and some passive skills) 28 | - Sound (scripted sound effects, music; not hardcoded sound effects, yet) 29 | 30 | Some features are more middle ground: 31 | 32 | - Combat works at an extremely basic level but not to a great degree (only the SMG and spear is really tested, you cannot swap ammo, etc.) 33 | - No equippable armor 34 | - The world map is rough and buggy, and on the area screens entrances are misplaced 35 | - Random encounters work, but not all of the setups are implemented 36 | - Lighting works, but has some minor bugs and inaccuracies. It is also particularly slow, especially outside of the WebGL backend. 37 | - Saving and loading is at an alpha stage: it works to a basic degree, but is missing some features and is not tested. As such, consider it experimental. 38 | - Some animations are off, particularly related to combat 39 | - Leveling up (including XP, leveling stats/skills, etc) partially works 40 | 41 | Some features are not implemented at all: 42 | 43 | - The PipBoy map 44 | 45 | and other minor features here and there. 46 | 47 | If you'd like to contribute, those might be major parts to look into. 48 | 49 | ## Installation 50 | 51 | To use this, you'll need a few things: 52 | 53 | - A copy of Fallout 2 (already installed) 54 | 55 | - Python 2.7 56 | 57 | - [Pillow](https://pillow.readthedocs.io/en/4.0.x) (just `pip install pillow`) 58 | 59 | - [NumPy](http://www.numpy.org/) (Windows binaries available [here](http://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy).) 60 | 61 | - The TypeScript compiler, installed via `npm install -g typescript` (you'll need [node.js](https://nodejs.org/en/)). 62 | 63 | You'll need an HTTP server to run (despite being all static content) due to the way browsers sandbox requests. 64 | If you're comfortable with setting up nginx, lighttpd, or Apache, go for that. If not, a simple way is to use Python: 65 | 66 | - Python 2: `python -m SimpleHTTPServer` (Python 2 is already required anyway) 67 | - Python 3: `python -m http.server` 68 | 69 | Alternatively, Firefox can load directly from `file://`. 70 | 71 | Once you've got all that, you can start trying it out. 72 | 73 | Open a command prompt inside the DarkFO directory, and then run: 74 | 75 | python setup.py path/to/Fallout2/installation/directory 76 | 77 | This will take a few minutes, it's unpacking the game archives and converting relevant game data into a format DarkFO can use. 78 | 79 | NOTE: You may need to use `python2` instead, as some Linux distributions package `python` as Python 3. Run `python --version` to check! 80 | 81 | Then run `tsc` to compile the source code. 82 | 83 | Browse to `http://localhost/play.html?artemple` (or whatever port you're using). If all went well, it should begin the game. If not, check the JavaScript console for errors. 84 | 85 | Review `src/config.ts` for engine options. Be sure to re-compile if you change them. 86 | 87 | OPTIONAL: If you want sound, run `python convertAudio.py`. You'll need the `acm2wav` tool (you can get it from No Mutants Allowed). 88 | 89 | ## FAQ 90 | 91 | - **Q:** Why TypeScript? Why a browser? 92 | 93 | A: Everyone has a browser: it's a portable platform for running code with more features than people expect. 94 | There are other projects that use native code already... and are already seeing segfaults. :) 95 | 96 | The project started out in JavaScript and was ported to TypeScript as it was continuing to grow. TypeScript strikes 97 | an excellent balance between useful and safe. 98 | 99 | - **Q:** But why Python? 100 | 101 | A: Python is actually quite fast when written well, despite many peoples' expectations. It is very elegant and allows me to write 102 | backend code like file parsers and exporters with tiny code, very few troubles, and that I know is portable and safe. 103 | 104 | - **Q:** Why do I need `acm2wav` for sound? 105 | 106 | A: Because it hasn't been ported to Python yet. If you're willing to contribute, give it a shot: the original Pascal source code is available online. 107 | 108 | Additionally, FFmpeg might be able to transcode ACM audio, so give that a shot. (See #30.) 109 | 110 | - **Q:** Why convert all assets up front, why not load them directly? 111 | 112 | A: Because it would require more processing time to load them each time they're needed rather than having them already in a sane, modern format. 113 | 114 | By converting, for example, FRMs (a proprietary Interplay format) to PNGs (a ubiquitous, open modern format) we allow normal browsers or image viewers to open them, as well as edit them -- a huge win for modders. Other games or tools could take advantage of the new formats as well. 115 | 116 | - **Q:** Why do this at all? 117 | 118 | A: Why not? It's a fun project, and I love Fallout. Fallout 1 and 2 do not run particularly well on modern machines, even with engine hacks. They're also hard to mod -- I'd like to change that. 119 | 120 | ## License 121 | 122 | DarkFO is licensed under the terms of the Apache 2 license. See `LICENSE.txt` for the full license text. 123 | 124 | ## Contributing 125 | 126 | Contributions are welcome! 127 | 128 | Testing is more than welcome: if you have issues running DarkFO, or if you find bugs, glitches, or other inaccuracies, please don't hesitate to file an issue on GitHub and/or contact the developers! 129 | 130 | To contribute code, simply submit a pull request with your changes. Take care to write sensible commit messages, and if you want to change major parts of the code, please discuss it with other developers first (see the Contact section below). 131 | I apologize in advance for any injury sustained while reading the code. :) 132 | 133 | 134 | Thanks! 135 | 136 | ## Contact 137 | 138 | If you have an issue, please file it in the GitHub issue tracker. 139 | -------------------------------------------------------------------------------- /colortest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /convertAudio.py: -------------------------------------------------------------------------------- 1 | import glob, os, subprocess 2 | 3 | def convertDir(inDir, outDir): 4 | for path in glob.glob(os.path.join(inDir, "*.ACM")): 5 | basename = os.path.splitext(os.path.basename(path))[0] 6 | result = basename.lower() + ".wav" # output path of acm2wav 7 | outpath = os.path.join(outDir, result) 8 | 9 | print(path) 10 | #print(basename) 11 | 12 | # convert to wav 13 | #os.system("acm2wav %s" % path) 14 | subprocess.call(["acm2wav", path], stdout=subprocess.PIPE) 15 | 16 | if not os.path.exists(result): 17 | print("result file (%s) not found!" % result) 18 | #break 19 | elif not os.path.exists(outpath): 20 | os.rename(result, outpath) 21 | 22 | def main(): 23 | if not os.path.exists("acm2wav.exe"): 24 | print("need acm2wav.exe") 25 | return 26 | if not os.path.exists("SFX"): 27 | print("need SFX/") 28 | return 29 | 30 | if not os.path.exists("audio"): 31 | os.mkdir("audio") 32 | 33 | if not os.path.exists("audio/sfx"): 34 | os.mkdir("audio/sfx") 35 | 36 | if not os.path.exists("audio/music"): 37 | os.mkdir("audio/music") 38 | 39 | convertDir("SFX", "audio/sfx") 40 | convertDir("sound/music", "audio/music") 41 | 42 | print("done!") 43 | 44 | if __name__ == '__main__': main() -------------------------------------------------------------------------------- /convertMaps.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from multiprocessing import Pool 3 | import os, glob 4 | import fomap 5 | 6 | N_PROCS = 2 7 | 8 | def convert(mapFile): 9 | try: 10 | mapName = os.path.splitext(os.path.basename(mapFile).lower())[0] 11 | print("converting %s (%s)..." % (mapName, mapFile)) 12 | fomap.exportMap("data", mapFile, outFile="maps2/" + mapName + ".json", verbose=False) 13 | except Exception as e: 14 | print("couldn't convert %s: %s" % (mapFile, str(e))) 15 | 16 | if __name__ == '__main__': 17 | if not os.path.exists("maps"): 18 | os.mkdir("maps") 19 | 20 | Pool(N_PROCS).map(convert, glob.glob("data/maps/*.MAP")) -------------------------------------------------------------------------------- /dat2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2015 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | from __future__ import print_function 18 | import sys, os, struct, zlib, collections 19 | 20 | # Fallout 2 .DAT (DAT2) file reader 21 | 22 | SEEK_END = 2 23 | 24 | File = collections.namedtuple('File', 'filename compressed unpackedSize packedSize offset') 25 | 26 | def read16(f): 27 | return struct.unpack(" bool) */ 35 | _canvasOffset: {x: 0, y: 0} /* offset of the canvas relative to the document */ 36 | }; 37 | 38 | heart.HeartImage = function(img) { 39 | this.img = img; 40 | }; 41 | 42 | heart.HeartImage.prototype.getWidth = function() { 43 | return this.img.width; 44 | }; 45 | 46 | heart.HeartImage.prototype.getHeight = function() { 47 | return this.img.height; 48 | }; 49 | 50 | heart.graphics = { 51 | rectangle: function(mode, x, y, w, h) { 52 | if(mode === "fill") 53 | heart.ctx.fillRect(x, y, w, h); 54 | else 55 | heart.ctx.strokeRect(x, y, w, h); 56 | }, 57 | 58 | circle: function(mode, x, y, radius) { 59 | heart.ctx.beginPath(); 60 | heart.ctx.arc(x, y, radius, 0, Math.PI*2, false); 61 | if(mode === "fill") 62 | heart.ctx.fill(); 63 | else 64 | heart.ctx.stroke(); 65 | }, 66 | 67 | line: function(x1, y1, x2, y2) { 68 | heart.ctx.beginPath(); 69 | heart.ctx.moveTo(x1, y1); 70 | heart.ctx.lineTo(x2, y2); 71 | heart.ctx.stroke(); 72 | }, 73 | 74 | polygon: function(mode, vertices) { 75 | if(vertices.length === undefined) 76 | vertices = Array.prototype.slice.call(arguments, 1); 77 | 78 | if(vertices.length <= 2) return; 79 | 80 | if(vertices.length % 2 !== 0) { 81 | throw "heart.graphics.polygon: number of vertices isn't even," + 82 | " meaning you don't have x,y pairs"; 83 | } 84 | 85 | heart.ctx.beginPath(); 86 | heart.ctx.moveTo(vertices[0], vertices[1]) 87 | for(var i = 2; i < vertices.length; i += 2) { 88 | heart.ctx.lineTo(vertices[i], vertices[i+1]); 89 | } 90 | 91 | if(mode === "fill") 92 | heart.ctx.fill(); 93 | else { 94 | heart.ctx.lineTo(vertices[0], vertices[1]); // close the polygon 95 | heart.ctx.stroke(); 96 | } 97 | }, 98 | 99 | print: function(text, x, y) { 100 | heart.ctx.fillText(text, x, y); 101 | }, 102 | 103 | setColor: function(r, g, b, a) { 104 | if(a === undefined) { 105 | heart.ctx.fillStyle = heart.ctx.strokeStyle = "rgb("+r+","+g+","+b+")"; 106 | } 107 | else { 108 | a = (a/255).toFixed(1); // input is in 0..255, output is in 0.0..1.0 109 | heart.ctx.fillStyle = heart.ctx.strokeStyle = "rgba("+r+","+g+","+b+","+a+")"; 110 | } 111 | }, 112 | 113 | getWidth: function() { 114 | return heart._size.w; 115 | }, 116 | 117 | getHeight: function() { 118 | return heart._size.h; 119 | }, 120 | 121 | getBackgroundColor: function() { 122 | return heart._bg; 123 | }, 124 | 125 | setBackgroundColor: function(r, g, b) { 126 | heart._bg = {r: r, g: g, b: b}; 127 | }, 128 | 129 | newImage: function(src, callback) { 130 | /* load an image */ 131 | /* XXX: does not handle errors */ 132 | var img = new Image(); 133 | heart._imagesLoading.push(img); 134 | img.onload = function() { 135 | heart._imagesLoading.splice(heart._imagesLoading.indexOf(img), 1); /* remove img from the loading sequence */ 136 | callback(new heart.HeartImage(img)); 137 | }; 138 | img.src = src; 139 | }, 140 | 141 | draw: function(drawable, x, y) { 142 | if(drawable.img !== undefined) { 143 | heart.ctx.drawImage(drawable.img, x, y); 144 | } 145 | }, 146 | 147 | translate: function(x, y) { 148 | heart.ctx.translate(x, y); 149 | }, 150 | 151 | rotate: function(angle) { 152 | heart.ctx.rotate(angle); 153 | }, 154 | 155 | push: function() { 156 | heart.ctx.save(); 157 | }, 158 | 159 | pop: function() { 160 | heart.ctx.restore(); 161 | } 162 | }; 163 | 164 | heart.timer = { 165 | getFPS: function() { 166 | return heart._fps; 167 | }, 168 | 169 | getTargetFPS: function() { 170 | return heart._targetFPS; 171 | }, 172 | 173 | setTargetFPS: function(fps) { 174 | heart._targetFPS = fps; 175 | heart._targetTickTime = 1000 / heart._targetFPS; 176 | }, 177 | 178 | getTime: function() { 179 | return window.performance.now(); 180 | } 181 | }; 182 | 183 | heart.keyboard = { 184 | isDown: function(key) { 185 | return heart._keysDown[key]; 186 | }, 187 | 188 | isUp: function(key) { 189 | return !heart.keyboard.isDown(key); 190 | } 191 | }; 192 | 193 | heart.mouse = { 194 | _pos: {x: 0, y: 0}, 195 | _btnState: {"l": false, "r": false}, /* left and right button press state */ 196 | 197 | getPosition: function() { 198 | return [heart.mouse._pos.x, heart.mouse._pos.y]; 199 | }, 200 | 201 | getX: function() { 202 | return heart.mouse._pos.x; 203 | }, 204 | 205 | getY: function() { 206 | return heart.mouse._pos.y; 207 | }, 208 | 209 | isDown: function(button) { 210 | return heart.mouse._btnState[button] !== undefined ? heart.mouse._btnState[button] : false; 211 | } 212 | }; 213 | 214 | heart._init = function() { 215 | /* if we're waiting on images to load, spinlock */ 216 | if(heart._imagesLoading.length !== 0) { 217 | setTimeout(heart._init, 30 /* ms */); 218 | return; 219 | } 220 | 221 | if(heart.load !== undefined) 222 | heart.load(); 223 | if(heart.canvas === undefined || heart.ctx === undefined) 224 | alert("no canvas"); 225 | 226 | var rect = heart.canvas.getBoundingClientRect() 227 | heart._canvasOffset.x = rect.left 228 | heart._canvasOffset.y = rect.top 229 | 230 | /* register for mouse-related events (pertaining to the canvas) */ 231 | heart.canvas.onmousedown = function(e) { 232 | var btn = heart._mouseButtonName(e.which); 233 | heart.mouse._btnState[btn] = true; 234 | if(heart.mousepressed) 235 | heart.mousepressed(e.pageX, e.pageY, btn); 236 | }; 237 | 238 | heart.canvas.onmouseup = function(e) { 239 | var btn = heart._mouseButtonName(e.which); 240 | heart.mouse._btnState[btn] = false; 241 | if(heart.mousereleased) 242 | heart.mousereleased(e.pageX, e.pageY, btn); 243 | }; 244 | 245 | heart.canvas.onmousemove = function(e) { 246 | heart.mouse._pos = {x: e.pageX - heart._canvasOffset.x, y: e.pageY - heart._canvasOffset.y}; 247 | if(heart.mousemoved) 248 | heart.mousemoved(e.pageX, e.pageY); 249 | }; 250 | 251 | /* keypressed and keyreleased are aliases to 252 | keydown and keyup, respectively. */ 253 | if(heart.keydown === undefined) 254 | heart.keydown = heart.keypressed; 255 | if(heart.keyup === undefined) 256 | heart.keyup = heart.keyreleased; 257 | 258 | heart._lastTick = window.performance.now(); 259 | heart.timer.setTargetFPS(heart._targetFPS); 260 | 261 | heart._tick(heart._lastTick); /* first tick */ 262 | }; 263 | 264 | heart._tick = function(time) { 265 | heart._dt = time - heart._lastTick; 266 | heart._lastTick = time; 267 | heart._frameAccum += heart._dt; 268 | 269 | if(heart._frameAccum >= heart._targetTickTime) { 270 | heart._frameAccum -= heart._targetTickTime; 271 | heart._numFrames++; 272 | heart._frameAccum = Math.min(heart._frameAccum, heart._targetTickTime); 273 | 274 | var deltaFPSTime = time - heart._lastFPSTime; 275 | if(deltaFPSTime >= 1000) { 276 | heart._fps = heart._numFrames / deltaFPSTime * 1000 | 0; 277 | heart._lastFPSTime = time; 278 | heart._numFrames = 0; 279 | } 280 | 281 | if(heart.update) 282 | heart.update(heart._dt / 1000); 283 | 284 | if(heart._bg) { 285 | heart.ctx.fillStyle = "rgb("+heart._bg.r+","+heart._bg.g+","+heart._bg.b+")"; 286 | heart.ctx.fillRect(0, 0, heart._size.w, heart._size.h); 287 | } 288 | 289 | if(heart.draw) 290 | heart.draw(); 291 | } 292 | 293 | window.requestAnimationFrame(heart._tick); 294 | }; 295 | 296 | heart.attach = function(canvas) { 297 | var el = document.getElementById(canvas); 298 | if(!el) 299 | return false; 300 | heart.canvas = el; 301 | heart.ctx = heart.canvas.getContext("2d"); 302 | if(!heart.ctx) 303 | alert("couldn't get canvas context") 304 | }; 305 | 306 | heart._mouseButtonName = function(n) { 307 | switch(n) { 308 | case 1: return "l"; 309 | case 2: return "m"; 310 | case 3: return "r"; 311 | } 312 | 313 | return "unknown"; 314 | }; 315 | 316 | heart._getKeyChar = function(c) { 317 | /* supply a hacky keymap */ 318 | switch(c) { 319 | /* arrow keys */ 320 | case 38: return "up"; 321 | case 37: return "left"; 322 | case 39: return "right"; 323 | case 40: return "down"; 324 | case 27: return "escape"; 325 | case 13: return "return"; 326 | } 327 | 328 | return String.fromCharCode(c).toLowerCase(); 329 | }; 330 | 331 | // XXX: we need a keymap, since browsers decide on being annoying and 332 | // not having a consistent keymap. (also, this won't work with special characters.) 333 | window.onkeydown = function(e) { 334 | var c = heart._getKeyChar(e.keyCode); 335 | heart._keysDown[c] = true; 336 | if(heart.keydown !== undefined) 337 | heart.keydown(c); 338 | }; 339 | 340 | window.onkeyup = function(e) { 341 | var c = heart._getKeyChar(e.keyCode); 342 | heart._keysDown[c] = false; 343 | if(heart.keyup !== undefined) 344 | heart.keyup(c); 345 | }; 346 | 347 | window.onfocus = function(e) { 348 | if (heart.focus) heart.focus(true); 349 | } 350 | window.onblur = function(e) { 351 | if (heart.focus) heart.focus(false); 352 | } 353 | 354 | window.onbeforeunload = function(e) { 355 | if (heart.quit) { 356 | var ret = heart.quit(); 357 | if (ret) return ret; 358 | } 359 | } 360 | 361 | window.onload = function() { 362 | if(heart.preload !== undefined) 363 | heart.preload(); 364 | heart._init(); 365 | }; 366 | 367 | return heart; 368 | }); 369 | -------------------------------------------------------------------------------- /lut/color_lut.json: -------------------------------------------------------------------------------- 1 | {"0": 255, "3418112": 67, "4732932": 66, "16515072": 133, "11564108": 173, "5785608": 65, "14997516": 58, "16560304": 127, "7100432": 64, "5799000": 87, "6568980": 124, "1320984": 94, "8153112": 63, "531460": 98, "15255704": 170, "13416476": 59, "5256196": 166, "14680064": 134, "2635808": 75, "9474224": 37, "6316068": 82, "8668160": 152, "3416064": 168, "12098600": 60, "2111580": 109, "13676720": 19, "4992044": 28, "1842204": 202, "10782768": 61, "5779464": 125, "3673140": 52, "7875608": 177, "3684408": 13, "7898232": 210, "2368572": 45, "5259300": 225, "4220992": 88, "8675424": 24, "6833220": 26, "1052704": 47, "4985928": 51, "1057804": 97, "2894924": 44, "7364688": 76, "16573624": 169, "5526612": 11, "1585176": 96, "3684440": 43, "16540772": 130, "7903324": 72, "2360356": 54, "6825056": 50, "2101264": 31, "6579300": 10, "265216": 100, "4473960": 42, "16307388": 183, "12882020": 172, "2638956": 108, "2651160": 219, "4727836": 194, "10008688": 71, "3688552": 113, "7086096": 178, "7631988": 9, "14211308": 33, "10253364": 161, "4732968": 206, "7902328": 86, "16549908": 147, "16579708": 57, "16562296": 143, "7358500": 123, "8421504": 8, "9741460": 212, "6316164": 40, "10768384": 151, "11034688": 190, "15769708": 186, "4222092": 105, "3418140": 226, "9474192": 7, "2889752": 30, "7368852": 39, "7880752": 192, "7902360": 102, "5793888": 213, "16554136": 128, "14220444": 69, "10526880": 6, "9728112": 23, "8421540": 38, "267264": 99, "12869800": 49, "16559200": 144, "16533576": 131, "11579568": 5, "16572616": 141, "7107692": 209, "4737096": 12, "12369084": 117, "1349652": 197, "7359508": 164, "2625572": 53, "10526912": 36, "2105376": 15, "5793924": 115, "14468292": 18, "2103296": 68, "16579784": 56, "16567500": 126, "5274768": 104, "16040096": 184, "13421772": 3, "11579600": 35, "5526648": 41, "15522008": 17, "3941412": 29, "12895452": 34, "4721668": 180, "8149044": 122, "6612068": 196, "1056784": 95, "2903164": 107, "15527148": 1, "4998180": 83, "5523508": 77, "3947580": 208, "16560368": 48, "2631720": 14, "6307848": 165, "5263432": 199, "15527164": 32, "3183636": 218, "11305028": 160, "10780800": 22, "15773828": 185, "12357716": 159, "12607488": 150, "3156000": 204, "1579052": 46, "9203808": 205, "8939596": 121, "7602176": 138, "2112556": 92, "10522748": 119, "16574684": 182, "9457720": 191, "3692632": 89, "11851908": 70, "27648": 200, "4194304": 140, "3676180": 195, "12845056": 135, "16527408": 132, "6838332": 224, "4989952": 154, "14474460": 2, "8950920": 211, "9215132": 101, "1838104": 55, "4216884": 74, "4204544": 167, "3682336": 84, "9993296": 223, "3168396": 106, "5767168": 139, "5781560": 27, "6303780": 193, "16546940": 129, "16555080": 145, "8149020": 163, "11571344": 21, "14445568": 149, "3686480": 116, "2638908": 91, "15766620": 187, "10509368": 174, "6588564": 103, "1583168": 111, "16565396": 142, "3453968": 217, "7370784": 80, "6836280": 203, "16575724": 16, "5265496": 114, "11314328": 118, "6034440": 179, "14200952": 157, "6829056": 153, "8666144": 176, "10267804": 85, "1576972": 227, "6060104": 73, "6320232": 214, "13678728": 222, "9437184": 137, "1847332": 93, "3933184": 181, "12624032": 20, "3724296": 216, "3995648": 215, "1847372": 110, "9731168": 120, "15789264": 221, "789516": 207, "16553004": 146, "9200680": 162, "10265764": 112, "3165256": 90, "9456684": 175, "14188628": 188, "6846544": 79, "9211012": 201, "16307364": 156, "3151872": 155, "16546816": 148, "41984": 198, "16579836": 220, "11010048": 136, "13148260": 158, "13937788": 171, "7366696": 81, "12611656": 189, "7885908": 25, "9467940": 62} -------------------------------------------------------------------------------- /lut/color_rgb.json: -------------------------------------------------------------------------------- 1 | {"0": [0, 0, 0], "1": [236, 236, 236], "2": [220, 220, 220], "3": [204, 204, 204], "4": [188, 188, 188], "5": [176, 176, 176], "6": [160, 160, 160], "7": [144, 144, 144], "8": [128, 128, 128], "9": [116, 116, 116], "10": [100, 100, 100], "11": [84, 84, 84], "12": [72, 72, 72], "13": [56, 56, 56], "14": [40, 40, 40], "15": [32, 32, 32], "16": [252, 236, 236], "17": [236, 216, 216], "18": [220, 196, 196], "19": [208, 176, 176], "20": [192, 160, 160], "21": [176, 144, 144], "22": [164, 128, 128], "23": [148, 112, 112], "24": [132, 96, 96], "25": [120, 84, 84], "26": [104, 68, 68], "27": [88, 56, 56], "28": [76, 44, 44], "29": [60, 36, 36], "30": [44, 24, 24], "31": [32, 16, 16], "32": [236, 236, 252], "33": [216, 216, 236], "34": [196, 196, 220], "35": [176, 176, 208], "36": [160, 160, 192], "37": [144, 144, 176], "38": [128, 128, 164], "39": [112, 112, 148], "40": [96, 96, 132], "41": [84, 84, 120], "42": [68, 68, 104], "43": [56, 56, 88], "44": [44, 44, 76], "45": [36, 36, 60], "46": [24, 24, 44], "47": [16, 16, 32], "48": [252, 176, 240], "49": [196, 96, 168], "50": [104, 36, 96], "51": [76, 20, 72], "52": [56, 12, 52], "53": [40, 16, 36], "54": [36, 4, 36], "55": [28, 12, 24], "56": [252, 252, 200], "57": [252, 252, 124], "58": [228, 216, 12], "59": [204, 184, 28], "60": [184, 156, 40], "61": [164, 136, 48], "62": [144, 120, 36], "63": [124, 104, 24], "64": [108, 88, 16], "65": [88, 72, 8], "66": [72, 56, 4], "67": [52, 40, 0], "68": [32, 24, 0], "69": [216, 252, 156], "70": [180, 216, 132], "71": [152, 184, 112], "72": [120, 152, 92], "73": [92, 120, 72], "74": [64, 88, 52], "75": [40, 56, 32], "76": [112, 96, 80], "77": [84, 72, 52], "78": [56, 48, 32], "79": [104, 120, 80], "80": [112, 120, 32], "81": [112, 104, 40], "82": [96, 96, 36], "83": [76, 68, 36], "84": [56, 48, 32], "85": [156, 172, 156], "86": [120, 148, 120], "87": [88, 124, 88], "88": [64, 104, 64], "89": [56, 88, 88], "90": [48, 76, 72], "91": [40, 68, 60], "92": [32, 60, 44], "93": [28, 48, 36], "94": [20, 40, 24], "95": [16, 32, 16], "96": [24, 48, 24], "97": [16, 36, 12], "98": [8, 28, 4], "99": [4, 20, 0], "100": [4, 12, 0], "101": [140, 156, 156], "102": [120, 148, 152], "103": [100, 136, 148], "104": [80, 124, 144], "105": [64, 108, 140], "106": [48, 88, 140], "107": [44, 76, 124], "108": [40, 68, 108], "109": [32, 56, 92], "110": [28, 48, 76], "111": [24, 40, 64], "112": [156, 164, 164], "113": [56, 72, 104], "114": [80, 88, 88], "115": [88, 104, 132], "116": [56, 64, 80], "117": [188, 188, 188], "118": [172, 164, 152], "119": [160, 144, 124], "120": [148, 124, 96], "121": [136, 104, 76], "122": [124, 88, 52], "123": [112, 72, 36], "124": [100, 60, 20], "125": [88, 48, 8], "126": [252, 204, 204], "127": [252, 176, 176], "128": [252, 152, 152], "129": [252, 124, 124], "130": [252, 100, 100], "131": [252, 72, 72], "132": [252, 48, 48], "133": [252, 0, 0], "134": [224, 0, 0], "135": [196, 0, 0], "136": [168, 0, 0], "137": [144, 0, 0], "138": [116, 0, 0], "139": [88, 0, 0], "140": [64, 0, 0], "141": [252, 224, 200], "142": [252, 196, 148], "143": [252, 184, 120], "144": [252, 172, 96], "145": [252, 156, 72], "146": [252, 148, 44], "147": [252, 136, 20], "148": [252, 124, 0], "149": [220, 108, 0], "150": [192, 96, 0], "151": [164, 80, 0], "152": [132, 68, 0], "153": [104, 52, 0], "154": [76, 36, 0], "155": [48, 24, 0], "156": [248, 212, 164], "157": [216, 176, 120], "158": [200, 160, 100], "159": [188, 144, 84], "160": [172, 128, 68], "161": [156, 116, 52], "162": [140, 100, 40], "163": [124, 88, 28], "164": [112, 76, 20], "165": [96, 64, 8], "166": [80, 52, 4], "167": [64, 40, 0], "168": [52, 32, 0], "169": [252, 228, 184], "170": [232, 200, 152], "171": [212, 172, 124], "172": [196, 144, 100], "173": [176, 116, 76], "174": [160, 92, 56], "175": [144, 76, 44], "176": [132, 60, 32], "177": [120, 44, 24], "178": [108, 32, 16], "179": [92, 20, 8], "180": [72, 12, 4], "181": [60, 4, 0], "182": [252, 232, 220], "183": [248, 212, 188], "184": [244, 192, 160], "185": [240, 176, 132], "186": [240, 160, 108], "187": [240, 148, 92], "188": [216, 128, 84], "189": [192, 112, 72], "190": [168, 96, 64], "191": [144, 80, 56], "192": [120, 64, 48], "193": [96, 48, 36], "194": [72, 36, 28], "195": [56, 24, 20], "196": [100, 228, 100], "197": [20, 152, 20], "198": [0, 164, 0], "199": [80, 80, 72], "200": [0, 108, 0], "201": [140, 140, 132], "202": [28, 28, 28], "203": [104, 80, 56], "204": [48, 40, 32], "205": [140, 112, 96], "206": [72, 56, 40], "207": [12, 12, 12], "208": [60, 60, 60], "209": [108, 116, 108], "210": [120, 132, 120], "211": [136, 148, 136], "212": [148, 164, 148], "213": [88, 104, 96], "214": [96, 112, 104], "215": [60, 248, 0], "216": [56, 212, 8], "217": [52, 180, 16], "218": [48, 148, 20], "219": [40, 116, 24], "220": [252, 252, 252], "221": [240, 236, 208], "222": [208, 184, 136], "223": [152, 124, 80], "224": [104, 88, 60], "225": [80, 64, 36], "226": [52, 40, 28], "227": [24, 16, 12], "228": [0, 0, 0], "229": [0, 0, 0], "230": [0, 0, 0], "231": [0, 0, 0], "232": [0, 0, 0], "233": [0, 0, 0], "234": [0, 0, 0], "235": [0, 0, 0], "236": [0, 0, 0], "237": [0, 0, 0], "238": [0, 0, 0], "239": [0, 0, 0], "240": [0, 0, 0], "241": [0, 0, 0], "242": [0, 0, 0], "243": [0, 0, 0], "244": [0, 0, 0], "245": [0, 0, 0], "246": [0, 0, 0], "247": [0, 0, 0], "248": [0, 0, 0], "249": [0, 0, 0], "250": [0, 0, 0], "251": [0, 0, 0], "252": [0, 0, 0], "253": [0, 0, 0], "254": [0, 0, 0], "255": [0, 0, 0]} -------------------------------------------------------------------------------- /lut/elevators.json: -------------------------------------------------------------------------------- 1 | {"elevators": [{"buttons": [{"tileNum": 18940, "mapID": 14, "level": 0}, {"tileNum": 18936, "mapID": 14, "level": 1}, {"tileNum": 21340, "mapID": 15, "level": 0}, {"tileNum": 21340, "mapID": 15, "level": 1}], "buttonCount": 4, "labels": -1, "type": 143}, {"buttons": [{"tileNum": 20502, "mapID": 13, "level": 0}, {"tileNum": 14912, "mapID": 14, "level": 0}], "buttonCount": 2, "labels": 150, "type": 143}, {"buttons": [{"tileNum": 12498, "mapID": 33, "level": 0}, {"tileNum": 20094, "mapID": 33, "level": 1}, {"tileNum": 17312, "mapID": 34, "level": 0}], "buttonCount": 3, "labels": -1, "type": 144}, {"buttons": [{"tileNum": 16140, "mapID": 34, "level": 0}, {"tileNum": 16140, "mapID": 34, "level": 1}], "buttonCount": 2, "labels": 145, "type": 144}, {"buttons": [{"tileNum": 14920, "mapID": 49, "level": 0}, {"tileNum": 15120, "mapID": 49, "level": 1}, {"tileNum": 12944, "mapID": 50, "level": 0}], "buttonCount": 3, "labels": -1, "type": 146}, {"buttons": [{"tileNum": 24520, "mapID": 50, "level": 0}, {"tileNum": 25316, "mapID": 50, "level": 1}], "buttonCount": 2, "labels": 147, "type": 146}, {"buttons": [{"tileNum": 22526, "mapID": 42, "level": 0}, {"tileNum": 22526, "mapID": 42, "level": 1}, {"tileNum": 22526, "mapID": 42, "level": 2}], "buttonCount": 3, "labels": -1, "type": 146}, {"buttons": [{"tileNum": 14086, "mapID": 42, "level": 2}, {"tileNum": 14086, "mapID": 43, "level": 0}, {"tileNum": 14086, "mapID": 43, "level": 2}], "buttonCount": 3, "labels": 151, "type": 146}, {"buttons": [{"tileNum": 14104, "mapID": 40, "level": 0}, {"tileNum": 22504, "mapID": 40, "level": 1}, {"tileNum": 17312, "mapID": 40, "level": 2}], "buttonCount": 3, "labels": -1, "type": 148}, {"buttons": [{"tileNum": 13704, "mapID": 9, "level": 0}, {"tileNum": 23302, "mapID": 9, "level": 1}, {"tileNum": 17308, "mapID": 9, "level": 2}], "buttonCount": 3, "labels": -1, "type": 146}, {"buttons": [{"tileNum": 19300, "mapID": 28, "level": 0}, {"tileNum": 19300, "mapID": 28, "level": 1}, {"tileNum": 20110, "mapID": 28, "level": 2}], "buttonCount": 3, "labels": -1, "type": 146}, {"buttons": [{"tileNum": 20118, "mapID": 28, "level": 2}, {"tileNum": 21710, "mapID": 29, "level": 0}], "buttonCount": 2, "labels": 147, "type": 146}, {"buttons": [{"tileNum": 20122, "mapID": 28, "level": 0}, {"tileNum": 20124, "mapID": 28, "level": 1}, {"tileNum": 20940, "mapID": 28, "level": 2}, {"tileNum": 22540, "mapID": 29, "level": 0}], "buttonCount": 4, "labels": -1, "type": 388}, {"buttons": [{"tileNum": 16052, "mapID": 12, "level": 1}, {"tileNum": 14480, "mapID": 12, "level": 2}], "buttonCount": 2, "labels": 150, "type": 143}, {"buttons": [{"tileNum": 14104, "mapID": 6, "level": 0}, {"tileNum": 22504, "mapID": 6, "level": 1}, {"tileNum": 17312, "mapID": 6, "level": 2}], "buttonCount": 3, "labels": -1, "type": 148}, {"buttons": [{"tileNum": 14104, "mapID": 30, "level": 0}, {"tileNum": 22504, "mapID": 30, "level": 1}, {"tileNum": 17312, "mapID": 30, "level": 2}], "buttonCount": 3, "labels": -1, "type": 148}, {"buttons": [{"tileNum": 13704, "mapID": 36, "level": 0}, {"tileNum": 23302, "mapID": 36, "level": 1}, {"tileNum": 17308, "mapID": 36, "level": 2}], "buttonCount": 3, "labels": -1, "type": 148}, {"buttons": [{"tileNum": 17285, "mapID": 39, "level": 0}, {"tileNum": 19472, "mapID": 36, "level": 0}], "buttonCount": 2, "labels": 150, "type": 143}, {"buttons": [{"tileNum": 10701, "mapID": 109, "level": 2}, {"tileNum": 10705, "mapID": 109, "level": 1}], "buttonCount": 2, "labels": 150, "type": 143}, {"buttons": [{"tileNum": 14697, "mapID": 109, "level": 2}, {"tileNum": 15099, "mapID": 109, "level": 1}], "buttonCount": 2, "labels": 150, "type": 143}, {"buttons": [{"tileNum": 23877, "mapID": 109, "level": 2}, {"tileNum": 25481, "mapID": 109, "level": 1}], "buttonCount": 2, "labels": 150, "type": 143}, {"buttons": [{"tileNum": 26130, "mapID": 109, "level": 2}, {"tileNum": 24721, "mapID": 109, "level": 1}], "buttonCount": 2, "labels": 150, "type": 143}, {"buttons": [{"tileNum": 23953, "mapID": 137, "level": 0}, {"tileNum": 16526, "mapID": 148, "level": 1}], "buttonCount": 2, "labels": 150, "type": 143}, {"buttons": [{"tileNum": 13901, "mapID": 62, "level": 0}, {"tileNum": 17923, "mapID": 63, "level": 1}], "buttonCount": 2, "labels": 150, "type": 143}], "buttonDown": 142, "buttonUp": 141, "positioner": 149} -------------------------------------------------------------------------------- /maps/nullmap.images.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /maps/nullmap.json: -------------------------------------------------------------------------------- 1 | { 2 | "startPosition": { 3 | "y": 0, 4 | "x": 0 5 | }, 6 | "startOrientation": 0, 7 | "levels": [ 8 | { 9 | "tiles": { 10 | "roof": [], 11 | "floor": [] 12 | }, 13 | "objects": [], 14 | "spatials": [] 15 | } 16 | ], 17 | "mapID": -1, 18 | "startElevation": 0 19 | } 20 | -------------------------------------------------------------------------------- /mpserv.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2017 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | # WebSocket server for DarkFO's multiplayer mode 18 | 19 | from eventlet import wsgi, websocket 20 | import eventlet 21 | import json, zlib, time, string, os 22 | import signal 23 | 24 | # For ^C on Windows 25 | signal.signal(signal.SIGINT, signal.SIG_DFL) 26 | 27 | def is_valid_name_char(c): 28 | return c in string.ascii_letters + string.digits + "-_" 29 | 30 | class GameContext: 31 | def __init__(self): 32 | self.host = None 33 | self.guest = None 34 | self.serializedMap = None 35 | self.elevation = None 36 | self.lastUID = 0 37 | 38 | def new_uid(self): 39 | uid = self.lastUID 40 | self.lastUID += 1 41 | return uid 42 | 43 | context = GameContext() 44 | 45 | # Connection handler 46 | class Connection: 47 | def __init__(self, ws): 48 | self.sock = ws 49 | self.is_host = None 50 | self.uid = None 51 | self.name = None 52 | self.pos = None 53 | self.orientation = 0 54 | 55 | def _send(self, msg): 56 | self.sock.send(json.dumps(msg)) 57 | 58 | def send(self, t, msg): 59 | msg.update({"t": t}) 60 | self._send(msg) 61 | 62 | def _recv(self): 63 | msg = self.sock.wait() 64 | if type(msg) is bytes: 65 | return msg 66 | return json.loads(msg) 67 | 68 | def recv(self): 69 | data = self.sock.wait() 70 | if data is None: 71 | raise EOFError() 72 | if type(data) is bytes: 73 | return None, data 74 | msg = json.loads(data) 75 | return msg["t"], msg 76 | 77 | def disconnected(self, reason=""): 78 | print("client", self.name, "disconnected:", reason) 79 | # TODO: Broadcast drop out 80 | 81 | def error(self, request, msg): 82 | self.send("error", {"request": request, "message": msg}) 83 | 84 | def sendMap(self): 85 | global context 86 | 87 | print("Sending map") 88 | 89 | self.pos = context.host.pos.copy() 90 | self.pos["x"] += 2 91 | 92 | # First send the map so the client has it in its buffer 93 | self.sock.send(context.serializedMap) 94 | 95 | # Then send the map change notification 96 | self.send("map", { 97 | "player": {"position": self.pos, "elevation": context.elevation, "uid": self.uid}, 98 | "hostPlayer": {"position": context.host.pos, "uid": context.host.uid, "name": context.host.name, "orientation": context.host.orientation} 99 | }) 100 | 101 | self.moved() 102 | 103 | def moved(self): 104 | # Relay movement to the other party 105 | target = context.host 106 | if self.is_host: 107 | target = context.guest 108 | 109 | if target: 110 | target.send("movePlayer", { "uid": self.uid, "position": self.pos }) 111 | 112 | def target(self): 113 | if self.is_host: 114 | return context.guest 115 | return context.host 116 | 117 | def relay(self, msg): 118 | target = self.target() 119 | if target: 120 | target.send(msg["t"], msg) 121 | 122 | def serve(self): 123 | global context 124 | 125 | self.send("hello", {"network": {"name": "test server"}}) 126 | 127 | try: 128 | while True: 129 | t, msg = self.recv() 130 | print("Received %s message from %r" % (t, self.name)) 131 | 132 | if t is None: 133 | print("Received binary/compressed data -- assuming it's a map") 134 | 135 | # We don't decompress it as we don't need the actual map data -- we can pass it along 136 | # compressed to the guest clients as well. 137 | context.serializedMap = msg 138 | 139 | elif t == "ident": 140 | self.name = msg["name"] 141 | print("Client identified as", msg["name"]) 142 | 143 | elif t == "changeMap": 144 | context.elevation = msg["player"]["elevation"] 145 | self.pos = msg["player"]["position"] 146 | self.orientation = msg["player"]["orientation"] 147 | 148 | print("Map changed to", msg["mapName"]) 149 | 150 | # Notify guest of map change and send map 151 | if context.guest: 152 | print("Notifying guest") 153 | context.guest.sendMap() 154 | 155 | elif t == "changeElevation": 156 | print("Elevation changed") 157 | 158 | context.elevation = msg["elevation"] 159 | self.pos = msg["position"] 160 | self.orientation = msg["orientation"] 161 | 162 | # Notify guest 163 | if context.guest: 164 | context.guest.send("elevationChanged", { "elevation": context.elevation }) 165 | 166 | self.moved() 167 | 168 | context.guest.pos = self.pos.copy() 169 | context.guest.pos["x"] += 2 170 | context.guest.moved() 171 | 172 | 173 | elif t == "host": 174 | context.host = self 175 | self.is_host = True 176 | self.uid = context.new_uid() 177 | 178 | print("Got a host:", self.name) 179 | 180 | elif t == "join": 181 | context.guest = self 182 | print("Got a guest:", self.name) 183 | 184 | self.is_host = False 185 | self.uid = context.new_uid() 186 | 187 | self.sendMap() 188 | 189 | print("Notifying host") 190 | context.host.send("guestJoined", { 191 | "name": self.name, 192 | "uid": self.uid, 193 | "position": self.pos, 194 | "orientation": self.orientation 195 | }) 196 | 197 | elif t == "moved": 198 | print("%s moved" % ("host" if self.is_host else "guest")) 199 | 200 | self.pos["x"] = msg["x"] 201 | self.pos["y"] = msg["y"] 202 | self.moved() 203 | 204 | elif t in ("objSetOpen", "objMove"): 205 | self.relay(msg) 206 | 207 | elif t == "close": 208 | self.disconnected("close message received") 209 | break 210 | except (EOFError, OSError): 211 | self.disconnected("socket closed") 212 | 213 | @websocket.WebSocketWSGI 214 | def connection(ws): 215 | con = Connection(ws) 216 | con.serve() 217 | 218 | if __name__ == "__main__": 219 | wsgi.server(eventlet.listen(('', 8090)), connection) -------------------------------------------------------------------------------- /pal.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2014-2015 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | # Fallout 2 .PAL palette parser 18 | 19 | import sys, os, struct 20 | 21 | def readPAL(f): 22 | palette = [None]*256 23 | 24 | for i in range(256): 25 | r,g,b = ord(f.read(1)), ord(f.read(1)), ord(f.read(1)) 26 | 27 | if r <= 63 and g <= 63 and b <= 63: # valid palette colors are 0..63 28 | r *= 4 29 | g *= 4 30 | b *= 4 31 | else: 32 | r = 0 33 | g = 0 34 | b = 0 35 | 36 | palette[i] = (r, g, b) 37 | 38 | return palette 39 | 40 | def readColorTable(f): 41 | f.seek(256*3) 42 | return map(ord, f.read(0x8000)) 43 | 44 | def main(): 45 | if len(sys.argv) != 2: 46 | print "USAGE: %s PAL" % sys.argv[0] 47 | sys.exit(1) 48 | 49 | with open(sys.argv[1], "rb") as f: 50 | print readPAL(f) 51 | 52 | if __name__ == '__main__': 53 | main() -------------------------------------------------------------------------------- /parseCritTable.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2014 darkf, Stratege 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | # Parser for Critical Hit tables from the Fallout 2 v1.02 .exe file 18 | 19 | import sys, os, struct, json 20 | 21 | areaName = {0: "head", 1: "leftArm", 2: "rightArm", 3: "torso", 4: "rightLeg", 22 | 5: "leftLeg", 6: "eyes", 7: "groin",8: "uncalled"} 23 | effectName = {0: "knockout", 1: "knockdown", 2: "crippledLeftLeg", 24 | 3: "crippledRightLeg", 4: "crippledLeftArm", 5: "crippledRightArm", 25 | 6: "blinded", 7: "death", 8: "onFire", 9: "bypassArmor", 10: "droppedWeapon", 26 | 11: "loseNextTurn", 12: "random"} 27 | 28 | def read32(f): 29 | return struct.unpack(" btnCount-1: 63 | # ignore unused buttons 64 | read32(f); read32(f); read32(f) 65 | else: 66 | elevators[i]['buttons'][btn]['mapID'] = read32(f) 67 | elevators[i]['buttons'][btn]['level'] = read32(f) 68 | elevators[i]['buttons'][btn]['tileNum'] = read32(f) 69 | 70 | if verbose: 71 | for i in range(NUM_ELEVATORS): 72 | print "elevator", i 73 | print " type:", elevators[i]['type'] 74 | if elevators[i]['labels'] != -1: 75 | print " labels:", elevators[i]['labels'] 76 | print " num buttons:", elevators[i]['buttonCount'] 77 | 78 | print " buttons:" 79 | for btn in elevators[i]['buttons']: 80 | print " -> map %d, level %d, tile %d" % (btn['mapID'], btn['level'], btn['tileNum']) 81 | 82 | return out 83 | 84 | def main(): 85 | if len(sys.argv) < 2: 86 | print "USAGE: %s fallout2.exe" % sys.argv[0] 87 | sys.exit(1) 88 | 89 | with open(sys.argv[1], "rb") as f: 90 | elevators = parseElevators(f, verbose=True) 91 | with open("lut/elevators.json", "w") as g: 92 | json.dump(elevators, g) 93 | 94 | print "done" 95 | 96 | if __name__ == '__main__': 97 | main() -------------------------------------------------------------------------------- /play.html: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | DarkFO 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 106 | 107 | 108 | 125 | 126 | 127 | 182 | 183 | 184 |
185 | your browser doesn't support <canvas> 186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 | 195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 | 207 |
208 |
209 |
210 |
211 | 212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 | 222 | 223 |
224 |
225 |
226 |
227 |
228 |
229 | 230 |
231 |
232 | 233 | 234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 | 242 |
243 |
244 |
245 |
246 |
247 |
248 | 249 |
250 |
251 |
252 |
253 |
254 |
255 | 256 |
257 | 258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 | 270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 | 288 |
the head
289 |
the eyes
290 |
the left arm
291 |
the right arm
292 |
the left leg
293 |
the right leg
294 |
the groin
295 |
the torso
296 | 297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 | 308 | -------------------------------------------------------------------------------- /proto.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2014 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | # Parser/converter for Fallout 1 and 2 .PRO files to a JSON format 18 | 19 | # Fallout 1 mode 20 | FO1 = True 21 | 22 | import sys, os, struct, json 23 | 24 | def read16(f): 25 | return struct.unpack("!h", f.read(2))[0] 26 | 27 | def read32(f): 28 | return struct.unpack("!l", f.read(4))[0] 29 | 30 | def read16At(buf, idx): 31 | return struct.unpack('!h', buf[idx:idx + 2])[0] 32 | 33 | def read32At(buf, idx): 34 | return struct.unpack('!l', buf[idx:idx + 4])[0] 35 | 36 | TYPE_ITEM = 0 37 | TYPE_CRITTER = 1 38 | TYPE_SCENERY = 2 39 | TYPE_WALL = 3 40 | TYPE_TILE = 4 41 | TYPE_MISC = 5 42 | 43 | SUBTYPE_ARMOR = 0 44 | SUBTYPE_CONTAINER = 1 45 | SUBTYPE_DRUG = 2 46 | SUBTYPE_WEAPON = 3 47 | SUBTYPE_AMMO = 4 48 | SUBTYPE_MISC = 5 49 | SUBTYPE_KEY = 6 50 | 51 | SCENERY_DOOR = 0 52 | SCENERY_STAIRS = 1 53 | SCENERY_ELEVATOR = 2 54 | SCENERY_LADDER_BOTTOM = 3 55 | SCENERY_LADDER_TOP = 4 56 | SCENERY_GENERIC = 5 57 | 58 | def readScenery(f): 59 | obj = {} 60 | 61 | obj["wallLightTypeFlags"] = read16(f) 62 | obj["actionFlags"] = read16(f) 63 | obj["scriptPID"] = read32(f) 64 | obj["subType"] = read32(f) 65 | obj["materialID"] = read32(f) 66 | obj["soundID"] = ord(f.read(1)) 67 | 68 | if obj["subType"] == SCENERY_DOOR: 69 | obj["walkthroughFlag"] = read32(f) 70 | # 4-byte unknown 71 | elif obj["subType"] == SCENERY_STAIRS: 72 | obj["destination"] = read32(f) 73 | obj["destinationMap"] = read32(f) 74 | elif obj["subType"] == SCENERY_ELEVATOR: 75 | obj["elevatorType"] = read32(f) 76 | obj["elevatorLevel"] = read32(f) 77 | elif obj["subType"] == SCENERY_LADDER_BOTTOM or obj["subType"] == SCENERY_LADDER_TOP: 78 | obj["destination"] = read32(f) 79 | elif obj["subType"] == SCENERY_GENERIC: 80 | pass # only 4-byte unknown 81 | 82 | return obj 83 | 84 | def readDrugEffect(f): 85 | obj = {} 86 | 87 | obj["duration"] = read32(f) 88 | obj["amount0"] = read32(f) 89 | obj["amount1"] = read32(f) 90 | obj["amount2"] = read32(f) 91 | 92 | return obj 93 | 94 | def readItem(f): 95 | obj = {} 96 | 97 | flagsExt = repr(f.read(3)) 98 | attackMode = ord(f.read(1)) 99 | scriptID = read32(f) 100 | objSubType = read32(f) 101 | materialID = read32(f) 102 | size = read32(f) 103 | weight = read32(f) 104 | cost = read32(f) 105 | invFRM = read32(f) 106 | soundID = ord(f.read(1)) 107 | 108 | obj["flagsExt"] = flagsExt 109 | obj["itemFlags"] = ord(flagsExt[0]) 110 | obj["actionFlags"] = ord(flagsExt[1]) 111 | obj["weaponFlags"] = ord(flagsExt[2]) 112 | obj["attackMode"] = attackMode 113 | obj["scriptID"] = scriptID 114 | obj["subType"] = objSubType 115 | obj["materialID"] = materialID 116 | obj["size"] = size 117 | obj["weight"] = weight 118 | obj["cost"] = cost 119 | obj["invFRM"] = invFRM 120 | obj["soundID"] = soundID 121 | 122 | if objSubType == SUBTYPE_WEAPON: 123 | obj["animCode"] = read32(f) 124 | obj["minDmg"] = read32(f) 125 | obj["maxDmg"] = read32(f) 126 | obj["dmgType"] = read32(f) 127 | obj["maxRange1"] = read32(f) 128 | obj["maxRange2"] = read32(f) 129 | obj["projPID"] = read32(f) 130 | obj["minST"] = read32(f) 131 | obj["APCost1"] = read32(f) 132 | obj["APCost2"] = read32(f) 133 | obj["critFail"] = read32(f) 134 | obj["perk"] = read32(f) 135 | obj["rounds"] = read32(f) 136 | obj["caliber"] = read32(f) 137 | obj["ammoPID"] = read32(f) 138 | obj["maxAmmo"] = read32(f) 139 | obj["soundID"] = f.read(1) 140 | elif objSubType == SUBTYPE_AMMO: 141 | obj["caliber"] = read32(f) 142 | obj["quantity"] = read32(f) 143 | obj["AC modifier"] = read32(f) 144 | obj["DR modifier"] = read32(f) 145 | obj["damMult"] = read32(f) 146 | obj["damDiv"] = read32(f) 147 | elif objSubType == SUBTYPE_ARMOR: 148 | obj["AC"] = read32(f) 149 | obj["stats"] = {} 150 | for stat in ["DR Normal", "DR Laser", "DR Fire", 151 | "DR Plasma", "DR Electrical", "DR EMP", "DR Explosive", 152 | "DT Normal", "DT Laser", "DT Fire", "DT Plasma", "DT Electrical", 153 | "DT EMP", "DT Explosive"]: 154 | obj["stats"][stat] = read32(f) 155 | 156 | obj["perk"] = read32(f) 157 | obj["maleFID"] = read32(f) 158 | obj["femaleFID"] = read32(f) 159 | elif objSubType == SUBTYPE_DRUG: 160 | obj["stat0"] = read32(f) 161 | obj["stat1"] = read32(f) 162 | obj["stat2"] = read32(f) 163 | 164 | obj["amount0"] = read32(f) 165 | obj["amount1"] = read32(f) 166 | obj["amount2"] = read32(f) 167 | 168 | obj["firstDelayed"] = readDrugEffect(f) 169 | obj["secondDelayed"] = readDrugEffect(f) 170 | 171 | obj["addictionRate"] = read32(f) 172 | obj["addictionEffect"] = read32(f) 173 | obj["addictionOnset"] = read32(f) 174 | 175 | #else: 176 | # print "warning: unhandled item subtype", objSubType 177 | 178 | return obj 179 | 180 | def readCritterStats(f): 181 | stats = {} 182 | 183 | for stat in ["STR", "PER", "END", "CHR", "INT", "AGI", "LUK", "HP", "AP", 184 | "AC", "Unarmed", "Melee", "Carry", "Sequence", "Healing Rate", 185 | "Critical Chance", "Better Criticals"]: 186 | stats[stat] = read32(f) 187 | 188 | for stat in ["DT Normal", "DT Laser", "DT Fire", "DT Plasma", "DT Electrical", 189 | "DT EMP", "DT Explosive", "DR Normal", "DR Laser", "DR Fire", 190 | "DR Plasma", "DR Electrical", "DR EMP", "DR Explosive", 191 | "DR Radiation", "DR Poison"]: 192 | stats[stat] = read32(f) 193 | 194 | return stats 195 | 196 | def readCritterSkills(f): 197 | skills = {} 198 | for skill in ["Small Guns", "Big Guns", "Energy Weapons", "Unarmed", 199 | "Melee", "Throwing", "First Aid", "Doctor", "Sneak", 200 | "Lockpick", "Steal", "Traps", "Science", "Repair", 201 | "Speech", "Barter", "Gambling", "Outdoorsman"]: 202 | skills[skill] = read32(f) 203 | return skills 204 | 205 | def readCritter(f): 206 | obj = {} 207 | 208 | obj["actionFlags"] = read32(f) 209 | obj["scriptID"] = read32(f) 210 | obj["headFID"] = read32(f) 211 | obj["AI"] = read32(f) 212 | obj["team"] = read32(f) 213 | obj["flags"] = read32(f) 214 | 215 | obj["baseStats"] = readCritterStats(f) 216 | 217 | obj["age"] = read32(f) 218 | obj["gender"] = read32(f) 219 | 220 | obj["bonusStats"] = readCritterStats(f) 221 | obj["bonusAge"] = read32(f) 222 | obj["bonusGender"] = read32(f) 223 | 224 | obj["skills"] = readCritterSkills(f) 225 | 226 | obj["bodyType"] = read32(f) 227 | obj["XPValue"] = read32(f) 228 | obj["killType"] = read32(f) 229 | 230 | 231 | if FO1 or obj["killType"] in (5, 10): # Robots/Brahmin 232 | obj["damageType"] = None 233 | else: 234 | obj["damageType"] = read32(f) 235 | 236 | return obj 237 | 238 | def readPRO(f): 239 | obj = {} 240 | 241 | objectTypeAndID = read32(f) 242 | textID = read32(f) 243 | frmTypeAndID = read32(f) 244 | lightRadius = read32(f) 245 | lightIntensity = read32(f) 246 | flags = read32(f) 247 | 248 | pid = objectTypeAndID & 0xffff 249 | objType = (objectTypeAndID >> 24) & 0xff 250 | 251 | obj["pid"] = pid 252 | obj["textID"] = textID 253 | obj["type"] = objType 254 | obj["flags"] = flags 255 | obj["lightRadius"] = lightRadius 256 | obj["lightIntensity"] = lightIntensity 257 | 258 | frmPID = frmTypeAndID & 0xffff 259 | frmType = (frmTypeAndID >> 24) & 0xff 260 | obj["frmPID"] = frmPID 261 | obj["frmType"] = frmType 262 | 263 | #print "type:", objType 264 | 265 | if objType == TYPE_ITEM: 266 | obj["extra"] = readItem(f) 267 | elif objType == TYPE_CRITTER: 268 | obj["extra"] = readCritter(f) 269 | elif objType == TYPE_SCENERY: 270 | obj["extra"] = readScenery(f) 271 | else: 272 | print "unhandled type", objType 273 | 274 | return obj 275 | 276 | def main(): 277 | if len(sys.argv) != 2: 278 | print "USAGE: %s PRO" % sys.argv[0] 279 | sys.exit(1) 280 | 281 | with open(sys.argv[1], "rb") as f: 282 | print json.dumps(readPRO(f)) 283 | 284 | if __name__ == '__main__': 285 | main() -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darkf/darkfo/9e9d9349e13b343bddefe3b8eed70797821cf0ba/screenshot.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2015-2017 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | # Setup script to import Fallout 2 data for DarkFO 18 | 19 | from __future__ import print_function 20 | import sys, os, glob, json, traceback 21 | 22 | def error(msg): 23 | print("ERROR:", msg) 24 | raw_input("") 25 | sys.exit(1) 26 | 27 | def warn(msg): 28 | print("WARNING:", msg) 29 | 30 | def info(msg): 31 | print(msg) 32 | 33 | # Check for numpy and pillow (required) 34 | info("Checking for necessary Python modules...") 35 | 36 | try: import numpy 37 | except ImportError: 38 | error("NumPy not found. Please install it from http://www.numpy.org or http://www.lfd.uci.edu/~gohlke/pythonlibs/#numpy . Make sure the version matches your installed version of Python (%d.%d)." % (sys.version_info.major, sys.version_info.minor)) 39 | 40 | try: from PIL import Image 41 | except ImportError: 42 | error("Pillow not found. Please see https://pillow.readthedocs.io/en/4.0.x for installation instructions. Make sure the version matches your installed version of Python (%d.%d)." % (sys.version_info.major, sys.version_info.minor)) 43 | 44 | # import local modules 45 | import dat2 46 | import parseCritTable 47 | import parseElevatorTable 48 | import exportImagesPar 49 | import exportPRO 50 | import fomap 51 | 52 | # global paths/flags 53 | SRC_DIR = None 54 | NO_EXTRACT_DAT = False 55 | NO_EXPORT_IMAGES = False 56 | EXE_PATH = None 57 | 58 | def setup_check(): 59 | global EXE_PATH 60 | 61 | # Check whether everything we need to set up is included in the given source directory 62 | 63 | print("Checking installation directory (%s)..." % SRC_DIR) 64 | 65 | def install_file_exists(path): 66 | return os.path.exists(os.path.join(SRC_DIR, path)) 67 | 68 | if not os.path.exists(SRC_DIR): 69 | error("Installation directory (%s) does not exist." % SRC_DIR) 70 | if not (install_file_exists("master.dat") and install_file_exists("critter.dat")): 71 | error("Installation directory does not contain master.dat or critter.dat, please ensure they exist.") 72 | if not install_file_exists("fallout2.exe"): 73 | warn("Installation directory does not contain fallout2.exe. Please ensure this is the right directory and the file exists. Some features may not be available without it!") 74 | else: 75 | EXE_PATH = os.path.join(SRC_DIR, "fallout2.exe") 76 | 77 | return True 78 | 79 | def parse_crit_table(): 80 | if EXE_PATH is not None: 81 | info("Parsing critical table from fallout2.exe...") 82 | try: 83 | with open(EXE_PATH, "rb") as fp: 84 | # TODO: Don't hardcode paths, and need version check! 85 | critTables = parseCritTable.readCriticalTables(fp, 0x000fef78, 0x00106597) 86 | json.dump(critTables, open("lut/criticalTables.json", "w")) 87 | info("Done parsing critical table") 88 | except Exception: 89 | traceback.print_exc() 90 | warn("Error occurred while parsing critical table (see traceback above).") 91 | else: 92 | warn("Cannot parse critical table, missing fallout2.exe") 93 | 94 | return True 95 | 96 | def parse_elevator_table(): 97 | if EXE_PATH is not None: 98 | info("Parsing elevator table from fallout2.exe...") 99 | try: 100 | with open(EXE_PATH, "rb") as fp: 101 | elevators = parseElevatorTable.parseElevators(fp) 102 | json.dump(elevators, open("lut/elevators.json", "w")) 103 | info("Done parsing elevator table") 104 | except Exception: 105 | traceback.print_exc() 106 | warn("Error occurred while parsing elevator table (see traceback above).") 107 | else: 108 | warn("Cannot parse elevator table, missing fallout2.exe") 109 | 110 | return True 111 | 112 | def extract_dats(): 113 | # Create data directory 114 | if not os.path.exists("data"): 115 | os.mkdir("data") 116 | 117 | def extract_dat(path): 118 | with open(path, "rb") as f: 119 | dat2.dumpFiles(f, "data") 120 | 121 | if not NO_EXTRACT_DAT: 122 | # Extract DATs 123 | info("Extracting master.dat...") 124 | extract_dat(os.path.join(SRC_DIR, "master.dat")) 125 | 126 | info("Extracting critter.dat...") 127 | extract_dat(os.path.join(SRC_DIR, "critter.dat")) 128 | 129 | info("Done extracting DAT archives.") 130 | 131 | return True 132 | 133 | def export_images(): 134 | # Export FRMs/FR[0-9]s 135 | 136 | try: 137 | palette = exportImagesPar.readPAL(os.path.join("data", "color.pal")) 138 | except IOError: 139 | error("Couldn't read data/color.pal") 140 | 141 | # Export them to art/, which will be created if it does not already exist. 142 | info("Converting images, please wait while this runs.") 143 | 144 | try: 145 | exportImagesPar.convertAll(palette, "data", "art", verbose=True) 146 | except Exception: 147 | traceback.print_exc() 148 | warn("Error when converting images (see traceback above). Will not finish converting images, but will continue setup.") 149 | return False 150 | 151 | return True 152 | 153 | def export_pros(): 154 | # Export PROs 155 | 156 | info("Converting prototypes (PROs), please wait while this runs.") 157 | 158 | exportPRO.extractPROs(os.path.join("data", "proto"), "proto") 159 | 160 | return True 161 | 162 | # TODO: extract audio using convertAudio 163 | 164 | def export_maps(): 165 | # Export MAPs 166 | 167 | info("Converting map files, please wait while this runs.") 168 | 169 | if not os.path.exists("maps"): 170 | os.mkdir("maps") 171 | 172 | for mapFile in glob.glob(os.path.join("data", "maps", "*.map")): 173 | mapName = os.path.basename(mapFile).lower() 174 | outFile = os.path.join("maps", os.path.splitext(mapName)[0] + ".json") 175 | 176 | try: 177 | info("Converting map %s ..." % mapFile) 178 | fomap.exportMap("data", mapFile, outFile) 179 | except Exception: 180 | traceback.print_exc() 181 | warn("Error converting map %s (see traceback above). Will continue converting the rest." % mapFile) 182 | 183 | return True 184 | 185 | def main(): 186 | global SRC_DIR, NO_EXTRACT_DAT, NO_EXPORT_IMAGES 187 | 188 | if len(sys.argv) < 2: 189 | print("USAGE:", sys.argv[0], "FALLOUT2_INSTALL_DIR [--no-extract-dat] [--no-export-images]") 190 | return 191 | 192 | NO_EXTRACT_DAT = "--no-extract-dat" in sys.argv 193 | if NO_EXTRACT_DAT: 194 | sys.argv.remove("--no-extract-dat") 195 | 196 | NO_EXPORT_IMAGES = "--no-export-images" in sys.argv 197 | if NO_EXPORT_IMAGES: 198 | sys.argv.remove("--no-export-images") 199 | 200 | SRC_DIR = sys.argv[1] 201 | 202 | setup_check() 203 | parse_crit_table() 204 | parse_elevator_table() 205 | extract_dats() 206 | if not NO_EXPORT_IMAGES: 207 | export_images() 208 | export_pros() 209 | export_maps() 210 | 211 | info("") 212 | info("Setup complete. Please review the messages above, looking for any warnings.") 213 | info("Please run tsc after this to compile the source files.") 214 | raw_input("") 215 | 216 | if __name__ == "__main__": 217 | main() -------------------------------------------------------------------------------- /src/audio.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Audio engine for handling music and sound effects 18 | 19 | interface AudioEngine { 20 | playSfx(sfx: string): void; 21 | playMusic(music: string): void; 22 | playSound(soundName: string): HTMLAudioElement|null; 23 | stopMusic(): void; 24 | stopAll(): void; 25 | tick(): void; 26 | } 27 | 28 | class NullAudioEngine implements AudioEngine { 29 | playSfx(sfx: string): void {} 30 | playMusic(music: string): void {} 31 | playSound(soundName: string): HTMLAudioElement|null { return null } 32 | stopMusic(): void {} 33 | stopAll(): void {} 34 | tick(): void {} 35 | } 36 | 37 | class HTMLAudioEngine implements AudioEngine { 38 | //lastSfxTime: number = 0 39 | nextSfxTime: number = 0 40 | nextSfx: string|null = null 41 | musicAudio: HTMLAudioElement|null = null 42 | 43 | playSfx(sfx: string): void { 44 | this.playSound("sfx/" + sfx) 45 | } 46 | 47 | playMusic(music: string): void { 48 | this.stopMusic() 49 | this.musicAudio = this.playSound("music/" + music) 50 | } 51 | 52 | playSound(soundName: string): HTMLAudioElement|null { 53 | var sound = new Audio() 54 | sound.addEventListener("loadeddata", () => sound.play(), false) 55 | sound.src = "audio/" + soundName + ".wav" 56 | return sound 57 | } 58 | 59 | stopMusic(): void { 60 | if(this.musicAudio) 61 | this.musicAudio.pause() 62 | } 63 | 64 | stopAll(): void { 65 | this.nextSfxTime = 0 66 | this.nextSfx = null 67 | this.stopMusic() 68 | } 69 | 70 | rollNextSfx(): string { 71 | // Randomly obtain the next map sfx 72 | const curMapInfo = getCurrentMapInfo() 73 | if(!curMapInfo) 74 | return "" 75 | 76 | const sfx = curMapInfo.ambientSfx 77 | const sumFreqs = sfx.reduce((sum: number, x: [string, number]) => sum + x[1], 0) 78 | let roll = getRandomInt(0, sumFreqs) 79 | 80 | for(var i = 0; i < sfx.length; i++) { 81 | var freq = sfx[i][1] 82 | 83 | if(roll >= freq) 84 | return sfx[i][0] 85 | 86 | roll -= freq 87 | } 88 | 89 | // XXX: What happens here when none roll? 90 | throw Error("shouldn't be here") 91 | } 92 | 93 | tick(): void { 94 | var time = heart.timer.getTime() 95 | 96 | if(!this.nextSfx) 97 | this.nextSfx = this.rollNextSfx() 98 | 99 | if(time >= this.nextSfxTime) { 100 | // play next sfx in queue 101 | this.playSfx(this.nextSfx) 102 | 103 | // queue up next sfx 104 | this.nextSfx = this.rollNextSfx() 105 | this.nextSfxTime = time + getRandomInt(15, 20)*1000 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /src/char.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 darkf, Stratege 3 | Copyright 2017 darkf 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | // Character Stats and Skills 19 | 20 | // TODO: "Melee Weapons" skill is called "Melee" in the PRO 21 | 22 | class SkillSet { 23 | baseSkills: { [name: string]: number } = {}; 24 | tagged: string[] = []; 25 | skillPoints: number = 0; 26 | 27 | constructor(baseSkills?: { [name: string]: number }, tagged?: string[], skillPoints?: number) { 28 | // Copy construct a SkillSet 29 | if(baseSkills) this.baseSkills = baseSkills; 30 | if(tagged) this.tagged = tagged; 31 | if(skillPoints) this.skillPoints = skillPoints; 32 | } 33 | 34 | clone(): SkillSet { 35 | return new SkillSet(this.baseSkills, this.tagged, this.skillPoints); 36 | } 37 | 38 | static fromPro(skills: any): SkillSet { 39 | // console.log("fromPro: %o", skills); 40 | 41 | return new SkillSet(skills); 42 | } 43 | 44 | getBase(skill: string): number { 45 | const skillDep = skillDependencies[skill]; 46 | 47 | if(!skillDep) 48 | throw Error(`No dependencies for skill '${skill}'`); 49 | 50 | return this.baseSkills[skill] || skillDep.startValue; 51 | } 52 | 53 | get(skill: string, stats: StatSet): number { 54 | const base = this.getBase(skill); 55 | const skillDep = skillDependencies[skill]; 56 | 57 | if(!skillDep) 58 | throw Error(`No dependencies for skill '${skill}'`); 59 | 60 | let skillValue = base; 61 | 62 | if(this.isTagged(skill)) { 63 | // Tagged skills add +20 skill value to the minimum, and increments afterwards by 2x 64 | skillValue = skillDep.startValue + (skillValue - skillDep.startValue) * 2 + 20; 65 | } 66 | 67 | for(const dep of skillDep.dependencies) { 68 | if(dep.statType) 69 | skillValue += Math.floor(stats.get(dep.statType) * dep.multiplier); 70 | } 71 | 72 | return skillValue; 73 | } 74 | 75 | setBase(skill: string, skillValue: number) { 76 | this.baseSkills[skill] = skillValue; 77 | } 78 | 79 | // TODO: Respect min and max bounds in inc/dec 80 | 81 | incBase(skill: string, useSkillPoints: boolean=true): boolean { 82 | const base = this.getBase(skill); 83 | 84 | if(useSkillPoints) { 85 | const cost = skillImprovementCost(base); 86 | 87 | if(this.skillPoints < cost) { 88 | // Not enough skill points to increment 89 | return false; 90 | } 91 | 92 | this.skillPoints -= cost; 93 | } 94 | 95 | this.setBase(skill, base + 1); 96 | return true; 97 | } 98 | 99 | decBase(skill: string, useSkillPoints: boolean=true) { 100 | const base = this.getBase(skill); 101 | 102 | if(useSkillPoints) { 103 | const cost = skillImprovementCost(base - 1); 104 | this.skillPoints += cost; 105 | } 106 | 107 | this.setBase(skill, base - 1); 108 | } 109 | 110 | isTagged(skill: string): boolean { 111 | return this.tagged.indexOf(skill) !== -1; 112 | } 113 | 114 | // TODO: There should be a limit on the number of tagged skills (3 by default) 115 | 116 | tag(skill: string) { 117 | this.tagged.push(skill); 118 | } 119 | 120 | untag(skill: string) { 121 | if(this.isTagged(skill)) 122 | this.tagged.splice(this.tagged.indexOf(skill), 1); 123 | } 124 | } 125 | 126 | class StatSet { 127 | baseStats: { [name: string]: number } = {}; 128 | useBonuses: boolean; 129 | 130 | constructor(baseStats?: { [name: string]: number }, useBonuses: boolean=true) { 131 | // Copy construct a StatSet 132 | if(baseStats) this.baseStats = baseStats; 133 | this.useBonuses = useBonuses; 134 | } 135 | 136 | clone(): StatSet { 137 | return new StatSet(this.baseStats, this.useBonuses); 138 | } 139 | 140 | static fromPro(pro: any): StatSet { 141 | // console.log("stats fromPro: %o", pro); 142 | 143 | const { baseStats, bonusStats } = pro.extra; 144 | 145 | const stats = Object.assign({}, baseStats); 146 | 147 | for(const stat in stats) { 148 | if(bonusStats[stat] !== undefined) 149 | stats[stat] += bonusStats[stat]; 150 | } 151 | 152 | // TODO: armor, appears to be hardwired into the proto? 153 | 154 | // Define Max HP = HP if it does not exist 155 | if(stats["Max HP"] === undefined && stats["HP"] !== undefined) 156 | stats["Max HP"] = stats["HP"]; 157 | 158 | // Define HP = Max HP if it does not exist 159 | if(stats["HP"] === undefined && stats["Max HP"] !== undefined) 160 | stats["HP"] = stats["Max HP"]; 161 | 162 | return new StatSet(stats, false); 163 | } 164 | 165 | getBase(stat: string): number { 166 | const statDep = statDependencies[stat]; 167 | 168 | if(!statDep) 169 | throw Error(`No dependencies for stat '${stat}'`); 170 | 171 | return this.baseStats[stat] || statDep.defaultValue; 172 | } 173 | 174 | get(stat: string): number { 175 | const base = this.getBase(stat); 176 | 177 | const statDep = statDependencies[stat]; 178 | 179 | if(!statDep) 180 | throw Error(`No dependencies for stat '${stat}'`); 181 | 182 | let statValue = base; 183 | if(this.useBonuses) { 184 | for(const dep of statDep.dependencies) { 185 | if(dep.statType) 186 | statValue += Math.floor(this.get(dep.statType) * dep.multiplier); 187 | } 188 | } 189 | 190 | return clamp(statDep.min, statDep.max, statValue); 191 | } 192 | 193 | setBase(stat: string, statValue: number) { 194 | this.baseStats[stat] = statValue; 195 | } 196 | 197 | modifyBase(stat: string, change: number) { 198 | this.setBase(stat, this.getBase(stat) + change); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | // Configuration for the engine internals, controls and UI 2 | 3 | const Config = { 4 | ui: { 5 | screenWidth: 800, 6 | screenHeight: 600, 7 | 8 | scrollPadding: 20, // how far the mouse has to be from an edge to scroll, in pixels 9 | floatMessageDuration: 3, // how long floating messages stay on screen, in seconds 10 | 11 | showHexOverlay: false, // show hex grid? 12 | showCoordinates: false, // show coordinates on hex grid? 13 | showCursor: true, // show hex cursor? 14 | showPath: false, // show player's path? 15 | showFloor: true, // show floor tiles? 16 | showRoof: true, // show roof tiles? 17 | hideRoofWhenUnder: true, // hide roof when we walk under it? 18 | showObjects: true, // show objects? 19 | showWalls: true, // show walls? 20 | showBoundingBox: false, // show bounding boxes around objects? 21 | showSpatials: true, // show spatial script triggers? 22 | }, 23 | 24 | engine: { 25 | renderer: "canvas", // which renderer backend to use ("canvas" or "webgl") 26 | doSaveDirtyMaps: true, // save dirty maps to in-memory cache? 27 | doLoadScripts: true, // should we load scripts? 28 | doUpdateCritters: true, // should we give critters heartbeats? 29 | doTimedEvents: true, // should we handle registered timed events? 30 | doSpatials: true, // should we handle spatial triggers? 31 | doCombat: true, // allow combat? 32 | doUseWeaponModel: true, // use weapon model for NPC models? 33 | doLoadItemInfo: true, // load item information (such as inventory images)? 34 | doAlwaysRun: true, // always run instead of walk? 35 | doZOrder: true, // Z-order objects? 36 | doEncounters: true, // allow random encounters? 37 | doInfiniteUse: false, // allow infinite-range object usage? 38 | doFloorLighting: false, // use FO2-realistic floor lighting? 39 | useLightColorLUT: true, // Use intensityColorTable/colorLUT/colorRGB for accurate lighting colors? 40 | doAudio: false, // enable audio? 41 | doLogLazyLoads: false, // Log lazy-loading of images? (Noisy) 42 | doLogScriptLoads: false, // Log script loads? (Noisy) 43 | doDisasmOnUnimplOp: true, // Disassemble script upon reaching unimplemented opcode? 44 | }, 45 | 46 | combat: { 47 | allowWalkDuringAnyTurn: false, // Allows the player to walk AP-free out of their turn 48 | maxAIDepth: 8, // Maximum number of turns the AI can consider (as a bail-out instead of infinitely recursing) 49 | }, 50 | 51 | controls: { 52 | cameraDown: "down", 53 | cameraUp: "up", 54 | cameraLeft: "left", 55 | cameraRight: "right", 56 | elevationDown: "q", 57 | elevationUp: "e", 58 | showRoof: "r", 59 | showFloor: "f", 60 | showObjects: "o", 61 | showWalls: "w", 62 | talkTo: "t", 63 | inspect: "i", 64 | moveTo: "m", 65 | runTo: "j", 66 | attack: "g", 67 | combat: "c", 68 | playerToTargetRaycast: "y", 69 | showTargetInventory: "v", 70 | use: "u", 71 | kill: "k", 72 | worldmap: "p", 73 | calledShot: "z", 74 | saveKey: "n", 75 | loadKey: "m", 76 | }, 77 | 78 | scripting: { 79 | debugLogShowType: { 80 | stub: true, 81 | log: false, 82 | timer: false, 83 | load: false, 84 | debugMessage: true, 85 | displayMessage: true, 86 | floatMessage: false, 87 | gvars: false, 88 | lvars: false, 89 | mvars: false, 90 | tiles: true, 91 | animation: false, 92 | movement: false, 93 | inventory: true, 94 | party: false, 95 | dialogue: false 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /src/data.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014-2017 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | var mapAreas: AreaMap|null = null 18 | 19 | var proMap: any = null // TODO: type 20 | var lstFiles: { [lsgFile: string]: string[] } = {} 21 | var messageFiles: { [msgFile: string]: { [msgID: string]: string } } = {} 22 | var mapInfo: { [mapID: number]: MapInfo }|null = null 23 | var elevatorInfo: { elevators: Elevator[] }|null = null 24 | var dirtyMapCache: { [mapName: string]: SerializedMap } = {} 25 | 26 | interface AreaMap { 27 | // XXX: Why does using a number key break areas? 28 | [areaID: string]: Area; 29 | } 30 | 31 | interface Area { 32 | name: string; 33 | id: number; 34 | size: string; 35 | state: boolean; 36 | worldPosition: Point; 37 | mapArt?: string; 38 | labelArt?: string; 39 | entrances: AreaEntrance[]; 40 | } 41 | 42 | interface AreaEntrance { 43 | startState: string; 44 | x: number; 45 | y: number; 46 | mapLookupName: string; 47 | mapName: string; 48 | elevation: number; 49 | tileNum: number; 50 | orientation: number; 51 | } 52 | 53 | interface MapInfo { 54 | name: string; 55 | lookupName: string; 56 | ambientSfx: [string, number][]; 57 | music: string; 58 | randomStartPoints: { elevation: number, tileNum: number }[]; 59 | } 60 | 61 | interface Elevator { 62 | buttons: { tileNum: number; mapID: number; level: number; }[]; 63 | buttonCount: number; 64 | labels: number; 65 | type: number; 66 | } 67 | 68 | function getElevator(type: number): Elevator { 69 | if(!elevatorInfo) { 70 | console.log("loading elevator info") 71 | elevatorInfo = getFileJSON("lut/elevators.json") 72 | } 73 | 74 | return elevatorInfo!.elevators[type] 75 | } 76 | 77 | function parseAreas(data: string): AreaMap { 78 | var areas = parseIni(data) 79 | var out: AreaMap = {} 80 | 81 | for(var _area in areas) { 82 | var area = areas[_area] 83 | var match = _area.match(/Area (\d+)/) 84 | if(match === null) throw "city.txt: invalid area name: " + area.area_name 85 | var areaID = parseInt(match[1]) 86 | var worldPos = area.world_pos.split(",").map((x: string) => parseInt(x)) 87 | 88 | var newArea: Area = { 89 | name: area.area_name, 90 | id: areaID, 91 | size: area.size.toLowerCase(), 92 | state: area.start_state.toLowerCase() === "on", 93 | worldPosition: {x: worldPos[0], y: worldPos[1]}, 94 | entrances: [] 95 | } 96 | 97 | // map/label art 98 | var mapArtIdx = parseInt(area.townmap_art_idx) 99 | var labelArtIdx = parseInt(area.townmap_label_art_idx) 100 | 101 | //console.log(mapArtIdx + " - " + labelArtIdx) 102 | 103 | if(mapArtIdx !== -1) 104 | newArea.mapArt = lookupInterfaceArt(mapArtIdx) 105 | if(labelArtIdx !== -1) 106 | newArea.labelArt = lookupInterfaceArt(labelArtIdx) 107 | 108 | // entrances 109 | for(const _key in area) { 110 | // entrance_N 111 | // e.g.: entrance_0=On,345,230,Destroyed Arroyo Bridge,-1,26719,0 112 | 113 | let s = _key.split("_") 114 | if(s[0] === "entrance") { 115 | const entranceString = area[_key] 116 | s = entranceString.split(",") 117 | 118 | const mapLookupName = s[3].trim(); 119 | const mapName = lookupMapNameFromLookup(mapLookupName); 120 | if(!mapName) throw Error("Couldn't look up map name"); 121 | 122 | const entrance = { 123 | startState: s[0], 124 | x: parseInt(s[1]), 125 | y: parseInt(s[2]), 126 | mapLookupName, 127 | mapName, 128 | elevation: parseInt(s[4]), 129 | tileNum: parseInt(s[5]), 130 | orientation: parseInt(s[6]) 131 | } 132 | newArea.entrances.push(entrance) 133 | } 134 | } 135 | 136 | out[areaID] = newArea 137 | } 138 | 139 | return out 140 | } 141 | 142 | function areaContainingMap(mapName: string) { 143 | if(!mapAreas) throw Error("mapAreas not loaded"); 144 | for(var area in mapAreas) { 145 | var entrances = mapAreas[area].entrances 146 | for(var i = 0; i < entrances.length; i++) { 147 | if(entrances[i].mapName === mapName) 148 | return mapAreas[area] 149 | } 150 | } 151 | return null 152 | } 153 | 154 | function loadAreas() { 155 | return parseAreas(getFileText("data/data/city.txt")) 156 | } 157 | 158 | function allAreas() { 159 | if(mapAreas === null) 160 | mapAreas = loadAreas() 161 | var areas = [] 162 | for(var area in mapAreas) 163 | areas.push(mapAreas[area]) 164 | return areas 165 | } 166 | 167 | function loadMessage(name: string) { 168 | name = name.toLowerCase() 169 | var msg = getFileText("data/text/english/game/" + name + ".msg") 170 | if(messageFiles[name] === undefined) 171 | messageFiles[name] = {} 172 | 173 | // parse message file 174 | var lines = msg.split(/\r|\n/) 175 | 176 | // preprocess and merge lines 177 | for(var i = 0; i < lines.length; i++) { 178 | // comments/blanks 179 | if(lines[i][0] === '#' || lines[i].trim() === '') { 180 | lines.splice(i--, 1) 181 | continue 182 | } 183 | 184 | // probably a continuation -- merge it with the last line 185 | if(lines[i][0] !== '{') { 186 | lines[i-1] += lines[i] 187 | lines.splice(i--, 1) 188 | continue 189 | } 190 | } 191 | 192 | for(var i = 0; i < lines.length; i++) { 193 | // e.g. {100}{}{You have entered a dark cave in the side of a mountain.} 194 | var m = lines[i].match(/\{(\d+)\}\{.*\}\{(.*)\}/) 195 | if(m === null) 196 | throw "message parsing: not a valid line: " + lines[i] 197 | // HACK: replace unicode replacement character with an apostrophe (because the Web sucks at character encodings) 198 | messageFiles[name][m[1]] = m[2].replace(/\ufffd/g, "'") 199 | } 200 | } 201 | 202 | 203 | function loadLst(lst: string): string[] { 204 | return getFileText("data/" + lst + ".lst").split('\n') 205 | } 206 | 207 | function getLstId(lst: string, id: number): string|null { 208 | if(lstFiles[lst] === undefined) 209 | lstFiles[lst] = loadLst(lst) 210 | if(lstFiles[lst] === undefined) 211 | return null 212 | 213 | return lstFiles[lst][id] 214 | } 215 | 216 | // Map info (data/data/maps.txt) 217 | 218 | function parseMapInfo() { 219 | if(mapInfo !== null) 220 | return 221 | 222 | // parse map info from data/data/maps.txt 223 | mapInfo = {} 224 | const text = getFileText("data/data/maps.txt") 225 | const ini = parseIni(text) 226 | for(var category in ini) { 227 | const m = category.match(/Map (\d+)/); 228 | if(!m) throw Error("maps.txt: invalid category: " + category); 229 | 230 | let id: string|number = m[1] 231 | if(id === null) throw "maps.txt: invalid category: " + category; 232 | id = parseInt(id); 233 | 234 | var randomStartPoints = [] 235 | for(var key in ini[category]) { 236 | if(key.indexOf("random_start_point_") === 0) { 237 | var startPoint = ini[category][key].match(/elev:(\d), tile_num:(\d+)/) 238 | if(startPoint === null) 239 | throw "invalid random_start_point: " + ini[category][key] 240 | randomStartPoints.push({elevation: parseInt(startPoint[1]), 241 | tileNum: parseInt(startPoint[2])}) 242 | } 243 | } 244 | 245 | // parse ambient sfx list 246 | var ambientSfx: [string, number][] = [] 247 | var ambient_sfx = ini[category].ambient_sfx 248 | if(ambient_sfx) { 249 | var s = ambient_sfx.split(",") 250 | for(var i = 0; i < s.length; i++) { 251 | var kv = s[i].trim().split(":") 252 | ambientSfx.push([kv[0].toLowerCase(), parseInt(kv[1].toLowerCase())]) 253 | } 254 | } 255 | 256 | mapInfo[id] = {name: ini[category].map_name, 257 | lookupName: ini[category].lookup_name, 258 | ambientSfx: ambientSfx, 259 | music: (ini[category].music || "").trim().toLowerCase(), 260 | randomStartPoints: randomStartPoints} 261 | } 262 | } 263 | 264 | function lookupMapFromLookup(lookupName: string) { 265 | if(mapInfo === null) 266 | parseMapInfo() 267 | 268 | for(var mapID in mapInfo!) { 269 | if(mapInfo![mapID].lookupName === lookupName) 270 | return mapInfo![mapID] 271 | } 272 | return null 273 | } 274 | 275 | function lookupMapNameFromLookup(lookupName: string) { 276 | if(mapInfo === null) 277 | parseMapInfo() 278 | 279 | for(var mapID in mapInfo!) { 280 | if(mapInfo![mapID].lookupName.toLowerCase() === lookupName.toLowerCase()) 281 | return mapInfo![mapID].name 282 | } 283 | return null 284 | } 285 | 286 | function lookupMapName(mapID: number): string|null { 287 | if(mapInfo === null) 288 | parseMapInfo() 289 | 290 | return mapInfo![mapID].name || null 291 | } 292 | 293 | function getMapInfo(mapName: string) { 294 | if(mapInfo === null) 295 | parseMapInfo() 296 | 297 | for(var mapID in mapInfo!) { 298 | if(mapInfo![mapID].name.toLowerCase() === mapName.toLowerCase()) 299 | return mapInfo![mapID] 300 | } 301 | return null 302 | } 303 | 304 | function getCurrentMapInfo() { 305 | return getMapInfo(gMap.name) 306 | } 307 | -------------------------------------------------------------------------------- /src/dis.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Disassembler for .INT files 18 | 19 | // Map of opcode -> function that returns a list of arguments 20 | // If not present, it is assumed that the opcode is nullary 21 | const opArgs: { [opcode: number]: (reader: BinaryReader) => number[] } = { 22 | 0xC001: function(reader: BinaryReader) { return [reader.read32()] } // op_push_d 23 | ,0x9001: function(reader: BinaryReader) { return [reader.read32()]} // 9001 op_push_d 24 | } 25 | 26 | const opNames: { [opcode: number]: string } = { 27 | 0x8004: "op_jmp" 28 | ,0x8005: "op_call" 29 | ,0x800C: "op_a_to_d" 30 | ,0x800D: "op_d_to_a" 31 | ,0x8010: "op_exit_prog" 32 | ,0x8012: "op_fetch_global" 33 | ,0x8013: "op_store_global" 34 | ,0x8015: "op_store_external" 35 | ,0x8016: "op_export_var" 36 | ,0x8018: "op_swap" 37 | ,0x8019: "op_swapa" 38 | ,0x801A: "op_pop" 39 | ,0x801C: "op_pop_return" 40 | ,0x8029: "op_pop_base" 41 | ,0x802A: "op_pop_to_base" 42 | ,0x802B: "op_push_base" 43 | ,0x802C: "op_set_global" 44 | ,0x802F: "op_if" 45 | ,0x8030: "op_while" 46 | ,0x8031: "op_store" 47 | ,0x8032: "op_fetch" 48 | ,0x8039: "op_add" 49 | ,0x803A: "op_sub" 50 | ,0x803B: "op_mul" 51 | ,0x803C: "op_div" 52 | ,0x803D: "op_mod" 53 | ,0x803E: "op_and" 54 | ,0x8040: "op_bwand" 55 | ,0x8041: "op_bwor" 56 | ,0x8042: "op_bwxor" 57 | ,0x8043: "op_bwnot" 58 | ,0x8044: "op_floor" 59 | ,0x8045: "op_not" 60 | ,0x8046: "op_negate" 61 | ,0x80AF: "is_success" 62 | ,0x80B0: "is_critical" 63 | ,0x80B8: "display_msg" 64 | ,0x80C1: "lvar" 65 | ,0x80C2: "set_local_var" 66 | ,0x80C7: "script_action" 67 | ,0x80D5: "tile_num_in_direction" 68 | ,0x80DE: "start_gdialog" 69 | ,0x80E1: "metarule3" 70 | ,0x80E9: "set_light_level" 71 | ,0x80EA: "gameTime" 72 | ,0x80F6: "game_time_hour" 73 | ,0x80FF: "critter_attempt_placement" 74 | ,0x8102: "critter_add_trait" 75 | ,0x810B: "metarule" 76 | ,0x810C: "anim" 77 | ,0x8115: "playMovie" 78 | ,0x8118: "get_month" 79 | ,0x8119: "get_day" 80 | ,0x8127: "critter_injure" 81 | ,0x814B: "party_member_obj" 82 | ,0x8154: "debug" 83 | ,0x9001: "push_d" 84 | ,0xA001: "push_d" 85 | ,0xC001: "push_d" 86 | ,0x8002: "op_critical_start" 87 | ,0x80D4: "tile_num" 88 | ,0x80C4: "set_map_var" 89 | ,0x80B7: "create_object_sid" 90 | ,0x80EC: "elevation" 91 | ,0x80F4: "destroy_object" 92 | ,0x80A7: "tile_contains_pid_obj" 93 | ,0x8147: "move_obj_inven_to_obj" 94 | ,0x8014: "op_fetch_external" 95 | ,0x80BF: "dude_obj" 96 | 97 | ,0x80CA: "get_critter_stat" 98 | ,0x80C5: "global_var" 99 | ,0x80C6: "set_global_var" 100 | ,0x80BC: "self_obj" 101 | ,0x806B: "display" 102 | 103 | // logic ops 104 | ,0x8033: "op_eq" 105 | ,0x8034: "op_neq" 106 | ,0x8035: "op_lte" 107 | ,0x8036: "op_gte" 108 | ,0x8037: "op_lt" 109 | ,0x8038: "op_gt" 110 | } 111 | 112 | function disassemble(intfile: IntFile, reader: BinaryReader): string { 113 | var str = "" 114 | function emit(msg: string, t: number=0) { 115 | for(var i = 0; i < t; i++) 116 | str += " " 117 | str += msg + "\n" 118 | } 119 | function emitOp(opcode: number, offset: number, args: any[], t: number=0) { 120 | var sargs = args.map(x => "0x" + x.toString(16)) 121 | var p = "" 122 | if(opcode === 0x9001) 123 | p = ` ("${intfile.strings[args[0]]}" | ${intfile.identifiers[args[0]]})` 124 | emit(`0x${offset.toString(16)}: ${opcode.toString(16)} ${opNames[opcode]} ${sargs}` + p, t) 125 | } 126 | 127 | function disasm(t: number=0): number { 128 | var offset = reader.offset 129 | var opcode = reader.read16() 130 | var args = opArgs[opcode] ? opArgs[opcode](reader) : [] 131 | emitOp(opcode, offset, args, t) 132 | return opcode 133 | } 134 | 135 | reader.seek(0) 136 | 137 | // disassemble __start (the code at 00x0-0x2A) 138 | emit("__start:") 139 | for(; reader.offset < 0x2A;) 140 | disasm(2) 141 | emit("") 142 | 143 | const procOffsets: { [offset: number]: string } = {} 144 | for(var procName in intfile.procedures) { 145 | var proc = intfile.procedures[procName] 146 | procOffsets[proc.offset] = procName 147 | } 148 | 149 | // disassemble the rest of the code, marking procedures 150 | reader.seek(intfile.codeOffset) 151 | var t = 0 152 | for(; reader.offset < reader.data.byteLength;) { 153 | if(procOffsets[reader.offset] !== undefined) { 154 | emit("") 155 | emit(procOffsets[reader.offset] + ":") 156 | t = 2 157 | } 158 | 159 | disasm(t) 160 | } 161 | 162 | return str 163 | } -------------------------------------------------------------------------------- /src/events.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Event manager 18 | 19 | module Events { 20 | export type EventHandler = (e: any) => void; 21 | 22 | const handlers: { [msgType: string]: EventHandler[] } = {}; 23 | 24 | export function on(msgType: string, handler: EventHandler): void { 25 | if(msgType in handlers) 26 | handlers[msgType].push(handler); 27 | else 28 | handlers[msgType] = [handler]; 29 | } 30 | 31 | export function emit(msgType: string, msg?: any): void { 32 | if(msgType in handlers) { 33 | for(const handler of handlers[msgType]) 34 | handler(msg); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/idbcache.ts: -------------------------------------------------------------------------------- 1 | module IDBCache { 2 | let db: IDBDatabase = null; 3 | 4 | function withTransaction(f: (trans: IDBTransaction) => void, finished?: () => void) { 5 | const trans = db.transaction("cache", "readwrite"); 6 | trans.oncomplete = finished; 7 | f(trans); 8 | } 9 | 10 | export function nuke(): void { 11 | withTransaction(trans => { 12 | trans.objectStore("cache").clear(); 13 | }); 14 | } 15 | 16 | export function add(key: string, value: any): any { 17 | withTransaction(trans => { 18 | trans.objectStore("cache").add({key, value}) 19 | }); 20 | 21 | return value; 22 | } 23 | 24 | export function exists(key: string, callback: (exists: boolean) => void): void { 25 | withTransaction(trans => { 26 | const req = trans.objectStore("cache").count(key); 27 | req.onsuccess = (e) => { callback((e).result !== 0); }; 28 | }); 29 | } 30 | 31 | export function get(key: string, callback: (value: any) => void): any { 32 | withTransaction(trans => { 33 | trans.objectStore("cache").get(key).onsuccess = function(e) { 34 | const result = (e.target).result; 35 | callback(result ? result.value : null); 36 | }; 37 | }); 38 | } 39 | 40 | export function init(callback?: () => void): void { 41 | const request = indexedDB.open("darkfo-cache", 1); 42 | 43 | request.onupgradeneeded = function() { 44 | const db = request.result; 45 | const store = db.createObjectStore("cache", {keyPath: "key"}); 46 | // store.createIndex("key", "key", {unique: true}); 47 | }; 48 | 49 | request.onsuccess = function() { 50 | db = request.result; 51 | 52 | db.onerror = function(e) { 53 | console.error("Database error: " + (e.target).errorCode, (e).target); 54 | }; 55 | 56 | console.log("Established Cache DB connection"); 57 | 58 | callback && callback(); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/intfile.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Parser for .INT files 18 | 19 | interface Procedure { 20 | nameIndex: number; 21 | name: string; 22 | offset: number; 23 | index: number; 24 | argc: number; 25 | } 26 | 27 | interface IntFile { 28 | procedures: { [name: string]: Procedure }; 29 | proceduresTable: Procedure[]; 30 | identifiers: { [offset: number]: string }; 31 | strings: { [offset: number]: string }; 32 | codeOffset: number; 33 | name: string; 34 | } 35 | 36 | // parse .INT files 37 | function parseIntFile(reader: BinaryReader, name: string=""): IntFile { 38 | reader.seek(0x2A) // seek to procedure table 39 | 40 | // read procedure table 41 | var numProcs = reader.read32() 42 | var procs: Procedure[] = [] 43 | var procedures: { [name: string]: Procedure } = {} 44 | //console.log("procs: %d", numProcs) 45 | //console.log("") 46 | 47 | for(var i = 0; i < numProcs; i++) { 48 | var nameIndex = reader.read32() 49 | var flags = reader.read32() 50 | //console.log("name index: %d", nameIndex) 51 | //console.log("flags: %d", flags) 52 | assertEq(reader.read32(), 0, "unk0 != 0") 53 | assertEq(reader.read32(), 0, "unk1 != 0") 54 | var offset = reader.read32() 55 | //console.log("offset: %d", offset) 56 | var argc = reader.read32() 57 | //console.log("argc: %d", argc) 58 | //console.log("") 59 | 60 | procs.push({nameIndex: nameIndex 61 | ,name: "" 62 | ,offset: offset 63 | ,index: i 64 | ,argc: argc 65 | }) 66 | } 67 | 68 | // offset->identifier table 69 | var identEnd = reader.read32() 70 | var identifiers: { [offset: number]: string } = {} 71 | 72 | var baseOffset = reader.offset 73 | while(true) { 74 | if(reader.offset - baseOffset >= identEnd) 75 | break 76 | 77 | var len = reader.read16() 78 | var offset = reader.offset - baseOffset + 4 79 | var str = "" 80 | // console.log("len=%d, offset=%d", len, offset) 81 | 82 | for(var j = 0; j < len; j++) { 83 | var c = reader.read8() 84 | if(c) 85 | str += String.fromCharCode(c) 86 | } 87 | 88 | // console.log("str=%s", str) 89 | identifiers[offset] = str 90 | } 91 | 92 | assertEq(reader.read32(), 0xFFFFFFFF, "did not get 0xFFFFFFFF signature") 93 | 94 | // give procedures their names from the identifier table 95 | procs.forEach(proc => proc.name = identifiers[proc.nameIndex]) 96 | 97 | // and populate the procedures table 98 | procs.forEach(proc => procedures[proc.name] = proc) 99 | 100 | // procs.forEach(proc => console.log("proc: %o", proc)) 101 | 102 | /*console.log("") 103 | console.log("strings:") 104 | console.log("")*/ 105 | 106 | // offset->strings table 107 | var stringEnd = reader.read32() 108 | var strings: { [offset: number]: string } = {} 109 | 110 | //assertEq(stringEnd, 0xFFFFFFFF, "TODO: string table") 111 | 112 | if(stringEnd !== 0xFFFFFFFF) { 113 | // read string table 114 | var baseOffset = reader.offset 115 | while(true) { 116 | if(reader.offset - baseOffset >= stringEnd) 117 | break 118 | 119 | var len = reader.read16() 120 | var offset = reader.offset - baseOffset + 4 121 | var str = "" 122 | // console.log("len=%d, offset=%d, stringEnd=%d", len, offset, stringEnd) 123 | 124 | for(var j = 0; j < len; j++) { 125 | var c = reader.read8() 126 | if(c) 127 | str += String.fromCharCode(c) 128 | } 129 | 130 | // console.log("str=%s", str) 131 | strings[offset] = str 132 | } 133 | } 134 | 135 | var codeOffset = reader.offset 136 | 137 | return {procedures: procedures 138 | ,proceduresTable: procs 139 | ,identifiers: identifiers 140 | ,strings: strings 141 | ,codeOffset: codeOffset 142 | ,name: name} 143 | } -------------------------------------------------------------------------------- /src/lighting.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Floor lighting 18 | 19 | module Lighting { 20 | // length 15 21 | var rightside_up_triangles = [2, 3, 0, 3, 4, 1, 5, 6, 3, 6, 7, 4, 8, 9, 6] 22 | var upside_down_triangles = [0, 3, 1, 2, 5, 3, 3, 6, 4, 5, 8, 6, 6, 9, 7] 23 | 24 | // length 26 25 | var rightside_up_table = [ 26 | -1, 27 | 0x2, 28 | 0x4E, 29 | 0x2, 30 | 0x4C, 31 | 0x6, 32 | 0x49, 33 | 0x8, 34 | 0x47, 35 | 0x0A, 36 | 0x44, 37 | 0x0E, 38 | 0x41, 39 | 0x10, 40 | 0x3F, 41 | 0x12, 42 | 0x3D, 43 | 0x14, 44 | 0x3A, 45 | 0x18, 46 | 0x37, 47 | 0x1A, 48 | 0x35, 49 | 0x1C, 50 | 0x32, 51 | 0x20 52 | ] 53 | 54 | var upside_down_table = [ 55 | 0x0, 56 | 0x20, 57 | 0x30, 58 | 0x20, 59 | 0x31, 60 | 0x1E, 61 | 0x34, 62 | 0x1A, 63 | 0x37, 64 | 0x18, 65 | 0x39, 66 | 0x16, 67 | 0x3C, 68 | 0x12, 69 | 0x3F, 70 | 0x10, 71 | 0x41, 72 | 0x0E, 73 | 0x43, 74 | 0x0C, 75 | 0x46, 76 | 0x8, 77 | 0x49, 78 | 0x6, 79 | 0x4B, 80 | 0x4 81 | ] 82 | 83 | // length 40 84 | export var vertices = [ 85 | 0x10, 86 | -1, 87 | -201, 88 | 0x0, 89 | 0x30, 90 | -2, 91 | -2, 92 | 0x0, 93 | 0x3C0, 94 | 0x0, 95 | 0x0, 96 | 0x0, 97 | 0x3E0, 98 | 0x0C7, 99 | -1, 100 | 0x0, 101 | 0x400, 102 | 0x0C6, 103 | 0x0C6, 104 | 0x0, 105 | 0x790, 106 | 0x0C8, 107 | 0x0C8, 108 | 0x0, 109 | 0x7B0, 110 | 0x18F, 111 | 0x0C7, 112 | 0x0, 113 | 0x7D0, 114 | 0x18E, 115 | 0x18E, 116 | 0x0, 117 | 0x0B60, 118 | 0x190, 119 | 0x190, 120 | 0x0, 121 | 0x0B80, 122 | 0x257, 123 | 0x18F, 124 | 0x0 125 | ] 126 | 127 | // Framebuffer for triangle-lit tiles 128 | // XXX: what size should this be? 129 | export var intensity_map = new Array(1024*12) 130 | 131 | // zero array 132 | for(var i = 0; i < intensity_map.length; i++) 133 | intensity_map[i] = 0 134 | 135 | var ambient = 0xA000 // ambient light level 136 | 137 | // Color look-up table by light intensity 138 | export declare var intensityColorTable: number[]; 139 | 140 | export var colorLUT: any = null; // string color integer -> palette index 141 | export var colorRGB: any = null; // palette index -> string color integer 142 | 143 | function light_get_tile(tilenum: number): number { 144 | return Math.min(65536, Lightmap.tile_intensity[tilenum]) 145 | } 146 | 147 | function init(tilenum: number): boolean { 148 | var start = (tilenum & 1); // even/odd 149 | 150 | for(var i = 0, j = start; i <= 36; i += 4, j += 4) { 151 | var offset = vertices[1 + j] 152 | var t = tilenum + offset 153 | var light = Math.max(light_get_tile(t), ambient) 154 | 155 | vertices[3 + i] = light 156 | } 157 | 158 | // do a uniformly-lit check 159 | // true means it's triangle lit 160 | 161 | if(vertices[7] !== vertices[3]) 162 | return true 163 | 164 | var uni = 1 165 | for(var i = 4; i < 36; i += 4) { 166 | if(vertices[7 + i] === vertices[3 + i]) 167 | uni++ //return true 168 | } 169 | 170 | return (uni !== 9) 171 | } 172 | 173 | function renderTris(isRightsideUp: boolean): void { 174 | var tris = isRightsideUp ? rightside_up_triangles : upside_down_triangles 175 | var table = isRightsideUp ? rightside_up_table : upside_down_table 176 | 177 | for(var i = 0; i < 15; i += 3) { 178 | var a = tris[i + 0] 179 | var b = tris[i + 1] 180 | var c = tris[i + 2] 181 | 182 | var x = vertices[3 + 4*a] 183 | var y = vertices[3 + 4*b] 184 | var z = vertices[3 + 4*c] 185 | 186 | var inc, intensityIdx, baseLight, lightInc 187 | 188 | if(isRightsideUp) { // rightside up triangles 189 | inc = (x - z) / 13 | 0 190 | lightInc = (y - x) / 32 | 0 191 | intensityIdx = vertices[4*c] 192 | baseLight = z 193 | } 194 | else { // upside down triangles 195 | inc = (y - x) / 13 | 0 196 | lightInc = (z - x) / 32 | 0 197 | intensityIdx = vertices[4*a] 198 | baseLight = x 199 | } 200 | 201 | for(var j = 0; j < 26; j += 2) { 202 | var edx = table[1 + j] 203 | intensityIdx += table[j] 204 | 205 | var light = baseLight 206 | for(var k = 0; k < edx; k++) { 207 | if(intensityIdx < 0 || intensityIdx >= intensity_map.length) 208 | throw "guard"; 209 | intensity_map[intensityIdx++] = light 210 | light += lightInc 211 | } 212 | 213 | baseLight += inc 214 | } 215 | } 216 | } 217 | 218 | export function initTile(hex: Point): boolean { 219 | return init(toTileNum(hex)); 220 | } 221 | 222 | export function computeFrame(): number[] { 223 | renderTris(true) 224 | renderTris(false) 225 | return intensity_map 226 | } 227 | } -------------------------------------------------------------------------------- /src/net.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Multiplayer network code (netcode) 18 | 19 | module Netcode { 20 | let ws: WebSocket|null = null; 21 | let connected = false; 22 | const handlers: { [msgType: string]: (msg: any) => void } = {}; 23 | 24 | export const netPlayerMap: { [uid: number]: NetPlayer } = {}; 25 | 26 | function send(t: string, msg: any={}) { 27 | assert(connected, "Can't send message to unconnected socket"); 28 | msg.t = t; 29 | ws.send(JSON.stringify(msg)); 30 | } 31 | 32 | export function on(msgType: string, handler: (msg: any) => void): void { 33 | if(msgType in handlers) 34 | console.warn("Overwriting existing message handler"); 35 | handlers[msgType] = handler; 36 | } 37 | 38 | export function connect(host: string, onConnected?: () => void): void { 39 | ws = new WebSocket(host); 40 | 41 | ws.binaryType = "arraybuffer"; 42 | 43 | ws.onopen = (e) => { 44 | console.log("WebSocket connected to %s", host); 45 | connected = true; 46 | onConnected && onConnected(); 47 | }; 48 | 49 | ws.onclose = (e: CloseEvent) => { 50 | console.warn("WebSocket closed (%d): %s", e.code, e.reason); 51 | connected = false; 52 | }; 53 | 54 | ws.onerror = (e) => { 55 | console.error("WebSocket error: %o", e); 56 | connected = false; 57 | }; 58 | 59 | ws.onmessage = (e) => { 60 | if(typeof e.data !== "string") { 61 | if("binary" in handlers) 62 | handlers.binary(e.data); 63 | return; 64 | } 65 | 66 | const msg = JSON.parse(e.data); 67 | 68 | console.log("net: Got %s message", msg.t); 69 | 70 | if(msg.t in handlers) 71 | handlers[msg.t](msg); 72 | }; 73 | } 74 | 75 | export function identify(name: string): void { 76 | send("ident", { name }); 77 | } 78 | 79 | function findObjectByUID(uid: number): Obj|null { 80 | return gMap.getObjects().find(obj => obj.uid === uid) || null; 81 | } 82 | 83 | function setupCommonEvents(): void { 84 | // Player movement 85 | on("movePlayer", (msg: any) => { 86 | if(msg.uid in netPlayerMap) 87 | netPlayerMap[msg.uid].move(msg.position, undefined, false); 88 | }); 89 | 90 | Events.on("playerMoved", (msg: any) => { 91 | send("moved", { x: msg.x, y: msg.y }); 92 | }); 93 | 94 | // Object open/close 95 | on("objSetOpen", (msg: any) => { 96 | const obj = findObjectByUID(msg.uid); 97 | assert(obj !== null, "net.objSetOpen: No such object"); 98 | setObjectOpen(obj, msg.open, false, false); 99 | }); 100 | 101 | Events.on("objSetOpen", (msg: any) => { 102 | send("objSetOpen", { uid: msg.obj.uid, open: msg.open }); 103 | }); 104 | } 105 | 106 | function getNetPlayers(): NetPlayer[] { 107 | return Object.values(netPlayerMap); 108 | } 109 | 110 | export function host(): void { 111 | send("host"); 112 | 113 | setupCommonEvents(); 114 | 115 | on("guestJoined", (msg: any) => { 116 | console.log("Guest '%s' (%d) joined @ %d, %d", msg.name, msg.uid, msg.position.x, msg.position.y); 117 | 118 | // Add guest network player 119 | const netPlayer = new NetPlayer(msg.name, msg.uid); 120 | netPlayer.position = msg.position; 121 | netPlayer.orientation = msg.orientation; 122 | gMap.addObject(netPlayer); 123 | 124 | netPlayerMap[msg.uid] = netPlayer; 125 | }); 126 | 127 | Events.on("loadMapPre", () => { 128 | // Remove the net players from the old map 129 | for(const netPlayer of getNetPlayers()) 130 | gMap.removeObject(netPlayer); 131 | }); 132 | 133 | Events.on("loadMapPost", () => { 134 | console.log("Map changed, sending map..."); 135 | changeMap(); 136 | 137 | // Add the net players to the new map 138 | for(const netPlayer of getNetPlayers()) 139 | gMap.addObject(netPlayer); 140 | }); 141 | 142 | Events.on("elevationChanged", (e: any) => { 143 | if(e.isMapLoading) 144 | return; 145 | 146 | // Move net player's elevation from old to new 147 | // TODO: Perhaps we should refactor this and make a Critter.changeElevation? 148 | 149 | console.log("net: Changing elevation..."); 150 | 151 | for(const netPlayer of getNetPlayers()) { 152 | arrayRemove(gMap.objects[e.oldElevation], netPlayer); 153 | gMap.objects[e.elevation].push(netPlayer); 154 | } 155 | 156 | send("changeElevation", { elevation: e.elevation, position: player.position, orientation: player.orientation }); 157 | }); 158 | 159 | Events.on("objMove", (e: any) => { 160 | if(e.obj.isPlayer) 161 | return; 162 | send("objMove", { uid: e.obj.uid, position: e.position }); 163 | }); 164 | } 165 | 166 | export function join(): void { 167 | send("join"); 168 | 169 | setupCommonEvents(); 170 | 171 | let serializedMap: any = null; 172 | 173 | on("binary", (data: any) => { 174 | console.log("Received binary remote map, decompressing..."); 175 | console.time("map decompression"); 176 | serializedMap = JSON.parse(pako.inflate(data, { to: "string" })); 177 | console.timeEnd("map decompression"); 178 | }); 179 | 180 | on("map", (msg: any) => { 181 | console.log("Received map change request, loading..."); 182 | console.time("map deserialization"); 183 | gMap.deserialize(serializedMap); 184 | console.timeEnd("map deserialization"); 185 | console.log("Loaded serialized remote map."); 186 | 187 | // TODO: Spawn the player somewhere sensible 188 | player.position = msg.player.position; 189 | player.orientation = 0; 190 | player.inventory = []; 191 | 192 | gMap.changeElevation(msg.player.elevation, false, false); 193 | 194 | // Add host network player 195 | const netPlayer = new Netcode.NetPlayer(msg.hostPlayer.name, msg.hostPlayer.uid); 196 | netPlayer.position = msg.hostPlayer.position; 197 | netPlayer.orientation = msg.hostPlayer.orientation; 198 | gMap.addObject(netPlayer); 199 | 200 | Netcode.netPlayerMap[msg.hostPlayer.uid] = netPlayer; 201 | 202 | isWaitingOnRemote = false; 203 | }); 204 | 205 | on("elevationChanged", (msg: any) => { 206 | const oldElevation = gMap.currentElevation; 207 | 208 | gMap.changeElevation(msg.elevation, false, false); 209 | 210 | console.log("net: Changing elevation..."); 211 | 212 | for(const netPlayer of getNetPlayers()) { 213 | arrayRemove(gMap.objects[oldElevation], netPlayer); 214 | gMap.objects[gMap.currentElevation].push(netPlayer); 215 | } 216 | }); 217 | 218 | on("objMove", (e: any) => { 219 | const obj = findObjectByUID(e.uid); 220 | assert(obj !== null, "net.objMove: No such object"); 221 | 222 | console.log("Move: uid %o, obj %o, pos %o", e.uid, obj, e.position); 223 | 224 | // Doesn't matter if we signal events or not, we're on the guest -- we won't be sending them (for now). 225 | // If this changes, we shouldn't signal events here. 226 | obj.move(e.position); 227 | }); 228 | } 229 | 230 | export function changeMap(): void { 231 | // First send the map so the server has it in its buffer 232 | console.log("Serializing and compressing map..."); 233 | console.time("serialize/compress map"); 234 | ws.send(pako.deflate(JSON.stringify(gMap.serialize()))); 235 | console.timeEnd("serialize/compress map"); 236 | 237 | // Now send the map change notification 238 | console.log("Sending map change request..."); 239 | send("changeMap", { mapName: gMap.name, 240 | player: { position: player.position, elevation: gMap.currentElevation, orientation: player.orientation } 241 | }); 242 | } 243 | 244 | export class NetPlayer extends Critter { 245 | // TODO: This should mean userid, it conflicts with Obj.uid 246 | uid: number; 247 | 248 | constructor(name: string, uid: number) { 249 | super(); 250 | 251 | this.name = name; 252 | this.uid = uid; 253 | } 254 | 255 | // isPlayer = true; // TODO: isNetPlayer? 256 | art = "art/critters/hmjmpsaa"; 257 | 258 | teamNum = 0; 259 | 260 | position = {x: 94, y: 109} 261 | orientation = 3 262 | gender = "male" 263 | 264 | lightRadius = 4 265 | lightIntensity = 65536 266 | 267 | toString() { return "The Dude, Mk.II" } 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/party.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Party member system for DarkFO 18 | 19 | class Party { 20 | // party members 21 | party: Critter[] = [] 22 | 23 | addPartyMember(obj: Critter) { 24 | console.log("party member %o added", obj) 25 | this.party.push(obj) 26 | } 27 | 28 | removePartyMember(obj: Critter) { 29 | console.log("party member %o removed", obj) 30 | if(!arrayRemove(this.party, obj)) 31 | throw Error("Could not remove party member"); 32 | } 33 | 34 | getPartyMembers(): Critter[] { 35 | return this.party 36 | } 37 | 38 | getPartyMembersAndPlayer(): Critter[] { 39 | return [player].concat(this.party) 40 | } 41 | 42 | isPartyMember(obj: Critter) { 43 | return arrayIncludes(this.party, obj) 44 | } 45 | 46 | getPartyMemberByPID(pid: number) { 47 | return this.party.find(obj => obj.pid === pid) || null 48 | } 49 | 50 | serialize(): SerializedObj[] { 51 | return this.party.map(obj => obj.serialize()) 52 | } 53 | 54 | deserialize(objs: SerializedObj[]): void { 55 | this.party.length = 0 56 | for(const obj of objs) 57 | this.party.push(deserializeObj(obj)) 58 | } 59 | } 60 | 61 | var gParty = new Party() 62 | -------------------------------------------------------------------------------- /src/player.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Contains the Player class and relevant initialization logic 18 | 19 | class Player extends Critter { 20 | name = "Player" 21 | 22 | isPlayer = true; 23 | art = "art/critters/hmjmpsaa"; 24 | 25 | stats = new StatSet({AGI: 8, INT: 8, STR: 8, CHA: 8, HP: 100}) 26 | skills = new SkillSet(undefined, undefined, 10) // Start off with 10 skill points 27 | 28 | teamNum = 0 29 | 30 | position = {x: 94, y: 109} 31 | orientation = 3 32 | gender = "male" 33 | leftHand = createObjectWithPID(9) 34 | 35 | inventory = [createObjectWithPID(41).setAmount(1337)] 36 | 37 | lightRadius = 4 38 | lightIntensity = 65536 39 | 40 | toString() { return "The Dude" } 41 | 42 | /* 43 | var obj = {position: {x: 94, y: 109}, orientation: 2, frame: 0, type: "critter", 44 | art: "art/critters/hmjmpsaa", isPlayer: true, anim: "idle", lastFrameTime: 0, 45 | path: null, animCallback: null, 46 | leftHand: playerWeapon, rightHand: null, weapon: null, armor: null, 47 | dead: false, name: "Player", gender: "male", inventory: [ 48 | {type: "misc", name: "Money", pid: 41, pidID: 41, amount: 1337, pro: {textID: 4100, extra: {cost: 1}, invFRM: 117440552}, invArt: 'art/inven/cap2'} 49 | ], stats: null, skills: null, tempChanges: null} 50 | */ 51 | 52 | move(position: Point, curIdx?: number, signalEvents: boolean=true): boolean { 53 | if(!super.move(position, curIdx, signalEvents)) 54 | return false 55 | 56 | if(signalEvents) 57 | Events.emit("playerMoved", position); 58 | 59 | // check if the player has entered an exit grid 60 | var objs = objectsAtPosition(this.position) 61 | for(var i = 0; i < objs.length; i++) { 62 | if(objs[i].type === "misc" && objs[i].extra && objs[i].extra.exitMapID !== undefined) { 63 | // walking on an exit grid 64 | // todo: exit grids are likely multi-hex (maybe have a set?) 65 | var exitMapID = objs[i].extra.exitMapID 66 | var startingPosition = fromTileNum(objs[i].extra.startingPosition) 67 | var startingElevation = objs[i].extra.startingElevation 68 | this.clearAnim() 69 | 70 | if(startingPosition.x === -1 || startingPosition.y === -1 || 71 | exitMapID < 0) { // world map 72 | console.log("exit grid -> worldmap") 73 | uiWorldMap() 74 | } 75 | else { // another map 76 | console.log("exit grid -> map " + exitMapID + " elevation " + startingElevation + 77 | " @ " + startingPosition.x + ", " + startingPosition.y) 78 | if(exitMapID === gMap.mapID) { 79 | // same map, different elevation 80 | gMap.changeElevation(startingElevation, true) 81 | player.move(startingPosition) 82 | centerCamera(player.position) 83 | } 84 | else 85 | gMap.loadMapByID(exitMapID, startingPosition, startingElevation) 86 | } 87 | 88 | return false 89 | } 90 | } 91 | 92 | return true 93 | } 94 | } -------------------------------------------------------------------------------- /src/pro.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Functions handling FO2 prototypes and lookups performed on them 18 | 19 | function getPROType(pid: number) { 20 | const map: { [pid: number]: string } = {0: 'items', 1: 'critters', 2: 'scenery', 3: 'walls', 4: 'tiles', 5: 'misc'} 21 | return map[(pid >> 24) & 0xff] 22 | } 23 | 24 | function loadPRO(pid: number, pidID: number) { 25 | if(!proMap) 26 | return null 27 | 28 | // use the proto/ .lst files to look up type/pid 29 | const type = getPROType(pid) 30 | const lsts: { [lst: string]: string } = { 31 | "items": "proto/items/items", "critters": "proto/critters/critters", 32 | "scenery": "proto/scenery/scenery", "misc": "proto/misc/misc", 33 | "walls": "proto/walls/walls"} 34 | const id = lsts[type] ? parseInt(getLstId(lsts[type], pidID - 1).split(".")[0], 10) : pidID 35 | 36 | return proMap[type][id] 37 | } 38 | 39 | function getPROTypeName(type: number) { 40 | // singular 41 | const map: { [type: number]: string } = {0: 'item', 1: 'critter', 2: 'scenery', 3: 'wall', 4: 'tile', 5: 'misc'} 42 | return map[type] 43 | } 44 | 45 | function getPROSubTypeName(type: number): string { 46 | const map: { [type: number]: string } = {0: 'armor', 1: 'container', 2: 'drug', 3: 'weapon', 4: 'ammo', 5: 'misc', 6: 'key'} 47 | return map[type] 48 | } 49 | 50 | function makePID(type: number, pid: number) { 51 | return (type << 24) | pid 52 | } 53 | 54 | function getCritterArtPath(frmPID: number) { 55 | console.log("FRM PID: " + frmPID) 56 | var idx = (frmPID & 0x00000fff) 57 | var id1 = (frmPID & 0x0000f000) >> 12 58 | var id2 = (frmPID & 0x00ff0000) >> 16 59 | //var id3 = (frmPID & 0x70000000) >> 28 60 | 61 | if (id2 == 0x1b || id2 == 0x1d || 62 | id2 == 0x1e || id2 == 0x37 || 63 | id2 == 0x39 || id2 == 0x3a || 64 | id2 == 0x21 || id2 == 0x40) { 65 | throw "reindex(?)" 66 | } 67 | 68 | var path = "art/critters/" + getLstId("art/critters/critters", idx).split(',')[0].toLowerCase() 69 | 70 | if(id1 >= 0x0b) 71 | throw "?" 72 | 73 | if(id2 >= 0x26 && id2 <= 0x2f) 74 | throw ("0x26 and 0x2f") 75 | else if(id2 === 0x24) 76 | path += "ch" 77 | else if(id2 === 0x25) 78 | path += "cj" 79 | else if(id2 >= 0x30) 80 | path += 'r' + String.fromCharCode(id2 + 0x31) 81 | else if(id2 >= 0x14) 82 | throw "0x14" 83 | else if (id2 === 0x12) { 84 | throw "0x12" 85 | /*if(id1 === 0x01) 86 | path += "dm" 87 | else if(id1 === 0x04) 88 | path += "gm" 89 | else 90 | path += "as"*/ 91 | } 92 | else if(id2 === 0x0d) 93 | throw "0x0d" 94 | else { 95 | if(id2 <= 1 && id1 > 0) { 96 | console.log("ID1: " + id1) 97 | path += String.fromCharCode(id1 + 'c'.charCodeAt(0)) 98 | } 99 | else 100 | path += 'a' 101 | path += String.fromCharCode(id2 + 'a'.charCodeAt(0)) 102 | } 103 | 104 | return path 105 | } 106 | 107 | function lookupInterfaceArt(idx: number) { 108 | return "art/intrface/" + getLstId("art/intrface/intrface", idx).split('.')[0].toLowerCase() 109 | } 110 | 111 | function lookupArt(frmPID: number) { 112 | var type = getPROType(frmPID) 113 | var pidID = frmPID & 0xffff 114 | 115 | if(type === "critters") 116 | return getCritterArtPath(frmPID) 117 | 118 | var lsts: { [lst: string]: string } = { 119 | "items": "art/items/items", 120 | "scenery": "art/scenery/scenery", "misc": "art/misc/misc"} 121 | var path = "art/" + type + "/" + getLstId(lsts[type], pidID).split('.')[0] 122 | 123 | // console.log("LOOKUP ART: " + path) 124 | return path.toLowerCase() 125 | } 126 | -------------------------------------------------------------------------------- /src/renderer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Abstract game renderer 18 | 19 | type TileMap = string[][] 20 | 21 | interface ObjectRenderInfo { 22 | x: number; y: number; spriteX: number; 23 | frameWidth: number; frameHeight: number; 24 | uniformFrameWidth: number; 25 | uniformFrameHeight: number; 26 | spriteFrameNum: number; 27 | artInfo: any; 28 | visible: boolean; 29 | } 30 | 31 | class Renderer { 32 | objects: Obj[]; 33 | roofTiles: TileMap; 34 | floorTiles: TileMap; 35 | 36 | initData(roof: TileMap, floor: TileMap, objects: Obj[]): void { 37 | this.roofTiles = roof 38 | this.floorTiles = floor 39 | this.objects = objects 40 | } 41 | 42 | render(): void { 43 | this.clear(127, 127, 127) 44 | 45 | if(isLoading) { 46 | this.color(0, 0, 0) 47 | var w = 256, h = 40 48 | var w2 = (loadingAssetsLoaded / loadingAssetsTotal) * w 49 | // draw a loading progress bar 50 | this.rectangle(SCREEN_WIDTH/2 - w/2, SCREEN_HEIGHT/2, 51 | w, h, false) 52 | this.rectangle(SCREEN_WIDTH/2 - w/2 + 2, SCREEN_HEIGHT/2 + 2, 53 | w2 - 4, h - 4) 54 | return 55 | } 56 | 57 | this.color(255, 255, 255) 58 | 59 | var mousePos = heart.mouse.getPosition() 60 | var mouseHex = hexFromScreen(mousePos[0] + cameraX, mousePos[1] + cameraY) 61 | var mouseSquare = tileFromScreen(mousePos[0] + cameraX, mousePos[1] + cameraY) 62 | //var mouseTile = tileFromScreen(mousePos[0] + cameraX, mousePos[1] + cameraY) 63 | 64 | if(Config.ui.showFloor) this.renderFloor(this.floorTiles) 65 | if(Config.ui.showCursor && hexOverlay) { 66 | var scr = hexToScreen(mouseHex.x, mouseHex.y) 67 | this.image(hexOverlay, scr.x - 16 - cameraX, scr.y - 12 - cameraY) 68 | } 69 | if(Config.ui.showObjects) this.renderObjects(this.objects) 70 | if(Config.ui.showRoof) this.renderRoof(this.roofTiles) 71 | 72 | if(inCombat) { 73 | var whose = combat.inPlayerTurn ? "player" : combat.combatants[combat.whoseTurn].name 74 | var AP = combat.inPlayerTurn ? player.AP : combat.combatants[combat.whoseTurn].AP 75 | this.text("[turn " + combat.turnNum + " of " + whose + " AP: " + AP.getAvailableMoveAP() + "]", SCREEN_WIDTH - 200, 15) 76 | } 77 | 78 | if(Config.ui.showSpatials && Config.engine.doSpatials) { 79 | gMap.getSpatials().forEach(spatial => { 80 | var scr = hexToScreen(spatial.position.x, spatial.position.y) 81 | //heart.graphics.draw(hexOverlay, scr.x - 16 - cameraX, scr.y - 12 - cameraY) 82 | this.text(spatial.script, scr.x - 10 - cameraX, scr.y - 3 - cameraY) 83 | }) 84 | } 85 | 86 | this.text("mh: " + mouseHex.x + "," + mouseHex.y, 5, 15) 87 | this.text("mt: " + mouseSquare.x + "," + mouseSquare.y, 75, 15) 88 | //heart.graphics.print("mt: " + mouseTile.x + "," + mouseTile.y, 100, 15) 89 | this.text("m: " + mousePos[0] + ", " + mousePos[1], 175, 15) 90 | 91 | //this.text("fps: " + heart.timer.getFPS(), SCREEN_WIDTH - 50, 15) 92 | 93 | for(var i = 0; i < floatMessages.length; i++) { 94 | var bbox = objectBoundingBox(floatMessages[i].obj) 95 | if(bbox === null) continue 96 | heart.ctx.fillStyle = floatMessages[i].color 97 | var centerX = bbox.x - bbox.w/2 - cameraX 98 | this.text(floatMessages[i].msg, centerX, bbox.y - cameraY - 16) 99 | } 100 | 101 | if(player.dead) { 102 | this.color(255, 0, 0, 50) 103 | this.rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) 104 | } 105 | } 106 | 107 | objectRenderInfo(obj: Obj): ObjectRenderInfo|null { 108 | var scr = hexToScreen(obj.position.x, obj.position.y) 109 | var visible = obj.visible 110 | 111 | if(images[obj.art] === undefined) { 112 | lazyLoadImage(obj.art) // try to load it in 113 | return null 114 | } 115 | 116 | var info = imageInfo[obj.art] 117 | if(info === undefined) 118 | throw "No image map info for: " + obj.art 119 | 120 | if(!(obj.orientation in info.frameOffsets)) 121 | obj.orientation = 0 // ... 122 | var frameInfo = info.frameOffsets[obj.orientation][obj.frame] 123 | var dirOffset = info.directionOffsets[obj.orientation] 124 | 125 | // Anchored from the bottom center 126 | var offsetX = -(frameInfo.w / 2 | 0) + dirOffset.x 127 | var offsetY = -frameInfo.h + dirOffset.y 128 | 129 | if(obj.shift) { 130 | offsetX += obj.shift.x 131 | offsetY += obj.shift.y 132 | } 133 | else { 134 | offsetX += frameInfo.ox 135 | offsetY += frameInfo.oy 136 | } 137 | 138 | var scrX = scr.x + offsetX, scrY = scr.y + offsetY 139 | 140 | if(scrX + frameInfo.w < cameraX || scrY + frameInfo.h < cameraY || 141 | scrX >= cameraX+SCREEN_WIDTH || scrY >= cameraY+SCREEN_HEIGHT) 142 | visible = false // out of screen bounds, no need to draw 143 | 144 | var spriteFrameNum = info.numFrames * obj.orientation + obj.frame 145 | var sx = spriteFrameNum * info.frameWidth 146 | 147 | return {x: scrX, y: scrY, spriteX: sx, 148 | frameWidth: frameInfo.w, frameHeight: frameInfo.h, 149 | uniformFrameWidth: info.frameWidth, 150 | uniformFrameHeight: info.frameHeight, 151 | spriteFrameNum: spriteFrameNum, 152 | artInfo: info, 153 | visible: visible} 154 | } 155 | 156 | renderObjects(objs: Obj[]) { 157 | for(const obj of objs) { 158 | if(!Config.ui.showWalls && obj.type === "wall") 159 | continue; 160 | if(obj.outline) 161 | this.renderObjectOutlined(obj); 162 | else 163 | this.renderObject(obj); 164 | } 165 | } 166 | 167 | // stubs to be overriden 168 | init(): void { } 169 | 170 | clear(r: number, g: number, b: number): void { } 171 | color(r: number, g: number, b: number, a: number=255): void { } 172 | rectangle(x: number, y: number, w: number, h: number, filled: boolean=true): void { } 173 | text(txt: string, x: number, y: number): void { } 174 | image(img: HTMLImageElement|HeartImage, x: number, y: number, w?: number, h?: number): void { } 175 | 176 | renderRoof(roof: TileMap): void { } 177 | renderFloor(floor: TileMap): void { } 178 | renderObjectOutlined(obj: Obj): void { this.renderObject(obj); } 179 | renderObject(obj: Obj): void { } 180 | } -------------------------------------------------------------------------------- /src/saveload.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2017 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Saving and loading support 18 | 19 | module SaveLoad { 20 | let db: IDBDatabase; 21 | 22 | // Save game metadata + maps 23 | export interface SaveGame { 24 | id?: number; 25 | version: number; 26 | name: string; 27 | timestamp: number; 28 | currentMap: string; 29 | currentElevation: number; 30 | 31 | player: { position: Point; orientation: number; inventory: SerializedObj[] }; 32 | party: SerializedObj[]; 33 | savedMaps: { [mapName: string]: SerializedMap } 34 | } 35 | 36 | function gatherSaveData(name: string): SaveGame { 37 | // Saves the game and returns the savegame 38 | 39 | const curMap = gMap.serialize(); 40 | 41 | return { version: 1 42 | , name 43 | , timestamp: Date.now() 44 | , currentElevation 45 | , currentMap: curMap.name 46 | , player: {position: player.position, orientation: player.orientation, inventory: player.inventory.map(obj => obj.serialize())} 47 | , party: gParty.serialize() 48 | , savedMaps: {[curMap.name]: curMap, ...dirtyMapCache} 49 | }; 50 | } 51 | 52 | export function formatSaveDate(save: SaveGame): string { 53 | const date = new Date(save.timestamp); 54 | return `${date.getMonth()+1}/${date.getDate()}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; 55 | } 56 | 57 | function withTransaction(f: (trans: IDBTransaction) => void, finished?: () => void) { 58 | const trans = db.transaction("saves", "readwrite"); 59 | if(finished) trans.oncomplete = finished; 60 | trans.onerror = (e: any) => { console.error("Database error: " + (e.target).errorCode) }; 61 | f(trans); 62 | } 63 | 64 | function getAll(store: IDBObjectStore, callback?: (result: T[]) => void) { 65 | const out: T[] = []; 66 | 67 | store.openCursor().onsuccess = function(e) { 68 | const cursor = (e.target).result; 69 | if(cursor) { 70 | out.push(cursor.value); 71 | cursor.continue(); 72 | } 73 | else if(callback) 74 | callback(out); 75 | }; 76 | } 77 | 78 | export function saveList(callback: (saves: SaveGame[]) => void): void { 79 | withTransaction(trans => { 80 | getAll(trans.objectStore("saves"), callback); 81 | }); 82 | } 83 | 84 | export function debugSaveList(): void { 85 | saveList((saves: SaveGame[]) => { 86 | console.log("Save List:"); 87 | for(const savegame of saves) 88 | console.log(" -", savegame.name, formatSaveDate(savegame), savegame); 89 | }); 90 | } 91 | 92 | export function debugSave(): void { 93 | save("debug", undefined, () => { console.log("[SaveLoad] Done"); }); 94 | } 95 | 96 | export function save(name: string, slot: number=-1, callback?: () => void): void { 97 | const save = gatherSaveData(name); 98 | 99 | const dirtyMapNames = Object.keys(dirtyMapCache); 100 | console.log(`[SaveLoad] Saving ${1 + dirtyMapNames.length} maps (current: ${gMap.name} plus dirty maps: ${dirtyMapNames.join(", ")})`); 101 | 102 | if(slot !== -1) 103 | save.id = slot; 104 | 105 | withTransaction(trans => { 106 | trans.objectStore("saves").put(save); 107 | 108 | console.log("[SaveLoad] Saving game data as '%s'", name); 109 | }, callback); 110 | } 111 | 112 | export function load(id: number): void { 113 | // Load stored savegame with id 114 | 115 | withTransaction(trans => { 116 | trans.objectStore("saves").get(id).onsuccess = function(e) { 117 | const save: SaveGame = (e.target).result; 118 | const savedMap = save.savedMaps[save.currentMap]; 119 | 120 | console.log("[SaveLoad] Loading save #%d ('%s') from %s", id, save.name, formatSaveDate(save)); 121 | 122 | gMap.deserialize(savedMap); 123 | console.log("[SaveLoad] Finished map deserialization"); 124 | 125 | // TODO: Properly (de)serialize the player! 126 | player.position = save.player.position; 127 | player.orientation = save.player.orientation; 128 | player.inventory = save.player.inventory.map(obj => deserializeObj(obj)); 129 | 130 | gParty.deserialize(save.party); 131 | 132 | gMap.changeElevation(save.currentElevation, false); 133 | 134 | // populate dirty map cache out of non-current saved maps 135 | dirtyMapCache = {...save.savedMaps}; 136 | delete dirtyMapCache[savedMap.name]; 137 | 138 | console.log("[SaveLoad] Finished loading map %s", savedMap.name); 139 | }; 140 | }); 141 | } 142 | 143 | export function init(): void { 144 | const request = indexedDB.open("darkfo", 1); 145 | 146 | request.onupgradeneeded = function() { 147 | const db = request.result; 148 | const store = db.createObjectStore("saves", {keyPath: "id", autoIncrement: true}); 149 | }; 150 | 151 | request.onsuccess = function() { 152 | db = request.result; 153 | 154 | db.onerror = function(e) { 155 | console.error("Database error: " + (e.target).errorCode); 156 | }; 157 | 158 | console.log("Established DB connection"); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/skillDependencies.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 darkf, Stratege 3 | Copyright 2015 darkf 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | // Skill Dependencies system 19 | 20 | enum StatType { STR, PER, END, CHR, INT, AGI, LCK, One } 21 | 22 | class Skill { 23 | constructor(public startValue: number, public dependencies: Dependency[]) { 24 | } 25 | } 26 | 27 | class Dependency { 28 | constructor(public statType: string, public multiplier: number) { 29 | } 30 | } 31 | 32 | class Stat { 33 | constructor(public min: number, public max: number, public defaultValue: number, public dependencies: Dependency[]) { 34 | } 35 | } 36 | 37 | // Skills 38 | 39 | // Fallout 2 specific, FO1 uses its own, possibly extracting this to an outside file that is loaded in would thus make sense 40 | const skillDependencies: { [name: string]: Skill } = { 41 | "Small Guns": new Skill(5, [new Dependency("AGI", 4)]), 42 | "Big Guns": new Skill(0, [new Dependency("AGI", 2)]), 43 | "Energy Weapons": new Skill(0, [new Dependency("AGI", 2)]), 44 | "Unarmed": new Skill(30, [new Dependency("AGI", 2), new Dependency("STR", 2)]), 45 | "Melee Weapons": new Skill(20, [new Dependency("AGI", 2), new Dependency("STR", 2)]), 46 | "Throwing": new Skill(0, [new Dependency("AGI", 4)]), 47 | "First Aid": new Skill(0, [new Dependency("PER", 2), new Dependency("INT", 2)]), 48 | "Doctor": new Skill(5, [new Dependency("PER", 1), new Dependency("INT", 1)]), 49 | "Sneak": new Skill(5, [new Dependency("AGI", 3)]), 50 | "Lockpick": new Skill(10, [new Dependency("PER",1), new Dependency("AGI", 1)]), 51 | "Steal": new Skill(0, [new Dependency("AGI", 3)]), 52 | "Traps": new Skill(10, [new Dependency("PER", 1), new Dependency("AGI", 1)]), 53 | "Science": new Skill(0, [new Dependency("INT", 4)]), 54 | "Repair": new Skill(0, [new Dependency("INT", 3)]), 55 | "Speech": new Skill(0, [new Dependency("CHA", 5)]), 56 | "Barter": new Skill(0, [new Dependency("CHA", 4)]), 57 | "Gambling": new Skill(5, [new Dependency("LUK", 5)]), 58 | "Outdoorsman": new Skill(0, [new Dependency("END", 2), new Dependency("INT", 2)]), 59 | }; 60 | 61 | // Stats 62 | 63 | const statDependencies: { [name: string]: Stat } = { 64 | "STR": new Stat(1, 10, 5, []), 65 | "PER": new Stat(1, 10, 5, []), 66 | "END": new Stat(1, 10, 5, []), 67 | "CHA": new Stat(1, 10, 5, []), 68 | "INT": new Stat(1, 10, 5, []), 69 | "AGI": new Stat(1, 10, 5, []), 70 | "LUK": new Stat(1, 10, 5, []), 71 | 72 | "Max HP": new Stat(0, 999, 0, [new Dependency('One', 15), new Dependency('END', 2), new Dependency('STR', 2)]), 73 | "AP": new Stat(1, 99, 0, [new Dependency('One', 5), new Dependency('AGI', 0.5)]), 74 | "AC": new Stat(0, 999, 0, [new Dependency('AGI', 1)]), 75 | "Melee": new Stat(1, 500, 0, [new Dependency('One', -5), new Dependency('STR', 1)]), 76 | "Carry": new Stat(0, 999, 0, [new Dependency('One', 25), new Dependency('STR', 25)]), 77 | "Sequence": new Stat(0, 60, 0, [new Dependency('PER', 2)]), 78 | "Healing Rate": new Stat(1, 30, 0, [new Dependency('END', 1/3)]), 79 | "Critical Chance": new Stat(0, 100, 0, [new Dependency('LUK', 1)]), 80 | "Better Criticals": new Stat(-60, 100, 0, []), 81 | "DT EMP": new Stat(0, 100, 0, []), 82 | "DT Electrical": new Stat(0, 100, 0, []), 83 | "DT Explosive": new Stat(0, 100, 0, []), 84 | "DT Fire": new Stat(0, 100, 0, []), 85 | "DT Laser": new Stat(0, 100, 0, []), 86 | "DT Normal": new Stat(0, 100, 0, []), 87 | "DT Plasma": new Stat(0, 100, 0, []), 88 | "DR EMP": new Stat(0, 100, 0, []), 89 | "DR Electrical": new Stat(0, 90, 0, []), 90 | "DR Explosive": new Stat(0, 90,0,[]), 91 | "DR Fire": new Stat(0, 90, 0, []), 92 | "DR Laser": new Stat(0, 90, 0, []), 93 | "DR Normal": new Stat(0, 90, 0, []), 94 | "DR Plasma": new Stat(0, 90, 0, []), 95 | "DR Radiation": new Stat(0, 95, 0, [new Dependency('END', 2)]), 96 | "DR Poison": new Stat(0, 95, 0, [new Dependency('END', 5)]), 97 | "Age": new Stat(16, 101, 25, []), 98 | "Gender": new Stat(0, 1, 0, []), 99 | //todo: figure out HP., 100 | "HP": new Stat(0, 999, 1, []), 101 | "Poison Level": new Stat(0, 2000, 0, []), 102 | "Radiation Level": new Stat(0, 2000, 0, []), 103 | "Skill Points": new Stat(0, 999999, 0, []), 104 | "Level": new Stat(1, 99, 1, []), 105 | "Experience": new Stat(0, 99999999, 0, []), 106 | "Reputation": new Stat(-20, 20, 0, []), 107 | "Karma": new Stat(-99999999, 99999999, 0, []), 108 | }; 109 | 110 | // TODO: figure out what is going on with Skill 111 | // all the weird pseudo stats 112 | //statDependencies['Party Limit'] = new Stat(0, 5, 0, [new Dependency('CHA', 0.5)]) 113 | //statDependencies['Skill Rate'] = new Skill(0, Math.pow(2, 31-1), 0, [new Dependency('IN', 2), new Dependency('One', 5)]) 114 | //statDependencies['Perk Rate'] = new Skill(1, Math.pow(2, 31-1), 0, [new Dependency('One', 3)]) 115 | 116 | //helper 117 | statDependencies['One'] = new Stat(1, 1, 1, []) 118 | 119 | function skillImprovementCost(skillPoints: number): number { 120 | // Fallout 2 specific, in FO1 it's always 1 121 | 122 | if(skillPoints < 101) return 1; 123 | if(skillPoints < 126) return 2; 124 | if(skillPoints < 151) return 3; 125 | if(skillPoints < 176) return 4; 126 | if(skillPoints < 201) return 5; 127 | if(skillPoints < 301) return 6; 128 | return 999999999; 129 | } 130 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 darkf, Stratege 3 | Copyright 2015 darkf 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | */ 17 | 18 | // Utility functions 19 | 20 | function parseIni(text: string) { 21 | // Parse a .ini-style categorized key-value format 22 | const ini: { [category: string]: any } = {} 23 | const lines = text.split('\n') 24 | let category = null 25 | 26 | for(var i = 0; i < lines.length; i++) { 27 | var line = lines[i].replace(/\s*;.*/, "") // replace comments 28 | if(line.trim() === '') { } 29 | else if(line[0] === '[') 30 | category = line.trim().slice(1, -1) 31 | else { 32 | // key=value 33 | var kv = line.match(/(.+?)=(.+)/) 34 | if(kv === null) { // MAPS.TXT has one of these, so it's not an exception 35 | console.log("warning: parseIni: not a key=value line: " + line) 36 | continue 37 | } 38 | if(category === null) throw "parseIni: key=value not in category: " + line 39 | 40 | if(ini[category] === undefined) ini[category] = {} 41 | ini[category][kv[1]] = kv[2] 42 | } 43 | } 44 | 45 | return ini 46 | } 47 | 48 | function getFileText(path: string, err?: () => void): string { 49 | const xhr = new XMLHttpRequest(); 50 | xhr.open("GET", path, false); 51 | xhr.send(null); 52 | if(xhr.status !== 200) 53 | throw Error(`getFileText: got status ${xhr.status} when requesting '${path}'`); 54 | 55 | return xhr.responseText; 56 | } 57 | 58 | function getFileJSON(path: string, err?: () => void): any { 59 | return JSON.parse(getFileText(path, err)); 60 | } 61 | 62 | // GET binary data into a DataView 63 | function getFileBinaryAsync(path: string, callback: (data: DataView) => void) { 64 | const xhr = new XMLHttpRequest(); 65 | xhr.open("GET", path, true); 66 | xhr.responseType = "arraybuffer"; 67 | xhr.onload = (evt) => { callback(new DataView(xhr.response)); }; 68 | xhr.send(null); 69 | } 70 | 71 | function getFileBinarySync(path: string) { 72 | // Synchronous requests aren't allowed by browsers to define response types 73 | // in a misguided attempt to force developers to switch to asynchronous requests, 74 | // so we transfer as a user-defined charset and then decode it manually. 75 | 76 | const xhr = new XMLHttpRequest(); 77 | xhr.open("GET", path, false); 78 | // Tell browser not to mess with the response type/encoding 79 | xhr.overrideMimeType("text/plain; charset=x-user-defined"); 80 | xhr.send(null); 81 | 82 | if(xhr.status !== 200) 83 | throw Error(`getFileBinarySync: got status ${xhr.status} when requesting '${path}'`); 84 | 85 | // Convert to ArrayBuffer, and then to DataView 86 | const data = xhr.responseText; 87 | const buffer = new ArrayBuffer(data.length); 88 | const arr = new Uint8Array(buffer); 89 | 90 | for(let i = 0; i < data.length; i++) 91 | arr[i] = data.charCodeAt(i) & 0xff; 92 | 93 | return new DataView(buffer); 94 | } 95 | 96 | // Min inclusive, max inclusive 97 | function getRandomInt(min: number, max: number) { 98 | return Math.floor(Math.random() * (max - min + 1)) + min 99 | } 100 | 101 | function rollSkillCheck(skill: number, modifier: number, isBounded: boolean) { 102 | const tempSkill = skill + modifier 103 | if(isBounded) 104 | clamp(0, 95, tempSkill) 105 | 106 | const roll = getRandomInt(0,100) 107 | return roll < tempSkill 108 | } 109 | 110 | function rollVsSkill(who: Critter, skill: string, modifier: number=0) { 111 | var skillLevel = critterGetSkill(who, skill) + modifier 112 | var roll = skillLevel - getRandomInt(1, 100) 113 | 114 | if(roll <= 0) { // failure 115 | if((-roll)/10 > getRandomInt(1, 100)) 116 | return 0 // critical failure 117 | return 1 // failure 118 | } 119 | else { // success 120 | var critChance = critterGetStat(who, "Critical Chance") 121 | if((roll/10 + critChance) > getRandomInt(1, 100)) 122 | return 3 // critical success 123 | return 2 // success 124 | } 125 | } 126 | 127 | function rollIsSuccess(roll: number) { 128 | return (roll == 2) || (roll == 3) 129 | } 130 | 131 | function rollIsCritical(roll: number) { 132 | return (roll == 0) || (roll == 3) 133 | } 134 | 135 | function arrayRemove(array: T[], value: T) { 136 | const index = array.indexOf(value) 137 | if(index !== -1) { 138 | array.splice(index, 1) 139 | return true 140 | } 141 | return false 142 | } 143 | 144 | function arrayWithout(array: T[], value: T): T[] { 145 | return array.filter(x => x !== value); 146 | } 147 | 148 | function arrayIncludes(array: T[], value: T): boolean { 149 | return array.indexOf(value) !== -1; 150 | } 151 | 152 | function clamp(min: number, max: number, value: number) { 153 | return Math.max(min, Math.min(max, value)) 154 | } 155 | 156 | function getMessage(name: string, id: number): string|null { 157 | if(messageFiles[name] !== undefined && messageFiles[name][id] !== undefined) 158 | return messageFiles[name][id] 159 | else { 160 | loadMessage(name) 161 | if(messageFiles[name] !== undefined && messageFiles[name][id] !== undefined) 162 | return messageFiles[name][id] 163 | else return null 164 | } 165 | } 166 | 167 | function getProtoMsg(id: number) { 168 | return getMessage("proto", id) 169 | } 170 | 171 | function pad(n: any, width: number, z?: string) { 172 | z = z || '0'; 173 | n = n + ''; 174 | return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; 175 | } 176 | 177 | class BinaryReader { 178 | data: DataView 179 | offset: number = 0 180 | length: number 181 | 182 | constructor(data: DataView) { 183 | this.data = data 184 | this.length = data.byteLength 185 | } 186 | 187 | seek(offset: number) { this.offset = offset } 188 | read8(): number { return this.data.getUint8(this.offset++) } 189 | read16(): number { var r = this.data.getUint16(this.offset); this.offset += 2; return r } 190 | read32(): number { var r = this.data.getUint32(this.offset); this.offset += 4; return r } 191 | 192 | peek8(): number { return this.data.getUint8(this.offset) } 193 | peek16(): number { return this.data.getUint16(this.offset) } 194 | peek32(): number { return this.data.getUint32(this.offset) } 195 | } 196 | 197 | function assert(value: boolean, message: string) { 198 | if(!value) 199 | throw "AssertionError: " + message 200 | } 201 | 202 | function assertEq(value: T, expected: T, message: string) { 203 | if(value !== expected) 204 | throw `AssertionError: value (${value}) does not match expected (${expected}): ${message}` 205 | } 206 | 207 | function jQuery_isPlainObject(obj: any): boolean { 208 | var proto, Ctor; 209 | 210 | // Detect obvious negatives 211 | // Use toString instead of jQuery.type to catch host objects 212 | if ( !obj || toString.call( obj ) !== "[object Object]" ) { 213 | return false; 214 | } 215 | 216 | proto = Object.getPrototypeOf( obj ); 217 | 218 | // Objects with no prototype (e.g., `Object.create( null )`) are plain 219 | if ( !proto ) { 220 | return true; 221 | } 222 | 223 | // Objects with prototype are plain iff they were constructed by a global Object function 224 | Ctor = Object.hasOwnProperty.call( proto, "constructor" ) && proto.constructor; 225 | return typeof Ctor === "function" && Object.toString.call( Ctor ) === Object.toString.call(Object); 226 | } 227 | 228 | function jQuery_extend(this: any, deep: boolean, target: any, obj: any): any { 229 | var options, name, src, copy, copyIsArray, clone, 230 | target = arguments[ 0 ] || {}, 231 | i = 1, 232 | length = arguments.length, 233 | deep = false; 234 | 235 | // Handle a deep copy situation 236 | if ( typeof target === "boolean" ) { 237 | deep = target; 238 | 239 | // Skip the boolean and the target 240 | target = arguments[ i ] || {}; 241 | i++; 242 | } 243 | 244 | // Handle case when target is a string or something (possible in deep copy) 245 | if ( typeof target !== "object" && typeof(target) !== "function") { 246 | target = {}; 247 | } 248 | 249 | // Extend jQuery itself if only one argument is passed 250 | if ( i === length ) { 251 | target = this; 252 | i--; 253 | } 254 | 255 | for ( ; i < length; i++ ) { 256 | 257 | // Only deal with non-null/undefined values 258 | if ( ( options = arguments[ i ] ) != null ) { 259 | 260 | // Extend the base object 261 | for ( name in options ) { 262 | src = target[ name ]; 263 | copy = options[ name ]; 264 | 265 | // Prevent never-ending loop 266 | if ( target === copy ) { 267 | continue; 268 | } 269 | 270 | // Recurse if we're merging plain objects or arrays 271 | if ( deep && copy && ( jQuery_isPlainObject( copy ) || 272 | ( copyIsArray = Array.isArray( copy ) ) ) ) { 273 | 274 | if ( copyIsArray ) { 275 | copyIsArray = false; 276 | clone = src && Array.isArray( src ) ? src : []; 277 | 278 | } else { 279 | clone = src && jQuery_isPlainObject( src ) ? src : {}; 280 | } 281 | 282 | // Never move original objects, clone them 283 | target[ name ] = jQuery_extend( deep, clone, copy ); 284 | 285 | // Don't bring in undefined values 286 | } else if ( copy !== undefined ) { 287 | target[ name ] = copy; 288 | } 289 | } 290 | } 291 | } 292 | 293 | // Return the modified object 294 | return target; 295 | }; 296 | 297 | function deepClone(obj: T): T { 298 | return jQuery_extend(true, {}, obj); 299 | } 300 | 301 | function isNumeric(str: string): boolean { 302 | return !isNaN((str as any) - parseFloat(str)); 303 | } 304 | -------------------------------------------------------------------------------- /src/vm.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2015 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Scripting VM for .INT files 18 | 19 | function binop(f: (x: any, y: any) => any) { 20 | return function(this: ScriptVM) { 21 | var rhs = this.pop() 22 | this.push(f(this.pop(), rhs)) 23 | } 24 | } 25 | 26 | var opMap: { [opcode: number]: (this: ScriptVM) => void } = { 27 | 0x8002: function() { } // start critical (nop) 28 | ,0xC001: function() { this.push(this.script.read32()) } // op_push_d 29 | ,0x800D: function() { this.retStack.push(this.pop()) } // op_d_to_a 30 | ,0x800C: function() { this.push(this.popAddr()) } // op_a_to_d 31 | ,0x801A: function() { this.pop() } // op_pop 32 | ,0x8004: function() { this.pc = this.pop() } // op_jmp 33 | ,0x8003: function() { } // op_critical_done (nop) 34 | ,0x802B: function() { // op_push_base 35 | var argc = this.pop() 36 | this.retStack.push(this.dvarBase) 37 | this.dvarBase = this.dataStack.length - argc 38 | // console.log("op_push_base (argc %d)", argc) 39 | } 40 | ,0x8019: function() { // op_swapa 41 | var a = this.popAddr() 42 | var b = this.popAddr() 43 | this.retStack.push(a) 44 | this.retStack.push(b) 45 | } 46 | ,0x802A: function() { this.dataStack.splice(this.dvarBase) } // op_pop_to_base 47 | ,0x8029: function() { this.dvarBase = this.popAddr() } // op_pop_base 48 | ,0x802C: function() { this.svarBase = this.dataStack.length } // op_set_global 49 | ,0x8013: function() { var num = this.pop(); this.dataStack[this.svarBase + num] = this.pop() } // op_store_global 50 | ,0x8012: function() { var num = this.pop(); this.push(this.dataStack[this.svarBase + num]) } // op_fetch_global 51 | ,0x801C: function() { // op_pop_return 52 | var addr = this.popAddr() 53 | if(addr === -1) 54 | this.halted = true 55 | else 56 | this.pc = addr 57 | } 58 | ,0x8010: function() { this.halted = true; /*console.log("op_exit_prog")*/ } // op_exit_prog 59 | 60 | ,0x802F: function() { if(!this.pop()) { this.pc = this.pop() } else this.pop() } // op_if 61 | ,0x8031: function() { var varNum = this.pop(); this.dataStack[this.dvarBase + varNum] = this.pop() } // op_store 62 | ,0x8032: function() { this.push(this.dataStack[this.dvarBase + this.pop()]) } // op_fetch 63 | ,0x8046: function() { this.push(-this.pop()) } // op_negate 64 | ,0x8044: function() { this.push(Math.floor(this.pop())) } // op_floor (TODO: should we truncate? Test negatives) 65 | ,0x801B: function() { this.push(this.dataStack[this.dataStack.length-1]) } // op_dup 66 | 67 | ,0x8030: function() { // op_while 68 | var cond = this.pop() 69 | if(!cond) { 70 | var pc = this.pop() 71 | this.pc = pc 72 | } 73 | } 74 | 75 | ,0x8028: function() { // op_lookup_string_proc (look up procedure index by name) 76 | this.push(this.intfile.procedures[this.pop()].index) 77 | } 78 | ,0x8027: function() { // op_check_arg_count 79 | var argc = this.pop() 80 | var procIdx = this.pop() 81 | var proc = this.intfile.proceduresTable[procIdx] 82 | console.log("CHECK ARGS: argc=%d procIdx=%d, proc=%o", argc, procIdx, proc) 83 | if(argc !== proc.argc) 84 | throw `vm error: expected ${proc.argc} args, got ${argc} args when calling ${proc.name}` 85 | } 86 | 87 | //,0x806B: function() { console.log("DISPLAY: %s", this.pop()) } 88 | 89 | ,0x8005: function() { // op_call (TODO: verify) 90 | // the script should have already pushed the return value (and possibly argc) 91 | this.pc = this.intfile.proceduresTable[this.pop()].offset 92 | } 93 | ,0x9001: function() { 94 | // push a string from either the strings or identifiers table. 95 | // normally Fallout 2 checks the type of the operand per-instruction 96 | // and treats the actual operand however it wants. in this case, 97 | // it will either be treated like a string, or an identifier. 98 | // 99 | // we just check the next instruction and match it up with the set of 100 | // instructions who use it as an identifier (whom use interpretGetName). 101 | 102 | var num = this.script.read32() 103 | var nextOpcode = this.script.peek16() 104 | 105 | if(arrayIncludes([0x8014 // op_fetch_external 106 | ,0x8015 // op_store_external 107 | ,0x8016 // op_export_var 108 | //,0x8017 // op_export_proc (TODO: verify) 109 | //,0x8005: // op_call (TODO: verify, might need more operands) 110 | ], nextOpcode)) { 111 | // fetch an identifier 112 | if(this.intfile.identifiers[num] === undefined) 113 | throw Error("ScriptVM: 9001 requested identifier " + num + " but it doesn't exist"); 114 | this.push(this.intfile.identifiers[num]) 115 | } 116 | else { 117 | // fetch a string 118 | if(this.intfile.strings[num] === undefined) 119 | throw Error("ScriptVM: 9001 requested string " + num + " but it doesn't exist"); 120 | this.push(this.intfile.strings[num]) 121 | } 122 | } 123 | 124 | // logic/comparison 125 | ,0x8045: function() { this.push(!this.pop()) } 126 | ,0x8033: binop(function(x,y) { return x == y }) 127 | ,0x8034: binop(function(x,y) { return x != y }) 128 | ,0x8035: binop(function(x,y) { return x <= y }) 129 | ,0x8036: binop(function(x,y) { return x >= y }) 130 | ,0x8037: binop(function(x,y) { return x < y }) 131 | ,0x8038: binop(function(x,y) { return x > y }) 132 | ,0x803E: binop(function(x,y) { return x && y }) 133 | ,0x803F: binop(function(x,y) { return x || y }) 134 | ,0x8040: binop(function(x,y) { return x & y }) 135 | ,0x8041: binop(function(x,y) { return x | y }) 136 | ,0x8039: binop(function(x,y) { return x + y }) 137 | ,0x803A: binop(function(x,y) { return x - y }) 138 | ,0x803B: binop(function(x,y) { return x * y }) 139 | ,0x803d: binop(function(x,y) { return x % y }) 140 | ,0x803C: binop(function(x,y) { return x / y | 0 }) // TODO: truncate or not? 141 | } 142 | 143 | class ScriptVM { 144 | script: BinaryReader 145 | intfile: IntFile 146 | pc: number = 0 147 | dataStack: any[] = [] 148 | retStack: number[] = [] 149 | svarBase: number = 0 150 | dvarBase: number = 0 151 | halted: boolean = false 152 | 153 | constructor(script: BinaryReader, intfile: IntFile) { 154 | this.script = script 155 | this.intfile = intfile 156 | } 157 | 158 | push(value: any): void { 159 | this.dataStack.push(value) 160 | } 161 | 162 | pop(): any { 163 | if(this.dataStack.length === 0) 164 | throw "VM data stack underflow" 165 | return this.dataStack.pop() 166 | } 167 | 168 | popAddr(): any { 169 | if(this.retStack.length === 0) 170 | throw "VM return stack underflow" 171 | return this.retStack.pop() 172 | } 173 | 174 | dis(): string { 175 | var offset = this.script.offset 176 | var disassembly = disassemble(this.intfile, this.script) 177 | this.script.seek(offset) 178 | return disassembly 179 | } 180 | 181 | // call a named procedure 182 | call(procName: string, args: any[]=[]): any { 183 | var proc = this.intfile.procedures[procName] 184 | // console.log("CALL " + procName + " @ " + proc.offset + " from " + this.scriptObj.scriptName) 185 | if(!proc) 186 | throw "ScriptVM: unknown procedure " + procName 187 | 188 | // TODO: which way are args passed on the stack? 189 | args.reverse() 190 | args.forEach(arg => this.push(arg)) 191 | this.push(args.length) 192 | 193 | this.retStack.push(-1) // push return address (TODO: how is this handled?) 194 | 195 | // run procedure code 196 | this.pc = proc.offset 197 | this.run() 198 | 199 | return this.pop() 200 | } 201 | 202 | step(): boolean { 203 | if(this.halted) 204 | return false 205 | 206 | // fetch op 207 | var pc = this.pc 208 | this.script.seek(pc) 209 | var opcode = this.script.read16() 210 | 211 | // dispatch based on opMap 212 | if(opMap[opcode] !== undefined) 213 | opMap[opcode].call(this) 214 | else { 215 | console.warn("unimplemented opcode %s (pc=%s) in %s", opcode.toString(16), this.pc.toString(16), this.intfile.name) 216 | if(Config.engine.doDisasmOnUnimplOp) { 217 | console.log("disassembly:") 218 | console.log(disassemble(this.intfile, this.script)) 219 | } 220 | return false 221 | } 222 | 223 | if(this.pc === pc) // PC wasn't explicitly set, let's advance it to the current file offset 224 | this.pc = this.script.offset 225 | return true 226 | } 227 | 228 | run(): void { 229 | this.halted = false 230 | while(this.step()) { } 231 | } 232 | } -------------------------------------------------------------------------------- /stitchWorldmap.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2014 darkf 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | """ 16 | 17 | from PIL import Image 18 | 19 | def main(): 20 | # 20 tiles, 7x6 squares each 21 | # each tile is 350x300 22 | # 4 tiles horizontally, 5 vertically 23 | 24 | img = Image.new("RGBA", (350*4, 300*5)) 25 | 26 | for i in range(0, 20): 27 | print i 28 | tilePath = "art/intrface/wrldmp%s.png" % str(i).zfill(2) 29 | tile = Image.open(tilePath) 30 | 31 | x = i % 4 32 | y = i / 4 33 | 34 | img.paste(tile, (x*350, y*300)) 35 | 36 | img.save("worldmap.png") 37 | 38 | 39 | if __name__ == '__main__': 40 | main() -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "removeComments": true, 4 | "preserveConstEnums": true, 5 | "noImplicitReturns": true, 6 | "alwaysStrict": true, // emit "use strict" 7 | "skipLibCheck": true, 8 | "noImplicitAny": true, 9 | "noImplicitThis": true, 10 | "outDir": "js", 11 | // "target": "es6", 12 | // "strict": true, 13 | "lib": ["es6", "dom", "es2017.object"] 14 | }, 15 | 16 | "files": [ 17 | "src/config.ts", 18 | 19 | "src/util.ts", 20 | "src/geometry.ts", 21 | 22 | "src/pro.ts", 23 | 24 | "src/scripting.ts", 25 | "src/intfile.ts", 26 | "src/dis.ts", 27 | "src/vm.ts", 28 | "src/vm_bridge.ts", 29 | 30 | "src/object.ts", 31 | "src/critter.ts", 32 | 33 | "src/criticalEffects.ts", 34 | "src/skillDependencies.ts", 35 | "src/char.ts", 36 | 37 | "src/data.ts", 38 | "src/worldmap.ts", 39 | "src/encounters.ts", 40 | 41 | "src/lightmap.ts", 42 | "src/lighting.ts", 43 | 44 | "src/renderer.ts", 45 | "src/canvasrenderer.ts", 46 | "src/webglrenderer.ts", 47 | 48 | "src/player.ts", 49 | "src/party.ts", 50 | "src/combat.ts", 51 | 52 | "src/ui.ts", 53 | "src/audio.ts", 54 | 55 | "src/saveload.ts", 56 | "src/idbcache.ts", 57 | 58 | "src/events.ts", 59 | 60 | "src/net.ts", 61 | 62 | "src/map.ts", 63 | "src/main.ts" 64 | ] 65 | } -------------------------------------------------------------------------------- /wrappers/macos/darkfo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /wrappers/macos/darkfo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // darkfo 4 | // 5 | // Created by Max Desiatov on 28/02/2019. 6 | // Copyright © 2019 Max Desiatov. DarkFO is licensed under the terms of the 7 | // Apache 2 license. See LICENSE.txt for the full license text. 8 | // 9 | 10 | import Cocoa 11 | 12 | @NSApplicationMain 13 | class AppDelegate: NSObject, NSApplicationDelegate { 14 | @IBOutlet weak var window: NSWindow! 15 | 16 | 17 | func applicationDidFinishLaunching(_ aNotification: Notification) { 18 | window.contentViewController = ViewController() 19 | window.makeKeyAndOrderFront(self) 20 | window.setFrameOrigin(CGPoint(x: 100, y: 1000)) 21 | } 22 | 23 | func applicationWillTerminate(_ aNotification: Notification) { 24 | // Insert code here to tear down your application 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /wrappers/macos/darkfo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoadsInWebContent 8 | 9 | 10 | CFBundleDevelopmentRegion 11 | $(DEVELOPMENT_LANGUAGE) 12 | CFBundleExecutable 13 | $(EXECUTABLE_NAME) 14 | CFBundleIconFile 15 | 16 | CFBundleIdentifier 17 | $(PRODUCT_BUNDLE_IDENTIFIER) 18 | CFBundleInfoDictionaryVersion 19 | 6.0 20 | CFBundleName 21 | $(PRODUCT_NAME) 22 | CFBundlePackageType 23 | APPL 24 | CFBundleShortVersionString 25 | 1.0 26 | CFBundleVersion 27 | 1 28 | LSMinimumSystemVersion 29 | $(MACOSX_DEPLOYMENT_TARGET) 30 | NSHumanReadableCopyright 31 | Copyright © 2019 Max Desiatov. DarkFO is licensed under the terms of the Apache 2 license. See LICENSE.txt for the full license text. 32 | NSMainNibFile 33 | MainMenu 34 | NSPrincipalClass 35 | NSApplication 36 | 37 | 38 | -------------------------------------------------------------------------------- /wrappers/macos/darkfo/PreferencesController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreferencesController.swift 3 | // darkfo 4 | // 5 | // Created by Max Desiatov on 05/03/2019. 6 | // Copyright © 2019 Max Desiatov. DarkFO is licensed under the terms of the 7 | // Apache 2 license. See LICENSE.txt for the full license text. 8 | // 9 | 10 | import AppKit 11 | import Foundation 12 | 13 | final class PreferencesController: NSViewController { 14 | private let stack = NSStackView() 15 | private let label = NSTextView() 16 | private let field = NSTextField() 17 | private let button = NSButton() 18 | 19 | private let defaultValue: String 20 | private let onReload: (String) -> () 21 | 22 | init(defaultValue: String, onReload: @escaping (String) -> ()) { 23 | self.defaultValue = defaultValue 24 | self.onReload = onReload 25 | 26 | super.init(nibName: nil, bundle: nil) 27 | } 28 | 29 | required init?(coder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | override func loadView() { 34 | view = NSView() 35 | } 36 | 37 | override func viewDidLoad() { 38 | stack.distribution = .fillProportionally 39 | stack.orientation = .vertical 40 | 41 | view.addSubview(stack) 42 | stack.translatesAutoresizingMaskIntoConstraints = false 43 | 44 | label.string = "URL parameters:" 45 | label.backgroundColor = .clear 46 | label.isEditable = false 47 | 48 | field.stringValue = defaultValue 49 | 50 | button.title = "Reload" 51 | button.bezelStyle = .rounded 52 | button.target = self 53 | button.action = #selector(onButtonPress) 54 | 55 | stack.addArrangedSubview(label) 56 | stack.addArrangedSubview(field) 57 | stack.addArrangedSubview(button) 58 | 59 | NSLayoutConstraint.activate([ 60 | view.widthAnchor.constraint(equalToConstant: 320), 61 | view.heightAnchor.constraint(equalToConstant: 150), 62 | label.heightAnchor.constraint(equalToConstant: 30), 63 | stack.centerYAnchor.constraint(equalTo: view.centerYAnchor), 64 | stack.centerXAnchor.constraint(equalTo: view.centerXAnchor), 65 | stack.widthAnchor.constraint(equalToConstant: 200) 66 | ]) 67 | 68 | field.becomeFirstResponder() 69 | } 70 | 71 | @objc func onButtonPress() { 72 | onReload(field.stringValue) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /wrappers/macos/darkfo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // darkfo 4 | // 5 | // Created by Max Desiatov on 28/02/2019. 6 | // Copyright © 2019 Max Desiatov. DarkFO is licensed under the terms of the 7 | // Apache 2 license. See LICENSE.txt for the full license text. 8 | // 9 | 10 | import AppKit 11 | import Foundation 12 | import WebKit 13 | 14 | final class ViewController: NSViewController { 15 | private let webView = WKWebView(frame: .zero) 16 | private var preferences: NSWindowController? 17 | 18 | private var urlParameter = "artemple" 19 | init() { 20 | super.init(nibName: nil, bundle: nil) 21 | } 22 | 23 | override func loadView() { 24 | view = NSView() 25 | } 26 | 27 | override func viewDidLoad() { 28 | webView.translatesAutoresizingMaskIntoConstraints = false 29 | 30 | view.addSubview(webView) 31 | 32 | NSLayoutConstraint.activate([ 33 | view.widthAnchor.constraint(greaterThanOrEqualToConstant: 800), 34 | // for some reason 700 leaves empty pixels at the bottom 35 | view.heightAnchor.constraint(greaterThanOrEqualToConstant: 699), 36 | 37 | webView.topAnchor.constraint(equalTo: view.topAnchor), 38 | webView.bottomAnchor.constraint(equalTo: view.bottomAnchor), 39 | webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), 40 | webView.trailingAnchor.constraint(equalTo: view.trailingAnchor) 41 | ]) 42 | 43 | reload() 44 | } 45 | 46 | required init?(coder: NSCoder) { 47 | fatalError("init(coder:) has not been implemented") 48 | } 49 | 50 | private func reload() { 51 | let req = URLRequest(url: 52 | URL(string: "http://localhost:8000/play.html?\(urlParameter)")! 53 | ) 54 | webView.load(req) 55 | } 56 | 57 | @IBAction func preferencesPressed(_ sender: NSMenuItem) { 58 | defer { preferences?.window?.makeKey() } 59 | 60 | guard preferences == nil else { 61 | return 62 | } 63 | 64 | let window = NSWindow(contentViewController: PreferencesController( 65 | defaultValue: urlParameter 66 | ) { [weak self] in 67 | self?.urlParameter = $0 68 | self?.reload() 69 | }) 70 | window.title = "Preferences" 71 | window.delegate = self 72 | 73 | preferences = NSWindowController(window: window) 74 | preferences?.showWindow(self) 75 | } 76 | } 77 | 78 | extension ViewController: NSWindowDelegate { 79 | func windowWillClose(_ notification: Notification) { 80 | preferences = nil 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /wrappers/macos/darkfo/darkfo.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | --------------------------------------------------------------------------------