Skip to content

Instantly share code, notes, and snippets.

@Thorium
Last active March 17, 2026 06:44
Show Gist options
  • Select an option

  • Save Thorium/8a79b24e150728231ae18c4c35f85f9e to your computer and use it in GitHub Desktop.

Select an option

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.
// ============================================================================
// 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