Saturday 21 September 2024

Web Tutorial: ReactJS Liar's Dice (Part 1/4)

At long last, after a lengthy break, we're back to making ReactJS apps again! This was partly due to the fact that my Lenovo died on me last year and I waited till I had moved house, to replace it. I could have burdened my MacBook with the task, but nah. In effect, I got a new secondhand laptop and installed the necessary on it, and one of the first things I did was make this app.

This is what I used to play when I was hanging out at bars and engaging in drunken games of Liar's Dice. The rules are simple - shake the dice and try to guess the total combined numbers of the dice that your opponent and you have. If you guess right, you win and your opponent loses. If you guess wrong, the reverse is true. And vice versa for your opponent's guess. Whomever loses, drinks.

Thus, the flow goes like this...
1. Shake the dice.
2. First guess by opponent
3. Player guesses or opens up.
3.1 If player guesses, in the guess, either the number of dice or the dice itself must be higher than the previous guess. Then it is the opponent's turn to guess, or open up. GO back to 2.
3.2 If player opens up, the total number of dice that are either 1 or the guessed value, are counted. If the guess is correct, the guesser wins.

Start creating the app!

If you've done this before, carry on with setting up whatever's needed. If you need instructions on how to do this in the command line interface, go here.

You'll wind up with a bunch of HTML, CSS and JavaScript files in various folders. Let's start making alterations to prepare for writing the app.

public/index.html
<title>Liars' Dice</title>


We'll write some very general-purpose CSS in this existing file. These new CSS classes are for layout - width and float properties only. Our components will be using these extensively.

src/index.css
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  background-color: rgb(0, 0, 0);
  color: rgba(255, 255, 255, 0.8);
  font-size: 10px;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

.hidden {
    display: none;
}

.invisible {
    visibility: hidden;
}

.left {
  float: left;
}

.right {
  float: right;
}

.width_half {
  width: 49%;
}

.width_long {
  width: 74%;
}

.width_short {
  width: 25%;
}


Clear out the contents of this file. Then start writing. There's a CSS file imported, and should already exist.

src/App.js
import React, { useState } from 'react';
import './App.css';


Clear this CSS file out as well. The first CSS class we'll write is App, and that's the container for the entire app.

src/App.css
.App {
  width: 850px;
  height: 600px;
  overflow: hidden;
  margin: 0 auto 0 auto;
}


Now let's write some more boilerplate.

src/App.js
import React, { useState } from 'react';
import './App.css';

function App() {
  return (
    <div className="App">

    </div>
  );
}

export default App;


Declare some variables. These will influence what is displayed, and will be passed down into individual components. lang is the language used, and we'll use "cn" as the default value. dialogSpeed is the number of milliseconds that will take dialog-related transitions to complete. stage is an integer from 0 to 6, and gameStarted is a Boolean value which is set to indicate if the game is in progress.

src/App.js
function App() {
  const [lang, setLang] = useState("cn");
  const [dialogSpeed, setDialogSpeed] = useState(500);
  const [stage, setStage] = useState(0);
  const [gameStarted, setGameStarted] = useState(false);


  return (
    <div className="App">

    </div>
  );
}


In here, we have a div with an id of Intro. If gameStarted is true, it will be styled using the hidden CSS class and won't show up on screen.

src/App.js
return (
  <div className="App">
    <div id="Intro" className={ (gameStarted ? "hidden" : "") }>

    </div>

  </div>
);


But when it does show up, it'll take full width and have this specified height.

src/App.css
.App {
  width: 850px;
  height: 600px;
  overflow: hidden;
  margin: 0 auto 0 auto;
}

#Intro {
  width: 100%;
  height: 540px;
}


We follow up with another div, with an id of Dashboard.

src/App.js
return (
  <div className="App">
    <div id="Intro" className={ (gameStarted ? "hidden" : "") }>

    </div>

    <div id="Dashboard">

    </div>

  </div>
);


We'll style it this way. It has a low width, and is either supposed to appear at the bottom of the screen (if gameStarted is false) or at the top (if gameStarted is false). Either way, it always appears, and always below Intro. The ony difference is that Intro will not take up any screen space depending on the value of gameStarted.

src/App.css
.App {
  width: 850px;
  height: 600px;
  overflow: hidden;
  margin: 0 auto 0 auto;
}

#Intro {
  width: 100%;
  height: 540px;
}

#Dashboard {
  width: 100%;
  height: 60px;
  float: left;
}


And this is how it should look like.


Cool! Let's add some flesh to the skeleton. For the Intro page, we want images and a Start button. So let's prepare some columns inside the Intro div. We add five divs, each styled using the col CSS class.

src/App.js
<div id="Intro" className={ (gameStarted ? "hidden" : "") }>
  <div className="col">

  </div>

  <div className="col">

  </div>

  <div className="col">

  </div>

   <div className="col">

  </div>

  <div className="col">

  </div>

</div>


In the CSS, you'll want to specify a small width and tall height, then float left. There's a bit of space in the left and right margins, and we'll want the overflow property to be hidden. This will be important later with animation.

src/App.css
#Intro {
  width: 100%;
  height: 540px;
}

.col {
  width: 164px;
  height: 540px;
  float: left;
  margin-left: 3px;
  margin-right: 3px;
  overflow: hidden;
}


#Dashboard {
  width: 100%;
  height: 60px;
  float: left;
}


There be your columns.


Now let's add some images that will fill up those columns. I first insert a div. If stage is 0, we will style it using introimage and introimage1. For the time being, we will give it the same class even if stage is not 0.

src/App.js
<div id="Intro" className={ (gameStarted ? "hidden" : "") }>
  <div className="col">
    <div className={ (stage === 0 ? "introimage introimage1" : "introimage introimage1") }>
      
    </div>

  </div>

  <div className="col">

  </div>

  <div className="col">

  </div>

   <div className="col">

  </div>

  <div className="col">

  </div>
</div>


Do the same for the other divs. Except, where introimage1 appears, replace them with introimage2, introimage3 and so on.

src/App.js
<div id="Intro" className={ (gameStarted ? "hidden" : "") }>
  <div className="col">
    <div className={ (stage === 0 ? "introimage introimage1" : "introimage introimage1") }>
      
    </div>
  </div>

  <div className="col">
    <div className={ (stage === 0 ? "introimage introimage2" : "introimage introimage2") }>
      
    </div>

  </div>

  <div className="col">
    <div className={ (stage === 0 ? "introimage introimage3" : "introimage introimage3") }>
      
    </div>

  </div>

  <div className="col">
    <div className={ (stage === 0 ? "introimage introimage4" : "introimage introimage4") }>
      
    </div>

  </div>

  <div className="col">
    <div className={ (stage === 0 ? "introimage introimage5" : "introimage introimage5") }>
      
    </div>

  </div>
</div>


Here are the images we're going to use. I generated them from an AI image generation app. Save them in the src folder inside the img directory. The filenames look weird right now, but it's for a purpose. We'll get into that later.
src/img/opponent1_75.png

src/img/opponent2_75.png

src/img/opponent3_75.png

src/img/opponent4_75.png

src/img/opponent5_75.png

Now, in the CSS, we define introimage. The  height and width is about 4 pixels smaller than col, with some round corners via the border-radius property. And then we'll give it a 2 pixel black border on all sides (thus explaining the 4 pixel difference). The background-related properties ensure that whatever image we choose for this, it will cover the div completely with no repeats.

src/App.css
.col {
  width: 164px;
  height: 540px;
  float: left;
  margin-left: 3px;
  margin-right: 3px;
  overflow: hidden;
}

.col .introimage {
  width: 160px;
  height: 536px;
  border-radius: 10px;
  border: 2px solid rgba(0, 0, 0, 0.5);
  background-size: cover;
  background-position: center center;
  background-repeat: no-repeat;
}


Of course, here we have the CSS classes for introimage1, introimage2, introimage3, intrimage4 and introimage5. They'll ll use the images of the incredibly sexy Anime babes I showed you earlier.

src/App.css
.col {
  width: 164px;
  height: 540px;
  float: left;
  margin-left: 3px;
  margin-right: 3px;
  overflow: hidden;
}

.col .introimage {
  width: 160px;
  height: 536px;
  border-radius: 10px;
  border: 2px solid rgba(0, 0, 0, 0.5);
  background-size: cover;
  background-position: center center;
  background-repeat: no-repeat;
}

.introimage1 {
  background-image: url(img/opponent1_75.png);
}

.introimage2 {
  background-image: url(img/opponent2_75.png);
}

.introimage3 {
  background-image: url(img/opponent3_75.png);
}

.introimage4 {
  background-image: url(img/opponent4_75.png);
}

.introimage5 {
  background-image: url(img/opponent5_75.png);
}


There you go...


Add a div with an id of IntroStart, in the middle column. We'll add a conditional block to check for the value of stage, but we will style it the same either way, for now.

src/App.js
<div className="col">
  <div className={ (stage === 0 ? "introimage introimage3" : "introimage introimage3") }>
    
  </div>
  <div id="IntroStart" className={ (stage === 0 ? "" : "") }>

  </div>

</div>


This is how we style IntroStart. We want position to be absolute because width is greater than the width of its parent. We specify negative values for margin-left and margin-top, because the div comes right after another div within the same parent, and we want it aligned to the middle, superimposing both its sibling and its parent. And then we give it a translucent black color for the background and round corners.

src/App.css
.introimage5 {
  background-image: url(img/opponent5_75.png);
}

#IntroStart {
  position: absolute;
  width: 300px;
  height: 200px;
  margin-left: -70px;
  margin-top: -350px;
  background-color: rgba(0, 0, 0, 0.8);
  border-radius: 20px;
}


#Dashboard {
  width: 100%;
  height: 60px;
  float: left;
}


See it?


Here's another image we will use now, one that I just cobbled together from stock images and some text art. Again, save it in the img directory.
src/img/logo.png

Now add a div inside IntroStart and style it using the logo CSS class.

src/App.js
<div id="IntroStart" className={ (stage === 0 ? "" : "") }>
    <div className="logo">

    </div>

</div>


This logo will be used in other places, so we will style logo this way. For the logo CSS class, define some background-related properties. The image we will use is what I've just shown you, the size is set to contain because we want all of the image to fit in, and the rest of it is just alignment.

src/App.css
#Dashboard {
  width: 100%;
  height: 60px;
  float: left;
}

.logo {
  background-image: url(img/logo.png);
  background-size: contain;
  background-position: center center;
  background-repeat: no-repeat;  
}


Then we specify that only logo within IntroStart will have this width and height.

src/App.css
#Dashboard {
  width: 100%;
  height: 60px;
  float: left;
}

.logo {
  background-image: url(img/logo.png);
  background-size: contain;
  background-position: center center;
  background-repeat: no-repeat;  
}

#IntroStart .logo {
  width: 250px;
  height: 150px;
}


Got that?


Next, we need a button. Add a button tag next to that div, style it using the actionButton CSS class, and add some text in the button.

src/App.js
<div id="IntroStart" className={ (stage === 0 ? "" : "") }>
    <div className="logo">

    </div>
    <br />    
    <button className="actionButton">BEGIN &#9658;</button>

</div>


Here's some styling for actionButton. It will have a white border, rounded corners and centered. There will be a different styling for when it's disabled. That will come in useful later, because pretty much all the other buttons in the app are styled the same way.

src/App.css
#Dashboard {
  width: 100%;
  height: 60px;
  float: left;
}

.actionButton {
  background-color: rgba(255, 255, 255, 0.2);
  color: rgba(255, 255, 255, 0.8);
  padding: 5px 10px 5px 10px;
  border: 1px solid rgba(255, 255, 255, 0.8);
  border-radius: 5px;
  margin: 0 auto 0 auto;
  display: block;
}

.actionButton:disabled {
  background-color: rgba(0, 0, 0, 0.2);
  color: rgba(55, 55, 55, 0.8);
  border: 1px solid rgba(55, 55, 55, 0.8);
}


.logo {
  background-image: url(img/logo.png);
  background-size: contain;
  background-position: center center;
  background-repeat: no-repeat;  
}


There's your button! We will add actions to it soon.


Let's move on to the Dashboard div. We want two divs in there. The first will be styled using left and width_half, and if gameStarted is true, it will also be styled using invisible. Since gameStarted is true by default, that makes it invisible and therefore we won't spend much time on it for now. The other div is styled using right and width_half.

src/App.js
<div id="Dashboard">
  <div className={ (gameStarted ? "left width_half" : "left width_half invisible") }>

  </div>

  <div className="right width_half">            

  </div>  

</div>


Now in this other div, add two label tags with ids. Put in some text with a non-breaking space, for the labels.

src/App.js
<div id="Dashboard">
  <div className={ (gameStarted ? "left width_half" : "left width_half invisible") }>

  </div>

  <div className="right width_half">            
    <label id="DashboardLanguage">
      Language 
    </label>

    <label id="DashboardDialogSpeed">
      Dialog Speed 
    </label>

  </div>  
</div>


Inside the first label tag, have a drop-down list. In it, you have two options - "CN" and "EN". This drop-down list basically changes the language setting on the app.

src/App.js
<div id="Dashboard">
  <div className={ (gameStarted ? "left width_half" : "left width_half invisible") }>

  </div>

  <div className="right width_half">            
    <label id="DashboardLanguage">
      Language 
      <select>
        <option value="cn">CN</option>
        <option value="en">EN</option>
      </select>
    </label>


    <label id="DashboardDialogSpeed">
      Dialog Speed 
    </label>
  </div>  
</div>


And to facilitate this, we use the onChange attribute to run the setLang() function, with the drop-down list's selected value passed in as an argument.

src/App.js
<div id="Dashboard">
  <div className={ (gameStarted ? "left width_half" : "left width_half invisible") }>

  </div>

  <div className="right width_half">            
    <label id="DashboardLanguage">
      Language 
      <select onChange={ (e)=>{ setLang(e.currentTarget.value); }}>
        <option value="cn">CN</option>
        <option value="en">EN</option>
      </select>
    </label>

    <label id="DashboardDialogSpeed">
      Dialog Speed 
    </label>
  </div>  
</div>


We'll do something similar with Dialog Speed.

src/App.js
<div id="Dashboard">
  <div className={ (gameStarted ? "left width_half" : "left width_half invisible") }>
    <div className="logo"></div>
    <div className="stage">
      <span className="stageName"></span>
      <br /><span className="opponentName"></span>
    </div>
  </div>

  <div className="right width_half">            
    <label id="DashboardLanguage">
      Language 
      <select onChange={ (e)=>{ setLang(e.currentTarget.value); }}>
        <option value="cn">CN</option>
        <option value="en">EN</option>
      </select>
    </label>

    <label id="DashboardDialogSpeed">
      Dialog Speed 
      <select onChange={ (e)=>{ setDialogSpeed(e.currentTarget.value); }}>
        <option value="500">Fast</option>
        <option value="1000">Medium</option>
        <option value="1500">Slow</option>
      </select>

    </label>
  </div>  
</div>


Let's style label tags. It's mostly display stuff. I want it floated right and in a smaller font.

src/App.css
#Dashboard {
  width: 100%;
  height: 60px;
  float: left;
}

label {
  margin-top: 5px;
  font-size: 0.8em;
  display: block;
  float: right;
  text-align: right;
  width: 100%;
}


.actionButton {
  background-color: rgba(255, 255, 255, 0.2);
  color: rgba(255, 255, 255, 0.8);
  padding: 5px 10px 5px 10px;
  border: 1px solid rgba(255, 255, 255, 0.8);
  border-radius: 5px;
  margin: 0 auto 0 auto;
  display: block;
}


And then we'll style the select tags inside Dashboard.

src/App.css
#Dashboard {
  width: 100%;
  height: 60px;
  float: left;
}

#Dashboard select {
  font-size: 8px;
  margin-top: 5px;
  width: 5em;
}


label {
  margin-top: 5px;
  font-size: 0.8em;
  display: block;
  float: right;
  text-align: right;
  width: 100%;
}


The dashboard controls should appear in the bottom right.


As you've probably surmised, this is supposed to be a multi-lingual app. Thus, we're going to write a utility to get the correct string when the language is changed. Create the directory utils inside src. Then create the file GetLabels.js. We'll start by defining the GetLabels() function. It has two parameters - labelName and lang. And we'll make GetLabels accessible with the export statement.

src/utils/GetLabels.js
const GetLabels = (labelName, lang) => {

}

export default GetLabels;


Create the labels array.

src/utils/GetLabels.js
const GetLabels = (labelName, lang) => {
  let labels = [

  ];

}

export default GetLabels;


Here are two objects to add to labels. Each object needs a labelName, lang and value properties. For this example, we're adding elements that describe the "start" label. And we'll have the values for "start" for English and Chinese.

src/utils/GetLabels.js
const GetLabels = (labelName, lang) => {
  let labels = [
    { labelName: "start", lang: "en", value: "BEGIN"},
    { labelName: "start", lang: "cn", value: "开始"}
  ];
}

export default GetLabels;


Declare match. Run the filter() method on labels and filer out the elements where labelName and lang match the parameter values. Then assign the resultant array to match. There should be only one element, but just in case, return an empty string if match is an empty array. But if all goes as planned, return the value property of the first (and only) element of match.

src/utils/GetLabels.js
const GetLabels = (labelName, lang) => {
  let labels = [
    { labelName: "start", lang: "en", value: "BEGIN"},
    { labelName: "start", lang: "cn", value: "开始"}
  ];

  let match = labels.filter((x)=> { return (x.labelName === labelName && x.lang === lang); });

  if (match.length === 0) return "";
  return match[0].value;

}

export default GetLabels;


Now add this import statement in App.js.

src/App.js
import React, { useState } from 'react';
import './App.css';

import GetLabels from './utils/GetLabels';

function App() {


In the IntroStart div use a call to the GetLabels() function instead of the hardcoded string "BEGIN".

src/App.js
<div id="IntroStart" className={ (stage === 0 ? "" : "") }>
    <div className="logo">

    </div>
    <br />
    <button className="actionButton" >{ GetLabels("start", lang) } &#9658;</button>
</div>


And you'll see that the text for the button has changed! Try changing the language to "EN", and see if it changes back.


More to add! Namely, the labels, and the option text for Dialog Speed.

src/App.js
<div className="right width_half">            
  <label id="DashboardLanguage">
    { GetLabels("language", lang) } 
    <select onChange={ (e)=>{ setLang(e.currentTarget.value); }}>
      <option value="cn">CN</option>
      <option value="en">EN</option>
    </select>
  </label>

  <label id="DashboardDialogSpeed">
    { GetLabels("dialogSpeed", lang) } 
    <select onChange={ (e)=>{ setDialogSpeed(e.currentTarget.value); }}>
      <option value="500">{ GetLabels("fast", lang) }</option>
      <option value="1000">{ GetLabels("medium", lang) }</option>
      <option value="1500">{ GetLabels("slow", lang) }</option>
    </select>
  </label>
</div>


We will also need to add more values to the labels array.

src/utils/GetLabels.js
let labels = [
  { labelName: "start", lang: "en", value: "BEGIN"},
  { labelName: "start", lang: "cn", value: "开始"},
  { labelName: "language", lang: "en", value: "Language"},
  { labelName: "language", lang: "cn", value: "语言"},
  { labelName: "dialogSpeed", lang: "en", value: "Dialog Speed"},
  { labelName: "dialogSpeed", lang: "cn", value: "对话速度"},

  { labelName: "fast", lang: "en", value: "Fast"},
  { labelName: "fast", lang: "cn", value: "快"},
  { labelName: "medium", lang: "en", value: "Medium"},
  { labelName: "medium", lang: "cn", value: "中"},
  { labelName: "slow", lang: "en", value: "Slow"},
  { labelName: "slow", lang: "cn", value: "慢"}

];


On the bottom right, you'll see the changes!


All good so far? Time to step this up a notch...

As you may have noticed, stage and gameStarted control certain visual elements. We absolutely want to get back to that. First, let's make the Start button do something. Using the onClick attribute, make it run the start() function.

src/App.js
<button onClick={ ()=>{ start(); }} className="actionButton" >{ GetLabels("start", lang) } &#9658;</button>


Time to create the function. We start by running setStage() to set stage to 1.

src/App.js
function App() {
  const [lang, setLang] = useState("cn");
  const [dialogSpeed, setDialogSpeed] = useState(500);
  const [stage, setStage] = useState(0);
  const [gameStarted, setGameStarted] = useState(false);

  function start() {
    setStage(1);
  }


  return (
    <div className="App">


And after a delay of about two seconds (via the setTimeout() function), set gameStarted to true. What's the delay for, you may ask? Tell you later.

src/App.js
function start() {
  setStage(1);
  setTimeout(()=> {
      setGameStarted(true);
    },
    2000
  );
}


For now, now that stage is no longer 0, there are some changes to the dashboard. Remember this div inside Dashboard? Well, add a div, styled using the logo CSS class, to it. Then add another div, styled using the stage CSS class, to it.

src/App.js
<div id="Dashboard">
  <div className={ (gameStarted ? "left width_half" : "left width_half invisible") }>
    <div className="logo"></div>
    <div className="stage"></div>

  </div>

  <div className="right width_half">            
    <label id="DashboardLanguage">
      { GetLabels("language", lang) } 
      <select data-testid="dashboard-language" onChange={ (e)=>{ setLang(e.currentTarget.value); }}>
        <option value="cn">CN</option>
        <option value="en">EN</option>
      </select>
    </label>

    <label id="DashboardDialogSpeed">
      { GetLabels("dialogSpeed", lang) } 
      <select data-testid="dashboard-dialogspeed" onChange={ (e)=>{ setDialogSpeed(e.currentTarget.value); }}>
        <option value="500">{ GetLabels("fast", lang) }</option>
        <option value="1000">{ GetLabels("medium", lang) }</option>
        <option value="1500">{ GetLabels("slow", lang) }</option>
      </select>
    </label>
  </div>
</div>


And in here, add two span tags separated by a break. Style them using the stageName and opponentName CSS classes, respectively.

src/App.js
<div className="stage">
  <span className="stageName"></span>
  <br /><span className="opponentName"></span>

</div>


Time to style! Remember logo? Now, logo within Dashboard will be a different size, much smaller. and its float property will be left.

src/App.css
.logo {
  background-image: url(img/logo.png);
  background-size: contain;
  background-position: center center;
  background-repeat: no-repeat;  
}

#IntroStart .logo {
  width: 250px;
  height: 150px;
}


#Dashboard .logo {
  width: 100px;
  height: 50px;
  float: left;
}

div {
  outline: 1px solid red;
}


The next div, stage, has a specified width, is floated left, and we've specified a font size.

src/App.css
.logo {
  background-image: url(img/logo.png);
  background-size: contain;
  background-position: center center;
  background-repeat: no-repeat;  
}

#IntroStart .logo {
  width: 250px;
  height: 150px;
}

#Dashboard .logo {
  width: 100px;
  height: 50px;
  float: left;
}

#Dashboard .stage {
  width: 200px;
  float: left;
  font-size: 16px;
}


div {
  outline: 1px solid red;
}


stageName and opponentName are also styled. Purely aesthetics here.

src/App.css
.logo {
  background-image: url(img/logo.png);
  background-size: contain;
  background-position: center center;
  background-repeat: no-repeat;  
}

#IntroStart .logo {
  width: 250px;
  height: 150px;
}

#Dashboard .logo {
  width: 100px;
  height: 50px;
  float: left;
}

#Dashboard .stage {
  width: 200px;
  float: left;
  font-size: 16px;
}

.stageName{
  font-size: 1.2em;
}

.opponentName{
  font-size: 1.2em;
  font-weight:bold;
}


div {
  outline: 1px solid red;
}


After clicking the button, this should happen after two seconds. The cute girls disappear and the dashboard is moved to the top. The left side is now visible! You should see the much smaller logo.


In here, let's set the value. If stage is 0 or 6, we show nothing.

src/App.js
<div className="stage">
  <span className="stageName">{ stage === 0 || stage === 6? "" :  }</span>
  <br /><span className="opponentName"></span>
</div>


But otherwise, we use GetLabels() to get the label for "stage1", "stage2", "stage3", "stage4" or "stage5", with a colon added to the end.

src/App.js
<div className="stage">
  <span className="stageName">{ stage === 0 || stage === 6? "" : GetLabels("stage" + stage, lang) + ": " }</span>
  <br /><span className="opponentName"></span>
</div>


Here, we do something similar, except we grab the label for "opponent1", "opponent2", "opponent3", "opponent4" or "opponent5".

src/App.js
<div className="stage">
  <span className="stageName">{ stage === 0 || stage === 6? "" : GetLabels("stage" + stage, lang) + ": " }</span>
  <br /><span className="opponentName">{ stage === 0 || stage === 6? "" : GetLabels("opponent" + stage, lang) }</span>
</div>


Add these to the labels array.

src/utils/GetLabels.js
let labels = [
  { labelName: "start", lang: "en", value: "BEGIN"},
  { labelName: "start", lang: "cn", value: "开始"},
  { labelName: "language", lang: "en", value: "Language"},
  { labelName: "language", lang: "cn", value: "语言"},
  { labelName: "dialogSpeed", lang: "en", value: "Dialog Speed"},
  { labelName: "dialogSpeed", lang: "cn", value: "对话速度"},
  { labelName: "fast", lang: "en", value: "Fast"},
  { labelName: "fast", lang: "cn", value: "快"},
  { labelName: "medium", lang: "en", value: "Medium"},
  { labelName: "medium", lang: "cn", value: "中"},
  { labelName: "slow", lang: "en", value: "Slow"},
  { labelName: "slow", lang: "cn", value: "慢"},
  { labelName: "stage1", lang: "en", value: "STAGE ONE"},
  { labelName: "stage1", lang: "cn", value: "第一关"},
  { labelName: "stage2", lang: "en", value: "STAGE TWO"},
  { labelName: "stage2", lang: "cn", value: "第二关"},
  { labelName: "stage3", lang: "en", value: "STAGE THREE"},
  { labelName: "stage3", lang: "cn", value: "第三关"},
  { labelName: "stage4", lang: "en", value: "STAGE FOUR"},
  { labelName: "stage4", lang: "cn", value: "第四关"},
  { labelName: "stage5", lang: "en", value: "STAGE FIVE"},
  { labelName: "stage5", lang: "cn", value: "第五关"},
  { labelName: "opponent1", lang: "en", value: "LITTLE GRASS"},
  { labelName: "opponent1", lang: "cn", value: "小草"},
  { labelName: "opponent2", lang: "en", value: "CHANEL"},
  { labelName: "opponent2", lang: "cn", value: "CHANEL"},
  { labelName: "opponent3", lang: "en", value: "LULU"},
  { labelName: "opponent3", lang: "cn", value: "鹿鹿"},
  { labelName: "opponent4", lang: "en", value: "GONGSUN LAN"},
  { labelName: "opponent4", lang: "cn", value: "公孙蓝"},
  { labelName: "opponent5", lang: "en", value: "BIG SISTER SPRING"},
  { labelName: "opponent5", lang: "cn", value: "春姐"}

];


There! If we change the language to English, you can see this much plainer!


Time for some animation...

You'll have noticed that when you press the Start button, there's a two-second delay before the changes in the interface occur. That delay is meant for the animations to complete.

Add the floatup CSS class to these conditional blocks.

src/App.js
<div id="Intro" className={ (gameStarted ? "hidden" : "") }>
  <div className="col">
    <div className={ (stage === 0 ? "introimage introimage1" : "introimage introimage1 floatup") }>
    
    </div>
  </div>

  <div className="col">
    <div className={ (stage === 0 ? "introimage introimage2" : "introimage introimage2 floatup") }>
    
    </div>
  </div>

  <div className="col">
    <div className={ (stage === 0 ? "introimage introimage3" : "introimage introimage3 floatup") }>
    
    </div>
    <div id="IntroStart" className={ (stage === 0 ? "" : "") }>
        <div className="logo">

        </div>
        <br />
        <button onClick={ ()=>{ start(); }} className="actionButton" >{ GetLabels("start", lang) } &#9658;</button>
    </div>
  </div>

  <div className="col">
    <div className={ (stage === 0 ? "introimage introimage4" : "introimage introimage4 floatup") }>
    
    </div>
  </div>

  <div className="col">
    <div className={ (stage === 0 ? "introimage introimage5" : "introimage introimage5 floatup") }>
    
    </div>
  </div>
</div>


And this is the styling for floatup. We want the content to move upwards, so we set the margin-top property to a large negative value. And let's also set the transition-duration property to 1 second.

src/App.css
.col .introimage {
  width: 160px;
  height: 536px;
  border-radius: 10px;
  border: 2px solid rgba(0, 0, 0, 0.5);
  background-size: cover;
  background-position: center center;
  background-repeat: no-repeat;
}

.floatup {
  margin-top: -800px;
  transition-duration: 1s;
}


.introimage1 {
  background-image: url(img/opponent1_75.png);
}


See? When you click the button, the cute ladies all move upwards!


Let's improve this. In the CSS, specify transition-delay property for introimage1 to introimage5. Note that each of these values are different, with introimage1's being the smallest and introimage5's being the largest.

src/App.css
.introimage1 {
  transition-delay: 0.2s;
  background-image: url(img/opponent1_75.png);
}

.introimage2 {
  transition-delay: 0.4s;
  background-image: url(img/opponent2_75.png);
}

.introimage3 {
  transition-delay: 0.6s;
  background-image: url(img/opponent3_75.png);
}

.introimage4 {
  transition-delay: 0.8s;
  background-image: url(img/opponent4_75.png);
}

.introimage5 {
  transition-delay: 1s;
  background-image: url(img/opponent5_75.png);
}

#IntroStart {
  position: absolute;
  width: 300px;
  height: 200px;
  margin-left: -70px;
  margin-top: -350px;
  background-color: rgba(0, 0, 0, 0.8);
  border-radius: 20px;
}


Now when you click the button, the leftmost image moves up first...


...followed in turn by the others!


Let's do one more thing. Set IntroStart's CSS class to fade if stage is not 0.
src/App.js
<div id="IntroStart" className={ (stage === 0 ? "" : "fade") }>
  <div className="logo">

  </div>
  <br />
  <button onClick={ ()=>{ start(); }} className="actionButton" >{ GetLabels("start", lang) } &#9658;</button>
</div>


fade is basically going to 0 opacity in 200 milliseconds.
src/App.css
.introimage5 {
  transition-delay: 1s;
  background-image: url(img/opponent5_75.png);
}

.fade {
  opacity: 0;
  transition: all 0.2s;
}


#IntroStart {
  position: absolute;
  width: 300px;
  height: 200px;
  margin-left: -70px;
  margin-top: -350px;
  background-color: rgba(0, 0, 0, 0.8);
  border-radius: 20px;
}


Now you see that the black box fades away before the hot chicks start moving up!


All right, this was fun. But it's not over yet. All we did was make an intro screen. We'll be moving on to serious business next!

Next

Game interface and rendering dice.

No comments:

Post a Comment