<div id="app"></div>
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P');

body {
	font-family: 'Press Start 2P', cursive;
	font-size: 16px;
}

.container {
	background-image: url('https://assets.codepen.io/430361/chicky-dice-bg.png');
	background-size: cover;
	width: 100vw;
	height: 100vh;
	position: absolute;
	top: 0;
	left: 0;
	image-rendering: pixelated;
}

.hud, .game-canvas {
	position: absolute;
	top: 0;
	left: 0;
}

.hud {
	color: #fff1e8;
	width: 100vw;
	height: 100vh;
}

.life {
	position: absolute;
	top: 1rem;
	left: 1rem;
	user-select: none;
	z-index: 2;
}

.enemy-life {
	position: absolute;
	top: 1rem;
	right: 1rem;
	user-select: none;
	z-index: 2;
}

.floor {
	position: absolute;
	top: 2.5rem;
	left: 1rem;
	user-select: none;
	z-index: 2;
}

.command {
	color: #fff1e8;
	background-color: #000000;
	width: 60vw;
	padding: 0.5rem 0;
	display: flex;
	flex-direction: column;
	flex-wrap: nowrap;
	z-index: 2;
	position: absolute;
	top: 50%;
	left: 20vw;
	transform: translateY(-50%);
	animation: fade-in 512ms ease-out;
	&.selected {
		animation: fade-out 512ms ease-out;
	}
}

.command-item {
	color: #fff1e8;
	text-decoration: none;
	padding: 0.5rem 1rem 0.5rem 2rem;
	display: block;
	position: relative;
	user-select: none;
	&.disabled {
		color: #5f574f;
	}
	&:hover:not(.disabled)::before, &.selected::before {
		content: '';
		width: 0;
		height: 0;
		border-top: solid 0.5rem transparent;
		border-bottom: solid 0.5rem transparent;
		border-left: solid 0.9rem #fff1e8;
		position: absolute;
		top: 50%;
		left: 0.5rem;
		transform: translateY(-50%);
	}
}

.dice-area {
	background-color: #000000;
	width: 100vw;
	height: 100vh;
	display: flex;
	align-items: center;
	justify-content: center;
	position: absolute;
	top: 0;
	left: 0;
	z-index: 3;
	animation: fade-in 512ms ease-out;
	&.hidden {
		animation: fade-out 512ms ease-out;
	}
}

.fade {
	background-color: #000000;
	width: 100vw;
	height: 100vh;
	position: absolute;
	top: 0;
	left: 0;
	z-index: 4;
	&.fade-in {
		animation: fade-in 512ms ease-out;
	}
	&.fade-out {
		animation: fade-out 512ms ease-out;
	}
}

.dice {
	--number: 0vmin;
	--rotation: 0deg;
	background-image: url('https://assets.codepen.io/430361/chicky-dice-game.png');
	background-position: var(--number) 0;
	background-size: 300vmin 50vmin;
	width: 50vmin;
	height: 50vmin;
	image-rendering: pixelated;
	transform: rotateZ(var(--rotation));
}

.effect {
	width: 100vw;
	height: 100vh;
	display: flex;
	align-items: center;
	justify-content: center;
	position: absolute;
	top: 0;
	left: 0;
	user-select: none;
	z-index: 1;
	animation:
		fade-in 512ms ease-out,
		fade-out 512ms ease-out 768ms;
}

.effect-image {
	background-image: var(--image-url);
	background-size: 50vmin 50vmin;
	width: 50vmin;
	height: 50vmin;
	image-rendering: pixelated;
}

@keyframes fade-in {
	0% {
		opacity: 0;
	}
	100% {
		opacity: 1;
	}
}

@keyframes fade-out {
	0% {
		opacity: 1;
	}
	100% {
		opacity: 0;
	}
}

@media screen and (min-width: 526px) {
	.container {
		background-size: contain;
	}
}

@media screen and (min-width: 961px) {
	.command {
		flex-direction: row;
	}
	
	.command-item {
		width: 50%;
	}
}
View Compiled
'use strict';

import * as THREE from 'https://cdn.skypack.dev/three@0.135.0';
import { MTLLoader } from 'https://cdn.skypack.dev/three@0.135.0/examples/jsm/loaders/MTLLoader.js';
import { DDSLoader } from 'https://cdn.skypack.dev/three@0.135.0/examples/jsm/loaders/DDSLoader.js';
import { OBJLoader } from 'https://cdn.skypack.dev/three@0.135.0/examples/jsm/loaders/OBJLoader.js';
import React, { useState, useEffect, useRef } from 'https://cdn.skypack.dev/react@18.2.0';
import ReactDOM from 'https://cdn.skypack.dev/react-dom@18.2.0';
import gsap from 'https://cdn.skypack.dev/gsap@3.10.4';

console.clear();

const IS_DEBUG = false;
const ASSETS_PATH = 'https://assets.codepen.io/430361';
const FPS = 24;
const SCREEN = [{
	FOV: 65,
	Y: 2,
	Z: 7,
}, {
	FOV: 45,
	Y: 1.15,
	Z: 6.5
}];
const DEFAULT_LIFE = 40;
const DEFAULT_ENEMY_LIFE = 10;
const COMMANDS = ['Skip', 'Defend', 'Attack', 'Heal'];

let scene;
let camera;
let renderer;

let chicky;
let ghost;
let stage;
let dice;

let renderTimeout = 0;

//////////////////////////////////////////////////
// ENUMS

enum ObjectType {
	Chicky,
	Ghost,
}

enum FadeType {
	Hidden,
	In,
	Out,
}

enum FloorType {
	None,
	Next,
	Reset,
}

//////////////////////////////////////////////////
// HELPERS

function getRadian(degree: number): number {
	return degree * Math.PI / 180;
}

function random(min: number, max: number): number {
	return Math.round(Math.random() * (max - min) + min);
}

//////////////////////////////////////////////////
// ThreeJS Settings

function setRenderer() {
	renderer.setPixelRatio(window.devicePixelRatio);
	renderer.setSize(window.innerWidth, window.innerHeight);
	// renderer.setClearColor(SKY_COLOR);
	
	renderer.shadowMap.enabled = true;
	renderer.shadowMap.type = THREE.PCFSoftShadowMap;
	
	document.body.appendChild(renderer.domElement);
}

function setLighting() {
	const ambientColor = 0xffffff;
  const ambientIntensity = 0.7;
  const ambientLight = new THREE.AmbientLight(ambientColor, ambientIntensity);
  
  scene.add(ambientLight);
	
	const directionalColor = 0xffffff;
  const directionalIntensity = 0.8;
  const directionalLight = new THREE.DirectionalLight(
		directionalColor, directionalIntensity);
	const directionalX = -3;
	const directionalY = 5;
	const directionalZ = 2;
  
  directionalLight.position.set(directionalX, directionalY, directionalZ);
	directionalLight.castShadow = true;
	directionalLight.shadow.camera.near = 0.1;
	directionalLight.shadow.camera.far = 100;
	directionalLight.shadow.bias = -0.0005;
	
  scene.add(directionalLight);
	
	if (IS_DEBUG) {
		scene.add(new THREE.CameraHelper(directionalLight.shadow.camera));
	}
}

function setScreenSettings() {
	let fov = SCREEN[0].FOV;
	let y = SCREEN[0].Y;
	let z = SCREEN[0].Z;
	
	if (window.innerWidth > 525) {
		fov = SCREEN[1].FOV;
		y = SCREEN[1].Y;
		z = SCREEN[1].Z;
	}
	
	camera.fov = fov;
	camera.position.y = y;
	camera.position.z = z;
}

//////////////////////////////////////////////////
// ThreeJS Models

function loadModel(name) {
  return new Promise((resolve, reject) => {
		const manager = new THREE.LoadingManager();

		manager.addHandler(/\.dds$/i, new DDSLoader());

		new MTLLoader(manager)
			.load(`${ASSETS_PATH}/${name}.mtl`, (materials) => {

			materials.preload();

			new OBJLoader()
				.setMaterials(materials)
				.load(`${ASSETS_PATH}/${name}.obj`, (obj) => {
				
				obj.traverse((o) => {
					o.castShadow = true;
					o.receiveShadow = true;
				});
				
				resolve(obj);
			}, undefined, (error) => {
				reject(error);
			});
		});
	});
}

async function setChicky() {
	chicky = await loadModel('RogueLikeChicky-5');
	
	scene.add(chicky);
	
	chicky.position.set(-2, 0, 0);
	chicky.traverse((obj) => {
		if (obj.isMesh === true) {
			obj.rotateY(getRadian(90));
		}
	});
}

async function setGhost() {
	ghost = await loadModel('RogueLikeGhost-6');
	
	scene.add(ghost);
	
	ghost.position.set(2, 0, 0);
	ghost.traverse((obj) => {
		if (obj.isMesh === true) {
			obj.rotateY(getRadian(-90));
		}
	});
}

async function setStage() {
	stage = await loadModel('RogueLikeStage');
	
	scene.add(stage);
	
	stage.position.set(0, -1.6, 0);
}

async function setDice() {
	dice = await loadModel('RogueLikeDice');
	
	scene.add(dice);
	
	dice.visible = false;
}

//////////////////////////////////////////////////
// Game Functions

function animateFloor(callback) {
	let y = SCREEN[0].Y;
	let z = SCREEN[0].Z;
	
	if (window.innerWidth > 525) {
		y = SCREEN[1].Y;
		z = SCREEN[1].Z;
	}

	gsap.fromTo(camera.position, {
		y: 0,
		z: 0,
	}, {
		y: y,
		z: z,
		duration: 0.75,
		ease: 'back.out(1.5)',
		onComplete() {
			callback();
		},
	});
}

function doSkip(obj: THREE.Group) {
	return new Promise((resolve) => {
		let tl = gsap.timeline();
		
		tl.to(obj.scale, {
			x: 1.2,
			y: 0.8,
			duration: 0.15,
			ease: 'power4.out',
		});
		tl.to(obj.scale, {
			x: 0.9,
			y: 1.1,
			duration: 0.15,
			ease: 'power4.out',
		});
		tl.to(obj.scale, {
			x: 1.2,
			y: 0.8,
			duration: 0.15,
			ease: 'power4.out',
		});
		tl.to(obj.scale, {
			x: 1,
			y: 1,
			duration: 1,
			ease: 'elastic.out',
			onComplete() {
				resolve();
			},
		});
	});
}

function doAttack(obj: THREE.Group, type: ObjectType) {
	return new Promise((resolve) => {
		let tl = gsap.timeline();
		
		tl.to(obj.scale, {
			x: 1.2,
			y: 0.8,
			duration: 0.3,
			ease: 'power4.out',
		});
		tl.to(obj.scale, {
			x: 1,
			y: 1,
			duration: 0.3,
			ease: 'elastic.out',
		});
		tl.to(obj.position, {
			y: 0.8,
			duration: 0.3,
			ease: 'power3.out',
		}, '-=0.3');
		tl.to(obj.position, {
			y: 0,
			duration: 0.3,
			ease: 'power3.in',
		});
		tl.to(obj.position, {
			x: (type === ObjectType.Chicky) ? 2 : -2,
			duration: 0.8,
			ease: 'power1.out',
		}, '-=0.6');
		tl.to(obj.scale, {
			x: 1.1,
			y: 0.9,
			duration: 0.1,
			ease: 'power4.out',
		});
		tl.to(obj.scale, {
			x: 1,
			y: 1,
			duration: 0.35,
			ease: 'elastic.out',
		});
		tl.to(obj.rotation, {
			y: getRadian(-25),
			z: getRadian(-10),
			ease: 'power4.out',
			duration: 0.2,
			delay: 0.05,
			onComplete() {
				let obj2 = (type === ObjectType.Chicky) ? ghost : chicky;
				let tl2 = gsap.timeline();
				
				tl2.to(obj2.position, {
					x: obj2.position.x - ((type === ObjectType.Chicky) ? -0.15 : 0.15),
					ease: 'power4.out',
					duration: 0.1,
					delay: 0.5,
				});
				tl2.to(obj2.position, {
					x: obj2.position.x + ((type === ObjectType.Chicky) ? -0.15 : 0.15),
					ease: 'bounce.out',
					duration: 0.5,
				});
			},
		});
		tl.to(obj.rotation, {
			y: getRadian(45),
			z: getRadian((type === ObjectType.Chicky) ? -20 : 20),
			ease: 'power4.inOut',
			duration: 0.5,
		});
		tl.to(obj.rotation, {
			y: 0,
			z: 0,
			ease: 'bounce.out',
			duration: 0.4,
			delay: 0.25,
		});
		tl.to(obj.position, {
			y: 0.2,
			duration: 0.3,
			ease: 'power4.out',
			delay: 0.2,
		});
		tl.to(obj.position, {
			x: (type === ObjectType.Chicky) ? -2 : 2,
			duration: 0.6,
			ease: 'power1.out',
		}, '-=0.2');
		tl.to(obj.rotation, {
			z: getRadian((type === ObjectType.Chicky) ? 10 : -10),
			duration: 0.1,
			ease: 'power1.out',
		}, '-=0.6');
		tl.to(obj.rotation, {
			z: 0,
			duration: 0.1,
			ease: 'power1.out',
		}, '-=0.3');
		tl.to(obj.position, {
			y: 0,
			duration: 0.3,
			ease: 'power4.out',
		}, '-=0.2');
		tl.to(obj.scale, {
			x: 1.1,
			y: 0.9,
			duration: 0.2,
			ease: 'power4.out',
		}, '-=0.2');
		tl.to(obj.scale, {
			x: 1,
			y: 1,
			duration: 0.4,
			ease: 'elastic.out',
			onComplete() {
				resolve();
			},
		});
	});
}

function doDefend(obj: THREE.Group, type: ObjectType) {
	return new Promise((resolve) => {
		let tl = gsap.timeline();
		
		tl.to(obj.rotation, {
			y: getRadian((type === ObjectType.Chicky) ? -90 : 90),
			ease: 'power4.out',
			duration: 0.75,
		});
		tl.to(obj.scale, {
			x: 1.2,
			y: 0.8,
			duration: 0.15,
			delay: 0.2,
			ease: 'power4.out',
		});
		tl.to(obj.scale, {
			x: 0.9,
			y: 1.1,
			duration: 0.15,
			ease: 'power4.out',
		});
		tl.to(obj.scale, {
			x: 1.2,
			y: 0.8,
			duration: 0.15,
			ease: 'power4.out',
		});
		tl.to(obj.scale, {
			x: 1,
			y: 1,
			duration: 1,
			ease: 'elastic.out',
		});
		tl.to(obj.rotation, {
			y: 0,
			ease: 'power4.out',
			duration: 0.75,
			onComplete() {
				resolve();
			},
		});
	});
}

function doHeal(obj: THREE.Group, type: ObjectType) {
	return new Promise((resolve) => {
		let tl = gsap.timeline();
		
		tl.to(obj.rotation, {
			y: getRadian((type === ObjectType.Chicky) ? 720 : -720),
			ease: 'power2.out',
			duration: 2,
		});
		tl.to(obj.position, {
			y: 1.5,
			ease: 'power2.out',
			duration: 1,
		}, '-=2');
		tl.to(obj.position, {
			y: 0,
			ease: 'power2.in',
			duration: 1,
		}, '-=1');
		tl.to(obj.scale, {
			x: 1.2,
			y: 0.8,
			duration: 0.15,
			ease: 'power4.out',
		});
		tl.to(obj.scale, {
			x: 0.9,
			y: 1.1,
			duration: 0.15,
			ease: 'power4.out',
		});
		tl.to(obj.scale, {
			x: 1.2,
			y: 0.8,
			duration: 0.15,
			ease: 'power4.out',
		});
		tl.to(obj.scale, {
			x: 1,
			y: 1,
			duration: 1,
			ease: 'elastic.out',
			onComplete() {
				obj.rotation.y = 0;

				resolve();
			},
		});
	});
}

function doCommand(command: Number, type: ObjectType) {
	let obj;
	
	if (type === ObjectType.Chicky) {
		obj = chicky;
	} else if (type === ObjectType.Ghost) {
		obj = ghost;
	}
	
	return new Promise(async (resolve) => {
		switch (command) {
			case 0:
				await doSkip(obj);
				break;
			case 1:
				await doDefend(obj, type);
				break;
			case 2:
				await doAttack(obj, type);
				break;
			case 3:
				await doHeal(obj, type);
				break;
		}

		resolve();
	});
}

function animateDefeat(type) {
	let obj;
	
	if (type === ObjectType.Chicky) {
		obj = chicky;
	} else if (type === ObjectType.Ghost) {
		obj = ghost;
	}
	
	return new Promise((resolve) => {
		let tl = gsap.timeline();
		
		tl.to(obj.scale, {
			x: 1.2,
			y: 0.8,
			duration: 0.15,
			ease: 'power4.out',
		});
		tl.to(obj.scale, {
			x: 1,
			y: 1,
			duration: 0.5,
			ease: 'elastic.out',
		});
		tl.to(obj.position, {
			y: 1.2,
			x: (type === ObjectType.Chicky) ? -2.5 : 2.5,
			duration: 0.3,
			ease: 'power4.out',
		});
		tl.to(obj.rotation, {
			z: getRadian((type === ObjectType.Chicky) ? 90 : -90),
			duration: 0.5,
			ease: 'power4.out',
		}, '-=0.3');
		tl.to(obj.position, {
			y: 0.9,
			duration: 0.5,
			ease: 'bounce.out',
			onComplete() {
				resolve();
			},
		}, '-=0.3');
	});
}

function doChangeFloor(callback) {
	return new Promise((resolve) => {
		let tl = new gsap.timeline();
		
		tl.to(stage.position, {
			x: -6,
			duration: 2,
		});
		tl.to(ghost.position, {
			x: -4,
			duration: 2,
		}, '-=2');
		tl.to(chicky.position, {
			y: 0.2,
			yoyo: true,
			ease: 'power2.out',
			repeat: 12,
			duration: 0.1,
		}, '-=2');
		tl.fromTo(chicky.rotation, {
			y: getRadian(5),
		}, {
			y: getRadian(-5),
			yoyo: true,
			ease: 'power2.inOut',
			repeat: 6,
			duration: 0.2,
			onComplete() {
				resolve();
			},
		}, '-=2');
	});
}

function repositionFloor() {
	return new Promise((resolve) => {
		let tl = gsap.timeline();

		tl.to(chicky.position, {
			x: -2,
			y: 0,
			duration: 0.1,
		})
		tl.to(chicky.rotation, {
			y: 0,
			z: 0,
			duration: 0.1,
		});
		tl.to(ghost.position, {
			x: 2,
			y: 0,
			duration: 0.1,
		});
		tl.to(ghost.rotation, {
			z: 0,
			duration: 0.1,
		});
		tl.to(stage.position, {
			x: 0,
			duration: 0.1,
			onComplete() {
				resolve();
			},
		});
	});
}

//////////////////////////////////////////////////
// Game Settings

async function create(callback) {
	setRenderer();
	
	if (IS_DEBUG) {
		scene.add(new THREE.CameraHelper(camera));
	}
	
	setLighting();
	
	await setChicky();
	await setGhost();
	await setStage();
	// await setDice();
	
	animateFloor(callback);
}

function update() {
}

function render() {
	if (performance.now() - renderTimeout > 1000 / FPS) {
		renderTimeout = performance.now();

		renderer.render(scene, camera);
	}
}

function loop() {
	requestAnimationFrame(loop);
	update();
	render();
}

//////////////////////////////////////////////////
// Initialization

async function initialize(gameCanvas, callback) {
	let fov = SCREEN[0].FOV;
	
	if (window.innerWidth > 525) {
		fov = SCREEN[1].FOV;
	}
	
	scene = new THREE.Scene();
	camera = new THREE.PerspectiveCamera(
		fov, window.innerWidth / window.innerHeight, 0.1, 1000);
	renderer = new THREE.WebGLRenderer({
		canvas: gameCanvas,
		antialias: true,
		alpha: true,
	});

	await create(callback);
	loop();
}

//////////////////////////////////////////////////
// ReactJS Components

function CommandMenu(props) {
	const [item, setItem] = useState(null);
	
	useEffect(() => {
		setItem(null);
	}, [props.shown]);

	if (props.shown === false) {
		return null;
	}
	
	let menuClassName = 'command';
	
	if (item !== null) {
		menuClassName += ' selected';
	}

	return (
		<div
			className={menuClassName}
			onAnimationEnd={(evt) => {
				if (evt.animationName === 'fade-out') {
					props.onSelect(item);
				}
			}}
		>
			{props.items.map((i, k) => {
				let itemClassName = 'command-item';
				
				if (item === k) {
					itemClassName += ' selected';
				}
				
				if (props.allowed[k] === undefined) {
					itemClassName += ' disabled';
				}
				
				return (
					<a
						href="#"
						className={itemClassName}
						onClick={(evt) => {
							evt.preventDefault();
							
							if (props.allowed[k] === undefined) {
								return;
							}

							if (item === null) {
								setItem(k);
							}
						}}
					>{i}</a>
				);
			})}
		</div>
	);
}

function DiceArea(props) {
	const [diceStyle, setDiceStyle] = useState({
		'--number': '-0vmin',
		'--rotation': '0deg',
	});
	const [diceValue, setDiceValue] = useState(null);
	
	useEffect(() => {
		if (props.shown === false) {
			return;
		}
		
		let diceOccurence = {
			count: 0,
			num: [],
			rotation: 0,
		};
		
		for (let i = 0; i < 20; i++) {
			diceOccurence.num[i] = random(1, 6);
		}
		
		gsap.fromTo(diceOccurence, {
			count: 0,
			rotation: 0,
		}, {
			count: diceOccurence.num.length - 1,
			rotation: 720,
			ease: 'power2.out',
			duration: 1.5,
			onUpdate() {
				let i = Math.floor(diceOccurence.count);
				let num = diceOccurence.num[i];
				
				setDiceStyle({
					'--number': `${num * -50}vmin`,
					'--rotation': `${diceOccurence.rotation}deg`,
				});
			},
			onComplete() {
				setTimeout(() => {
					let value = diceOccurence.num.pop() + 1;
					value = (value > 6) ? (value - 6) : value;
					setDiceValue(value);
				}, 512);
			},
		});
	}, [props.shown]);

	if (props.shown === false) {
		return null;
	}
	
	let diceAreaStyle = 'dice-area';
	
	if (diceValue !== null) {
		diceAreaStyle += ' hidden';
	}
	
	return (
		<div className={diceAreaStyle} onAnimationEnd={(evt) => {
			if (evt.animationName === 'fade-out') {
				setDiceStyle({
					'--number': '-0vmin',
					'--rotation': '0deg',
				});
				setDiceValue(null);

				props.onSelect(diceValue);
			}
		}}>
			<div className="dice" style={diceStyle}></div>
		</div>
	);
}

function Fade(props) {
	if (props.type === FadeType.Hidden) {
		return null;
	}

	let fadeClassName = 'fade';
	
	if (props.type === FadeType.In) {
		fadeClassName += ' fade-in';
	} else if (props.type === FadeType.Out) {
		fadeClassName += ' fade-out';
	}
	
	return (
		<div
			className={fadeClassName}
			onAnimationEnd={(evt) => {
				props.onFadeEnd(evt.animationName);
			}}
		></div>
	);
}

function CommandEffect(props) {
	if (props.shown === false) {
		return null;
	}
	
	let imageUrl = `url(${ASSETS_PATH}/${props.link})`;

	return (
		<div
			className="effect"
			onAnimationEnd={(evt) => {
				if (evt.animationName === 'fade-out') {
					props.onEnded();
				}
			}}
		>
			<div className="effect-image" style={{'--image-url': imageUrl}}></div>
		</div>
	);
}

function ChickyAdventure() {
	const [turn, setTurn] = useState(ObjectType.Chicky);
	const [life, setLife] = useState(DEFAULT_LIFE);
	const [enemyLife, setEnemyLife] = useState(DEFAULT_ENEMY_LIFE);
	const [defend, setDefend] = useState(0);
	const [enemyDefend, setEnemyDefend] = useState(0);
	const [floor, setFloor] = useState(1);
	const [diceCmdShown, setDiceCmdShown] = useState(false);
	const [diceShown, setDiceShown] = useState(false);
	const [diceValue, setDiceValue] = useState(null);
	const [allowedCommand, setAllowedCommand] = useState([]);
	const [fadeType, setFadeType] = useState(FadeType.Hidden);
	const [floorType, setFloorType] = useState(FloorType.None);
	const [showGameOver, setShowGameOver] = useState(false);
	const [showDefendEffect, setShowDefendEffect] = useState(false);
	const [showAttackEffect, setShowAttackEffect] = useState(false);
	const [showHealEffect, setShowHealEffect] = useState(false);

	let canvasElm = useRef(null);

	useEffect(() => {
		initialize(canvasElm.current, () => {
			setDiceCmdShown(true);
		});
		
		window.addEventListener('resize', (evt) => {
			camera.aspect = window.innerWidth / window.innerHeight;
			camera.updateProjectionMatrix();

			renderer.setSize(window.innerWidth, window.innerHeight);
			
			setScreenSettings();
		});
	}, []);
	
	useEffect(() => {
		if (diceValue === null) {
			return;
		}

		let tmpAllowedCommand = [];
		
		tmpAllowedCommand = [...tmpAllowedCommand, 0];
		
		if (diceValue >= 2) {
			tmpAllowedCommand = [...tmpAllowedCommand, 1];
		}
		
		if (diceValue >= 3) {
			tmpAllowedCommand = [...tmpAllowedCommand, 2];
		}
		
		if (diceValue >= 5) {
			tmpAllowedCommand = [...tmpAllowedCommand, 3];
		}
		
		setAllowedCommand(tmpAllowedCommand);
	}, [diceValue]);
	
	useEffect(() => {
		if (turn === ObjectType.Chicky) {
			return;
		}
		
		setDiceShown(true);
	}, [turn]);
	
	const doEnemyTurn = async (value: number) => {
		if (value < 3) {
			await doCommand(0, ObjectType.Ghost);
		} else {
			setShowAttackEffect(true);
			
			await doCommand(2, ObjectType.Ghost);
			
			let damage = value - defend;
			
			damage = (damage < 0) ? 0 : damage;
			
			setDefend(0);
			
			if (life - damage <= 0) {
				setLife(0);
				
				await animateDefeat(ObjectType.Chicky);
				
				setShowGameOver(true);
				
				return;
			} else {
				setLife(life - damage);
			}
		}
		
		setTurn(ObjectType.Chicky);
		setDiceCmdShown(true);
	};
	
	const doChickyTurn = async (value: number) => {
		switch (value) {
			case 1:
				setShowDefendEffect(true);
				break;
			case 2:
				setShowAttackEffect(true);
				break;
			case 3:
				setShowHealEffect(true);
				break;
		}
		await doCommand(value, ObjectType.Chicky);

		switch (value) {
			case 1:
				setDefend(diceValue);
				break;
			case 2:
				let damage = diceValue - enemyDefend;

				damage = (damage < 0) ? 0 : damage;
				
				setEnemyDefend(0);

				if (enemyLife - damage <= 0) {
					setEnemyLife(0);

					await animateDefeat(ObjectType.Ghost);

					changeFloor();

					return;
				} else {
					setEnemyLife(enemyLife - damage);
				}
				break;
			case 3:
				setLife(life + diceValue);
				break;
		}

		setTurn(ObjectType.Ghost);
	};
	
	const changeFloor = () => {
		setTimeout(async() => {
			await doChangeFloor();
			
			setFloorType(FloorType.Next);
			setFadeType(FadeType.In);
		}, 512);
	};
	
	const changeFloorFade = async (fadeType: FadeType) => {
		if (fadeType === 'fade-in') {
			if (floorType === FloorType.Reset) {
				setFloor(1);
				setLife(DEFAULT_LIFE);
				setEnemyLife(DEFAULT_ENEMY_LIFE);
			} else if (floorType === FloorType.Next) {
				setFloor(floor + 1);
				setEnemyLife(DEFAULT_ENEMY_LIFE + Math.floor((floor / 8) * 2));
			}
			
			await repositionFloor();
			
			setFadeType(FadeType.Out);
		} else if (fadeType === 'fade-out') {
			setFadeType(FadeType.Hidden);
			setFloorType(FloorType.None);
			
			setDiceCmdShown(true);
		}
	};
	
	return (
		<div className="container">
			<canvas className="game-canvas" ref={canvasElm}></canvas>
			<div className="hud">
				<span className="life">HP:{life}</span>
				<span className="enemy-life">Enemy:{enemyLife}</span>
				<span className="floor">Floor:{floor}</span>
			</div>
			<CommandMenu
				items={['Roll the dice']}
				allowed={[0]}
				shown={diceCmdShown}
				onSelect={(item) => {
					setDiceCmdShown(false);
					setDiceShown(true);
				}}
			/>
			<CommandMenu
				items={COMMANDS}
				allowed={allowedCommand}
				shown={diceValue !== null}
				onSelect={(item) => {
					setDiceValue(null);
					doChickyTurn(item);
				}}
			/>
			<CommandMenu
				items={['Try Again!']}
				allowed={[0]}
				shown={showGameOver}
				onSelect={(item) => {
					setFloorType(FloorType.Reset);
					setFadeType(FadeType.In);
					setShowGameOver(false);
				}}
			/>
			<CommandEffect
				shown={showDefendEffect}
				link="chicky-dice-shield.png"
				onEnded={() => {
					setShowDefendEffect(false);
				}}
			/>
			<CommandEffect
				shown={showAttackEffect}
				link="chicky-dice-sword.png"
				onEnded={() => {
					setShowAttackEffect(false);
				}}
			/>
			<CommandEffect
				shown={showHealEffect}
				link="chicky-dice-heal.png"
				onEnded={() => {
					setShowHealEffect(false);
				}}
			/>
			<DiceArea shown={diceShown} onSelect={(value) => {
				setDiceShown(false);
				
				if (IS_DEBUG === true) {
					value = 6;
				}

				if (turn === ObjectType.Chicky) {
					setDiceValue(value);
				} else {
					doEnemyTurn(value);
				}
			}} />
			<Fade
				type={fadeType}
				onFadeEnd={(type) => {
					changeFloorFade(type);
				}}
			/>
		</div>
	);
}

ReactDOM.render(<ChickyAdventure />, document.querySelector('#app'));
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.