Support playing battles

This should be a much nicer architecture than the old
`client-battle.js`.

In particular, much of the logic of choosing moves/switches has been
moved into a new `battle-choices.ts`, with `panel-battle.tsx` only
covering the UI.
This commit is contained in:
Guangcong Luo 2020-03-23 01:53:38 -07:00
parent 523d63ab01
commit 40d077903f
15 changed files with 1009 additions and 160 deletions

View File

@ -6,6 +6,7 @@ node_modules/
/js/battle.js
/js/battledata.js
/js/battle-log.js
/js/battle-choices.js
/js/battle-text-parser.js
/js/battle-dex.js
/js/battle-dex-data.js

1
.gitignore vendored
View File

@ -16,6 +16,7 @@ package-lock.json
/js/battle.js
/js/battledata.js
/js/battle-log.js
/js/battle-choices.js
/js/battle-text-parser.js
/js/battle-dex.js
/js/battle-dex-data.js

View File

@ -570,7 +570,7 @@
} else if (!pokemon || pokemon.fainted) {
targetMenus[0] += '<button name="chooseMoveTarget" value="' + (i + 1) + '"><span class="picon" style="' + Dex.getPokemonIcon('missingno') + '"></span></button> ';
} else {
targetMenus[0] += '<button name="chooseMoveTarget" value="' + (i + 1) + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + (this.battle.ignoreOpponent || this.battle.ignoreNicks ? pokemon.species : BattleLog.escapeHTML(pokemon.name)) + '<span class="hpbar' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
targetMenus[0] += '<button name="chooseMoveTarget" value="' + (i + 1) + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + (this.battle.ignoreOpponent || this.battle.ignoreNicks ? pokemon.species : BattleLog.escapeHTML(pokemon.name)) + '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
}
}
for (var i = 0; i < myActive.length; i++) {
@ -590,7 +590,7 @@
} else if (!pokemon || pokemon.fainted) {
targetMenus[1] += '<button name="chooseMoveTarget" value="' + (-(i + 1)) + '"><span class="picon" style="' + Dex.getPokemonIcon('missingno') + '"></span></button> ';
} else {
targetMenus[1] += '<button name="chooseMoveTarget" value="' + (-(i + 1)) + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '<span class="hpbar' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
targetMenus[1] += '<button name="chooseMoveTarget" value="' + (-(i + 1)) + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
}
}
@ -701,9 +701,9 @@
pokemon.name = pokemon.ident.substr(4);
var tooltipArgs = 'switchpokemon|' + i;
if (pokemon.fainted || i < this.battle.mySide.active.length || this.choice.switchFlags[i]) {
switchMenu += '<button class="disabled has-tooltip" name="chooseDisabled" value="' + BattleLog.escapeHTML(pokemon.name) + (pokemon.fainted ? ',fainted' : i < this.battle.mySide.active.length ? ',active' : '') + '" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (pokemon.hp ? '<span class="hpbar' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
switchMenu += '<button class="disabled has-tooltip" name="chooseDisabled" value="' + BattleLog.escapeHTML(pokemon.name) + (pokemon.fainted ? ',fainted' : i < this.battle.mySide.active.length ? ',active' : '') + '" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (pokemon.hp ? '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
} else {
switchMenu += '<button name="chooseSwitch" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '<span class="hpbar' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
switchMenu += '<button name="chooseSwitch" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
}
}
if (this.finalDecisionSwitch && this.battle.gen > 2) {
@ -751,11 +751,11 @@
var pokemon = this.battle.myPokemon[i];
var tooltipArgs = 'switchpokemon|' + i;
if (pokemon && !pokemon.fainted || this.choice.switchOutFlags[i]) {
controls += '<button disabled class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (!pokemon.fainted ? '<span class="hpbar' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
controls += '<button disabled class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (!pokemon.fainted ? '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
} else if (!pokemon) {
controls += '<button disabled></button> ';
} else {
controls += '<button name="chooseSwitchTarget" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '<span class="hpbar' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
controls += '<button name="chooseSwitchTarget" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '"><span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') + '</button> ';
}
}
controls += '</div>';
@ -781,7 +781,7 @@
} else {
switchMenu += '<button name="chooseSwitch" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '">';
}
switchMenu += '<span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (!pokemon.fainted ? '<span class="hpbar' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
switchMenu += '<span class="picon" style="' + Dex.getPokemonIcon(pokemon) + '"></span>' + BattleLog.escapeHTML(pokemon.name) + (!pokemon.fainted ? '<span class="' + pokemon.getHPColorClass() + '"><span style="width:' + (Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1) + 'px"></span></span>' + (pokemon.status ? '<span class="status ' + pokemon.status + '"></span>' : '') : '') + '</button> ';
}
var controls = (

View File

@ -54,7 +54,7 @@
<script defer src="/js/client-core.js?"></script>
<script defer src="/js/battle-dex.js?"></script>
<script defer src="/js/battle-text-parser.js"></script>
<script defer src="/js/battle-text-parser.js?"></script>
<script defer src="/js/client-main.js?"></script>
<script defer src="/js/lib/sockjs-1.4.0-nwjsfix.min.js?"></script>
<script defer src="/js/client-connection.js?"></script>
@ -71,11 +71,12 @@
<script defer src="/js/lib/soundmanager2-nodebug-jsmin.js"></script>
<script defer src="/js/lib/jquery-2.1.4.min.js"></script>
<script defer src="/data/graphics.js"></script>
<script defer src="/data/text.js"></script>
<script defer src="/data/graphics.js?"></script>
<script defer src="/data/text.js?"></script>
<script defer src="/js/battle-tooltips.js"></script>
<script defer src="/js/battle.js"></script>
<script defer src="/js/panel-battle.js"></script>
<script defer src="/js/battle.js?"></script>
<script defer src="/js/battle-choices.js?"></script>
<script defer src="/js/panel-battle.js?"></script>
<script defer src="/js/battle-dex-data.js?"></script>
<script defer src="/data/pokedex.js?"></script>
@ -84,10 +85,13 @@
<script defer src="/data/abilities.js?"></script>
<script defer src="/data/search-index.js?"></script>
<script defer src="/data/teambuilder-tables.js?"></script>
<script defer src="/js/panel-teamdropdown.js"></script>
<script defer src="/js/panel-teamdropdown.js?"></script>
<script defer src="/js/panel-teambuilder.js?"></script>
<script defer src="/js/battle-search.js?"></script>
<script defer src="/js/battle-searchresults.js?"></script>
<script defer src="/js/panel-teambuilder-team.js?"></script>
<script defer src="/data/pokedex-mini.js?"></script>
<script defer src="/data/pokedex-mini-bw.js?"></script>
</body></html>

View File

@ -1314,7 +1314,7 @@ class BattleScene {
let $hp = pokemon.sprite.$statbar.find('div.hp');
let w = pokemon.hpWidth(150);
let hpcolor = pokemon.getHPColor();
let hpcolor = BattleScene.getHPColor(pokemon);
let callback;
if (hpcolor === 'y') {
callback = () => { $hp.addClass('hp-yellow'); };
@ -1337,7 +1337,7 @@ class BattleScene {
let $hp = pokemon.sprite.$statbar.find('div.hp');
let w = pokemon.hpWidth(150);
let hpcolor = pokemon.getHPColor();
let hpcolor = BattleScene.getHPColor(pokemon);
let callback;
if (hpcolor === 'g') {
callback = () => { $hp.removeClass('hp-yellow hp-red'); };
@ -1553,6 +1553,12 @@ class BattleScene {
}
this.battle = null!;
}
static getHPColor(pokemon: {hp: number, maxhp: number}) {
let ratio = pokemon.hp / pokemon.maxhp;
if (ratio > 0.5) return 'g';
if (ratio > 0.2) return 'y';
return 'r';
}
}
interface ScenePos {
@ -2485,7 +2491,7 @@ class PokemonSprite extends Sprite {
}
let hpcolor;
if (updatePrevhp || updateHp) {
hpcolor = pokemon.getHPColor();
hpcolor = BattleScene.getHPColor(pokemon);
let w = pokemon.hpWidth(150);
let $hp = this.$statbar.find('.hp');
$hp.css({

472
src/battle-choices.ts Normal file
View File

@ -0,0 +1,472 @@
/**
* Battle choices
*
* PS will send requests "what do you do this turn?", and you send back
* choices "I switch Pikachu for Caterpie, and Squirtle uses Water Gun"
*
* This file contains classes for handling requests and choices.
*
* Dependencies: battle-dex
*
* @author Guangcong Luo <guangcongluo@gmail.com>
* @license MIT
*/
interface BattleRequestSideInfo {
name: string;
id: 'p1' | 'p2' | 'p3' | 'p4';
pokemon: ServerPokemon[];
}
interface BattleRequestActivePokemon {
moves: {
name: string,
id: ID,
pp: number,
maxpp: number,
target: MoveTarget,
disabled?: boolean,
}[];
maxMoves?: {
name: string,
id: ID,
target: MoveTarget,
disabled?: boolean,
}[];
zMoves?: ({
name: string,
id: ID,
target: MoveTarget,
} | null)[];
/** also true if the pokemon can Gigantamax */
canDynamax?: boolean;
canGigantamax?: boolean;
canMegaEvo?: boolean;
canUltraBurst?: boolean;
trapped?: boolean;
maybeTrapped?: boolean;
}
interface BattleMoveRequest {
requestType: 'move';
rqid: number;
side: BattleRequestSideInfo;
active: (BattleRequestActivePokemon | null)[];
noCancel?: boolean;
}
interface BattleSwitchRequest {
requestType: 'switch';
rqid: number;
side: BattleRequestSideInfo;
forceSwitch: boolean[];
noCancel?: boolean;
}
interface BattleTeamRequest {
requestType: 'team';
rqid: number;
side: BattleRequestSideInfo;
maxTeamSize?: number;
noCancel?: boolean;
}
interface BattleWaitRequest {
requestType: 'wait';
rqid: number;
side: undefined;
noCancel?: boolean;
}
type BattleRequest = BattleMoveRequest | BattleSwitchRequest | BattleTeamRequest | BattleWaitRequest;
interface BattleMoveChoice {
choiceType: 'move';
/** 1-based move */
move: number;
targetLoc: number;
mega: boolean;
ultra: boolean;
max: boolean;
z: boolean;
}
interface BattleSwitchChoice {
choiceType: 'switch' | 'team';
/** 1-based pokemon */
targetPokemon: number;
}
type BattleChoice = BattleMoveChoice | BattleSwitchChoice;
/**
* Tracks a partial choice, allowing you to build it up one step at a time,
* and maybe even construct a UI to build it!
*
* Doesn't support going backwards; just use `new BattleChoiceBuilder`.
*/
class BattleChoiceBuilder {
request: BattleRequest;
/** Completed choices in string form */
choices: string[] = [];
/** Currently active partial move choice - not used for other choices, which don't have partial states */
current: BattleMoveChoice = {
choiceType: 'move', // unused
/** if nonzero, show target screen; if zero, show move screen */
move: 0,
targetLoc: 0, // unused
mega: false,
ultra: false,
z: false,
max: false,
};
/** Indexes of Pokémon already chosen to switch in */
plannedToSwitchIn: number[] = [];
alreadyMega = false;
alreadyMax = false;
alreadyZ = false;
constructor(request: BattleRequest) {
this.request = request;
this.fillPasses();
}
toString() {
let choices = this.choices;
if (this.current.move) choices = choices.concat(this.stringChoice(this.current));
return choices.join(', ').replace(/, team /g, ', ');
}
isDone() {
return this.choices.length >= this.requestLength();
}
isEmpty() {
for (const choice of this.choices) {
if (choice !== 'pass') return false;
}
if (this.current.move) return false;
return true;
}
/** Index of the current Pokémon to make choices for */
index() {
return this.choices.length;
}
/** How many choices is the server expecting? */
requestLength() {
const request = this.request;
switch (request.requestType) {
case 'move':
return request.active.length;
case 'switch':
return request.forceSwitch.length;
case 'team':
if (request.maxTeamSize) return request.maxTeamSize;
return 1;
case 'wait':
return 0;
}
}
currentMoveRequest() {
if (this.request.requestType !== 'move') return null;
return this.request.active[this.index()];
}
addChoice(choiceString: string) {
let choice: BattleChoice | null;
try {
choice = this.parseChoice(choiceString);
} catch (err) {
return (err as Error).message;
}
if (!choice) {
return "You do not need to manually choose to pass; the client handles it for you automatically";
}
if (choice.choiceType === 'move') {
if (!choice.targetLoc && this.requestLength() > 1) {
const choosableTargets = ['normal', 'any', 'adjacentAlly', 'adjacentAllyOrSelf', 'adjacentFoe'];
if (choosableTargets.includes(this.getChosenMove(choice, this.index()).target)) {
this.current.move = choice.move;
this.current.mega = choice.mega;
this.current.ultra = choice.ultra;
this.current.z = choice.z;
this.current.max = choice.max;
return null;
}
}
if (choice.mega) this.alreadyMega = true;
if (choice.z) this.alreadyZ = true;
if (choice.max) this.alreadyMax = true;
this.current.move = 0;
this.current.mega = false;
this.current.ultra = false;
this.current.z = false;
this.current.max = false;
} else if (choice.choiceType === 'switch' || choice.choiceType === 'team') {
if (this.plannedToSwitchIn.includes(choice.targetPokemon)) {
if (choice.choiceType === 'switch') {
return "You've already chosen to switch that Pokémon in";
}
// remove choice instead
for (let i = 0; i < this.plannedToSwitchIn.length; i++) {
if (this.plannedToSwitchIn[i] === choice.targetPokemon) {
this.plannedToSwitchIn.splice(i, 1);
this.choices.splice(i, 1);
return null;
}
}
return "Unexpected bug, please report this";
}
this.plannedToSwitchIn.push(choice.targetPokemon);
}
this.choices.push(this.stringChoice(choice));
this.fillPasses();
return null;
}
/**
* Move and switch requests will often skip over some active Pokémon (mainly
* fainted Pokémon). This will fill them in automatically, so we don't need
* to ask a user for them.
*/
fillPasses() {
const request = this.request;
switch (request.requestType) {
case 'move':
while (this.choices.length < request.active.length && !request.active[this.choices.length]) {
this.choices.push('pass');
}
break;
case 'switch':
while (this.choices.length < request.forceSwitch.length && !request.forceSwitch[this.choices.length]) {
this.choices.push('pass');
}
}
}
getChosenMove(choice: BattleMoveChoice, pokemonIndex: number) {
const request = this.request as BattleMoveRequest;
const activePokemon = request.active[pokemonIndex]!;
const moveIndex = choice.move - 1;
if (choice.z) {
return activePokemon.zMoves![moveIndex]!;
}
if (choice.max || (activePokemon.maxMoves && !activePokemon.canDynamax)) {
return activePokemon.maxMoves![moveIndex];
}
return activePokemon.moves[moveIndex];
}
/**
* Parses a choice from string form to BattleChoice form
*/
parseChoice(choice: string) {
const request = this.request;
if (request.requestType === 'wait') throw new Error(`It's not your turn to choose anything`);
const index = this.choices.length;
if (choice.startsWith('move ')) {
if (request.requestType !== 'move') {
throw new Error(`You must switch in a Pokémon, not move.`);
}
const moveRequest = request.active[index]!;
choice = choice.slice(5);
let current: BattleMoveChoice = {
choiceType: 'move',
move: 0,
targetLoc: 0,
mega: false,
ultra: false,
z: false,
max: false,
};
while (true) {
// If data ends with a number, treat it as a target location.
// We need to special case 'Conversion 2' so it doesn't get
// confused with 'Conversion' erroneously sent with the target
// '2' (since Conversion targets 'self', targetLoc can't be 2).
if (/\s(?:-|\+)?[1-3]$/.test(choice) && toID(choice) !== 'conversion2') {
if (current.targetLoc) throw new Error(`Move choice has multiple targets`);
current.targetLoc = parseInt(choice.slice(-2), 10);
choice = choice.slice(0, -2).trim();
} else if (choice.endsWith(' mega')) {
current.mega = true;
choice = choice.slice(0, -5);
} else if (choice.endsWith(' zmove')) {
current.z = true;
choice = choice.slice(0, -6);
} else if (choice.endsWith(' ultra')) {
current.ultra = true;
choice = choice.slice(0, -6);
} else if (choice.endsWith(' dynamax')) {
current.max = true;
choice = choice.slice(0, -8);
} else if (choice.endsWith(' max')) {
current.max = true;
choice = choice.slice(0, -4);
} else {
break;
}
}
if (/^[0-9]+$/.test(choice)) {
// Parse a one-based move index.
current.move = parseInt(choice, 10);
} else {
// Parse a move ID.
// Move names are also allowed, but may cause ambiguity (see client issue #167).
let moveid = toID(choice);
if (moveid.startsWith('hiddenpower')) moveid = 'hiddenpower' as ID;
for (let i = 0; i < moveRequest.moves.length; i++) {
if (moveid === moveRequest.moves[i].id) {
current.move = i + 1;
break;
}
}
if (!current.move && moveRequest.zMoves) {
for (let i = 0; i < moveRequest.zMoves.length; i++) {
if (!moveRequest.zMoves[i]) continue;
if (moveid === moveRequest.zMoves[i]!.id) {
current.move = i + 1;
current.z = true;
break;
}
}
}
if (!current.move && moveRequest.maxMoves) {
for (let i = 0; i < moveRequest.maxMoves.length; i++) {
if (moveid === moveRequest.maxMoves[i].id) {
current.move = i + 1;
current.max = true;
break;
}
}
}
}
if (current.max && !moveRequest.canDynamax) current.max = false;
return current;
}
if (choice.startsWith('switch ') || choice.startsWith('team ')) {
choice = choice.slice(choice.startsWith('team ') ? 5 : 7);
const isTeamPreview = request.requestType === 'team';
let current: BattleSwitchChoice = {
choiceType: isTeamPreview ? 'team' : 'switch',
targetPokemon: 0,
};
if (/^[0-9]+$/.test(choice)) {
// Parse a one-based move index.
current.targetPokemon = parseInt(choice, 10);
} else {
// Parse a pokemon name
const lowerChoice = choice.toLowerCase();
const choiceid = toID(choice);
let matchLevel = 0;
let match = 0;
for (let i = 0 ; i < request.side.pokemon.length; i++) {
const serverPokemon = request.side.pokemon[i];
let curMatchLevel = 0;
if (choice === serverPokemon.name) {
curMatchLevel = 10;
} else if (lowerChoice === serverPokemon.name.toLowerCase()) {
curMatchLevel = 9;
} else if (choiceid === toID(serverPokemon.name)) {
curMatchLevel = 8;
} else if (choiceid === toID(serverPokemon.species)) {
curMatchLevel = 7;
} else if (choiceid === toID(Dex.getTemplate(serverPokemon.species).baseSpecies)) {
curMatchLevel = 6;
}
if (curMatchLevel > matchLevel) {
match = i + 1;
matchLevel = curMatchLevel;
}
}
if (!match) {
throw new Error(`Couldn't find Pokémon "${choice}" to switch to`);
}
current.targetPokemon = match;
}
if (!isTeamPreview && current.targetPokemon - 1 < this.requestLength()) {
throw new Error(`That Pokémon is already in battle!`);
}
return current;
}
if (choice === 'pass') return null;
throw new Error(`Unrecognized choice "${choice}"`);
}
/**
* Converts a choice from `BattleChoice` into string form
*/
stringChoice(choice: BattleChoice) {
switch (choice.choiceType) {
case 'move':
const target = choice.targetLoc ? ` ${choice.targetLoc > 0 ? '+' : ''}${choice.targetLoc}` : ``;
const boost = `${choice.max ? ' max' : ''}${choice.mega ? ' mega' : ''}${choice.z ? ' zmove' : ''}`;
return `move ${choice.move}${boost}${target}`;
case 'switch':
case 'team':
return `${choice.choiceType} ${choice.targetPokemon}`;
}
}
/**
* The request sent from the server is actually really gross, but we'll have
* to wait until we transition to the new client before fixing it in the
* protocol, in the interests of not needing to fix it twice (or needing to
* fix it without TypeScript).
*
* In the meantime, this function converts a request from a shitty request
* to a request that makes sense.
*
* I'm sorry for literally all of this.
*/
static fixRequest(request: any, battle: Battle) {
if (!request.requestType) {
request.requestType = 'move';
if (request.forceSwitch) {
request.requestType = 'switch';
} else if (request.teamPreview) {
request.requestType = 'team';
} else if (request.wait) {
request.requestType = 'wait';
}
}
if (request.side) {
for (const serverPokemon of request.side.pokemon) {
battle.parseDetails(serverPokemon.ident.substr(4), serverPokemon.ident, serverPokemon.details, serverPokemon);
battle.parseHealth(serverPokemon.condition, serverPokemon);
}
}
if (request.active) {
request.active = request.active.map(
(active: any, i: number) => request.side.pokemon[i].fainted ? null : active
);
for (const active of request.active) {
if (!active) continue;
for (const move of active.moves) {
if (move.move) move.name = move.move;
move.id = toID(move.name);
}
if (active.maxMoves) {
if (active.maxMoves.maxMoves) {
active.canGigantamax = active.maxMoves.gigantamax;
active.maxMoves = active.maxMoves.maxMoves;
}
for (const move of active.maxMoves) {
if (move.move) move.name = Dex.getMove(move.move).name;
move.id = toID(move.name);
}
}
if (active.canZMove) {
active.zMoves = active.canZMove;
for (const move of active.zMoves) {
if (!move) continue;
if (move.move) move.name = move.move;
move.id = toID(move.name);
}
}
}
}
}
}

View File

@ -1089,6 +1089,11 @@ interface MoveFlags {
sound?: 1 | 0;
}
type MoveTarget = 'normal' | 'any' | 'adjacentAlly' | 'adjacentFoe' | 'adjacentAllyOrSelf' | // single-target
'self' | 'randomNormal' | // single-target, automatic
'allAdjacent' | 'allAdjacentFoes' | // spread
'allySide' | 'foeSide' | 'all'; // side and field
class Move implements Effect {
// effect
readonly effectType = 'Move';
@ -1103,11 +1108,7 @@ class Move implements Effect {
readonly type: TypeName;
readonly category: 'Physical' | 'Special' | 'Status';
readonly priority: number;
readonly target:
'normal' | 'any' | 'adjacentAlly' | 'adjacentFoe' | 'adjacentAllyOrSelf' | // single-target
'self' | 'randomNormal' | // single-target, automatic
'allAdjacent' | 'allAdjacentFoes' | // spread
'allySide' | 'foeSide' | 'all'; // side and field
readonly target: MoveTarget;
readonly flags: Readonly<MoveFlags>;
readonly critRatio: number;

View File

@ -699,20 +699,23 @@ const Dex = new class implements ModdedDex {
return num;
}
getPokemonIcon(pokemon: any, facingLeft?: boolean) {
getPokemonIcon(pokemon: string | Pokemon | ServerPokemon | null, facingLeft?: boolean) {
if (pokemon === 'pokeball') {
return `background:transparent url(${Dex.resourcePrefix}sprites/pokemonicons-pokeball-sheet.png) no-repeat scroll -0px 4px`;
} else if (pokemon === 'pokeball-statused') {
return `background:transparent url(${Dex.resourcePrefix}sprites/pokemonicons-pokeball-sheet.png) no-repeat scroll -40px 4px`;
} else if (pokemon === 'pokeball-fainted') {
return `background:transparent url(${Dex.resourcePrefix}sprites/pokemonicons-pokeball-sheet.png) no-repeat scroll -80px 4px;opacity:.4;filter:contrast(0)`;
} else if (pokemon === 'pokeball-none') {
} else if (pokemon === 'pokeball-none' || !pokemon) {
return `background:transparent url(${Dex.resourcePrefix}sprites/pokemonicons-pokeball-sheet.png) no-repeat scroll -80px 4px`;
}
let id = toID(pokemon);
if (typeof pokemon === 'string') pokemon = null;
if (pokemon?.species) id = toID(pokemon.species);
// @ts-ignore
if (pokemon?.volatiles?.formechange && !pokemon.volatiles.transform) {
// @ts-ignore
id = toID(pokemon.volatiles.formechange[1]);
}
let num = this.getPokemonIconNum(id, pokemon?.gender === 'F', facingLeft);

View File

@ -55,7 +55,7 @@ class PSSearchResults extends preact.Component<{search: BattleSearch}> {
<span class="col numcol">{search.getTier(pokemon)}</span>
<span class="col iconcol">
<span style={Dex.getPokemonIcon(pokemon)}></span>
<span style={Dex.getPokemonIcon(pokemon.id)}></span>
</span>
<span class="col pokemonnamecol">{this.renderName(pokemon.name, matchStart, matchEnd, tagStart)}</span>
@ -68,7 +68,7 @@ class PSSearchResults extends preact.Component<{search: BattleSearch}> {
<span class="col numcol">{search.getTier(pokemon)}</span>
<span class="col iconcol">
<span style={Dex.getPokemonIcon(pokemon)}></span>
<span style={Dex.getPokemonIcon(pokemon.id)}></span>
</span>
<span class="col pokemonnamecol">{this.renderName(pokemon.name, matchStart, matchEnd, tagStart)}</span>

View File

@ -186,7 +186,7 @@ class BattleTooltips {
if (!BattleTooltips.isLocked) BattleTooltips.hideTooltip();
}
listen(elem: HTMLElement) {
listen(elem: HTMLElement | JQuery<HTMLElement>) {
const $elem = $(elem);
$elem.on('mouseover', '.has-tooltip', this.showTooltipEvent);
$elem.on('click', '.has-tooltip', this.clickTooltipEvent);

View File

@ -121,19 +121,21 @@ class Pokemon implements PokemonDetails, PokemonHealth {
return this.side.active.includes(this);
}
getHPColor(): HPColor {
/** @deprecated */
private getHPColor(): HPColor {
if (this.hpcolor) return this.hpcolor;
let ratio = this.hp / this.maxhp;
if (ratio > 0.5) return 'g';
if (ratio > 0.2) return 'y';
return 'r';
}
getHPColorClass() {
/** @deprecated */
private getHPColorClass() {
switch (this.getHPColor()) {
case 'y': return ' hpbar-yellow';
case 'r': return ' hpbar-red';
case 'y': return 'hpbar hpbar-yellow';
case 'r': return 'hpbar hpbar-red';
}
return '';
return 'hpbar';
}
static getPixelRange(pixels: number, color: HPColor | ''): [number, number] {
let epsilon = 0.5 / 714;

View File

@ -87,7 +87,13 @@ class BattleRoom extends ChatRoom {
challengeMenuOpen!: false;
challengingFormat!: null;
challengedFormat!: null;
battle: Battle = null!;
/** null if spectator, otherwise current player's info */
side: BattleRequestSideInfo | null = null;
request: BattleRequest | null = null;
choices: BattleChoiceBuilder | null = null;
/**
* @return true to prevent line from being sent to server
*/
@ -123,6 +129,30 @@ class BattleRoom extends ChatRoom {
} case 'switchsides': {
this.battle.switchSides();
return true;
} case 'cancel': case 'undo': {
if (!this.choices || !this.request) {
this.receiveLine([`error`, `/choose - You are not a player in this battle`]);
return true;
}
if (this.choices.isDone() || this.choices.isEmpty()) {
this.send('/undo', true);
}
this.choices = new BattleChoiceBuilder(this.request);
this.update(null);
return true;
} case 'move': case 'switch': case 'team': case 'choose': {
if (!this.choices) {
this.receiveLine([`error`, `/choose - You are not a player in this battle`]);
return true;
}
const possibleError = this.choices.addChoice(line.slice(cmd === 'choose' ? 8 : 1));
if (possibleError) {
this.receiveLine([`error`, possibleError]);
return true;
}
if (this.choices.isDone()) this.send(`/choose ${this.choices.toString()}`, true);
this.update(null);
return true;
}}
return super.handleMessage(line);
}
@ -137,6 +167,50 @@ class BattleDiv extends preact.Component {
}
}
function MoveButton(props: {
children: string, cmd: string, moveData: {pp: number, maxpp: number}, type: TypeName, tooltip: string,
}) {
return <button name="cmd" value={props.cmd} class={`type-${props.type} has-tooltip`} data-tooltip={props.tooltip}>
{props.children}<br />
<small class="type">{props.type}</small> <small class="pp">{props.moveData.pp}/{props.moveData.maxpp}</small>&nbsp;
</button>;
}
function PokemonButton(props: {
pokemon: Pokemon | ServerPokemon | null, cmd: string, noHPBar?: boolean, disabled?: boolean | 'fade', tooltip: string,
}) {
const pokemon = props.pokemon;
if (!pokemon) {
return <button
name="cmd" value={props.cmd} class={`${props.disabled ? 'disabled ' : ''}has-tooltip`}
style={{opacity: props.disabled === 'fade' ? 0.5 : 1}} data-tooltip={props.tooltip}
>
(empty slot)
</button>;
}
let hpColorClass;
switch (BattleScene.getHPColor(pokemon)) {
case 'y': hpColorClass = 'hpbar hpbar-yellow'; break;
case 'r': hpColorClass = 'hpbar hpbar-red'; break;
default: hpColorClass = 'hpbar'; break;
}
return <button
name="cmd" value={props.cmd} class={`${props.disabled ? 'disabled ' : ''}has-tooltip`}
style={{opacity: props.disabled === 'fade' ? 0.5 : 1}} data-tooltip={props.tooltip}
>
<span class="picon" style={Dex.getPokemonIcon(pokemon)}></span>
{pokemon.name}
{
!props.noHPBar && !pokemon.fainted &&
<span class={hpColorClass}>
<span style={{width: Math.round(pokemon.hp * 92 / pokemon.maxhp) || 1}}></span>
</span>
}
{!props.noHPBar && pokemon.status && <span class={`status ${pokemon.status}`}></span>}
</button>;
}
class BattlePanel extends PSRoomPanel<BattleRoom> {
send = (text: string) => {
this.props.room.send(text);
@ -161,29 +235,87 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
}
return false;
};
toggleBoostedMove = (e: Event) => {
const checkbox = e.currentTarget as HTMLInputElement;
const choices = this.props.room.choices;
if (!choices) return; // shouldn't happen
switch (checkbox.name) {
case 'mega':
choices.current.mega = checkbox.checked;
break;
case 'ultra':
choices.current.ultra = checkbox.checked;
break;
case 'z':
choices.current.z = checkbox.checked;
break;
case 'max':
choices.current.max = checkbox.checked;
break;
}
this.props.room.update(null);
};
componentDidMount() {
const battle = new Battle($(this.base!).find('.battle'), $(this.base!).find('.battle-log'));
const $elem = $(this.base!);
const battle = new Battle($elem.find('.battle'), $elem.find('.battle-log'));
this.props.room.battle = battle;
battle.endCallback = () => this.forceUpdate();
battle.play();
(battle.scene as BattleScene).tooltips.listen($elem.find('.battle-controls'));
super.componentDidMount();
}
receiveLine(args: Args) {
if (args[0] === `initdone`) {
this.props.room.battle.fastForwardTo(-1);
const room = this.props.room;
switch (args[0]) {
case 'initdone':
room.battle.fastForwardTo(-1);
return;
case 'request':
this.receiveRequest(args[1] ? JSON.parse(args[1]) : null);
return;
case 'error':
if (args[1].startsWith('[Invalid choice]') && room.request) {
room.choices = new BattleChoiceBuilder(room.request);
room.update(null);
}
break;
}
room.battle.add('|' + args.join('|'));
}
receiveRequest(request: BattleRequest | null) {
const room = this.props.room;
if (!request) {
room.request = null;
room.choices = null;
return;
}
this.props.room.battle.add('|' + args.join('|'));
BattleChoiceBuilder.fixRequest(request, room.battle);
if (request.side) {
room.battle.myPokemon = request.side.pokemon;
if (room.battle.sidesSwitched !== !!(request.side.id === 'p2')) {
room.battle.switchSides();
}
room.side = request.side;
}
room.request = request;
room.choices = new BattleChoiceBuilder(request);
room.update(null);
}
renderControls() {
const battle = this.props.room.battle;
if (!battle) return null;
const atEnd = battle.playbackState === Playback.Finished;
return <div class="battle-controls" role="complementary" aria-label="Battle Controls" style="top: 370px;">
const room = this.props.room;
if (!room.battle) return null;
if (room.side) {
return this.renderPlayerControls();
}
const atEnd = room.battle.playbackState === Playback.Finished;
return <div class="controls">
<p>
{atEnd ?
<button class="button disabled" name="cmd" value="/play"><i class="fa fa-play"></i><br />Play</button>
: battle.paused ?
: room.battle.paused ?
<button class="button" name="cmd" value="/play"><i class="fa fa-play"></i><br />Play</button>
:
<button class="button" name="cmd" value="/pause"><i class="fa fa-pause"></i><br />Pause</button>
@ -198,6 +330,311 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
</p>
</div>;
}
renderMoveControls(request: BattleMoveRequest, choices: BattleChoiceBuilder) {
const dex = this.props.room.battle.dex;
const pokemonIndex = choices.index();
const active = choices.currentMoveRequest();
if (!active) return <div class="message-error">Invalid pokemon</div>;
if (choices.current.max || (active.maxMoves && !active.canDynamax)) {
if (!active.maxMoves) {
return <div class="message-error">Maxed with no max moves</div>;
}
return active.moves.map((moveData, i) => {
const move = dex.getMove(moveData.name);
const maxMoveData = active.maxMoves![i];
const gmaxTooltip = maxMoveData.id.startsWith('gmax') ? `|${maxMoveData.id}` : ``;
const tooltip = `maxmove|${moveData.name}|${pokemonIndex}${gmaxTooltip}`;
return <MoveButton cmd={`/move ${i + 1} max`} type={move.type} tooltip={tooltip} moveData={moveData}>
{maxMoveData.name}
</MoveButton>;
});
}
if (choices.current.z) {
if (!active.zMoves) {
return <div class="message-error">No Z moves</div>;
}
return active.moves.map((moveData, i) => {
const move = dex.getMove(moveData.name);
const zMoveData = active.zMoves![i];
if (!zMoveData) {
return <button disabled>&nbsp;</button>;
}
const tooltip = `zmove|${moveData.name}|${pokemonIndex}`;
return <MoveButton cmd={`/move ${i + 1} zmove`} type={move.type} tooltip={tooltip} moveData={{pp: 1, maxpp: 1}}>
{zMoveData.name}
</MoveButton>;
});
}
return active.moves.map((moveData, i) => {
const move = dex.getMove(moveData.name);
const tooltip = `move|${moveData.name}|${pokemonIndex}`;
return <MoveButton cmd={`/move ${i + 1}`} type={move.type} tooltip={tooltip} moveData={moveData}>
{move.name}
</MoveButton>;
});
}
renderMoveTargetControls(request: BattleMoveRequest, choices: BattleChoiceBuilder) {
const battle = this.props.room.battle;
const moveTarget = choices.getChosenMove(choices.current, choices.index()).target;
const moveChoice = choices.stringChoice(choices.current);
const userSlot = choices.index();
const userSlotCross = battle.yourSide.active.length - 1 - userSlot;
return [
battle.yourSide.active.map((pokemon, i) => {
let disabled = false;
if (moveTarget === 'adjacentAlly' || moveTarget === 'adjacentAllyOrSelf') {
disabled = true;
} else if (moveTarget === 'normal' || moveTarget === 'adjacentFoe') {
if (Math.abs(userSlotCross - i) > 1) disabled = true;
}
if (pokemon?.fainted) pokemon = null;
return <PokemonButton
pokemon={pokemon}
cmd={disabled ? `` : `/${moveChoice} +${i + 1}`} disabled={disabled && 'fade'} tooltip={`activepokemon|1|${i}`}
/>;
}).reverse(),
<div style="clear: left"></div>,
battle.mySide.active.map((pokemon, i) => {
let disabled = false;
if (moveTarget === 'adjacentFoe') {
disabled = true;
} else if (moveTarget === 'normal' || moveTarget === 'adjacentAlly' || moveTarget === 'adjacentAllyOrSelf') {
if (Math.abs(userSlot - i) > 1) disabled = true;
}
if (moveTarget !== 'adjacentAllyOrSelf' && userSlot === i) disabled = true;
if (pokemon?.fainted) pokemon = null;
return <PokemonButton
pokemon={pokemon}
cmd={disabled ? `` : `/${moveChoice} -${i + 1}`} disabled={disabled && 'fade'} tooltip={`activepokemon|0|${i}`}
/>;
}),
];
}
renderSwitchControls(request: BattleMoveRequest | BattleSwitchRequest, choices: BattleChoiceBuilder) {
const numActive = choices.requestLength();
const trapped = choices.currentMoveRequest()?.trapped;
return request.side.pokemon.map((serverPokemon, i) => {
const cantSwitch = trapped || i < numActive || choices.plannedToSwitchIn.includes(i + 1);
return <PokemonButton
pokemon={serverPokemon} cmd={`/switch ${i + 1}`} disabled={cantSwitch} tooltip={`switchpokemon|${i}`}
/>;
});
}
renderTeamControls(request: | BattleTeamRequest, choices: BattleChoiceBuilder) {
return request.side.pokemon.map((serverPokemon, i) => {
const cantSwitch = choices.plannedToSwitchIn.includes(i + 1);
return <PokemonButton
pokemon={serverPokemon} cmd={`/switch ${i + 1}`} noHPBar disabled={cantSwitch && 'fade'} tooltip={`switchpokemon|${i}`}
/>;
});
}
renderTeamList() {
const team = this.props.room.battle.myPokemon;
if (!team) return;
return <div class="switchcontrols">
<h3 class="switchselect">Team</h3>
<div class="switchmenu">
{team.map((serverPokemon, i) => {
return <PokemonButton
pokemon={serverPokemon} cmd={``} noHPBar disabled={true} tooltip={`switchpokemon|${i}`}
/>;
})}
</div>
</div>;
}
renderChosenTeam(request: BattleTeamRequest, choices: BattleChoiceBuilder) {
return choices.plannedToSwitchIn.map(slot => {
const serverPokemon = request.side.pokemon[slot - 1];
return <PokemonButton
pokemon={serverPokemon} cmd={`/switch ${slot}`} disabled tooltip={`switchpokemon|${slot - 1}`}
/>;
});
}
renderOldChoices(request: BattleRequest, choices: BattleChoiceBuilder) {
if (!choices) return null; // should not happen
if (request.requestType !== 'move' && request.requestType !== 'switch') return;
if (choices.isEmpty()) return null;
let buf: preact.ComponentChild[] = [
<button name="cmd" value="/cancel" class="button"><i class="fa fa-chevron-left"></i> Back</button>, ' ',
];
if (choices.isDone() && request.noCancel) {
buf = [];
}
const battle = this.props.room.battle;
for (let i = 0; i < choices.choices.length; i++) {
const choiceString = choices.choices[i];
const choice = choices.parseChoice(choiceString);
if (!choice) continue;
const pokemon = request.side.pokemon[i];
const active = request.requestType === 'move' ? request.active[i] : null;
if (choice.choiceType === 'move') {
buf.push(`${pokemon.name} will `);
if (choice.mega) buf.push(`Mega Evolve and `);
if (choice.ultra) buf.push(`Ultra Burst and `);
if (choice.max && active?.canDynamax) buf.push(active?.canGigantamax ? `Gigantamax and ` : `Dynamax and `);
buf.push(`use `, <strong>{choices.getChosenMove(choice, i).name}</strong>);
if (choice.targetLoc > 0) {
const target = battle.yourSide.active[choice.targetLoc - 1];
if (!target) {
buf.push(` at slot ${choice.targetLoc}`);
} else {
buf.push(` at ${target.name}`);
}
} else if (choice.targetLoc < 0) {
const target = battle.mySide.active[-choice.targetLoc - 1];
if (!target) {
buf.push(` at ally slot ${choice.targetLoc}`);
} else {
buf.push(` at ally ${target.name}`);
}
}
} else if (choice.choiceType === 'switch') {
const target = request.side.pokemon[choice.targetPokemon - 1];
buf.push(`${pokemon.name} will switch to `, <strong>{target.name}</strong>);
}
buf.push(<br />);
}
return buf;
}
renderPlayerControls() {
const room = this.props.room;
const request = room.request;
let choices = room.choices;
if (!request) return 'Error: Missing request';
if (!choices) return 'Error: Missing BattleChoiceBuilder';
if (choices.request !== request) {
choices = new BattleChoiceBuilder(request);
room.choices = choices;
}
if (choices.isDone()) {
return <div class="controls">
<div class="whatdo">
<button name="openTimer" class="button disabled timerbutton"><i class="fa fa-hourglass-start"></i> Timer</button>
{this.renderOldChoices(request, choices)}
</div>
<div class="pad">
{request.noCancel ? null : <button name="cmd" value="/cancel" class="button">Cancel</button>}
</div>
{this.renderTeamList()}
</div>;
}
if (request.side) room.battle.myPokemon = request.side.pokemon;
switch (request.requestType) {
case 'move': {
const index = choices.index();
const pokemon = request.side.pokemon[index];
const moveRequest = choices.currentMoveRequest()!;
const canDynamax = moveRequest.canDynamax && !choices.alreadyMax;
const canMegaEvo = moveRequest.canMegaEvo && !choices.alreadyMega;
const canZMove = moveRequest.zMoves && !choices.alreadyZ;
if (choices.current.move) {
const moveName = choices.getChosenMove(choices.current, choices.index()).name;
return <div class="controls">
<div class="whatdo">
<button name="openTimer" class="button disabled timerbutton"><i class="fa fa-hourglass-start"></i> Timer</button>
{this.renderOldChoices(request, choices)}
{pokemon.name} should use <strong>{moveName}</strong> at where? {}
</div>
<div class="switchcontrols">
<div class="switchmenu">
{this.renderMoveTargetControls(request, choices)}
</div>
</div>
</div>;
}
return <div class="controls">
<div class="whatdo">
<button name="openTimer" class="button disabled timerbutton"><i class="fa fa-hourglass-start"></i> Timer</button>
{this.renderOldChoices(request, choices)}
What will <strong>{pokemon.name}</strong> do?
</div>
<div class="movecontrols">
<h3 class="moveselect">Attack</h3>
<div class="movemenu">
{this.renderMoveControls(request, choices)}
<div style="clear:left"></div>
{canDynamax && <label class={`megaevo${choices.current.max ? ' cur' : ''}`}>
<input type="checkbox" name="max" checked={choices.current.max} onChange={this.toggleBoostedMove} /> {}
{moveRequest.canGigantamax ? 'Gigantamax' : 'Dynamax'}
</label>}
{canMegaEvo && <label class={`megaevo${choices.current.mega ? ' cur' : ''}`}>
<input type="checkbox" name="mega" checked={choices.current.mega} onChange={this.toggleBoostedMove} /> {}
Mega Evolution
</label>}
{moveRequest.canUltraBurst && <label class={`megaevo${choices.current.ultra ? ' cur' : ''}`}>
<input type="checkbox" name="ultra" checked={choices.current.ultra} onChange={this.toggleBoostedMove} /> {}
Ultra Burst
</label>}
{canZMove && <label class={`megaevo${choices.current.z ? ' cur' : ''}`}>
<input type="checkbox" name="z" checked={choices.current.z} onChange={this.toggleBoostedMove} /> {}
Z-Power
</label>}
</div>
</div>
<div class="switchcontrols">
<h3 class="switchselect">Switch</h3>
<div class="switchmenu">
{this.renderSwitchControls(request, choices)}
</div>
</div>
</div>;
} case 'switch': {
const pokemon = request.side.pokemon[choices.index()];
return <div class="controls">
<div class="whatdo">
<button name="openTimer" class="button disabled timerbutton"><i class="fa fa-hourglass-start"></i> Timer</button>
{this.renderOldChoices(request, choices)}
What will <strong>{pokemon.name}</strong> do?
</div>
<div class="switchcontrols">
<h3 class="switchselect">Switch</h3>
<div class="switchmenu">
{this.renderSwitchControls(request, choices)}
</div>
</div>
</div>;
} case 'team': {
return <div class="controls">
<div class="whatdo">
<button name="openTimer" class="button disabled timerbutton"><i class="fa fa-hourglass-start"></i> Timer</button>
{choices.plannedToSwitchIn.length > 0 ?
[<button name="cmd" value="/cancel" class="button"><i class="fa fa-chevron-left"></i> Back</button>,
" What about the rest of your team? "]
:
"How will you start the battle? "
}
</div>
<div class="switchcontrols">
<h3 class="switchselect">Choose {choices.plannedToSwitchIn.length <= 0 ? `lead` : `slot ${choices.plannedToSwitchIn.length + 1}`}</h3>
<div class="switchmenu">
{this.renderTeamControls(request, choices)}
<div style="clear:left"></div>
</div>
</div>
<div class="switchcontrols">
{choices.plannedToSwitchIn.length > 0 && <h3 class="switchselect">Team so far</h3>}
<div class="switchmenu">
{this.renderChosenTeam(request, choices)}
</div>
</div>
</div>;
}}
}
render() {
const room = this.props.room;
@ -208,7 +645,9 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
</ChatLog>
<ChatTextEntry room={this.props.room} onMessage={this.send} onKey={this.onKey} left={640} />
<ChatUserList room={this.props.room} left={640} minimized />
{this.renderControls()}
<div class="battle-controls" role="complementary" aria-label="Battle Controls" style="top: 370px;">
{this.renderControls()}
</div>
</PSPanelWrapper>;
}
}

View File

@ -107,6 +107,7 @@ class ChatRoom extends PSRoom {
}
send(line: string, direct?: boolean) {
this.updateTarget();
if (!direct && !line) return;
if (!direct && this.handleMessage(line)) return;
if (this.pmTarget) {
PS.send(`|/pm ${this.pmTarget}, ${line}`);
@ -366,14 +367,14 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
</TeamForm>
</div> : room.challengeMenuOpen ? <div class="challenge">
<TeamForm onSubmit={this.makeChallenge}>
<button type="submit" class="button disabled"><strong>Challenge</strong></button> {}
<button type="submit" class="button"><strong>Challenge</strong></button> {}
<button name="cmd" value="/cancelchallenge" class="button">Cancel</button>
</TeamForm>
</div> : null;
const challengeFrom = room.challengedFormat ? <div class="challenge">
<TeamForm format={room.challengedFormat} onSubmit={this.acceptChallenge}>
<button type="submit" class="button disabled"><strong>Accept</strong></button> {}
<button type="submit" class="button"><strong>Accept</strong></button> {}
<button name="cmd" value="/reject" class="button">Reject</button>
</TeamForm>
</div> : null;

View File

@ -1497,6 +1497,7 @@ a.ilink.yours {
margin-top: -2px;
padding: 0 8px;
font-size: 9pt;
line-height: 2;
color: #555555;
}
.battle-controls .whatdo small {
@ -1539,29 +1540,24 @@ a.ilink.yours {
left: 0;
display: block;
}
.shiftselect button,
.moveselect button,
.switchselect button {
background: transparent;
border: 0;
.shiftselect,
.moveselect,
.switchselect {
font-weight: bold;
font-style: italic;
color: #555555;
font-size: 12pt;
display: block;
margin: 0;
padding: 9px 7px 0 7px;
}
.shiftselect button {
.shiftselect {
color: #445588;
}
.moveselect button {
.moveselect {
color: #884422;
cursor: default;
}
.switchselect button {
.switchselect {
color: #445588;
cursor: default;
}
.switchmenu button {
position: relative;
@ -1668,28 +1664,34 @@ a.ilink.yours {
font-size: 8pt;
}
.megaevo {
clear: both;
display: block;
width: 180px;
margin: 0 auto 0;
position: relative;
top: 6px;
left: -7px;
padding: 2px;
text-align: center;
clear: both;
display: block;
width: 180px;
margin: 0 auto 0;
position: relative;
top: 6px;
left: -7px;
padding: 2px;
text-align: center;
cursor: pointer;
border: 1px solid #BBB;
border-radius: 3px;
color: #333;
background: #EEF2F5;
font-size: 10pt;
font-weight: bold;
cursor: pointer;
border: 1px solid #BBB;
border-radius: 3px;
color: #333;
background: #EEF2F5;
font-size: 10pt;
font-weight: bold;
}
.megaevo:hover {
border-color: #888;
background: #E5E5E5;
color: black;
border-color: #888;
background: #E5E5E5;
color: black;
}
.megaevo input {
width: 16px;
height: 16px;
vertical-align: -1px;
cursor: pointer;
}
.switchmenu,
@ -1740,93 +1742,6 @@ a.ilink.yours {
color: #777777 !important;
}
@media (max-height:570px) and (min-width: 440px) {
/*
* This is the black move/switch menu for low-res screens
*/
.controls {
position: absolute;
bottom: 10px;
left: 0;
right: 0;
width: auto;
background: #444444;
background: rgba(40,40,40,.85);
color: #FFFFFF;
padding: 4px 8px;
}
.battle-controls .whatdo {
color: #FFFFFF;
}
.battle-controls .whatdo small.weak {
color: #DDDD55;
border-color: #DDDD55;
}
.battle-controls .whatdo small.critical {
color: #FF7766;
border-color: #FF7766;
}
.battle-controls .movecontrols, .battle-controls .switchcontrols {
max-width: 640px;
}
.movemenu {
display: none;
padding: 0 75px 0 85px;
}
.switchmenu {
display: none;
max-width: 325px;
padding: 0 75px 0 85px;
margin: 0 0 0 auto;
}
.moveselect {
position: absolute;
left: 20px;
bottom: 20px;
}
.switchselect {
position: absolute;
right: 20px;
bottom: 20px;
}
.shiftselect {
position: absolute;
right: 150px;
bottom: 20px;
}
.moveselect button, .switchselect button, .shiftselect button {
padding: 4px 8px;
border-radius: 6px;
background: #E5E5E5;
}
.megaevo {
margin: 0 auto 8px 50px;
}
.battle-controls .whatdo {
padding-bottom: 50px;
}
.battle-controls .move-controls .whatdo,
.battle-controls .switch-controls .whatdo {
padding-bottom: 5px;
}
.move-controls .movemenu,
.switch-controls .switchmenu {
display: block;
margin-right: 0;
}
.move-controls .moveselect button,
.switch-controls .switchselect button,
.shiftselect button {
background: #BBBBBB;
}
.controls .timer {
float: right;
margin-top: -25px;
}
}
/*********************************************************
* Team Selector
*********************************************************/

View File

@ -76,6 +76,7 @@
<script src="data/text.js"></script>
<script src="js/battle-tooltips.js"></script>
<script src="js/battle.js"></script>
<script src="js/battle-choices.js"></script>
<script src="js/panel-battle.js"></script>
<script src="js/battle-dex-data.js"></script>
@ -91,4 +92,7 @@
<script src="js/battle-searchresults.js?"></script>
<script src="js/panel-teambuilder-team.js?"></script>
<script src="https://play.pokemonshowdown.com/data/pokedex-mini.js"></script>
<script src="https://play.pokemonshowdown.com/data/pokedex-mini-bw.js"></script>
</body></html>