diff --git a/assets/levels/R0/MAP16.COL b/assets/levels/R0/MAP16.COL index 01d079f..ca683a4 100644 Binary files a/assets/levels/R0/MAP16.COL and b/assets/levels/R0/MAP16.COL differ diff --git a/assets/levels/R1/MAP16.COL b/assets/levels/R1/MAP16.COL index 992d873..90bcc68 100644 Binary files a/assets/levels/R1/MAP16.COL and b/assets/levels/R1/MAP16.COL differ diff --git a/include/level.h b/include/level.h index 63c3071..8526e9e 100644 --- a/include/level.h +++ b/include/level.h @@ -9,9 +9,13 @@ #define LEVEL_ARENA_SIZE 65536 typedef struct { + int32_t floor_angle; uint8_t floor[8]; + int32_t rwall_angle; uint8_t rwall[8]; + int32_t ceiling_angle; uint8_t ceiling[8]; + int32_t lwall_angle; uint8_t lwall[8]; } Collision; @@ -47,6 +51,7 @@ typedef struct { uint8_t collided; uint8_t direction; // horizontal/vertical int16_t pushback; + int32_t angle; } CollisionEvent; diff --git a/include/util.h b/include/util.h index 9595676..a32426b 100644 --- a/include/util.h +++ b/include/util.h @@ -10,6 +10,8 @@ int RotTransPers(SVECTOR *v, uint32_t *xy0); int RotAverageNclip4(SVECTOR *a, SVECTOR *b, SVECTOR *c, SVECTOR *d, uint32_t *xy0, uint32_t *xy1, uint32_t *xy2, uint32_t *xy3, int *otz); +void CrossProduct0(VECTOR *v0, VECTOR *v1, VECTOR *out); +void CrossProduct12(VECTOR *v0, VECTOR *v1, VECTOR *out); uint8_t *file_read(const char *filename, uint32_t *length); void load_texture(uint8_t *data, TIM_IMAGE *tim); @@ -17,6 +19,7 @@ void load_texture(uint8_t *data, TIM_IMAGE *tim); uint8_t get_byte(uint8_t *bytes, uint32_t *b); uint16_t get_short_be(uint8_t *bytes, uint32_t *b); uint16_t get_short_le(uint8_t *bytes, uint32_t *b); +uint32_t get_long_be(uint8_t *bytes, uint32_t *b); uint32_t adler32(const char *s); diff --git a/src/level.c b/src/level.c index d75cbed..c48ee0c 100644 --- a/src/level.c +++ b/src/level.c @@ -62,12 +62,19 @@ _load_collision(TileMap16 *mapping, const char *filename) mapping->collision[tile_id] = alloc_arena_malloc(&_level_arena, sizeof(Collision)); Collision *collision = mapping->collision[tile_id]; + collision->floor_angle = (int32_t)get_long_be(bytes, &b); for(int j = 0; j < 8; j++) collision->floor[j] = get_byte(bytes, &b); + + collision->rwall_angle = (int32_t)get_long_be(bytes, &b); for(int j = 0; j < 8; j++) collision->rwall[j] = get_byte(bytes, &b); + + collision->ceiling_angle = (int32_t)get_long_be(bytes, &b); for(int j = 0; j < 8; j++) collision->ceiling[j] = get_byte(bytes, &b); + + collision->lwall_angle = (int32_t)get_long_be(bytes, &b); for(int j = 0; j < 8; j++) collision->lwall[j] = get_byte(bytes, &b); } @@ -478,29 +485,38 @@ linecast(LevelData *lvl, TileMap128 *map128, TileMap16 *map16, // to determine which height mask we are supposed to use. uint8_t *mask0 = NULL, *mask1 = NULL; uint8_t index = 0; + int32_t angle0, angle1; if(direction == 0) { // horizontal // index is related to y if(magnitude < 0) { - mask0 = piece0 ? piece0->lwall : NULL; - mask1 = piece1 ? piece1->lwall : NULL; + mask0 = piece0 ? piece0->lwall : NULL; + angle0 = piece0 ? piece0->lwall_angle : 0; + mask1 = piece1 ? piece1->lwall : NULL; + angle1 = piece1 ? piece1->lwall_angle : 0; // index starts at 0 from uppermost index = vpiecey[0]; // piece 1 or 0, doesn't matter } else { - mask0 = piece0 ? piece0->rwall : NULL; - mask1 = piece1 ? piece1->rwall : NULL; + mask0 = piece0 ? piece0->rwall : NULL; + angle0 = piece0 ? piece0->rwall_angle : 0; + mask1 = piece1 ? piece1->rwall : NULL; + angle1 = piece1 ? piece1->rwall_angle : 0; // index starts at 15 from uppermost index = 15 - vpiecey[0]; } } else { // vertical // index is related to x if(magnitude >= 0) { - mask0 = piece0 ? piece0->floor : NULL; - mask1 = piece1 ? piece1->floor : NULL; + mask0 = piece0 ? piece0->floor : NULL; + angle0 = piece0 ? piece0->floor_angle : 0; + mask1 = piece1 ? piece1->floor : NULL; + angle1 = piece1 ? piece1->floor_angle : 0; // index starts at 0 from leftmost index = vpiecex[0]; } else { - mask0 = piece0 ? piece0->ceiling : NULL; - mask1 = piece1 ? piece1->ceiling : NULL; + mask0 = piece0 ? piece0->ceiling : NULL; + angle0 = piece0 ? piece0->ceiling_angle : 0; + mask1 = piece1 ? piece1->ceiling : NULL; + angle1 = piece1 ? piece1->ceiling_angle : 0; // index starts at 15 from rightmost index = 15 - vpiecey[0]; } @@ -514,8 +530,10 @@ linecast(LevelData *lvl, TileMap128 *map128, TileMap16 *map16, // But, if our height on heightmask has a 0 height, then we proceed. uint8_t mask_byte = 0; int16_t h; + int32_t angle = 0; if(mask0) { mask_byte = mask0[index >> 1]; + angle = angle0; mask_byte = mask_byte >> (((index & 0x1) ^ 0x1) << 2); mask_byte = mask_byte & 0xf; @@ -529,6 +547,7 @@ linecast(LevelData *lvl, TileMap128 *map128, TileMap16 *map16, // if there was no collision on that spot. if((mask_byte == 0) && mask1) { mask_byte = mask1[index >> 1]; + angle = angle1; mask_byte = mask_byte >> (((index & 0x1) ^ 0x1) << 2); mask_byte = mask_byte & 0xf; @@ -578,6 +597,7 @@ linecast(LevelData *lvl, TileMap128 *map128, TileMap16 *map16, if(ev.collided) { ev.direction = direction; ev.pushback = (int16_t)mask_byte - h - (magnitude < 0 ? 16 : 0); + ev.angle = angle; /* if(ev.direction == 0) { // horizontal */ /* // TODO */ /* ev.pushback = (int16_t)mask_byte - h - (magnitude < 0 ? 16 : 0); */ diff --git a/src/main.c b/src/main.c index 1b8737f..98bc62c 100644 --- a/src/main.c +++ b/src/main.c @@ -330,21 +330,24 @@ engine_draw() "VEL %08x %08x\n" "GSP %08x\n" "DIR %c\n" - "GRN %c(%2d) // %c(%2d)\n" - "CEI %c(%2d) // %c(%2d)\n" - "LEF %c(%2d)\n" - "RIG %c(%2d)\n", + "GR1 %c(%2d %5d)\n" + "GR2 %c(%2d %5d)\n" + /* "CEI %c(%2d) // %c(%2d)\n" */ + /* "LEF %c(%2d)\n" */ + /* "RIG %c(%2d)\n" */ + , /* cam_pos.vx, cam_pos.vy, */ player.pos.vx, player.pos.vy, player.vel.vx, player.vel.vy, player.vel.vz, player.anim_dir >= 0 ? 'R' : 'L', player.ev_grnd1.collided ? 'Y' : 'N', player.ev_grnd1.pushback, - player.ev_grnd2.collided ? 'Y' : 'N', player.ev_grnd2.pushback, - player.ev_ceil1.collided ? 'Y' : 'N', player.ev_ceil1.pushback, - player.ev_ceil2.collided ? 'Y' : 'N', player.ev_ceil2.pushback, - player.ev_left.collided ? 'Y' : 'N', player.ev_left.pushback, - player.ev_right.collided ? 'Y' : 'N', player.ev_right.pushback); + player.ev_grnd2.collided ? 'Y' : 'N', player.ev_grnd2.pushback + /* player.ev_ceil1.collided ? 'Y' : 'N', player.ev_ceil1.pushback, */ + /* player.ev_ceil2.collided ? 'Y' : 'N', player.ev_ceil2.pushback, */ + /* player.ev_left.collided ? 'Y' : 'N', player.ev_left.pushback, */ + /* player.ev_right.collided ? 'Y' : 'N', player.ev_right.pushback */ + ); draw_text(8, 12, 0, buffer); } } diff --git a/src/player.c b/src/player.c index 09f17d6..6a32d04 100644 --- a/src/player.c +++ b/src/player.c @@ -241,8 +241,21 @@ _player_collision_detection(Player *player) } if(!player->grnd) { + player->angle = 0; if((player->ev_grnd1.collided || player->ev_grnd2.collided) && (player->vel.vy >= 0)) { - player->vel.vy = 0; + // Set angle according to movement + if(player->ev_grnd1.collided && !player->ev_grnd2.collided) + player->angle = player->ev_grnd1.angle; + else if(!player->ev_grnd1.collided && player->ev_grnd2.collided) + player->angle = player->ev_grnd2.angle; + // In case both are available, get the angle on the left. + // This introduces certain collision bugs but let's leave it + // like this for now + else player->angle = player->ev_grnd1.angle; + + // TODO: Set ground speed according to X and Y velocity + player->vel.vz = player->vel.vx; + int32_t pushback = (player->ev_grnd1.pushback > player->ev_grnd2.pushback) ? player->ev_grnd1.pushback @@ -284,28 +297,44 @@ player_update(Player *player) } // X movement - if(pad_pressing(PAD_RIGHT)) { - if(player->grnd && (player->vel.vx < 0)) { - player->vel.vx += X_DECEL; + /* Ground movement */ + if(player->grnd) { + if(pad_pressing(PAD_RIGHT)) { + if(player->vel.vz < 0) { + player->vel.vz += X_DECEL; + } else { + player->vel.vz += X_ACCEL; + player->anim_dir = 1; + } + } else if(pad_pressing(PAD_LEFT)) { + if(player->vel.vz > 0) { + player->vel.vz -= X_DECEL; + } else { + player->vel.vz -= X_ACCEL; + player->anim_dir = -1; + } } else { + player->vel.vz -= (player->vel.vz > 0 ? X_FRICTION : -X_FRICTION); + if(abs(player->vel.vz) <= X_FRICTION) player->vel.vz = 0; + } + + if(player->vel.vz > X_TOP_SPD) player->vel.vz = X_TOP_SPD; + else if(player->vel.vz < -X_TOP_SPD) player->vel.vz = -X_TOP_SPD; + + // Distribute ground speed onto X and Y components + player->vel.vx = (player->vel.vz * rcos(player->angle)) >> 12; + player->vel.vy = (player->vel.vz * -rsin(player->angle)) >> 12; + } else { + // Air X movement + if(pad_pressing(PAD_RIGHT)) { player->vel.vx += X_ACCEL; player->anim_dir = 1; - } - } else if(pad_pressing(PAD_LEFT)) { - if(player->grnd && (player->vel.vx > 0)) { - player->vel.vx -= X_DECEL; - } else { + } else if(pad_pressing(PAD_LEFT)) { player->vel.vx -= X_ACCEL; player->anim_dir = -1; } - } else { - player->vel.vx -= (player->vel.vx > 0 ? X_FRICTION : -X_FRICTION); - if(abs(player->vel.vx) <= X_FRICTION) player->vel.vx = 0; } - if(player->vel.vx > X_TOP_SPD) player->vel.vx = X_TOP_SPD; - else if(player->vel.vx < -X_TOP_SPD) player->vel.vx = -X_TOP_SPD; - // Y movement if(!player->grnd) { player->vel.vy += Y_GRAVITY; diff --git a/src/util.c b/src/util.c index d7e1a18..c85d06c 100644 --- a/src/util.c +++ b/src/util.c @@ -42,6 +42,24 @@ RotTransPers(SVECTOR *v, uint32_t *xy0) return otz; } +void +CrossProduct0(VECTOR *v0, VECTOR *v1, VECTOR *out) +{ + gte_ldopv1(v0); + gte_ldopv2(v1); + gte_op0(); + gte_stlvnl(out); +} + +void +CrossProduct12(VECTOR *v0, VECTOR *v1, VECTOR *out) +{ + gte_ldopv1(v0); + gte_ldopv2(v1); + gte_op12(); + gte_stlvnl(out); +} + uint8_t * file_read(const char *filename, uint32_t *length) { @@ -105,6 +123,17 @@ get_short_le(uint8_t *bytes, uint32_t *b) return value; } +uint32_t +get_long_be(uint8_t *bytes, uint32_t *b) +{ + uint32_t value = 0; + value |= bytes[(*b)++] << 24; + value |= bytes[(*b)++] << 16; + value |= bytes[(*b)++] << 8; + value |= bytes[(*b)++]; + return value; +} + uint32_t adler32(const char *s) diff --git a/tools/.gitignore b/tools/.gitignore new file mode 100644 index 0000000..74a4d86 --- /dev/null +++ b/tools/.gitignore @@ -0,0 +1,2 @@ +venv/ +.mypy_cache/ diff --git a/tools/cookcollision.py b/tools/cookcollision.py index 14bfcf8..0e72691 100755 --- a/tools/cookcollision.py +++ b/tools/cookcollision.py @@ -5,13 +5,16 @@ import json import sys -from ctypes import c_ushort, c_ubyte +import numpy as np +import math +from ctypes import c_ushort, c_ubyte, c_int32 from enum import Enum from pprint import pp as pprint from math import sqrt # Ensure binary C types are encoded as big endian c_ushort = c_ushort.__ctype_be__ +c_int32 = c_int32.__ctype_be__ # This package depends on shapely because I'm fed up with attempting to code # point and polygon checking myself. On arch linux, install python-shapely. @@ -32,6 +35,35 @@ def point_in_geometry(p, points): return shape.contains(point) or shape.intersects(point) +def normalize(v): + norm = np.linalg.norm(v) + return [c / norm for c in v] + + +def fix_angle(x): + # This ensures that an angle in radians is always on their + # 1st or 4th quadrant equivalent, and also on the first lap. + fixed = x + if (x >= (np.pi / 2)) and (x < np.pi): + fixed = (2 * np.pi) - (np.pi - x) + if (x >= np.pi) and (x < (1.5 * np.pi)): + fixed = x - np.pi + return fixed % (2 * np.pi) + + +def to_psx_angle(a): + # PSX angles are given in degrees, ranged from 0.0 to 1.0 in 20.12 + # fixed-point format (therefore from 0 to 4096). + # All we need to do is fix its quadrant and lap, convert it to a + # ratio [0..360], then multiply it by 4096. This is how we get + # our angle. + # Final gsp->(xsp, ysp) conversions in-game are given as + # {x: (gsp * cos(x) >> 12), y: (gsp * -sin(x)) >> 12}. + a = np.rad2deg(fix_angle(a)) + rat = a / 360 + return math.floor(rat * 4096) + + def get_height_mask(d: Direction, points): # Perform iterative linecast. # Linecast checks for a point within a geometry starting at a height @@ -40,6 +72,7 @@ def get_height_mask(d: Direction, points): # Of course, if pointing downwards, we go from left to right, top to bottom. # If using any other direction... flip it accordingly. heightmask = [] + angle = 0 for pos in range(16): found = False for height in reversed(range(1, 16)): @@ -62,7 +95,39 @@ def get_height_mask(d: Direction, points): break if not found: heightmask.append(0) - return heightmask + + # Build vector according to direction + # and heightmask + # TODO: Maybe the referential dirvec is wrong? + vector = [0, 0] + dirvec = [0, 0] + dirv = 0 + delta = heightmask[0] - heightmask[-1] + if d == Direction.DOWN: + # Vector points left to right + vector = [16, delta] + dirvec = [1, 0] + dirv = 0 + elif d == Direction.UP: + # Vector points right to left + vector = [-16, -delta] + dirvec = [-1, 0] + dirv = 0 + elif d == Direction.LEFT: + # Vector points top to bottom + vector = [-delta, 16] + dirvec = [0, 1] + dirv = 1 + elif d == Direction.RIGHT: + # Vector points bottom to top + vector = [delta, -16] + dirvec = [0, -1] + dirv = 1 + + vector = normalize(vector) + angle = math.atan2(dirvec[1], dirvec[0]) - math.atan2(vector[1], vector[0]) + angle = to_psx_angle(angle) + return (heightmask, angle) def parse_masks(tiles): @@ -70,14 +135,19 @@ def parse_masks(tiles): for tile in tiles: points = tile.get("points") id = tile.get("id") + (floor, floor_angle) = get_height_mask(Direction.DOWN, points) + (ceil, ceil_angle) = get_height_mask(Direction.UP, points) + (rwall, rwall_angle) = get_height_mask(Direction.RIGHT, points) + (lwall, lwall_angle) = get_height_mask(Direction.LEFT, points) + res.append( { "id": tile.get("id"), "masks": { - "floor": get_height_mask(Direction.DOWN, points), - "ceiling": get_height_mask(Direction.UP, points), - "rwall": get_height_mask(Direction.RIGHT, points), - "lwall": get_height_mask(Direction.LEFT, points), + "floor": [floor_angle, floor], + "ceiling": [ceil_angle, ceil], + "rwall": [rwall_angle, rwall], + "lwall": [lwall_angle, lwall], }, } ) @@ -153,20 +223,33 @@ def write_mask_data(f, mask_data): # Binary layout: # 1. Number of tiles (ushort, 2 bytes) -# 2. Tile data +# 2. Tile data [many] # 2.1. tile id (ushort, 2 bytes) -# 2.2. Floor mode data (8 bytes) -# 2.3. Right wall mode data (8 bytes) -# 2.4. Ceiling mode data (8 bytes) -# 2.5. Left wall mode data (8 bytes) +# 2.2. Floor +# 2.2.1. Angle (4 bytes - PSX format) +# 2.2.2. Data (8 bytes) +# 2.3. Right wall +# 2.3.1. Angle (4 bytes - PSX format) +# 2.3.2. Data (8 bytes) +# 2.4. Ceiling +# 2.4.1. Angle (4 bytes - PSX format) +# 2.4.2. Data (8 bytes) +# 2.5. Left wall +# 2.5.1. Angle (4 bytes - PSX format) +# 2.5.2. Data (8 bytes) def write_file(f, tile_data): f.write(c_ushort(len(tile_data))) for tile in tile_data: f.write(c_ushort(tile.get("id"))) - write_mask_data(f, tile.get("masks").get("floor")) - write_mask_data(f, tile.get("masks").get("rwall")) - write_mask_data(f, tile.get("masks").get("ceiling")) - write_mask_data(f, tile.get("masks").get("lwall")) + masks = tile.get("masks") + f.write(c_int32(masks.get("floor")[0])) + write_mask_data(f, masks.get("floor")[1]) + f.write(c_int32(masks.get("rwall")[0])) + write_mask_data(f, masks.get("rwall")[1]) + f.write(c_int32(masks.get("ceiling")[0])) + write_mask_data(f, masks.get("ceiling")[1]) + f.write(c_int32(masks.get("lwall")[0])) + write_mask_data(f, masks.get("lwall")[1]) def main(): diff --git a/tools/guide.org b/tools/guide.org new file mode 100644 index 0000000..5a75fc4 --- /dev/null +++ b/tools/guide.org @@ -0,0 +1,144 @@ +* Startup + +Everything starts with generating 16x16 tiles. You are welcome to start with +128x128 tiles if you find it easier, but you'll have to control well your tiles +so they don't add up much. Remember that VRAM is limited on the PlayStation, so +your art is going to have to be cut into 8x8 pieces, and these pieces should +fill a 256x256 texture at max, at the end of these steps, and shouldn't have too +many colors, so let's say you're constrained to 1023 tiles of 8x8 pixels (tile 0 +is always a blank tile). + +Your first step is creating a 16x16.png file with your 16x16 tiles. If you +started working with 128x128 you could probably cut them up into a single sprite +sheet with the aid of Aseprite. + + +** Generating Python's virtualenv + +This is how you can create a virtualenv with all Python pendencies to run the +tools, though you won't really need it in most cases, since there are no "weird" +packages being used: + +#+begin_src bash +cd tools/ +python -m venv ./venv +./venv/bin/pip install -r requirements.txt +#+end_src + +To run any scripts with this venv, run ~./tools/venv/bin/python +./tools/script.py~. + +* Generating 8x8 tiles and their 16x16 mappings + +The following steps will allow you to create intermediate files 'tiles.png', +'map16.json' and 'collision16.json'. + +You will also be able to cook these files into PlayStation-only engine files +'TILES.TIM', 'MAP16.MAP' and 'MAP16.COL'. These are binary equivalents to the +files above, with only relevant information. + +Extra files such as 'tiles16.tsx' will also be generated. + + 1. Create '16x16.png' tiles. + 2. Import '16x16.png' tiles into a 'tiles16.tsx'. + 3. Export 'tiles16.tsx' from Tiled as 'collision16.json'. + 4. Copy '16x16.png' to '8x8.png'. + 5. Open '8x8.png' (still 16x16 tiles) on Aseprite. + 6. File > Import > Import Sprite Sheet. The single image will be used as + one. Make it a 16x16 grid. + 7. Right click layer > Convert to > tilemap. Make it a 8x8 grid. + 8. File > Scripts > export_tilemap_psx. This will create a '8x8.json' + file. Rename it to 'map16.json'. + 9. File > Scripts > export_tileset_psx. Use a 8x8 grid. This will overwrite + '8x8.png'. Rename it to 'tiles.png'. +10. Open 'tiles.png' with your favorite editor and make sure that all + transparent pixels are set to color `#000000` (black). +11. Use TIMTOOL.EXE (preferably) from Psy-Q library to generate a .TIM for your + tiles. This will generate a 'TILES.TIM' file on the same directory of the + texture. + - Make sure you un-mark the "Set for Black" option in Semi Transparent + Information. + - Make sure your tileset is at 448x0 and that the CLUT information is 4-bit + depth and at 448x256. Notice that texture pages 8 and 24 are for level + tiles and CLUT information, respectively. + - *NOTE:* If you use another tool such as TIMEDIT, just make sure the black + color is accurately picked as transparent color, and that no + semi-transparency is enabled. Also ensure the positions for the texture + and the CLUT on proper texture pages. +12. Use the tool 'framepacker.py' to turn 'map16.json' into a 'MAP16.MAP' file:\ + ~framepacker.py --tilemap map16.json MAP16.MAP~ +13. Use the tool 'cookcollision.py' to turn 'collision16.json' into a + 'MAP16.COL' file:\ + ~cookcollision.py collision16.json MAP16.COL~ + + + +* Generating 128x128 tiles and mappings + +The following steps will allow you to generate a 'MAP128.MAP' file from a +'map128.tmx'. +This 'map128.tmx' tile is supposed to be a map comprised of 16x16 tiles, created +from the same '16x16.png' file we addressed earlier. +Each 128x128 tile is supposed to be equivalent to every eight rows and columns +on the .tmx map. + +Please make sure that the first tile is COMPLETELY BLANK and mind the tile +sequence (tiles are counted first from left to right, then up to down). + +1. Create a 'tileset16.tsx' map from '16x16.png', if you haven't already. +2. Create a 'map128.tmx' map and use 'tileset16.tsx' as tileset. This map must + have infinite dimensions. +3. Create your tiles from left to right, and if you must, up to down. Be mindful + of tile order, and make sure that the first tile (first eight rows and + columns) are completely blank. +4. Once you're done with your map (you may save your project for later + manipulation), export your .tmx to a 'map128.csv'. +5. Use the tool 'chunkgen.py' to turn 'map128.csv' into a 'MAP128.MAP' file:\ + ~chunkgen.py map128.csv MAP128.MAP~ + +** Preparation for level map creation + +Do this in preparation for creating your actual level map: + +1. Go back to your 'map128.tmx' and export it to an image called '128x128.png'. + - Make sure you didn't mess up the tile mapping, and that the tile is + properly aligned with the upper left corner of your frame. You'll see that + by looking at the continuous line in your 128x128 infinite map. + - Make sure you didn't mess up the map size also. Generally speaking, extra + tiles on the right side are just as bad; use Map > Resize Map as needed to + ensure that there are no extra tiles to the right. +2. Create a '128x128.tsx' tileset and use image '128x128.png' as base. + - If you already created this file, once you re-export '128x128.png', it + should update with no extra effort needed, and so will your level maps that + use this tileset. + + +* Generating your level + +The following steps will allow you to create level maps such as 'Z1.tmx' and +'Z2.tmx', and generate levels such as 'Z1.LVL' and 'Z1.LVL', in PlayStation +format. + +This will also create intermediate files such as 'Z1.json' and 'Z1.json'. This +intermediate representation is necessary because Tiled is unable to export +levels in binary format in one go, due to scripting limitations. + +You'll need to have Python scripting enabled in Tiled, and you'll also need to +have `lvlexporter.py` on your Tiled scripts directory (generally `~/.tiled` on +Linux). + +1. Create a 'Z1.tmx' or 'Z2.tmx' file using '128x128.tsx' as tileset. The level + must be exacly 255x31 blocks long; block size must be 128x128. +2. Create a layer called 'LAYER0' and another one called 'LAYER1'. Make sure + that 'LAYER1' is above 'LAYER0'; level layers are exported from bottom to + top. +3. Draw your tiles preferably on 'LAYER0' (this part is still unfinished, but + this is the only layer where collision detection happens). Use 'LAYER1' to + draw tiles that should go on front of your character (this part is also a + work-in-progress). +4. Once you're done with your map, go to File > Export as..., pick the + "PlayStation proto map" format, and save it as 'Z1.json' or 'Z2.json'. +5. Use the tool 'cooklvl.py' to turn 'Z1.json' or 'Z2.json' into 'Z1.LVL' or + 'Z2.LVL':\ + ~cooklvl.py Z1.json Z1.LVL~ + diff --git a/tools/guide.txt b/tools/guide.txt deleted file mode 100644 index 6973515..0000000 --- a/tools/guide.txt +++ /dev/null @@ -1,105 +0,0 @@ -* Startup - -Everything starts with generating 16x16 tiles. You are welcome to start with 128x128 tiles if you find it easier, -but you'll have to control well your tiles so they don't add up much. Remember that VRAM is limited on the -PlayStation, so your art is going to have to be cut into 8x8 pieces, and these pieces should fill a 256x256 -texture at max, at the end of these steps, and shouldn't have too many colors, so let's say you're constrained to -1023 tiles of 8x8 pixels (tile 0 is always a blank tile). - -Your first step is creating a 16x16.png file with your 16x16 tiles. If you started working with 128x128, you could probably cut them up into a single sprite sheet with the aid of Aseprite. - - - -* Generating 8x8 tiles and their 16x16 mappings - -The following steps will allow you to create intermediate files 'tiles.png', 'map16.json' and 'collision16.json'. - -You will also be able to cook these files into PlayStation-only engine files 'TILES.TIM', 'MAP16.MAP' and -'MAP16.COL'. These are binary equivalents to the files above, with only relevant information. - -Extra files such as 'tiles16.tsx' will also be generated. - - 1. Create '16x16.png' tiles. - 2. Import '16x16.png' tiles into a 'tiles16.tsx'. - 3. Export 'tiles16.tsx' from Tiled as 'collision16.json'. - 4. Copy '16x16.png' to '8x8.png'. - 5. Open '8x8.png' (still 16x16 tiles) on Aseprite. - 6. File > Import > Import Sprite Sheet. The single image will be used as one. Make it a 16x16 grid. - 7. Right click layer > Convert to > tilemap. Make it a 8x8 grid. - 8. File > Scripts > export_tilemap_psx. This will create a '8x8.json' file. Rename it to 'map16.json'. - 9. File > Scripts > export_tileset_psx. Use a 8x8 grid. This will overwrite '8x8.png'. Rename it to - 'tiles.png'. -10. Open 'tiles.png' with your favorite editor and make sure that all transparent pixels are set to color - `#000000` (black). -11. Use TIMTOOL.EXE (preferably) from Psy-Q library to generate a .TIM for your tiles. This will generate a - 'TILES.TIM' file on the same directory of the texture. - 12.1 Make sure you un-mark the "Set for Black" option in Semi Transparent Information. - 13.2 Make sure your tileset is at 448x0 and that the CLUT information is 4-bit depth and at 448x256. - Notice that texture pages 8 and 24 are for level tiles and CLUT information, respectively. - NOTE: If you use another tool such as TIMEDIT, just make sure the black color is accurately picked as - transparent color, and that no semi-transparency is enabled. Also ensure the positions for the texture - and the CLUT on proper texture pages. -12. Use the tool 'framepacker.py' to turn 'map16.json' into a 'MAP16.MAP' file: - `framepacker.py --tilemap map16.json MAP16.MAP` -13. Use the tool 'cookcollision.py' to turn 'collision16.json' into a 'MAP16.COL' file: - `cookcollision.py collision16.json MAP16.COL` - - - -* Generating 128x128 tiles and mappings - -The following steps will allow you to generate a 'MAP128.MAP' file from a 'map128.tmx'. -This 'map128.tmx' tile is supposed to be a map comprised of 16x16 tiles, created from the same '16x16.png' -file we addressed earlier. -Each 128x128 tile is supposed to be equivalent to every eight rows and columns on the .tmx map. - -Please make sure that the first tile is COMPLETELY BLANK and mind the tile sequence (tiles are counted -first from left to right, then up to down). - -1. Create a 'tileset16.tsx' map from '16x16.png', if you haven't already. -2. Create a 'map128.tmx' map and use 'tileset16.tsx' as tileset. This map must have infinite dimensions. -3. Create your tiles from left to right, and if you must, up to down. Be mindful of tile order, and make - sure that the first tile (first eight rows and columns) are completely blank. -4. Once you're done with your map (you may save your project for later manipulation), export your .tmx - to a 'map128.csv'. -5. Use the tool 'chunkgen.py' to turn 'map128.csv' into a 'MAP128.MAP' file: - `chunkgen.py map128.csv MAP128.MAP' - -Do this in preparation for creating your actual level map: - -1. Go back to your 'map128.tmx' and export it to an image called '128x128.png'. - 1.1. Make sure you didn't mess up the tile mapping, and that the tile is properly aligned with the - upper left corner of your frame. You'll see that by looking at the continuous line in your 128x128 - infinite map. - 1.2. Make sure you didn't mess up the map size also. Generally speaking, extra tiles on the right side - are just as bad; use Map > Resize Map as needed to ensure that there are no extra tiles to the - right. -2. Create a '128x128.tsx' tileset and use image '128x128.png' as base. - 2.1. If you already created this file, once you re-export '128x128.png', it should update with no - extra effort needed, and so will your level maps that use this tileset. - - -* Generating your level - -The following steps will allow you to create level maps such as 'Z1.tmx' and 'Z2.tmx', and generate -levels such as 'Z1.LVL' and 'Z1.LVL', in PlayStation format. - -This will also create intermediate files such as 'Z1.json' and 'Z1.json'. This intermediate representation -is necessary because Tiled is unable to export levels in binary format in one go, due to scripting -limitations. - -You'll need to have Python scripting enabled in Tiled, and you'll also need to have `lvlexporter.py` on -your Tiled scripts directory (generally `~/.tiled` on Linux). - -1. Create a 'Z1.tmx' or 'Z2.tmx' file using '128x128.tsx' as tileset. The level must be exacly 255x31 - blocks long; block size must be 128x128. -2. Create a layer called 'LAYER0' and another one called 'LAYER1'. Make sure that 'LAYER1' is above - 'LAYER0'; level layers are exported from bottom to top. -3. Draw your tiles preferably on 'LAYER0' (this part is still unfinished, but this is the only layer where - collision detection happens). Use 'LAYER1' to draw tiles that should go on front of your character - (this part is also a work-in-progress). -4. Once you're done with your map, go to File > Export as..., pick the "PlayStation proto map" format, and - save it as 'Z1.json' or 'Z2.json'. -5. Use the tool 'cooklvl.py' to turn 'Z1.json' or 'Z2.json' into 'Z1.LVL' or 'Z2.LVL': - `cooklvl.py Z1.json Z1.LVL` - diff --git a/tools/layouts/collision.hexpat b/tools/layouts/col.hexpat similarity index 78% rename from tools/layouts/collision.hexpat rename to tools/layouts/col.hexpat index 58d4e9c..add98b7 100644 --- a/tools/layouts/collision.hexpat +++ b/tools/layouts/col.hexpat @@ -1,6 +1,6 @@ // -*- mode: c; -*- -bitfield HeightMask { +bitfield HeightMaskData { unsigned c0 : 4; unsigned c1 : 4; unsigned c2 : 4; @@ -19,16 +19,21 @@ bitfield HeightMask { unsigned cF : 4; }; +struct HeightMask { + be s32 angle; + be HeightMaskData data; +}; + struct Collision { be u16 tile_id; // Height mask downwards (left to right) - be HeightMask floor; + HeightMask floor; // Height mask to right (bottom to top) - be HeightMask rwall; + HeightMask rwall; // Height mask upwards (right to left) - be HeightMask ceiling; + HeightMask ceiling; // Height mask to left (top to bottom) - be HeightMask lwall; + HeightMask lwall; }; struct TileData { diff --git a/tools/requirements.txt b/tools/requirements.txt new file mode 100644 index 0000000..fb2f3c4 --- /dev/null +++ b/tools/requirements.txt @@ -0,0 +1,7 @@ +numpy==2.0.1 +pandas==2.2.2 +python-dateutil==2.9.0.post0 +pytz==2024.1 +shapely==2.0.5 +six==1.16.0 +tzdata==2024.1