Created
March 14, 2024 00:30
-
-
Save roxwize/727c2881e1ae6141c61c8dfe04cf8327 to your computer and use it in GitHub Desktop.
TEA FORTRESS 8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width"> | |
| <title>TEA FORTRESS 8</title> | |
| <link href="style.css" rel="stylesheet" type="text/css" /> | |
| </head> | |
| <body> | |
| <h1>TEA FORTRESS 8</h1> | |
| <div id="game"> | |
| <pre id="playfield"></pre> | |
| <div id="p-stats"> | |
| Turn <strong id="p-turn">0</strong><br><br> | |
| <strong id="p-class">None</strong><br> | |
| <strong id="p-health">0</strong>/<strong id="p-health-max">0</strong>HP<br><br> | |
| <strong id="p-wep-name">None</strong><br> | |
| <strong id="p-ammo-clip">0</strong>/<strong id="p-ammo-reserve">0</strong><br><br> | |
| <strong id="p-status"></strong> | |
| </div> | |
| <div id="leaderboard"></div> | |
| </div> | |
| <h3>Chat</h3> | |
| <pre id="chat"></pre> | |
| <input type="text" id="chatinput" placeholder="ENTER to send" autocomplete="off"> | |
| <h3>The Controls</h3> | |
| <p>you can also use WASD/arrow keys to move</p> | |
| <p>click on a player to shoot in their general direction</p> | |
| <button onclick="player.move(-1,0);doTurn()">W</button><button onclick="player.move(0,-1);doTurn()">N</button><button onclick="player.move(0,1);doTurn()">S</button><button onclick="player.move(1,0);doTurn()">E</button><button onclick="if(player.reload())doTurn()">RELOAD</button><button onclick="doTurn()">PASS</button><br> | |
| <button id="p-b-primary">PRIMARY</button><button id="p-b-secondary">SECONDARY</button><button id="p-b-tertiary">TERTIARY</button><button id="p-b-melee">MELEE</button><br> | |
| <button onclick="player.sayVoiceline(VOICELINE_TYPE.TAUNT)">TAUNT</button> | |
| <button onclick="player.sayVoiceline(VOICELINE_TYPE.TAUNT2)">TAUNT2</button><br> | |
| <button onclick="player.kill();doTurn()">DIE</button><br> | |
| <button onclick="bot()">BOT</button><br> | |
| <button id="d-kill-update">UPDATE</button> <select id="d-kill-select"></select> <button id="d-kill-btn">KILL</button> <button id="d-kill-from">DAMAGE FROM</button> | |
| <h3>The Information</h3> | |
| <p>This is the work of <a href="//roxwize.xyz">roxwize</a></p> | |
| <p>Team Fortress belongs to Valve Software</p> | |
| <script src="index.js"></script> | |
| </body> | |
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /***********************/ | |
| /****TEA FORTRESS 2*****/ | |
| /***(ↄ) roxwize 2024****/ | |
| /*****tf2 by valve******/ | |
| /***********************/ | |
| /******v0.1.0 !!********/ | |
| /***********************/ | |
| const WEAPON_TYPE = { | |
| PRIMARY: 0, | |
| SECONDARY: 1, | |
| TERTIARY: 2, | |
| MELEE: 3, | |
| NONE: 4, | |
| toString: (type) => { | |
| switch (type) { | |
| case WEAPON_TYPE.PRIMARY: | |
| return "primary"; | |
| case WEAPON_TYPE.SECONDARY: | |
| return "secondary"; | |
| case WEAPON_TYPE.TERTIARY: | |
| return "tertiary"; | |
| case WEAPON_TYPE.MELEE: | |
| return "melee"; | |
| case WEAPON_TYPE.NONE: | |
| return ""; | |
| } | |
| } | |
| } | |
| const VOICELINE_TYPE = { | |
| YES: 0, // Da | |
| NO: 1, // Nyet | |
| SPY: 2, // Icy spy | |
| SPY_IDENTIFY: 3, // Heavy is pie | |
| HELP: 4, // Halp meh | |
| PAIN: 5, // Oof | |
| TAUNT: 6, // I was told we would be fighting men | |
| TAUNT2: 7, // Im coming for yuoo, | |
| CRIT_DEATH: 8, // AAAAAAAAAGHHYGAYADHAAAHH | |
| CRIT_PAIN: 9 // Ouuuggghhhhh | |
| } | |
| const VOICELINE_DEFAULT = [ | |
| "Yes.", | |
| "No.", | |
| "I sense a spy.", | |
| "That {} is a spy.", | |
| "Help me!", | |
| "Yowch!", | |
| "I am taunting you.", | |
| "I am taunting you in a different fashion.", | |
| "AAAOUUGHHH!!!!!!!!", | |
| "YOWWW!!!!!" | |
| ] | |
| ///////////////////////////////////////// | |
| // WEAPON CLASS // | |
| ///////////////////////////////////////// | |
| class WeaponStats { | |
| constructor() { | |
| this.damage = 12; | |
| this.damageScaling = 0; | |
| this.range = 6; | |
| this.maxClip = 0; | |
| } | |
| setDamage(dmg) { | |
| this.damage = dmg; | |
| return this; | |
| } | |
| setDamageScaling(scale) { | |
| this.damageScaling = scale; | |
| return this; | |
| } | |
| setRange(range) { | |
| this.range = range; | |
| return this; | |
| } | |
| setMaxClip(clip) { | |
| this.maxClip = clip; | |
| return this; | |
| } | |
| } | |
| class Weapon { | |
| /** | |
| * @param {number} type | |
| * @param {string} name | |
| * @param {WeaponStats} stats | |
| */ | |
| constructor(type, name, stats) { | |
| this.type = type; | |
| this.name = name; | |
| this.stats = stats; | |
| } | |
| } | |
| ///////////////////////////////////////// | |
| // CLASS CLASS(??) // | |
| ///////////////////////////////////////// | |
| class ClassStats { | |
| constructor() { | |
| this.speed = 1; | |
| this.maxHp = 200; | |
| } | |
| setSpeed(speed) { | |
| this.speed = speed; | |
| return this; | |
| } | |
| setMaxHp(hp) { | |
| this.maxHp = hp; | |
| return this; | |
| } | |
| } | |
| class Class { | |
| /** | |
| * @param {string} name | |
| * @param {Weapon[]} loadout | |
| * @param {ClassStats} stats | |
| */ | |
| constructor(name, loadout, stats) { | |
| this.name = name; | |
| this.defaultLoadout = loadout; | |
| this.stats = stats; | |
| this.voicelines = VOICELINE_DEFAULT; | |
| } | |
| setVoicelines(voicelines) { | |
| this.voicelines = voicelines; | |
| return this; | |
| } | |
| } | |
| class DamageInfo { | |
| /** | |
| * @param {number} damage | |
| * @param {Mercenary} damager | |
| * @param {Mercenary} damagee | |
| * @param {boolean} crit | |
| */ | |
| constructor(damage, damager, damagee, crit = false) { | |
| this.damage = damage; | |
| this.damager = damager; | |
| this.damagee = damagee; | |
| this.critical = crit; | |
| if (this.damager == this.damagee) this.selfInflicted = true; | |
| if (this.critical) this.damage *= 3; | |
| this.weapon = null; | |
| } | |
| /** | |
| * param {Weapon} weapon | |
| */ | |
| setWeapon(weapon) { | |
| this.weapon = weapon; | |
| return this; | |
| } | |
| toKillfeedString() { | |
| return "<strong>" + (this.selfInflicted ? `${this.damagee.getName()} bid farewell, cruel world!` | |
| : `${this.critical ? "[CRIT!] " : ""}${this.damager.getName()} killed ${this.damagee.getName()}${this.weapon && this.weapon != Weapons._INTERNAL_world ? ` with ${this.weapon.name}` : ""}`) + "</strong>"; | |
| } | |
| } | |
| ///////////////////////////////////////// | |
| // MERCENARY CLASS // | |
| ///////////////////////////////////////// | |
| class Mercenary { | |
| /** | |
| * @param {string} name Username | |
| * @param {Class} classType | |
| * @param {{x:number,y:number}} pos | |
| */ | |
| constructor(name, classType, pos = { x: 0, y: 0 }, accuracy = 0.8) { | |
| this.name = name; | |
| this.classType = classType; | |
| this.pos = pos; | |
| this.kills = 0; | |
| this.deaths = 0; | |
| this.accuracy = accuracy; | |
| this.isBot = false; | |
| this.isPlayer = false; | |
| this.health = classType.stats.maxHp; | |
| this.lastDamage = null; | |
| this.dead = false; | |
| this.respawnTime = 0; | |
| this.loadout = classType.defaultLoadout; | |
| this.clip = [this.loadout[0]?.stats.maxClip || 0, this.loadout[1]?.stats.maxClip || 0, this.loadout[2]?.stats.maxClip || 0]; | |
| this.reserve = [this.clip[0] * 4, this.clip[1] * 4, this.clip[2] * 4]; | |
| this.weaponId = WEAPON_TYPE.PRIMARY; | |
| } | |
| getName() { | |
| return `${this.dead ? "[DEAD] " : ""}${this.name}`; | |
| } | |
| move(dx, dy) { | |
| if (this.dead) return; | |
| if (dx !== 0) { | |
| if (playfield[this.pos.y][this.pos.x + dx] === undefined) return false; | |
| if (playerColMap[this.pos.y][this.pos.x + dx]) return false; // check entity collision map | |
| if (playfield[this.pos.y][this.pos.x + dx] === 1) return false; // check world | |
| // assume it's 1, otherwise we can pass through walls which might need to be fixed at some point | |
| else this.pos.x += dx; | |
| } | |
| if (dy !== 0) { | |
| if (playfield[this.pos.y + dy] == undefined) return false; | |
| if (playerColMap[this.pos.y + dy][this.pos.x]) return false; // check entity collision map | |
| if (playfield[this.pos.y + dy][this.pos.x] === 1) return false; // check world | |
| else this.pos.y += dy; | |
| } | |
| updateColMap(); | |
| return true; | |
| } | |
| reload() { | |
| const clipSize = this.loadout[this.weaponId].stats.maxClip; | |
| if (this.clip[this.weaponId] === clipSize) return false; | |
| const reserve = this.reserve[this.weaponId]; | |
| const toReload = clipSize - this.clip[this.weaponId]; | |
| if (reserve > toReload) { | |
| // dont empty | |
| this.reserve[this.weaponId] -= toReload; | |
| this.clip[this.weaponId] += toReload; | |
| statusText = "Reloading..."; | |
| } else if (reserve > 0) { | |
| // empty all of reserve into clip | |
| this.clip[this.weaponId] += this.reserve[this.weaponId]; | |
| this.reserve[this.weaponId] = 0; | |
| statusText = "Reloading..."; | |
| } else { | |
| statusText = "Empty magazine."; | |
| printStats(); | |
| return false; | |
| } | |
| return true; | |
| } | |
| fire(merc) { | |
| if (this.dead || merc == this) return false; // Can't fire at yo self | |
| const wep = this.loadout[this.weaponId]; | |
| if (this.clip[this.weaponId]) { | |
| let accuracy = this.accuracy; | |
| const dist = this.distanceTo(merc.pos.x, merc.pos.y); | |
| // lower accuracy if range exceeded | |
| if (dist > wep.stats.range) { | |
| accuracy = 0.3; | |
| } | |
| if (Math.random() < accuracy) { | |
| // didn't miss | |
| let damage = wep.stats.damage; | |
| const scale = wep.stats.damageScaling * (dist - 1); | |
| if (wep.stats.damageScaling && scale > 0) { | |
| // damage scaling per range (dumb bodge for shotgun mechanics) | |
| damage = Math.floor(wep.stats.damage / scale); | |
| } | |
| const crit = Math.random() > 0.7 // Random crits :3 | |
| if (crit) statusText = "CRITICAL HIT!!"; | |
| else statusText = `Hit ${merc.getName()}!`; | |
| statusText += `<br>${crit ? damage * 3 : damage} DMG`; | |
| merc.hurt(new DamageInfo(damage, this, merc, crit).setWeapon(wep)); | |
| } else { | |
| statusText = "MISS!" | |
| } | |
| this.clip[this.weaponId]--; | |
| } else { | |
| this.reload(); | |
| } | |
| return true; | |
| } | |
| say(message) { | |
| say(`${this.getName()}: ${message}`, false); | |
| } | |
| sayVoiceline(voiceline) { | |
| this.say("[VOICE] " + this.classType.voicelines[voiceline]); | |
| } | |
| /** | |
| * @param {DamageInfo} dmginfo | |
| */ | |
| hurt(dmginfo) { | |
| if (this.dead) return; | |
| this.health -= dmginfo.damage; | |
| this.lastDamage = dmginfo; | |
| if (/*Math.random() > 0.5 && */!dmginfo.critical) { | |
| this.sayVoiceline(VOICELINE_TYPE.PAIN); | |
| } else if (dmginfo.critical && this.health > 0) { | |
| this.sayVoiceline(VOICELINE_TYPE.CRIT_PAIN); | |
| } | |
| // Player death logic | |
| if (this.health <= 0) { | |
| say(dmginfo.toKillfeedString()) // Log to chat | |
| this.health = 0; // clamp | |
| this.dead = true; // X_X | |
| this.deaths++; | |
| if (dmginfo.damager && !dmginfo.selfInflicted) dmginfo.damager.kills++; | |
| this.respawnTime = 15; // TODO: this is a placeholder value | |
| // Crit death scream | |
| if (dmginfo.critical) this.sayVoiceline(VOICELINE_TYPE.CRIT_DEATH); | |
| } | |
| printStats(); | |
| printLeaderboard(); | |
| } | |
| kill() { | |
| this.hurt( | |
| new DamageInfo(this.classType.stats.maxHp * 5, this, this, true).setWeapon(Weapons._INTERNAL_world) | |
| ); | |
| } | |
| respawn() { | |
| this.dead = false; | |
| this.health = this.classType.stats.maxHp; | |
| this.respawnTime = 0; | |
| this.lastDamage = null; | |
| } | |
| distanceTo(x, y) { | |
| return Math.sqrt(Math.pow(x - this.pos.x, 2) + Math.pow(y - this.pos.y, 2)); | |
| } | |
| } | |
| class Bot extends Mercenary { | |
| constructor(name, classType, pos = { x: 0, y: 0 }) { | |
| super(name, classType, pos); | |
| this.isBot = true; | |
| } | |
| getName() { | |
| return `${this.dead ? "[DEAD] " : ""}[BOT]${this.name}`; | |
| } | |
| think() { | |
| if (this.dead) return; | |
| this.move( | |
| Math.round(Math.random() * 2) - 1, | |
| Math.round(Math.random() * 2) - 1 | |
| ) | |
| } | |
| } | |
| const Weapons = { | |
| Pistol: new Weapon(WEAPON_TYPE.SECONDARY, "Pistol", new WeaponStats().setDamage(12).setRange(15).setMaxClip(12)), | |
| Bat: new Weapon(WEAPON_TYPE.MELEE, "Bat", new WeaponStats().setDamage(33).setRange(1).setMaxClip(0)), | |
| Scattergun: new Weapon(WEAPON_TYPE.PRIMARY, "Scattergun", new WeaponStats().setDamage(75).setRange(6).setMaxClip(6).setDamageScaling(1.3)), | |
| _INTERNAL_world: new Weapon(WEAPON_TYPE.NONE, "world", new WeaponStats().setDamage(1E10)) | |
| }; | |
| const Classes = { | |
| Scout: new Class( | |
| "Scout", | |
| [Weapons.Scattergun, Weapons.Pistol, null, Weapons.Bat], | |
| new ClassStats() | |
| .setSpeed(2) | |
| .setMaxHp(125)) | |
| .setVoicelines([ | |
| "Yeah!", "Uhh, no.", "Hey, there's a spy over here!", "That frickin' {}'s a spy.", "I'm dyin' here!", "Augh!", "I don't usually kill morons this fast.", "I'm a force o' nature.", "AAAAAAAAAAAAAGGHHHH!!", "AaaaAaAAaACk!" | |
| ]), | |
| // Soldier: new Class( | |
| // "Soldier", | |
| // [null, null, null, null], | |
| // new ClassStat() | |
| // .setSpeed(1) | |
| // .setMaxHp(200)) | |
| // .setVoicelines([ | |
| // "Affirmative!", "Negatory!", "Boys, we have a traitor!", "That {} is a spy!", "Heeeelp!", "Agh!", "That was an amazing killing spree... by the other team!", "Pain is weakness leaving the body.", "AAUUUUAUUUUUUUUUUGGHH!!!!" | |
| // ]) | |
| } | |
| const playfield = [ | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], | |
| ]; | |
| let playerColMap; | |
| const player = new Mercenary("Player", Classes.Scout, { | |
| x: 14, y: 14 | |
| }); | |
| player.isPlayer = true; | |
| const mercs = [player]; | |
| let turn = 0; | |
| ///////////////////////////////////////// | |
| // UI ELEMENTS // | |
| ///////////////////////////////////////// | |
| const chatEl = document.getElementById("chat"); | |
| const chatInputEl = document.getElementById("chatinput"); | |
| const playfieldEl = document.getElementById("playfield"); | |
| const statEls = { | |
| turn: document.getElementById("p-turn"), | |
| class: document.getElementById("p-class"), | |
| hp: document.getElementById("p-health"), | |
| hpMax: document.getElementById("p-health-max"), | |
| weaponName: document.getElementById("p-wep-name"), | |
| ammoClip1: document.getElementById("p-ammo-clip"), | |
| ammoReserve1: document.getElementById("p-ammo-reserve"), | |
| status: document.getElementById("p-status"), | |
| weaponControls: [ | |
| document.getElementById("p-b-primary"), | |
| document.getElementById("p-b-secondary"), | |
| document.getElementById("p-b-tertiary"), | |
| document.getElementById("p-b-melee") | |
| ] | |
| } | |
| const leaderboardEl = document.getElementById("leaderboard"); | |
| const _DEBUG_killSelectEl = document.getElementById("d-kill-select"); | |
| function updateColMap() { | |
| playerColMap = []; | |
| for (let y = 0; y < playfield.length; y++) { | |
| playerColMap.push([]); | |
| for (let x = 0; x < playfield[y].length; x++) { | |
| playerColMap[y].push(false); | |
| } | |
| } | |
| mercs.forEach((merc) => { | |
| playerColMap[merc.pos.y][merc.pos.x] = true; | |
| }) | |
| } | |
| ///////////////////////////////////////// | |
| // UI RENDERING // | |
| ///////////////////////////////////////// | |
| function printPlayfield() { | |
| const out = []; | |
| for (let y of playfield) { | |
| for (let x of y) { | |
| out.push(x ? (x === 1 ? "#" : x) : "."); | |
| } | |
| out.push("<br>"); | |
| } | |
| mercs.forEach((merc, idx) => { | |
| const pos = (merc.pos.y * (playfield.length + 1)) + merc.pos.x; | |
| let sprite = "O"; | |
| if (merc.isPlayer) sprite = "Y" | |
| if (merc.dead) sprite = "X" | |
| out[pos] = `<span class="merc" data-name="${merc.getName()} [${merc.classType.name}]" onclick="if(player.fire(mercs[${idx}]))doTurn()">${sprite}</span>`; | |
| }); | |
| playfieldEl.innerHTML = out.join(""); | |
| } | |
| let statusText = ""; | |
| function printStats() { | |
| statEls.class.innerHTML = player.classType.name; | |
| statEls.hp.innerHTML = player.health; | |
| statEls.hpMax.innerHTML = player.classType.stats.maxHp; | |
| statEls.weaponName.innerHTML = player.loadout[player.weaponId].name; | |
| statEls.ammoClip1.innerHTML = player.clip[player.weaponId]; | |
| statEls.ammoReserve1.innerHTML = player.reserve[player.weaponId]; | |
| if (player.dead) { | |
| statEls.status.innerHTML = `Killed by ${player.lastDamage.selfInflicted ? "yourself!" : player.lastDamage.damager.getName()}<br>${player.respawnTime} turns 'til respawn` | |
| } else { | |
| statEls.status.innerHTML = statusText; | |
| } | |
| // Weapon switcher | |
| for (let wep in player.loadout) { | |
| const ctrl = statEls.weaponControls[wep]; | |
| const weapon = player.loadout[wep]; | |
| if (weapon) { | |
| ctrl.innerHTML = weapon.name.toUpperCase(); | |
| ctrl.onclick = () => { | |
| player.weaponId = wep; | |
| statusText = `Switched to ${weapon.name}...`; | |
| printStats(); | |
| } | |
| } else { | |
| ctrl.innerHTML = "NONE"; | |
| ctrl.disabled = true; | |
| } | |
| } | |
| } | |
| function printLeaderboard() { | |
| let out = ""; | |
| mercs.forEach((merc) => { | |
| out += `${merc.getName()} (${merc.kills}/${merc.deaths})<br>` | |
| }); | |
| leaderboardEl.innerHTML = out; | |
| } | |
| function printChat() { | |
| chatEl.innerHTML = chat.join("<br>"); | |
| } | |
| function doTurn() { | |
| turn++; | |
| statEls.turn.innerHTML = turn; | |
| mercs.forEach((merc) => { | |
| if (merc.dead) { | |
| merc.respawnTime--; | |
| if (merc.respawnTime === 0) merc.respawn(); | |
| } | |
| if (merc.isBot) { | |
| merc.think(); | |
| } | |
| }); | |
| printPlayfield(); | |
| printStats(); | |
| } | |
| function randArray(array) { | |
| return array[Math.floor(Math.random() * array.length)]; | |
| } | |
| function randObject(object) { | |
| const e = Object.entries(object); | |
| return e[Math.floor(Math.random() * e.length)][1]; | |
| } | |
| let chat = new Array(5).fill(""); | |
| function say(msg, system = false) { | |
| chat.shift(); | |
| chat.push(`${system ? "[System] " : ""}${msg}`); | |
| printChat(); | |
| } | |
| const botNames = ["0xDEADBEEF", "Herr Doktor", "Archimedes", "GLaDOS", "GutsAndGlory!", "HI THERE", "AmNot", "AimBot", "A Professional With Standards", "Glorified Toaster with Legs", "Zepheniah Mann", "KillMe", "Numnutz", "CRITAWKETS", "C++", "CrySomeMore", "Me", "Force of Nature", "Crazed Gunman", "NotMe", "H@XX0RZ", "Hostage", "Poopy Joe", "Pow!", "RageQuit", "Ribs Grow Back", "Saxton Hale", "Screamin' Eagles", "SMELLY UNFORTUNATE", "GENTLE MANNE of LEISURE", "MoreGun"]; | |
| function bot(callout = false) { | |
| let pos = { | |
| x: Math.floor(Math.random() * playfield.length), | |
| y: Math.floor(Math.random() * playfield[0].length) | |
| }; | |
| while (playerColMap[pos.x][pos.y]) { | |
| pos = { | |
| x: Math.floor(Math.random() * playfield.length), | |
| y: Math.floor(Math.random() * playfield[0].length) | |
| }; | |
| } | |
| const merc = ( | |
| new Bot(randArray(botNames), randObject(Classes), pos) | |
| ); | |
| mercs.push(merc); | |
| say(`${merc.getName()} joined the game`); | |
| if (callout) { | |
| merc.sayVoiceline(Math.floor(Math.random() * 9)); | |
| } | |
| printPlayfield(); | |
| printLeaderboard(); | |
| } | |
| ///////////////////////////////////////// | |
| // EVENT LISTENERS // | |
| ///////////////////////////////////////// | |
| document.addEventListener("DOMContentLoaded", () => { | |
| statusText = "Welcome to Tea Fortress 8"; | |
| updateColMap(); | |
| printPlayfield(); | |
| printStats(); | |
| printLeaderboard(); | |
| say("Wecome to Tea Fortress 8", true); | |
| for (let _ = 0; _ < 5; _++) { | |
| bot(); | |
| } | |
| updateColMap(); | |
| }); | |
| document.addEventListener("keydown", (ev) => { | |
| switch (ev.code) { | |
| case "KeyW": | |
| case "ArrowUp": | |
| player.move(0, -1); | |
| doTurn(); | |
| break; | |
| case "KeyA": | |
| case "ArrowLeft": | |
| player.move(-1, 0); | |
| doTurn(); | |
| break; | |
| case "KeyS": | |
| case "ArrowDown": | |
| player.move(0, 1); | |
| doTurn(); | |
| break; | |
| case "KeyD": | |
| case "ArrowRight": | |
| player.move(1, 0); | |
| doTurn(); | |
| break; | |
| case "KeyR": | |
| if (player.reload()) doTurn(); | |
| break; | |
| } | |
| }); | |
| chatInputEl.addEventListener("keypress", (ev) => { | |
| if (ev.code !== "Enter") return; | |
| player.say(chatInputEl.value); | |
| chatInputEl.value = ""; | |
| }); | |
| /* DEBUG */ | |
| document.getElementById("d-kill-update").onclick = () => { | |
| for (let child of _DEBUG_killSelectEl.children) { | |
| _DEBUG_killSelectEl.removeChild(child); | |
| } | |
| mercs.forEach((el, idx) => { | |
| const opt = document.createElement("option"); | |
| opt.innerHTML = `${idx} ${el.getName()}`; | |
| opt.value = idx; | |
| _DEBUG_killSelectEl.appendChild(opt); | |
| }); | |
| }; | |
| document.getElementById("d-kill-btn").onclick = () => { | |
| const merc = mercs[parseInt(_DEBUG_killSelectEl.value)]; | |
| merc.hurt(new DamageInfo(1000, player, merc, true).setWeapon(Weapons._INTERNAL_world)); | |
| doTurn(); | |
| }; | |
| document.getElementById("d-kill-from").onclick = () => { | |
| player.hurt(new DamageInfo(25, mercs[parseInt(_DEBUG_killSelectEl.value)], player)); | |
| doTurn(); | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| html { | |
| height: 100%; | |
| width: 100%; | |
| } | |
| body { | |
| font-family: monospace; | |
| font-feature-settings: "liga" 0; | |
| font-variant-ligatures: none; | |
| } | |
| #game { | |
| display: flex; | |
| flex-direction: row; | |
| gap: 1rem; | |
| } | |
| #playfield { | |
| line-height: 8px; | |
| cursor: default; | |
| } | |
| #playfield > span:hover { | |
| cursor: pointer; | |
| background-color: rgba(0, 0, 0, 0.25); | |
| } | |
| #p-stats { | |
| background-color: rgba(0, 0, 0, 0.1); | |
| border: 1px solid rgba(0, 0, 0, 0.2); | |
| padding: 0.5em; | |
| } | |
| #leaderboard { | |
| max-height: 45vh; | |
| overflow-y: auto; | |
| } | |
| pre, p { | |
| margin: 0; | |
| } | |
| button, input { | |
| border: 1px solid black; | |
| border-radius: 0; | |
| margin: 3px; | |
| background-color: black; | |
| color: white; | |
| } | |
| button:not(:disabled):hover, input:hover { | |
| background-color: white; | |
| color: black; | |
| cursor: pointer; | |
| } | |
| button:focus-visible, input:focus-visible { | |
| outline: 0; | |
| } | |
| button:disabled { | |
| opacity: 0.5; | |
| } | |
| .merc { | |
| position: relative; | |
| } | |
| .merc::after { | |
| position: absolute; | |
| top: -50%; | |
| left: 12px; | |
| background-color: black; | |
| color: white; | |
| content: attr(data-name); | |
| padding: 4px; | |
| opacity: 0; | |
| height: fit-content; | |
| width: 0; | |
| pointer-events: none; | |
| z-index: 99; | |
| } | |
| .merc:hover::after { | |
| opacity: 1; | |
| width: fit-content; | |
| cursor: help; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment