Client-side support for Multi Battles (#1649)

This commit is contained in:
Spandan Punwatkar
2021-03-29 03:43:07 +05:30
committed by GitHub
parent 576f773653
commit 1fe0b7eb85
9 changed files with 292 additions and 124 deletions

View File

@@ -7,7 +7,6 @@
minMainWidth: 956,
maxWidth: 1180,
initialize: function (data) {
this.me = {};
this.choice = undefined;
/** are move/switch/team-preview controls currently being shown? */
this.controlsShown = false;
@@ -335,6 +334,9 @@
if (this.request) {
// TODO: investigate when to do this
this.updateSide();
if (this.request.ally) {
this.addAlly(this.request.ally);
}
act = this.request.requestType;
if (this.request.side) {
@@ -382,7 +384,7 @@
if (this.request.forceSwitch !== true) {
var faintedLength = _.filter(this.request.forceSwitch, function (fainted) {return fainted;}).length;
var freedomDegrees = faintedLength - _.filter(switchables.slice(this.battle.nearSide.active.length), function (mon) {return !mon.fainted;}).length;
var freedomDegrees = faintedLength - _.filter(switchables.slice(this.battle.pokemonControlled), function (mon) {return !mon.fainted;}).length;
this.choice.freedomDegrees = Math.max(freedomDegrees, 0);
this.choice.canSwitch = faintedLength - this.choice.freedomDegrees;
}
@@ -404,6 +406,9 @@
done: 0,
count: 1
};
if (this.battle.gameType === 'multi') {
this.choice.count = 1;
}
if (this.battle.gameType === 'doubles') {
this.choice.count = 2;
}
@@ -516,7 +521,8 @@
}
var moveTarget = this.choice ? this.choice.moveTarget : '';
var pos = this.choice.choices.length - (type === 'movetarget' ? 1 : 0);
var pos = this.choice.choices.length;
if (type === 'movetarget') pos--;
var hpRatio = switchables[pos].hp / switchables[pos].maxhp;
@@ -555,10 +561,12 @@
if (type === 'movetarget') {
requestTitle += 'At who? ';
var activePos = this.battle.mySide.n > 1 ? pos + this.battle.pokemonControlled : pos;
var targetMenus = ['', ''];
var nearActive = this.battle.nearSide.active;
var farActive = this.battle.farSide.active;
var farSlot = farActive.length - 1 - pos;
var farSlot = farActive.length - 1 - activePos;
for (var i = farActive.length - 1; i >= 0; i--) {
var pokemon = farActive[i];
@@ -587,9 +595,9 @@
if (moveTarget === 'adjacentFoe') {
disabled = true;
} else if (moveTarget === 'normal' || moveTarget === 'adjacentAlly' || moveTarget === 'adjacentAllyOrSelf') {
if (Math.abs(pos - i) > 1) disabled = true;
if (Math.abs(activePos - i) > 1) disabled = true;
}
if (moveTarget !== 'adjacentAllyOrSelf' && pos == i) disabled = true;
if (moveTarget !== 'adjacentAllyOrSelf' && activePos == i) disabled = true;
if (disabled) {
targetMenus[1] += '<button disabled="disabled" style="visibility:hidden"></button> ';
@@ -615,7 +623,8 @@
var hasMoves = false;
var moveMenu = '';
var movebuttons = '';
var typeValueTracker = new ModifiableValue(this.battle, this.battle.nearSide.active[pos], this.battle.myPokemon[pos]);
var activePos = this.battle.mySide.n > 1 ? pos + this.battle.pokemonControlled : pos;
var typeValueTracker = new ModifiableValue(this.battle, this.battle.nearSide.active[activePos], this.battle.myPokemon[pos]);
var currentlyDynamaxed = (!canDynamax && maxMoves);
for (var i = 0; i < curActive.moves.length; i++) {
var moveData = curActive.moves[i];
@@ -627,7 +636,7 @@
if (move.id === 'Recharge') move.type = '&ndash;';
if (name.substr(0, 12) === 'Hidden Power') name = 'Hidden Power';
var moveType = this.tooltips.getMoveType(move, typeValueTracker)[0];
var tooltipArgs = 'move|' + moveData.move + '|' + pos;
var tooltipArgs = 'move|' + moveData.move + '|' + activePos;
if (moveData.disabled) {
movebuttons += '<button disabled="disabled" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '">';
} else {
@@ -715,7 +724,6 @@
'<div class="switchmenu">' + switchMenu + '</div>' +
'</div>'
);
this.$controls.html(
'<div class="controls">' +
'<div class="whatdo">' + requestTitle + this.getTimerHTML() + '</div>' +
@@ -730,12 +738,25 @@
var pokemon = switchables[i];
pokemon.name = pokemon.ident.substr(4);
var tooltipArgs = 'switchpokemon|' + i;
if (pokemon.fainted || i < this.battle.nearSide.active.length || this.choice.switchFlags[i] || trapped) {
if (pokemon.fainted || i < this.battle.pokemonControlled || this.choice.switchFlags[i] || trapped) {
party += '<button class="disabled has-tooltip" name="chooseDisabled" value="' + BattleLog.escapeHTML(pokemon.name) + (pokemon.fainted ? ',fainted' : trapped ? ',trapped' : i < this.battle.nearSide.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 {
party += '<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.battle.mySide.ally) party += this.displayAllyParty();
return party;
},
displayAllyParty: function () {
var party = '';
if (!this.battle.mySide.ally) return;
var allyParty = this.battle.myAllyPokemon;
for (var i = 0; i < allyParty.length; i++) {
var pokemon = allyParty[i];
pokemon.name = pokemon.ident.substr(4);
var tooltipArgs = 'allypokemon|' + i;
party += '<button class="disabled has-tooltip" name="chooseDisabled" value="' + BattleLog.escapeHTML(pokemon.name) + ',notMine' + '" 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> ';
}
return party;
},
updateSwitchControls: function (type) {
@@ -760,7 +781,7 @@
// TODO? hpbar
requestTitle += "Which Pokémon will it switch in for?";
var controls = '<div class="switchmenu" style="display:block">';
for (var i = 0; i < nearActive.length; i++) {
for (var i = 0; i < this.battle.pokemonControlled; i++) {
var pokemon = this.battle.myPokemon[i];
var tooltipArgs = 'switchpokemon|' + i;
if (pokemon && !pokemon.fainted || this.choice.switchOutFlags[i]) {
@@ -789,8 +810,8 @@
for (var i = 0; i < switchables.length; i++) {
var pokemon = switchables[i];
var tooltipArgs = 'switchpokemon|' + i;
if (pokemon.fainted || i < this.battle.nearSide.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.nearSide.active.length ? ',active' : '') + '" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '">';
if (pokemon.fainted || i < this.battle.pokemonControlled || this.choice.switchFlags[i]) {
switchMenu += '<button class="disabled has-tooltip" name="chooseDisabled" value="' + BattleLog.escapeHTML(pokemon.name) + (pokemon.fainted ? ',fainted' : i < this.battle.pokemonControlled ? ',active' : '') + '" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '">';
} else {
switchMenu += '<button name="chooseSwitch" value="' + i + '" class="has-tooltip" data-tooltip="' + BattleLog.escapeHTML(tooltipArgs) + '">';
}
@@ -877,19 +898,20 @@
}
buf += leads.join(', ') + ' will be sent out first.<br />';
} else if (this.choice.choices && this.request) {
var nearActive = this.battle.nearSide.active;
for (var i = 0; i < this.choice.choices.length; i++) {
var myActive = this.battle.myPokemon;
for (var i = 0; i < (this.battle.myPokemon || this.choice.choices.length); i++) {
var parts = this.choice.choices[i].split(' ');
switch (parts[0]) {
case 'move':
var move;
myActive = this.battle.nearSide.active;
if (this.request.active[i].maxMoves && !this.request.active[i].canDynamax) { // it's a max move
move = this.request.active[i].maxMoves.maxMoves[parseInt(parts[1], 10) - 1].move;
} else { // it's a normal move
move = this.request.active[i].moves[parseInt(parts[1], 10) - 1].move;
}
var target = '';
buf += nearActive[i].speciesForme + ' will ';
buf += myActive[i].speciesForme + ' will ';
if (parts.length > 2) {
var targetPos = parts[2];
if (targetPos === 'mega') {
@@ -909,7 +931,7 @@
var targetActive = this.battle.farSide.active;
// Targeting your own side in doubles / triples
if (targetPos < 0) {
targetActive = nearActive;
targetActive = myActive;
targetPos = -targetPos;
target += 'your ';
}
@@ -924,13 +946,13 @@
break;
case 'switch':
buf += '' + this.battle.myPokemon[parts[1] - 1].speciesForme + ' will switch in';
if (nearActive[i]) {
buf += ', replacing ' + nearActive[i].speciesForme;
if (myActive[i]) {
buf += ', replacing ' + myActive[i].speciesForme;
}
buf += '.<br />';
break;
case 'shift':
buf += nearActive[i].speciesForme + ' will shift position.<br />';
buf += myActive[i].speciesForme + ' will shift position.<br />';
break;
}
}
@@ -1007,7 +1029,7 @@
updateSideLocation: function (sideData) {
if (!sideData.id) return;
this.side = sideData.id;
if (this.battle.sidesSwitched !== !!(this.side === 'p2')) {
if (this.battle.sidesSwitched !== !!(this.side === 'p2' || this.side === 'p4')) {
this.battle.switchSides();
this.$chat = this.$chatFrame.find('.inner');
}
@@ -1015,6 +1037,14 @@
updateSide: function () {
var sideData = this.request.side;
this.battle.myPokemon = sideData.pokemon;
this.battle.mySide = this.battle[sideData.id];
this.battle.nearSide = this.battle.sides[this.battle.mySide.n % 2];
this.battle.farSide = this.battle.mySide.foe;
this.battle.mySide.isFar = false;
this.battle.farSide.isFar = true;
if (this.battle.mySide.ally) this.battle.mySide.ally.isFar = false;
if (this.battle.farSide.ally) this.battle.farSide.ally.isFar = true;
this.battle.myPokemon = sideData.pokemon;
for (var i = 0; i < sideData.pokemon.length; i++) {
var pokemonData = sideData.pokemon[i];
if (this.request.active && this.request.active[i]) pokemonData.canGmax = this.request.active[i].gigantamax || false;
@@ -1027,6 +1057,20 @@
pokemonData.getHPColor = Pokemon.prototype.getHPColor;
}
},
addAlly: function (allyData) {
this.battle.myAllyPokemon = allyData.pokemon;
for (var i = 0; i < allyData.pokemon.length; i++) {
var pokemonData = allyData.pokemon[i];
this.battle.parseDetails(pokemonData.ident.substr(4), pokemonData.ident, pokemonData.details, pokemonData);
this.battle.parseHealth(pokemonData.condition, pokemonData);
pokemonData.hpDisplay = Pokemon.prototype.hpDisplay;
pokemonData.getPixelRange = Pokemon.prototype.getPixelRange;
pokemonData.getFormattedRange = Pokemon.prototype.getFormattedRange;
pokemonData.getHPColorClass = Pokemon.prototype.getHPColorClass;
pokemonData.getHPColor = Pokemon.prototype.getHPColor;
pokemonData.side = this.battle.mySide.ally;
}
},
// buttons
joinBattle: function () {
@@ -1218,6 +1262,8 @@
data = data.split(',');
if (data[1] === 'fainted') {
app.addPopupMessage("" + data[0] + " has no energy left to battle!");
} else if (data[1] === 'notMine') {
app.addPopupMessage("You cannot decide for your partner!");
} else if (data[1] === 'trapped') {
app.addPopupMessage("You are trapped and cannot select " + data[0] + "!");
} else if (data[1] === 'active') {
@@ -1241,20 +1287,20 @@
var nearActive = this.battle.nearSide.active;
if (this.request.requestType === 'switch' && this.request.forceSwitch !== true) {
while (choices.length < nearActive.length && !this.request.forceSwitch[choices.length]) {
while (choices.length < this.battle.pokemonControlled && !this.request.forceSwitch[choices.length]) {
choices.push('pass');
}
if (choices.length < nearActive.length) {
if (choices.length < this.battle.pokemonControlled) {
this.choice.type = 'switch2';
this.updateControlsForPlayer();
return true;
}
} else if (this.request.requestType === 'move') {
while (choices.length < nearActive.length && !nearActive[choices.length]) {
while (choices.length < this.battle.pokemonControlled && !nearActive[choices.length]) {
choices.push('pass');
}
if (choices.length < nearActive.length) {
if (choices.length < this.battle.pokemonControlled) {
this.choice.type = 'move2';
this.updateControlsForPlayer();
return true;
@@ -1275,12 +1321,12 @@
if (act === 'switch') {
// Assert that the remaining Pokémon won't switch, even though
// the player could have decided otherwise.
for (var i = 0; i < this.battle.nearSide.active.length; i++) {
for (var i = 0; i < this.battle.pokemonControlled; i++) {
if (!this.choice.choices[i]) this.choice.choices[i] = 'pass';
}
}
if (this.choice.choices.length >= (this.choice.count || this.battle.nearSide.active.length)) {
if (this.choice.choices.length >= (this.choice.count || this.battle.pokemonControlled || this.request.active.length)) {
this.sendDecision(this.choice.choices);
}

View File

@@ -100,7 +100,7 @@ class BattleScene {
const pokemon = battle.getPokemon(pokemonId);
if (pokemon) return pokemon.speciesForme;
}
if (!pokemonId.startsWith('p1') && !pokemonId.startsWith('p2')) return '???pokemon:' + pokemonId + '???';
if (!pokemonId.startsWith('p')) return '???pokemon:' + pokemonId + '???';
if (pokemonId.charAt(3) === ':') return pokemonId.slice(4).trim();
else if (pokemonId.charAt(2) === ':') return pokemonId.slice(3).trim();
return '???pokemon:' + pokemonId + '???';
@@ -208,7 +208,9 @@ class BattleScene {
this.curTerrain = '';
this.curWeather = '';
this.log.battleParser!.perspective = this.battle.sidesSwitched ? 1 : 0;
this.log.battleParser!.perspective = this.battle.mySide!.sideid;
this.resetSides(true);
}
animationOff() {
@@ -616,9 +618,7 @@ class BattleScene {
}
return BattleLog.escapeHTML(name);
}
updateSidebar(side: Side) {
if (!this.animating) return;
getSidebarHTML(side: Side, isAlly?: boolean): string {
let noShow = this.battle.hardcoreMode && this.battle.gen < 7;
let speciesOverage = this.battle.speciesClause ? Infinity : Math.max(side.pokemon.length - side.totalPokemon, 0);
@@ -693,17 +693,25 @@ class BattleScene {
if (i % 3 === 2) pokemonhtml += `</div><div class="teamicons">`;
}
pokemonhtml = '<div class="teamicons">' + pokemonhtml + '</div>';
const ratinghtml = side.rating ? ` title="Rating: ${BattleLog.escapeHTML(side.rating)}"` : ``;
let posStr = side.isFar ? 'far' : 'near';
if (isAlly) posStr += '2';
return `<div class="trainer trainer-${posStr}"><strong>${BattleLog.escapeHTML(side.name)}</strong><div class="trainersprite"${ratinghtml} style="background-image:url(${Dex.resolveAvatar(side.avatar)})"></div>${pokemonhtml}</div>`;
}
updateSidebar(side: Side) {
if (side.n > 1) side = side.ally;
const $sidebar = (side.isFar ? this.$rightbar : this.$leftbar);
let sidebarhtml = this.getSidebarHTML(side) + (side.ally ? this.getSidebarHTML(side.ally, true) : '');
if (side.name) {
const ratinghtml = side.rating ? ` title="Rating: ${BattleLog.escapeHTML(side.rating)}"` : ``;
$sidebar.html(`<div class="trainer"><strong>${BattleLog.escapeHTML(side.name)}</strong><div class="trainersprite"${ratinghtml} style="background-image:url(${Dex.resolveAvatar(side.avatar)})"></div>${pokemonhtml}</div>`);
$sidebar.html(sidebarhtml);
$sidebar.find('.trainer').css('opacity', 1);
} else {
$sidebar.find('.trainer').css('opacity', 0.4);
}
}
updateSidebars() {
for (const side of this.battle.sides) this.updateSidebar(side);
this.updateSidebar(this.battle.nearSide);
this.updateSidebar(this.battle.farSide);
}
updateStatbars() {
for (const side of this.battle.sides) {
@@ -713,17 +721,33 @@ class BattleScene {
}
}
teamPreviewEnd() {
for (let siden = 0; siden < 2; siden++) {
this.$sprites[siden].empty();
this.battle.sides[siden].updateSprites();
resetSides(skipEmpty?: boolean) {
if (!skipEmpty) {
for (const $spritesContainer of this.$sprites) {
$spritesContainer.empty();
}
}
for (const side of this.battle.sides) {
side.z = (side.isFar ? 200 : 0);
side.missedPokemon?.sprite?.destroy();
side.missedPokemon = {
sprite: new PokemonSprite(null, {
x: side.leftof(-100),
y: side.y,
z: side.z,
opacity: 0,
}, this, side.isFar),
} as any;
side.missedPokemon.sprite.isMissedPokemon = true;
}
}
teamPreview() {
let newBGNum = 0;
for (let siden = 0; siden < 2; siden++) {
const side = this.battle.sides[siden];
const spriteIndex = +this.battle.sidesSwitched ^ siden;
for (let siden = 0; siden < 2 || (this.battle.gameType === 'multi' && siden < 4); siden++) {
let side = this.battle.sides[siden];
const spriteIndex = +this.battle.sidesSwitched ^ (siden % 2);
let textBuf = '';
let buf = '';
let buf2 = '';
@@ -1426,20 +1450,6 @@ class BattleScene {
afterMove(pokemon: Pokemon) {
return pokemon.sprite.afterMove();
}
updateSpritesForSide(side: Side) {
side.missedPokemon?.sprite?.destroy();
side.missedPokemon = {
sprite: new PokemonSprite(null, {
x: side.leftof(-100),
y: side.y,
z: side.z,
opacity: 0,
}, this, side.isFar),
} as any;
side.missedPokemon.sprite.isMissedPokemon = true;
}
// Misc
/////////////////////////////////////////////////////////////////////

View File

@@ -42,8 +42,9 @@ class BattleSceneStub {
runStatusAnim(moveid: ID, participants: Pokemon[]): void { }
startAnimations(): void { }
teamPreview(): void { }
teamPreviewEnd(): void { }
resetSides(): void { }
updateGen(): void { }
getSidebarHTML(side: Side) { }
updateSidebar(side: Side): void { }
updateSidebars(): void { }
updateStatbars(): void { }
@@ -68,7 +69,6 @@ class BattleSceneStub {
anim(pokemon: Pokemon, end: ScenePos, transition?: string) { }
beforeMove(pokemon: Pokemon) { }
afterMove(pokemon: Pokemon) { }
updateSpritesForSide(side: Side) { }
unlink(userid: string, showRevealButton = false) { }
}

View File

@@ -12,16 +12,20 @@ declare const BattleText: {[id: string]: {[templateName: string]: string}};
type Args = [string, ...string[]];
type KWArgs = {[kw: string]: string};
type SideID = 'p1' | 'p2' | 'p3' | 'p4';
class BattleTextParser {
p1 = "Player 1";
p2 = "Player 2";
perspective: 0 | 1;
p3 = "Player 3";
p4 = "Player 4";
perspective: SideID;
gen = 7;
turn = 0;
curLineSection: 'break' | 'preMajor' | 'major' | 'postMajor' = 'break';
lowercaseRegExp: RegExp | null | undefined = undefined;
constructor(perspective: 0 | 1 = 0) {
constructor(perspective: SideID = 'p1') {
this.perspective = perspective;
}
@@ -226,7 +230,7 @@ class BattleTextParser {
pokemonName = (pokemon: string) => {
if (!pokemon) return '';
if (!pokemon.startsWith('p1') && !pokemon.startsWith('p2')) return `???pokemon:${pokemon}???`;
if (!pokemon.startsWith('p')) return `???pokemon:${pokemon}???`;
if (pokemon.charAt(3) === ':') return pokemon.slice(4).trim();
else if (pokemon.charAt(2) === ':') return pokemon.slice(3).trim();
return `???pokemon:${pokemon}???`;
@@ -234,14 +238,11 @@ class BattleTextParser {
pokemon(pokemon: string) {
if (!pokemon) return '';
let side;
switch (pokemon.slice(0, 2)) {
case 'p1': side = 0; break;
case 'p2': side = 1; break;
default: return `???pokemon:${pokemon}???`;
}
let side = pokemon.slice(0, 2);
if (!['p1', 'p2', 'p3', 'p4'].includes(side)) return `???pokemon:${pokemon}???`;
const name = this.pokemonName(pokemon);
const template = BattleText.default[side === this.perspective ? 'pokemon' : 'opposingPokemon'];
const isNear = side === this.perspective || side === BattleTextParser.allyID(side as SideID);
const template = BattleText.default[isNear ? 'pokemon' : 'opposingPokemon'];
return template.replace('[NICKNAME]', name);
}
@@ -257,20 +258,30 @@ class BattleTextParser {
side = side.slice(0, 2);
if (side === 'p1') return this.p1;
if (side === 'p2') return this.p2;
if (side === 'p3') return this.p3;
if (side === 'p4') return this.p4;
return `???side:${side}???`;
}
team(side: string, isFar: 0 | 1 = 0) {
static allyID(sideid: SideID): SideID | '' {
if (sideid === 'p1') return 'p3';
if (sideid === 'p2') return 'p4';
if (sideid === 'p3') return 'p1';
if (sideid === 'p4') return 'p2';
return '';
}
team(side: string, isFar: boolean = false) {
side = side.slice(0, 2);
if (side === (this.perspective === isFar ? 'p1' : 'p2')) {
return BattleText.default.team;
if (side === this.perspective || side === BattleTextParser.allyID(side as SideID)) {
return isFar ? BattleText.default.team : BattleText.default.opposingTeam;
}
return BattleText.default.opposingTeam;
return !isFar ? BattleText.default.team : BattleText.default.opposingTeam;
}
own(side: string) {
side = side.slice(0, 2);
if (side === (this.perspective === 0 ? 'p1' : 'p2')) {
if (side === this.perspective) {
return 'OWN';
}
return '';
@@ -278,7 +289,7 @@ class BattleTextParser {
party(side: string) {
side = side.slice(0, 2);
if (side === (this.perspective === 0 ? 'p1' : 'p2')) {
if (side === this.perspective || side === BattleTextParser.allyID(side as SideID)) {
return BattleText.default.party;
}
return BattleText.default.opposingParty;
@@ -347,7 +358,7 @@ class BattleTextParser {
switch (cmd) {
case 'done' : case 'turn':
return 'break';
case 'move' : case 'cant': case 'switch': case 'drag': case 'upkeep': case 'start': case '-mega':
case 'move' : case 'cant': case 'switch': case 'drag': case 'upkeep': case 'start': case '-mega': case '-candynamax':
return 'major';
case 'switchout': case 'faint':
return 'preMajor';
@@ -409,6 +420,10 @@ class BattleTextParser {
this.p1 = name;
} else if (side === 'p2' && name) {
this.p2 = name;
} else if (side === 'p3' && name) {
this.p3 = name;
} else if (side === 'p4' && name) {
this.p4 = name;
}
return '';
}
@@ -421,6 +436,7 @@ class BattleTextParser {
case 'turn': {
const [, num] = args;
this.turn = Number.parseInt(num, 10);
return this.template('turn').replace('[NUMBER]', num) + '\n';
}
@@ -529,6 +545,18 @@ class BattleTextParser {
return line1 + template.replace('[POKEMON]', this.pokemon(pokemon)).replace('[MOVE]', move);
}
case '-candynamax': {
let [, side] = args;
const own = this.own(side);
let template = '';
if (this.turn === 1) {
if (own) template = this.template('canDynamax', own);
} else {
template = this.template('canDynamax', own);
}
return template.replace('[TRAINER]', this.trainer(side));
}
case 'message': {
let [, message] = args;
return '' + message + '\n';
@@ -609,7 +637,7 @@ class BattleTextParser {
const id = BattleTextParser.effectId(ability);
if (id === 'unnerve') {
const template = this.template('start', ability);
return line1 + template.replace('[TEAM]', this.team(pokemon.slice(0, 2), 1));
return line1 + template.replace('[TEAM]', this.team(pokemon.slice(0, 2), true));
}
let templateId = 'start';
if (id === 'anticipation' || id === 'sturdy') templateId = 'activate';

View File

@@ -271,11 +271,11 @@ class BattleTooltips {
case 'zmove':
case 'maxmove': { // move|MOVE|ACTIVEPOKEMON|[GMAXMOVE]
let move = this.battle.dex.getMove(args[1]);
let index = parseInt(args[2], 10);
let pokemon = this.battle.nearSide.active[index];
let serverPokemon = this.battle.myPokemon![index];
let teamIndex = parseInt(args[2], 10);
let pokemon = this.battle.nearSide.active[teamIndex + this.battle.pokemonControlled * Math.floor(this.battle.mySide!.n / 2)];
let gmaxMove = args[3] ? this.battle.dex.getMove(args[3]) : undefined;
if (!pokemon) return false;
let serverPokemon = this.battle.myPokemon![teamIndex];
buf = this.showMoveTooltip(move, type, pokemon, serverPokemon, gmaxMove);
break;
}
@@ -307,6 +307,22 @@ class BattleTooltips {
let sideIndex = parseInt(args[1], 10);
let side = this.battle.sides[+this.battle.sidesSwitched ^ sideIndex];
let activeIndex = parseInt(args[2], 10);
if (this.battle.gameType === 'multi') {
if (activeIndex >= side.active.length) return;
if (activeIndex && side.sideid !== 'p3' && side.sideid !== 'p4') side = side.ally;
if (!activeIndex && (side.sideid === 'p3' || side.sideid === 'p4')) side = side.ally;
let pokemon = side.active[activeIndex];
if (!pokemon) return;
let serverPokemon = null;
if (side === this.battle.mySide && this.battle.myPokemon) {
serverPokemon = this.battle.myPokemon[0];
} else if (side === this.battle.mySide!.ally && this.battle.myAllyPokemon) {
serverPokemon = this.battle.myAllyPokemon[0];
}
if (!pokemon) return false;
buf = this.showPokemonTooltip(pokemon, serverPokemon, true);
break;
}
let pokemon = side.active[activeIndex];
let serverPokemon = null;
if (sideIndex === 0 && this.battle.myPokemon) {
@@ -319,16 +335,30 @@ class BattleTooltips {
case 'switchpokemon': { // switchpokemon|POKEMON
// mouse over switchable pokemon
// serverPokemon definitely exists, sidePokemon maybe
let side = this.battle.mySide;
// let side = this.battle.mySide;
let activeIndex = parseInt(args[1], 10);
let pokemon = null;
if (activeIndex < side.active.length) {
/* if (activeIndex < side.active.length && activeIndex < this.battle.pokemonControlled) {
pokemon = side.active[activeIndex];
}
if (pokemon && pokemon.side === side.ally) pokemon = null;
} */
let serverPokemon = this.battle.myPokemon![activeIndex];
buf = this.showPokemonTooltip(pokemon, serverPokemon);
break;
}
case 'allypokemon': { // allypokemon|POKEMON
// mouse over ally's pokemon in multi battles
// serverPokemon definitely exists, sidePokemon maybe
// let side = this.battle.mySide.ally;
let activeIndex = parseInt(args[1], 10);
let pokemon = null;
/*if (activeIndex < side.pokemon.length) {
pokemon = side.pokemon[activeIndex] || side.ally ? side.ally.pokemon[activeIndex] : null;
}*/
let serverPokemon = this.battle.myAllyPokemon ? this.battle.myAllyPokemon[activeIndex] : null;
buf = this.showPokemonTooltip(pokemon, serverPokemon);
break;
}
case 'field': {
buf = this.showFieldTooltip();
break;
@@ -1838,8 +1868,8 @@ class BattleTooltips {
// this will only be available if the ability announced itself in some way
let allyAbility = Dex.getAbility(ally.ability).name;
// otherwise fall back on the original set data sent from the server
if (!allyAbility && this.battle.myPokemon) {
allyAbility = Dex.getAbility(this.battle.myPokemon[ally.slot].ability).name;
if (!allyAbility && this.battle.myAllyPokemon) {
allyAbility = Dex.getAbility(this.battle.myAllyPokemon[ally.slot].ability).name;
}
return allyAbility;
}

View File

@@ -577,9 +577,11 @@ class Side {
battle: Battle;
name = '';
id = '';
sideid: SideID;
n: number;
isFar: boolean;
foe: Side = null!;
ally: Side = null!;
avatar: string = 'unknown';
rating: string = '';
totalPokemon = 6;
@@ -597,11 +599,11 @@ class Side {
/** [effectName, levels, minDuration, maxDuration] */
sideConditions: {[id: string]: [string, number, number, number]} = {};
constructor(battle: Battle, n: number, isOpp?: boolean) {
constructor(battle: Battle, n: number) {
this.battle = battle;
this.n = n;
this.isFar = isOpp || !!n;
this.updateSprites();
this.sideid = ['p1', 'p2', 'p3', 'p4'][n] as SideID;
this.isFar = !!(n % 2);
}
rollTrainerSprites() {
@@ -630,13 +632,8 @@ class Side {
}
reset() {
this.clearPokemon();
this.updateSprites();
this.sideConditions = {};
}
updateSprites() {
this.z = (this.isFar ? 200 : 0);
this.battle.scene.updateSpritesForSide(this);
}
setAvatar(avatar: string) {
this.avatar = avatar;
}
@@ -1026,13 +1023,17 @@ class Battle {
pseudoWeather = [] as WeatherState[];
weatherTimeLeft = 0;
weatherMinTimeLeft = 0;
mySide: Side = null!;
mySide: Side | null = null;
nearSide: Side = null!;
farSide: Side = null!;
p1: Side = null!;
p2: Side = null!;
p3?: Side = null!;
p4?: Side = null!;
pokemonControlled = 0;
sides: Side[] = null!;
myPokemon: ServerPokemon[] | null = null;
sides: [Side, Side] = [null!, null!];
myAllyPokemon: ServerPokemon[] | null = null;
lastMove = '';
gen = 8;
@@ -1040,7 +1041,7 @@ class Battle {
teamPreviewCount = 0;
speciesClause = false;
tier = '';
gameType: 'singles' | 'doubles' | 'triples' = 'singles';
gameType: 'singles' | 'doubles' | 'triples' | 'multi' = 'singles';
rated: string | boolean = false;
isBlitz = false;
endLastTurnPending = false;
@@ -1154,6 +1155,7 @@ class Battle {
if (side) side.reset();
}
this.myPokemon = null;
this.myAllyPokemon = null;
// DOM state
this.scene.reset();
@@ -1176,6 +1178,8 @@ class Battle {
this.farSide = null!;
this.p1 = null!;
this.p2 = null!;
this.p3 = null!;
this.p4 = null!;
}
log(args: Args, kwArgs?: KWArgs, preempt?: boolean) {
@@ -1192,14 +1196,16 @@ class Battle {
setSidesSwitched(sidesSwitched: boolean) {
this.sidesSwitched = sidesSwitched;
if (this.sidesSwitched) {
this.nearSide = this.mySide = this.p2;
this.nearSide = this.p2;
this.farSide = this.p1;
} else {
this.nearSide = this.mySide = this.p1;
this.nearSide = this.p1;
this.farSide = this.p2;
}
this.nearSide.isFar = false;
if (this.nearSide.ally) this.nearSide.ally.isFar = false;
this.farSide.isFar = true;
if (this.farSide.ally) this.farSide.ally.isFar = true;
// nothing else should need updating - don't call this function after sending out pokemon
}
@@ -2824,7 +2830,7 @@ class Battle {
this.scene.afterMove(poke);
break;
}
case '-hint': case '-message': {
case '-hint': case '-message': case '-candynamax': {
this.log(args, kwArgs);
break;
}
@@ -2940,23 +2946,15 @@ class Battle {
let siden = -1;
let slot = -1; // if there is an explicit slot for this pokemon
let slotChart: {[k: string]: number} = {a: 0, b: 1, c: 2, d: 3, e: 4, f: 5};
if (name.substr(0, 4) === 'p2: ' || name === 'p2') {
siden = this.p2.n;
name = name.substr(4);
} else if (name.substr(0, 4) === 'p1: ' || name === 'p1') {
siden = this.p1.n;
name = name.substr(4);
} else if (name.substr(0, 2) === 'p2' && name.substr(3, 2) === ': ') {
slot = slotChart[name.substr(2, 1)];
siden = this.p2.n;
name = name.substr(5);
pokemonid = 'p2: ' + name;
} else if (name.substr(0, 2) === 'p1' && name.substr(3, 2) === ': ') {
slot = slotChart[name.substr(2, 1)];
siden = this.p1.n;
name = name.substr(5);
pokemonid = 'p1: ' + name;
if (/^p[1-9]($|: )/.test(name)) {
siden = parseInt(name.charAt(1), 10) - 1;
name = name.slice(4);
} else if (/^p[1-9][a-f]: /.test(name)) {
const slotChart: {[k: string]: number} = {a: 0, b: 1, c: 2, d: 3, e: 4, f: 5};
siden = parseInt(name.charAt(1), 10) - 1;
slot = slotChart[name.charAt(2)];
name = name.slice(5);
pokemonid = `p${siden + 1}: ${name}`;
}
return {name, siden, slot, pokemonid};
}
@@ -3035,8 +3033,10 @@ class Battle {
return null;
}
getSide(sidename: string): Side {
if (sidename === 'p1' || sidename.substr(0, 3) === 'p1:') return this.p1;
if (sidename === 'p2' || sidename.substr(0, 3) === 'p2:') return this.p2;
if (sidename === 'p1' || sidename.startsWith('p1:')) return this.p1;
if (sidename === 'p2' || sidename.startsWith('p2:')) return this.p2;
if ((sidename === 'p3' || sidename.startsWith('p3:')) && this.p3) return this.p3;
if ((sidename === 'p4' || sidename.startsWith('p4:')) && this.p4) return this.p4;
if (this.nearSide.id === sidename) return this.nearSide;
if (this.farSide.id === sidename) return this.farSide;
if (this.nearSide.name === sidename) return this.nearSide;
@@ -3071,9 +3071,9 @@ class Battle {
runMajor(args: Args, kwArgs: KWArgs, preempt?: boolean) {
switch (args[0]) {
case 'start': {
this.scene.teamPreviewEnd();
this.nearSide.active[0] = null;
this.farSide.active[0] = null;
this.scene.resetSides();
this.start();
break;
}
@@ -3103,8 +3103,22 @@ class Battle {
this.gameType = args[1] as any;
switch (args[1]) {
default:
this.nearSide.active = [null];
this.farSide.active = [null];
for (const side of this.sides) side.active = [null];
break;
case 'multi':
this.pokemonControlled = 1;
this.p3 = new Side(this, 2);
this.p4 = new Side(this, 3);
this.p3.foe = this.p4.ally = this.p2;
this.p3.ally = this.p4.foe = this.p1;
this.p1.ally = this.p3;
this.p2.ally = this.p4;
this.p3.isFar = this.p1.isFar;
this.p4.isFar = this.p2.isFar;
this.sides = [this.p1, this.p2, this.p3, this.p4];
// intentionally sync p1/p3 and p2/p4's active arrays
this.p1.active = this.p3.active = [null, null];
this.p2.active = this.p4.active = [null, null];
break;
case 'doubles':
this.nearSide.active = [null, null];
@@ -3116,7 +3130,9 @@ class Battle {
this.farSide.active = [null, null, null];
break;
}
if (!this.pokemonControlled) this.pokemonControlled = this.nearSide.active.length;
this.scene.updateGen();
this.scene.resetSides();
break;
}
case 'rule': {
@@ -3221,9 +3237,9 @@ class Battle {
side.setName(args[2]);
if (args[3]) side.setAvatar(args[3]);
if (args[4]) side.rating = args[4];
this.scene.updateSidebar(side);
if (this.joinButtons) this.scene.hideJoinButtons();
this.log(args);
this.scene.updateSidebar(side);
break;
}
case 'teamsize': {

View File

@@ -10,6 +10,8 @@ type BattleDesc = {
minElo?: number | string,
p1?: string,
p2?: string,
p3?: string,
p4?: string,
};
class BattlesRoom extends PSRoom {

View File

@@ -315,11 +315,22 @@ License: GPLv2
position: absolute;
left: 0;
right: 0;
bottom: 102px;
bottom: 75px;
}
.rightbar .trainer {
.trainer-near {
bottom: 75px;
}
.trainer-far {
bottom: 232px;
}
.trainer-near2 {
bottom: 232px;
}
.trainer-far2 {
bottom: 75px;
}
.trainer strong {
display: block;
font-size: 8pt;

View File

@@ -1851,6 +1851,13 @@ a.ilink.yours {
width: 640px;
background: #EEF2F5;
}
.ps-room .ally-pokemon {
position: absolute;
top: 280px;
left: 0;
width: 640px;
background: #EEF2F5;
}
.battle-chat-toggle {
display: none;
}
@@ -1946,6 +1953,7 @@ a.ilink.yours {
left: 0;
display: block;
}
.allyTeam button,
.shiftselect button,
.moveselect button,
.switchselect button {
@@ -1970,6 +1978,7 @@ a.ilink.yours {
color: #445588;
cursor: default;
}
.allyparty button,
.switchmenu button {
position: relative;
display: block;
@@ -1982,11 +1991,14 @@ a.ilink.yours {
white-space: pre;
overflow: hidden;
}
.allyparty button .picon,
.switchmenu button .picon {
float: left;
margin: -6px -3px -6px -4px;
opacity: 0.8;
}
.allyparty button .hpbar,
.switchmenu button .hpbar {
position: absolute;
display: block;
@@ -2000,6 +2012,7 @@ a.ilink.yours {
/* The declaration order of these three hpbar* classes is significant. */
.allyparty button .hpbar span,
.switchmenu button .hpbar span {
display: block;
height: 1px;
@@ -2007,10 +2020,14 @@ a.ilink.yours {
border-top: 1px solid #3C0;
border-radius: 1px;
}
.allyparty button .hpbar-yellow span,
.switchmenu button .hpbar-yellow span {
border-top-color: #a5aa53;
background-color: #a2a822;
}
.allyparty button .hpbar-red span,
.switchmenu button .hpbar-red span {
border-top-color: #faa;
background-color: #f55;
@@ -2018,6 +2035,7 @@ a.ilink.yours {
/****************/
.allyparty button .status,
.switchmenu button .status {
position: absolute;
display: block;
@@ -2098,13 +2116,14 @@ a.ilink.yours {
background: #E5E5E5;
color: black;
}
.allyparty,
.switchmenu,
.movemenu {
display: block;
margin-right: -10px;
padding-left: 4px;
}
.allyparty button,
.switchmenu button,
.movemenu button {
position: relative;
@@ -2122,12 +2141,14 @@ a.ilink.yours {
background: #e3e3e3;
background: linear-gradient(to bottom, #f6f6f6, #e3e3e3);
}
.allyparty button:hover,
.switchmenu button:hover,
.movemenu button:hover {
background: #cfcfcf;
background: linear-gradient(to bottom, #f2f2f2, #cfcfcf);
border-color: #606060;
}
.allyparty button:active,
.switchmenu button:active,
.movemenu button:active,
.switchmenu button.pressed,
@@ -2135,6 +2156,8 @@ a.ilink.yours {
background: linear-gradient(to bottom, #cfcfcf, #f2f2f2);
}
.allyparty button.disabled,
.allyparty button:disabled,
.switchmenu button.disabled,
.switchmenu button:disabled,
.movemenu button:disabled {
@@ -2181,6 +2204,7 @@ a.ilink.yours {
display: none;
padding: 0 75px 0 85px;
}
.allyparty,
.switchmenu {
display: none;
max-width: 325px;
@@ -2192,6 +2216,7 @@ a.ilink.yours {
left: 20px;
bottom: 20px;
}
.allyTeam,
.switchselect {
position: absolute;
right: 20px;