JSFiddle - React, Tailwind, and code Playground

by tlgreg

HTML

<link rel="stylesheet" href="https://cdn.foundation5.zurb.com/foundation.css">
<div id="app">
    <section class="row">
        <div class="small-6 columns">
            <h1 class="text-center">YOU</h1>
            <div class="healthbar">
                <div class="healthbar text-center" style="margin: 0; color: white;" :style="{ width: `${player.health}%`, backgroundColor: healthColor(player.health) }">
                  <div>{{ player.health }}</div>
                </div>
            </div>
        </div>
        <div class="small-6 columns">
            <h1 class="text-center">MONSTER</h1>
            <div class="healthbar">
                <div class="healthbar text-center" style="margin: 0; color: white;" :style="{ width: `${monster.health}%`, backgroundColor: healthColor(monster.health) }">
                  <div>{{ monster.health }}</div>
                </div>
            </div>
        </div>
    </section>
    <section class="row controls" v-if="!ingame">
        <div class="small-12 columns">
            <button id="start-game" @click="startGame">START NEW GAME</button>
        </div>
    </section>
    <section class="row controls" v-if="ingame">
        <div class="small-12 columns">
            <button id="attack" @click="playerRound('attack')">ATTACK</button>
            <button id="special-attack" @click="playerRound('special')">SPECIAL ATTACK</button>
            <button id="heal" @click="playerRound('heal')">HEAL</button>
            <button id="give-up" @click="playerRound('giveup')">GIVE UP</button>
        </div>
    </section>
    <section class="row log" v-if="actions.length > 0">
        <div class="small-12 columns">
            <ul>
                <li v-for="log in actions" :class="{ 'player-turn': log.player, 'monster-turn': !log.player }">
                  <span class="action">{{ actionIcon(log.action, log.success, log.player) }}</span> {{ log.player ? 'YOU' : 'The MONSTER' }} {{ actionText(log.action, log.success, log.ammount) }}
...

SCSS

.text-center {
    text-align: center;
}

.healthbar {
    width: 80%;
    min-width: 1em;
    height: 40px;
    background-color: #eee;
    margin: auto;
    transition: width 500ms;
    &.text-center > div {
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
    }
}

.controls, .log {
    margin-top: 30px;
    text-align: center;
    padding: 10px;
    border: 1px solid #ccc;
    box-shadow: 0px 3px 6px #ccc;
}

.turn {
    margin-top: 20px;
    margin-bottom: 20px;
    font-weight: bold;
    font-size: 22px;
}

.log ul {
    list-style: none;
    font-weight: bold;
    text-transform: uppercase;
}

.log ul li {
    margin: 5px;
}

.log ul .player-turn {
    color: blue;
    background-color: #e4e8ff;
}

.log ul .monster-turn {
    color: red;
    background-color: #ffc0c1;
}

button {
    font-size: 20px;
    background-color: #eee;
    padding: 12px;
    box-shadow: 0 1px 1px black;
    margin: 10px;
}

#start-game {
    background-color: #aaffb0;
}

#start-game:hover {
    background-color: #76ff7e;
}

#attack {
    background-color: #ff7367;
}

#attack:hover {
    background-color: #ff3f43;
}

#special-attack {
    background-color: #ffaf4f;
}

#special-attack:hover {
    background-color: #ff9a2b;
}

#heal {
    background-color: #aaffb0;
}

#heal:hover {
    background-color: #76ff7e;
}

#give-up {
    background-color: #ffffff;
}

#give-up:hover {
    background-color: #c7c7c7;
}

button {
  user-select: none;
}

Babel + JSX

// easy monster, less random wins if alternating between "attack" and "special attack"
// spamming only "attack" or only "special attack" mostly means a loss
// with alternating healing is rarely needed

const logger = { log: [] }

class Character {
	constructor() {
  	this.health = this.maxHealth = 100
    this.mana = this.maxMana = 10
    this.attr = {
    	attack: 0.5,
      defense: 0.5,
      strength: 10,
      skill: 3,
      magic: 10,
    }
    this.magic = {
    	heal: 2,
    }
    this.rng = {
    	attack: () => this.rand(0, 1),
    	damage: () => this.rand(0, 10),
      special: () => this.rand(0, 0.5),
      heal: () => this.rand(5, 10),
    }
  }
  toString() {
  	return 'Character'
  }
  between(min, max, num) {
  	return num < min ? min : num > max ? max : num
  }
  rand(min, max) {
  	return min + Math.ceil(Math.random() * (max - min))
  }
  attack(target) {
  	const success = this.between(0, 1, this.attr.attack + this.rng.attack()) > target.defense()
    if (success) {
    	logger.log.push({ character: this, success: true, message: `Attacked the ${target}!` })
      target.damage(this.attr.strength + this.rng.damage())
    } else {
    	logger.log.push({ character: this, success: false, message: `Missed an attack!` })
    }
  }
  special(target) {
  	const success = this.between(0, 1, this.attr.attack + this.rng.special()) > target.defense()
    if (success) {
    	logger.log.push({ character: this, success: true, message: 'Done a special attack!' })
      target.damage(this.attr.strength * (this.skill / 2) + this.rng.damage() * this.skill)
    } else {
    	logger.log.push({ character: this, success: false, message: 'Failed with a special attack!' })
    }
  }
  heal() {
  	const cost = this.magic.heal
    const success = this.mana >= cost
    if (success) {
    	this.mana = this.between(0, this.maxMana, this.mana - cost)
      const pow = this.attr.magic + this.rng.heal()
      const dmg = this.maxHealth - this.health
      const healing = pow...