From 25655a02f68eeed8c724df17dad3c47d73638987 Mon Sep 17 00:00:00 2001 From: Badal Shah <72878759+dhyanamgit@users.noreply.github.com> Date: Sat, 17 Oct 2026 18:59:43 +0530 Subject: [PATCH 1/2] Sprig App - The Liar --- games/The-Liar.js | 3466 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3466 insertions(+) create mode 100644 games/The-Liar.js diff --git a/games/The-Liar.js b/games/The-Liar.js new file mode 100644 index 0000000000..86e98ae103 --- /dev/null +++ b/games/The-Liar.js @@ -0,0 +1,3466 @@ +const PLAYER = "p"; +const GUARD = "g"; +const MIRA = "1"; +const JON = "2"; +const RAVI = "3"; +const DARK = "8"; +const WALL = "w"; +const FLOOR = "f"; +const HUD = "h"; +const DOOR = "d"; +const PC = "c"; +const NOTEBOOK = "n"; +const EMAIL = "m"; +const PHOTO = "t"; +const SHELF = "k"; +const PLANT = "i"; +const RACK = "r"; +const LOG = "l"; +const PHONE = "v"; +const VENT = "e"; +const KEYCARD = "y"; +const SAFE = "s"; +const CABINET = "z"; +const COATRACK = "j"; +const SIGNBOOK = "q"; +const MONITOR = "u"; +const LOCKER = "x"; +const BREAKER = "o"; +const TOOLBOX = "b"; +const BOX = "a"; +const COPIER = "4"; +const CAMBAG = "5"; +const DRAWER = "6"; +const TRASH = "7"; + +const BOOM = "Z"; +const PBULLET = "P"; +const EBULLET = "B"; +const CRUISER = "C"; +const VAN = "V"; +const TRAFFIC = "T"; +const SPIKES = "S"; +const AMMO = "A"; +const MEDKIT = "H"; +const CRATE = "X"; +const DOCK_CRATE = "K"; +const CRACKED_CRATE = "Q"; +const ROAD = "R"; +const EDGE_A = "E"; +const EDGE_B = "F"; +const PLANK = "O"; +const WATER = "W"; +const BOAT = "Y"; + +setLegend( + [PLAYER, bitmap` +................ +....00000000.... +...0000000000... +..000000000000.. +....08888880.... +....08088080.... +....08888880.... +.....888888..... +...CCCCCCCCCC... +..CCCC2CC2CCCC.. +..CC.CCCCCC.CC.. +..88.CCCCCC.88.. +.....CCCCCC..... +.....CC..CC..... +.....CC..CC..... +.....00..00.....`], + [GUARD, bitmap` +................ +....55555555.... +...5555555555... +....00000000.... +....08888880.... +....08088080.... +....08888880.... +.....888888..... +...5555555555... +..555565565555.. +..55.555555.55.. +..88.555555.88.. +.....555555..... +.....55..55..... +.....55..55..... +.....00..00.....`], + [MIRA, bitmap` +................ +.....000000..... +....00000000.... +...0088888800... +...0080880800... +...0088888800... +...0088888800... +....HHHHHHHH.... +...HH22HHHHHH... +...HH22HHHHHH... +...8HHHHHHHH8... +....HHHHHHHH.... +....LLL..LLL.... +....LLL..LLL.... +....000..000.... +................`], + [JON, bitmap` +................ +.....CCCCCC..... +....CCCCCCCC.... +....C888888C.... +....C808808C.... +....C888888C.... +.....888888..... +....LLLLLLLL.... +...LL99LLLLLL... +...LL99LLLLLL... +...8LLLLLLLL8... +....LLLLLLLL.... +....000..000.... +....000..000.... +....000..000.... +................`], + [RAVI, bitmap` +................ +.....000000..... +....00000000.... +....08888880.... +....08088080.... +....08888880.... +.....888888..... +....55555555.... +...5566555555... +...5566555555... +...8555555558... +....55555555.... +....LLL..LLL.... +....LLL..LLL.... +....000..000.... +................`], + [BOOM, bitmap` +................ +.......9........ +...9...9...9.... +....9.999.9..... +.....99699...... +...9996669999... +..999666666999.. +...9996669999... +.....99699...... +....9.999.9..... +...9...9...9.... +.......9........ +................ +................ +................ +................`], + [PBULLET, bitmap` +................ +................ +................ +................ +................ +................ +......6666...... +......6666...... +......6666...... +......6666...... +................ +................ +................ +................ +................ +................`], + [EBULLET, bitmap` +................ +................ +................ +................ +................ +................ +......3333...... +......3333...... +......3333...... +......3333...... +................ +................ +................ +................ +................ +................`], + [CRUISER, bitmap` +................ +.....000000..... +....02222220.... +...0222222220... +...0277777720... +...0277777720... +...0222222220... +...0555223330... +...0222222220... +...0200000020... +...0222222220... +...0222222220... +...0277777720... +...0222222220... +....00000000.... +................`], + [VAN, bitmap` +................ +...0000000000... +...0LLLLLLLL0... +...0L777777L0... +...0L777777L0... +...0LLLLLLLL0... +...0LLLLLLLL0... +...0LLLLLLLL0... +...0LLLLLLLL0... +...0LLLLLLLL0... +...0LLLLLLLL0... +...0LLLLLLLL0... +...03LLLLLL30... +...0LLLLLLLL0... +...0000000000... +....00....00....`], + [TRAFFIC, bitmap` +................ +................ +....00000000.... +...0666666660... +...0677777760... +...0677777760... +...0666666660... +...0666666660... +...0666666660... +...0666666660... +...0677777760... +...0666666660... +...0366666630... +....00000000.... +................ +................`], + [SPIKES, bitmap` +................ +................ +................ +................ +................ +................ +..1...1...1...1. +.111.111.111.111 +1111111111111111 +LLLLLLLLLLLLLLLL +0000000000000000 +................ +................ +................ +................ +................`], + [AMMO, bitmap` +................ +................ +................ +...0000000000... +...0FFFFFFFF0... +...0F669966F0... +...0F669966F0... +...0FFFFFFFF0... +...0F222222F0... +...0FFFFFFFF0... +...0000000000... +................ +................ +................ +................ +................`], + [MEDKIT, bitmap` +................ +................ +...0000000000... +...0222222220... +...0222332220... +...0222332220... +...0233333320... +...0233333320... +...0222332220... +...0222332220... +...0222222220... +...0000000000... +................ +................ +................ +................`], + [CRATE, bitmap` +................ +................ +..CCCCCCCCCCCC.. +..C9999999999C.. +..C9C999999C9C.. +..C99C9999C99C.. +..C999C99C999C.. +..C9999CC9999C.. +..C9999CC9999C.. +..C999C99C999C.. +..C99C9999C99C.. +..C9C999999C9C.. +..C9999999999C.. +..CCCCCCCCCCCC.. +................ +................`], + [DOCK_CRATE, bitmap` +CCCCCCCCCCCCCCCC +C99999999999999C +C9C9999999999C9C +C99C99999999C99C +C999C999999C999C +C9999C9999C9999C +C99999C99C99999C +C999999CC999999C +C999999CC999999C +C99999C99C99999C +C9999C9999C9999C +C999C999999C999C +C99C99999999C99C +C9C9999999999C9C +C99999999999999C +CCCCCCCCCCCCCCCC`], + [CRACKED_CRATE, bitmap` +CCCCCCC..CCCCCCC +C99999.0..99999C +C9C999.0.9999C9C +C99C9..0.999C99C +C999C.0..99C999C +C9999.0...C9999C +C9999..0.C99999C +C99999.0C999999C +C99999.0C999999C +C9999..0.C99999C +C999..0...C9999C +C99C..0..999C99C +C9C9...0.9999C9C +C99999.0.999999C +C999999..999999C +CCCCCCC..CCCCCCC`], + [DARK, bitmap` +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000`], + [WALL, bitmap` +LLLLLLLLLLLLLLLL +1111111L1111111L +1111111L1111111L +1111111L1111111L +LLLLLLLLLLLLLLLL +111L1111111L1111 +111L1111111L1111 +111L1111111L1111 +LLLLLLLLLLLLLLLL +1111111L1111111L +1111111L1111111L +1111111L1111111L +LLLLLLLLLLLLLLLL +111L1111111L1111 +111L1111111L1111 +111L1111111L1111`], + [FLOOR, bitmap` +2222222222222221 +2222222222222221 +2222222222222221 +2222222222222221 +2222222222222221 +2222222222222221 +2222222222222221 +2222222222222221 +2222222222222221 +2222222222222221 +2222222222222221 +2222222222222221 +2222222222222221 +2222222222222221 +2222222222222221 +1111111111111111`], + [HUD, bitmap` +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000 +0000000000000000`], + [DOOR, bitmap` +0000000000000000 +0CCCCCCCCCCCCCC0 +0C999999999999C0 +0C9CCCCCCCCCC9C0 +0C9C99999999C9C0 +0C9C99999999C9C0 +0C9CCCCCCCCCC9C0 +0C999999999669C0 +0C999999999669C0 +0C9CCCCCCCCCC9C0 +0C9C99999999C9C0 +0C9C99999999C9C0 +0C9C99999999C9C0 +0C9CCCCCCCCCC9C0 +0C999999999999C0 +0CCCCCCCCCCCCCC0`], + [PC, bitmap` +................ +..000000000000.. +..055555555550.. +..057777777750.. +..057222777750.. +..057777777750.. +..055555555550.. +..000000000000.. +.......00....... +.....000000..... +CCCCCCCCCCCCCCCC +CFFFFFFFFFFFFFFC +CCCCCCCCCCCCCCCC +.CC..........CC. +.CC..........CC. +.CC..........CC.`], + [NOTEBOOK, bitmap` +................ +................ +...0000000000... +...0H66666660... +...0H66666660... +...0H60000060... +...0H66666660... +...0H60000060... +...0H66666660... +...0H60000060... +...0H66666660... +...0000000000... +................ +................ +................ +................`], + [EMAIL, bitmap` +................ +................ +..000000000000.. +..022222222220.. +..020222222020.. +..022022220220.. +..022200002220.. +..022222222220.. +..021111111120.. +..022222222220.. +..021111111120.. +..022222222220.. +..000000000000.. +................ +................ +................`], + [PHOTO, bitmap` +................ +................ +..000000000000.. +..0LLLLLLLLLL0.. +..0L11111111L0.. +..0L11199111L0.. +..0L11199111L0.. +..0L11999911L0.. +..0L11999911L0.. +..0L11199111L0.. +..0LLLLLLLLLL0.. +..000000000000.. +................ +................ +................ +................`], + [SHELF, bitmap` +CCCCCCCCCCCCCCCC +C33H55744H366D5C +C33H55744H366D5C +C33H55744H366D5C +CCCCCCCCCCCCCCCC +C5577H33D44HH77C +C5577H33D44HH77C +C5577H33D44HH77C +CCCCCCCCCCCCCCCC +C944HH5533DD99HC +C944HH5533DD99HC +C944HH5533DD99HC +CCCCCCCCCCCCCCCC +C..............C +C..............C +CCCCCCCCCCCCCCCC`], + [PLANT, bitmap` +................ +......4..4...... +.....44.444..... +....4444444..... +...44D44D444.... +...4D4444D44.... +....44D4444..... +.....44D44...... +......DDD....... +.....CCCCCC..... +.....C9999C..... +.....C9999C..... +......C99C...... +......CCCC...... +................ +................`], + [RACK, bitmap` +0000000000000000 +0LLLLLLLLLLLLLL0 +0L000000000000L0 +0L044000000330L0 +0L000000000000L0 +0LLLLLLLLLLLLLL0 +0L000000000000L0 +0L040000000770L0 +0L000000000000L0 +0LLLLLLLLLLLLLL0 +0L000000000000L0 +0L044000000440L0 +0L000000000000L0 +0LLLLLLLLLLLLLL0 +0LLLLLLLLLLLLLL0 +0000000000000000`], + [LOG, bitmap` +................ +..000000000000.. +..022222222220.. +..044444444440.. +..022222222220.. +..044444444440.. +..022222222220.. +..044444444440.. +..022222222220.. +..044444444440.. +..022222222220.. +..000000000000.. +................ +................ +................ +................`], + [PHONE, bitmap` +................ +................ +...3333333333... +..333333333333.. +..333......333.. +..33........33.. +......3333...... +.....333333..... +....33333333.... +...3320202023... +...3302020203... +...3320202023... +...3333333333... +................ +................ +................`], + [VENT, bitmap` +................ +................ +................ +.LLLLLLLLLLLLLL. +.L111111111111L. +.L000000000000L. +.L111111111111L. +.L000000000000L. +.L111111111111L. +.L000000000000L. +.L111111111111L. +.LLLLLLLLLLLLLL. +................ +................ +................ +................`], + [KEYCARD, bitmap` +................ +................ +................ +................ +...0000000000... +...0999999990... +...0922299990... +...0922299990... +...0999999990... +...0900000090... +...0999999990... +...0000000000... +................ +................ +................ +................`], + [SAFE, bitmap` +................ +.00000000000000. +.0LLLLLLLLLLLL0. +.0L1111111111L0. +.0L1111111111L0. +.0L1110000111L0. +.0L1110660111L0. +.0L1110660111L0. +.0L1110000111L0. +.0L1111111111L0. +.0L1111111166L0. +.0L1111111111L0. +.0LLLLLLLLLLLL0. +.00000000000000. +..00........00.. +................`], + [CABINET, bitmap` +.00000000000000. +.01111111111110. +.01LLLLLLLLLL10. +.01L22222222L10. +.01LLLLLLLLLL10. +.01111111111110. +.01111LLLL11110. +.01111111111110. +.00000000000000. +.01110444401110. +.01110626201110. +.01110262601110. +.01110000001110. +.01111111111110. +.01111111111110. +.00000000000000.`], + [COATRACK, bitmap` +.......CC....... +.....CCCCCC..... +.......CC....... +.....LLLLLL..... +....LLLLLLLL.... +....LLLLLLLL.... +....LL99LLLL.... +....LL99LLLL.... +....LLLLLLLL.... +....LLLLLLLL.... +.......CC....... +.......CC....... +.......CC....... +.......CC....... +.....CCCCCC..... +................`], + [SIGNBOOK, bitmap` +................ +................ +....22222222.... +....20000002.... +....22222222.... +....20000002.... +....22222222.... +CCCCCCCCCCCCCCCC +CFFFFFFFFFFFFFFC +CCCCCCCCCCCCCCCC +.CC..........CC. +.CC..........CC. +.CC..........CC. +................ +................ +................`], + [MONITOR, bitmap` +0000000000000000 +0LLLLLLLLLLLLLL0 +0L000000000000L0 +0L044400077700L0 +0L040400070700L0 +0L044400077700L0 +0L000000000000L0 +0LLLLLLLLLLLLLL0 +0000000000000000 +.......00....... +.....000000..... +CCCCCCCCCCCCCCCC +CFFFFFFFFFFFFFFC +CCCCCCCCCCCCCCCC +.CC..........CC. +.CC..........CC.`], + [LOCKER, bitmap` +0000000000000000 +0LLLLLL00LLLLLL0 +0L1111L00L1111L0 +0L0000L00L0000L0 +0L1111L00L1111L0 +0L0000L00L0000L0 +0L1111L00L1111L0 +0LLLLLL00LLLLLL0 +0LLLL6L00L6LLLL0 +0LLLLLL00LLLLLL0 +0LLLLLL00LLLLLL0 +0LLLLLL00LLLLLL0 +0LLLLLL00LLLLLL0 +0LLLLLL00LLLLLL0 +0000000000000000 +.00..........00.`], + [BREAKER, bitmap` +0000000000000000 +0LLLLLLLLLLLLLL0 +0L111111111111L0 +0L144114411441L0 +0L144114411441L0 +0L111111111111L0 +0L133113311331L0 +0L133113311331L0 +0L111111111111L0 +0L144114411331L0 +0L144114411331L0 +0L111111111111L0 +0LLLLLLLLLLLLLL0 +0LLLLLL66LLLLLL0 +0LLLLLLLLLLLLLL0 +0000000000000000`], + [TOOLBOX, bitmap` +................ +................ +................ +.....000000..... +.....0....0..... +.....0....0..... +..333333333333.. +..333333333333.. +..300000000003.. +..333333333333.. +..333332233333.. +..333333333333.. +..333333333333.. +..333333333333.. +................ +................`], + [BOX, bitmap` +................ +................ +................ +..CCCCCCCCCCCC.. +..C9999CC9999C.. +..C9999CC9999C.. +..CCCCCCCCCCCC.. +..C9999999999C.. +..C9999999999C.. +..C9922222299C.. +..C9922222299C.. +..C9999999999C.. +..C9999999999C.. +..CCCCCCCCCCCC.. +................ +................`], + [COPIER, bitmap` +................ +.11111111111111. +.1LLLLLLLLLLLL1. +.11111111111111. +.12222222222221. +.11111111111111. +.1L7777777777L1. +.1LLLLLLLLLLLL1. +.11111111111441. +.11111111111111. +.1LLLLLLLLLLLL1. +.12222222222221. +.11111111111111. +.11111111111111. +.LL..........LL. +................`], + [CAMBAG, bitmap` +................ +................ +......0000...... +.....0....0..... +....0......0.... +...0000000000... +...0LLLLLLLL0... +...0L000000L0... +...0L011110L0... +...0L017710L0... +...0L011110L0... +...0L000000L0... +...0LLLL22LL0... +...0000000000... +................ +................`], + [DRAWER, bitmap` +................ +................ +................ +................ +CCCCCCCCCCCCCCCC +CFFFFFFFFFFFFFFC +CCCCCCCCCCCCCCCC +CCCCCCCC......CC +C999999C......CC +C996699C......CC +C999999C......CC +CCCCCCCC......CC +C999999C......CC +C996699C......CC +C999999C......CC +CCCCCCCC......CC`], + [TRASH, bitmap` +................ +................ +................ +....22.2..2..... +...LLLLLLLLLL... +....L111111L.... +....L1L11L1L.... +....L1L11L1L.... +....L1L11L1L.... +....L1L11L1L.... +....L1L11L1L.... +....L1L11L1L.... +....L111111L.... +....LLLLLLLL.... +................ +................`], + [ROAD, bitmap` +LLLLLLLLLLLLLLLL +LLLLLLLLLLLLLLLL +LLLLLLLLLLL1LLLL +LLLLLLLLLLLLLLLL +1LLLLLLLLLLLLLLL +1LLLLLLLLLLLLLLL +1LLL1LLLLLLLLLLL +1LLLLLLLLLLLLLLL +1LLLLLLLLLLLLLLL +1LLLLLLLLLLL1LLL +1LLLLLLLLLLLLLLL +1LLLLLLLLLLLLLLL +LLLLLLLLLLLLLLLL +LLLLLL1LLLLLLLLL +LLLLLLLLLLLLLLLL +LLLLLLLLLLLLLLLL`], + [EDGE_A, bitmap` +4444444444444444 +44D44444444D4444 +4DDD4444444DD444 +4444444444444444 +4444444444444444 +4444444D44444444 +444444DDD4444444 +4444444444444444 +4444444444444444 +44D44444444D4444 +4DDD4444444DD444 +4444444444444444 +4444444444444444 +4444444D44444444 +444444DDD4444444 +4444444444444444`], + [EDGE_B, bitmap` +4444444444444444 +4444444D44444444 +444444DDD4444444 +4444444444444444 +4444444444444444 +44D44444444D4444 +4DDD4444444DD444 +4444444444444444 +4444444444444444 +4444444D44444444 +444444DDD4444444 +4444444444444444 +4444444444444444 +44D44444444D4444 +4DDD4444444DD444 +4444444444444444`], + [PLANK, bitmap` +CCCCCCCCCCCCCCCC +9999999999999999 +9999999C99999999 +9999999999999999 +CCCCCCCCCCCCCCCC +9999999999999999 +999999999999C999 +9999999999999999 +CCCCCCCCCCCCCCCC +9999999999999999 +99C9999999999999 +9999999999999999 +CCCCCCCCCCCCCCCC +9999999999999999 +9999999999C99999 +9999999999999999`], + [WATER, bitmap` +5555555555555555 +5555555555555555 +5577555555557755 +5755755555575575 +5555555555555555 +5555555555555555 +5555555775555555 +5555557557555555 +5555555555555555 +5555555555555555 +5775555555555775 +7557555555557557 +5555555555555555 +5555555555555555 +5555555555555555 +5555555555555555`], + [BOAT, bitmap` +5555555555555555 +5555555005555555 +5555550220555555 +5555502222055555 +5555022222205555 +5550222222220555 +5502222222222055 +5022222222222205 +0222222222222220 +0333333333333330 +0222222222222220 +5022222222222205 +5500000000000055 +5577555555557755 +5755755555575575 +5555555555555555`] +); + +setBackground(FLOOR); + +setSolids([ + PLAYER, GUARD, MIRA, JON, RAVI, WALL, HUD, DOOR, PC, NOTEBOOK, EMAIL, + PHOTO, SHELF, PLANT, RACK, LOG, PHONE, VENT, KEYCARD, SAFE, CABINET, + COATRACK, SIGNBOOK, MONITOR, LOCKER, BREAKER, TOOLBOX, BOX, COPIER, + CAMBAG, DRAWER, TRASH +]); + +const WHITE = color`2`; +const GREY = color`1`; +const YELLOW = color`6`; +const GREEN = color`4`; +const RED = color`3`; +const ORANGE = color`9`; +const CYAN = color`7`; + +function randInt(min, max) { + return min + Math.floor(Math.random() * (max - min + 1)); +} + +function pickOne(list) { + return list[Math.floor(Math.random() * list.length)]; +} + +function shuffled(list) { + const copy = list.slice(); + for (let i = copy.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const temp = copy[i]; + copy[i] = copy[j]; + copy[j] = temp; + } + return copy; +} + +function pad2(n) { + return n < 10 ? "0" + n : "" + n; +} + +function padRight(text, size) { + let out = text; + while (out.length < size) out += " "; + return out; +} + +function sign(n) { + if (n > 0) return 1; + if (n < 0) return -1; + return 0; +} + +const SCREEN_MAP = map` +hhhhhhhhhh +hhhhhhhhhh +hhhhhhhhhh +hhhhhhhhhh +hhhhhhhhhh +hhhhhhhhhh +hhhhhhhhhh +hhhhhhhhhh`; + +const HIGHWAY_MAP = map` +ERRRRRRRRE +ERRRRRRRRE +ERRRRRRRRE +ERRRRRRRRE +ERRRRRRRRE +ERRRRRRRRE +ERRRRRRRRE +hhhhhhhhhh`; + +const DOCKS_MAP = map` +WWWWYWWWWW +OOOOOOOOOO +OOOOOOOOOO +OOOOOOOOOO +OOOOOOOOOO +OOOOOOOOOO +OOOOOOOOOO +hhhhhhhhhh`; + +const LOBBY = 0; +const OFFICE = 1; +const SECURITY = 2; +const SERVERS = 3; +const STORAGE = 4; +const ELECTRICAL = 5; +const ARCHIVE = 6; +const INTERVIEW = 7; + +const ROOMS = [ + { + name: "LOBBY", + map: map` +wwwwdwwwww +wq.....j.w +w........w +d........d +w........w +wi...7..iw +wwwwdwwwww +hhhhhhhhhh` + }, + { + name: "OFFICE", + map: map` +wwwwwwwwww +wk...c..iw +w........w +wn.......d +w........w +wm.6...t.w +wwwwdwwwww +hhhhhhhhhh` + }, + { + name: "SECURITY", + map: map` +wwwwdwwwww +wuu....x.w +w........w +w........d +w........w +wi.......w +wwwwwwwwww +hhhhhhhhhh` + }, + { + name: "SERVERS", + map: map` +wwwwwwwwww +wrr..l..ew +w........w +d........d +w........w +wv.....rrw +wwwwwwwwww +hhhhhhhhhh` + }, + { + name: "STORAGE", + map: map` +wwwwwwwwww +wz...s..bw +w........w +d........w +w........w +w....y..iw +wwwwwwwwww +hhhhhhhhhh` + }, + { + name: "ELECTRICAL", + map: map` +wwwwwwwwww +w...o...nw +w........w +w........w +w........w +wi.......w +wwwwdwwwww +hhhhhhhhhh` + }, + { + name: "ARCHIVE", + map: map` +wwwwwwwwww +wkk..4..5w +w........w +d........w +w........w +wkk.a...nw +wwwwwwwwww +hhhhhhhhhh` + }, + { + name: "INTERVIEW", + map: map` +wwwwdwwwww +w........w +w........w +wi......iw +w.1..2..3w +w........w +wwwwwwwwww +hhhhhhhhhh` + } +]; + +const DOORS = [ + { room: LOBBY, x: 4, y: 0, to: OFFICE, spawn: [4, 5] }, + { room: LOBBY, x: 0, y: 3, to: SECURITY, spawn: [8, 3] }, + { room: LOBBY, x: 9, y: 3, to: SERVERS, spawn: [1, 3], needs: "alexbadge" }, + { room: LOBBY, x: 4, y: 6, to: INTERVIEW, spawn: [4, 1] }, + { room: OFFICE, x: 4, y: 6, to: LOBBY, spawn: [4, 1] }, + { room: OFFICE, x: 9, y: 3, to: ARCHIVE, spawn: [1, 3] }, + { room: SECURITY, x: 9, y: 3, to: LOBBY, spawn: [1, 3] }, + { room: SECURITY, x: 4, y: 0, to: ELECTRICAL, spawn: [4, 5] }, + { room: SERVERS, x: 0, y: 3, to: LOBBY, spawn: [8, 3] }, + { room: SERVERS, x: 9, y: 3, to: STORAGE, spawn: [1, 3] }, + { room: STORAGE, x: 0, y: 3, to: SERVERS, spawn: [8, 3] }, + { room: ELECTRICAL, x: 4, y: 6, to: SECURITY, spawn: [4, 1] }, + { room: ARCHIVE, x: 0, y: 3, to: OFFICE, spawn: [8, 3] }, + { room: INTERVIEW, x: 4, y: 0, to: LOBBY, spawn: [4, 5] } +]; + +const START_SPAWN = [4, 3]; + +const GUARD_ROOM = SERVERS; +const GUARD_PATH = [ + [3, 2], [4, 2], [5, 2], [6, 2], [6, 3], + [6, 4], [5, 4], [4, 4], [3, 4], [3, 3] +]; + +const START_MINUTES = 8 * 60; +const DEADLINE_MINUTES = 12 * 60; +const TICKS_PER_MINUTE = 3; +const CAUGHT_COST = 60; +const BLUFF_COST = 15; +const MAX_CREDIBILITY = 5; +const MAX_WRONG = 2; +const HELP_TXT = "I:look J:log K:bag"; + +const PEOPLE = { + mira: { + first: "Mira", upper: "MIRA SHAH", short: "MIRA", initials: "M.S.", + sheetName: "M. SHAH", badge: "MIRA-01", badgeColor: "WHITE", + role: "Lab technician", sprite: MIRA, inTime: "08:10" + }, + jon: { + first: "Jon", upper: "JON REED", short: "JON", initials: "J.R.", + sheetName: "J. REED", badge: "JON-04", badgeColor: "ORANGE", + role: "Sysadmin", sprite: JON, inTime: "08:02" + }, + ravi: { + first: "Ravi", upper: "RAVI DAS", short: "RAVI", initials: "R.D.", + sheetName: "R. DAS", badge: "RAVI-07", badgeColor: "YELLOW", + role: "Archivist", sprite: RAVI, inTime: "09:30" + } +}; + +const SUSPECT_BY_SPRITE = { [MIRA]: "mira", [JON]: "jon", [RAVI]: "ravi" }; +const SUSPECT_ORDER = ["mira", "jon", "ravi"]; + +const SECRETS = { + mira: { + text: ["\"I never touched", "the audit files.\""], + breaks: ["copylog"], + reveal: ["\"Okay! I copied", "the report for the", "board. That's all.\""] + }, + jon: { + text: ["\"I only run work", "jobs on servers.\""], + breaks: ["minerlog"], + reveal: ["\"Fine, the miner", "is mine. It's just", "side cash.\""] + }, + ravi: { + text: ["\"I never borrowed", "Alex's camera.\""], + breaks: ["notebook", "camerabag"], + reveal: ["\"Okay, I borrowed", "it for archive", "photos.\""] + } +}; + +const PASSWORD_RULES = [ + { line1: "sorted by FIRST", line2: "name, A to Z.", answer: "RSD" }, + { line1: "sorted by SURNAME,", line2: "A to Z.", answer: "DRS" }, + { line1: "sorted by FIRST", line2: "name, Z to A.", answer: "DSR" } +]; + +const ITEMS = { + flashlight: "Flashlight", + smallkey: "Small brass key", + alexbadge: "Alex's badge", + screwdriver: "Screwdriver" +}; + +const INTERACTIVE = [ + DOOR, PC, NOTEBOOK, EMAIL, PHOTO, SHELF, PLANT, RACK, LOG, PHONE, VENT, + KEYCARD, SAFE, CABINET, COATRACK, SIGNBOOK, MONITOR, LOCKER, BREAKER, + TOOLBOX, BOX, COPIER, CAMBAG, DRAWER, TRASH, MIRA, JON, RAVI, GUARD +]; + +let theCase = null; + +function buildCase() { + const culpritId = pickOne(SUSPECT_ORDER); + const innocents = shuffled(SUSPECT_ORDER.filter(id => id !== culpritId)); + const leaverId = innocents[0]; + const witnessId = innocents[1]; + + const c = PEOPLE[culpritId]; + const lv = PEOPLE[leaverId]; + const wt = PEOPLE[witnessId]; + + const badgeMin = randInt(14, 19); + const loginMin = badgeMin + 2; + const deleteMin = loginMin + randInt(3, 6); + const exportMin = deleteMin + 1; + const badgeTime = "21:" + pad2(badgeMin); + const loginTime = "21:" + pad2(loginMin); + const deleteTime = "21:" + pad2(deleteMin); + const exportTime = "21:" + pad2(exportMin); + const rule = pickOne(PASSWORD_RULES); + + const outTimes = {}; + outTimes[culpritId] = "----"; + outTimes[leaverId] = "21:03"; + outTimes[witnessId] = "21:40"; + + const signLines = ["Yesterday:", ""]; + for (const id of SUSPECT_ORDER) { + signLines.push(padRight(PEOPLE[id].short, 6) + "in " + PEOPLE[id].inTime); + signLines.push(" out " + outTimes[id]); + } + + const evidence = { + signin: { label: "Sign-in book", fact: c.short + " never left" }, + coat: { label: c.short + "'s coat", fact: "Left behind at 9?" }, + receipt: { label: "Vending receipt", fact: c.short + "'s card 22:47" }, + notebook: { label: "Alex's notebook", fact: "Ravi had my camera" }, + email: { label: "Email draft", fact: c.short + " 'home' at 9" }, + photo: { label: "Camera printout", fact: c.badgeColor + " badge " + loginTime }, + pwhint: { label: "Password hint", fact: "Initials in order" }, + adminacct: { label: "Security notice", fact: "Temp admin " + c.badge }, + deleted: { label: "Recovered file", fact: "Signed " + c.initials }, + camerameta: { label: "Photo metadata", fact: "Taken " + loginTime + ":03" }, + cctvlobby: { label: "Lobby camera", fact: c.short + " never exits" }, + cctvserver: { label: "Server door camera", fact: c.badgeColor + " badge " + badgeTime }, + cctvback: { label: "Loading dock cam", fact: c.short + " exits 23:52" }, + archivesheet: { label: "Archive sheet", fact: wt.short + " 21:12-21:40" }, + copylog: { label: "Copier log", fact: "Mira copied report" }, + camerabag: { label: "Camera bag", fact: "Ravi's name tag" }, + minerlog: { label: "Rack screen", fact: "Jon's crypto miner" }, + serverlog: { label: "Server log", fact: "Deleted at " + deleteTime }, + voicemail: { label: wt.short + "'s voicemail", fact: c.short + ": forget it" }, + memcard: { label: "Vent memory card", fact: "Export by " + c.badge }, + keycard: { label: "Keycard slip", fact: c.badge + " in " + badgeTime }, + backup: { label: "Backup drive", fact: c.badge + " deleted" }, + statement: { label: "Alex's statement", fact: c.short + " had a motive" } + }; + + const clues = { + [SIGNBOOK]: { title: "SIGN-IN BOOK", evidence: "signin", lines: signLines }, + [COATRACK]: { + title: "COAT RACK", + evidence: "coat", + lines: [ + "A grey jacket with", + "a " + c.badgeColor + " lanyard.", + "Name tag: " + c.upper, + "", + c.first + " claims to", + "have gone home at", + "9:00 PM. In October.", + "Without a coat?" + ] + }, + [TRASH]: { + title: "LOBBY BIN", + evidence: "receipt", + lines: [ + "Coffee cups and a", + "vending receipt:", + "", + "CHIPS + COLA", + "22:47 yesterday", + "Card: " + c.upper + ] + }, + [PLANT]: { + title: "POTTED PLANT", + evidence: null, + lines: ["Dry soil.", "Nobody watered it.", "", "Not a clue. Just", "a sad plant."] + }, + [NOTEBOOK]: { + title: "ALEX'S NOTEBOOK", + evidence: "notebook", + lines: [ + "Staff on late shift:", + "- Mira Shah, lab", + "- Jon Reed, sysadmin", + "- Ravi Das, archive", + "", + c.first + " kept asking", + "about my audit.", + "Ravi borrowed my", + "camera again." + ] + }, + [EMAIL]: { + title: "EMAIL DRAFT", + evidence: "email", + lines: [ + "To: " + lv.upper, + "Re: audit file", + "", + c.first + " said they'd", + "go home at 9:00.", + "They NEVER leave", + "before the backup.", + "Something is wrong." + ] + }, + [PHOTO]: { + title: "CAMERA PRINTOUT", + evidence: "photo", + lines: [ + "Server room door,", + "seen from the", + "archive window.", + "", + "Clock: " + loginTime, + "A hooded figure", + "wears a staff badge.", + "Badge color: " + c.badgeColor + ] + }, + [SHELF]: { + title: "BOOKSHELF", + evidence: "pwhint", + lines: [ + "A sticky note on a", + "book reads:", + "", + "PC PASSWORD =", + "surname initials", + "of my 3 coworkers,", + rule.line1, + rule.line2 + ] + }, + [COPIER]: { + title: "COPIER LOG", + evidence: "copylog", + lines: [ + "LAST JOBS:", + "", + "20:40 M.SHAH", + "AUDIT_REPORT.pdf", + "48 pages, 1 copy", + "", + "20:55 R.DAS", + "Box labels, 2 pages" + ] + }, + [CAMBAG]: { + title: "CAMERA BAG", + evidence: "camerabag", + lines: [ + "Tag: PROPERTY OF", + "ALEX MORGAN", + "", + "A second tag, in", + "different ink:", + "\"Borrowed - R.Das\"", + "", + "The SD card is gone." + ] + }, + [RACK]: { + title: "SERVER RACK", + evidence: "minerlog", + lines: [ + "Fans roar. A tiny", + "screen blinks:", + "", + "MINER.EXE running", + "Owner: JON-04", + "Uptime: 93 days", + "", + "BACKUP MISSING.", + "Copy moved to the", + "storage safe." + ] + }, + [LOG]: { + title: "SERVER ACCESS LOG", + evidence: "serverlog", + lines: [ + "20:58 ALEX logout", + "21:03 " + padRight(lv.short, 6) + "logout", + "21:12 " + padRight(wt.short, 6) + "archive", + loginTime + " ADMIN login", + deleteTime + " FILE DELETED", + exportTime + " LOG EXPORT", + "", + "No logout for " + c.short + ".", + "Admin login used a", + "temp account." + ] + }, + [PHONE]: { + title: wt.short + "'S VOICEMAIL", + evidence: "voicemail", + lines: [ + "\"Alex, it's " + wt.first + ".", + "I saw " + lv.first + " leave", + "at 9:03. At 9:" + pad2(loginMin), + "I saw someone at", + "the server door.", + c.first + " told me to", + "forget it...\"" + ] + }, + [KEYCARD]: { + title: "DROPPED KEYCARD", + evidence: "keycard", + lines: [ + "Badge: " + c.badge, + "(" + c.badgeColor + " staff tag)", + "", + "Clipped reader slip:", + "SERVER DOOR", + "IN " + badgeTime, + "OUT none" + ] + } + }; + + const roomClues = { + [ELECTRICAL]: { + [NOTEBOOK]: { + title: "ELECTRICIAN NOTE", + evidence: null, + lines: [ + "Breaker panel tips:", + "", + "Flipping a switch", + "also flips the ones", + "directly above,", + "below, left and", + "right of it.", + "", + "Get all 9 to ON." + ] + } + }, + [ARCHIVE]: { + [SHELF]: { + title: "DUSTY SHELVES", + evidence: null, + lines: ["Boxes of reports", "from 2009.", "", "Nothing about", "last night."] + }, + [NOTEBOOK]: { + title: "ARCHIVE SHEET", + evidence: "archivesheet", + lines: [ + "ARCHIVE VISITORS", + "", + padRight(wt.sheetName, 8) + "in 21:12", + padRight(wt.sheetName, 8) + "out 21:40", + "", + "Purpose: sorting", + "old record boxes." + ] + } + } + }; + + const containers = { + [LOCKER]: { + title: "STAFF LOCKER", + needs: null, + gives: "flashlight", + evidence: null, + lines: ["Spare uniform,", "an umbrella and", "a heavy flashlight.", "", "You take the", "flashlight."], + emptyLines: ["Spare uniform and", "an umbrella.", "Nothing else."] + }, + [BOX]: { + title: "RECORD BOX", + needs: null, + gives: "smallkey", + evidence: null, + lines: ["Under old folders", "is a small brass", "key labelled", "\"ALEX - DESK\".", "", "You take it."], + emptyLines: ["Old folders.", "Nothing else."] + }, + [DRAWER]: { + title: "ALEX'S DESK", + needs: "smallkey", + lockedLines: ["The drawer has a", "small brass lock.", "", "The key must be", "somewhere nearby."], + gives: "alexbadge", + evidence: null, + lines: ["The key turns.", "Inside: Alex's", "staff badge, with", "SERVER ROOM access.", "", "You take it."], + emptyLines: ["Pens, gum and a", "stapler."] + }, + [TOOLBOX]: { + title: "TOOLBOX", + needs: null, + gives: "screwdriver", + evidence: null, + lines: ["Tape, wire, and a", "screwdriver.", "", "You take the", "screwdriver."], + emptyLines: ["Tape and wire."] + }, + [VENT]: { + title: "LOOSE VENT", + needs: "screwdriver", + lockedLines: ["A vent cover held", "by four screws.", "Something is taped", "behind it.", "", "You need a tool."], + gives: null, + evidence: "memcard", + lines: [ + "Taped inside is a", + "memory card.", + "", + "LOG EXPORT " + exportTime, + "Exported by:", + c.badge + " (temp)", + "", + "Someone hid this." + ] + } + }; + + const terminalFiles = [ + { + name: "readme.txt", + evidence: null, + lines: ["Welcome, alex.", "Last login 20:58.", "", "Use A/D to browse", "the files."] + }, + { + name: "mail.log", + evidence: null, + lines: [ + "20:51 From: " + c.short, + "\"Heading home now.\"", + "", + deleteTime + " From: ADMIN", + "\"File removed.\"", + "\"No backups left.\"" + ] + }, + { + name: "security.txt", + evidence: "adminacct", + lines: [ + "NOTICE:", + "Temp admin account", + "TEMP-09 was made", + "for badge " + c.badge + ".", + "It was never", + "disabled." + ] + }, + { + name: "deleted.tmp", + evidence: "deleted", + lines: [ + "RECOVERED FRAGMENT", + "\"If anyone asks,", + "I was home by 9.", + "The badge proves", + "nothing.\"", + "- " + c.initials + ] + }, + { + name: "camera.meta", + evidence: "camerameta", + lines: [ + "IMAGE: srv_21" + pad2(loginMin) + ".jpg", + "TIME: " + loginTime + ":03", + "DEVICE: ALEX-CAM", + "Unedited original." + ] + }, + { + name: "audit.txt", + evidence: null, + lines: [ + "Audit board: 12:00", + "", + "If my file goes", + "missing, the copy", + "is in the storage", + "safe. - Alex" + ] + } + ]; + + const cctvFeeds = [ + { + name: "CAM 1: LOBBY", + evidence: "cctvlobby", + lines: [ + "21:03 " + lv.first + " walks", + "out the front.", + "21:40 " + wt.first + " walks", + "out the front.", + "", + c.first + " never passes", + "through the lobby." + ] + }, + { + name: "CAM 2: SERVER DOOR", + evidence: "cctvserver", + lines: [ + badgeTime + " A hooded", + "figure swipes a", + c.badgeColor + " badge.", + "", + "21:" + pad2(deleteMin + 2) + " They leave", + "holding a drive." + ] + }, + { + name: "CAM 3: LOADING DOCK", + evidence: "cctvback", + lines: [ + "23:52 " + c.upper, + "slips out through", + "the loading dock.", + "", + "No coat. They look", + "straight at the", + "camera." + ] + } + ]; + + const locks = { + pc: { + title: "ALEX-PC LOGIN", + hint: ["Enter password.", "3 letters.", "", "Alex hid a hint", "in this office."], + symbols: "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + answer: rule.answer + }, + safe: { + title: "EVIDENCE SAFE", + hint: ["Engraved on door:", "Set the time the", "file was deleted.", "Format HHMM (24h)."], + symbols: "0123456789", + answer: "21" + pad2(deleteMin) + }, + cabinet: { + title: "FILING CABINET", + hint: ["Keypad label:", "Time the intruder's", "badge opened the", "server door.", "Format HHMM (24h)."], + symbols: "0123456789", + answer: "21" + pad2(badgeMin) + } + }; + + function claimsFor(id) { + const isCulprit = id === culpritId; + const list = []; + if (isCulprit) { + list.push({ + text: ["\"I went home at", "9:00 PM. Ask", "anyone.\""], + breaks: ["signin", "coat", "receipt", "cctvback", "cctvlobby"], + supports: [], + reveal: ["\"Fine. I stayed", "late to finish", "some work.\""], + required: true + }); + } else if (id === leaverId) { + list.push({ + text: ["\"I left at 9:03 PM.", "Check the log.\""], + breaks: [], + supports: ["serverlog", "cctvlobby", "signin", "voicemail"], + reveal: [], + required: false + }); + } else { + list.push({ + text: ["\"I was in the", "archive from 9:12", "to 9:40.\""], + breaks: [], + supports: ["archivesheet", "cctvlobby", "signin", "serverlog"], + reveal: [], + required: false + }); + } + list.push({ + text: ["\"I never went near", "the server room.\""], + breaks: isCulprit ? ["keycard", "cctvserver", "photo"] : [], + supports: isCulprit ? [] : ["keycard", "cctvserver", "photo"], + reveal: ["\"I checked a rack.", "Routine stuff.\""], + required: isCulprit + }); + list.push({ + text: ["\"Temp admin? I", "never made one.\""], + breaks: isCulprit ? ["adminacct", "memcard"] : [], + supports: isCulprit ? [] : ["adminacct", "memcard"], + reveal: ["\"It was for tests!", "I forgot to turn", "it off.\""], + required: isCulprit + }); + list.push({ + text: ["\"I'd never delete", "Alex's audit file.\""], + breaks: isCulprit ? ["backup", "deleted"] : [], + supports: isCulprit ? [] : ["backup", "deleted"], + reveal: ["A long silence.", "Their hands shake."], + required: isCulprit + }); + list.push({ + text: ["\"Alex and I got", "along just fine.\""], + breaks: isCulprit ? ["statement"] : [], + supports: isCulprit ? [] : ["statement"], + reveal: ["\"Alex was going", "to report me...\""], + required: isCulprit + }); + const secret = SECRETS[id]; + list.push({ + text: secret.text, + breaks: secret.breaks, + supports: [], + reveal: secret.reveal, + required: false + }); + return list; + } + + const claims = {}; + for (const id of SUSPECT_ORDER) claims[id] = claimsFor(id); + + return { + culpritId: culpritId, + leaverId: leaverId, + witnessId: witnessId, + badgeTime: badgeTime, + deleteTime: deleteTime, + evidence: evidence, + clues: clues, + roomClues: roomClues, + containers: containers, + terminalFiles: terminalFiles, + cctvFeeds: cctvFeeds, + locks: locks, + claims: claims + }; +} + +function culprit() { + return PEOPLE[theCase.culpritId]; +} + +function evCount() { + return Object.keys(theCase.evidence).length; +} + +function totalLies() { + let count = 0; + for (const id of SUSPECT_ORDER) { + for (const claim of theCase.claims[id]) if (claim.breaks.length > 0) count++; + } + return count; +} + +function neededLies() { + return theCase.claims[theCase.culpritId].filter(claim => claim.required).length; +} + +let mode = "intro"; + +let roomIndex = LOBBY; +let playerPos = { x: START_SPAWN[0], y: START_SPAWN[1] }; +let facing = { dx: 0, dy: -1 }; +let statusText = HELP_TXT; +let ticks = 0; +let steps = 0; +let found = []; +let items = []; +let pcUnlocked = false; +let safeUnlocked = false; +let cabinetUnlocked = false; +let powerOn = false; +let breakerGrid = []; +let swCursor = { x: 1, y: 1 }; +let breakerMoves = 0; +let filePage = 0; +let camPage = 0; +let journalPage = 0; +let activeLock = null; +let digits = []; +let digitIdx = 0; +let codeMsg = ""; +let talkingTo = null; +let claimIndex = 0; +let brokenClaims = {}; +let pickIdx = 0; +let interviewed = {}; +let wrongAccusations = 0; +let credibility = MAX_CREDIBILITY; +let timesCaught = 0; +let guardStep = 0; +let invScore = 0; +let doneAt = ""; + +function gotEv(id) { + return found.includes(id); +} + +function giveEv(id) { + if (!id || gotEv(id)) return false; + found.push(id); + return true; +} + +function gotItem(id) { + return items.includes(id); +} + +function giveItem(id) { + if (!id || gotItem(id)) return false; + items.push(id); + return true; +} + +function minutesNow() { + return START_MINUTES + Math.floor(ticks / TICKS_PER_MINUTE); +} + +function minutesLeft() { + return Math.max(0, DEADLINE_MINUTES - minutesNow()); +} + +function clockText() { + const total = minutesNow(); + return pad2(Math.floor(total / 60)) + ":" + pad2(total % 60); +} + +function claimKey(suspectId, index) { + return suspectId + ":" + index; +} + +function liesExposed() { + return Object.keys(brokenClaims).length; +} + +function provenVs(suspectId) { + let count = 0; + theCase.claims[suspectId].forEach((claim, i) => { + if (claim.required && brokenClaims[claimKey(suspectId, i)]) count++; + }); + return count; +} + +function spendTime(amount) { + ticks += amount; + if (minutesNow() >= DEADLINE_MINUTES) { + showLose("time"); + return true; + } + return false; +} + +function openScreen(nextMode) { + if (mode === "world") { + const player = getFirst(PLAYER); + if (player) playerPos = { x: player.x, y: player.y }; + } + mode = nextMode; + setMap(SCREEN_MAP); + clearText(); +} + +function showScreen(title, lines, footer, nextMode) { + openScreen(nextMode || "screen"); + addText(title, { x: 0, y: 0, color: YELLOW }); + lines.forEach((line, i) => { + if (line) addText(line, { x: 0, y: 2 + i, color: WHITE }); + }); + if (footer) addText(footer, { x: 0, y: 15, color: GREY }); +} + +function drawHud() { + clearText(); + const room = ROOMS[roomIndex]; + addText((roomIndex + 1) + "/" + ROOMS.length + " " + room.name, { x: 0, y: 14, color: YELLOW }); + const clockColor = minutesLeft() <= 30 ? RED : GREEN; + addText(clockText(), { x: 15, y: 14, color: clockColor }); + addText(statusText, { x: 0, y: 15, color: WHITE }); +} + +function setStatus(text) { + statusText = text; + drawHud(); +} + +function isDark() { + return roomIndex === ARCHIVE && !gotItem("flashlight"); +} + +function applyDarkness() { + if (!isDark()) return; + for (let y = 1; y < height() - 2; y++) { + for (let x = 1; x < width() - 1; x++) addSprite(x, y, DARK); + } +} + +function placeGuard() { + if (roomIndex !== GUARD_ROOM) return; + const spot = GUARD_PATH[guardStep]; + addSprite(spot[0], spot[1], GUARD); +} + +function guardTouching() { + const guard = getFirst(GUARD); + const player = getFirst(PLAYER); + if (!guard || !player) return false; + return Math.abs(guard.x - player.x) + Math.abs(guard.y - player.y) <= 1; +} + +function advanceGuard() { + const guard = getFirst(GUARD); + if (!guard) return; + const next = (guardStep + 1) % GUARD_PATH.length; + const target = GUARD_PATH[next]; + guard.x = target[0]; + guard.y = target[1]; + if (guard.x === target[0] && guard.y === target[1]) { + guardStep = next; + } else { + + guard.x = GUARD_PATH[guardStep][0]; + guard.y = GUARD_PATH[guardStep][1]; + } +} + +function caughtByGuard() { + timesCaught++; + roomIndex = LOBBY; + if (spendTime(CAUGHT_COST)) return; + showScreen("CAUGHT!", [ + "Guard Bruno grabs", + "your shoulder.", + "", + "\"No visitors in the", + "server room during", + "an audit!\"", + "", + "He walks you back", + "to the lobby.", + "", + "-20 minutes" + ], "I: continue"); + playerPos = { x: 8, y: 3 }; + facing = { dx: -1, dy: 0 }; +} + +function enterWorld(status) { + mode = "world"; + setMap(ROOMS[roomIndex].map); + addSprite(playerPos.x, playerPos.y, PLAYER); + placeGuard(); + applyDarkness(); + if (status) statusText = status; + else if (isDark()) statusText = "Pitch black in here"; + else statusText = HELP_TXT; + drawHud(); +} + +function enterRoom(index, spawn) { + roomIndex = index; + playerPos = { x: spawn[0], y: spawn[1] }; + let status = "Entered " + ROOMS[index].name; + if (index === GUARD_ROOM) status = "Avoid the guard!"; + if (index === ARCHIVE && isDark()) status = "Too dark to search"; + if (index === INTERVIEW) status = "Face a suspect + I"; + enterWorld(status); +} + +function movePlayer(dx, dy) { + const player = getFirst(PLAYER); + if (!player) return; + facing = { dx: dx, dy: dy }; + const oldX = player.x; + const oldY = player.y; + + if (dx !== 0) player.x += dx; + if (dy !== 0) player.y += dy; + if (player.x !== oldX || player.y !== oldY) { + steps++; + statusText = isDark() ? "Pitch black in here" : HELP_TXT; + } else { + statusText = "You wait a moment."; + } + if (spendTime(1)) return; + + if (roomIndex === GUARD_ROOM) { + if (guardTouching()) { + caughtByGuard(); + return; + } + advanceGuard(); + if (guardTouching()) { + caughtByGuard(); + return; + } + } + drawHud(); +} + +function inspect() { + const player = getFirst(PLAYER); + if (!player) return; + const directions = [ + [facing.dx, facing.dy], + [0, -1], + [0, 1], + [-1, 0], + [1, 0] + ]; + for (const dir of directions) { + const x = player.x + dir[0]; + const y = player.y + dir[1]; + if (x < 0 || y < 0 || x >= width() || y >= height()) continue; + const target = getTile(x, y).find(sprite => INTERACTIVE.includes(sprite.type)); + if (target) { + facing = { dx: dir[0], dy: dir[1] }; + interactWith(target.type, x, y); + return; + } + } + setStatus(isDark() ? "Too dark to see." : "Nothing to inspect."); +} + +function clueFor(type) { + const roomSet = theCase.roomClues[roomIndex]; + if (roomSet && roomSet[type]) return roomSet[type]; + return theCase.clues[type] || null; +} + +function interactWith(type, x, y) { + if (type === DOOR) { + tryDoor(x, y); + return; + } + if (isDark()) { + setStatus("Too dark. Need light"); + return; + } + if (type === GUARD) { + caughtByGuard(); + } else if (type === PC) { + if (pcUnlocked) showFile(filePage); + else openCode("pc"); + } else if (type === SAFE) { + if (safeUnlocked) openSafe(); + else openCode("safe"); + } else if (type === CABINET) { + if (cabinetUnlocked) openCabinet(); + else openCode("cabinet"); + } else if (type === MONITOR) { + openCctv(); + } else if (type === BREAKER) { + openBreaker(); + } else if (SUSPECT_BY_SPRITE[type]) { + openInterview(SUSPECT_BY_SPRITE[type], 0, ""); + } else if (theCase.containers[type]) { + openContainer(theCase.containers[type]); + } else if (clueFor(type)) { + const clue = clueFor(type); + showClue(clue.title, clue.lines, clue.evidence); + } else { + setStatus("Nothing to inspect."); + } +} + +function showClue(title, lines, evidenceId) { + const isNew = giveEv(evidenceId); + showScreen(title, lines, "I: back"); + if (isNew) addText("+ NEW EVIDENCE", { x: 0, y: 13, color: GREEN }); +} + +function openContainer(box) { + if (box.needs && !gotItem(box.needs)) { + showScreen(box.title, box.lockedLines, "I: back"); + return; + } + if (box.gives) { + if (giveItem(box.gives)) { + showScreen(box.title, box.lines, "I: back"); + addText("+ NEW TOOL", { x: 0, y: 13, color: CYAN }); + } else { + showScreen(box.title, box.emptyLines, "I: back"); + } + return; + } + showClue(box.title, box.lines, box.evidence); +} + +function tryDoor(x, y) { + const door = DOORS.find(d => d.room === roomIndex && d.x === x && d.y === y); + if (!door) { + setStatus("It won't budge."); + return; + } + if (door.needs && !gotItem(door.needs)) { + showScreen("SERVER ROOM", [ + "A badge reader", + "blinks red.", + "", + "Only staff with", + "server access can", + "enter. Alex must", + "have had a badge." + ], "I: back"); + return; + } + enterRoom(door.to, door.spawn); +} + +function showHint() { + + let hint = "Accuse the liar!"; + if (!gotItem("flashlight")) hint = "Check security"; + else if (!powerOn) hint = "Power is out"; + else if (!pcUnlocked) hint = "Alex's PC is locked"; + else if (!gotItem("smallkey")) hint = "Search the archive"; + else if (!gotItem("alexbadge")) hint = "Alex's desk drawer"; + else if (!gotEv("serverlog")) hint = "Server room log"; + else if (!gotItem("screwdriver")) hint = "Storage has tools"; + else if (!safeUnlocked || !cabinetUnlocked) hint = "Storage locks"; + else if (!gotEv("memcard")) hint = "Server room vent"; + else if (provenVs(theCase.culpritId) < neededLies()) hint = "Break 5 lies"; + setStatus(hint); +} + +function openCode(lockId) { + activeLock = lockId; + digits = []; + for (let i = 0; i < theCase.locks[lockId].answer.length; i++) digits.push(0); + digitIdx = 0; + codeMsg = ""; + drawCode(); +} + +function drawCode() { + const lock = theCase.locks[activeLock]; + openScreen("code"); + addText(lock.title, { x: 0, y: 0, color: YELLOW }); + lock.hint.forEach((line, i) => { + if (line) addText(line, { x: 0, y: 2 + i, color: WHITE }); + }); + const shown = digits.map(v => lock.symbols[v]).join(" "); + const left = Math.floor((20 - shown.length) / 2); + addText(shown, { x: left, y: 9, color: CYAN }); + addText("^", { x: left + digitIdx * 2, y: 10, color: ORANGE }); + if (codeMsg) addText(codeMsg, { x: 0, y: 12, color: RED }); + addText("W/S change A/D move", { x: 0, y: 14, color: GREY }); + addText("K enter I cancel", { x: 0, y: 15, color: GREY }); +} + +function spinDigit(step) { + const count = theCase.locks[activeLock].symbols.length; + digits[digitIdx] = (digits[digitIdx] + step + count) % count; + codeMsg = ""; + drawCode(); +} + +function moveDigit(step) { + const length = digits.length; + digitIdx = (digitIdx + step + length) % length; + drawCode(); +} + +function submitCode() { + const lock = theCase.locks[activeLock]; + const entered = digits.map(v => lock.symbols[v]).join(""); + if (entered !== lock.answer) { + codeMsg = "ACCESS DENIED"; + drawCode(); + return; + } + if (activeLock === "pc") { + pcUnlocked = true; + showFile(0); + } else if (activeLock === "safe") { + safeUnlocked = true; + openSafe(); + } else if (activeLock === "cabinet") { + cabinetUnlocked = true; + openCabinet(); + } +} + +function openSafe() { + const c = culprit(); + showClue("SAFE OPEN", [ + "A backup drive!", + "", + "History:", + theCase.deleteTime + " DELETE by", + c.badge + " temp admin", + "", + "The original file", + "is still here." + ], "backup"); +} + +function openCabinet() { + const c = culprit(); + showClue("CABINET OPEN", [ + "Alex's statement:", + "", + "\"" + c.first + " asked me to", + "delete the backup", + "before the audit.", + "I refused.\"", + "", + "- Alex Morgan" + ], "statement"); +} + +function flipSwitch(grid, x, y) { + const spots = [[x, y], [x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]; + for (const spot of spots) { + const sx = spot[0]; + const sy = spot[1]; + if (sx >= 0 && sy >= 0 && sx < 3 && sy < 3) grid[sy][sx] = !grid[sy][sx]; + } +} + +function resetBreaker() { + breakerGrid = [ + [true, true, true], + [true, true, true], + [true, true, true] + ]; + const cells = shuffled([0, 1, 2, 3, 4, 5, 6, 7, 8]).slice(0, 3); + for (const cell of cells) flipSwitch(breakerGrid, cell % 3, Math.floor(cell / 3)); + swCursor = { x: 1, y: 1 }; + breakerMoves = 0; +} + +function breakerSolved() { + return breakerGrid.every(row => row.every(on => on)); +} + +function openBreaker() { + if (powerOn) { + showScreen("BREAKER PANEL", [ + "All nine switches", + "are ON.", + "", + "The security room", + "has power again." + ], "I: back"); + return; + } + drawBreaker(); +} + +function drawBreaker() { + openScreen("breaker"); + addText("BREAKER PANEL", { x: 0, y: 0, color: YELLOW }); + addText("Turn every switch", { x: 0, y: 2, color: WHITE }); + addText("ON to restore power", { x: 0, y: 3, color: WHITE }); + for (let y = 0; y < 3; y++) { + for (let x = 0; x < 3; x++) { + const on = breakerGrid[y][x]; + const cellX = 5 + x * 4; + const cellY = 5 + y * 2; + addText(on ? "(#)" : "( )", { x: cellX, y: cellY, color: on ? GREEN : RED }); + if (swCursor.x === x && swCursor.y === y) { + addText(">", { x: cellX - 1, y: cellY, color: YELLOW }); + } + } + } + addText("Flips: " + breakerMoves, { x: 0, y: 12, color: CYAN }); + addText("WASD move K flip", { x: 0, y: 14, color: GREY }); + addText("I leave", { x: 0, y: 15, color: GREY }); +} + +function moveSwitch(dx, dy) { + swCursor.x = (swCursor.x + dx + 3) % 3; + swCursor.y = (swCursor.y + dy + 3) % 3; + drawBreaker(); +} + +function pressBreaker() { + flipSwitch(breakerGrid, swCursor.x, swCursor.y); + breakerMoves++; + if (breakerSolved()) { + powerOn = true; + showScreen("POWER RESTORED", [ + "The panel hums.", + "Lights flicker on", + "next door.", + "", + "The security", + "cameras should", + "work now." + ], "I: back"); + return; + } + drawBreaker(); +} + +function showFile(index) { + const files = theCase.terminalFiles; + const count = files.length; + filePage = (index + count) % count; + const file = files[filePage]; + const isNew = giveEv(file.evidence); + openScreen("terminal"); + addText("ALEX-PC FILE " + (filePage + 1) + "/" + count, { x: 0, y: 0, color: YELLOW }); + addText(file.name, { x: 0, y: 1, color: ORANGE }); + file.lines.forEach((line, i) => { + if (line) addText(line, { x: 0, y: 3 + i, color: GREEN }); + }); + if (isNew) addText("+ NEW EVIDENCE", { x: 0, y: 12, color: CYAN }); + addText("A/D files I close", { x: 0, y: 15, color: GREY }); +} + +function openCctv() { + if (!powerOn) { + showScreen("SECURITY MONITORS", [ + "NO SIGNAL", + "", + "The cameras lost", + "power last night.", + "Someone flipped", + "the breakers in", + "the room north", + "of here." + ], "I: back"); + return; + } + showCctvFeed(camPage); +} + +function showCctvFeed(index) { + const feeds = theCase.cctvFeeds; + const count = feeds.length; + camPage = (index + count) % count; + const feed = feeds[camPage]; + const isNew = giveEv(feed.evidence); + openScreen("cctv"); + addText("CCTV PLAYBACK " + (camPage + 1) + "/" + count, { x: 0, y: 0, color: YELLOW }); + addText(feed.name, { x: 0, y: 1, color: ORANGE }); + feed.lines.forEach((line, i) => { + if (line) addText(line, { x: 0, y: 3 + i, color: CYAN }); + }); + if (isNew) addText("+ NEW EVIDENCE", { x: 0, y: 12, color: GREEN }); + addText("A/D feeds I close", { x: 0, y: 15, color: GREY }); +} + +function showJournal(page) { + const perPage = 5; + const pages = Math.max(1, Math.ceil(found.length / perPage)); + journalPage = page % pages; + openScreen("journal"); + addText("JOURNAL " + (journalPage + 1) + "/" + pages, { x: 0, y: 0, color: YELLOW }); + if (found.length === 0) { + addText("No evidence yet.", { x: 0, y: 2, color: WHITE }); + addText("Inspect with I.", { x: 0, y: 3, color: WHITE }); + } + const start = journalPage * perPage; + found.slice(start, start + perPage).forEach((id, i) => { + addText(theCase.evidence[id].label, { x: 0, y: 2 + i * 2, color: WHITE }); + addText(theCase.evidence[id].fact, { x: 1, y: 3 + i * 2, color: CYAN }); + }); + addText("Found " + found.length + "/" + evCount(), { x: 0, y: 13, color: GREEN }); + addText("J next I close", { x: 0, y: 15, color: GREY }); +} + +function showBag() { + openScreen("bag"); + addText("CASE BAG", { x: 0, y: 0, color: YELLOW }); + addText("Tools:", { x: 0, y: 2, color: ORANGE }); + if (items.length === 0) { + addText("(none)", { x: 1, y: 3, color: GREY }); + } + items.forEach((id, i) => { + addText(ITEMS[id], { x: 1, y: 3 + i, color: WHITE }); + }); + addText("Evidence " + pad2(found.length) + "/" + evCount(), { x: 0, y: 8, color: GREEN }); + addText("Lies " + liesExposed() + "/" + totalLies(), { x: 0, y: 9, color: GREEN }); + addText("Credibility " + credibility + "/" + MAX_CREDIBILITY, { x: 0, y: 10, color: CYAN }); + addText("Strikes " + wrongAccusations + "/" + MAX_WRONG, { x: 0, y: 11, color: RED }); + addText("Time left " + minutesLeft() + " min", { x: 0, y: 12, color: YELLOW }); + addText("J help", { x: 0, y: 14, color: GREY }); + addText("I close", { x: 0, y: 15, color: GREY }); +} + +function showHelp() { + showScreen("FIELD GUIDE", [ + "WASD move, bump a", + " wall to wait", + "I inspect / back", + "J journal K bag", + "L hint", + "", + "Interview: A/D pick", + "a claim, K present", + "evidence, L accuse.", + "", + "Chase: WASD move,", + "K shoot, I pause." + ], "I: continue"); +} + +function showIntro() { + showScreen("THE LIAR'S COMPUTER", [ + "08:00. Last night", + "someone deleted", + "Alex Morgan's audit", + "file.", + "", + "Three coworkers", + "stayed late. One is", + "lying, and it's", + "never the same one.", + "", + "Board meets: 12:00" + ], null, "intro"); + addText("I: start", { x: 0, y: 14, color: GREY }); + addText("J: controls", { x: 0, y: 15, color: GREY }); +} + +function openInterview(suspectId, index, message) { + const person = PEOPLE[suspectId]; + const claims = theCase.claims[suspectId]; + const count = claims.length; + talkingTo = suspectId; + claimIndex = (index + count) % count; + interviewed[suspectId] = true; + const claim = claims[claimIndex]; + const broken = brokenClaims[claimKey(suspectId, claimIndex)]; + + openScreen("interview"); + addText("INTERVIEW", { x: 0, y: 0, color: YELLOW }); + addText("CRED " + credibility, { x: 14, y: 0, color: CYAN }); + addText(person.upper, { x: 0, y: 1, color: WHITE }); + addText(person.role, { x: 0, y: 2, color: GREY }); + addText("Badge: " + person.badgeColor, { x: 0, y: 3, color: GREY }); + addText("CLAIM " + (claimIndex + 1) + "/" + count, { x: 0, y: 5, color: ORANGE }); + if (broken) addText("LIE!", { x: 16, y: 5, color: RED }); + claim.text.forEach((line, i) => { + addText(line, { x: 0, y: 6 + i, color: CYAN }); + }); + if (broken) { + claim.reveal.slice(0, 3).forEach((line, i) => { + addText(line, { x: 0, y: 10 + i, color: GREEN }); + }); + } + if (message) addText(message, { x: 0, y: 13, color: RED }); + addText("A/D claim K present", { x: 0, y: 14, color: GREY }); + addText("L accuse I back", { x: 0, y: 15, color: GREY }); +} + +function backToInterview(message) { + openInterview(talkingTo, claimIndex, message || ""); +} + +function openPicker() { + if (found.length === 0) { + backToInterview("No evidence yet!"); + return; + } + if (pickIdx >= found.length) pickIdx = 0; + drawPicker(); +} + +function drawPicker() { + openScreen("picker"); + addText("PRESENT EVIDENCE", { x: 0, y: 0, color: YELLOW }); + addText("vs claim " + (claimIndex + 1), { x: 0, y: 1, color: GREY }); + const visible = 9; + let start = pickIdx - Math.floor(visible / 2); + if (start > found.length - visible) start = found.length - visible; + if (start < 0) start = 0; + for (let i = start; i < start + visible && i < found.length; i++) { + const row = 3 + (i - start); + const selected = i === pickIdx; + if (selected) addText(">", { x: 0, y: row, color: YELLOW }); + addText(theCase.evidence[found[i]].label, { x: 1, y: row, color: selected ? YELLOW : WHITE }); + } + addText((pickIdx + 1) + "/" + found.length, { x: 15, y: 1, color: GREY }); + addText("W/S pick K present", { x: 0, y: 14, color: GREY }); + addText("I back", { x: 0, y: 15, color: GREY }); +} + +function movePicker(step) { + pickIdx = (pickIdx + step + found.length) % found.length; + drawPicker(); +} + +function presentEvidence() { + const evidenceId = found[pickIdx]; + const person = PEOPLE[talkingTo]; + const claim = theCase.claims[talkingTo][claimIndex]; + const key = claimKey(talkingTo, claimIndex); + const label = theCase.evidence[evidenceId].label; + + if (brokenClaims[key]) { + backToInterview("Already proven!"); + return; + } + + if (claim.breaks.includes(evidenceId)) { + brokenClaims[key] = true; + showScreen("CONTRADICTION!", [ + "You show the", + label + ".", + "", + person.upper + ":" + ].concat(claim.reveal), "I: back", "reaction"); + addText("+ LIE EXPOSED", { x: 0, y: 13, color: GREEN }); + return; + } + + if (claim.supports.includes(evidenceId)) { + showScreen("THAT CHECKS OUT", [ + "The " + label, + "backs up this claim.", + "", + "No contradiction,", + "but no harm done." + ], "I: back", "reaction"); + return; + } + + credibility--; + if (credibility <= 0) { + showLose("credibility"); + return; + } + if (spendTime(BLUFF_COST)) return; + showScreen("SO WHAT?", [ + person.upper + ":", + "\"What does that", + "have to do with", + "anything?\"", + "", + "Bad bluff.", + "-1 credibility", + "-5 minutes" + ], "I: back", "reaction"); +} + +function askAccuse() { + const missing = SUSPECT_ORDER.filter(id => !interviewed[id]); + if (missing.length > 0) { + backToInterview("Talk to all 3 first"); + return; + } + showScreen("ACCUSE?", [ + PEOPLE[talkingTo].upper, + "", + "Once you accuse,", + "there's no undo.", + "", + "Strikes left: " + (MAX_WRONG - wrongAccusations) + ], null, "confirm"); + addText("L: accuse", { x: 0, y: 14, color: RED }); + addText("I: not yet", { x: 0, y: 15, color: GREY }); +} + +function resolveAccusation() { + const person = PEOPLE[talkingTo]; + if (talkingTo === theCase.culpritId) { + const proven = provenVs(talkingTo); + const needed = neededLies(); + if (proven >= needed) { + invScore = calcScore(); + doneAt = clockText(); + startChase(); + return; + } + wrongAccusations++; + if (wrongAccusations >= MAX_WRONG) { + showLose("strikes"); + return; + } + showScreen("NOT ENOUGH PROOF", [ + person.first + " smirks.", + "\"You've got", + "nothing on me.\"", + "", + "Lies proven: " + proven + "/" + needed, + "Break EVERY core", + "claim first.", + "", + "1 strike left." + ], "I: keep digging"); + return; + } + wrongAccusations++; + if (wrongAccusations >= MAX_WRONG) { + showLose("strikes"); + return; + } + showScreen("WRONG CALL", [ + person.upper, + "had a secret, but", + "not THIS secret.", + "", + "Compare every alibi", + "with the logs and", + "camera footage.", + "", + "1 strike left." + ], "I: keep digging"); +} + +const TICK_MS = 120; +const FPS = 1000 / TICK_MS; +const CONTINUES = 2; + +const hwSprites = [CRUISER, VAN, TRAFFIC, CRATE, EBULLET, PBULLET, SPIKES, AMMO, MEDKIT, BOOM]; +const dkSprites = [PLAYER, MIRA, JON, RAVI, DOCK_CRATE, CRACKED_CRATE, EBULLET, PBULLET, AMMO, MEDKIT, BOOM]; + +const HW = { + left: 1, + right: 8, + topRow: 3, + bottomRow: 6, + vanHp: 15, + playerHp: 4, + startAmmo: 20, + maxAmmo: 30, + ammoPickup: 8, + timeLimit: 95, + fireCooldown: 3, + hitInvincible: 10 +}; + +const DK = { + bossHp: 12, + playerHp: 4, + startAmmo: 12, + maxAmmo: 20, + ammoPickup: 5, + timeLimit: 75, + fireCooldown: 3, + hitInvincible: 10, + crateHp: 3, + bossTop: 1, + bossBottom: 3 +}; + +const DOCK_CRATES = [ + [1, 2], [4, 2], [7, 2], + [2, 4], [5, 4], [8, 4], + [0, 5], [9, 5] +]; + +let chaseStage = 0; +let continuesLeft = CONTINUES; +let wasMode = ""; +let chaseTick = 0; +let hw = null; +let dk = null; +let bonus = 0; + +function startChase() { + continuesLeft = CONTINUES; + bonus = 0; + const c = culprit(); + showScreen("BUSTED... ALMOST", [ + c.upper + " shoves", + "the table over and", + "bolts for the", + "loading dock!", + "", + "An armed getaway", + "van is waiting.", + "", + "Grab the cruiser.", + "Don't let them go." + ], null, "busted"); + chaseStage = 1; + addText("I: start the chase", { x: 0, y: 14, color: RED }); + addText("Continues: " + continuesLeft, { x: 0, y: 15, color: GREY }); +} + +function stageIntro(stage) { + chaseStage = stage; + if (stage === 1) { + showScreen("STAGE 1: HIGHWAY", [ + "Shoot the van (K).", + "Dodge traffic,", + "crates and bullets.", + "", + "Below 2/3 HP it", + "shoots back. Below", + "1/3: spike strips!", + "", + "Grab ammo + repair", + "kits on the road." + ], null, "chaseIntro"); + } else { + showScreen("STAGE 2: THE DOCKS", [ + "The van flips! The", + "culprit runs for a", + "boat with a pistol.", + "", + "Crates are cover", + "but they break.", + "You shoot the way", + "you last moved.", + "", + "Boat leaves in " + DK.timeLimit + "s" + ], null, "chaseIntro"); + } + addText("I: GO", { x: 0, y: 14, color: RED }); + addText("Continues: " + continuesLeft, { x: 0, y: 15, color: GREY }); +} + +function beginStage() { + chaseTick = 0; + if (chaseStage === 1) startHighway(); + else startDocks(); +} + +function clearSprites(types) { + for (const type of types) for (const sprite of getAll(type)) sprite.remove(); +} + +function pauseChase() { + wasMode = mode; + mode = "paused"; + clearText(); + addText("PAUSED", { x: 7, y: 6, color: YELLOW }); + addText("I: resume", { x: 5, y: 8, color: WHITE }); +} + +function resumeChase() { + mode = wasMode; + if (mode === "highway") renderHighway(); + else renderDocks(); +} + +function chaseLost(reason) { + const c = culprit(); + let lines; + if (reason === "wrecked") { + lines = ["Your cruiser spins", "into the barrier.", "Totaled."]; + } else if (reason === "vanEscaped") { + lines = ["The van vanishes", "into traffic."]; + } else if (reason === "shot") { + lines = ["You take a hit and", "go down behind a", "crate."]; + } else { + lines = ["The boat roars off", "with " + c.first + " on it."]; + } + if (continuesLeft <= 0) { + showLose("escaped"); + return; + } + continuesLeft--; + showScreen("STAGE FAILED", lines.concat([ + "", + "Continues left: " + continuesLeft + ]), null, "chaseLost"); + addText("I: retry stage", { x: 0, y: 15, color: RED }); +} + +function startHighway() { + mode = "highway"; + setMap(HIGHWAY_MAP); + hw = { + px: 4, + py: 6, + hp: HW.playerHp, + ammo: HW.startAmmo, + cool: 0, + inv: 0, + vanX: 5, + vanTarget: 5, + vanHp: HW.vanHp, + timeLeft: HW.timeLimit * FPS, + things: [], + booms: [], + gapLanes: [], + gapUntil: 0 + }; + renderHighway(); +} + +function highwayPhase() { + if (hw.vanHp > Math.ceil(HW.vanHp * 2 / 3)) return 1; + if (hw.vanHp > Math.ceil(HW.vanHp / 3)) return 2; + return 3; +} + +function hwThingAt(x, y, kind) { + return hw.things.find(t => t.x === x && t.y === y && (!kind || t.kind === kind)); +} + +function hwAdd(kind, x, y, every) { + hw.things.push({ kind: kind, x: x, y: y, every: every, dead: false }); +} + +function hwBoom(x, y) { + hw.booms.push({ x: x, y: y, life: 3 }); +} + +const hazards = ["traffic", "crate", "ebullet", "spike"]; + +function hwCheckPlayer() { + for (const thing of hw.things) { + if (thing.dead || thing.x !== hw.px || thing.y !== hw.py) continue; + if (thing.kind === "ammo") { + thing.dead = true; + hw.ammo = Math.min(HW.maxAmmo, hw.ammo + HW.ammoPickup); + } else if (thing.kind === "repair") { + thing.dead = true; + hw.hp = Math.min(HW.playerHp, hw.hp + 1); + } else if (hazards.includes(thing.kind)) { + if (hw.inv > 0) continue; + thing.dead = true; + hw.hp--; + hw.inv = HW.hitInvincible; + hwBoom(hw.px, hw.py); + } + } +} + +function hwCheckBullets() { + for (const bullet of hw.things) { + if (bullet.dead || bullet.kind !== "pbullet") continue; + if (bullet.y === 0 && bullet.x === hw.vanX) { + bullet.dead = true; + hw.vanHp--; + hwBoom(bullet.x, 0); + continue; + } + for (const other of hw.things) { + if (other.dead || other === bullet || other.x !== bullet.x || other.y !== bullet.y) continue; + if (other.kind === "crate" || other.kind === "ebullet") { + + other.dead = true; + bullet.dead = true; + hwBoom(other.x, other.y); + } else if (other.kind === "traffic") { + + bullet.dead = true; + } + if (bullet.dead) break; + } + } +} + +function hwFire() { + if (hw.cool > 0 || hw.ammo <= 0) return; + hw.ammo--; + hw.cool = HW.fireCooldown; + if (hw.py - 1 < 0) return; + hwAdd("pbullet", hw.px, hw.py - 1, 1); + hwCheckBullets(); + renderHighway(); +} + +function hwMove(dx, dy) { + const nx = hw.px + dx; + const ny = hw.py + dy; + if (nx < HW.left || nx > HW.right || ny < HW.topRow || ny > HW.bottomRow) return; + hw.px = nx; + hw.py = ny; + hwCheckPlayer(); + if (hw.hp <= 0) { + chaseLost("wrecked"); + return; + } + renderHighway(); +} + +function inGap(lane) { + return chaseTick < hw.gapUntil && hw.gapLanes.includes(lane); +} + +function hwFreeLane() { + for (let tries = 0; tries < 8; tries++) { + const lane = randInt(HW.left, HW.right); + if (lane === hw.vanX || inGap(lane)) continue; + if (hwThingAt(lane, 0) || hwThingAt(lane, 1)) continue; + return lane; + } + return -1; +} + +function highwayTick() { + chaseTick++; + hw.timeLeft--; + if (hw.cool > 0) hw.cool--; + if (hw.inv > 0) hw.inv--; + const phase = highwayPhase(); + + for (const thing of hw.things) { + if (thing.kind === "pbullet" && !thing.dead) { + thing.y--; + if (thing.y < 0) thing.dead = true; + } + } + hwCheckBullets(); + + for (const thing of hw.things) { + if (thing.dead || thing.kind === "pbullet") continue; + if (chaseTick % thing.every === 0) { + thing.y++; + if (thing.y > HW.bottomRow) thing.dead = true; + } + } + hwCheckBullets(); + + const moveEvery = phase === 1 ? 5 : phase === 2 ? 4 : 3; + const retargetEvery = phase === 1 ? 18 : 12; + if (chaseTick % retargetEvery === 0) { + if (phase >= 2 && Math.random() < 0.45) hw.vanTarget = hw.px; + else hw.vanTarget = randInt(HW.left, HW.right); + } + if (chaseTick % moveEvery === 0) hw.vanX += sign(hw.vanTarget - hw.vanX); + + const crateEvery = phase === 1 ? 14 : phase === 2 ? 12 : 10; + if (chaseTick % crateEvery === 0 && !hwThingAt(hw.vanX, 1) && !inGap(hw.vanX)) { + hwAdd("crate", hw.vanX, 1, 2); + } + if (phase >= 2) { + const shootEvery = phase === 2 ? 14 : 10; + if (chaseTick % shootEvery === 0) hwAdd("ebullet", hw.vanX, 1, 1); + } + if (phase === 3 && chaseTick % 40 === 0) { + const gap = randInt(HW.left, HW.right - 1); + hw.gapLanes = [gap, gap + 1]; + hw.gapUntil = chaseTick + 16; + + for (const thing of hw.things) { + if (hw.gapLanes.includes(thing.x) && thing.y <= 3 && thing.kind !== "pbullet") thing.dead = true; + } + for (let lane = HW.left; lane <= HW.right; lane++) { + if (hw.gapLanes.includes(lane)) continue; + hwAdd("spike", lane, 1, 2); + } + } + + const trafficEvery = phase === 1 ? 10 : phase === 2 ? 9 : 8; + if (chaseTick % trafficEvery === 0) { + const lane = hwFreeLane(); + if (lane >= 0) hwAdd("traffic", lane, 0, phase === 3 ? 2 : 3); + } + const ammoEvery = hw.ammo < 5 ? 15 : 30; + if (chaseTick % ammoEvery === 0 && hw.ammo < HW.maxAmmo) { + const lane = hwFreeLane(); + if (lane >= 0) hwAdd("ammo", lane, 0, 2); + } + if (chaseTick % 110 === 0 && hw.hp < HW.playerHp) { + const lane = hwFreeLane(); + if (lane >= 0) hwAdd("repair", lane, 0, 2); + } + + hwCheckPlayer(); + hw.things = hw.things.filter(t => !t.dead); + + if (chaseTick % 2 === 0) { + const from = getAll(EDGE_A).length > 0 ? EDGE_A : EDGE_B; + const to = from === EDGE_A ? EDGE_B : EDGE_A; + for (const tile of getAll(from)) tile.type = to; + } + for (const boom of hw.booms) boom.life--; + hw.booms = hw.booms.filter(b => b.life > 0); + + if (hw.vanHp <= 0) { + bonus += hw.hp * 25 + Math.floor(hw.timeLeft / FPS); + stageIntro(2); + return; + } + if (hw.hp <= 0) { + chaseLost("wrecked"); + return; + } + if (hw.timeLeft <= 0) { + chaseLost("vanEscaped"); + return; + } + renderHighway(); +} + +const hwLook = { + traffic: TRAFFIC, + crate: CRATE, + ebullet: EBULLET, + pbullet: PBULLET, + spike: SPIKES, + ammo: AMMO, + repair: MEDKIT +}; + +function renderHighway() { + clearSprites(hwSprites); + for (const thing of hw.things) { + if (thing.y >= 0 && thing.y <= HW.bottomRow) addSprite(thing.x, thing.y, hwLook[thing.kind]); + } + addSprite(hw.vanX, 0, VAN); + + if (hw.inv === 0 || chaseTick % 2 === 0) addSprite(hw.px, hw.py, CRUISER); + for (const boom of hw.booms) addSprite(boom.x, boom.y, BOOM); + + clearText(); + addText("VAN " + hw.vanHp + "/" + HW.vanHp, { x: 0, y: 14, color: RED }); + const secs = Math.ceil(hw.timeLeft / FPS); + addText("T " + secs, { x: 14, y: 14, color: secs <= 20 ? RED : YELLOW }); + addText("HP " + hw.hp + " AMMO " + hw.ammo, { x: 0, y: 15, color: WHITE }); +} + +function startDocks() { + mode = "docks"; + setMap(DOCKS_MAP); + dk = { + px: 4, + py: 6, + fdx: 0, + fdy: -1, + hp: DK.playerHp, + ammo: DK.startAmmo, + cool: 0, + inv: 0, + bx: 4, + by: 1, + bossHp: DK.bossHp, + timeLeft: DK.timeLimit * FPS, + crates: DOCK_CRATES.map(pos => ({ x: pos[0], y: pos[1], hp: DK.crateHp })), + bullets: [], + pickups: [], + booms: [] + }; + renderDocks(); +} + +function dkCrateAt(x, y) { + return dk.crates.find(crate => crate.x === x && crate.y === y && crate.hp > 0); +} + +function dkInside(x, y) { + return x >= 0 && x <= 9 && y >= 1 && y <= 6; +} + +function dkBlocked(x, y) { + return !dkInside(x, y) || !!dkCrateAt(x, y); +} + +function dkHurtPlayer() { + if (dk.inv > 0) return; + dk.hp--; + dk.inv = DK.hitInvincible; + dk.booms.push({ x: dk.px, y: dk.py, life: 3 }); +} + +function stepBullet(bullet) { + const nx = bullet.x + bullet.dx; + const ny = bullet.y + bullet.dy; + if (nx < 0 || nx > 9 || ny < 0 || ny > 6) { + bullet.dead = true; + return; + } + const crate = dkCrateAt(nx, ny); + if (crate) { + crate.hp--; + bullet.dead = true; + if (crate.hp <= 0) dk.booms.push({ x: nx, y: ny, life: 3 }); + return; + } + bullet.x = nx; + bullet.y = ny; + if (bullet.owner === "player" && nx === dk.bx && ny === dk.by) { + bullet.dead = true; + dk.bossHp--; + dk.booms.push({ x: nx, y: ny, life: 2 }); + } else if (bullet.owner === "enemy" && nx === dk.px && ny === dk.py) { + bullet.dead = true; + dkHurtPlayer(); + } +} + +function dkFire() { + if (dk.cool > 0 || dk.ammo <= 0) return; + dk.ammo--; + dk.cool = DK.fireCooldown; + const bullet = { x: dk.px, y: dk.py, dx: dk.fdx, dy: dk.fdy, owner: "player", dead: false }; + stepBullet(bullet); + if (!bullet.dead) dk.bullets.push(bullet); + dkAfterAction(); +} + +function dkMove(dx, dy) { + dk.fdx = dx; + dk.fdy = dy; + const nx = dk.px + dx; + const ny = dk.py + dy; + if (!dkBlocked(nx, ny) && !(nx === dk.bx && ny === dk.by)) { + dk.px = nx; + dk.py = ny; + + for (const bullet of dk.bullets) { + if (!bullet.dead && bullet.owner === "enemy" && bullet.x === nx && bullet.y === ny) { + bullet.dead = true; + dkHurtPlayer(); + } + } + grabPickups(); + } + dkAfterAction(); +} + +function grabPickups() { + for (const pickup of dk.pickups) { + if (pickup.dead || pickup.x !== dk.px || pickup.y !== dk.py) continue; + pickup.dead = true; + if (pickup.kind === "ammo") dk.ammo = Math.min(DK.maxAmmo, dk.ammo + DK.ammoPickup); + else dk.hp = Math.min(DK.playerHp, dk.hp + 1); + } + dk.pickups = dk.pickups.filter(p => !p.dead); +} + +function dkCheckEnd() { + if (dk.bossHp <= 0) { + bonus += dk.hp * 25 + Math.floor(dk.timeLeft / FPS); + showWin(); + return true; + } + if (dk.hp <= 0) { + chaseLost("shot"); + return true; + } + if (dk.timeLeft <= 0) { + chaseLost("boat"); + return true; + } + return false; +} + +function dkAfterAction() { + dk.bullets = dk.bullets.filter(b => !b.dead); + if (dkCheckEnd()) return; + renderDocks(); +} + +function dkFreeTile(minRow, maxRow) { + for (let tries = 0; tries < 12; tries++) { + const x = randInt(0, 9); + const y = randInt(minRow, maxRow); + if (dkCrateAt(x, y)) continue; + if (x === dk.px && y === dk.py) continue; + if (x === dk.bx && y === dk.by) continue; + if (dk.pickups.some(p => p.x === x && p.y === y)) continue; + return { x: x, y: y }; + } + return null; +} + +function rotate45(dx, dy, turn) { + const dirs = [[0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1]]; + let index = dirs.findIndex(d => d[0] === dx && d[1] === dy); + if (index < 0) return [dx, dy]; + index = (index + turn + 8) % 8; + return dirs[index]; +} + +function dkBossShoot(dx, dy) { + if (dx === 0 && dy === 0) return; + const bullet = { x: dk.bx, y: dk.by, dx: dx, dy: dy, owner: "enemy", dead: false }; + stepBullet(bullet); + if (!bullet.dead) dk.bullets.push(bullet); +} + +function docksTick() { + chaseTick++; + dk.timeLeft--; + if (dk.cool > 0) dk.cool--; + if (dk.inv > 0) dk.inv--; + const angry = dk.bossHp <= Math.floor(DK.bossHp / 2); + + for (const bullet of dk.bullets) if (!bullet.dead) stepBullet(bullet); + + const moveEvery = angry ? 4 : 6; + if (chaseTick % moveEvery === 0 && Math.random() < 0.8) { + const options = [[1, 0], [-1, 0], [0, 1], [0, -1]].filter(d => { + const nx = dk.bx + d[0]; + const ny = dk.by + d[1]; + return ny >= DK.bossTop && ny <= DK.bossBottom && !dkBlocked(nx, ny) && + !(nx === dk.px && ny === dk.py); + }); + if (options.length > 0) { + const step = pickOne(options); + dk.bx += step[0]; + dk.by += step[1]; + } + } + + const shootEvery = angry ? 9 : 12; + if (chaseTick % shootEvery === 0) { + const aimX = sign(dk.px - dk.bx); + const aimY = sign(dk.py - dk.by); + dkBossShoot(aimX, aimY); + if (angry) { + const left = rotate45(aimX, aimY, -1); + const right = rotate45(aimX, aimY, 1); + dkBossShoot(left[0], left[1]); + dkBossShoot(right[0], right[1]); + } + } + + if (chaseTick % 55 === 0 && !dk.pickups.some(p => p.kind === "ammo")) { + const spot = dkFreeTile(4, 6); + if (spot) dk.pickups.push({ x: spot.x, y: spot.y, kind: "ammo", dead: false }); + } + if (chaseTick % 200 === 0 && dk.hp < DK.playerHp && !dk.pickups.some(p => p.kind === "medkit")) { + const spot = dkFreeTile(5, 6); + if (spot) dk.pickups.push({ x: spot.x, y: spot.y, kind: "medkit", dead: false }); + } + + for (const boom of dk.booms) boom.life--; + dk.booms = dk.booms.filter(b => b.life > 0); + dk.bullets = dk.bullets.filter(b => !b.dead); + if (dkCheckEnd()) return; + renderDocks(); +} + +function renderDocks() { + clearSprites(dkSprites); + for (const crate of dk.crates) { + if (crate.hp <= 0) continue; + addSprite(crate.x, crate.y, crate.hp === 1 ? CRACKED_CRATE : DOCK_CRATE); + } + for (const pickup of dk.pickups) { + addSprite(pickup.x, pickup.y, pickup.kind === "ammo" ? AMMO : MEDKIT); + } + for (const bullet of dk.bullets) { + addSprite(bullet.x, bullet.y, bullet.owner === "player" ? PBULLET : EBULLET); + } + addSprite(dk.bx, dk.by, culprit().sprite); + if (dk.inv === 0 || chaseTick % 2 === 0) addSprite(dk.px, dk.py, PLAYER); + for (const boom of dk.booms) addSprite(boom.x, boom.y, BOOM); + + clearText(); + addText(culprit().short + " " + dk.bossHp + "/" + DK.bossHp, { x: 0, y: 14, color: RED }); + const secs = Math.ceil(dk.timeLeft / FPS); + addText("BOAT " + secs, { x: 12, y: 14, color: secs <= 15 ? RED : YELLOW }); + addText("HP " + dk.hp + " AMMO " + dk.ammo, { x: 0, y: 15, color: WHITE }); +} + +function gameTick() { + if (mode === "highway") highwayTick(); + else if (mode === "docks") docksTick(); +} + +function calcScore() { + return found.length * 10 + + liesExposed() * 15 + + credibility * 20 + + minutesLeft() - + wrongAccusations * 50 - + timesCaught * 15; +} + +function rankFor(score) { + if (score >= 900) return "LEGEND"; + if (score >= 780) return "MASTER SLEUTH"; + if (score >= 650) return "DETECTIVE"; + if (score >= 500) return "INSPECTOR"; + return "ROOKIE"; +} + +function showWin() { + const c = culprit(); + const usedContinues = CONTINUES - continuesLeft; + const score = Math.max(0, invScore + bonus + continuesLeft * 40); + showScreen("CASE CLOSED", [ + c.upper + " is in", + "handcuffs.", + theCase.badgeTime + " badge in", + theCase.deleteTime + " file deleted", + "", + "Evidence " + found.length + "/" + evCount(), + "Lies " + liesExposed() + "/" + totalLies(), + "Solved at " + doneAt, + "Continues " + usedContinues + " used", + "Score " + score, + "Rank: " + rankFor(score) + ], null, "ending"); + addText("J: play again", { x: 0, y: 15, color: GREEN }); +} + +function showLose(reason) { + let lines; + if (reason === "time") { + lines = [ + "It's 12:00.", + "The board meets", + "without your proof.", + "", + "The case is closed", + "as \"system error\"." + ]; + } else if (reason === "credibility") { + lines = [ + "Too many bluffs.", + "", + "The suspects call", + "their lawyers and", + "refuse to talk." + ]; + } else if (reason === "escaped") { + lines = [ + "You solved it, but", + culprit().upper + " got", + "away. Out of", + "continues.", + "", + "Somewhere, a boat", + "is heading south." + ]; + } else { + lines = [ + "Two wrong calls.", + "", + "You're taken off", + "the case. The liar", + "walks free." + ]; + } + lines.push(""); + lines.push("Evidence " + found.length + "/" + evCount()); + showScreen("CASE LOST", lines, null, "ending"); + addText("J: try again", { x: 0, y: 15, color: RED }); +} + +function restartGame() { + theCase = buildCase(); + roomIndex = LOBBY; + playerPos = { x: START_SPAWN[0], y: START_SPAWN[1] }; + facing = { dx: 0, dy: -1 }; + statusText = HELP_TXT; + ticks = 0; + steps = 0; + found = []; + items = []; + pcUnlocked = false; + safeUnlocked = false; + cabinetUnlocked = false; + powerOn = false; + resetBreaker(); + filePage = 0; + camPage = 0; + journalPage = 0; + activeLock = null; + digits = []; + digitIdx = 0; + codeMsg = ""; + talkingTo = null; + claimIndex = 0; + brokenClaims = {}; + pickIdx = 0; + interviewed = {}; + wrongAccusations = 0; + credibility = MAX_CREDIBILITY; + timesCaught = 0; + guardStep = 0; + invScore = 0; + doneAt = ""; + chaseStage = 0; + continuesLeft = CONTINUES; + wasMode = ""; + chaseTick = 0; + hw = null; + dk = null; + bonus = 0; + showIntro(); +} + +function handleDirection(dx, dy) { + if (mode === "world") { + movePlayer(dx, dy); + } else if (mode === "highway") { + hwMove(dx, dy); + } else if (mode === "docks") { + dkMove(dx, dy); + } else if (mode === "code") { + if (dy !== 0) spinDigit(-dy); + else moveDigit(dx); + } else if (mode === "breaker") { + moveSwitch(dx, dy); + } else if (mode === "terminal" && dx !== 0) { + showFile(filePage + dx); + } else if (mode === "cctv" && dx !== 0) { + showCctvFeed(camPage + dx); + } else if (mode === "interview" && dx !== 0) { + openInterview(talkingTo, claimIndex + dx, ""); + } else if (mode === "picker" && dy !== 0) { + movePicker(dy); + } +} + +onInput("w", () => handleDirection(0, -1)); +onInput("s", () => handleDirection(0, 1)); +onInput("a", () => handleDirection(-1, 0)); +onInput("d", () => handleDirection(1, 0)); + +onInput("i", () => { + if (mode === "intro") enterWorld("Case opened. 4 hrs"); + else if (mode === "world") inspect(); + else if (mode === "code") enterWorld("Still locked."); + else if (mode === "picker" || mode === "reaction" || mode === "confirm") backToInterview(""); + else if (mode === "busted") stageIntro(1); + else if (mode === "chaseIntro" || mode === "chaseLost") beginStage(); + else if (mode === "highway" || mode === "docks") pauseChase(); + else if (mode === "paused") resumeChase(); + else if (mode === "screen" || mode === "terminal" || mode === "cctv" || + mode === "journal" || mode === "bag" || mode === "breaker" || + mode === "interview") enterWorld(); + +}); + +onInput("j", () => { + if (mode === "intro" || mode === "bag") showHelp(); + else if (mode === "world") showJournal(0); + else if (mode === "journal") showJournal(journalPage + 1); + else if (mode === "ending") restartGame(); +}); + +onInput("k", () => { + if (mode === "world") showBag(); + else if (mode === "highway") hwFire(); + else if (mode === "docks") dkFire(); + else if (mode === "code") submitCode(); + else if (mode === "breaker") pressBreaker(); + else if (mode === "interview") openPicker(); + else if (mode === "picker") presentEvidence(); +}); + +onInput("l", () => { + if (mode === "world") showHint(); + else if (mode === "interview") askAccuse(); + else if (mode === "confirm") resolveAccusation(); +}); + +setInterval(gameTick, TICK_MS); +restartGame(); \ No newline at end of file From f66066e28105e320ec90da051913934b1ef8b0ae Mon Sep 17 00:00:00 2001 From: Badal Shah <72878759+dhyanamgit@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:04:47 +0530 Subject: [PATCH 2/2] Sprig App - The Liar --- games/The-Liar.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/games/The-Liar.js b/games/The-Liar.js index 86e98ae103..ad12462d48 100644 --- a/games/The-Liar.js +++ b/games/The-Liar.js @@ -1,3 +1,11 @@ +/* +@title: The Liar +@author: dhyanamgit +@description: This a really good detective game. +@tags: ['puzzle'] +@addedOn: 2026-10-17 +*/ + const PLAYER = "p"; const GUARD = "g"; const MIRA = "1"; @@ -1348,7 +1356,7 @@ function buildCase() { lines: [ "Server room door,", "seen from the", - "archive window.", + "archive pane.", "", "Clock: " + loginTime, "A hooded figure",