Apps Home
|
My Uploads
|
Create an App
Dungeon Crawl
Author:
lemons_
Description
Source Code
Launch App
Current Users
Created by:
Lemons_
App Images
// Enemy class Enemy { constructor(level, health) { const options = Enemy.levels[level] const option = options[Math.floor(Math.random() * options.length)] this.name = option.name this.type = option.type this.initHealths(health, option.count) } initHealths(health, count) { this.healths = [] const healthPerEnemy = Math.floor(health / count) for (let i = 0; i < count - 1; i++) { this.healths.push(healthPerEnemy) health -= healthPerEnemy } this.healths.push(health) } removeDead() { this.healths = this.healths.filter((hp) => hp > 0) } applyDamage(amount, position=0) { const damage = Math.min(this.healths[position], amount) this.healths[position] -= damage return damage } get isDefeated() { for (let i = 0; i < this.healths.length; i++) { if (this.healths[i] > 0) { return false } } return true } get count() { return this.healths.length } get healthsStr() { return this.healths.join(", ") } get totalHealthRemaining() { return this.healths.reduce((a, h) => (a + h), 0) } health(i) { return this.healths[i] } image(i) { return 'c9074406-2ebc-4783-a9d5-bb63e3533a8f' } } Enemy.types = { HUMANOID: {name: "Humanoid"}, BEAST: {name: "Beast"}, MONSTER: {name: "Monster"}, UNDEAD: {name: "Undead"}, BOSS: {name: "Boss"}, } Enemy.levels = { // Max count == 7 if they are to fit in space on panel. 1: [ {name: "Kobold", count: 5, type: Enemy.types.HUMANOID}, {name: "Dire Rat", count: 3, type: Enemy.types.BEAST}, {name: "Slime Mold", count: 7, type: Enemy.types.MONSTER}, {name: "Zombie", count: 4, type: Enemy.types.UNDEAD}, ], 2: [ {name: "Goblin", count: 4, type: Enemy.types.HUMANOID}, {name: "Giant Spider", count: 5, type: Enemy.types.BEAST}, {name: "Jelly Cube", count: 1, type: Enemy.types.MONSTER}, {name: "Skeleton", count: 3, type: Enemy.types.UNDEAD}, ], 3: [ {name: "Drow", count: 4, type: Enemy.types.HUMANOID}, {name: "Dire Wolf", count: 5, type: Enemy.types.BEAST}, {name: "Troll", count: 1, type: Enemy.types.MONSTER}, {name: "Ghoul", count: 4, type: Enemy.types.UNDEAD}, ], 4: [ {name: "Gnoll", count: 3, type: Enemy.types.HUMANOID}, {name: "Owlbear", count: 2, type: Enemy.types.BEAST}, {name: "Stone Golem", count: 2, type: Enemy.types.MONSTER}, {name: "Vampire", count: 1, type: Enemy.types.UNDEAD}, ], FINAL: [ {name: "Lich", count: 1, type: Enemy.types.BOSS}, {name: "Dragon", count: 1, type: Enemy.types.BOSS}, {name: "Beholder", count: 2, type: Enemy.types.BOSS}, {name: "Mind Flayer", count: 3, type: Enemy.types.BOSS}, ] } class Class { constructor(name) { this.data = Class.config[name] } get icon() { return cb.settings.useIcons === "Yes" ? `:lemons_dc_class_${this.data.icon}` : `(${this.name})` } get image() { return this.data.image } get name() { return this.data.name } get action() { return this.data.action } get bonusStr() { if (this.data.bonus) { return ` (Bonus vs ${this.data.bonus.map((b) => b.name).join(", ")})` } else { return "" } } hasBonusVs(enemyType) { return this.data.bonus && this.data.bonus.includes(enemyType) } get specialStr() { switch (this.special) { case "BACKSTAB": return " (Backstab: Attacks the enemy at the back)" case "BLAST": return " (Blast: Damage spreads evenly over all enemies)" case "PIERCE": return " (Pierce: Extra damage carries over to second enemy)" default: return "" } } get description() { return `${"AEIOU".search(this.name[0]) !== -1 ? "an" : "a"} ${this.name}${this.bonusStr}${this.specialStr}` } get upgrade() { return this.data.upgrade } get special() { return this.data.special } get canUpgrade() { return !!this.data.upgrade } } Class.config = { ARCHER: {name: "Archer", icon: "archer", action: "shoot", special: "PIERCE", upgrade: "RANGER"}, RANGER: {name: "Ranger", icon: "ranger", bonus: [Enemy.types.BOSS], special: "PIERCE", action: "impale"}, KNIGHT: {name: "Knight", icon: "knight", bonus: [Enemy.types.HUMANOID], action: "bash", upgrade: "CHAMPION"}, CHAMPION: {name: "Champion", icon: "champion", bonus: [Enemy.types.HUMANOID, Enemy.types.BOSS], action: "smash"}, WIZARD: {name: "Wizard", icon: "wizard", action: "burn", special: "BLAST", upgrade: "ARCHMAGE"}, ARCHMAGE: {name: "Archmage", icon: "champion", bonus: [Enemy.types.BOSS], special: "BLAST", action: "electrocute"}, PRIEST: {name: "Priest", icon: "priest", bonus: [Enemy.types.UNDEAD], action: "bless", upgrade: "BISHOP"}, BISHOP: {name: "Heirophant", icon: "bishop", bonus: [Enemy.types.UNDEAD, Enemy.types.BOSS], action: "smite"}, ROGUE: {name: "Rogue", icon: "rogue", action: "stab", special: "BACKSTAB", upgrade: "ASSASSIN"}, ASSASSIN: {name: "Assassin", icon: "assassin", bonus: [Enemy.types.BOSS], special: "BACKSTAB", action: "assassinate"}, PIRATE: {name: "Pirate", icon: "pirate", bonus: [Enemy.types.MONSTER], action: "slash", upgrade: "CAPTAIN"}, CAPTAIN: {name: "Captain", icon: "captain", bonus: [Enemy.types.MONSTER, Enemy.types.BOSS], action: "brutally slash"}, BARD: {name: "Bard", icon: "bard", bonus: [Enemy.types.BEAST], action: "entrances", upgrade: "DRUID"}, DRUID: {name: "Druid", icon: "druid", bonus: [Enemy.types.BEAST, Enemy.types.BOSS], action: "blasts", image: "62c799a2-08c5-4460-b6a4-0fb8468fc0f8"}, PEASANT: {name: "Peasant", icon: "peasant", action: "slap"}, // Default. DUNGEON_MASTER: {name: "Dungeon Master", icon: "dungeon_master"}, // Model. } // Classes class Classes { constructor() { this.classes = {} for (const classID of Object.keys(Class.config)) { this.classes[classID] = new Class(classID) } } get(id) { return this.classes[id] } } // Player class Player { constructor(name) { this.name = name this.xp = 0 // Amount of damage dealt this.tipped = 0 // Amount of tokens tipped this.klass = new Class(this.isBroadcaster ? "DUNGEON_MASTER" : "PEASANT") } get icon() { return this.klass.icon } attack(enemy, tipAmount) { this.tipped += tipAmount // Damage = tip * 0.5 to * 1.5 (or tip * 1 to * 2 with bonus) const baseDamage = Math.floor(tipAmount / 2) let damage = baseDamage + roll(baseDamage, 2) if (this.klass.hasBonusVs(enemy.type)) { damage += baseDamage } let effectiveDamage = 0 switch (this.klass.special) { case "BACKSTAB": // Attack the last one in the line! effectiveDamage += enemy.applyDamage(damage, enemy.count - 1) break case "BLAST": // Spead damage over all enemies const individualDamage = Math.round(damage / enemy.count) for (let i = 0; i < enemy.count; i++) { effectiveDamage += enemy.applyDamage(individualDamage, i) } break case "PIERCE": // Attack first and second. let damageRemaining = damage for (let i = 0; i < Math.min(2, enemy.count); i++) { const damageApplied = enemy.applyDamage(damageRemaining, i) damageRemaining -= damageApplied effectiveDamage += damageApplied } break default: // Attack front enemy. effectiveDamage += enemy.applyDamage(damage, 0) break } const overkill = effectiveDamage < damage ? ` (${damage - effectiveDamage} excess damage wasted!)` : "" this.xp += effectiveDamage print(`${this.icon} ${this.name} ${pluralize(this.klass.action)} the ${enemy.name} for ${effectiveDamage} damage${overkill}`) if (this.klass.canUpgrade && this.xp >= cb.settings.xpToAdvance) { this.setClass(this.klass.upgrade) } enemy.removeDead() return effectiveDamage } setClass(klassID) { this.klass = new Class(klassID) this.xp = Math.max(0, this.xp - cb.settings.xpToAdvance) if (this.klass.canUpgrade) { print(`You train very hard and become ${this.klass.description}!`, this.name) } else { print(`You have fought so hard that you have levelled up and become ${this.klass.description}!`, this.name) } } display(player) { print(`You are ${this.klass.description} and have ${this.xp}/${cb.settings.xpToAdvance} XP (tipped a total of ${this.tipped} tokens in this game)`, player.name) } get isBroadcaster() { return this.name == cb.room_slug } get isMod() { return false // TODO: implement } get isAdmin() { return this.isBroadcaster || this.isMod } } // Players class Players { constructor() { this.players = {} } get(name) { if (!this.players[name]) { this.players[name] = new Player(name) } return this.players[name] } } // Leaderboard class Leaderboard { constructor() { this.hits = [] this.heros = [] } displayHeros(player) { const lines = [title("Hall of Heros")] for (let i = 0; i < this.heros.length; i++) { lines.push(`${i + 1}) ${this.describeHero(i)} xp`) } print(lines.join("\n"), player.name) } displayHits(player) { const lines = [title("Greatest Hits")] for (let i = 0; i < this.hits.length; i++) { lines.push(`${i + 1}) ${this.describeHit(i)} damage`) } print(lines.join("\n"), player.name) } describeHit(n) { const hit = this.hits[n] return `${hit.player.icon} ${hit.player.name}: ${hit.damage}` } get greatestHero() { if (this.heros.length === 0) { return null } return this.heros[0] } record(player, damage) { this.hits.push({player: player, damage: damage}) this.hits.sort((a, b) => b.damage - a.damage) this.hits = this.hits.slice(0, cb.settings.leaderboardSize) if (!this.heros.find((p) => p.name == player.name)) { this.heros.push(player) } this.heros.sort((a, b) => b.xp - a.xp) this.heros = this.heros.slice(0, cb.settings.leaderboardSize) } } // Game class Game { constructor() { this.players = new Players() this.classes = new Classes() this.leaderboard = new Leaderboard() this.initLevels() this.currentLevelIndex = -1 this.delveDeeper() } level() { return this.levels[this.currentLevelIndex] } update() { if (this.isPlaying && this.enemy.isDefeated) { this.delveDeeper() } cb.drawPanel() } initLevels() { this.levels = [] for (let i = 1; i < cb.settings.numLevels; i++) { this.addLevel(i) } this.addLevel("FINAL") } addLevel(id) { this.levels.push({ enemy: new Enemy(id, cb.settings[`level${id}health`]), reward: cb.settings[`level${id}reward`].replace("MODEL", cb.room_slug), }) } showLevels(player) { const lines = [title("Levels")] for (let i = 0; i < this.numLevels; i++) { const enemy = this.levels[i].enemy, reward = this.levels[i].reward lines.push(`${i + 1}) ${enemy.name} (${enemy.type.name}) - ${enemy.healthsStr} (${enemy.totalHealthRemaining}) - ${reward}`) } print(lines.join("\n"), player.name) } delveDeeper() { this.currentLevelIndex += 1 if (this.currentLevelIndex >= this.levels.length) { print(`Having defeated the ${this.enemy.name}, the Dungeon Crawl is complete!`) print(`Rescued ${this.reward}`, null, "#f00", "#fff") this.level = null cb.cancelTimeout(remindEvent) } else { if (this.currentLevelIndex == 0) { print(`Collecting all of our wits and valor, we delve into the dungeon, in order to rescue ${cb.room_slug}!`) } else { print(`Having defeated all foes on this level, we delve deeper into the dungeon!`) print(`Found ${this.reward}`, null, "#f00", "#fff") } this.level = this.levels[this.currentLevelIndex] } } get enemy() { return this.level.enemy } get reward() { return this.level.reward } get isPlaying() { return this.enemy !== null } get numLevels() { return this.levels.length } drawPanel() { const progress = this.isPlaying ? `${this.currentLevelIndex + 1} of ${this.numLevels}` : "Complete" const hero = this.leaderboard.greatestHero const panel = { template: "image_template", layers: [ // Background { type: "image", fileID: "9dff9d71-5a31-4380-b39a-6184771e7263" }, // Labels { type: "text", text: "Dungeon Crawl:", top: 6, left: 6, color: 'orange', }, { type: "text", text: `${progress} (${this.reward})`, top: 6, left: 90, color: "white", }, { type: "text", text: `Greatest Hero`, top: 52, left: 6, color: 'orange', }, { type: "image", fileID: '296288e6-700a-427d-9e73-83ecefb66a38', top: 52, left: 90 }, { type: "text", text: hero ? `${hero.name}: ${hero.xp}` : "No-one is brave enough!", top: 52, left: 90 + 24, color: "white", }, ] } this.addPlayer(panel.layers) this.addEnemys(panel.layers) return panel } addPlayer(layers) { layers.push({ type: 'image', fileID: this.klass("DRUID").image, left: 6, top: 19, }) } addEnemys(layers) { for (let i = 0; i < this.enemy.count; i++) { const left = 90 + i * 24 layers.push({ type: 'image', fileID: this.enemy.image(i), left: left, top: 19, }) if (i === 0) { layers.push({ type: 'image', fileID: "b5284fa2-b8df-46c0-96f1-de23ea3f20a7", left: left, top: 19, }) layers.push({ type: 'image', fileID: "fd200874-1d61-414a-98d7-73a72c7bc0e2", left: left - 24, top: 19, }) layers.push({ type: 'text', text: "12", left: left + 7, top: 19 + 6, color: "white", 'font-size': 8 }) } layers.push({ type: 'text', text: this.enemy.health(i), left: left + 5 + 1, top: 43 + 1, color: 'black', 'font-size': 9, }) layers.push({ type: 'text', text: this.enemy.health(i), left: left + 5, top: 43, color: 'lightgreen', 'font-size': 9, }) } } player(name) { return this.players.get(name) } klass(id) { return this.classes.get(id) } showHelp(player) { const s = cb.settings const lines = [ title("Help"), `Initially, you are a lowly ${this.klass("PEASANT").description}`, `If you learn a class and gain ${cb.settings.xpToAdvance} XP, you will advance to a better class!`, `You can change your class at any time, but you lose ${cb.settings.xpToAdvance} XP!`, `TIP ${s.costArcher} to become ${this.klass("ARCHER").description}!`, `TIP ${s.costKnight} to become ${this.klass("KNIGHT").description}!`, `TIP ${s.costWizard} to become ${this.klass("WIZARD").description}!`, `TIP ${s.costPriest} to become ${this.klass("PRIEST").description}!`, `TIP ${s.costRogue} to become ${this.klass("ROGUE").description}!`, `TIP ${s.costPirate} to become ${this.klass("PIRATE").description}!`, `TIP ${s.costBard} to become ${this.klass("BARD").description}!`, "TIP any other amount to make attacks against the current foe or foes!", "/status - Get information about your class, bonuses, special abilities and XP", "/heros - Get information about who are the greatest heros (by XP)", "/hits - Get information about the best hits (by damage)", "/help - Get this message", ] if (player.isAdmin) { lines.push("/restart - restart the game (ADMIN ONLY)") lines.push("/levels - show what is on each level (ADMIN ONLY)") } print(lines.join("\n"), player.name) } } let game, remindDelayMs, remindEvent // print() const print = cb.sendNotice // roll() const roll = (sides, count=1) => { let total = 0 for (let i = 0; i < count; i++) { total += Math.floor(Math.random() * sides) + 1 } return total } const title = (label) => { return `--- Dungeon Crawl: ${label} ---` } const reset = () => { game = new Game() remindEvent = cb.setTimeout(remind, remindDelayMs) } const remind = () => { cb.chatNotice("We are playing Dungeon Crawl by lemons_! (/help for instructions)") remindEvent = cb.setTimeout(remind, remindDelayMs) } const pluralize = (word) => { return `${word}${(word.endsWith("sh") || word.endsWith("s")) ? "es" : "s"}` } // onEnter() cb.onEnter((user) => { if (!game.isPlaying) { return } print(`Welcome ${user.user}, we are playing Dungeon Crawl! (/help for instructions)`, user.user) }) // onStart() cb.onStart((user) => { remindDelayMs = cb.settings.reminderDelayMinutes * 60 * 1000 reset() }) // onDrawPanel() cb.onDrawPanel(() => { if (game) { return game.drawPanel() } else { return { template: "3_rows_of_labels", } } }) // onTip() cb.onTip((tip) => { if (!game.isPlaying) { return } const amount = tip.amount const name = tip.from_user const s = cb.settings const player = game.player(name) switch (amount) { case s.costArcher: player.setClass("ARCHER") break case s.costKnight: player.setClass("KNIGHT") break case s.costWizard: player.setClass("WIZARD") break case s.costPriest: player.setClass("PRIEST") break case s.costRogue: player.setClass("ROGUE") break case s.costPirate: player.setClass("PIRATE") break case s.costBard: player.setClass("BARD") break default: damage = player.attack(game.enemy, amount) game.leaderboard.record(player, damage) game.update() break } }) // onMessage cb.onMessage((msg) => { const message = msg.m const player = game.player(msg.user) const s = cb.settings, c = Class.config msg.m = `${player.icon} ${msg.m}` if (!game.isPlaying) { return msg } if (message === "/help") { game.showHelp(player) msg['X-Spam'] = true } else if (message === "/restart" && player.isAdmin) { reset() msg['X-Spam'] = true } else if (message === "/levels" && player.isAdmin) { game.showLevels(player) msg['X-Spam'] = true } else if (message === "/status") { if (player.isAdmin) { otherPlayer = game.player(message.slice(10).trim()) otherPlayer.display(player) } else { player.display(player) } msg['X-Spam'] = true } else if (message === "/heros" || message === "/heroes") { game.leaderboard.displayHeros(player) msg['X-Spam'] = true } else if (message === "/hits") { game.leaderboard.displayHits(player) msg['X-Spam'] = true } }) cb.settings_choices = [ {name: 'costArcher', type: 'int', minValue: 1, defaultValue: 61, label: "Token cost to become an Archer"}, {name: 'costKnight', type: 'int', minValue: 1, defaultValue: 62, label: "Token cost to become a Knight"}, {name: 'costWizard', type: 'int', minValue: 1, defaultValue: 63, label: "Token cost to become a Wizard"}, {name: 'costPriest', type: 'int', minValue: 1, defaultValue: 64, label: "Token cost to become a Priest"}, {name: 'costRogue', type: 'int', minValue: 1, defaultValue: 65, label: "Token cost to become a Rogue"}, {name: 'costPirate', type: 'int', minValue: 1, defaultValue: 66, label: "Token cost to become a Pirate"}, {name: 'costBard', type: 'int', minValue: 1, defaultValue: 67, label: "Token cost to become a Bard"}, {name: 'xpToAdvance', type: 'int', minValue: 1, defaultValue: 500, label: "XP (damage dealt) to advance class"}, {name: 'level1health', type: 'int', minValue: 1, defaultValue: 250, label: "Health of level 1 foe(s)"}, {name: 'level1reward', type: 'str', defaultValue: "Flask of Oil: Oil boobs!", label: "Treasure for defeating level 1"}, {name: 'level2health', type: 'int', minValue: 1, defaultValue: 500, label: "Health of level 2 foe(s)"}, {name: 'level2reward', type: 'str', defaultValue: "Fireball Scroll: Remove item of clothing!", label: "Treasure for defeating level 2"}, {name: 'level3health', type: 'int', minValue: 1, defaultValue: 750, label: "Health of level 3 foe(s)"}, {name: 'level3reward', type: 'str', defaultValue: "Scroll of Truesight: Get naked!", label: "Treasure for defeating level 3"}, {name: 'level4health', type: 'int', minValue: 1, defaultValue: 1000, label: "Health of level 4 foe(s)"}, {name: 'level4reward', type: 'str', defaultValue: "", label: "Treasure for defeating level 4"}, {name: 'levelFINALhealth', type: 'int', minValue: 1, defaultValue: 2000, label: "Health of final level boss(es)"}, {name: 'levelFINALreward', type: 'str', defaultValue: "MODEL: Cum show!", label: "Person rescued/reward for defeating final level"}, {name: 'leaderBoardSize', type: 'int', defaultValue: 8, minValue: 3, label: "Size of leaderboard (/heros list)"}, {name: 'numLevels', type: 'int', defaultValue: 3, minValue: 1, maxValue: 5, label: "Number of levels (goals) including the final level (goal): 1-5"}, {name: 'useIcons', type: 'choice', defaultValue: "Yes", choice1: "Yes", choice2: "No", label: "Do we want to show icons rather than just text?"}, {name: 'reminderDelayMinutes', type: 'int', defaultValue: 4, minValue: 1, label: "Remind users every X minutes"}, ]
© Copyright Chaturbate 2011- 2024. All Rights Reserved.