Skip to content
Snippets Groups Projects
Verified Commit 55f5fca9 authored by Julien Wittouck's avatar Julien Wittouck
Browse files

:sparkles: : add practice subject

parent c1da477f
Branches
No related tags found
No related merge requests found
Pipeline #54022 passed
Showing with 13648 additions and 2 deletions
......@@ -355,10 +355,16 @@
<div class="card-body">
<h5 class="card-title">
Sujet de projet
<small class="text-muted">wed. 4 dec.</small>
<small class="text-muted">tue. 26 nov.</small>
</h5>
<p class="card-text">Un jour je serai le meilleur Dresseur</p>
</div>
<div class="card-footer">
<div class="btn-group" role="group">
<a href="w10-practice/10-tp-practice.html" class="btn btn-primary">TP</a>
<a href="w10-practice/10-tp-practice.pdf" class="btn btn-secondary"><i class="bi bi-file-earmark-pdf-fill"></i></a>
</div>
</div>
</div>
</div>
<div class="col">
......@@ -384,7 +390,7 @@
</div>
<div id="footer">
<div id="footer-text" class="index-last-update">Last updated Tue. 19 Nov</div>
<div id="footer-text" class="index-last-update">Last updated Tue. 26 Nov</div>
</div>
<script
......
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>ALOM - Practice</title>
<meta name="description" content="ALOM - Practice">
<meta name="author" content="Julien WITTOUCK <julien@codeka.io>">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, minimal-ui">
<link rel="stylesheet" href="../css/reveal.css">
<link rel="stylesheet" href="../css/theme/white.css" id="theme">
<link rel="stylesheet" href="../css/miage-lille.css"/>
<link href="https://afeld.github.io/emoji-css/emoji.css" rel="stylesheet">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css" integrity="sha384-B4dIYHKNBt8Bc12p+WXckhzcICo0wtJAoU8YZTY5qE0Id1GSseTk6S+L3BlXeVIU" crossorigin="anonymous">
</head>
<body>
<div class="reveal">
<div class="slides">
<section>
<h1>ALOM</h1>
<h2><i class="em em-runner"></i> Practice</h2>
</section>
</div>
</div>
<script src="../js/reveal.js"></script>
<script>
Reveal.initialize({
controls: true,
controlsBackArrows: 'faded',
progress: true,
history: true,
center: true,
transition: 'slide'
});
</script>
<aside class="miage_aside_logo"></aside>
</body>
</html>
:source-highlighter: rouge
:prewrap!:
:icons: font
:toc: left
:toclevels: 4
:linkattrs:
:sectlinks:
:sectanchors:
:sectnums:
:experimental:
:stem:
= ALOM - TP 10 - Practice even more !
== Présentation et objectifs
Le but est de continuer le développement de notre architecture "à la microservice".
Nous allons :
* continuer (ou commencer) le développpement de notre micro-service de combat !
* développer un micro-service de boutique
.Notre architecture !
image::images/battle-architecture.png[]
== `game-ui`
=== Ecran de combat !
NOTE: Cette partie nécessite d'avoir un projet `battle-api` complètement fonctionnel.
Pour vous faciliter le travail, j'ai développé pour vous un écran simple de combat, ainsi qu'un javascript (vanilla), qui requête en AJAX l'api `/battles` et permet de jouer un combat !
.Un combat en cours !
image::images/combat.png[]
Le JS est disponible ici : link:battle.js[battle.js,window="_blank"].
L'image de fond est disponible ici : link:images/battle_background.png[battle_background.png,window="_blank"].
L'écran de jeu est défini en template mustache via le code suivant :
[source,xml]
.fight.html
----
<!doctype html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Pokemon Manager</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.2/animate.min.css">
</head>
<body>
<div class="container">
<h1 class="pt-md-5 pb-md-5">Arena <img src="/icons/fist.png" style="width: 32px;"/></h1>
<h2>{{trainerName}} Vs {{opponentName}}</h2>
<div class="gameZone" style="width: 800px; height: 480px; position:relative; margin: auto; background: url('/images/battle_background.png')">
<img id="{{trainerName}}-pokemon-img" style="position:absolute; width: 300px; left: 0px; bottom: 0px;"/>
<img id="{{opponentName}}-pokemon-img" style="position:absolute; width: 200px; right: 100px; top: 100px;"/>
<div class="card bg-light" style="width: 18rem; position:absolute; bottom:10px; right: 10px; ">
<div class="card-body">
<h5 class="card-title" id="{{trainerName}}-pokemon-name"></h5>
<p>
<div class="progress">
<div class="progress-bar" id="{{trainerName}}-pokemon-hp" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</p>
<button class="btn btn-danger" id="attack-btn" onclick="playerCommand('ATTACK');">Attack</button>
</div>
</div>
<div class="card bg-light" style="width: 18rem; position:absolute; top: 10px; left: 10px;">
<div class="card-body">
<h5 class="card-title" id="{{opponentName}}-pokemon-name"></h5>
<p>
<div class="progress">
<div class="progress-bar" id="{{opponentName}}-pokemon-hp" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</p>
</div>
</div>
</div>
<div class="card card-body" id="message" style="width: 800px; margin: auto;">
</div>
</div>
<script src="https://code.jquery.com/jquery-3.4.1.min.js" integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
<script src="/scripts/battle.js" type="application/ecmascript"></script>
<script type="application/ecmascript">
$( document ).ready(function() {
startBattle("{{trainerName}}", "{{opponentName}}");
});
</script>
</body>
</html>
----
Le template a besoin de 2 variables : `trainerName` et `opponentName`.
Le fichier `battle.js` doit être déposé dans `src/main/resources/static/scripts`.
L'image de fond doit être déposée dans `src/main/resources/static/images`.
Le combat utilise `Bootstrap`, `JQuery` et `Animate.css`.
==== Le controlleur
Le controlleur servant cet écran peut être codé de cette manière :
[source,java,linenums]
.BattleController.java
----
@GetMapping("/fight/{opponent}")
public ModelAndView fight(Principal principal, @PathVariable String opponent){
var modelAndView = new ModelAndView("fight");
modelAndView.addObject("trainerName", principal.getName());
modelAndView.addObject("opponentName", opponent);
return modelAndView;
}
----
De cette manière, on peut déclencher un combat en se rendant sur l'URL http://localhost:9000/fight/Misty[,window="_blank"]
=== Cross Origin sur Battle-API
Le script `battle.js` appelle l'API battle. Cette API est appelée en `Cross-Origin`.
Du point de vue du navigateur web (firefox/chrome), l'origine est composée :
* du scheme (ex: http/https)
* de l'hôte (ex: localhost)
* du port (ex: 9000/8080...)
Lorsqu'une requête est émise vers une autre origine que celle de la page affichée, le navigateur
exécute tout d'abord une requête `HEAD`, pour demander au serveur s'il accepte d'être appelé
depuis une autre origine que lui-même. C'est une mécanique de sécurité permettant d'éviter
les appels indésirables sur une API.
Nous devons donc activer le support du `Cross-Origin` sur notre API battle, pour qu'elle
accepte les requêtes provenant du navigateur web.
NOTE: Nous n'avons à gérer le `Cross-Origin` uniquement quand c'est un navigateur qui est la source d'une requête.
Pas besoin de `Cross-Origin` pour les appels entre APIs.
==== Configuration
La configuration du `Cross-Origin` en Spring se fait en ajoutant l'annotation `@CrossOrigin` sur les controlleurs ou méthodes
à autoriser.
Ajoutez cette annotation sur le controlleur de votre `battle-api`.
== Gain d'XP
Avec l'arrivée des combats, on constate que les statistiques de nos chers Pokemon sont importantes.
Les combats successifs mettent les Pokemon et leurs dresseurs à rude épreuve.
Ajoutez la statistique "experience" aux Pokemon, dans l'API Trainer.
La statistique "level" devient une propriété calculée.
Le niveau du Pokemon "level" est la racine cubique du nombre de points d'expérience (arrondi à l'inférieur).
À l'inverse, un pokemon d'un certain niveau possède en points d'expérience la valeur du niveau au cube.
Par exemple, Ondine (Misty), possède un Stari et un Staross, de niveaux 18 et 21.
Stari a donc 18³ points d'expérience, soit 5832.
Staross a 21³ points d'xp, soit 9261.
Avec 6245 points d'expérience, Pikachu est au niveau 18 également. Pour passer au niveau 19, il lui faudra 19³-6245 points d'XP supplémentaire.
Les niveaux des Pokemon sont limités à 100 pour un nombre d'XP maximum de 1 000 000.
Le gain d'expérience lors d'un combat est régi par la formule suivante :
`1.5*(baseExperience)*(level pokemon vaincu)`
Quand Pikachu met KO le Stari d'Ondine, il gagne `1.5 * 112 * 18 = 3024` points d'expérience
L'expérience des Pokemon grandi à chaque combat, et leur niveau avec.
Dans l'API Battle, lorsqu'un Pokemon en met KO un autre, modifiez l'expérience du Pokemon qui a gagné avec ces formules,
et sauvegardez la nouvelle information dans l'API Trainer (via un PUT HTTP)
// == Multiplicateurs de type
//
// Lorsqu'un Pokemon en attaque un autre, les dégâts infligés dépendent également du type de pokemon attaquant / défenseur.
//
// Voici le tableau de multiplicateur de dégâts en fonction des types attaquants / défenseurs :
//
// image::images/type-table.png[]
//
// Appliquez les modificateurs de type dans votre `battle-api` !
== La boutique
Le lien pour créer le squelette de votre projet sur GitLab est le suivant : https://gitlab-classrooms.cleverapps.io/assignments/feb53495-4129-49af-8421-392fa372cd79/accept
Les dresseurs de Pokemon ont un portefeuille virtuel composé de Poké-Dollar 💰.
Ils peuvent créditer leur portefeuille en achetant des Poké-Dollar, au taux de change d'1 euro pour 20 000 💰.
Chaque dresseur démarre avec la somme de 10 000 💰 offerts.
Un dresseur qui se crée un compte, peut aussi également choisir un Pokemon offert parmi les 3 starters (id 1, 4, 7).
Une boutique leur permet :
* d'acheter des Pokeballs contenant des Pokemon aléatoires !
* d'acheter des bonbons pour augmenter le niveau d'un Pokemon !
Voici quelques objets disponibles dans la boutique:
|===
| Super Bonbon | Augmente le niveau d'un Pokemon ! | 5 000 💰
| Pokeball | Contient un Pokemon aléatoire commun de niveau 5 | 10 000 💰
| SuperBall | Contient un Pokemon aléatoire non-commun de niveau 10 | 25 000 💰
| HyperBall | Contient un Pokemon aléatoire rare de niveau 20 | 50 000 💰
| MasterBall | Contient un Pokemon aléatoire légendaire (un seul achat max par dresseur) de niveau 40 | 100 000 💰
|===
L'achat d'une PokeBall, SuperBall, HyperBall ou MasterBall a pour effet d'ajouter le Pokemon à la liste des Pokemon du dresseur, avec le niveau 5.
.Ids de rareté des Pokémon (probabilités égales pour chaque id)
|===
| Pokemons communs | 10 13 16 19 41 133 48 43 129 96 52 21 69 46 98 116
| Pokemons non-communs | 35 32 29 23 104 118 60 90 39 81 92 102 79 54 124 120 72 132
| Pokemons rares | 147 58 74 95 77 37 109 27 126 63 25 125 66 88 111 100 108 123 127 114 138 140
| Pokemons légendaires | 144 145 146 150 151
|===
La boutique doit :
* gérer le portefeuille de chaque dresseur, et lui permettre de l'approvisionner via une API REST
* exposer une API REST consommable par le `game-ui` pour afficher la liste des produits
* mettre à jour l'équipe du dresseur lors d'un achat (avec un PUT REST)
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
let battle;
let trainerName;
let opponentName;
let trainerCurrentPokemon;
let opponentCurrentPokemon;
const skipAnimations = false;
const battleApiUrl = "http://localhost:8082";
async function startBattle(a, b){
trainerName = a;
opponentName = b;
const result = await $.post(`${battleApiUrl}/battles?trainer=${trainerName}&opponent=${opponentName}`);
battle = result;
await showMessage(`Starting battle between ${trainerName} and ${opponentName} !`);
console.log(battle);
// battle starts, get first pokemon for each trainer !
trainerCurrentPokemon = battle.trainer.team[0];
opponentCurrentPokemon = battle.opponent.team[0];
await pokemonEntersBattle(trainerName, trainerCurrentPokemon, false);
await pokemonEntersBattle(opponentName, opponentCurrentPokemon, true);
await refreshBattle();
}
async function pokemonEntersBattle(trainerName, pokemon, frontView){
await showMessage(`${trainerName}'s ${pokemon.type.name} enters battle !`);
const pokemonSprite = frontView ? pokemon.type.sprites.front_default : pokemon.type.sprites.back_default;
const pokemonImageSelector = `[id='${trainerName}-pokemon-img']`;
$(pokemonImageSelector).attr("src", pokemonSprite);
$(`#${trainerName}-pokemon-name`).text(pokemon.type.name);
await animateCss(pokemonImageSelector, "zoomIn");
updatePokemonView(trainerName, pokemon);
}
function updatePokemonView(trainerName, pokemon){
let pokemonHpBar = $(`[id='${trainerName}-pokemon-hp']`);
pokemonHpBar.text(pokemon.hp);
pokemonHpBar.attr("aria-valuemax",pokemon.maxHp);
pokemonHpBar.attr("aria-valuenow",pokemon.hp);
pokemonHpBar.css("width",`${pokemon.hp * 100 / pokemon.maxHp}%` );
}
async function pokemonExitsBattle(trainerName, pokemon){
await showMessage(`${trainerName}'s ${pokemon.type.name} exits battle !`);
updatePokemonView(trainerName, pokemon);
const pokemonImageSelector = `[id='${trainerName}-pokemon-img']`;
return animateCss(pokemonImageSelector, "zoomOut");
}
function checkBattleEnd(){
// battle ends if all pokemons of a trainer are KO !
if(battle.trainer.team.every(poke => poke.ko === true)){
// show the battle end
showMessage(`${trainerName} lost the battle !`);
// exitPokemon(trainerName, trainerCurrentPokemon);
return true;
}
if(battle.opponent.team.every(poke => poke.ko === true)){
showMessage(`${trainerName} win the battle !`);
// exitPokemon(opponentName, opponentCurrentPokemon);
return true;
}
}
async function refreshBattle(){
updatePokemonView(trainerName, trainerCurrentPokemon);
updatePokemonView(opponentName, opponentCurrentPokemon);
if(checkBattleEnd()){
return;
}
// checking for ko
if(trainerCurrentPokemon.ko){
await showMessage(`${trainerName}'s ${trainerCurrentPokemon.type.name} is KO !`);
await pokemonExitsBattle(trainerName, trainerCurrentPokemon);
const firstAliveTrainerPokemon = battle.trainer.team.find(poke => poke.ko !== true);
trainerCurrentPokemon = firstAliveTrainerPokemon;
await pokemonEntersBattle(trainerName, trainerCurrentPokemon, false);
}
if(opponentCurrentPokemon.ko){
await showMessage(`${opponentName}'s ${opponentCurrentPokemon.type.name} is KO !`);
await pokemonExitsBattle(opponentName, opponentCurrentPokemon);
const firstAliveOpponentPokemon = battle.opponent.team.find(poke => poke.ko !== true);
opponentCurrentPokemon = firstAliveOpponentPokemon;
await pokemonEntersBattle(opponentName, opponentCurrentPokemon, true);
}
updateControls();
if(battle.opponent.nextTurn){
await showMessage(`This is ${opponentName}'s turn.`);
// wait a bit, then execute an IA turn if necessary
setTimeout(() => {
iaTurn();
}, 1000);
}
else{
await showMessage(`This is ${trainerName}'s turn.`);
}
}
async function iaTurn(){
await showMessage(`${opponentCurrentPokemon.type.name} attacks !`);
await sendAttack(opponentName);
await animateAttack(opponentName, trainerName);
refreshBattle();
}
function updateControls(){
// check if player's turn
const playersTurn = battle.trainer.nextTurn;
if(playersTurn){
enableControls();
}
else{
disableControls();
}
}
function enableControls(){
$("#attack-btn").removeAttr("disabled");
}
function disableControls(){
$("#attack-btn").attr("disabled","disabled");
}
async function playerCommand(command){
disableControls();
if("ATTACK" === command){
await showMessage(`${trainerCurrentPokemon.type.name} attacks !`);
await sendAttack(trainerName);
await animateAttack(trainerName, opponentName);
}
refreshBattle();
}
async function animateAttack(attackingTrainer, defendingTrainer){
console.log(`${attackingTrainer} attacks ${defendingTrainer}`);
const attackingPokemonImageSelector = `[id='${attackingTrainer}-pokemon-img']`;
const defendingPokemonImageSelector = `[id='${defendingTrainer}-pokemon-img']`;
await animateCss(attackingPokemonImageSelector, "bounce");
await animateCss(defendingPokemonImageSelector, "flash");
}
async function sendAttack(trainerName){
const result = await $.post(`${battleApiUrl}/battles/${battle.uuid}/${trainerName}/attack`);
updateBattleData(result);
}
function updateBattleData(data){
battle = data;
trainerCurrentPokemon = battle.trainer.team.find(poke => poke.id === trainerCurrentPokemon.id);
opponentCurrentPokemon = battle.opponent.team.find(poke => poke.id === opponentCurrentPokemon.id);
}
async function animateCss(element, animationName){
return new Promise(function(resolve, reject){
if(skipAnimations){
console.log(`skipping animation ${animationName} on ${element}`);
return resolve();
}
console.log(`starting animation ${animationName} on ${element}`);
const node = $(element);
node.removeClass()
.addClass('animated ' + animationName)
.one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', () => {
node.removeClass();
resolve();
});
});
}
async function showMessage(message){
const selector = "#message";
console.log(message);
$(selector).text("");
if(skipAnimations){
$(selector).text(message);
return;
}
return new Promise(function(resolve, reject){
let i = 0;
let timer = setInterval(() => {
if(i<message.length){
$(selector).append(message.charAt(i));
i++;
}
else{
clearInterval(timer);
resolve();
}
}, 50);
});
}
\ No newline at end of file
w10-practice/images/UML.png

34.6 KiB

w10-practice/images/battle-architecture.png

75 KiB

w10-practice/images/battle_background.png

430 KiB

w10-practice/images/combat.png

647 KiB

w10-practice/images/gateway-proxy.png

45.6 KiB

w10-practice/images/intellij-run-configuration.png

83 KiB

w10-practice/images/type-table.png

108 KiB

0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment