Last active
March 17, 2026 06:44
-
-
Save Thorium/8a79b24e150728231ae18c4c35f85f9e to your computer and use it in GitHub Desktop.
Legend Of The Red Dragon (L.O.R.D) ported to Zachtronics Last Call BBS.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ============================================================================ | |
| // LEGEND OF THE RED DRAGON - QuickServe Edition | |
| // A faithful recreation for Zachtronics Last Call BBS | |
| // ============================================================================ | |
| // How to install: Copy the raw file to your servers folder. | |
| // --- CONSTANTS --- | |
| var SCREEN_W = 56; | |
| var SCREEN_H = 20; | |
| // Color palette (QuickServe: 0=darkest, 17=lightest) | |
| var C = { | |
| BLACK: 0, | |
| DKBLUE: 2, | |
| DKGREEN: 3, | |
| DKCYAN: 4, | |
| DKRED: 5, | |
| DKPURPLE: 6, | |
| BROWN: 7, | |
| GREY: 8, | |
| DKGREY: 1, | |
| BLUE: 10, | |
| GREEN: 11, | |
| CYAN: 12, | |
| RED: 13, | |
| PURPLE: 14, | |
| YELLOW: 15, | |
| WHITE: 17 | |
| }; | |
| // Weapons data: [name, price, attack bonus] | |
| var WEAPONS = [ | |
| ['Stick', 200, 5], | |
| ['Dagger', 1000, 10], | |
| ['Short Sword', 3000, 20], | |
| ['Long Sword', 10000, 30], | |
| ['Huge Axe', 30000, 40], | |
| ['Bone Cruncher', 100000, 60], | |
| ['Twin Swords', 150000, 80], | |
| ['Power Axe', 200000, 120], | |
| ['Ables Sword', 400000, 180], | |
| ['Wans Weapon', 1000000, 250], | |
| ['Spear of Gold', 4000000, 350], | |
| ['Crystal Shard', 10000000, 500], | |
| ['Niras Teeth', 40000000, 800], | |
| ['Blood Sword', 100000000, 1200], | |
| ['Death Sword', 400000000, 1800] | |
| ]; | |
| // Armor data: [name, price, defense bonus] | |
| var ARMORS = [ | |
| ['Coat', 200, 1], | |
| ['Heavy Coat', 1000, 3], | |
| ['Leather Vest', 3000, 10], | |
| ['Bronze Armour', 10000, 15], | |
| ['Iron Armour', 30000, 25], | |
| ['Graphite Armour', 100000, 35], | |
| ['Erdricks Armour', 150000, 50], | |
| ['Armour of Death', 200000, 75], | |
| ['Ables Armour', 400000, 100], | |
| ['Full Body Armour', 1000000, 150], | |
| ['Blood Armour', 4000000, 225], | |
| ['Magic Protection', 10000000, 300], | |
| ['Belars Mail', 40000000, 400], | |
| ['Golden Armour', 100000000, 600], | |
| ['Armour of Lore', 400000000, 1000] | |
| ]; | |
| // Level data: [xp_required, base_hp, base_atk, base_def] | |
| var LEVELS = [ | |
| [0, 20, 5, 0], | |
| [100, 30, 10, 2], | |
| [400, 45, 17, 5], | |
| [1000, 65, 27, 10], | |
| [4000, 95, 39, 20], | |
| [10000, 145, 59, 35], | |
| [40000, 220, 94, 57], | |
| [100000, 345, 144, 92], | |
| [400000, 530, 219, 152], | |
| [1000000, 780, 329, 232], | |
| [4000000, 1130, 479, 352], | |
| [10000000, 1680, 679, 502] | |
| ]; | |
| // Master names per level | |
| var MASTERS = [ | |
| 'Halds', 'Huge', 'Sandtiger', 'Sparhawk', | |
| 'Barak', 'Aragorn', 'Pern', 'Theria', | |
| 'Skeletor', 'Garfield', 'Death', 'Turgon' | |
| ]; | |
| // Monsters by level: [name, hp, attack, defense, gold, xp] | |
| var MONSTERS = [ | |
| // Level 1 | |
| [['Small Thief', 6, 3, 0, 31, 3], ['Rude Boy', 7, 4, 0, 43, 4], ['Old Hag', 10, 4, 1, 52, 5], | |
| ['Large Mosquito', 5, 3, 0, 25, 2], ['Ugly Old Hag', 11, 5, 1, 64, 6], ['Small Bear', 12, 5, 1, 71, 7], | |
| ['Wild Boar', 14, 6, 2, 80, 8], ['Bran the Warrior', 16, 7, 2, 104, 10]], | |
| // Level 2 | |
| [['Foot Pad', 20, 10, 2, 120, 14], ['Small Troll', 22, 11, 3, 160, 18], ['Dwarven Fighter', 24, 12, 3, 200, 22], | |
| ['Dark Elf', 28, 14, 4, 270, 28], ['Evil Woodsman', 30, 15, 4, 300, 32], ['Bandit', 33, 16, 5, 380, 38], | |
| ['Huge Ugly Toad', 36, 17, 5, 420, 42], ['Young Wizard', 40, 20, 6, 530, 50]], | |
| // Level 3 | |
| [['Shadow Warrior', 60, 25, 8, 700, 80], ['Weak Goblin', 50, 22, 7, 560, 65], ['Bone', 70, 28, 9, 840, 95], | |
| ['Raging Lion', 65, 27, 8, 770, 88], ['Charging Soldier', 72, 30, 10, 900, 100], | |
| ['Black Knight', 80, 32, 11, 1050, 115], ['Winged Demon', 85, 35, 12, 1200, 130], | |
| ['Master Thief', 90, 37, 13, 1400, 150]], | |
| // Level 4 | |
| [['Apprentice Wizard', 120, 45, 18, 2000, 250], ['Dark Mage', 135, 50, 20, 2500, 300], | |
| ['Rockman', 160, 55, 22, 3100, 370], ['Wicked Witch', 140, 52, 21, 2800, 330], | |
| ['Headless Horseman', 155, 54, 21, 3000, 360], ['Manticore', 170, 58, 23, 3400, 400], | |
| ['Giant Spider', 150, 53, 20, 2900, 340], ['Evil Knight', 180, 62, 25, 3800, 450]], | |
| // Level 5 | |
| [['Huge Bear', 250, 80, 30, 6000, 800], ['Sand Warrior', 260, 82, 32, 6400, 850], | |
| ['Trojan Warrior', 280, 87, 34, 7200, 950], ['Silent Death', 300, 95, 38, 8500, 1100], | |
| ['Deadly Viper', 270, 85, 33, 6800, 900], ['Acid Dragon', 310, 98, 40, 9200, 1200], | |
| ['Fire Warrior', 290, 90, 35, 7800, 1000], ['Evil Sorcerer', 320, 100, 42, 10000, 1400]], | |
| // Level 6 | |
| [['White Knight', 420, 130, 55, 16000, 2500], ['Green Dragon', 450, 140, 58, 18000, 2800], | |
| ['Black Sorceror', 430, 135, 56, 17000, 2600], ['Belar', 500, 155, 65, 22000, 3500], | |
| ['Death Dealer', 470, 145, 60, 19500, 3000], ['Huge Stone Warrior', 480, 148, 62, 20000, 3200], | |
| ['Iron Warrior', 440, 138, 57, 17500, 2700], ['Wraith', 510, 158, 67, 23000, 3800]], | |
| // Level 7 | |
| [['Goliath', 700, 220, 90, 40000, 7000], ['Swiss Butcher', 720, 230, 95, 44000, 7500], | |
| ['Dark Ranger', 660, 210, 85, 36000, 6200], ['Fire Demon', 740, 235, 98, 46000, 8000], | |
| ['Frost Giant', 680, 215, 88, 38000, 6500], ['Black Unicorn', 690, 218, 89, 39000, 6800], | |
| ['Flying Serpent', 710, 225, 92, 42000, 7200], ['Shadow Knight', 760, 240, 100, 48000, 8500]], | |
| // Level 8 | |
| [['Troll King', 1000, 340, 150, 80000, 16000], ['King Vidion', 1200, 380, 170, 100000, 20000], | |
| ['Fire Dragon', 1050, 350, 155, 85000, 17000], ['Demon Lord', 1100, 360, 160, 90000, 18000], | |
| ['Hell Hound', 950, 330, 145, 75000, 15000], ['Storm Warrior', 1050, 355, 158, 87000, 17500], | |
| ['Death Angel', 1150, 370, 165, 95000, 19000], ['Wyvern', 980, 335, 148, 78000, 15500]], | |
| // Level 9 | |
| [['Earth Shaker', 1700, 520, 240, 180000, 50000], ['Scallion Rap', 1800, 550, 255, 200000, 55000], | |
| ['Death Reaper', 1600, 500, 230, 165000, 45000], ['Storm Giant', 1650, 510, 235, 170000, 47000], | |
| ['Arch Demon', 1750, 540, 250, 190000, 52000], ['Shadow Overlord', 1680, 515, 238, 175000, 48000], | |
| ['Blood Knight', 1720, 525, 242, 182000, 51000], ['Vampire Lord', 1780, 545, 252, 195000, 53000]], | |
| // Level 10 | |
| [['Sweet Little Girl', 2500, 780, 360, 400000, 150000], ['Corinthian Giant', 2600, 800, 375, 430000, 160000], | |
| ['Death Master', 2400, 760, 350, 380000, 140000], ['Ancient Wyrm', 2700, 820, 385, 460000, 170000], | |
| ['Shadow Dragon', 2550, 790, 365, 410000, 155000], ['Undead King', 2450, 770, 355, 390000, 145000], | |
| ['Hell Lord', 2650, 810, 380, 440000, 165000], ['Soul Devourer', 2350, 750, 345, 370000, 135000]], | |
| // Level 11 | |
| [['Mountain', 3800, 1100, 550, 900000, 500000], ['Shadowstorm Warrior', 4000, 1150, 575, 1000000, 550000], | |
| ['Death Incarnate', 3600, 1050, 525, 800000, 450000], ['Doom Knight', 3700, 1075, 538, 850000, 475000], | |
| ['Arch Lich', 3900, 1125, 562, 950000, 520000], ['Elder Dragon', 4100, 1175, 588, 1050000, 580000], | |
| ['World Ender', 3850, 1110, 555, 920000, 510000], ['Titan Lord', 3950, 1140, 570, 980000, 540000]], | |
| // Level 12 | |
| [['Corinthian Giant', 5500, 1600, 800, 2000000, 1500000], ['Mutant Widow', 5800, 1680, 840, 2200000, 1600000], | |
| ['Black Wyre', 4500, 1400, 700, 3500000, 600000], ['Dragon of Doom', 6000, 1750, 870, 2400000, 1700000], | |
| ['Chaos Lord', 5600, 1630, 815, 2100000, 1550000], ['Godlike Being', 6200, 1800, 900, 2600000, 1800000], | |
| ['Elder God', 5900, 1700, 850, 2300000, 1650000], ['Death Himself', 6500, 1900, 950, 2800000, 2000000]] | |
| ]; | |
| // Red Dragon stats | |
| var RED_DRAGON = { name: 'The Red Dragon', hp: 6000, maxHp: 6000, attack: 2000, defense: 800 }; | |
| // --- GAME STATE --- | |
| var state = 'title'; // Current screen/state | |
| var player = null; // Player data | |
| var inputBuffer = ''; // For text input | |
| var menuMessage = ''; // Temporary message to display | |
| var msgTimer = 0; // Message display timer | |
| var currentEnemy = null; | |
| var shopPage = 0; // For paginated shop lists | |
| var scrollOffset = 0; // For scrollable content | |
| var dailyLog = []; // Daily news entries | |
| var subState = ''; // Sub-state for multi-step interactions | |
| var healerReturn = 'main'; // Where to return from healer | |
| var dailyReturn = 'main'; // Where to return from daily news | |
| var animFrame = 0; // Animation counter | |
| // --- HELPER FUNCTIONS --- | |
| function numFormat(n) { | |
| if (n === undefined || n === null) return '0'; | |
| var neg = false; | |
| var val = Math.floor(n); | |
| if (val < 0) { neg = true; val = -val; } | |
| var s = val.toString(); | |
| var result = ''; | |
| var count = 0; | |
| for (var i = s.length - 1; i >= 0; i--) { | |
| if (count > 0 && count % 3 === 0) result = ',' + result; | |
| result = s[i] + result; | |
| count++; | |
| } | |
| return neg ? '-' + result : result; | |
| } | |
| function rand(min, max) { | |
| return Math.floor(Math.random() * (max - min + 1)) + min; | |
| } | |
| function clamp(val, min, max) { | |
| return Math.max(min, Math.min(max, val)); | |
| } | |
| function padRight(str, len) { | |
| str = str.toString(); | |
| while (str.length < len) str += ' '; | |
| return str.substring(0, len); | |
| } | |
| function padLeft(str, len) { | |
| str = str.toString(); | |
| while (str.length < len) str = ' ' + str; | |
| return str.substring(0, len); | |
| } | |
| function centerText(str, width) { | |
| var pad = Math.floor((width - str.length) / 2); | |
| return padRight(padLeft('', pad) + str, width); | |
| } | |
| function totalAttack() { | |
| if (!player) return 0; | |
| return player.baseAtk + WEAPONS[player.weapon][2] + player.gemAtk; | |
| } | |
| function totalDefense() { | |
| if (!player) return 0; | |
| return player.baseDef + ARMORS[player.armor][2] + player.gemDef; | |
| } | |
| function getMaxHp() { | |
| if (!player) return 20; | |
| return LEVELS[player.level - 1][1] + player.gemHp; | |
| } | |
| function getForestFights() { | |
| var base = 15; | |
| var total = base + player.children; | |
| if (player.horse) total = Math.floor(total * 1.25); | |
| return total; | |
| } | |
| function getPlayerFights() { | |
| return 3; | |
| } | |
| // --- DATA PERSISTENCE --- | |
| function saveGame() { | |
| if (!player) return; | |
| // Cap gold to prevent display overflow | |
| if (player.gold > 2000000000) player.gold = 2000000000; | |
| var data = { | |
| player: player, | |
| dailyLog: dailyLog.slice(-20), | |
| day: player.day | |
| }; | |
| saveData(JSON.stringify(data)); | |
| } | |
| function loadGame() { | |
| var raw = loadData(); | |
| if (!raw || raw === '') return null; | |
| try { | |
| var data = JSON.parse(raw); | |
| if (data && data.player) { | |
| player = data.player; | |
| dailyLog = data.dailyLog || []; | |
| checkNewDay(); | |
| return player; | |
| } | |
| } catch (e) {} | |
| return null; | |
| } | |
| function checkNewDay() { | |
| if (!player) return; | |
| var today = Math.floor(Date.now() / 86400000); | |
| if (player.day !== today) { | |
| // New day! | |
| player.day = today; | |
| player.forestFights = getForestFights(); | |
| player.playerFights = getPlayerFights(); | |
| player.flirted = false; | |
| player.heardSong = false; | |
| player.skillUses = getSkillUses(); | |
| player.alive = true; | |
| player.fairy = false; | |
| // If stayed at inn, restore HP to full | |
| if (player.stayInn) { | |
| player.hp = getMaxHp(); | |
| player.stayInn = false; | |
| } else { | |
| player.hp = Math.min(player.hp, getMaxHp()); | |
| } | |
| // Bank interest (10%) | |
| player.bank = Math.floor(player.bank * 1.1); | |
| if (player.bank > 2000000000) player.bank = 2000000000; | |
| addLog(player.name + ' entered the realm.'); | |
| saveGame(); | |
| } | |
| } | |
| function getSkillUses() { | |
| if (!player) return 0; | |
| var pts = 0; | |
| if (player.skillClass === 'M') pts = player.skillMystic; | |
| else if (player.skillClass === 'D') pts = player.skillDeath; | |
| else pts = player.skillThief; | |
| if (player.skillClass === 'M') return pts; | |
| // DK/Thief: 1 use per 3 points, minimum 1 if any points | |
| if (pts <= 0) return 0; | |
| return Math.max(1, Math.floor(pts / 3)); | |
| } | |
| function addLog(msg) { | |
| dailyLog.push(msg); | |
| if (dailyLog.length > 50) dailyLog.shift(); | |
| } | |
| // --- NEW PLAYER --- | |
| function createNewPlayer(name) { | |
| player = { | |
| name: name, | |
| sex: 'M', | |
| level: 1, | |
| exp: 0, | |
| hp: 20, | |
| baseAtk: 5, | |
| baseDef: 0, | |
| weapon: 0, | |
| armor: 0, | |
| gold: 500, | |
| bank: 0, | |
| gems: 10, | |
| gemAtk: 0, | |
| gemDef: 0, | |
| gemHp: 0, | |
| charm: 1, | |
| children: 0, | |
| fairy: false, | |
| horse: false, | |
| alive: true, | |
| forestFights: 15, | |
| playerFights: 3, | |
| flirted: false, | |
| heardSong: false, | |
| skillClass: 'D', | |
| skillMystic: 0, | |
| skillDeath: 0, | |
| skillThief: 0, | |
| skillUses: 0, | |
| kills: 0, | |
| heroDeeds: 0, | |
| married: '', | |
| stayInn: false, | |
| day: Math.floor(Date.now() / 86400000), | |
| spirits: 'high' | |
| }; | |
| player.forestFights = getForestFights(); | |
| player.skillUses = getSkillUses(); | |
| } | |
| // --- DRAWING HELPERS --- | |
| function drawTitle(title, y) { | |
| var x = Math.floor((SCREEN_W - title.length) / 2); | |
| drawText(title, C.YELLOW, x, y); | |
| } | |
| function drawMenuOption(key, text, y, x) { | |
| if (x === undefined) x = 3; | |
| drawText('(' + key + ') ', C.CYAN, x, y); | |
| drawText(text, C.WHITE, x + 4, y); | |
| } | |
| function drawBar(label, current, max, y) { | |
| drawText(padRight(label + ':', 12), C.GREY, 2, y); | |
| var barW = 25; | |
| var filled = Math.min(barW, Math.floor((current / Math.max(max, 1)) * barW)); | |
| var bar = ''; | |
| for (var i = 0; i < barW; i++) bar += (i < filled) ? '>' : '-'; | |
| drawText(bar, C.GREEN, 14, y); | |
| drawText(numFormat(current) + '/' + numFormat(max), C.WHITE, 40, y); | |
| } | |
| function showMessage(msg) { | |
| menuMessage = msg; | |
| msgTimer = 90; // ~3 seconds at 30fps | |
| } | |
| // --- SCREEN RENDERERS --- | |
| function drawTitleScreen() { | |
| clearScreen(); | |
| drawBox(C.BROWN, 0, 0, SCREEN_W, SCREEN_H); | |
| animFrame++; | |
| var flicker = (animFrame % 60 < 30) ? C.RED : C.YELLOW; | |
| drawTitle('L.O.R.D.', 2); | |
| drawText(centerText('Legend of the Red Dragon', SCREEN_W), C.RED, 0, 4); | |
| drawText(centerText('QuickServe Edition', SCREEN_W), C.DKRED, 0, 5); | |
| drawText(centerText('Originally by Seth Able', SCREEN_W), C.GREY, 0, 7); | |
| drawText(centerText('Robinson', SCREEN_W), C.GREY, 0, 8); | |
| // ASCII dragon | |
| drawText(' /\\ /\\', C.RED, 16, 10); | |
| drawText(' / \\/ \\', C.RED, 16, 11); | |
| drawText(' / /\\ /\\ \\', C.RED, 16, 12); | |
| drawText(' \\/ /\\/ \\//', C.DKRED, 16, 13); | |
| drawText(' \\ /', C.DKRED, 16, 14); | |
| drawText(' \\/', flicker, 16, 15); | |
| drawText(centerText('Press any key...', SCREEN_W), C.CYAN, 0, 17); | |
| } | |
| function drawCharCreate() { | |
| clearScreen(); | |
| drawBox(C.BROWN, 0, 0, SCREEN_W, SCREEN_H); | |
| drawTitle('Create Your Legend', 1); | |
| if (subState === 'name') { | |
| drawText(' Enter thy name, warrior:', C.WHITE, 2, 4); | |
| drawText(' > ' + inputBuffer + '_', C.YELLOW, 2, 6); | |
| drawText(' (Press Enter when done)', C.GREY, 2, 8); | |
| } else if (subState === 'sex') { | |
| drawText(' Welcome, ' + player.name + '!', C.GREEN, 2, 4); | |
| drawText(' Art thou...', C.WHITE, 2, 6); | |
| drawMenuOption('M', 'Male', 8, 4); | |
| drawMenuOption('F', 'Female', 9, 4); | |
| } else if (subState === 'class') { | |
| drawText(' Choose thy profession:', C.WHITE, 2, 4); | |
| drawMenuOption('D', 'Death Knight Skills', 6, 4); | |
| drawText(' (Powerful offensive strikes)', C.GREY, 4, 7); | |
| drawMenuOption('M', 'Mystical Skills', 9, 4); | |
| drawText(' (Healing, shielding, magic)', C.GREY, 4, 10); | |
| drawMenuOption('T', 'Thief Skills', 12, 4); | |
| drawText(' (Quick and deadly attacks)', C.GREY, 4, 13); | |
| } | |
| } | |
| function drawMainMenu() { | |
| clearScreen(); | |
| drawBox(C.BROWN, 0, 0, SCREEN_W, SCREEN_H); | |
| drawText(' Legend of the Red Dragon ', C.RED, 2, 1); | |
| drawText('Town Square', C.YELLOW, 38, 1); | |
| drawText(player.name + ' (Lv.' + player.level + ')', C.GREEN, 2, 2); | |
| drawText('HP:' + numFormat(player.hp) + '/' + numFormat(getMaxHp()), C.WHITE, 33, 2); | |
| // Draw a line separator | |
| var sep = ''; | |
| for (var i = 0; i < SCREEN_W - 2; i++) sep += '-'; | |
| drawText(sep, C.BROWN, 1, 3); | |
| drawMenuOption('F', 'Forest', 4, 2); | |
| drawMenuOption('K', 'King Arthurs Weapons', 4, 24); | |
| drawMenuOption('A', 'Abduls Armour', 5, 2); | |
| drawMenuOption('H', 'Healers Hut', 5, 24); | |
| drawMenuOption('V', 'View Stats', 6, 2); | |
| drawMenuOption('I', 'The Inn', 6, 24); | |
| drawMenuOption('T', 'Training', 7, 2); | |
| drawMenuOption('Y', 'Ye Olde Bank', 7, 24); | |
| drawMenuOption('D', 'Daily News', 8, 2); | |
| drawMenuOption('L', 'List Warriors', 8, 24); | |
| drawMenuOption('Q', 'Quit', 9, 2); | |
| // Player info bar | |
| drawText(sep, C.BROWN, 1, 10); | |
| drawText(' Gold: ' + numFormat(player.gold), C.YELLOW, 1, 11); | |
| drawText(' Bank: ' + numFormat(player.bank), C.YELLOW, 1, 12); | |
| drawText(' Gems: ' + numFormat(player.gems), C.CYAN, 28, 11); | |
| drawText(' Exp: ' + numFormat(player.exp), C.GREEN, 28, 12); | |
| drawText(' ATK: ' + numFormat(totalAttack()), C.RED, 1, 13); | |
| drawText(' DEF: ' + numFormat(totalDefense()), C.BLUE, 28, 13); | |
| drawText(' W: ' + WEAPONS[player.weapon][0], C.WHITE, 1, 14); | |
| drawText(' A: ' + ARMORS[player.armor][0], C.WHITE, 1, 15); | |
| drawText(' FF: ' + player.forestFights, C.GREEN, 1, 16); | |
| drawText(' PF: ' + player.playerFights, C.GREEN, 14, 16); | |
| if (player.fairy) drawText('Fairy', C.PURPLE, 26, 16); | |
| if (player.horse) drawText('Horse', C.BROWN, 34, 16); | |
| drawText(' Kids: ' + player.children, C.WHITE, 42, 16); | |
| if (menuMessage && msgTimer > 0) { | |
| drawText(sep, C.BROWN, 1, 17); | |
| drawText(' ' + menuMessage, C.YELLOW, 1, 18); | |
| } else { | |
| drawText(' Your choice? ', C.CYAN, 1, 18); | |
| } | |
| } | |
| function drawForest() { | |
| clearScreen(); | |
| drawBox(C.DKGREEN, 0, 0, SCREEN_W, SCREEN_H); | |
| drawTitle('The Forest', 1); | |
| drawText(' The dark forest surrounds you.', C.GREEN, 0, 3); | |
| drawText(' Fights remaining: ' + player.forestFights, C.YELLOW, 0, 5); | |
| drawMenuOption('L', 'Look for something to kill', 7, 3); | |
| drawMenuOption('H', 'Healers Hut', 8, 3); | |
| drawMenuOption('R', 'Return to town', 9, 3); | |
| if (player.horse) drawMenuOption('T', 'Dark Cloak Tavern', 10, 3); | |
| if (player.level >= 12) { | |
| drawMenuOption('S', 'Search for the Dragon', 11, 3); | |
| } | |
| if (menuMessage && msgTimer > 0) { | |
| drawText(' ' + menuMessage, C.YELLOW, 1, 17); | |
| } else { | |
| drawText(' Your choice? ', C.CYAN, 1, 18); | |
| } | |
| } | |
| function drawBattle() { | |
| if (!currentEnemy) return; | |
| clearScreen(); | |
| drawBox(C.DKRED, 0, 0, SCREEN_W, SCREEN_H); | |
| drawText(' ' + currentEnemy.name, C.RED, 1, 1); | |
| drawBar('Enemy HP', currentEnemy.hp, currentEnemy.maxHp, 3); | |
| var sep = ''; | |
| for (var i = 0; i < SCREEN_W - 2; i++) sep += '-'; | |
| drawText(sep, C.BROWN, 1, 5); | |
| drawText(' ' + player.name + ' (Lv.' + player.level + ')', C.GREEN, 1, 6); | |
| drawBar('Your HP', player.hp, getMaxHp(), 7); | |
| drawText(sep, C.BROWN, 1, 9); | |
| if (menuMessage) { | |
| drawTextWrapped(menuMessage, C.YELLOW, 2, 10, SCREEN_W - 4); | |
| } | |
| if (subState === 'win') { | |
| drawText(' VICTORY!', C.YELLOW, 1, 14); | |
| drawText(' Gold: +' + numFormat(currentEnemy.goldReward), C.YELLOW, 1, 15); | |
| drawText(' Exp: +' + numFormat(currentEnemy.expReward), C.GREEN, 1, 16); | |
| if (currentEnemy.gemReward > 0) { | |
| drawText(' Gems: +' + currentEnemy.gemReward, C.CYAN, 1, 17); | |
| } | |
| drawText(' Press any key to continue...', C.GREY, 1, 18); | |
| } else if (subState === 'lose') { | |
| drawText(' You have been DEFEATED!', C.RED, 1, 14); | |
| drawText(' You lost ' + numFormat(player.gold) + ' gold.', C.RED, 1, 15); | |
| drawText(' Press any key...', C.GREY, 1, 18); | |
| } else if (subState === 'run') { | |
| drawText(' You run away!', C.YELLOW, 1, 14); | |
| drawText(' Press any key...', C.GREY, 1, 18); | |
| } else if (subState === 'runfail') { | |
| drawText(' You tried to run but FAILED!', C.RED, 1, 14); | |
| drawText(' Press any key...', C.GREY, 1, 18); | |
| } else { | |
| // Battle menu | |
| drawMenuOption('A', 'Attack', 14, 3); | |
| drawMenuOption('R', 'Run', 15, 3); | |
| if (player.skillClass === 'D' && player.skillUses > 0) | |
| drawMenuOption('D', 'Death Knight (' + player.skillUses + ')', 16, 3); | |
| if (player.skillClass === 'M' && player.skillUses > 0) | |
| drawMenuOption('M', 'Mystical (' + player.skillUses + ')', 16, 3); | |
| if (player.skillClass === 'T' && player.skillUses > 0) | |
| drawMenuOption('T', 'Thief (' + player.skillUses + ')', 16, 3); | |
| drawText(' Your move? ', C.CYAN, 1, 18); | |
| } | |
| } | |
| function drawWeaponShop() { | |
| clearScreen(); | |
| drawBox(C.BROWN, 0, 0, SCREEN_W, SCREEN_H); | |
| drawTitle('King Arthurs Weapons', 1); | |
| drawText(' Gold: ' + numFormat(player.gold), C.YELLOW, 1, 2); | |
| drawText(' Equipped: ' + WEAPONS[player.weapon][0], C.WHITE, 1, 3); | |
| var start = shopPage * 7; | |
| var end = Math.min(start + 7, WEAPONS.length); | |
| for (var i = start; i < end; i++) { | |
| var y = 5 + (i - start); | |
| var col = (i === player.weapon) ? C.GREEN : C.WHITE; | |
| var num = (i + 1).toString(); | |
| if (i < 9) num = ' ' + num; | |
| drawText(num + '. ' + padRight(WEAPONS[i][0], 16), col, 2, y); | |
| drawText(padLeft(numFormat(WEAPONS[i][1]), 12), C.YELLOW, 20, y); | |
| drawText('+' + numFormat(WEAPONS[i][2]) + ' atk', C.RED, 34, y); | |
| } | |
| var sep = ''; | |
| for (var j = 0; j < SCREEN_W - 2; j++) sep += '-'; | |
| drawText(sep, C.BROWN, 1, 13); | |
| drawMenuOption('B', 'Buy (enter #)', 14, 2); | |
| drawMenuOption('S', 'Sell current weapon', 15, 2); | |
| drawMenuOption('N', 'Next page', 16, 2); | |
| drawMenuOption('R', 'Return to town', 17, 2); | |
| if (subState === 'buy_weapon') { | |
| drawText(' Buy #: ' + inputBuffer + '_', C.YELLOW, 1, 18); | |
| } else if (menuMessage && msgTimer > 0) { | |
| drawText(' ' + menuMessage, C.YELLOW, 1, 18); | |
| } else { | |
| drawText(' Choice? ', C.CYAN, 1, 18); | |
| } | |
| } | |
| function drawArmorShop() { | |
| clearScreen(); | |
| drawBox(C.BROWN, 0, 0, SCREEN_W, SCREEN_H); | |
| drawTitle('Abduls Armour', 1); | |
| drawText(' Gold: ' + numFormat(player.gold), C.YELLOW, 1, 2); | |
| drawText(' Equipped: ' + ARMORS[player.armor][0], C.WHITE, 1, 3); | |
| var start = shopPage * 7; | |
| var end = Math.min(start + 7, ARMORS.length); | |
| for (var i = start; i < end; i++) { | |
| var y = 5 + (i - start); | |
| var col = (i === player.armor) ? C.GREEN : C.WHITE; | |
| var num = (i + 1).toString(); | |
| if (i < 9) num = ' ' + num; | |
| drawText(num + '. ' + padRight(ARMORS[i][0], 18), col, 2, y); | |
| drawText(padLeft(numFormat(ARMORS[i][1]), 12), C.YELLOW, 22, y); | |
| drawText('+' + numFormat(ARMORS[i][2]) + ' def', C.BLUE, 36, y); | |
| } | |
| var sep = ''; | |
| for (var j = 0; j < SCREEN_W - 2; j++) sep += '-'; | |
| drawText(sep, C.BROWN, 1, 13); | |
| drawMenuOption('B', 'Buy (enter #)', 14, 2); | |
| drawMenuOption('S', 'Sell current armour', 15, 2); | |
| drawMenuOption('N', 'Next page', 16, 2); | |
| drawMenuOption('R', 'Return to town', 17, 2); | |
| if (subState === 'buy_armor') { | |
| drawText(' Buy #: ' + inputBuffer + '_', C.YELLOW, 1, 18); | |
| } else if (menuMessage && msgTimer > 0) { | |
| drawText(' ' + menuMessage, C.YELLOW, 1, 18); | |
| } else { | |
| drawText(' Choice? ', C.CYAN, 1, 18); | |
| } | |
| } | |
| function drawHealer() { | |
| clearScreen(); | |
| drawBox(C.DKCYAN, 0, 0, SCREEN_W, SCREEN_H); | |
| drawTitle('Healers Hut', 1); | |
| var maxHp = getMaxHp(); | |
| var missing = maxHp - player.hp; | |
| var cost = Math.max(missing * player.level * 2, 0); | |
| drawText(' HP: ' + numFormat(player.hp) + ' / ' + numFormat(maxHp), C.GREEN, 1, 3); | |
| drawText(' Gold: ' + numFormat(player.gold), C.YELLOW, 1, 4); | |
| if (missing <= 0) { | |
| drawText(' "You look healthy to me!"', C.WHITE, 1, 6); | |
| } else { | |
| drawText(' "I can heal you fully"', C.WHITE, 1, 6); | |
| drawText(' "for ' + numFormat(cost) + ' gold."', C.WHITE, 1, 7); | |
| } | |
| drawMenuOption('H', 'Heal completely (' + numFormat(cost) + 'g)', 10, 3); | |
| drawMenuOption('R', 'Return', 11, 3); | |
| if (menuMessage && msgTimer > 0) { | |
| drawText(' ' + menuMessage, C.YELLOW, 1, 15); | |
| } | |
| drawText(' Choice? ', C.CYAN, 1, 18); | |
| } | |
| function drawBank() { | |
| clearScreen(); | |
| drawBox(C.YELLOW, 0, 0, SCREEN_W, SCREEN_H); | |
| drawTitle('Ye Olde Bank', 1); | |
| drawText(' Gold on hand: ' + numFormat(player.gold), C.YELLOW, 1, 3); | |
| drawText(' Gold in bank: ' + numFormat(player.bank), C.GREEN, 1, 4); | |
| drawText(' (10% daily interest!)', C.GREY, 1, 5); | |
| drawMenuOption('D', 'Deposit all gold', 7, 3); | |
| drawMenuOption('W', 'Withdraw all gold', 8, 3); | |
| drawMenuOption('R', 'Return to town', 9, 3); | |
| if (menuMessage && msgTimer > 0) { | |
| drawText(' ' + menuMessage, C.YELLOW, 1, 15); | |
| } | |
| drawText(' Choice? ', C.CYAN, 1, 18); | |
| } | |
| function drawStats() { | |
| clearScreen(); | |
| drawBox(C.CYAN, 0, 0, SCREEN_W, SCREEN_H); | |
| drawTitle(player.name + '\'s Stats', 1); | |
| drawText(' Level: ' + player.level + ' (' + (player.sex === 'M' ? 'Male' : 'Female') + ')', C.WHITE, 1, 3); | |
| drawText(' Exp: ' + numFormat(player.exp), C.GREEN, 1, 4); | |
| if (player.level < 12) { | |
| drawText(' Next Lv: ' + numFormat(LEVELS[player.level][0]), C.GREY, 1, 5); | |
| } else { | |
| drawText(' MAX LEVEL', C.YELLOW, 1, 5); | |
| } | |
| drawText(' HP: ' + numFormat(player.hp) + '/' + numFormat(getMaxHp()), C.GREEN, 1, 6); | |
| drawText(' Attack: ' + numFormat(totalAttack()), C.RED, 1, 7); | |
| drawText(' Defense: ' + numFormat(totalDefense()), C.BLUE, 1, 8); | |
| drawText(' Charm: ' + numFormat(player.charm), C.PURPLE, 1, 9); | |
| drawText(' Weapon: ' + WEAPONS[player.weapon][0], C.WHITE, 1, 10); | |
| drawText(' Armour: ' + ARMORS[player.armor][0], C.WHITE, 1, 11); | |
| drawText(' Gold: ' + numFormat(player.gold), C.YELLOW, 1, 12); | |
| drawText(' Bank: ' + numFormat(player.bank), C.YELLOW, 1, 13); | |
| drawText(' Gems: ' + numFormat(player.gems), C.CYAN, 1, 14); | |
| var classLabel = player.skillClass === 'D' ? 'Death Knight' : player.skillClass === 'M' ? 'Mystical' : 'Thief'; | |
| var skillPts = player.skillClass === 'D' ? player.skillDeath : player.skillClass === 'M' ? player.skillMystic : player.skillThief; | |
| drawText(' Class: ' + classLabel, C.PURPLE, 1, 15); | |
| drawText(' Skills: Lv.' + skillPts + ' (' + player.skillUses + ' uses left)', C.PURPLE, 1, 16); | |
| drawText(' Heroes: ' + player.heroDeeds + ' deeds', C.YELLOW, 1, 17); | |
| drawText(' Press any key to return...', C.GREY, 1, 18); | |
| } | |
| function drawTraining() { | |
| clearScreen(); | |
| drawBox(C.DKPURPLE, 0, 0, SCREEN_W, SCREEN_H); | |
| drawTitle('Turgons Warrior Training', 1); | |
| var masterName = MASTERS[player.level - 1]; | |
| drawText(' Your master: ' + masterName, C.WHITE, 1, 3); | |
| drawText(' Level: ' + player.level + ' / 12', C.GREEN, 1, 4); | |
| if (player.level >= 12) { | |
| drawText(' You have defeated all masters!', C.YELLOW, 1, 6); | |
| drawText(' Seek the Red Dragon in the', C.RED, 1, 7); | |
| drawText(' forest to prove your worth!', C.RED, 1, 8); | |
| } else { | |
| var needed = LEVELS[player.level][0]; | |
| drawText(' Exp needed: ' + numFormat(needed), C.GREY, 1, 5); | |
| drawText(' Your exp: ' + numFormat(player.exp), C.GREEN, 1, 6); | |
| if (player.exp >= needed) { | |
| drawText(' "You are ready, warrior!"', C.YELLOW, 1, 8); | |
| drawMenuOption('A', 'Attack your master!', 10, 3); | |
| } else { | |
| drawText(' "You are not ready yet."', C.RED, 1, 8); | |
| drawText(' Need ' + numFormat(needed - player.exp) + ' more exp.', C.GREY, 1, 9); | |
| } | |
| } | |
| drawMenuOption('R', 'Return to town', 15, 3); | |
| if (menuMessage && msgTimer > 0) { | |
| drawText(' ' + menuMessage, C.YELLOW, 1, 17); | |
| } | |
| drawText(' Choice? ', C.CYAN, 1, 18); | |
| } | |
| function drawInn() { | |
| clearScreen(); | |
| drawBox(C.BROWN, 0, 0, SCREEN_W, SCREEN_H); | |
| drawTitle('The Inn', 1); | |
| drawText(' The fire crackles warmly...', C.BROWN, 1, 3); | |
| var flirtTarget = (player.sex === 'M') ? 'Violet' : 'Seth Able'; | |
| var roomCost = player.charm >= 101 ? 0 : player.level * 400; | |
| drawMenuOption('F', 'Flirt with ' + flirtTarget, 5, 3); | |
| drawMenuOption('G', 'Get a room (' + (roomCost === 0 ? 'FREE!' : numFormat(roomCost) + 'g') + ')', 6, 3); | |
| drawMenuOption('H', 'Hear Seth Able sing', 7, 3); | |
| drawMenuOption('T', 'Talk to Bartender', 8, 3); | |
| drawMenuOption('C', 'Converse with people', 9, 3); | |
| drawMenuOption('D', 'Daily News', 10, 3); | |
| drawMenuOption('R', 'Return to town', 11, 3); | |
| if (menuMessage && msgTimer > 0) { | |
| drawText(' ' + menuMessage, C.YELLOW, 1, 15); | |
| } | |
| drawText(' Choice? ', C.CYAN, 1, 18); | |
| } | |
| function drawFlirt() { | |
| clearScreen(); | |
| drawBox(C.PURPLE, 0, 0, SCREEN_W, SCREEN_H); | |
| var target = (player.sex === 'M') ? 'Violet' : 'Seth Able'; | |
| drawTitle('Flirting with ' + target, 1); | |
| if (player.flirted) { | |
| drawText(' "Not now, maybe tomorrow..."', C.WHITE, 1, 5); | |
| drawText(' Press any key...', C.GREY, 1, 18); | |
| return; | |
| } | |
| var flirtOptions; | |
| if (player.sex === 'M') { | |
| flirtOptions = [ | |
| ['W', 'Wink', 1, 5], | |
| ['K', 'Kiss her hand', 2, 10], | |
| ['P', 'Peck on the lips', 4, 20], | |
| ['S', 'Sit on your lap', 8, 30], | |
| ['G', 'Grab her', 16, 40], | |
| ['C', 'Carry upstairs', 32, 40], | |
| ['M', 'Marry her', 100, 1000] | |
| ]; | |
| } else { | |
| flirtOptions = [ | |
| ['W', 'Wink', 1, 5], | |
| ['F', 'Flutter eyelashes', 2, 10], | |
| ['D', 'Drop hanky', 4, 20], | |
| ['A', 'Ask for a drink', 8, 30], | |
| ['K', 'Kiss soundly', 16, 40], | |
| ['C', 'Completely seduce', 32, 40], | |
| ['M', 'Marry', 120, 0] | |
| ]; | |
| } | |
| drawText(' Your charm: ' + player.charm, C.PURPLE, 1, 3); | |
| for (var i = 0; i < flirtOptions.length; i++) { | |
| var opt = flirtOptions[i]; | |
| var canDo = player.charm >= opt[2]; | |
| var keyCol = canDo ? C.CYAN : C.DKGREY; | |
| var textCol = canDo ? C.WHITE : C.DKGREY; | |
| drawText('(' + opt[0] + ') ', keyCol, 3, 5 + i); | |
| drawText(opt[1] + ' (charm ' + opt[2] + ')', textCol, 7, 5 + i); | |
| } | |
| drawMenuOption('N', 'Nevermind', 5 + flirtOptions.length, 3); | |
| if (menuMessage && msgTimer > 0) { | |
| drawText(' ' + menuMessage, C.YELLOW, 1, 17); | |
| } | |
| drawText(' Choice? ', C.CYAN, 1, 18); | |
| } | |
| function drawSong() { | |
| clearScreen(); | |
| drawBox(C.PURPLE, 0, 0, SCREEN_W, SCREEN_H); | |
| drawTitle('Seth Able the Bard', 1); | |
| if (player.heardSong) { | |
| drawText(' "Come back tomorrow for', C.WHITE, 1, 5); | |
| drawText(' another tune!"', C.WHITE, 1, 6); | |
| drawText(' Press any key...', C.GREY, 1, 18); | |
| } else if (subState === 'singing') { | |
| drawText(' Seth strums his lute...', C.PURPLE, 1, 4); | |
| drawText(' ' + menuMessage, C.YELLOW, 1, 7); | |
| drawText(' Press any key...', C.GREY, 1, 18); | |
| } else { | |
| drawText(' Seth holds his lute and', C.WHITE, 1, 5); | |
| drawText(' looks at you expectantly.', C.WHITE, 1, 6); | |
| drawMenuOption('A', 'Ask him to sing', 8, 3); | |
| drawMenuOption('R', 'Return to Inn', 9, 3); | |
| drawText(' Choice? ', C.CYAN, 1, 18); | |
| } | |
| } | |
| function drawBartender() { | |
| clearScreen(); | |
| drawBox(C.BROWN, 0, 0, SCREEN_W, SCREEN_H); | |
| drawTitle('The Bartender', 1); | |
| drawText(' "What can I do for ya?"', C.WHITE, 1, 3); | |
| drawText(' Gems: ' + player.gems, C.CYAN, 1, 4); | |
| drawMenuOption('G', 'Trade gems for elixirs (2 gems)', 6, 3); | |
| drawMenuOption('V', 'Ask about Violet', 7, 3); | |
| drawMenuOption('R', 'Return to Inn', 8, 3); | |
| if (menuMessage && msgTimer > 0) { | |
| drawTextWrapped(menuMessage, C.YELLOW, 2, 12, SCREEN_W - 4); | |
| } | |
| drawText(' Choice? ', C.CYAN, 1, 18); | |
| } | |
| function drawElixir() { | |
| clearScreen(); | |
| drawBox(C.CYAN, 0, 0, SCREEN_W, SCREEN_H); | |
| drawTitle('Gem Elixirs', 1); | |
| drawText(' Gems: ' + player.gems, C.CYAN, 1, 3); | |
| drawText(' (Each elixir costs 2 gems)', C.GREY, 1, 4); | |
| drawMenuOption('S', 'Strength (+1 attack)', 6, 3); | |
| drawMenuOption('V', 'Vitality (+1 defense)', 7, 3); | |
| drawMenuOption('H', 'Hit Points (+1 max HP)', 8, 3); | |
| drawMenuOption('R', 'Return', 9, 3); | |
| if (menuMessage && msgTimer > 0) { | |
| drawText(' ' + menuMessage, C.YELLOW, 1, 15); | |
| } | |
| drawText(' Choice? ', C.CYAN, 1, 18); | |
| } | |
| function drawDailyNews() { | |
| clearScreen(); | |
| drawBox(C.GREEN, 0, 0, SCREEN_W, SCREEN_H); | |
| drawTitle('Daily News', 1); | |
| var maxShow = 14; | |
| var start = Math.max(0, dailyLog.length - maxShow - scrollOffset); | |
| var end = Math.min(start + maxShow, dailyLog.length); | |
| for (var i = start; i < end; i++) { | |
| var msg = dailyLog[i]; | |
| if (msg.length > SCREEN_W - 4) msg = msg.substring(0, SCREEN_W - 4); | |
| drawText(' ' + msg, C.WHITE, 1, 3 + (i - start)); | |
| } | |
| if (dailyLog.length === 0) { | |
| drawText(' No news today.', C.GREY, 1, 5); | |
| } | |
| if (dailyLog.length > maxShow) { | |
| drawText(' [U]p/[D]own to scroll, other key=back', C.GREY, 1, 18); | |
| } else { | |
| drawText(' Press any key to return...', C.GREY, 1, 18); | |
| } | |
| } | |
| function drawForestEvent() { | |
| clearScreen(); | |
| drawBox(C.DKGREEN, 0, 0, SCREEN_W, SCREEN_H); | |
| drawTitle('Forest Event', 1); | |
| drawTextWrapped(menuMessage, C.YELLOW, 2, 4, SCREEN_W - 4); | |
| if (subState === 'oldman') { | |
| drawMenuOption('H', 'Help the old man', 12, 3); | |
| drawMenuOption('I', 'Ignore him', 13, 3); | |
| drawText(' Choice? ', C.CYAN, 1, 18); | |
| } else if (subState === 'hag') { | |
| drawMenuOption('G', 'Give her a gem', 12, 3); | |
| drawMenuOption('I', 'Ignore her', 13, 3); | |
| drawText(' Choice? ', C.CYAN, 1, 18); | |
| } else if (subState === 'fairy') { | |
| drawMenuOption('A', 'Ask for a blessing', 12, 3); | |
| drawMenuOption('C', 'Catch the fairy!', 13, 3); | |
| drawText(' Choice? ', C.CYAN, 1, 18); | |
| } else { | |
| drawText(' Press any key...', C.GREY, 1, 18); | |
| } | |
| } | |
| function drawDragonFight() { | |
| if (!currentEnemy) return; | |
| clearScreen(); | |
| drawBox(C.RED, 0, 0, SCREEN_W, SCREEN_H); | |
| drawText(' THE RED DRAGON', C.RED, 1, 1); | |
| drawText(' /\\ /\\', C.RED, 1, 2); | |
| drawText(' / \\ / \\', C.RED, 1, 3); | |
| drawText('/ \\/ \\', C.DKRED, 1, 4); | |
| drawText('\\ GRRRR! /', C.YELLOW, 1, 5); | |
| drawText(' \\________/', C.DKRED, 1, 6); | |
| drawBar('Dragon', currentEnemy.hp, currentEnemy.maxHp, 8); | |
| drawBar('You', player.hp, getMaxHp(), 9); | |
| if (menuMessage) { | |
| drawTextWrapped(menuMessage, C.YELLOW, 2, 11, SCREEN_W - 4); | |
| } | |
| if (subState === 'dragonwin') { | |
| drawText(' THE RED DRAGON IS SLAIN!', C.YELLOW, 1, 14); | |
| drawText(' You are a HERO!', C.GREEN, 1, 15); | |
| drawText(' Hero deeds: ' + player.heroDeeds, C.YELLOW, 1, 16); | |
| drawText(' Press any key...', C.GREY, 1, 18); | |
| } else if (subState === 'dragonlose') { | |
| drawText(' The dragon has defeated you!', C.RED, 1, 14); | |
| drawText(' Try again tomorrow...', C.GREY, 1, 15); | |
| drawText(' Press any key...', C.GREY, 1, 18); | |
| } else { | |
| drawMenuOption('A', 'Attack', 14, 3); | |
| drawMenuOption('R', 'Run away', 15, 3); | |
| if (player.skillUses > 0) { | |
| var sk = player.skillClass === 'D' ? 'Death Knight' : player.skillClass === 'M' ? 'Mystical' : 'Thief'; | |
| drawMenuOption('S', sk + ' (' + player.skillUses + ')', 16, 3); | |
| } | |
| drawText(' Your move? ', C.CYAN, 1, 18); | |
| } | |
| } | |
| function drawDarkTavern() { | |
| clearScreen(); | |
| drawBox(C.DKGREY, 0, 0, SCREEN_W, SCREEN_H); | |
| drawTitle('Dark Cloak Tavern', 1); | |
| drawText(' Shadowy figures lurk about.', C.GREY, 1, 3); | |
| drawMenuOption('G', 'Gamble with locals', 5, 3); | |
| drawMenuOption('C', 'Change profession', 6, 3); | |
| drawMenuOption('R', 'Return to forest', 7, 3); | |
| if (menuMessage && msgTimer > 0) { | |
| drawTextWrapped(menuMessage, C.YELLOW, 2, 12, SCREEN_W - 4); | |
| } | |
| drawText(' Choice? ', C.CYAN, 1, 18); | |
| } | |
| function drawGamble() { | |
| clearScreen(); | |
| drawBox(C.DKGREY, 0, 0, SCREEN_W, SCREEN_H); | |
| drawTitle('Gambling', 1); | |
| drawText(' Gold: ' + numFormat(player.gold), C.YELLOW, 1, 3); | |
| if (subState === 'gamble_bet') { | |
| drawText(' How much to bet?', C.WHITE, 1, 5); | |
| drawText(' > ' + inputBuffer + '_', C.YELLOW, 3, 7); | |
| drawText(' (Enter amount, R to return)', C.GREY, 3, 9); | |
| } else if (subState === 'gamble_result') { | |
| drawTextWrapped(menuMessage, C.YELLOW, 2, 6, SCREEN_W - 4); | |
| drawText(' Press any key...', C.GREY, 1, 18); | |
| } else { | |
| drawMenuOption('G', 'Place a bet', 5, 3); | |
| drawMenuOption('R', 'Return', 6, 3); | |
| drawText(' Choice? ', C.CYAN, 1, 18); | |
| } | |
| } | |
| function drawMasterBattle() { | |
| if (!currentEnemy) return; | |
| clearScreen(); | |
| drawBox(C.DKPURPLE, 0, 0, SCREEN_W, SCREEN_H); | |
| drawText(' Master: ' + currentEnemy.name, C.PURPLE, 1, 1); | |
| drawBar('Master', currentEnemy.hp, currentEnemy.maxHp, 3); | |
| var sep = ''; | |
| for (var i = 0; i < SCREEN_W - 2; i++) sep += '-'; | |
| drawText(sep, C.BROWN, 1, 5); | |
| drawText(' ' + player.name, C.GREEN, 1, 6); | |
| drawBar('Your HP', player.hp, getMaxHp(), 7); | |
| drawText(sep, C.BROWN, 1, 9); | |
| if (menuMessage) { | |
| drawTextWrapped(menuMessage, C.YELLOW, 2, 10, SCREEN_W - 4); | |
| } | |
| if (subState === 'master_win') { | |
| drawText(' You DEFEATED your master!', C.YELLOW, 1, 14); | |
| drawText(' You are now level ' + player.level + '!', C.GREEN, 1, 15); | |
| drawText(' Press any key...', C.GREY, 1, 18); | |
| } else if (subState === 'master_lose') { | |
| drawText(' Your master defeated you!', C.RED, 1, 14); | |
| drawText(' Train harder and return.', C.GREY, 1, 15); | |
| drawText(' Press any key...', C.GREY, 1, 18); | |
| } else { | |
| drawMenuOption('A', 'Attack', 14, 3); | |
| drawMenuOption('R', 'Run away', 15, 3); | |
| if (player.skillUses > 0) { | |
| var sk = player.skillClass === 'D' ? 'Death Knight' : player.skillClass === 'M' ? 'Mystical' : 'Thief'; | |
| drawMenuOption('S', sk + ' (' + player.skillUses + ')', 16, 3); | |
| } | |
| drawText(' Your move? ', C.CYAN, 1, 18); | |
| } | |
| } | |
| // --- COMBAT ENGINE --- | |
| function startForestBattle() { | |
| var lvl = player.level - 1; | |
| var monsterList = MONSTERS[lvl]; | |
| var m = monsterList[rand(0, monsterList.length - 1)]; | |
| currentEnemy = { | |
| name: m[0], | |
| hp: m[1], | |
| maxHp: m[1], | |
| attack: m[2], | |
| defense: m[3], | |
| goldReward: m[4] + rand(0, Math.floor(m[4] * 0.3)), | |
| expReward: m[5] + rand(0, Math.floor(m[5] * 0.2)), | |
| gemReward: (rand(1, 10) === 1) ? 1 : 0 | |
| }; | |
| subState = 'fighting'; | |
| menuMessage = 'A ' + currentEnemy.name + ' appears!'; | |
| state = 'battle'; | |
| } | |
| function playerAttack(powerMult) { | |
| if (powerMult === undefined) powerMult = 1.0; | |
| var atk = totalAttack(); | |
| var def = currentEnemy.defense; | |
| var baseDmg = Math.max(1, atk - def + rand(-Math.floor(atk * 0.15), Math.floor(atk * 0.15))); | |
| baseDmg = Math.floor(baseDmg * powerMult); | |
| // Power move chance (1 in 20) | |
| if (rand(1, 20) === 1) { | |
| baseDmg = Math.floor(baseDmg * 3); | |
| menuMessage = 'POWER MOVE! You deal ' + numFormat(baseDmg) + ' damage!'; | |
| } else { | |
| menuMessage = 'You hit for ' + numFormat(baseDmg) + ' damage!'; | |
| } | |
| currentEnemy.hp -= baseDmg; | |
| if (currentEnemy.hp <= 0) { | |
| currentEnemy.hp = 0; | |
| return true; // enemy dead | |
| } | |
| return false; | |
| } | |
| function enemyAttack() { | |
| var atk = currentEnemy.attack; | |
| var def = totalDefense(); | |
| var baseDmg = Math.max(1, atk - def + rand(-Math.floor(atk * 0.15), Math.floor(atk * 0.15))); | |
| // Enemy power move (1 in 25) | |
| if (rand(1, 25) === 1) { | |
| baseDmg = Math.floor(baseDmg * 3); | |
| menuMessage += ' ENEMY POWER MOVE! ' + numFormat(baseDmg) + ' dmg to you!'; | |
| } else { | |
| menuMessage += ' Enemy hits for ' + numFormat(baseDmg) + '.'; | |
| } | |
| player.hp -= baseDmg; | |
| if (player.hp <= 0) { | |
| player.hp = 0; | |
| return true; // player dead | |
| } | |
| return false; | |
| } | |
| function winBattle() { | |
| player.gold += currentEnemy.goldReward; | |
| player.exp += currentEnemy.expReward; | |
| player.gems += currentEnemy.gemReward; | |
| player.kills++; | |
| subState = 'win'; | |
| addLog(player.name + ' defeated a ' + currentEnemy.name + '.'); | |
| saveGame(); | |
| } | |
| function loseBattle() { | |
| if (player.fairy) { | |
| player.fairy = false; | |
| player.hp = getMaxHp(); | |
| menuMessage = 'A fairy saved you! Full HP restored!'; | |
| return false; // not really dead | |
| } | |
| subState = 'lose'; | |
| addLog(player.name + ' was killed by a ' + currentEnemy.name + '.'); | |
| player.gold = 0; | |
| player.alive = false; | |
| player.forestFights = 0; | |
| saveGame(); | |
| return true; | |
| } | |
| function startMasterBattle() { | |
| var lvl = player.level; | |
| // Master stats: slightly above current level's base stats | |
| // This ensures masters are beatable with decent gear + some grinding | |
| var curLvl = lvl - 1; // 0-indexed for LEVELS array | |
| var masterHp = Math.floor(LEVELS[curLvl][1] * 1.8); | |
| var masterAtk = Math.floor(LEVELS[curLvl][2] * 1.1); | |
| var masterDef = Math.floor(LEVELS[curLvl][3] * 0.8); | |
| currentEnemy = { | |
| name: MASTERS[lvl - 1], | |
| hp: masterHp, | |
| maxHp: masterHp, | |
| attack: masterAtk, | |
| defense: masterDef, | |
| goldReward: 0, | |
| expReward: 0, | |
| gemReward: 0 | |
| }; | |
| subState = 'master_fighting'; | |
| menuMessage = MASTERS[lvl - 1] + ' prepares to fight!'; | |
| state = 'master_battle'; | |
| } | |
| function levelUp() { | |
| if (player.level >= 12) return; | |
| player.level++; | |
| var ld = LEVELS[player.level - 1]; | |
| player.baseAtk = ld[2]; | |
| player.baseDef = ld[3]; | |
| player.hp = ld[1] + player.gemHp; | |
| // Gain skill point | |
| if (player.skillClass === 'D') player.skillDeath = Math.min(40, player.skillDeath + 1); | |
| else if (player.skillClass === 'M') player.skillMystic = Math.min(40, player.skillMystic + 1); | |
| else player.skillThief = Math.min(40, player.skillThief + 1); | |
| player.skillUses = getSkillUses(); | |
| addLog(player.name + ' advanced to level ' + player.level + '!'); | |
| saveGame(); | |
| } | |
| // --- FOREST EVENTS --- | |
| function triggerForestEvent() { | |
| var roll = rand(1, 100); | |
| if (roll <= 12) { | |
| // Old Man | |
| menuMessage = 'You see an old man stumbling around the forest. He looks lost and confused.'; | |
| subState = 'oldman'; | |
| state = 'forest_event'; | |
| } else if (roll <= 22) { | |
| // Find gold | |
| var gold = player.level * rand(100, 300); | |
| player.gold += gold; | |
| menuMessage = 'You find a pouch of gold on the ground! +' + numFormat(gold) + ' gold!'; | |
| subState = ''; | |
| state = 'forest_event'; | |
| saveGame(); | |
| } else if (roll <= 30) { | |
| // Find gems | |
| var gems = rand(1, 3); | |
| player.gems += gems; | |
| menuMessage = 'You discover ' + gems + ' sparkling gem' + (gems > 1 ? 's' : '') + '!'; | |
| subState = ''; | |
| state = 'forest_event'; | |
| saveGame(); | |
| } else if (roll <= 38) { | |
| // Old Hag | |
| menuMessage = 'An old hag approaches you. "Give me a gem and I shall heal you completely!"'; | |
| subState = 'hag'; | |
| state = 'forest_event'; | |
| } else if (roll <= 46) { | |
| // Fairies | |
| menuMessage = 'You spot beautiful fairies bathing in a woodland pond!'; | |
| subState = 'fairy'; | |
| state = 'forest_event'; | |
| } else if (roll <= 52) { | |
| // Pretty stick (+5 charm) | |
| player.charm += 5; | |
| menuMessage = 'You find a Pretty Stick! Your charm increases by 5! (Now: ' + player.charm + ')'; | |
| subState = ''; | |
| state = 'forest_event'; | |
| saveGame(); | |
| } else if (roll <= 56) { | |
| // Ugly stick (-1 charm) | |
| player.charm = Math.max(0, player.charm - 1); | |
| menuMessage = 'You are hit by an Ugly Stick! Charm -1. (Now: ' + player.charm + ')'; | |
| subState = ''; | |
| state = 'forest_event'; | |
| saveGame(); | |
| } else if (roll <= 62) { | |
| // Merry Men | |
| player.hp = getMaxHp(); | |
| menuMessage = 'The Merry Men find you! They nurse you back to full health!'; | |
| subState = ''; | |
| state = 'forest_event'; | |
| saveGame(); | |
| } else { | |
| // No event, just a fight | |
| startForestBattle(); | |
| } | |
| } | |
| // --- DRAGON FIGHT --- | |
| function startDragonFight() { | |
| currentEnemy = { | |
| name: 'The Red Dragon', | |
| hp: RED_DRAGON.hp, | |
| maxHp: RED_DRAGON.maxHp, | |
| attack: RED_DRAGON.attack, | |
| defense: RED_DRAGON.defense, | |
| goldReward: 0, | |
| expReward: 0, | |
| gemReward: 0 | |
| }; | |
| subState = 'dragon_fighting'; | |
| menuMessage = 'The Red Dragon rears back and ROARS!'; | |
| state = 'dragon'; | |
| } | |
| function slayDragon() { | |
| player.heroDeeds++; | |
| addLog('*** ' + player.name + ' has SLAIN the Red Dragon! ***'); | |
| // Reset player | |
| player.level = 1; | |
| player.exp = 0; | |
| player.gold = 500; | |
| player.bank = 0; | |
| player.weapon = 0; | |
| player.armor = 0; | |
| player.baseAtk = LEVELS[0][2]; | |
| player.baseDef = LEVELS[0][3]; | |
| player.gemAtk = 0; | |
| player.gemDef = 0; | |
| player.gemHp = 0; | |
| player.hp = LEVELS[0][1]; // gems already zeroed above | |
| player.gems = 10; | |
| player.skillDeath = 0; | |
| player.skillMystic = 0; | |
| player.skillThief = 0; | |
| if (player.skillClass === 'D') player.skillDeath = 1; | |
| else if (player.skillClass === 'M') player.skillMystic = 1; | |
| else player.skillThief = 1; | |
| player.forestFights = getForestFights(); | |
| player.skillUses = getSkillUses(); | |
| subState = 'dragonwin'; | |
| saveGame(); | |
| } | |
| // --- INPUT HANDLERS --- | |
| function handleTitleInput(key) { | |
| state = 'loading'; | |
| var loaded = loadGame(); | |
| if (loaded) { | |
| state = 'main'; | |
| } else { | |
| state = 'create'; | |
| subState = 'name'; | |
| inputBuffer = ''; | |
| } | |
| } | |
| function handleCreateInput(key) { | |
| if (subState === 'name') { | |
| if (key === 13 || key === 10) { // Enter | |
| if (inputBuffer.length > 0) { | |
| createNewPlayer(inputBuffer); | |
| subState = 'sex'; | |
| } | |
| } else if (key === 8) { // Backspace | |
| if (inputBuffer.length > 0) { | |
| inputBuffer = inputBuffer.substring(0, inputBuffer.length - 1); | |
| } | |
| } else if (key >= 32 && key < 127 && inputBuffer.length < 15) { | |
| inputBuffer += String.fromCharCode(key); | |
| } | |
| } else if (subState === 'sex') { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| if (ch === 'M') { | |
| player.sex = 'M'; | |
| subState = 'class'; | |
| } else if (ch === 'F') { | |
| player.sex = 'F'; | |
| subState = 'class'; | |
| } | |
| } else if (subState === 'class') { | |
| var ch2 = String.fromCharCode(key).toUpperCase(); | |
| if (ch2 === 'D' || ch2 === 'M' || ch2 === 'T') { | |
| player.skillClass = ch2; | |
| if (ch2 === 'D') player.skillDeath = 1; | |
| else if (ch2 === 'M') player.skillMystic = 1; | |
| else player.skillThief = 1; | |
| player.skillUses = getSkillUses(); | |
| addLog(player.name + ' entered the realm for the first time!'); | |
| saveGame(); | |
| state = 'main'; | |
| } | |
| } | |
| } | |
| function handleMainInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| if (!player.alive) { | |
| showMessage('You are dead! Return tomorrow.'); | |
| return; | |
| } | |
| switch (ch) { | |
| case 'F': | |
| state = 'forest'; | |
| break; | |
| case 'K': | |
| state = 'weapon_shop'; | |
| shopPage = 0; | |
| break; | |
| case 'A': | |
| state = 'armor_shop'; | |
| shopPage = 0; | |
| break; | |
| case 'H': | |
| state = 'healer'; | |
| healerReturn = 'main'; | |
| break; | |
| case 'V': | |
| state = 'stats'; | |
| break; | |
| case 'I': | |
| state = 'inn'; | |
| break; | |
| case 'T': | |
| state = 'training'; | |
| break; | |
| case 'Y': | |
| state = 'bank'; | |
| break; | |
| case 'D': | |
| state = 'daily'; | |
| dailyReturn = 'main'; | |
| scrollOffset = 0; | |
| break; | |
| case 'L': | |
| state = 'stats'; // just show own stats in single player | |
| break; | |
| case 'Q': | |
| showMessage('You rest in the fields...'); | |
| saveGame(); | |
| break; | |
| } | |
| } | |
| function handleForestInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| switch (ch) { | |
| case 'L': | |
| if (player.forestFights <= 0) { | |
| showMessage('No fights left today!'); | |
| return; | |
| } | |
| player.forestFights--; | |
| // 30% chance of event, 70% chance of monster | |
| if (rand(1, 100) <= 30) { | |
| triggerForestEvent(); | |
| } else { | |
| startForestBattle(); | |
| } | |
| break; | |
| case 'H': | |
| state = 'healer'; | |
| healerReturn = 'forest'; | |
| break; | |
| case 'R': | |
| state = 'main'; | |
| break; | |
| case 'T': | |
| if (player.horse) { | |
| state = 'dark_tavern'; | |
| } | |
| break; | |
| case 'S': | |
| if (player.level >= 12) { | |
| startDragonFight(); | |
| } | |
| break; | |
| } | |
| } | |
| function handleBattleInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| if (subState === 'win' || subState === 'lose' || subState === 'run') { | |
| if (subState === 'lose') { | |
| state = 'main'; | |
| } else { | |
| state = 'forest'; | |
| } | |
| currentEnemy = null; | |
| subState = ''; | |
| return; | |
| } | |
| if (subState === 'runfail') { | |
| // Enemy gets a free attack | |
| var died = enemyAttack(); | |
| if (died) { | |
| if (loseBattle()) return; | |
| } | |
| subState = 'fighting'; | |
| return; | |
| } | |
| if (subState !== 'fighting') return; | |
| switch (ch) { | |
| case 'A': | |
| var killed = playerAttack(1.0); | |
| if (killed) { | |
| winBattle(); | |
| return; | |
| } | |
| // Enemy turn | |
| var died = enemyAttack(); | |
| if (died) { | |
| loseBattle(); | |
| } | |
| break; | |
| case 'R': | |
| if (rand(1, 3) <= 2) { | |
| subState = 'run'; | |
| menuMessage = 'You flee from battle!'; | |
| } else { | |
| subState = 'runfail'; | |
| menuMessage = 'You tried to run but failed!'; | |
| } | |
| break; | |
| case 'D': case 'M': case 'T': | |
| if (ch === player.skillClass && player.skillUses > 0) { | |
| player.skillUses--; | |
| var mult = 1.5; | |
| if (player.skillClass === 'M') { | |
| // Mystical: pinch=1.2, shatter=2.5 (simplified) | |
| var pts = player.skillMystic; | |
| if (pts >= 16) mult = 2.5; | |
| else if (pts >= 8) mult = 1.8; | |
| else mult = 1.3; | |
| } | |
| var killed2 = playerAttack(mult); | |
| menuMessage = 'Special attack! ' + menuMessage; | |
| if (killed2) { | |
| currentEnemy.gemReward = Math.max(currentEnemy.gemReward, 1); // skill kill bonus | |
| winBattle(); | |
| return; | |
| } | |
| var died2 = enemyAttack(); | |
| if (died2) { | |
| loseBattle(); | |
| } | |
| } | |
| break; | |
| } | |
| } | |
| function handleMasterBattleInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| if (subState === 'master_win' || subState === 'master_lose') { | |
| state = 'training'; | |
| currentEnemy = null; | |
| subState = ''; | |
| return; | |
| } | |
| if (subState !== 'master_fighting') return; | |
| switch (ch) { | |
| case 'A': | |
| var killed = playerAttack(1.0); | |
| if (killed) { | |
| levelUp(); | |
| subState = 'master_win'; | |
| return; | |
| } | |
| var died = enemyAttack(); | |
| if (died) { | |
| if (player.fairy) { | |
| player.fairy = false; | |
| player.hp = getMaxHp(); | |
| menuMessage += ' A fairy saved you!'; | |
| } else { | |
| subState = 'master_lose'; | |
| player.hp = Math.max(1, getMaxHp()); | |
| addLog(player.name + ' was defeated by ' + currentEnemy.name + '.'); | |
| saveGame(); | |
| } | |
| } | |
| break; | |
| case 'R': | |
| state = 'training'; | |
| currentEnemy = null; | |
| subState = ''; | |
| showMessage('You retreat from your master.'); | |
| break; | |
| case 'S': | |
| if (player.skillUses > 0) { | |
| player.skillUses--; | |
| var mult = 1.5; | |
| if (player.skillClass === 'M') { | |
| var pts = player.skillMystic; | |
| if (pts >= 16) mult = 2.5; | |
| else if (pts >= 8) mult = 1.8; | |
| else mult = 1.3; | |
| } | |
| var skilledKill = playerAttack(mult); | |
| menuMessage = 'Special attack! ' + menuMessage; | |
| if (skilledKill) { | |
| levelUp(); | |
| subState = 'master_win'; | |
| return; | |
| } | |
| var skilledDied = enemyAttack(); | |
| if (skilledDied) { | |
| if (player.fairy) { | |
| player.fairy = false; | |
| player.hp = getMaxHp(); | |
| menuMessage += ' A fairy saved you!'; | |
| } else { | |
| subState = 'master_lose'; | |
| player.hp = Math.max(1, getMaxHp()); | |
| addLog(player.name + ' was defeated by ' + currentEnemy.name + '.'); | |
| saveGame(); | |
| } | |
| } | |
| } | |
| break; | |
| } | |
| } | |
| function handleWeaponShopInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| if (subState === 'buy_weapon') { | |
| if (key === 13 || key === 10) { | |
| var idx = parseInt(inputBuffer) - 1; | |
| if (idx >= 0 && idx < WEAPONS.length) { | |
| if (idx === player.weapon) { | |
| showMessage('You already have that weapon!'); | |
| } else if (player.weapon > 0) { | |
| showMessage('Sell your current weapon first!'); | |
| } else if (player.gold >= WEAPONS[idx][1]) { | |
| player.gold -= WEAPONS[idx][1]; | |
| player.weapon = idx; | |
| showMessage('Bought ' + WEAPONS[idx][0] + '!'); | |
| addLog(player.name + ' bought a ' + WEAPONS[idx][0] + '.'); | |
| saveGame(); | |
| } else { | |
| showMessage('Not enough gold!'); | |
| } | |
| } | |
| subState = ''; | |
| inputBuffer = ''; | |
| return; | |
| } else if (key === 8) { | |
| if (inputBuffer.length > 0) inputBuffer = inputBuffer.substring(0, inputBuffer.length - 1); | |
| return; | |
| } else if (key >= 48 && key <= 57 && inputBuffer.length < 2) { | |
| inputBuffer += String.fromCharCode(key); | |
| return; | |
| } | |
| return; | |
| } | |
| switch (ch) { | |
| case 'B': | |
| subState = 'buy_weapon'; | |
| inputBuffer = ''; | |
| break; | |
| case 'S': | |
| if (player.weapon > 0) { | |
| var refund = Math.floor(WEAPONS[player.weapon][1] * 0.55); | |
| player.gold += refund; | |
| showMessage('Sold ' + WEAPONS[player.weapon][0] + ' for ' + numFormat(refund) + 'g.'); | |
| player.weapon = 0; | |
| saveGame(); | |
| } else { | |
| showMessage('Nothing to sell!'); | |
| } | |
| break; | |
| case 'N': | |
| shopPage = (shopPage + 1) % 3; | |
| break; | |
| case 'R': | |
| state = 'main'; | |
| subState = ''; | |
| break; | |
| } | |
| } | |
| function handleArmorShopInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| if (subState === 'buy_armor') { | |
| if (key === 13 || key === 10) { | |
| var idx = parseInt(inputBuffer) - 1; | |
| if (idx >= 0 && idx < ARMORS.length) { | |
| if (idx === player.armor) { | |
| showMessage('You already have that armour!'); | |
| } else if (player.armor > 0) { | |
| showMessage('Sell your current armour first!'); | |
| } else if (player.gold >= ARMORS[idx][1]) { | |
| player.gold -= ARMORS[idx][1]; | |
| player.armor = idx; | |
| showMessage('Bought ' + ARMORS[idx][0] + '!'); | |
| addLog(player.name + ' bought ' + ARMORS[idx][0] + '.'); | |
| saveGame(); | |
| } else { | |
| showMessage('Not enough gold!'); | |
| } | |
| } | |
| subState = ''; | |
| inputBuffer = ''; | |
| return; | |
| } else if (key === 8) { | |
| if (inputBuffer.length > 0) inputBuffer = inputBuffer.substring(0, inputBuffer.length - 1); | |
| return; | |
| } else if (key >= 48 && key <= 57 && inputBuffer.length < 2) { | |
| inputBuffer += String.fromCharCode(key); | |
| return; | |
| } | |
| return; | |
| } | |
| switch (ch) { | |
| case 'B': | |
| subState = 'buy_armor'; | |
| inputBuffer = ''; | |
| break; | |
| case 'S': | |
| if (player.armor > 0) { | |
| var refund = Math.floor(ARMORS[player.armor][1] * 0.55); | |
| player.gold += refund; | |
| showMessage('Sold ' + ARMORS[player.armor][0] + ' for ' + numFormat(refund) + 'g.'); | |
| player.armor = 0; | |
| saveGame(); | |
| } else { | |
| showMessage('Nothing to sell!'); | |
| } | |
| break; | |
| case 'N': | |
| shopPage = (shopPage + 1) % 3; | |
| break; | |
| case 'R': | |
| state = 'main'; | |
| subState = ''; | |
| break; | |
| } | |
| } | |
| function handleHealerInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| switch (ch) { | |
| case 'H': | |
| var maxHp = getMaxHp(); | |
| var missing = maxHp - player.hp; | |
| var cost = Math.max(missing * player.level * 2, 0); | |
| if (missing <= 0) { | |
| showMessage('You are already at full health!'); | |
| } else if (player.gold >= cost) { | |
| player.gold -= cost; | |
| player.hp = maxHp; | |
| showMessage('You are fully healed!'); | |
| saveGame(); | |
| } else { | |
| // Partial heal | |
| var canHeal = Math.floor(player.gold / (player.level * 2)); | |
| if (canHeal > 0) { | |
| player.hp = Math.min(maxHp, player.hp + canHeal); | |
| player.gold -= canHeal * player.level * 2; | |
| showMessage('Healed ' + canHeal + ' HP (spent ' + numFormat(canHeal * player.level * 2) + 'g).'); | |
| saveGame(); | |
| } else { | |
| showMessage('Not enough gold!'); | |
| } | |
| } | |
| break; | |
| case 'R': | |
| state = healerReturn; | |
| break; | |
| } | |
| } | |
| function handleBankInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| switch (ch) { | |
| case 'D': | |
| if (player.gold > 0) { | |
| player.bank += player.gold; | |
| showMessage('Deposited ' + numFormat(player.gold) + ' gold.'); | |
| player.gold = 0; | |
| saveGame(); | |
| } else { | |
| showMessage('No gold to deposit!'); | |
| } | |
| break; | |
| case 'W': | |
| if (player.bank > 0) { | |
| player.gold += player.bank; | |
| showMessage('Withdrew ' + numFormat(player.bank) + ' gold.'); | |
| player.bank = 0; | |
| saveGame(); | |
| } else { | |
| showMessage('No gold in the bank!'); | |
| } | |
| break; | |
| case 'R': | |
| state = 'main'; | |
| break; | |
| } | |
| } | |
| function handleStatsInput(key) { | |
| state = 'main'; | |
| } | |
| function handleTrainingInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| switch (ch) { | |
| case 'A': | |
| if (player.level >= 12) { | |
| showMessage('You are already max level!'); | |
| return; | |
| } | |
| var needed = LEVELS[player.level][0]; | |
| if (player.exp >= needed) { | |
| startMasterBattle(); | |
| } else { | |
| showMessage('Not enough experience!'); | |
| } | |
| break; | |
| case 'R': | |
| state = 'main'; | |
| break; | |
| } | |
| } | |
| function handleInnInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| switch (ch) { | |
| case 'F': | |
| state = 'flirt'; | |
| break; | |
| case 'G': | |
| var cost = player.level * 400; | |
| if (player.charm >= 101) cost = 0; | |
| if (player.gold >= cost) { | |
| player.gold -= cost; | |
| player.stayInn = true; | |
| if (cost === 0) { | |
| showMessage('Free room for you, charmer!'); | |
| } else { | |
| showMessage('You take a room for ' + numFormat(cost) + 'g.'); | |
| } | |
| saveGame(); | |
| } else { | |
| showMessage('Not enough gold! Need ' + numFormat(cost) + 'g.'); | |
| } | |
| break; | |
| case 'H': | |
| state = 'song'; | |
| subState = ''; | |
| break; | |
| case 'T': | |
| state = 'bartender'; | |
| break; | |
| case 'C': | |
| showMessage('The patrons mutter quietly...'); | |
| break; | |
| case 'D': | |
| state = 'daily'; | |
| dailyReturn = 'inn'; | |
| scrollOffset = 0; | |
| break; | |
| case 'R': | |
| state = 'main'; | |
| break; | |
| } | |
| } | |
| function handleFlirtInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| if (player.flirted) { | |
| state = 'inn'; | |
| return; | |
| } | |
| var flirtData; | |
| if (player.sex === 'M') { | |
| flirtData = { 'W': [1, 5], 'K': [2, 10], 'P': [4, 20], 'S': [8, 30], 'G': [16, 40], 'C': [32, 40], 'M': [100, 1000] }; | |
| } else { | |
| flirtData = { 'W': [1, 5], 'F': [2, 10], 'D': [4, 20], 'A': [8, 30], 'K': [16, 40], 'C': [32, 40], 'M': [120, 0] }; | |
| } | |
| if (ch === 'N') { | |
| state = 'inn'; | |
| return; | |
| } | |
| if (flirtData[ch]) { | |
| var needed = flirtData[ch][0]; | |
| var baseXp = flirtData[ch][1]; | |
| if (player.charm >= needed) { | |
| var xpGain = baseXp * player.level; | |
| player.exp += xpGain; | |
| player.flirted = true; | |
| if (ch === 'M') { | |
| var target = (player.sex === 'M') ? 'Violet' : 'Seth Able'; | |
| player.married = target; | |
| showMessage('You married ' + target + '! (+' + numFormat(xpGain) + ' xp)'); | |
| addLog(player.name + ' married ' + target + '!'); | |
| // Chance of child | |
| if (rand(1, 3) === 1) { | |
| player.children++; | |
| player.forestFights = getForestFights(); | |
| showMessage('You married ' + target + '! A child is born!'); | |
| addLog(player.name + ' had a child!'); | |
| } | |
| } else if (ch === 'C' || ch === 'G' || ch === 'S' || ch === 'K' || ch === 'A') { | |
| showMessage('Success! +' + numFormat(xpGain) + ' exp!'); | |
| // Chance of child for higher flirt actions | |
| if (rand(1, 5) === 1) { | |
| player.children++; | |
| player.forestFights = getForestFights(); | |
| addLog(player.name + ' had a child!'); | |
| } | |
| } else { | |
| showMessage('Success! +' + numFormat(xpGain) + ' exp!'); | |
| } | |
| saveGame(); | |
| } else { | |
| player.hp = Math.max(1, player.hp - Math.floor(getMaxHp() * 0.1)); | |
| player.flirted = true; | |
| showMessage('REJECTED! Lost some HP! Need charm ' + needed + '.'); | |
| saveGame(); | |
| } | |
| state = 'inn'; | |
| } | |
| } | |
| function handleSongInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| if (player.heardSong || subState === 'singing') { | |
| state = 'inn'; | |
| subState = ''; | |
| return; | |
| } | |
| switch (ch) { | |
| case 'A': | |
| player.heardSong = true; | |
| subState = 'singing'; | |
| var roll = rand(1, 100); | |
| if (roll <= 20) { | |
| player.hp = getMaxHp(); | |
| menuMessage = 'A healing melody! Full HP restored!'; | |
| } else if (roll <= 40) { | |
| var bonus = rand(1, 3); | |
| player.forestFights += bonus; | |
| menuMessage = 'An energizing tune! +' + bonus + ' forest fights!'; | |
| } else if (roll <= 55) { | |
| player.charm++; | |
| menuMessage = 'A charming ballad! +1 charm!'; | |
| } else if (roll <= 70) { | |
| player.playerFights++; | |
| menuMessage = 'A battle hymn! +1 player fight!'; | |
| } else if (roll <= 85) { | |
| player.gemHp++; | |
| menuMessage = 'An empowering anthem! +1 max HP!'; | |
| } else { | |
| if (player.bank > 0) { | |
| player.bank = Math.min(2000000000, player.bank * 2); | |
| menuMessage = 'INCREDIBLE! Bank account DOUBLED!'; | |
| addLog(player.name + '\'s bank was doubled by Seth!'); | |
| } else { | |
| player.hp = getMaxHp(); | |
| menuMessage = 'A soothing melody! Full HP restored!'; | |
| } | |
| } | |
| saveGame(); | |
| break; | |
| case 'R': | |
| state = 'inn'; | |
| subState = ''; | |
| break; | |
| } | |
| } | |
| function handleBartenderInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| switch (ch) { | |
| case 'G': | |
| if (player.gems >= 2) { | |
| state = 'elixir'; | |
| } else { | |
| showMessage('You need at least 2 gems!'); | |
| } | |
| break; | |
| case 'V': | |
| showMessage('"She likes folks who help the elderly in the forest."'); | |
| break; | |
| case 'R': | |
| state = 'inn'; | |
| break; | |
| } | |
| } | |
| function handleElixirInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| switch (ch) { | |
| case 'S': | |
| if (player.gems >= 2) { | |
| player.gems -= 2; | |
| player.gemAtk++; | |
| showMessage('+1 Attack Strength!'); | |
| saveGame(); | |
| } else { | |
| showMessage('Need 2 gems!'); | |
| } | |
| break; | |
| case 'V': | |
| if (player.gems >= 2) { | |
| player.gems -= 2; | |
| player.gemDef++; | |
| showMessage('+1 Defense Strength!'); | |
| saveGame(); | |
| } else { | |
| showMessage('Need 2 gems!'); | |
| } | |
| break; | |
| case 'H': | |
| if (player.gems >= 2) { | |
| player.gems -= 2; | |
| player.gemHp++; | |
| showMessage('+1 Max Hit Points!'); | |
| saveGame(); | |
| } else { | |
| showMessage('Need 2 gems!'); | |
| } | |
| break; | |
| case 'R': | |
| state = 'bartender'; | |
| break; | |
| } | |
| } | |
| function handleDailyInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| var maxShow = 14; | |
| var maxScroll = Math.max(0, dailyLog.length - maxShow); | |
| if (ch === 'U') { | |
| scrollOffset = Math.min(scrollOffset + 3, maxScroll); | |
| } else if (ch === 'D') { | |
| scrollOffset = Math.max(scrollOffset - 3, 0); | |
| } else { | |
| state = dailyReturn; | |
| scrollOffset = 0; | |
| } | |
| } | |
| function handleForestEventInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| if (subState === 'oldman') { | |
| if (ch === 'H') { | |
| var gold = player.level * 500; | |
| player.gold += gold; | |
| player.charm++; | |
| // Note: forest fight already consumed when entering the forest event | |
| menuMessage = 'You helped the old man! +' + numFormat(gold) + ' gold, +1 charm!'; | |
| subState = ''; | |
| addLog(player.name + ' helped an old man in the forest.'); | |
| saveGame(); | |
| } else if (ch === 'I') { | |
| menuMessage = 'You walk away feeling slightly ashamed.'; | |
| subState = ''; | |
| } | |
| } else if (subState === 'hag') { | |
| if (ch === 'G') { | |
| if (player.gems > 0) { | |
| player.gems--; | |
| player.hp = getMaxHp(); | |
| player.gemHp++; | |
| menuMessage = 'Full heal and +1 max HP!'; | |
| saveGame(); | |
| } else { | |
| player.hp = 1; | |
| menuMessage = 'No gems! The hag curses you! HP = 1!'; | |
| saveGame(); | |
| } | |
| subState = ''; | |
| } else if (ch === 'I') { | |
| menuMessage = 'The hag vanishes in a puff of smoke.'; | |
| subState = ''; | |
| } | |
| } else if (subState === 'fairy') { | |
| if (ch === 'A') { | |
| var blessing = rand(1, 4); | |
| if (blessing === 1 && !player.horse) { | |
| player.horse = true; | |
| menuMessage = 'A fairy gives you a horse!'; | |
| player.forestFights = getForestFights(); | |
| addLog(player.name + ' received a horse from fairies!'); | |
| } else if (blessing === 1 && player.horse) { | |
| // Already have a horse, give XP instead | |
| var xp2 = player.level * 150; | |
| player.exp += xp2; | |
| menuMessage = 'A fairy blesses your steed! +' + numFormat(xp2) + ' exp!'; | |
| } else if (blessing === 2) { | |
| var heal = Math.floor(getMaxHp() * 0.3); | |
| player.hp = Math.min(getMaxHp(), player.hp + heal); | |
| menuMessage = 'A fairy heals you for ' + heal + ' HP!'; | |
| } else if (blessing === 3) { | |
| var gems = rand(1, 2); | |
| player.gems += gems; | |
| menuMessage = 'A fairy gives you ' + gems + ' gem(s)!'; | |
| } else { | |
| var xp = player.level * 100; | |
| player.exp += xp; | |
| menuMessage = 'A fairy blesses you! +' + numFormat(xp) + ' exp!'; | |
| } | |
| subState = ''; | |
| saveGame(); | |
| } else if (ch === 'C') { | |
| if (rand(1, 3) === 1) { | |
| player.fairy = true; | |
| menuMessage = 'You caught a fairy! (Extra life!)'; | |
| addLog(player.name + ' caught a fairy!'); | |
| } else { | |
| player.hp = 1; | |
| menuMessage = 'The fairies are angry! HP = 1!'; | |
| } | |
| subState = ''; | |
| saveGame(); | |
| } | |
| } else { | |
| // Generic event, press any key | |
| state = 'forest'; | |
| subState = ''; | |
| } | |
| } | |
| function handleDragonInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| if (subState === 'dragonwin' || subState === 'dragonlose') { | |
| state = 'main'; | |
| currentEnemy = null; | |
| subState = ''; | |
| return; | |
| } | |
| if (subState !== 'dragon_fighting') return; | |
| switch (ch) { | |
| case 'A': | |
| var killed = playerAttack(1.0); | |
| if (killed) { | |
| slayDragon(); | |
| return; | |
| } | |
| var died = enemyAttack(); | |
| if (died) { | |
| if (player.fairy) { | |
| player.fairy = false; | |
| player.hp = getMaxHp(); | |
| menuMessage += ' A fairy saved you!'; | |
| } else { | |
| subState = 'dragonlose'; | |
| player.hp = getMaxHp(); // dragon is merciful, you live | |
| player.forestFights = 0; | |
| menuMessage = 'The dragon spares your life...'; | |
| saveGame(); | |
| } | |
| } | |
| break; | |
| case 'R': | |
| state = 'forest'; | |
| currentEnemy = null; | |
| subState = ''; | |
| showMessage('You flee from the dragon!'); | |
| break; | |
| case 'S': | |
| if (player.skillUses > 0) { | |
| player.skillUses--; | |
| var mult = 2.0; | |
| if (player.skillClass === 'M') { | |
| var pts = player.skillMystic; | |
| if (pts >= 16) mult = 3.0; | |
| else if (pts >= 8) mult = 2.0; | |
| else mult = 1.5; | |
| } | |
| var killed2 = playerAttack(mult); | |
| menuMessage = 'Special attack! ' + menuMessage; | |
| if (killed2) { | |
| slayDragon(); | |
| return; | |
| } | |
| var died2 = enemyAttack(); | |
| if (died2) { | |
| if (player.fairy) { | |
| player.fairy = false; | |
| player.hp = getMaxHp(); | |
| menuMessage += ' A fairy saved you!'; | |
| } else { | |
| subState = 'dragonlose'; | |
| player.hp = getMaxHp(); | |
| player.forestFights = 0; | |
| menuMessage = 'The dragon spares your life...'; | |
| saveGame(); | |
| } | |
| } | |
| } | |
| break; | |
| } | |
| } | |
| function handleDarkTavernInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| switch (ch) { | |
| case 'G': | |
| state = 'gamble'; | |
| subState = ''; | |
| break; | |
| case 'C': | |
| // Change class | |
| if (player.skillClass === 'D') { | |
| player.skillClass = 'M'; | |
| showMessage('You are now studying Mystical Skills!'); | |
| } else if (player.skillClass === 'M') { | |
| player.skillClass = 'T'; | |
| showMessage('You are now studying Thief Skills!'); | |
| } else { | |
| player.skillClass = 'D'; | |
| showMessage('You are now studying Death Knight Skills!'); | |
| } | |
| player.skillUses = getSkillUses(); | |
| saveGame(); | |
| break; | |
| case 'R': | |
| state = 'forest'; | |
| break; | |
| } | |
| } | |
| function handleGambleInput(key) { | |
| var ch = String.fromCharCode(key).toUpperCase(); | |
| if (subState === 'gamble_result') { | |
| subState = ''; | |
| state = 'gamble'; | |
| return; | |
| } | |
| if (subState === 'gamble_bet') { | |
| if (key === 13 || key === 10) { | |
| var bet = parseInt(inputBuffer); | |
| if (isNaN(bet) || bet <= 0) { | |
| subState = ''; | |
| inputBuffer = ''; | |
| return; | |
| } | |
| bet = Math.min(bet, player.gold); | |
| if (bet <= 0) { | |
| showMessage('No gold to bet!'); | |
| subState = ''; | |
| inputBuffer = ''; | |
| return; | |
| } | |
| if (rand(1, 2) === 1) { | |
| player.gold += bet; | |
| menuMessage = 'You WIN ' + numFormat(bet) + ' gold!'; | |
| } else { | |
| player.gold -= bet; | |
| menuMessage = 'You LOSE ' + numFormat(bet) + ' gold!'; | |
| } | |
| subState = 'gamble_result'; | |
| inputBuffer = ''; | |
| saveGame(); | |
| return; | |
| } else if (key === 8) { | |
| if (inputBuffer.length > 0) inputBuffer = inputBuffer.substring(0, inputBuffer.length - 1); | |
| return; | |
| } else if (key >= 48 && key <= 57 && inputBuffer.length < 10) { | |
| inputBuffer += String.fromCharCode(key); | |
| return; | |
| } else if (ch === 'R') { | |
| subState = ''; | |
| inputBuffer = ''; | |
| return; | |
| } | |
| return; | |
| } | |
| switch (ch) { | |
| case 'G': | |
| subState = 'gamble_bet'; | |
| inputBuffer = ''; | |
| break; | |
| case 'R': | |
| state = 'dark_tavern'; | |
| subState = ''; | |
| break; | |
| } | |
| } | |
| // --- QUICKSERVE REQUIRED FUNCTIONS --- | |
| function getName() { | |
| return 'L.O.R.D.'; | |
| } | |
| function onConnect() { | |
| state = 'title'; | |
| player = null; | |
| inputBuffer = ''; | |
| menuMessage = ''; | |
| msgTimer = 0; | |
| currentEnemy = null; | |
| shopPage = 0; | |
| scrollOffset = 0; | |
| dailyLog = []; | |
| subState = ''; | |
| healerReturn = 'main'; | |
| dailyReturn = 'main'; | |
| animFrame = 0; | |
| } | |
| function onUpdate() { | |
| // Tick message timer (only clear messages set by showMessage) | |
| if (msgTimer > 0) { | |
| msgTimer--; | |
| if (msgTimer <= 0) menuMessage = ''; | |
| } | |
| // Draw current state | |
| switch (state) { | |
| case 'title': | |
| drawTitleScreen(); | |
| break; | |
| case 'loading': | |
| clearScreen(); | |
| drawText('Loading...', C.WHITE, 20, 10); | |
| break; | |
| case 'create': | |
| drawCharCreate(); | |
| break; | |
| case 'main': | |
| drawMainMenu(); | |
| break; | |
| case 'forest': | |
| drawForest(); | |
| break; | |
| case 'battle': | |
| drawBattle(); | |
| break; | |
| case 'master_battle': | |
| drawMasterBattle(); | |
| break; | |
| case 'weapon_shop': | |
| drawWeaponShop(); | |
| break; | |
| case 'armor_shop': | |
| drawArmorShop(); | |
| break; | |
| case 'healer': | |
| drawHealer(); | |
| break; | |
| case 'bank': | |
| drawBank(); | |
| break; | |
| case 'stats': | |
| drawStats(); | |
| break; | |
| case 'training': | |
| drawTraining(); | |
| break; | |
| case 'inn': | |
| drawInn(); | |
| break; | |
| case 'flirt': | |
| drawFlirt(); | |
| break; | |
| case 'song': | |
| drawSong(); | |
| break; | |
| case 'bartender': | |
| drawBartender(); | |
| break; | |
| case 'elixir': | |
| drawElixir(); | |
| break; | |
| case 'daily': | |
| drawDailyNews(); | |
| break; | |
| case 'forest_event': | |
| drawForestEvent(); | |
| break; | |
| case 'dragon': | |
| drawDragonFight(); | |
| break; | |
| case 'dark_tavern': | |
| drawDarkTavern(); | |
| break; | |
| case 'gamble': | |
| drawGamble(); | |
| break; | |
| } | |
| } | |
| function onInput(key) { | |
| switch (state) { | |
| case 'title': | |
| handleTitleInput(key); | |
| break; | |
| case 'create': | |
| handleCreateInput(key); | |
| break; | |
| case 'main': | |
| handleMainInput(key); | |
| break; | |
| case 'forest': | |
| handleForestInput(key); | |
| break; | |
| case 'battle': | |
| handleBattleInput(key); | |
| break; | |
| case 'master_battle': | |
| handleMasterBattleInput(key); | |
| break; | |
| case 'weapon_shop': | |
| handleWeaponShopInput(key); | |
| break; | |
| case 'armor_shop': | |
| handleArmorShopInput(key); | |
| break; | |
| case 'healer': | |
| handleHealerInput(key); | |
| break; | |
| case 'bank': | |
| handleBankInput(key); | |
| break; | |
| case 'stats': | |
| handleStatsInput(key); | |
| break; | |
| case 'training': | |
| handleTrainingInput(key); | |
| break; | |
| case 'inn': | |
| handleInnInput(key); | |
| break; | |
| case 'flirt': | |
| handleFlirtInput(key); | |
| break; | |
| case 'song': | |
| handleSongInput(key); | |
| break; | |
| case 'bartender': | |
| handleBartenderInput(key); | |
| break; | |
| case 'elixir': | |
| handleElixirInput(key); | |
| break; | |
| case 'daily': | |
| handleDailyInput(key); | |
| break; | |
| case 'forest_event': | |
| handleForestEventInput(key); | |
| break; | |
| case 'dragon': | |
| handleDragonInput(key); | |
| break; | |
| case 'dark_tavern': | |
| handleDarkTavernInput(key); | |
| break; | |
| case 'gamble': | |
| handleGambleInput(key); | |
| break; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment