level scrolling
[dd2d.git] / d2dmap.d
blob59134f4c3615bd560f37a7634f10af6934645aa2
1 module d2dmap is aliced;
2 private:
4 import arsd.color;
5 import iv.stream;
7 import glutils;
8 import console;
9 import wadarc;
11 import d2dgfx;
12 //import d2dtpl;
14 import iv.encoding;
17 // ////////////////////////////////////////////////////////////////////////// //
18 Color getPixel (TrueColorImage img, int x, int y) {
19 if (x >= 0 && y >= 0 && x < img.width && y < img.height) {
20 return img.imageData.colors.ptr[y*img.width+x];
21 } else {
22 return Color(0, 0, 0, 0);
27 void putPixel (TrueColorImage img, int x, int y, Color clr) {
28 if (x >= 0 && y >= 0 && x < img.width && y < img.height && clr.a != 0) {
29 img.imageData.colors.ptr[y*img.width+x] = clr;
34 void fixPixelA (TrueColorImage img, int x, int y, ubyte a) {
35 if (x >= 0 && y >= 0 && x < img.width && y < img.height) {
36 img.imageData.colors.ptr[y*img.width+x].a = a;
41 void fixPixelTileA (TrueColorImage img, int x, int y, ubyte a) {
42 foreach (int dy; 0..8) {
43 foreach (int dx; 0..8) {
44 fixPixelA(img, x+dx, y+dy, a);
50 void putPixelTile (TrueColorImage img, int x, int y, Color clr) {
51 foreach (int dy; 0..8) {
52 foreach (int dx; 0..8) {
53 putPixel(img, x+dx, y+dy, clr);
59 void putTile (TrueColorImage img, int x, int y, D2DImage wt) {
60 if (wt is null) return;
61 x -= wt.sx;
62 y -= wt.sy;
63 auto s = wt.data.ptr;
64 foreach (int dy; 0..wt.height) {
65 foreach (int dx; 0..wt.width) {
66 img.putPixel(x+dx, y, d2dpal[*s++]);
68 ++y;
73 // temp
74 static ubyte clampByte (ubyte b, int delta) {
75 delta += b;
76 return cast(ubyte)(delta < 0 ? 0 : delta > 255 ? 255 : delta);
80 // ////////////////////////////////////////////////////////////////////////// //
81 public final class LevelMap {
82 private:
83 private import std.stdio : File;
85 enum MapVersion = 2; // óÁÍÁÑ ÐÏÓÌÅÄÎÑÑ ×ÅÒÓÉÑ ËÁÒÔÙ
86 public enum MapSize = 100;
88 public:
89 // tile type
90 enum : ubyte {
91 TILE_EMPTY = 0,
92 TILE_WALL = 1,
93 TILE_DOORC = 2, // closed door
94 TILE_DOORO = 3, // opened door
95 TILE_STEP = 4,
96 TILE_WATER = 5,
97 TILE_ACID1 = 6,
98 TILE_ACID2 = 7,
99 TILE_MBLOCK = 8, // just blocks monsters
100 TILE_LIFTU = 9,
101 TILE_LIFTD = 10,
103 TILE_ACTTRAP = 255,
106 enum : short {
107 MB_COMMENT = -1,
108 MB_END = 0,
109 MB_WALLNAMES,
110 MB_BACK,
111 MB_WTYPE,
112 MB_FRONT,
113 MB_THING,
114 MB_SWITCH,
115 MB_MUSIC, // usually 8 bytes
116 MB_SKY, // ushort: [1..3]
117 MB_SWITCH2,
120 enum {
121 SW_PL_PRESS = 1<<0,
122 SW_MN_PRESS = 1<<1,
123 SW_PL_NEAR = 1<<2,
124 SW_MN_NEAR = 1<<3,
125 SW_KEY_R = 1<<4,
126 SW_KEY_G = 1<<5,
127 SW_KEY_B = 1<<6,
130 static struct MapThing {
131 // thing flags
132 enum : ushort {
133 DirRight = 0x0001,
134 DeathMatch = 0x0010, // ÐÏÑ×ÌÑÅÔÓÑ ÔÏÌØËÏ × DeathMatch'Å
137 short x, y; // ËÏÏÒÄÉÎÁÔÙ
138 ushort type; // ÔÉÐ
139 ushort flags; // ÆÌÁÇÉ
141 @property const pure nothrow @safe @nogc {
142 bool left () => ((flags&DirRight) == 0);
143 bool right () => ((flags&DirRight) != 0);
144 bool dmonly () => ((flags&DeathMatch) != 0);
148 static struct MapSwitch {
149 ubyte x, y; // ËÏÏÒÄÉÎÁÔÙ/8
150 ubyte type; // ÔÉÐ
151 ubyte tm; // ÄÏÌÖÎÏ ÂÙÔØ 0
152 ubyte a, b; // ÏÂÙÞÎÏ - ËÏÏÒÄÉÎÁÔÙ/8 Ä×ÅÒÉ
153 ushort c; // ÎÅ ÉÓÐÏÌØÚÕÅÔÓÑ (×ÒÏÄÅ ÂÙ)
154 ubyte flags; // ÆÌÁÇÉ (SW_*)
157 public:
158 enum { Type, Front, Back, AllLiquids, LiquidMask, LightMask, MapTexturesMax } // tile types, Front+: megatexture types
159 ubyte[][3] tiles; // Type, Front, Back
160 string[] wallnames; // "_water_0": water, "_water_1": acid, "_water_2": lava
161 ubyte[] walltypes; // 0: solid; 1: non-solid
162 D2DImage[] textures;
163 int width, height;
164 Texture[MapTexturesMax] texgl;
165 D2DImage skytex;
166 Texture skytexgl;
168 MapThing[] things;
169 MapSwitch[] switches;
171 this (string fname) { load(fname); }
172 this (File fl) { load(fl); }
174 bool hasLiquidAt (int x, int y) {
175 if (x < 0 || y < 0 || x >= MapSize || y >= MapSize) return false;
176 auto tt = tiles.ptr[Front].ptr[y*MapSize+x];
177 return (wallnames[tt] == "_water_0" || wallnames[tt] == "_water_1" || wallnames[tt] == "_water_2");
180 // build OpenGL "megatextures" with the whole level on it
181 void oglBuildMega () {
182 skytexgl = new Texture(skytex.asTCImage, Texture.Option.Linear, Texture.Option.Repeat);
183 auto img = new TrueColorImage(width*8, height*8);
184 foreach (immutable type; Front..LightMask+1) {
185 img.imageData.colors[] = Color(0, 0, 0, 0);
186 if (type == Back) {
188 foreach (int y; 0..(height*8+skytex.height-1)/skytex.height) {
189 foreach (int x; 0..(width*8+skytex.width-1)/skytex.width) {
190 img.putTile(x*skytex.width, y*skytex.height, skytex);
195 foreach (int y; 0..height) {
196 foreach (int x; 0..width) {
197 if (type == LightMask) {
198 // in the lightmask texture, we should have only occluders' pixels
199 auto tt = tiles.ptr[Type].ptr[y*MapSize+x];
200 if (tt == TILE_WALL || tt == TILE_DOORC) {
201 img.putTile(x*8, y*8, textures.ptr[tiles.ptr[Back].ptr[y*MapSize+x]]);
202 img.putTile(x*8, y*8, textures.ptr[tiles.ptr[Front].ptr[y*MapSize+x]]);
203 } /*else if (tt != TILE_LIFTU && tt != TILE_LIFTD) {
204 img.putTile(x*8, y*8, textures.ptr[tiles.ptr[Front].ptr[y*MapSize+x]]);
206 } else if (type == AllLiquids) {
207 // texture with liquid background, for distortion
208 auto tt = tiles.ptr[Front].ptr[y*MapSize+x];
209 if (wallnames[tt] == "_water_0" || wallnames[tt] == "_water_1" || wallnames[tt] == "_water_2") {
210 tt = tiles.ptr[Back].ptr[y*MapSize+x];
211 img.putTile(x*8, y*8, textures.ptr[tt]);
212 foreach (int dy; 0..8) {
213 foreach (int dx; 0..8) {
214 //img.putPixel(x*8+dx, y*8+dy, Color((tt == 3 ? 128 : 0), (tt == 2 ? 128 : 0), (tt == 1 ? 128 : 0), 128));
215 auto c = img.getPixel(x*8+dx, y*8+dy);
216 if (c.a == 0) img.putPixel(x*8+dx, y*8+dy, Color(0, 0, 0, 255));
220 } else if (type == LiquidMask) {
221 // texture with liquid colors, will be blended on top of the level
222 auto tt = tiles.ptr[Front].ptr[y*MapSize+x];
223 auto wclr = Color(0, 0, 0, 0);
224 if (wallnames[tt] == "_water_0") wclr = Color(0, 0, 100, 128); // water
225 else if (wallnames[tt] == "_water_1") wclr = Color(24, 140, 0, 128); // acid
226 else if (wallnames[tt] == "_water_2") wclr = Color(160, 0, 0, 128); // lava
227 if (wclr.a) {
228 img.putPixelTile(x*8, y*8, wclr);
229 // if this is top one, make some light border
230 if (!hasLiquidAt(x, y-1)) {
231 // border
232 auto wcc = wclr.lighten(0.6);
233 wcc.a = wclr.a;
234 foreach (int dx; 0..8) img.putPixel(x*8+dx, y*8+0, wcc);
235 wcc = wclr.lighten(0.4);
236 wcc.a = wclr.a;
237 foreach (int dx; 0..8) img.putPixel(x*8+dx, y*8+1, wcc);
238 wcc = wclr.lighten(0.2);
239 wcc.a = wclr.a;
240 foreach (int dx; 0..8) img.putPixel(x*8+dx, y*8+2, wcc);
243 } else {
244 auto tf = tiles.ptr[Front].ptr[y*MapSize+x];
245 auto tb = tiles.ptr[Back].ptr[y*MapSize+x];
246 auto tt = tiles.ptr[type].ptr[y*MapSize+x];
247 if (wallnames[tf] == "_water_0" || wallnames[tf] == "_water_1" || wallnames[tf] == "_water_2" ||
248 wallnames[tb] == "_water_0" || wallnames[tb] == "_water_1" || wallnames[tb] == "_water_2") {
249 } else {
250 img.putTile(x*8, y*8, textures.ptr[tt]);
256 import std.string : format;
257 import arsd.png : writePng;
258 writePng("zpng%02s.png".format(type), img);
260 texgl[type] = new Texture(img, Texture.Option.Nearest, Texture.Option.Clamp);
264 void clear () {
265 tiles[] = null;
266 wallnames = null;
267 walltypes = null;
268 textures = null;
269 foreach (Texture tex; texgl) if (tex !is null) tex.clear;
270 texgl[] = null;
271 skytex = null;
272 things = null;
273 switches = null;
276 void dump (int idx) {
277 static char to62 (ubyte b) { pragma(inline, true); return cast(char)(b < 10 ? '0'+b : (b-10 < 26 ? 'A'+(b-10) : 'a'+(b-10-26))); }
278 foreach (immutable y; 0..MapSize) {
279 foreach (immutable x; 0..MapSize) {
280 conwrite(to62(tiles[idx][y*MapSize+x]));
282 conwriteln;
286 // true: found
287 bool getThingPos (ushort id, int* x=null, int* y=null, ushort* flags=null) {
288 foreach (ref th; things[]) {
289 if (th.type == id) {
290 if (x !is null) *x = th.x;
291 if (y !is null) *y = th.y;
292 if (flags !is null) *flags = th.flags;
293 return true;
296 if (x !is null) *x = 0;
297 if (y !is null) *y = 0;
298 if (flags !is null) *flags = 0;
299 return false;
302 private:
303 void calcMapSize () {
304 bool isEmpty(string dir) (int x, int y) if (dir == "col" || dir == "row") {
305 while (x < MapSize && y < MapSize) {
306 if (tiles[0][y*MapSize+x] || tiles[1][y*MapSize+x] || tiles[2][y*MapSize+x]) return false;
307 static if (dir == "row") ++x; else ++y;
309 return true;
311 width = height = MapSize;
312 // fix width
313 while (width > 0 && isEmpty!"col"(width-1, 0)) --width;
314 // fix height
315 while (height > 0 && isEmpty!"row"(0, height-1)) --height;
318 void load (string fname) {
319 import std.stdio : File;
320 load(openFile(fname));
323 void load(ST) (auto ref ST st) if (isReadableStream!ST) {
324 clear();
325 scope(failure) clear;
327 char[8] sign;
328 st.rawReadExact(sign[]);
329 if (sign != "Doom2D\x1a\x00") throw new Exception("invalid map signature");
330 if (st.readNum!ushort() != MapVersion) throw new Exception("invalid map version");
332 // load map blocks
333 foreach (ref a; tiles[]) a = new ubyte[](MapSize*MapSize);
334 char[$] skyname = "sprites/sky/rsky1.vga";
335 for (;;) {
336 auto btype = st.readNum!ushort();
337 if (btype == MB_END) break; // no more blocks
338 auto bsubtype = st.readNum!ushort();
339 auto bsize = st.readNum!uint();
340 if (bsize == 0) continue; // skip this block, it has no data (wtf?!)
341 // various tile types
342 switch (btype) {
343 case MB_SKY:
344 if (bsize != 2) throw new Exception("invalid sky data size");
345 ushort num = st.readNum!ushort();
346 if (num >= 1 && num <= 3) skyname[$-5] = cast(char)('0'+num);
347 break;
348 case MB_BACK:
349 case MB_FRONT:
350 case MB_WTYPE:
351 if (bsubtype > 1) throw new Exception("unknown tile block subtype");
352 int idx = (btype == MB_BACK ? Back : (btype == MB_FRONT ? Front : Type));
353 //ubyte[MapSize*MapSize] data = 0;
354 auto data = tiles[idx];
355 if (bsubtype == 0) {
356 if (bsize != data.length) throw new Exception("invalid tile data size");
357 st.rawReadExact(data[]);
358 } else {
359 // unpack RLE data
360 auto pkdata = new ubyte[](bsize);
361 st.rawReadExact(pkdata[]);
362 int spos = 0, opos = 0;
363 while (spos < pkdata.length) {
364 ubyte b = pkdata[spos++];
365 if (b != 255) {
366 data[opos++] = b;
367 } else {
368 int count = pkdata[spos++];
369 count |= pkdata[spos++]<<8;
370 b = pkdata[spos++];
371 while (count-- > 0) data[opos++] = b;
374 assert(opos == data.length);
376 // copy unpacked data
377 //foreach (immutable y; 0..MapSize) tiles[idx][y*MapSize] = data[y*MapSize..(y+1)*MapSize];
378 break;
379 case MB_WALLNAMES:
380 wallnames.length = 0;
381 wallnames ~= null;
382 //wallnames[] = null;
383 //walltypes.length = 0;
384 while (bsize >= 8+1) {
385 char[8] texname = 0;
386 st.rawReadExact(texname[]);
387 auto type = st.readNum!ubyte();
388 //char[] tn;
389 string tns;
390 foreach (char ch; texname) {
391 if (ch == 0) break;
392 if (ch >= 'A' && ch <= 'Z') ch += 32;
393 ch = dos2koi8(ch);
394 tns ~= ch;
395 //tn = texname[0..idx+1];
397 import std.uni : toLower;
398 wallnames ~= koi8lotranslit(tns).toLower;
399 //wallnames ~= recodeToKOI8(recode(tn.idup, "utf-8", "cp866").toLower, "utf-8");
400 //debug { conwriteln(wallnames.length-1, " : ", wallnames[$-1]); }
401 //if (wallnames[$-1][$-1] == '_') wallnames[$-1] ~= "1";
402 //wallnames[$-1] ~= ".vga";
403 //conwriteln(wallnames[$-1]);
404 //walltypes ~= type;
405 bsize -= 8+1;
407 if (bsize != 0) throw new Exception("invalid texture chunk size");
408 debug { conwriteln(wallnames.length, " textures loaded"); }
409 break;
410 case MB_THING:
411 while (bsize >= 8) {
412 bsize -= 8;
413 MapThing t = void;
414 t.x = st.readNum!short();
415 t.y = st.readNum!short();
416 t.type = st.readNum!ushort();
417 t.flags = st.readNum!ushort();
418 if (t.type != 0) things ~= t;
420 if (bsize != 0) throw new Exception("invalid thing chunk size");
421 break;
422 case MB_SWITCH2:
423 while (bsize >= 9) {
424 bsize -= 9;
425 MapSwitch sw = void;
426 sw.x = st.readNum!ubyte();
427 sw.y = st.readNum!ubyte();
428 sw.type = st.readNum!ubyte();
429 sw.tm = st.readNum!ubyte();
430 sw.a = st.readNum!ubyte();
431 sw.b = st.readNum!ubyte();
432 sw.c = st.readNum!ushort();
433 sw.flags = st.readNum!ubyte();
434 switches ~= sw;
436 if (bsize != 0) throw new Exception("invalid thing chunk size");
437 break;
438 default:
439 auto pkdata = new ubyte[](bsize);
440 st.rawReadExact(pkdata[]);
441 break;
444 calcMapSize();
445 // load textures
446 textures.length = 0;
447 foreach (immutable idx, string name; wallnames) {
448 if (name.length == 0 || name[0] == '_') {
449 textures ~= null;
450 continue;
451 } else {
452 textures ~= new D2DImage("tilegfx/"~name~".vga");
455 assert(textures.length == wallnames.length);
456 // fix tiles
457 foreach (immutable y; 0..height) {
458 foreach (immutable x; 0..width) {
459 if (tiles[Front][y*MapSize+x] >= textures.length) tiles[Front][y*MapSize+x] = 0;
460 if (tiles[Back][y*MapSize+x] >= textures.length) tiles[Back][y*MapSize+x] = 0;
463 skytex = new D2DImage(skyname.idup); //"sprites/rsky1.vga");
464 //skytex = new D2DImage("sprites/rsky3.vga");
465 skytex.sx = skytex.sy = 0;
466 debug { conwriteln(width, "x", height); }