├── .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 |
186 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
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 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
262 |
263 |
264 |
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 |
--------------------------------------------------------------------------------