Something went wrong. Try again.
A Rust + SDL automapper for Wizardry 6 and 7 that uses Dosbox Staging's memory webAPI
Something went wrong. Try again.
43 kB · 990 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991use sdl3::pixels::Color;use sdl3::render::{Canvas, FRect, Texture};use sdl3::video::Window;
use crate::notes::NoteMap;use crate::wiz6::{Wiz6Frame, QUADRANT_COUNT as W6_Q, SQUARE_SIZE as W6_SZ};use crate::wiz7::{Wiz7Frame, QUADRANT_COUNT as W7_Q, SQUARE_SIZE as W7_SZ};
// ── Geometric fallback colours (stairs / fountain — not in working atlas) ─────const COL_STAIRS_UP: Color = Color::RGB(0xFF, 0xD7, 0x00);const COL_STAIRS_UP_D: Color = Color::RGB(0x80, 0x6C, 0x00);const COL_STAIRS_DN: Color = Color::RGB(0xFF, 0x64, 0x00);const COL_STAIRS_DN_D: Color = Color::RGB(0x80, 0x32, 0x00);const COL_FOUNTAIN: Color = Color::RGB(0x00, 0xC8, 0xFF);const COL_FOUNTAIN_D: Color = Color::RGB(0x00, 0x64, 0x80);const COL_CHEST: Color = Color::RGB(0xFF, 0x00, 0xFF);
fn frect(x: i32, y: i32, w: i32, h: i32) -> FRect { FRect::new(x as f32, y as f32, w as f32, h as f32)}
// ── Sprite blit ───────────────────────────────────────────────────────────────//// Scales the source rect (sx,sy,sw,sh) from the atlas texture into the// destination rect (dx,dy,dw,dh) on the canvas. dark=true dims to 50%.// Matches AM_DrawSprite() from am_main.cpp.
fn blit( canvas: &mut Canvas<Window>, tex: &mut Texture, dx: i32, dy: i32, dw: i32, dh: i32, sx: i32, sy: i32, sw: i32, sh: i32, dark: bool, flip_h: bool, flip_v: bool,) { if dw <= 0 || dh <= 0 || sw <= 0 || sh <= 0 { return; } if dark { tex.set_color_mod(128, 128, 128); } let src = FRect::new(sx as f32, sy as f32, sw as f32, sh as f32); let dst = FRect::new(dx as f32, dy as f32, dw as f32, dh as f32); if flip_h || flip_v { let _ = canvas.copy_ex(tex, Some(src), Some(dst), 0.0, None, flip_h, flip_v); } else { let _ = canvas.copy(tex, Some(src), Some(dst)); } if dark { tex.set_color_mod(255, 255, 255); }}
// ── Hit-testing (public) ──────────────────────────────────────────────────────
pub fn wiz7_screen_to_world( mx: f32, my: f32, canvas_w: u32, canvas_h: u32, player_abs_x: i32, player_abs_y: i32,) -> Option<(i32, i32)> { let cx = canvas_w as i32 / 2; let cy = canvas_h as i32 / 2; Some(( (mx as i32 - cx).div_euclid(W7_SZ) + player_abs_x, player_abs_y - (my as i32 - cy).div_euclid(W7_SZ), ))}
pub fn wiz7_world_to_cell(world_x: i32, world_y: i32, frame: &Wiz7Frame) -> Option<(u16, u16, u16)> { for q in 0..W7_Q { let col = world_x - frame.qsx[q] as i32; let row = world_y - frame.qsy[q] as i32; if (0..8).contains(&col) && (0..8).contains(&row) { return Some((q as u16, col as u16, row as u16)); } } None}
pub fn wiz7_player_abs(frame: &Wiz7Frame) -> Option<(i32, i32)> { let pq = frame.quadrant as usize; if pq >= W7_Q || frame.qsx.len() < W7_Q { return None; } Some(( frame.qsx[pq] as i32 + frame.player_qx as i32, frame.qsy[pq] as i32 + frame.player_qy as i32, ))}
// Wiz6 hit-testing — mirrors the Wiz7 helpers above, using the W6 square size.
pub fn wiz6_screen_to_world( mx: f32, my: f32, canvas_w: u32, canvas_h: u32, player_abs_x: i32, player_abs_y: i32,) -> Option<(i32, i32)> { let cx = canvas_w as i32 / 2; let cy = canvas_h as i32 / 2; Some(( (mx as i32 - cx).div_euclid(W6_SZ) + player_abs_x, player_abs_y - (my as i32 - cy).div_euclid(W6_SZ), ))}
pub fn wiz6_world_to_cell(world_x: i32, world_y: i32, frame: &Wiz6Frame) -> Option<(u16, u16, u16)> { frame .abs_to_quadrant(world_x, world_y) .map(|(q, qx, qy)| (q as u16, qx as u16, qy as u16))}
pub fn wiz6_player_abs(frame: &Wiz6Frame) -> Option<(i32, i32)> { let pq = frame.quadrant as usize; if pq >= W6_Q || frame.qsx.len() < W6_Q || frame.qsy.len() < W6_Q { return None; } Some(( frame.qsx[pq] as i32 + frame.player_qx as i32, frame.qsy[pq] as i32 + frame.player_qy as i32, ))}
// ── Geometric helpers (fallbacks for stairs / fountain) ───────────────────────
fn tri_up(canvas: &mut Canvas<Window>, cx: i32, cy: i32, hw: i32, h: i32) { for row in 0..h { let w = ((row + 1) * hw * 2 / h).max(1); let _ = canvas.fill_rect(frect(cx - w / 2, cy - h / 2 + row, w, 1)); }}fn tri_dn(canvas: &mut Canvas<Window>, cx: i32, cy: i32, hw: i32, h: i32) { for row in 0..h { let w = ((h - row) * hw * 2 / h).max(1); let _ = canvas.fill_rect(frect(cx - w / 2, cy - h / 2 + row, w, 1)); }}fn diamond(canvas: &mut Canvas<Window>, cx: i32, cy: i32, hw: i32, hh: i32) { for row in 0..hh * 2 { let t = if row < hh { row } else { hh * 2 - 1 - row }; let w = (t * hw * 2 / hh).max(1); let _ = canvas.fill_rect(frect(cx - w / 2, cy - hh + row, w, 1)); }}
// ── Wiz7 tileset tables ───────────────────────────────────────────────────────//// Mirrors W7TileSet / w7tilesets[] from am_wiz7.cpp.// Tile indices < 0: -1 = ROOF/ORCHID (skip overlay), -2 = DARK/PDARK (dark zone).// Wall indices are AMW7_* constants.
struct W7Ts { def_tile: i8, alt_tile: i8, add_tile0: i8, add_tile1: i8, def_wall: u8, // used for boundary walls passage: u8, solid0: u8, solid1: u8, // Solid-rock fill (W7_DetectSolidTiles): a fully wall-enclosed cell is filled // with solid_tile0, or solid_tile1 when enough of its walls are "solid wall 1" // (type 4). Counts of type-4 sides ≤ no_solid_coeff pick tile0; ≥ solid1_coeff // pick tile1; in-between draws nothing. Negative tile index = skip. solid_tile0: i8, solid_tile1: i8, no_solid_coeff: u8, solid1_coeff: u8,}
// Sprite indices match am_wiz7.cpp #definesconst WL_DUNGEON_SOLID: u8 = 0;const WL_DUNGEON_PASSAGE: u8 = 1;const WL_DUNGEON_PORTCULLIS: u8 = 2;const WL_CITY_SOLID: u8 = 3;const WL_CITY_PASSAGE: u8 = 4;const WL_CITY_DOOR: u8 = 5;const WL_FOREST_SOLID: u8 = 6;const WL_CAVE_SOLID: u8 = 7;const WL_BAD_WALL: u8 = 8;const WL_CITY_WINDOW: u8 = 10;const WL_CAVE_PASSAGE: u8 = 11;
const FL_WATER: i8 = 1; // AMW7_WATER_TILE// Solid-rock fill floor sprites (AMW7_*_SOLID_TILE / BAD_TILE).const FL_DUNGEON_SOLID: i8 = 5;const FL_CITY_SOLID: i8 = 6;const FL_FOREST_SOLID: i8 = 7;const FL_CAVE_SOLID: i8 = 8;const FL_BAD: i8 = 9;
#[rustfmt::skip]const W7_TILESETS: [W7Ts; 7] = [ // 0: invalid W7Ts { def_tile: 9, alt_tile: 9, add_tile0: 9, add_tile1: 9, def_wall: WL_BAD_WALL, passage: WL_BAD_WALL, solid0: WL_BAD_WALL, solid1: WL_BAD_WALL, solid_tile0: FL_BAD, solid_tile1: FL_BAD, no_solid_coeff: 0, solid1_coeff: 3 }, // 1: dungeon W7Ts { def_tile: 0, alt_tile: 1, add_tile0: -2, add_tile1: 9, def_wall: WL_DUNGEON_SOLID, passage: WL_DUNGEON_PASSAGE, solid0: WL_DUNGEON_SOLID, solid1: WL_BAD_WALL, solid_tile0: FL_DUNGEON_SOLID, solid_tile1: FL_BAD, no_solid_coeff: 0, solid1_coeff: 3 }, // 2: outdoor W7Ts { def_tile: 4, alt_tile: 3, add_tile0: 2, add_tile1: -1, def_wall: WL_FOREST_SOLID, passage: WL_BAD_WALL, solid0: WL_CITY_SOLID, solid1: WL_FOREST_SOLID, solid_tile0: FL_CITY_SOLID, solid_tile1: FL_FOREST_SOLID, no_solid_coeff: 0, solid1_coeff: 3 }, // 3: city W7Ts { def_tile: 3, alt_tile: 2, add_tile0: -1, add_tile1: 1, def_wall: WL_CITY_SOLID, passage: WL_CITY_PASSAGE, solid0: WL_CITY_SOLID, solid1: WL_BAD_WALL, solid_tile0: FL_CITY_SOLID, solid_tile1: FL_BAD, no_solid_coeff: 0, solid1_coeff: 3 }, // 4: forest+cave W7Ts { def_tile: 0, alt_tile: 3, add_tile0: -2, add_tile1: 9, def_wall: WL_FOREST_SOLID, passage: WL_BAD_WALL, solid0: WL_CAVE_SOLID, solid1: WL_FOREST_SOLID, solid_tile0: FL_CAVE_SOLID, solid_tile1: FL_FOREST_SOLID, no_solid_coeff: 0, solid1_coeff: 1 }, // 5: cave W7Ts { def_tile: 0, alt_tile: 1, add_tile0: -1, add_tile1: 5, def_wall: WL_CAVE_SOLID, passage: WL_BAD_WALL, solid0: WL_CAVE_SOLID, solid1: WL_BAD_WALL, solid_tile0: FL_CAVE_SOLID, solid_tile1: FL_BAD, no_solid_coeff: 0, solid1_coeff: 3 }, // 6: forest+outdoor W7Ts { def_tile: 4, alt_tile: 3, add_tile0: 1, add_tile1: 5, def_wall: WL_FOREST_SOLID, passage: WL_BAD_WALL, solid0: WL_CITY_SOLID, solid1: WL_FOREST_SOLID, solid_tile0: FL_CITY_SOLID, solid_tile1: FL_FOREST_SOLID, no_solid_coeff: 0, solid1_coeff: 3 },];
fn w7_ts(level: u16) -> &'static W7Ts { &W7_TILESETS[Wiz7Frame::map_tileset(level)]}
/// Returns true if this cell is a dark zone (floor0 set + addTile0==DARK).fn w7_is_dark_zone(frame: &Wiz7Frame, ts: &W7Ts, q: usize, col: usize, row: usize) -> bool { if ts.add_tile0 == -2 { let cell = q * 64 + row * 8 + col; return Wiz7Frame::get_bits(&frame.floor0, cell, 1) != 0; } false}
/// Floor tile sprite index for this cell, or None to skip.fn w7_floor_sprite(frame: &Wiz7Frame, ts: &W7Ts, q: usize, col: usize, row: usize) -> Option<i8> { let cell = q * 64 + row * 8 + col; if Wiz7Frame::get_bits(&frame.features, cell, 6) == 13 { return Some(FL_WATER); // fake water overrides } let f2 = Wiz7Frame::get_bits(&frame.floor2, cell, 1); let mut tile = if f2 != 0 { ts.alt_tile } else { ts.def_tile }; if tile < 0 { return None; } if ts.add_tile0 >= 0 && Wiz7Frame::get_bits(&frame.floor0, cell, 1) != 0 { tile = ts.add_tile0; } if ts.add_tile1 >= 0 && Wiz7Frame::get_bits(&frame.floor1, cell, 1) != 0 { tile = ts.add_tile1; } if tile < 0 { None } else { Some(tile) }}
/// Whether a 4-bit wall value blocks movement (mirrors W7VIS_IsWall): true for/// solid/solid1/window (2,4,7), false for none/passage/door/cave-passage/portcullis.fn w7_is_wall(val: u16) -> bool { !matches!(val, 0 | 1 | 3 | 5 | 6)}
/// Solid-rock fill sprite for a fully wall-enclosed cell, or None. Mirrors/// W7_DetectSolidTiles: a cell whose four sides are all walls is filled, choosing/// tile0/tile1 by how many of those sides are "solid wall 1" (type 4). Cross-/// quadrant neighbours are resolved via world coords; map edges count as walls.fn w7_solid_tile(frame: &Wiz7Frame, ts: &W7Ts, q: usize, col: usize, row: usize, qsx: i32, qsy: i32) -> Option<i8> { let cell = q * 64 + row * 8 + col; let r = Wiz7Frame::get_bits(&frame.vwalls, cell, 4); // right wall of this cell let t = Wiz7Frame::get_bits(&frame.hwalls, cell, 4); // top wall of this cell
// Left wall = right wall of the cell to the left (prev col, or prev quadrant). let (l, lw) = if col > 0 { let v = Wiz7Frame::get_bits(&frame.vwalls, q * 64 + row * 8 + (col - 1), 4); (v, w7_is_wall(v)) } else { match wiz7_world_to_cell(qsx - 1, qsy + row as i32, frame) { Some((nq, nc, nr)) => { let v = Wiz7Frame::get_bits(&frame.vwalls, nq as usize * 64 + nr as usize * 8 + nc as usize, 4); (v, w7_is_wall(v)) } None => (r, true), // map edge: treat as wall } }; // Bottom wall = top wall of the cell below (prev row, or prev quadrant). let (b, bw) = if row > 0 { let v = Wiz7Frame::get_bits(&frame.hwalls, q * 64 + (row - 1) * 8 + col, 4); (v, w7_is_wall(v)) } else { match wiz7_world_to_cell(qsx + col as i32, qsy - 1, frame) { Some((nq, nc, nr)) => { let v = Wiz7Frame::get_bits(&frame.hwalls, nq as usize * 64 + nr as usize * 8 + nc as usize, 4); (v, w7_is_wall(v)) } None => (t, true), } };
if !(lw && w7_is_wall(r) && w7_is_wall(t) && bw) { return None; } let s1c = [r, l, b, t].iter().filter(|&&v| v == 4).count() as u8; let tile = if s1c <= ts.no_solid_coeff { ts.solid_tile0 } else if s1c >= ts.solid1_coeff { ts.solid_tile1 } else { return None; }; if tile < 0 { None } else { Some(tile) }}
/// Wall sprite index for a 4-bit map wall value, using the current tileset.fn w7_wall_sprite(val: u16, ts: &W7Ts) -> Option<u8> { Some(match val { 0 => return None, 1 => ts.passage, // passage 2 => ts.solid0, // solid wall 0 3 => WL_CITY_DOOR, // door 4 => ts.solid1, // solid wall 1 5 => WL_CAVE_PASSAGE, // cave passage 6 => WL_DUNGEON_PORTCULLIS, // portcullis 7 => WL_CITY_WINDOW, // city window _ => ts.def_wall, // fallback })}
// ── Atlas sprite helpers ──────────────────────────────────────────────────────//// All pixel coords refer to the W7 atlas (256×32) or W6 atlas (256×16).// Sprite layout matches am_wiz7.cpp / am_wiz6.cpp UV tables.
// Wiz7 floor tile: 7×7 px at (index*8, 0) — one per tileset variantfn blit_w7_floor(canvas: &mut Canvas<Window>, tex: &mut Texture, dx: i32, dy: i32, dw: i32, dh: i32, idx: i8, dark: bool) { if idx < 0 { return; } blit(canvas, tex, dx, dy, dw, dh, idx as i32 * 8, 0, 7, 7, dark, false, false);}
// Wiz7 h-wall tile: 9×2 px at (index*10, 8)fn blit_w7_hwall(canvas: &mut Canvas<Window>, tex: &mut Texture, dx: i32, dy: i32, dw: i32, dh: i32, idx: u8, dark: bool) { blit(canvas, tex, dx, dy, dw, dh, idx as i32 * 10, 8, 9, 2, dark, false, false);}
// Wiz7 v-wall tile: 2×9 px at (index*4, 12)fn blit_w7_vwall(canvas: &mut Canvas<Window>, tex: &mut Texture, dx: i32, dy: i32, dw: i32, dh: i32, idx: u8, dark: bool) { blit(canvas, tex, dx, dy, dw, dh, idx as i32 * 4, 12, 2, 9, dark, false, false);}
// Wiz7 pit/ladder: 5×5 px at (16 + index*8, 24)fn blit_w7_pit(canvas: &mut Canvas<Window>, tex: &mut Texture, dx: i32, dy: i32, dw: i32, dh: i32, idx: u8, dark: bool) { blit(canvas, tex, dx, dy, dw, dh, 16 + idx as i32 * 8, 24, 5, 5, dark, false, false);}
// Cursor arrow (same atlas positions in both W6 and W7 atlases).// N: (174,0,11,14) E: (157,2,14,11) S=N+flip_v W=E+flip_hfn blit_cursor(canvas: &mut Canvas<Window>, tex: &mut Texture, dx: i32, dy: i32, dw: i32, dh: i32, dir: u16) { match dir & 3 { 0 => blit(canvas, tex, dx, dy, dw, dh, 174, 0, 11, 14, false, false, false), 1 => blit(canvas, tex, dx, dy, dw, dh, 157, 2, 14, 11, false, false, false), 2 => blit(canvas, tex, dx, dy, dw, dh, 174, 0, 11, 14, false, false, true), _ => blit(canvas, tex, dx, dy, dw, dh, 157, 2, 14, 11, false, true, false), }}
// ── Wiz7 feature icons ────────────────────────────────────────────────────────//// Stairs and fountain UVs are inverted in the atlas (v0>v1 → sh<0 → not drawn// by the C++ SDL port either). We keep geometric fallbacks for those.
fn draw_w7_feature( canvas: &mut Canvas<Window>, atlas: &mut Texture, px: i32, py: i32, sz: i32, feat: u16, dir: u16, dark: bool,) { // Direction offsets matching C++ direction[4][2] in W7_DrawQuadrantThirdPass. // Applied only to stairs (cases 1, 2) to displace the icon toward the adjacent cell. const DIR_DX: [i32; 4] = [0, W7_SZ, 0, -W7_SZ]; const DIR_DY: [i32; 4] = [-W7_SZ, 0, W7_SZ, 0]; let d = (dir as usize).min(3);
let cx = px + sz / 2; let cy = py + sz / 2; let hw = sz / 2 - 4; let hh = sz / 2 - 4; let off = 3; let icon_sz = sz - 5; let ix = px + off; let iy = py + off;
match feat { 1 => { // stairs up — geometric fallback, offset toward adjacent cell canvas.set_draw_color(if dark { COL_STAIRS_UP_D } else { COL_STAIRS_UP }); tri_up(canvas, cx + DIR_DX[d], cy + DIR_DY[d], hw, hh); } 2 => { // stairs down — geometric fallback, offset toward adjacent cell canvas.set_draw_color(if dark { COL_STAIRS_DN_D } else { COL_STAIRS_DN }); tri_dn(canvas, cx + DIR_DX[d], cy + DIR_DY[d], hw, hh); } 4 => { // fountain — geometric fallback (no direction) canvas.set_draw_color(if dark { COL_FOUNTAIN_D } else { COL_FOUNTAIN }); diamond(canvas, cx, cy, hw, hh); } // Pits and ladders — atlas pit/ladder sprites at y=24 (5×5 each), no direction 8 => blit_w7_pit(canvas, atlas, ix, iy, icon_sz, icon_sz, 3, dark), // ladder down 9 => blit_w7_pit(canvas, atlas, ix, iy, icon_sz, icon_sz, 1, dark), // ladder up 14 => blit_w7_pit(canvas, atlas, ix, iy, icon_sz, icon_sz, 2, dark), // pit down 15 => blit_w7_pit(canvas, atlas, ix, iy, icon_sz, icon_sz, 0, dark), // pit up 17 => blit_w7_pit(canvas, atlas, ix, iy, icon_sz, icon_sz, 4, dark), // pit both _ => {} }}
// ── Wiz6 feature icons ────────────────────────────────────────────────────────//// StairsDown has valid UVs in the atlas (142,0,13,13).// StairsUp and both Fountain sprites are inverted → geometric fallback.
fn draw_w6_feature( canvas: &mut Canvas<Window>, atlas: &mut Texture, px: i32, py: i32, sz: i32, feat: u16, _dir: u16, dark: bool,) { let cx = px + sz / 2; let cy = py + sz / 2; let hw = sz / 2 - 4; let hh = sz / 2 - 4; // offset matches C++: x+3, y+3, sz-3, sz-3 let off = 3; let icon_sz = sz - 3; let ix = px + off; let iy = py + off;
match feat { 1 => { // stairs up — geometric fallback (UV inverted in atlas) canvas.set_draw_color(if dark { COL_STAIRS_UP_D } else { COL_STAIRS_UP }); tri_up(canvas, cx, cy, hw, hh); } 2 => { // stairs down — sprite (142,0,13,13) valid blit(canvas, atlas, ix, iy, icon_sz, icon_sz, 142, 0, 13, 13, dark, false, false); } 4 => { // fountain — geometric fallback (UV inverted in atlas) canvas.set_draw_color(if dark { COL_FOUNTAIN_D } else { COL_FOUNTAIN }); diamond(canvas, cx, cy, hw, hh); } _ => {} }}
// ── Wiz6 main draw ────────────────────────────────────────────────────────────
pub fn draw_wiz6( canvas: &mut Canvas<Window>, frame: &Wiz6Frame, visdata: &[[[u8; 8]; 8]], notes: &NoteMap, atlas: &mut Texture, // Drag-to-pan offset in pixels, identical to the Wiz7 view: (0,0) keeps the // party centred and the cursor glued to screen centre. pan_x: i32, pan_y: i32,) { // main() already clears the canvas to COL_BG each frame. let level = frame.level as usize;
let pq = frame.quadrant as usize; if pq >= W6_Q || frame.qsx.len() < W6_Q || frame.qsy.len() < W6_Q { return; } let (cw, ch) = canvas.output_size().unwrap_or((704, 704)); let scr_cx = cw as i32 / 2 + pan_x; let scr_cy = ch as i32 / 2 + pan_y;
// The party's absolute world position. Every quadrant is placed relative to // it so the map stays centred on the party and scrolls smoothly across // quadrant boundaries (mirrors map_draw_x/y in am_wiz6.cpp). let player_abs_x = frame.qsx[pq] as i32 + frame.player_qx as i32; let player_abs_y = frame.qsy[pq] as i32 + frame.player_qy as i32;
for q in 0..W6_Q { let qsx = frame.qsx.get(q).copied().unwrap_or(0) as i32; let qsy = frame.qsy.get(q).copied().unwrap_or(0) as i32; let ox = scr_cx + (qsx - player_abs_x) * W6_SZ; let oy = scr_cy + (player_abs_y - qsy - 7) * W6_SZ;
// Pass 1: floor tiles — atlas sprites (dark/water/light) for row in 0..8usize { for col in 0..8usize { let vis = visdata[q][col][row]; if vis == 0 { continue; } let dark = vis != 1; let cell = q * 64 + row * 8 + col; let floor = Wiz6Frame::get_bits(&frame.floor, cell, 1); let feat = Wiz6Frame::get_bits(&frame.features, cell, 4); let roof = Wiz6Frame::get_bits(&frame.roof, cell, 1); let px = ox + col as i32 * W6_SZ + 1; let py = oy + (7 - row as i32) * W6_SZ + 1; let dw = W6_SZ - 1; let dh = W6_SZ - 1;
if floor == 0 && feat != 14 { // dark zone / unvisited ceiling blit(canvas, atlas, px, py, dw, dh, 16, 0, 7, 7, dark, false, false); } else if (level == 8 || level == 10 || level == 12) && floor != 0 && (roof != 0 || level == 8) { // water tile blit(canvas, atlas, px, py, dw, dh, 0, 0, 7, 7, dark, false, false); } else { blit(canvas, atlas, px, py, dw, dh, 32, 0, 7, 7, dark, false, false); } } }
// Pass 2: vertical walls — atlas sprites for row in 0..8i32 { for col in 0..8i32 { let vis = visdata[q][col as usize][row as usize]; let nbr = w6_neighbor_vis(visdata, frame, qsx, qsy, col + 1, row); if vis == 0 && nbr == 0 { continue; } let dark = vis != 1 && nbr != 1; let cell = q * 64 + row as usize * 8 + col as usize; let vwall = Wiz6Frame::get_bits(&frame.vwalls, cell, 2); let feat = Wiz6Frame::get_bits(&frame.features, cell, 4); let fdir = Wiz6Frame::get_bits(&frame.feat_dirs, cell, 2); let px = ox + (col + 1) * W6_SZ; let py = oy + (7 - row) * W6_SZ; let (sx, sy, sw, sh) = match vwall { 1 => (71, 0, 3, 9), // VPassage 2 => if feat == 7 && fdir == 1 { (42, 0, 3, 9) // VPortcullis } else { (103, 0, 3, 9) // VWall }, 3 => (135, 0, 3, 9), // VDoor _ => continue, }; blit(canvas, atlas, px, py, 3, W6_SZ + 2, sx, sy, sw, sh, dark, false, false); } }
// Boundary left wall for row in 0..8usize { if visdata[q][0][row] == 0 { continue; } let world_y = qsy + row as i32; let has_left = (0..W6_Q).any(|q2| { if q2 == q { return false; } let q2x = frame.qsx.get(q2).copied().unwrap_or(0) as i32; let q2y = frame.qsy.get(q2).copied().unwrap_or(0) as i32; q2x == qsx - 8 && (q2y..q2y + 8).contains(&world_y) }); if !has_left { let dark = visdata[q][0][row] != 1; blit(canvas, atlas, ox, oy + (7 - row as i32) * W6_SZ, 3, W6_SZ + 2, 103, 0, 3, 9, dark, false, false); } } // Boundary bottom wall for col in 0..8usize { if visdata[q][col][0] == 0 { continue; } let world_x = qsx + col as i32; let has_below = (0..W6_Q).any(|q2| { if q2 == q { return false; } let q2x = frame.qsx.get(q2).copied().unwrap_or(0) as i32; let q2y = frame.qsy.get(q2).copied().unwrap_or(0) as i32; q2y + 8 == qsy && (q2x..q2x + 8).contains(&world_x) }); if !has_below { let dark = visdata[q][col][0] != 1; blit(canvas, atlas, ox + col as i32 * W6_SZ, oy + 8 * W6_SZ, W6_SZ + 2, 3, 80, 0, 9, 3, dark, false, false); } }
// Pass 3: horizontal walls — atlas sprites for row in 0..8i32 { for col in 0..8i32 { let vis = visdata[q][col as usize][row as usize]; let nbr = w6_neighbor_vis(visdata, frame, qsx, qsy, col, row + 1); if vis == 0 && nbr == 0 { continue; } let dark = vis != 1 && nbr != 1; let cell = q * 64 + row as usize * 8 + col as usize; let hwall = Wiz6Frame::get_bits(&frame.hwalls, cell, 2); let feat = Wiz6Frame::get_bits(&frame.features, cell, 4); let fdir = Wiz6Frame::get_bits(&frame.feat_dirs, cell, 2); let px = ox + col * W6_SZ; let py = oy + (7 - row) * W6_SZ; let (sx, sy, sw, sh) = match hwall { 1 => (48, 0, 9, 3), // HPassage 2 => if feat == 7 && fdir == 0 { (60, 0, 9, 3) // HPortcullis } else { (80, 0, 9, 3) // HWall }, 3 => (112, 0, 9, 3), // HDoor _ => continue, }; blit(canvas, atlas, px, py, W6_SZ + 2, 3, sx, sy, sw, sh, dark, false, false); } }
// Pass 4: feature icons for row in 0..8i32 { for col in 0..8i32 { let vis = visdata[q][col as usize][row as usize]; if vis == 0 { continue; } let dark = vis != 1; let cell = q * 64 + row as usize * 8 + col as usize; let feat = Wiz6Frame::get_bits(&frame.features, cell, 4); let fdir = Wiz6Frame::get_bits(&frame.feat_dirs, cell, 2); let px = ox + col * W6_SZ + 1; let py = oy + (7 - row) * W6_SZ + 1; draw_w6_feature(canvas, atlas, px, py, W6_SZ - 1, feat, fdir, dark); } }
// Pass 5: note dots for row in 0..8i32 { for col in 0..8i32 { let key = (frame.level, q as u16, col as u16, row as u16); if let Some(note) = notes.get(&key) { let [r, g, b] = note.color; let px = ox + col * W6_SZ + W6_SZ / 2 - 3; let py = oy + (7 - row) * W6_SZ + W6_SZ / 2 - 3; canvas.set_draw_color(Color::RGB(r, g, b)); let _ = canvas.fill_rect(frect(px, py, 7, 7)); } } } }
// Player cursor — always at screen centre, like the Wiz7 view. blit_cursor(canvas, atlas, scr_cx, scr_cy, W6_SZ, W6_SZ, frame.direction);}
// ── Wiz7 main draw ────────────────────────────────────────────────────────────
pub fn draw_wiz7( canvas: &mut Canvas<Window>, frame: &Wiz7Frame, notes: &NoteMap, atlas: &mut Texture, // When true, walls are gated by the party's accumulated mapping reveal-power // (`spell_power`, the current level's flat [SP_CELLS] array): poorly-mapped // cells hide their walls (power≤1) or collapse passages/doors to solid (≤2), // matching the in-game cartography behaviour. When false, full detail. mapping_mode: bool, spell_power: &[u8], // Drag-to-pan offset in pixels. (0,0) keeps the party centred; the player // cursor is drawn at this same offset so it stays glued to its map cell. pan_x: i32, pan_y: i32,) { let (cw, ch) = canvas.output_size().unwrap_or((704, 704)); let cx = cw as i32 / 2 + pan_x; let cy = ch as i32 / 2 + pan_y;
let pq = frame.quadrant as usize; if pq >= W7_Q || frame.qsx.len() < W7_Q || frame.qsy.len() < W7_Q { return; }
let player_abs_x = frame.qsx[pq] as i32 + frame.player_qx as i32; let player_abs_y = frame.qsy[pq] as i32 + frame.player_qy as i32;
// main() already clears the canvas to COL_BG each frame. let ts = w7_ts(frame.level);
for q in 0..W7_Q { let qsx = frame.qsx[q] as i32; let qsy = frame.qsy[q] as i32; let ox = cx + (qsx - player_abs_x) * W7_SZ; let oy = cy + (player_abs_y - qsy - 7) * W7_SZ;
// Pass 1: floor tiles — atlas sprites for row in 0..8usize { for col in 0..8usize { let vis = frame.get_visited(q, col, row); if vis == 0 { continue; } if w7_is_dark_zone(frame, ts, q, col, row) { continue; } let dark = vis != 1; let px = ox + col as i32 * W7_SZ + 1; let py = oy + (7 - row as i32) * W7_SZ + 1; if let Some(idx) = w7_floor_sprite(frame, ts, q, col, row) { blit_w7_floor(canvas, atlas, px, py, W7_SZ - 1, W7_SZ - 1, idx, dark); } // Solid-rock fill overlay for fully-enclosed cells. if let Some(sidx) = w7_solid_tile(frame, ts, q, col, row, qsx, qsy) { blit_w7_floor(canvas, atlas, px, py, W7_SZ - 1, W7_SZ - 1, sidx, dark); } } }
// Pass 2: vertical walls — atlas sprites, bidir visibility for row in 0..8i32 { for col in 0..8i32 { let vis = frame.get_visited(q, col as usize, row as usize); let nbr = neighbor_visited(frame, qsx, qsy, col + 1, row); if vis == 0 && nbr == 0 { continue; } let dark = vis != 1 && nbr != 1; let cell = q * 64 + row as usize * 8 + col as usize; let mut vwall = Wiz7Frame::get_bits(&frame.vwalls, cell, 4); if mapping_mode { let sp = spell_power.get(cell).copied().unwrap_or(0) .max(w7_sp_at(frame, spell_power, qsx, qsy, col + 1, row)); if sp <= 1 { continue; } if sp <= 2 && matches!(vwall, 1 | 3 | 5 | 6 | 7) { vwall = 2; } } let Some(wi) = w7_wall_sprite(vwall, ts) else { continue }; let px = ox + (col + 1) * W7_SZ; let py = oy + (7 - row) * W7_SZ; blit_w7_vwall(canvas, atlas, px, py, 3, W7_SZ + 2, wi, dark); } }
// Pass 3: horizontal walls — atlas sprites, bidir visibility for row in 0..8i32 { for col in 0..8i32 { let vis = frame.get_visited(q, col as usize, row as usize); let nbr = neighbor_visited(frame, qsx, qsy, col, row + 1); if vis == 0 && nbr == 0 { continue; } let dark = vis != 1 && nbr != 1; let cell = q * 64 + row as usize * 8 + col as usize; let mut hwall = Wiz7Frame::get_bits(&frame.hwalls, cell, 4); if mapping_mode { let sp = spell_power.get(cell).copied().unwrap_or(0) .max(w7_sp_at(frame, spell_power, qsx, qsy, col, row + 1)); if sp <= 1 { continue; } if sp <= 2 && matches!(hwall, 1 | 3 | 5 | 6 | 7) { hwall = 2; } } let Some(wi) = w7_wall_sprite(hwall, ts) else { continue }; let px = ox + col * W7_SZ; let py = oy + (7 - row) * W7_SZ; blit_w7_hwall(canvas, atlas, px, py, W7_SZ + 2, 3, wi, dark); } }
// Pass 4: note dots for row in 0..8i32 { for col in 0..8i32 { let key = (frame.level, q as u16, col as u16, row as u16); if let Some(note) = notes.get(&key) { let [r, g, b] = note.color; let px = ox + col * W7_SZ + W7_SZ / 2 - 3; let py = oy + (7 - row) * W7_SZ + W7_SZ / 2 - 3; canvas.set_draw_color(Color::RGB(r, g, b)); let _ = canvas.fill_rect(frect(px, py, 7, 7)); } } }
// Pass 5: outer boundary walls for row in 0..8usize { if frame.get_visited(q, 0, row) == 0 { continue; } let world_y = qsy + row as i32; let has_left = (0..W7_Q).any(|q2| { if q2 == q { return false; } let q2x = frame.qsx[q2] as i32; let q2y = frame.qsy[q2] as i32; q2x == qsx - 8 && (q2y..q2y + 8).contains(&world_y) }); if !has_left { let dark = frame.get_visited(q, 0, row) != 1; blit_w7_vwall(canvas, atlas, ox, oy + (7 - row as i32) * W7_SZ, 3, W7_SZ + 2, ts.def_wall, dark); } } for col in 0..8usize { if frame.get_visited(q, col, 0) == 0 { continue; } let world_x = qsx + col as i32; let has_below = (0..W7_Q).any(|q2| { if q2 == q { return false; } let q2x = frame.qsx[q2] as i32; let q2y = frame.qsy[q2] as i32; q2y + 8 == qsy && (q2x..q2x + 8).contains(&world_x) }); if !has_below { let dark = frame.get_visited(q, col, 0) != 1; blit_w7_hwall(canvas, atlas, ox + col as i32 * W7_SZ, oy + 8 * W7_SZ, W7_SZ + 2, 3, ts.def_wall, dark); } }
// Pass 6: feature icons (stairs/fountain geometric, pits/ladders sprite) for row in 0..8i32 { for col in 0..8i32 { let vis = frame.get_visited(q, col as usize, row as usize); if vis == 0 { continue; } if w7_is_dark_zone(frame, ts, q, col as usize, row as usize) { continue; } let dark = vis != 1; let cell = q * 64 + row as usize * 8 + col as usize; let feat = Wiz7Frame::get_bits(&frame.features, cell, 6); let fdir = Wiz7Frame::get_bits(&frame.features_dirs, cell, 2); let px = ox + col * W7_SZ + 1; let py = oy + (7 - row) * W7_SZ + 1; draw_w7_feature(canvas, atlas, px, py, W7_SZ - 1, feat, fdir, dark); } }
// Pass 7: located objects (chests — magenta dot) for lo in &frame.located_objects { if lo.quadrant as usize != q { continue; } if frame.get_visited(q, lo.qx as usize, lo.qy as usize) == 0 { continue; } let px = ox + lo.qx as i32 * W7_SZ + W7_SZ / 2 - 1; let py = oy + (7 - lo.qy as i32) * W7_SZ + W7_SZ / 2; canvas.set_draw_color(COL_CHEST); let _ = canvas.fill_rect(frect(px, py, 3, 3)); } }
// Pass 8: player cursor (always at screen centre) blit_cursor(canvas, atlas, cx, cy, W7_SZ, W7_SZ, frame.direction);}
// ── Text dialog overlay ───────────────────────────────────────────────────────
const CHAR_W: i32 = 8;const CHAR_H: i32 = 8;
/// Selectable note colours, cycled with Tab in the note dialog. Index 0 is the/// historical default (red), so notes saved before per-note colours map to it.pub const NOTE_COLORS: [[u8; 3]; 8] = [ [0xFF, 0x20, 0x20], // 1 red (default) [0xFF, 0x80, 0x00], // 2 orange [0xFF, 0xE0, 0x00], // 3 yellow [0x40, 0xE0, 0x40], // 4 green [0x30, 0xD0, 0xE0], // 5 cyan [0x60, 0x80, 0xFF], // 6 blue [0xFF, 0x50, 0xFF], // 7 magenta [0xF0, 0xF0, 0xF0], // 8 white];
pub fn draw_text_dialog(canvas: &mut Canvas<Window>, text: &str, title: &str, color_idx: usize) { let (cw, ch) = canvas.output_size().unwrap_or((704, 704)); let dw = 420i32; let dh = 116i32; let dx = (cw as i32 - dw) / 2; let dy = (ch as i32 - dh) / 2; let pad = 10i32;
canvas.set_draw_color(Color::RGB(0x10, 0x10, 0x30)); let _ = canvas.fill_rect(frect(dx, dy, dw, dh)); canvas.set_draw_color(Color::RGB(0x80, 0x80, 0xFF)); for (x, y, w, h) in [ (dx, dy, dw, 1), (dx, dy + dh - 1, dw, 1), (dx, dy, 1, dh), (dx + dw - 1, dy, 1, dh), ] { let _ = canvas.fill_rect(frect(x, y, w, h)); }
canvas.set_draw_color(Color::RGB(0xFF, 0xFF, 0x80)); let _ = canvas.draw_debug_text(title, (dx + pad, dy + pad));
let input_y = dy + pad + CHAR_H + 6; canvas.set_draw_color(Color::RGB(0x20, 0x20, 0x50)); let _ = canvas.fill_rect(frect(dx + pad, input_y - 2, dw - pad * 2, CHAR_H + 4)); canvas.set_draw_color(Color::RGB(0xFF, 0xFF, 0xFF)); // Blink the caret on a ~1s cycle (500ms on / 500ms off). A space (not empty) // in the off phase keeps the text width and truncation window stable. let caret_on = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_millis() % 1000 < 500) .unwrap_or(true); let display = format!("{text}{}", if caret_on { '|' } else { ' ' }); let max_chars = ((dw - pad * 2) / CHAR_W) as usize; let shown = if display.len() > max_chars { &display[display.len() - max_chars..] } else { &display }; let _ = canvas.draw_debug_text(shown, (dx + pad, input_y));
// Colour swatch row — the selected swatch gets a white outline. let sw = 22i32; let gap = 6i32; let sw_y = input_y + CHAR_H + 8; for (i, c) in NOTE_COLORS.iter().enumerate() { let sx = dx + pad + i as i32 * (sw + gap); canvas.set_draw_color(Color::RGB(c[0], c[1], c[2])); let _ = canvas.fill_rect(frect(sx, sw_y, sw, CHAR_H + 4)); if i == color_idx { canvas.set_draw_color(Color::RGB(0xFF, 0xFF, 0xFF)); for (x, y, w, h) in [ (sx - 1, sw_y - 1, sw + 2, 1), (sx - 1, sw_y + CHAR_H + 4, sw + 2, 1), (sx - 1, sw_y - 1, 1, CHAR_H + 6), (sx + sw, sw_y - 1, 1, CHAR_H + 6), ] { let _ = canvas.fill_rect(frect(x, y, w, h)); } } }
let hint_y = dy + dh - pad - CHAR_H; canvas.set_draw_color(Color::RGB(0x80, 0x80, 0x80)); let _ = canvas.draw_debug_text("Enter=ok Esc=cancel Tab=colour blank=delete", (dx + pad, hint_y));}
/// Centered overlay listing every interaction. Toggled with H.pub fn draw_help(canvas: &mut Canvas<Window>) { let (cw, ch) = canvas.output_size().unwrap_or((704, 704)); let lines: [&str; 12] = [ "Controls", "", "Left-click cell Add / edit a note", " Tab Cycle note colour", " Enter Save note (blank = delete)", " Esc Cancel note", "Right-click Clear all notes", "Middle-drag Pan the map", "C / Home Recenter on party", "M Toggle mapping-skill view", "H Toggle this help", "Esc Quit (closes help first)", ]; let pad = 16i32; let line_h = CHAR_H + 6; let dw = 480i32; let dh = pad * 2 + lines.len() as i32 * line_h; let dx = (cw as i32 - dw) / 2; let dy = (ch as i32 - dh) / 2;
canvas.set_draw_color(Color::RGB(0x10, 0x10, 0x30)); let _ = canvas.fill_rect(frect(dx, dy, dw, dh)); canvas.set_draw_color(Color::RGB(0x80, 0x80, 0xFF)); for (x, y, w, h) in [ (dx, dy, dw, 1), (dx, dy + dh - 1, dw, 1), (dx, dy, 1, dh), (dx + dw - 1, dy, 1, dh), ] { let _ = canvas.fill_rect(frect(x, y, w, h)); }
for (i, line) in lines.iter().enumerate() { let y = dy + pad + i as i32 * line_h; let color = if i == 0 { Color::RGB(0xFF, 0xFF, 0x80) } else { Color::RGB(0xE0, 0xE0, 0xE0) }; canvas.set_draw_color(color); let _ = canvas.draw_debug_text(line, (dx + pad, y)); }}
// ── Internal helpers ──────────────────────────────────────────────────────────
fn neighbor_visited(frame: &Wiz7Frame, qsx: i32, qsy: i32, qcol: i32, qrow: i32) -> u8 { match wiz7_world_to_cell(qsx + qcol, qsy + qrow, frame) { Some((q, col, row)) => frame.get_visited(q as usize, col as usize, row as usize), None => 0, }}
/// Reveal power for the cell at quadrant-relative (qcol,qrow) offset from a/// quadrant origin (qsx,qsy), resolving across quadrant boundaries. 0 if off-map.fn w7_sp_at(frame: &Wiz7Frame, sp: &[u8], qsx: i32, qsy: i32, qcol: i32, qrow: i32) -> u8 { match wiz7_world_to_cell(qsx + qcol, qsy + qrow, frame) { Some((q, col, row)) => sp .get(q as usize * 64 + row as usize * 8 + col as usize) .copied() .unwrap_or(0), None => 0, }}
/// Stamp the party's mapping-skill reveal power into the per-level accumulator,/// mirroring W7_Mapping(useSkill=true): a fixed 15×15-square area centred on the/// party, each cell raised to at least `frame.mapping_power`. `sp` is the current/// level's flat [SP_CELLS] cell array. No-op when nobody can map or when the/// party stands in a magic-dark zone (matches the C++ early-out).pub fn w7_stamp_mapping(frame: &Wiz7Frame, sp: &mut [u8]) { let power = frame.mapping_power; if power == 0 { return; } let Some((px, py)) = wiz7_player_abs(frame) else { return }; let ts = w7_ts(frame.level); if let Some((pq, pcol, prow)) = wiz7_world_to_cell(px, py, frame) { if w7_is_dark_zone(frame, ts, pq as usize, pcol as usize, prow as usize) { return; } } const HALF: i32 = 7; // 15-square effect window, half-size for dy in -HALF..=HALF { for dx in -HALF..=HALF { if let Some((q, col, row)) = wiz7_world_to_cell(px + dx, py + dy, frame) { let cell = q as usize * 64 + row as usize * 8 + col as usize; if let Some(slot) = sp.get_mut(cell) { if power > *slot { *slot = power; } } } } }}
fn w6_neighbor_vis( visdata: &[[[u8; 8]; 8]], frame: &Wiz6Frame, qsx: i32, qsy: i32, qcol: i32, qrow: i32,) -> u8 { let wx = qsx + qcol; let wy = qsy + qrow; for q2 in 0..W6_Q { let q2x = frame.qsx.get(q2).copied().unwrap_or(0) as i32; let q2y = frame.qsy.get(q2).copied().unwrap_or(0) as i32; let col = wx - q2x; let row = wy - q2y; if (0..8).contains(&col) && (0..8).contains(&row) { return visdata[q2][col as usize][row as usize]; } } 0}