mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
296 lines
11 KiB
Lua
296 lines
11 KiB
Lua
-- Runtime map built from generated data. All queries use "cells": the
|
|
-- 16x16 walk grid (2x2 tiles). A map is width x height blocks; each block
|
|
-- is 2x2 cells (4x4 tiles).
|
|
--
|
|
-- Collision follows the original engine: a cell is passable when the
|
|
-- BOTTOM-LEFT 8x8 tile of the cell is in the tileset's walkable list
|
|
-- (pokered checks the tile at the sprite's feet). Doors, warp tiles and
|
|
-- grass use the same convention.
|
|
|
|
local Map = {}
|
|
Map.__index = Map
|
|
|
|
-- Stale-cache fallbacks for the tileset properties the importer does not
|
|
-- stamp yet (item_effects.asm IsNextTileShoreOrWater, home/overworld.asm
|
|
-- CollisionCheckOnWater): $14 is water everywhere; the shore tiles $32 and
|
|
-- $48 (Safari Zone) everywhere EXCEPT SHIP_PORT, where $32 is the dock's
|
|
-- boarding platform -- a land tile. A tileset record that carries
|
|
-- waterTiles/shoreTiles wins outright, which is how a new tileset gets
|
|
-- surfable water without naming Kanto's.
|
|
local WATER_TILES = { 0x14 }
|
|
local SHORE_TILES = { 0x32, 0x48 }
|
|
local NO_SHORE_TILESETS = { SHIP_PORT = true }
|
|
|
|
-- what counts as "outside" for the wLastMap memory (CheckIfInOutsideMap)
|
|
local OUTSIDE_TILESETS = { "OVERWORLD", "PLATEAU" }
|
|
|
|
-- pokered's fly destination gate: BuildFlyLocationsList
|
|
-- (engine/items/town_map.asm) walks map ids 0..NUM_CITY_MAPS-1, the eleven
|
|
-- towns PALLET_TOWN..SAFFRON_CITY, so routes never appear even though
|
|
-- ROUTE_4/ROUTE_10 carry fly-warp landing spots (those exist for the
|
|
-- dungeon-escape/heal tables, special_warps.asm FlyWarpDataPtr)
|
|
local NUM_CITY_MAPS = 11
|
|
|
|
-- warp pads and fall-through holes (data/tilesets/warp_pad_hole_tile_ids
|
|
-- .asm WarpPadAndHoleData); a tileset record carrying warpPadTiles
|
|
-- ({ [tileId] = "pad"|"hole" }) wins over these vanilla rows
|
|
local WARP_PAD_TILES = {
|
|
FACILITY = { [0x20] = "pad", [0x11] = "hole" },
|
|
CAVERN = { [0x22] = "hole" },
|
|
INTERIOR = { [0x55] = "pad" },
|
|
}
|
|
|
|
local function hashSet(list, into)
|
|
for _, t in ipairs(list) do into[t] = true end
|
|
return into
|
|
end
|
|
|
|
-- Collision tile (bottom-left 8x8) of a cell on an UNLOADED map def --
|
|
-- the connected neighbor during an edge crossing. pokered's
|
|
-- GetTileAndCoordsInFrontOfPlayer / collision checks read the neighbor
|
|
-- strip's tile bytes the same way.
|
|
function Map.defCellTile(def, tilesetDef, cx, cy)
|
|
if not (def and tilesetDef and tilesetDef.blocks) then return nil end
|
|
local tx, ty = cx * 2, cy * 2 + 1
|
|
local bx, by = math.floor(tx / 4), math.floor(ty / 4)
|
|
local id
|
|
if bx < 0 or by < 0 or bx >= def.width or by >= def.height then
|
|
id = def.borderBlock
|
|
else
|
|
id = def.blocks[by * def.width + bx + 1]
|
|
end
|
|
local block = tilesetDef.blocks[(id or 0) + 1]
|
|
if not block then return nil end
|
|
return block[(ty % 4) * 4 + (tx % 4) + 1]
|
|
end
|
|
|
|
local function defWaterTileSet(def, tilesetDef)
|
|
local water = {}
|
|
hashSet(tilesetDef.waterTiles or WATER_TILES, water)
|
|
local shore = tilesetDef.shoreTiles
|
|
if shore == nil and not NO_SHORE_TILESETS[def.tileset] then shore = SHORE_TILES end
|
|
hashSet(shore or {}, water)
|
|
return water
|
|
end
|
|
|
|
-- Water/shore on an unloaded map def (same tile ids as Map:isWaterCell).
|
|
function Map.defIsWaterCell(def, tilesetDef, cx, cy)
|
|
local tile = Map.defCellTile(def, tilesetDef, cx, cy)
|
|
if tile == nil then return false end
|
|
return defWaterTileSet(def, tilesetDef)[tile] or false
|
|
end
|
|
|
|
function Map.defIsWalkableCell(def, tilesetDef, cx, cy)
|
|
if not (tilesetDef and tilesetDef.walkable) then return false end
|
|
local tile = Map.defCellTile(def, tilesetDef, cx, cy)
|
|
if tile == nil then return false end
|
|
for _, t in ipairs(tilesetDef.walkable) do
|
|
if t == tile then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Passability of a cell of an UNLOADED map def -- the connected neighbor
|
|
-- during an edge crossing. pokered's collision check reads the neighbor
|
|
-- strip's tile bytes, so a step off the map edge onto a solid tile of
|
|
-- the connected map bumps exactly like an in-map wall; the port needs
|
|
-- the same read without building the whole Map. Same math as cellTile
|
|
-- on the raw blocks, honoring the surf rule (water/shore passable only
|
|
-- while surfing, same fallbacks as Map.new).
|
|
function Map.defPassable(def, tilesetDef, cx, cy, surfing)
|
|
-- Fail closed: a missing tileset used to return true and re-open the
|
|
-- Pallet south-shore stranding (cross onto ROUTE_21 solids). No data
|
|
-- means we cannot prove the landing is safe, so the step bumps.
|
|
if not (def and tilesetDef and tilesetDef.blocks and tilesetDef.walkable) then
|
|
return false
|
|
end
|
|
if Map.defIsWalkableCell(def, tilesetDef, cx, cy) then return true end
|
|
if surfing and Map.defIsWaterCell(def, tilesetDef, cx, cy) then return true end
|
|
return false
|
|
end
|
|
|
|
function Map.new(def, tilesetDef)
|
|
local self = setmetatable({}, Map)
|
|
self.def = def
|
|
self.tileset = tilesetDef
|
|
self.id = def.id
|
|
self.widthCells = def.width * 2
|
|
self.heightCells = def.height * 2
|
|
|
|
self.walkable = {}
|
|
for _, t in ipairs(tilesetDef.walkable) do self.walkable[t] = true end
|
|
self.doorTiles = {}
|
|
for _, t in ipairs(tilesetDef.doorTiles or {}) do self.doorTiles[t] = true end
|
|
self.warpTiles = {}
|
|
for _, t in ipairs(tilesetDef.warpTiles or {}) do self.warpTiles[t] = true end
|
|
-- water and shore share one lookup: both are surfable, only the caller's
|
|
-- water_tilesets.asm membership check separates them
|
|
self.waterTiles = hashSet(tilesetDef.waterTiles or WATER_TILES, {})
|
|
local shore = tilesetDef.shoreTiles
|
|
if shore == nil and not NO_SHORE_TILESETS[def.tileset] then shore = SHORE_TILES end
|
|
hashSet(shore or {}, self.waterTiles)
|
|
|
|
self.warpAt = {}
|
|
for i, w in ipairs(def.warps or {}) do
|
|
self.warpAt[w.y * self.widthCells + w.x] = { index = i, def = w }
|
|
end
|
|
self.signAt = {}
|
|
for _, s in ipairs(def.signs or {}) do
|
|
self.signAt[s.y * self.widthCells + s.x] = s
|
|
end
|
|
return self
|
|
end
|
|
|
|
-- ------- map record properties (authored maps set them; vanilla falls back)
|
|
|
|
-- town/route surface: door SFX, the walk-out step, the Fly menu and the
|
|
-- town map all mean this one
|
|
function Map.isOutdoor(def)
|
|
if def.outdoor ~= nil then return def.outdoor end
|
|
return def.tileset == "OVERWORLD"
|
|
end
|
|
|
|
-- CheckIfInOutsideMap, a strictly wider set: Route 23 / Indigo Plateau are
|
|
-- outside for the wLastMap memory without being outdoor for the door SFX
|
|
function Map.isOutside(def, tilesets)
|
|
if Map.isOutdoor(def) then return true end
|
|
for _, ts in ipairs(tilesets or OUTSIDE_TILESETS) do
|
|
if ts == def.tileset then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- FLY destination (LoadTownMap_Fly / BuildFlyLocationsList): the eleven
|
|
-- towns, map indices 0..NUM_CITY_MAPS-1. ROUTE_4 and ROUTE_10 are outdoor
|
|
-- and have fly warps but are not towns, so the outdoor test alone offered
|
|
-- their Pokemon Centers as fly targets (#788). Maps without a vanilla
|
|
-- index (mod-authored) keep the old outdoor/PLATEAU surface test, which is
|
|
-- how a mod adds its own fly town.
|
|
function Map.isFlyTown(def)
|
|
if def.index ~= nil then return def.index < NUM_CITY_MAPS end
|
|
return Map.isOutdoor(def) or def.tileset == "PLATEAU"
|
|
end
|
|
|
|
-- region groups maps a rule applies to without naming them; the id prefix
|
|
-- is the fallback for caches that predate the property
|
|
function Map.inRegion(def, region, prefix)
|
|
if def.region ~= nil then return def.region == region end
|
|
return prefix ~= nil and def.id:find(prefix, 1, true) == 1
|
|
end
|
|
|
|
-- unidentifiable wild battles on this map unless the player holds an item
|
|
function Map.ghostBattles(def)
|
|
if def.ghostBattles ~= nil then return def.ghostBattles end
|
|
if def.id:find("POKEMON_TOWER", 1, true) == 1 then
|
|
return { unlessItem = "SILPH_SCOPE" }
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- strength-pushable map objects (engine/overworld/push_boulder.asm)
|
|
function Map.isPushable(objDef)
|
|
if objDef.pushable ~= nil then return objDef.pushable end
|
|
return objDef.sprite == "SPRITE_BOULDER"
|
|
end
|
|
|
|
function Map:blockAt(bx, by)
|
|
if bx < 0 or by < 0 or bx >= self.def.width or by >= self.def.height then
|
|
return self.def.borderBlock
|
|
end
|
|
return self.def.blocks[by * self.def.width + bx + 1]
|
|
end
|
|
|
|
-- tile id at tile coordinates (8px grid), border-extended
|
|
function Map:tileAt(tx, ty)
|
|
local bx, by = math.floor(tx / 4), math.floor(ty / 4)
|
|
local block = self.tileset.blocks[self:blockAt(bx, by) + 1]
|
|
local ix = (ty % 4) * 4 + (tx % 4) + 1
|
|
return block[ix]
|
|
end
|
|
|
|
-- the collision tile of a cell: bottom-left 8x8 tile
|
|
function Map:cellTile(cx, cy)
|
|
return self:tileAt(cx * 2, cy * 2 + 1)
|
|
end
|
|
|
|
function Map:inBounds(cx, cy)
|
|
return cx >= 0 and cy >= 0 and cx < self.widthCells and cy < self.heightCells
|
|
end
|
|
|
|
function Map:isWalkableCell(cx, cy)
|
|
return self.walkable[self:cellTile(cx, cy)] or false
|
|
end
|
|
|
|
function Map:isGrassCell(cx, cy)
|
|
-- Off-map cells never count as tall grass (issue #217). cellTile
|
|
-- border-extends out-of-bounds coordinates with the map's borderBlock,
|
|
-- and some border blocks (e.g. ROUTE_1's block 11) have the grass tile
|
|
-- ($52 = 82) in their bottom row -- filler scenery, never standable
|
|
-- grass. During a map-connection seam step crossConnection parks the
|
|
-- player one cell before the entry point (cellY = -1 crossing Viridian
|
|
-- City -> Route 1), so without this guard the feet-overdraw painted an
|
|
-- animated grass tuft over the player's head for the whole step. pokered
|
|
-- only ever reads $52 from loaded map tiles, not the border filler.
|
|
if not self:inBounds(cx, cy) then return false end
|
|
local grass = self.tileset.grassTile
|
|
return grass ~= nil and self:cellTile(cx, cy) == grass
|
|
end
|
|
|
|
-- Water and eastern-shore tiles, from the tileset's waterTiles/shoreTiles
|
|
-- (hash sets built in Map.new). Tileset membership in water_tilesets.asm
|
|
-- is checked by the caller.
|
|
function Map:isWaterCell(cx, cy)
|
|
return self.waterTiles[self:cellTile(cx, cy)] or false
|
|
end
|
|
|
|
-- Replace a block (Cut trees); the caller rebuilds the renderer.
|
|
function Map:setBlock(bx, by, block)
|
|
if bx < 0 or by < 0 or bx >= self.def.width or by >= self.def.height then
|
|
return
|
|
end
|
|
self.def.blocks[by * self.def.width + bx + 1] = block
|
|
end
|
|
|
|
-- true if the cell's collision tile is a door tile
|
|
-- (pokered IsPlayerStandingOnDoorTile)
|
|
function Map:isDoorTileCell(cx, cy)
|
|
return self.doorTiles[self:cellTile(cx, cy)] or false
|
|
end
|
|
|
|
-- true if the cell's collision tile is a door or warp-activating tile
|
|
function Map:isWarpTileCell(cx, cy)
|
|
local t = self:cellTile(cx, cy)
|
|
return self.doorTiles[t] or self.warpTiles[t] or false
|
|
end
|
|
|
|
-- "pad"/"hole" when the cell's collision tile is a teleporter warp pad or
|
|
-- a fall-through hole (IsPlayerStandingOnWarpPadOrHole), nil otherwise
|
|
function Map:warpPadOrHoleAt(cx, cy)
|
|
local table_ = self.tileset.warpPadTiles or WARP_PAD_TILES[self.def.tileset]
|
|
if not table_ then return nil end
|
|
return table_[self:cellTile(cx, cy)]
|
|
end
|
|
|
|
-- counter tiles allow talking to NPCs across them (mart clerks, nurses)
|
|
function Map:isCounterCell(cx, cy)
|
|
local t = self:cellTile(cx, cy)
|
|
for _, c in ipairs(self.tileset.counterTiles or {}) do
|
|
if c == t then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function Map:warpAtCell(cx, cy)
|
|
return self.warpAt[cy * self.widthCells + cx]
|
|
end
|
|
|
|
function Map:signAtCell(cx, cy)
|
|
return self.signAt[cy * self.widthCells + cx]
|
|
end
|
|
|
|
function Map:connection(dir)
|
|
return self.def.connections and self.def.connections[dir]
|
|
end
|
|
|
|
return Map
|