Apps Home
|
My Uploads
|
Create an App
unaadevtest
Author:
unaa
Description
Source Code
Launch App
Current Users
Created by:
Unaa
App Images
/* Barrier Battle App ___ _ ___ __ __ __ * / _ )___ _________(_)__ ____/ _ )___ _/ /_/ /_/ /__ * Let your viewers break down barriers from around your goals. / _ / _ `/ __/ __/ / -_) __/ _ / _ `/ __/ __/ / -_) * - by Unaa. /____/\_,_/_/ /_/ /_/\__/_/ /____/\_,_/\__/\__/_/\_*/ "use strict"; if (typeof cb === "undefined") { // IDE sanity. cb = {}; } // LIVE const PANEL_BACKGROUND_IMAGE_1_GOAL = "323d26f3-d892-4efe-9abc-5073a2170566"; const PANEL_BACKGROUND_IMAGE_2_GOAL = "e532b196-72e8-46eb-833f-fe13e8e8c2b4"; const PANEL_BACKGROUND_IMAGE_3_GOAL = "0448ee0f-25ec-4890-aff8-848388ca7051"; class Game { constructor() { // Initialize services when the game starts. Users.init(); GoalState.init(); Chat.init(); Panel.init(); // Register chat commands to enable listening for these commands and also filter any chat messages where any of // these commands have been executed to keep the chat clean. The user executing the command will still be able // to see the message in chat. Chat.registerChatCommand("class"); Chat.registerChatCommand("classinfo"); Chat.registerChatCommand("scores"); // Send a welcome message to a user that joined the room after the game was started. Chat.on("user-joined", (user) => { this.sendWelcomeMessage(user); }); // Send a message if a user re-joined the room after leaving during the game. Chat.on("user-rejoined", (user) => { if (user.userClass) { Chat.sendMessageToUser([ `Welcome back, ${user.name}! You are currently participating in a game of BARRIER BATTLE and are`, `playing as a ${user.userClass.name}. Get smashin'!` ].join(" "), user.name); } else { this.sendWelcomeMessage(user); } }); // Proxy the command to the user instance that executed it. Chat.on("command", (user, cmd, args) => user.onCommand(cmd, args)); // Send intro message to all users that are watching right now. this.sendWelcomeMessage(); } sendWelcomeMessage(user) { let send = (message, color, bgColor, weight) => { if (user) { Chat.sendMessageToUser(message, user.name, color, bgColor, weight); } else { Chat.sendMessage(message, color, bgColor, weight); } }; if (user) { send(`Welcome ${user.name}! We are currently playing BARRIER BATTLE!`, `#ffffff`, `#412C7C`, `bolder`); } else { send(`----------------[ Lets play BARRIER BATTLE! ]----------------`, `#ffffff`, `#412C7C`, `bolder`); } Object.keys(Settings.classes).forEach((c) => { send(`type "/class ${c}" to play as a ${c}, or "/classinfo ${c}" to see detailed information about this class.`, "#574b73", undefined, "bolder"); }); send("", "#fff", "#fff"); GoalState.goals.forEach((goal) => { send(` - Tip ${goal.trigger} tokens to attack the barrier around "${goal.title}".`, "#9b82ba", undefined, "normal"); }); send("", "#fff", "#fff"); send(`If you start tipping for a goal without choosing a class, one will be randomly assigned to you instead.`, "#A33", "#eee", "bold"); send("", "#fff", "#fff"); send(`Type "/scores" to show the score board.`, "#78a", undefined); } /** * Returns the intro message for the app. * * @returns {string} */ getIntroMessage() { let lines = []; lines.push(`Each goal is protected by a barrier! You can help bring down those barriers by tipping the right amount of tokens.`); GoalState.goals.forEach((goal) => { lines.push([ ` - Tip ${goal.trigger} tokens to attack the barrier around "${goal.title}".` ].join(" ")); }); lines.push(""); lines.push(`In order to help bring the barriers down, you have to assign a class to yourself.`); lines.push(`Type "/class" to show the list of available classes to choose from. Make sure you pick one before tipping.`); lines.push(""); lines.push("Some classes may have the ability to sometimes heal a goal that was not targeted by a small amount to keep the competition going."); lines.push("You can check out the detailed class description of each class before choosing one."); Object.keys(Settings.classes).forEach((className) => { lines.push(` - Type "/class ${className}" to assume to role of ${className}. Type "/classinfo ${className}" for detailed statistics about this class.`); }); if (!Settings.allowClassSwitch) { lines.push(""); lines.push("Beware that you may not switch to a different class after you've chosen one."); } return lines.join("\n"); } } /** * Enable binding of event listeners to services that emit events. */ class EventEmitter { static get _events() { if (!EventEmitter.__events) { EventEmitter.__events = new Map(); } return EventEmitter.__events; } constructor() { this.events = new Map(); } /** * Executes the given callback when the specified event is triggered. * * @param {string} name * @param {function} callback */ on(name, callback) { if (!this.events.has(name)) { this.events.set(name, []); } this.events.get(name).push(callback); } /** * [STATIC] Executes the given callback when the specified event is triggered. * * @param {string} name * @param {function} callback */ static on(name, callback) { if (!EventEmitter._events.has(name)) { EventEmitter._events.set(name, []); } EventEmitter._events.get(name).push(callback); } /** * Emits the given event. Any additional specified arguments will be passed on to the executed listeners. * * @param {string} name * @param {*} args */ emit(name, ...args) { if (!this.events.has(name)) { return; } this.events.get(name).forEach((callback) => { callback(...args); }); } /** * [STATIC] Emits the given event. Any additional specified arguments will be passed on to the executed listeners. * * @param {string} name * @param {*} args */ static emit(name, ...args) { if (!EventEmitter._events.has(name)) { return; } EventEmitter._events.get(name).forEach((callback) => { callback(...args); }); } } /** * Service class that holds the goal state and defines gameplay logic. */ class GoalState extends EventEmitter { static init() { GoalState.goals = []; let index = 0, now = new Date().getTime() / 1000; Settings.goals.forEach((goal) => { GoalState.goals.push({ title: goal.title, trigger: goal.trigger, health: goal.health, maxHealth: goal.health, id: index, reviveTokens: goal.reviveTokens, healInterval: goal.healInterval, healPerInterval: goal.healPerInterval, lastHealTick: now }); index++; }); Users.on("user-tip", (user, amount) => { GoalState.handleTip(user, amount); }); GoalState.tickGoalHPI(); } /** * Shows a score board detailing which goal has had most attacks and lists * all users that attacked that barrier, in order of highest tippers to the lowest. */ static showScoreBoard() { let users = Users.getParticipants(); let goals = []; GoalState.goals.forEach((goal) => { let goalScore = { title: goal.title, score: 0, attackers: [] }; // Collect all users that have participated in this goal. users.forEach((user) => { if (user.tipScore.has(goal.title)) { goalScore.attackers.push({ user: user, tips: user.tipScore.get(goal.title), damage: user.damageScore.get(goal.title) || 0 }); // Add the final score to the goal. goalScore.score += (user.damageScore.get(goal.title) || 0); } }); // Sort the attacker list based on most damage done. goalScore.attackers.sort((u1, u2) => u2.damage - u1.damage); goals.push(goalScore); }); // Sort the goals based on damage done. goals.sort((a, b) => b.score - a.score); // Print the score board. Chat.sendMessage(`-----------[ BARRIER BATTLE SCORE BOARD ]-----------`, '#ffffff', '#48318b', 'normal'); goals.forEach((goal) => { Chat.sendMessage(`[${goal.score}] :: ${goal.title}`, '#59429c', undefined, 'bolder'); goal.attackers.forEach((attacker) => { Chat.sendMessage([ ` ----- ${attacker.user.userClass.name} ${attacker.user.name} dealt ${attacker.damage} damage with`, `${attacker.tips} tips.` ].join(' '), '#9b5fbf', undefined, 'normal'); }); }); Chat.sendMessage(`------------------------------------------------------------------`, '#ffffff', '#48318b', 'normal'); } /** * Handles heal-per-interval for goals that have this enabled. */ static tickGoalHPI() { let needsUpdate = false, now = new Date().getTime() / 1000; this.goals.forEach((goal) => { // Make sure we need to tick this goal's health up. if (goal.health < 1 || goal.healInterval < 1 || goal.healPerInterval < 1) { return; } if (goal.lastHealTick < now && goal.health < goal.maxHealth) { goal.health = Math.min(goal.maxHealth, goal.health + goal.healPerInterval); goal.lastHealTick = now + goal.healInterval; needsUpdate = true; } }); if (needsUpdate) { Panel.update(); } setTimeout(() => GoalState.tickGoalHPI(), 1000); } /** * Handle tip logic. * * @param {User} user * @param {number} amount */ static handleTip(user, amount) { // Check if we can revive a goal barrier. let goalWasRevived = false; GoalState.goals.forEach((g) => { if (g.health === 0 && amount === g.reviveTokens) { Chat.sendMessage(`The barrier around "${g.title}" was revived by ${user.name}! BRING IT DOWN!`); g.health = Settings.goals[g.id].health; Panel.update(); goalWasRevived = true; } }); if (goalWasRevived) { return; } // Find the goal that should be triggered by the given amount of tokens. let goal = GoalState.getTriggeredGoal(amount); if (!goal) { return; } // Make sure the player has selected a class. If not, select and assign one at random. if (!user.userClass) { user.assignRandomClass(); Chat.sendMessageToUser([ `Thank you for your tip ${user.name}! Since you did not choose a class to play, the`, `${user.userClass.name} has been randomly assigned to you.`, Settings.allowClassSwitch ? `You can switch classes by typing "/class".` : `` ].join(" "), user.name); } // Store last health so we know what it was before the current user attacks it. let lastHealth = goal.health; // Let the user attack the selected goal. user.attack(goal, amount); // Send a message notifying users can revive the defeated goal. if (lastHealth !== goal.health && goal.health === 0) { if (goal.reviveTokens > 0) { Chat.sendMessage(`The barrier around ${goal.title} was already destroyed! However, it may be revived by tipping ${goal.reviveTokens} tokens to play another round!`); } else { Chat.sendMessage(`The barrier around ${goal.title} was already destroyed! Try another one or wait for the performer to start a new round.`); } } // Let all viewers update the panel. Panel.update(); } /** * Returns the goal that was triggered by the given amount of tokens. * * @param {number} amount */ static getTriggeredGoal(amount) { return GoalState.goals.find((g) => g.trigger === amount); } } /** * Service class for handling app settings. */ class Settings { static get DEFAULT_GOAL_NAMES() { return ["A very sexy dance", "Seductive striptease", "Sexy cameltoe tease"]; } static get DEFAULT_GOAL_TRIGGERS() { return [15, 16, 17]; } static get DEFAULT_CLASS_NAMES() { return ["ranger", "wizard", "hacker"]; } static get DEFAULT_CLASS_PRIZES() { return ["Boobies flash", "Kisses", "Booty slap"]; } static get DEFAULT_CLASS_ATTACK_SKILLS() { return ["shoots an oak tree with some leaves still attached to it", "waves his magic wand with an utmost majestic motion", "grabs a laptop and starts hitting the keys really hard"]; } static get DEFAULT_CLASS_MIN_DMG() { return [15, 25, 20]; } static get DEFAULT_CLASS_MAX_DMG() { return [25, 50, 35]; } static get DEFAULT_CLASS_CRIT_CHANCE() { return [20, 1, 15]; } static get DEFAULT_CLASS_HEAL_CHANCE() { return [8, 0, 5]; } static get DEFAULT_CLASS_CRIT_MUL() { return [2, 1, 3]; } static get DEFAULT_CLASS_SHARE_BOOST_CHANCE() { return [25, 25, 25]; } static get DEFAULT_CLASS_MIN_HEAL() { return [5, 5, 5]; } static get DEFAULT_CLASS_MAX_HEAL() { return [10, 10, 10]; } static get allowClassSwitch() { return cb.settings.allowClassSwitch === "Yes"; } /** * Returns an array of configured goals. * * @returns {Array} */ static get goals() { let goals = []; for (let i = 1; i < 4; i++) { if (!cb.settings["goalTitle" + i]) { continue; } goals.push({ title: cb.settings["goalTitle" + i], trigger: cb.settings["goalTrigger" + i], health: cb.settings["goalHealth" + i], reviveTokens: cb.settings["goalReviveTokens" + i], healInterval: cb.settings["goalHealInterval" + i], healPerInterval: cb.settings["goalHealPerInterval" + i] }); } return goals; } /** * Returns a dictionary of user classes indexed by name. * * @returns {{UserClass}} */ static get classes() { if (typeof Settings.userClasses !== "undefined") { return Settings.userClasses; } Settings.userClasses = {}; for (let i = 1; i < 4; i++) { let name = cb.settings["className" + i]; Settings.userClasses[name] = new UserClass({ name: name, minDamage: cb.settings["classMinDamage" + i], maxDamage: cb.settings["classMaxDamage" + i], critChance: cb.settings["classCritChance" + i], critMultiplier: cb.settings["classCritMultiplier" + i], critPrize: cb.settings["classCritPrize" + i], healChance: cb.settings["classHealChance" + i], minHeal: cb.settings["classMinHeal" + i], maxHeal: cb.settings["classMaxHeal" + i], shareBoostChance: cb.settings["classBoostShareChance" + i], attackSkill: cb.settings["classAttackSkill" + i] }); } return Settings.userClasses; } /** * Returns true if the class with the given name exists. * * @param {string} className * @returns {boolean} */ static classExists(className) { return typeof Settings.userClasses[className] !== "undefined"; } /** * Returns the settings form fields to configure the app. * * @returns {*[]} */ static renderForm() { return [ ...Settings._renderGoalForm(1, true), ...Settings._renderGoalForm(2), ...Settings._renderGoalForm(3), ...Settings._renderBoostForm(), ...Settings._renderClassForm(1), ...Settings._renderClassForm(2), ...Settings._renderClassForm(3) ]; } static _renderGoalForm(goalId, required = false) { return [ { name: "goalTitle" + goalId, label: "Goal #" + goalId + " prize", type: "str", required: required, minLength: 1, maxLength: 32, defaultValue: Settings.DEFAULT_GOAL_NAMES[goalId - 1] }, { name: "goalTrigger" + goalId, label: "Goal #" + goalId + " tip trigger", type: "int", required: required, minValue: 1, maxValue: 100, defaultValue: Settings.DEFAULT_GOAL_TRIGGERS[goalId - 1] }, { name: "goalHealth" + goalId, label: "Goal #" + goalId + " total health", type: "int", required: required, minValue: 1, maxValue: 9999, defaultValue: 1000 }, { name: "goalReviveTokens" + goalId, label: "Goal #" + goalId + " tokens required to start over after defeat (0 = disabled)", type: "int", required: required, minValue: 0, maxValue: 9999, defaultValue: 1000 }, { name: "goalHealInterval" + goalId, label: "Goal #" + goalId + " auto heal interval in seconds (set to 0 to disable)", type: "int", required: required, minValue: 0, maxValue: 1000, defaultValue: 0 }, { name: "goalHealPerInterval" + goalId, label: "Goal #" + goalId + " auto heal points per interval (force viewers to keep the tips flowing)", type: "int", required: required, minValue: 0, maxValue: 1000, defaultValue: 1 } ]; } static _renderBoostForm() { return [ { name: "boostTokens", label: "Tokens needed for time-based damage boost. Multiple values allowed, separated by comma.", type: "str", required: true, defaultValue: "100, 500, 1000" }, { name: "boostMultiplier", label: "Boost damage multiplier", type: "int", minValue: 1, maxValue: 10, defaultValue: 2 }, { name: "boostTimer", label: "Boost timer (in seconds)", type: "int", minValue: 10, maxValue: 120, defaultValue: 30 }, { name: "allowClassSwitch", label: "Allow switching classes mid-game", type: "choice", choice1: "No", choice2: "Yes", defaultValue: "No" } ]; } static _renderClassForm(classId) { return [ { name: "className" + classId, label: "Class #" + classId + " name (1 word, keep it short)", type: "str", minLength: 1, maxLength: 32, defaultValue: Settings.DEFAULT_CLASS_NAMES[classId - 1] }, { name: "classCritPrize" + classId, label: "Class #" + classId + " critical hit prize", type: "str", minLength: 1, maxLength: 128, defaultValue: Settings.DEFAULT_CLASS_PRIZES[classId - 1] }, { name: "classAttackSkill" + classId, label: "Class #" + classId + " attack text", type: "str", minLength: 1, maxLength: 128, defaultValue: Settings.DEFAULT_CLASS_ATTACK_SKILLS[classId - 1] }, { name: "classMinDamage" + classId, label: "Class #" + classId + " min damage points", type: "int", minValue: 1, maxValue: 100, defaultValue: Settings.DEFAULT_CLASS_MIN_DMG[classId - 1] }, { name: "classMaxDamage" + classId, label: "Class #" + classId + " max damage points", type: "int", minValue: 1, maxValue: 100, defaultValue: Settings.DEFAULT_CLASS_MAX_DMG[classId - 1] }, { name: "classCritChance" + classId, label: "Class #" + classId + " chance to critically hit (percent)", type: "int", minValue: 0, maxValue: 100, defaultValue: Settings.DEFAULT_CLASS_CRIT_CHANCE[classId - 1] }, { name: "classCritMultiplier" + classId, label: "Class #" + classId + " critical hit multiplier", type: "int", minValue: 1, maxValue: 100, defaultValue: Settings.DEFAULT_CLASS_CRIT_MUL[classId - 1] }, { name: "classHealChance" + classId, label: "Class #" + classId + " chance to heal a goal (percent - 0~50)", type: "int", minValue: 0, maxValue: 50, defaultValue: Settings.DEFAULT_CLASS_HEAL_CHANCE[classId - 1] }, { name: "classMinHeal" + classId, label: "Class #" + classId + " min heal points", type: "int", minValue: 0, maxValue: 50, defaultValue: Settings.DEFAULT_CLASS_MIN_HEAL[classId - 1] }, { name: "classMaxHeal" + classId, label: "Class #" + classId + " max heal points", type: "int", minValue: 0, maxValue: 50, defaultValue: Settings.DEFAULT_CLASS_MAX_HEAL[classId - 1] }, { name: "classBoostShareChance" + classId, label: "Class #" + classId + " chance to share boost with another participant (percent)", type: "int", minValue: 1, maxValue: 100, defaultValue: Settings.DEFAULT_CLASS_SHARE_BOOST_CHANCE[classId - 1] } ]; } } /** * Service class for rendering the panel. */ class Panel { static init() { cb.onDrawPanel(() => Panel.getPanelData()); Panel.update(); } static update() { cb.drawPanel(); } /** * Returns the data to draw the panel below the cam frame. * * @returns {{}} */ static getPanelData() { if (GoalState.goals.length === 1) { return Panel.renderOneGoalPanel(); } if (GoalState.goals.length === 2) { return Panel.renderTwoGoalsPanel(); } if (GoalState.goals.length === 3) { return Panel.renderThreeGoalsPanel(); } } /** * Renders the panel that shows one configured goal. * * @returns {{template: string, layers: *[]}} */ static renderOneGoalPanel() { let layers = [...Panel.renderGoal(GoalState.goals[0], 26)]; return { "template": "image_template", "layers": [ {"type": "image", "fileID": PANEL_BACKGROUND_IMAGE_1_GOAL}, ...layers ] }; } /** * Renders the panel that shows two configured goals. * * @returns {{template: string, layers: *[]}} */ static renderTwoGoalsPanel() { let layers = [ ...Panel.renderGoal(GoalState.goals[0], 14), ...Panel.renderGoal(GoalState.goals[1], 37) ]; return { "template": "image_template", "layers": [ {"type": "image", "fileID": PANEL_BACKGROUND_IMAGE_2_GOAL}, ...layers ] }; } /** * Renders the panel that shows three configured goals. * * @returns {{template: string, layers: *[]}} */ static renderThreeGoalsPanel() { let layers = [ ...Panel.renderGoal(GoalState.goals[0], 4), ...Panel.renderGoal(GoalState.goals[1], 26), ...Panel.renderGoal(GoalState.goals[2], 48) ]; return { "template": "image_template", "layers": [ {"type": "image", "fileID": PANEL_BACKGROUND_IMAGE_3_GOAL}, ...layers ] }; } /** * Returns the layers for the given goal. * * @param {Object<*>} goal * @param {number} top * @returns {*[]} */ static renderGoal(goal, top) { return [ { "type": "text", "top": top, "left": 5, "text": goal.trigger + " tks: " + goal.title, "font-size": 12, "font-family": "Roboto, Lato, arial, sans-serif", "color": goal.health > 0 ? "#e2beff" : "#ffffff", "max-width": 190 }, { "type": "text", "text": goal.health > 0 ? (goal.health + " hp") : "DONE!", "top": top, "left": 205, "font-family": "Roboto, Lato, arial, sans-serif", "font-size": 12, "max-width": 66, "color": goal.health > 0 ? "#e2beff" : "#ffffff" } ]; } } /** * Service class for the Chat API. */ class Chat extends EventEmitter { static init() { Chat.commandNames = []; cb.onMessage((msg) => Chat._handleMessage(msg)); } /** * Registers a chat command. * * This allows events to be emitted for the given command and will also hide any chat messages from users executing * these commands to keep the chat clean. * * @param command */ static registerChatCommand(command) { if (!Chat.commandNames) { Chat.commandNames = []; } Chat.commandNames.push(command); } /** * Sends a message to the given recipient. If the recipient is omitted, the message is broadcast to all users in * the general chat. * * @param {string} message * @param {string} color * @param {string} backgroundColor * @param {string} weight * @param {string} recipient */ static sendMessage(message, color = "#57219C", backgroundColor = undefined, weight = "bold", recipient = undefined) { cb.sendNotice(message, recipient, backgroundColor, color, weight); } /** * Alias for sendMessage but with easier access to the recipient argument. * * @param {string} message * @param {string} color * @param {string} backgroundColor * @param {string} weight * @param {string} recipient */ static sendMessageToUser(message, recipient, color = "#4721CC", backgroundColor = undefined, weight = "bold") { Chat.sendMessage(message, color, backgroundColor, weight, recipient); } /** * Handles incoming chat messages. * * @param {*} msg * @private */ static _handleMessage(msg) { let user = Users.get(msg.user, msg); if (msg.m.startsWith("/")) { msg["background"] = "#FFF"; msg["color"] = "#aaa"; if (Chat._handleCommand(user, msg.m)) { msg["X-Spam"] = true; } } if (user.userClass) { msg.user = user.userClass.name + " " + msg.user; } return msg; } /** * Emits the "command" event if the user executed a command that was properly formatted. * * @param {User} user * @param {string} message * @private */ static _handleCommand(user, message) { // Grab the command from the chat in the format of '/command [optional args]'. let m = /^\/(\w+)\s?(.+)?$/.exec(message); // Make sure the chat command is properly formatted. if (!m || m.length !== 3) { return false; } let command = m[1], args = m[2] || ""; if (Chat.commandNames.indexOf(command) !== -1) { Chat.emit("command", user, command, args); return true; } return false; } } /** * Service class for managing users. */ class Users extends EventEmitter { static init() { Users.users = new Map(); cb.onEnter((u) => { if (Users.users.has(u.user)) { // Update the user. Users.users.get(u.user).update(u); Users.emit("user-rejoined", Users.users.get(u.user)); } else { let user = new User(u); Users.users.set(u.user, user); Users.emit("user-joined", user); } }); cb.onLeave((u) => { Users.emit("user-left", Users.users.get(u.user)); // Users.users.delete(u.user); // @FIXME: Make this configurable? }); cb.onTip((tip) => { let amount = tip.amount, user = Users.get(tip.from_user, { user: tip.from_user, gender: tip.from_user_gender, has_tokens: tip.from_user_has_tokens, in_fanclub: tip.from_user_in_fanclub, is_mod: tip.from_user_is_mod }); user.onTip(amount); this.emit("user-tip", user, amount); }); } /** * Returns a User instance from the player pool. * * @param {string} username * @param {object} userData * @returns {User} */ static get(username, userData) { if (Users.users.has(username) === false) { Users.users.set(username, new User(userData)); } return Users.users.get(username); } /** * Returns all participants that have assigned a class and have tipped at least once. */ static getParticipants() { let p = []; Users.users.forEach((u) => { if (u.userClass && u.damageScore.size > 0 && u.tipScore.size > 0) { p.push(u); } }); return p; } /** * Picks a random user that is not the given user. * * @param {User} forUser */ static getRandomUserForUser(forUser) { let u = Array.from(this.users.keys()), i = 0; if (u.length === 1) { return; } let user; do { user = u[Math.floor(Math.random() * u.length) - 1]; i++; } while (i < 10 && !(user && user !== forUser.name && this.users.get(user).userClass)); if (!user || user === forUser.name || ! this.users.get(user).userClass) { return; } return Users.users.get(user); } } class User { constructor(u) { this.isTicking = false; this.userClass = undefined; this.boostTimer = 0; this.tipScore = new Map(); this.damageScore = new Map(); this.update(u); } /** * Updates the user data. * * @param {object} data */ update(data) { this.name = data.user; this.gender = data.gender; this.hasTokens = data.has_tokens; this.isFanclubMember = data.in_fanclub; this.isModerator = data.is_mod; } /** * Assign a random class to this user if it did not choose one yet. */ assignRandomClass() { // Don't overwrite if the user already made a choice. if (this.userClass) { return; } let names = Object.keys(Settings.classes), className = names[Math.floor(Math.random() * names.length)]; this.userClass = Settings.classes[className]; Chat.sendMessage(`${this.name} has assumed the role of ${this.userClass.name}!`); } /** * Executed every second to keep track of timers. */ tick() { let now = new Date().getTime() / 1000; let bt = cb.settings.boostTokens.split(',').map(t => parseInt(t.trim())); if (this.isBoosted && this.boostTimer <= now) { Chat.sendMessage([ `${this.name}'s boost just ran out, but ${this.gender === "m" ? "he" : "she"} can reapply one by tipping`, `${bt.join(' or ')} tokens. Get down those barriers!` ].join(" ")); this.isTicking = false; return; } this.isBoosted = this.boostTimer > now; setTimeout(() => this.tick(), 1000); } /** * Attacks the given goal. * * @param {*} goal * @param {number} tipAmount */ attack(goal, tipAmount) { if (!this.userClass) { return; } if (!this.tipScore.has(goal.title)) { this.tipScore.set(goal.title, 0); } this.tipScore.set(goal.title, this.tipScore.get(goal.title) + tipAmount); // If the goal was already achieved, let the user know that it can be revived (if applicable). if (goal.health === 0) { if (goal.reviveTokens > 0) { Chat.sendMessageToUser( `The barrier around "${goal.title}" is already down. You can revive it by tipping ${goal.reviveTokens} tokens.`, this.name ); } return; } let damage = Math.round(this.userClass.minDamage + (Math.random() * (this.userClass.maxDamage - this.userClass.minDamage))); let isCritical = (Math.random() * 100 <= this.userClass.critChance); let message = [`${this.userClass.name} ${this.name} ${this.userClass.attackSkill}`]; let heShe = this.gender === "m" ? "he" : (this.gender === "f" ? "she" : "has"); // Modify damage based on boost status & critical hit. damage = this.isBoosted ? damage * cb.settings.boostMultiplier : damage; damage = isCritical ? damage * this.userClass.critMultiplier : damage; // Apply damage to goal. goal.health = Math.max(0, goal.health - damage); // Compose the rest of the chat message. if (goal.health > 0) { if (isCritical) { message.push(`and deals a critical strike of ${damage} damage points on the barrier around "${goal.title}"!`); message.push(`With that ${heShe} earned ${this.userClass.critPrize}!`); } else { message.push(`and deals ${damage} damage points on the barrier around "${goal.title}"!`); } Chat.sendMessage(message.join(" "), "#7741AC", undefined, "normal"); } else { if (isCritical) { message = [ `${this.name.toUpperCase()} ${this.userClass.attackSkill.toUpperCase()} AND DEALT A DEVASTATING`, `FINAL BLOW ON THE BARRIER OF "${goal.title}" WITH A CRITICAL STRIKE OF ${damage} DAMAGE POINTS!` ]; } else { message = [ `${this.name.toUpperCase()} ${this.userClass.attackSkill.toUpperCase()} AND DEALT THE FINAL BLOW`, `ON THE BARRIER OF "${goal.title}" WITH ${damage} DAMAGE POINTS!` ]; } Chat.sendMessage(message.join(" "), "#ffffff", "#57219c", "bolder"); } if (!this.damageScore.has(goal.title)) { this.damageScore.set(goal.title, 0); } this.damageScore.set(goal.title, this.damageScore.get(goal.title) + damage); // Check if we can heal another goal barrier. let doHeal = GoalState.goals.length > 1 && (Math.random() * 100) <= this.userClass.healChance; if (!doHeal) { return; } let healPoints = Math.round(this.userClass.minHeal + (Math.random() * (this.userClass.maxHeal - this.userClass.minHeal))); let hGoal, maxAttempts = 0; do { hGoal = GoalState.goals[Math.round(Math.random() * GoalState.goals.length)]; maxAttempts++; } while (maxAttempts < 10 && (!hGoal || hGoal === goal || hGoal.health === 0)); // Check if we found a qualified goal that we can heal up a bit. if (!(hGoal && hGoal !== goal && hGoal.health > 0)) { return; } // Don't attempt to heal if it's already at full health. if (hGoal.health === Settings.goals[hGoal.id].health) { return; } hGoal.health = Math.min(Settings.goals[hGoal.id].health, hGoal.health + healPoints); Chat.sendMessage( `${this.name} has healed the barrier around "${hGoal.title}" by ${healPoints} points.`, "#218C22", undefined, "normal" ); } /** * Applies a boost to this user. */ applyBoost(giftedFrom) { let now = new Date().getTime() / 1000; let hisHer = this.gender === "m" ? "his" : (this.gender === "f" ? "her" : "the"); let su, shareWith; // Roll the dice - but only if this boost was not gifted from somebody else. Can't have a chain reaction going // on by tipping for one boost...let along having a chance of recursion happening :') if (!giftedFrom && ((Math.random() * 100) < this.userClass.shareBoostChance)) { shareWith = Users.getRandomUserForUser(this); } if (this.boostTimer > now) { this.boostTimer = this.boostTimer + cb.settings.boostTimer; // Notify a 'gifted from' message if this boost was a gift. if (giftedFrom) { return Chat.sendMessage(`${this.name} got ${hisHer} boost timer extended by a gift from ${giftedFrom.name}!`); } // Notify on chat that this user extended the boost timer. Chat.sendMessage([ `${this.name} has extended ${hisHer} boost timer with ${cb.settings.boostTimer} seconds`, shareWith ? ` and gave a free boost to ${shareWith.name} as well!` : `!` ].join("")); if (shareWith) { shareWith.applyBoost(this); } return; } this.boostTimer = now + cb.settings.boostTimer; if (!this.isTicking) { this.isTicking = true; this.tick(); } if (giftedFrom) { Chat.sendMessage(`${this.name} activated a boost that was gifted by ${giftedFrom.name}! Let's rumble!`); } else { Chat.sendMessage([ `${this.name} has applied a boost for ${cb.settings.boostTimer} seconds`, shareWith ? ` and gave a free boost to ${shareWith.name} as well!` : `!` ].join("")); } if (shareWith) { shareWith.applyBoost(this); } } /** * Executed when this user tipped some tokens. * * @param {number} amount */ onTip(amount) { let bt = cb.settings.boostTokens.split(',').map(t => parseInt(t.trim())); if (bt.indexOf(amount) !== -1) { if (! this.userClass) { this.assignRandomClass(); } this.applyBoost(); } } /** * Executed when this user executes a command in chat. * * @param {string} command * @param {string} args */ onCommand(command, args) { switch (command) { case "scores": GoalState.showScoreBoard(); break; // Send class information about the given class to this user. case "classinfo": if (!args || args === "") { return Chat.sendMessageToUser([ `Please specify a class name to see the detailed statistics of. You can choose from the following: `, Object.keys(Settings.classes).join(", ") ].join(" "), this.name); } if (!Settings.classExists(args)) { return Chat.sendMessageToUser([ `I'm sorry ${this.name}, but that class does not exist. Type "/class" to see a list of`, `available classes to choose from.` ].join(" "), this.name); } return Chat.sendMessageToUser(Settings.classes[args].getDetailsMessage(), this.name); // Select the class for this user. case "class": // If no arguments were given, show the list of available classes. if (!args || args.length === 0) { let lines = []; Object.keys(Settings.classes).forEach((className) => { lines.push([ `Type "/class ${className}" to assume to role of ${className}. Type "/classinfo`, `${className}" for detailed statistics about this class.` ].join(" ")); }); Chat.sendMessageToUser(lines.join("\n"), this.name); return; } // Don't allow switching to another class mid-game if the broadcaster does not permit it. if (!Settings.allowClassSwitch && typeof this.userClass !== "undefined") { return Chat.sendMessageToUser( `I'm sorry ${this.name}, but you are only allowed to choose a class once per game.`, this.name ); } // Make sure the selected class exists. if (!Settings.classExists(args)) { return Chat.sendMessageToUser([ `I'm sorry ${this.name}, but that class does not exist. Type "/class" to see a list of`, `available classes to choose from.` ].join(" "), this.name); } this.userClass = Settings.classes[args]; Chat.sendMessage(`${this.name} has assumed the role of ${this.userClass.name}!`); break; } } } /** * Represents the selected class for a user. */ class UserClass { constructor(options) { this.name = options.name; this.minDamage = options.minDamage; this.maxDamage = options.maxDamage; this.critChance = options.critChance; this.critMultiplier = options.critMultiplier; this.critPrize = options.critPrize; this.healChance = options.healChance; this.minHeal = options.minHeal; this.maxHeal = options.maxHeal; this.shareBoostChance = options.shareBoostChance; this.attackSkill = options.attackSkill || "attacks"; } /** * Returns the detailed statistics message for this class. * * @returns {string} */ getDetailsMessage() { let message = [ `The ${this.name} class ${this.attackSkill} and may deal between ${this.minDamage} and ${this.maxDamage}`, `points of damage.\n` ]; if (this.critChance > 0 && this.critMultiplier > 1) { message.push(...[ `There is a ${this.critChance}% chance that an attack will result in a critical hit. If this happens`, `the damage done may range between ${this.minDamage * this.critMultiplier} and `, `${this.maxDamage * this.critMultiplier}. You will be rewarded with the prize: "${this.critPrize}".` ]); } if (this.healChance > 0) { message.push(...[ `\n`, `This class has a ${this.healChance}% chance to heal a different goal than the one that was targeted`, `last by ${this.minHeal}~${this.maxHeal} points.` ]); } let bt = cb.settings.boostTokens.split(',').map(t => parseInt(t.trim())); message.push(...[ `\nYou may apply a boost to yourself by tipping ${bt.join(' or ')} tokens to increase your base damage`, `to ${this.minDamage * cb.settings.boostMultiplier}~${this.maxDamage * cb.settings.boostMultiplier} points.`, `A boost will last for ${cb.settings.boostTimer} seconds.` ]); if (this.shareBoostChance > 0) { message.push(...[ `There is also a ${this.shareBoostChance}% chance of you sharing your boost with another player!` ]); } return message.join(" "); } } // ------------------------------------------------------------------------------------------------------------------ // // Render the settings form for the app. cb.settings_choices = Settings.renderForm(); // Run the game. new Game();
© Copyright Chaturbate 2011- 2025. All Rights Reserved.