From ec1ca0e265bf88be11f90035e8db9a31479768b2 Mon Sep 17 00:00:00 2001 From: Karthik <32044378+Karthik99999@users.noreply.github.com> Date: Tue, 17 Oct 2023 19:15:25 -0700 Subject: [PATCH 1/7] Populate Tooltips with information from OTS (#2161) --- src/battle-text-parser.ts | 2 +- src/battle.ts | 45 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/battle-text-parser.ts b/src/battle-text-parser.ts index b64e399e042..11da55e34ad 100644 --- a/src/battle-text-parser.ts +++ b/src/battle-text-parser.ts @@ -46,7 +46,7 @@ class BattleTextParser { case 'fieldhtml': case 'controlshtml': case 'bigerror': case 'debug': case 'tier': case 'challstr': case 'popup': case '': return [cmd, line.slice(index + 1)]; - case 'c': case 'chat': case 'uhtml': case 'uhtmlchange': case 'queryresponse': + case 'c': case 'chat': case 'uhtml': case 'uhtmlchange': case 'queryresponse': case 'showteam': // three parts const index2a = line.indexOf('|', index + 1); return [cmd, line.slice(index + 1, index2a), line.slice(index2a + 1)]; diff --git a/src/battle.ts b/src/battle.ts index 9f3985be786..f86564a45b9 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -736,14 +736,19 @@ export class Side { this.battle.scene.removeSideCondition(this.n, id); } addPokemon(name: string, ident: string, details: string, replaceSlot = -1) { - const oldItem = replaceSlot >= 0 ? this.pokemon[replaceSlot].item : undefined; + const oldPokemon = replaceSlot >= 0 ? this.pokemon[replaceSlot] : undefined; const data = this.battle.parseDetails(name, ident, details); const poke = new Pokemon(data, this); - if (oldItem) poke.item = oldItem; + if (oldPokemon) { + poke.item = oldPokemon.item; + poke.baseAbility = oldPokemon.baseAbility; + poke.teraType = oldPokemon.teraType; + } if (!poke.ability && poke.baseAbility) poke.ability = poke.baseAbility; poke.reset(); + if (oldPokemon?.moveTrack.length) poke.moveTrack = oldPokemon.moveTrack; if (replaceSlot >= 0) { this.pokemon[replaceSlot] = poke; @@ -3571,6 +3576,42 @@ export class Battle { this.scene.teamPreview(); break; } + case 'showteam': { + if (this.turn !== 0) return; + // @ts-ignore + if (!window.Storage?.unpackTeam || !window.Storage?.exportTeam) return; + // @ts-ignore + const team: PokemonSet[] = Storage.unpackTeam(args[2]); + if (!team) return; + const side = this.getSide(args[1]); + side.clearPokemon(); + for (const set of team) { + const details = set.species + (!set.level || set.level === 100 ? '' : ', L' + set.level) + + (!set.gender || set.gender === 'N' ? '' : ', ' + set.gender) + (set.shiny ? ', shiny' : ''); + const pokemon = side.addPokemon('', '', details); + if (set.item) pokemon.item = set.item; + if (set.ability) pokemon.rememberAbility(set.ability); + for (const move of set.moves) { + pokemon.rememberMove(move, 0); + } + if (set.teraType) pokemon.teraType = set.teraType; + } + const exportedTeam = team.map(set => { + // @ts-ignore + let buf = Storage.exportTeam([set], this.gen).replace(/\n/g, '
'); + if (set.name && set.name !== set.species) { + buf = buf.replace(set.name, BattleLog.sanitizeHTML(`
${set.name}`)); + } else { + buf = buf.replace(set.species, `
${set.species}`); + } + if (set.item) { + buf = buf.replace(set.item, `${set.item} `); + } + return buf; + }).join(''); + this.add(`|raw|
Open Team Sheet for ${side.name}${exportedTeam}
`); + break; + } case 'switch': case 'drag': case 'replace': { this.endLastTurn(); let poke = this.getSwitchedPokemon(args[1], args[2])!; From b47c5939661dcc5fa25fd4b2e061a8f0d1d858f4 Mon Sep 17 00:00:00 2001 From: Karthik <32044378+Karthik99999@users.noreply.github.com> Date: Thu, 19 Oct 2023 22:12:34 -0700 Subject: [PATCH 2/7] Fix `showteam` in replays (#2164) --- src/battle-log.ts | 26 ++++- src/battle.ts | 273 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 276 insertions(+), 23 deletions(-) diff --git a/src/battle-log.ts b/src/battle-log.ts index 146a94f1e39..31decf131b6 100644 --- a/src/battle-log.ts +++ b/src/battle-log.ts @@ -99,8 +99,8 @@ export class BattleLog { } add(args: Args, kwArgs?: KWArgs, preempt?: boolean) { if (kwArgs?.silent) return; - if (this.scene?.battle.seeking) { - const battle = this.scene.battle; + const battle = this.scene?.battle; + if (battle?.seeking) { if (battle.stepQueue.length > 2000) { // adding elements gets slower and slower the more there are // (so showing 100 turns takes around 2 seconds, and 1000 turns takes around a minute) @@ -122,7 +122,6 @@ export class BattleLog { if (!['name', 'n'].includes(args[0])) this.lastRename = null; switch (args[0]) { case 'chat': case 'c': case 'c:': - let battle = this.scene?.battle; let name; let message; if (args[0] === 'c:') { @@ -259,6 +258,27 @@ export class BattleLog { app.rooms[roomid].notifyOnce(title, body, 'highlight'); break; + case 'showteam': { + if (!battle) return; + const team = battle.unpackTeam(args[2]); + if (!team.length) return; + const side = battle.getSide(args[1]); + const exportedTeam = team.map(set => { + let buf = battle.exportTeam([set]).replace(/\n/g, '
'); + if (set.name && set.name !== set.species) { + buf = buf.replace(set.name, BattleLog.sanitizeHTML(`
${set.name}`)); + } else { + buf = buf.replace(set.species, `
${set.species}`); + } + if (set.item) { + buf = buf.replace(set.item, `${set.item} `); + } + return buf; + }).join(''); + divHTML = `
Open Team Sheet for ${side.name}${exportedTeam}
`; + break; + } + case 'seed': case 'choice': case ':': case 'timer': case 't:': case 'J': case 'L': case 'N': case 'spectator': case 'spectatorleave': case 'initdone': diff --git a/src/battle.ts b/src/battle.ts index f86564a45b9..7b1f62548bf 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -3325,6 +3325,256 @@ export class Battle { } as any; } + // Taken from Storage, they need to be implemented here so `showteam` can work + unpackTeam(buf: string) { + if (!buf) return []; + + const team = []; + let i = 0; + let j = 0; + + while (true) { + const set: PokemonSet = {} as any; + team.push(set); + + // name + j = buf.indexOf('|', i); + set.name = buf.substring(i, j); + i = j + 1; + + // species + j = buf.indexOf('|', i); + set.species = Dex.species.get(buf.substring(i, j)).name || set.name; + i = j + 1; + + // item + j = buf.indexOf('|', i); + set.item = Dex.items.get(buf.substring(i, j)).name; + i = j + 1; + + // ability + j = buf.indexOf('|', i); + const ability = Dex.abilities.get(buf.substring(i, j)).name; + const species = Dex.species.get(set.species); + set.ability = (species.abilities && + ['', '0', '1', 'H', 'S'].includes(ability) ? species.abilities[ability as '0' || '0'] : ability); + i = j + 1; + + // moves + j = buf.indexOf('|', i); + set.moves = buf.substring(i, j).split(',').map(function (moveid) { + return Dex.moves.get(moveid).name; + }); + i = j + 1; + + // nature + j = buf.indexOf('|', i); + set.nature = buf.substring(i, j) as NatureName; + if (set.nature as any === 'undefined') delete set.nature; + i = j + 1; + + // evs + j = buf.indexOf('|', i); + if (j !== i) { + const evstring = buf.substring(i, j); + if (evstring.length > 5) { + const evs = evstring.split(','); + set.evs = { + hp: Number(evs[0]) || 0, + atk: Number(evs[1]) || 0, + def: Number(evs[2]) || 0, + spa: Number(evs[3]) || 0, + spd: Number(evs[4]) || 0, + spe: Number(evs[5]) || 0, + }; + } else if (evstring === '0') { + set.evs = {hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0}; + } + } + i = j + 1; + + // gender + j = buf.indexOf('|', i); + if (i !== j) set.gender = buf.substring(i, j); + i = j + 1; + + // ivs + j = buf.indexOf('|', i); + if (j !== i) { + const ivs = buf.substring(i, j).split(','); + set.ivs = { + hp: ivs[0] === '' ? 31 : Number(ivs[0]), + atk: ivs[1] === '' ? 31 : Number(ivs[1]), + def: ivs[2] === '' ? 31 : Number(ivs[2]), + spa: ivs[3] === '' ? 31 : Number(ivs[3]), + spd: ivs[4] === '' ? 31 : Number(ivs[4]), + spe: ivs[5] === '' ? 31 : Number(ivs[5]), + }; + } + i = j + 1; + + // shiny + j = buf.indexOf('|', i); + if (i !== j) set.shiny = true; + i = j + 1; + + // level + j = buf.indexOf('|', i); + if (i !== j) set.level = parseInt(buf.substring(i, j), 10); + i = j + 1; + + // happiness + j = buf.indexOf(']', i); + let misc; + if (j < 0) { + if (i < buf.length) misc = buf.substring(i).split(',', 6); + } else { + if (i !== j) misc = buf.substring(i, j).split(',', 6); + } + if (misc) { + set.happiness = (misc[0] ? Number(misc[0]) : 255); + set.hpType = misc[1]; + set.pokeball = misc[2]; + set.gigantamax = !!misc[3]; + set.dynamaxLevel = (misc[4] ? Number(misc[4]) : 10); + set.teraType = misc[5]; + } + if (j < 0) break; + i = j + 1; + } + + return team; + } + exportTeam(team: PokemonSet[] | string, gen = this.gen, hidestats = false) { + if (!team) return ''; + if (typeof team === 'string') { + if (team.indexOf('\n') >= 0) return team; + team = this.unpackTeam(team); + } + let text = ''; + for (const curSet of team) { + if (curSet.name && curSet.name !== curSet.species) { + text += '' + curSet.name + ' (' + curSet.species + ')'; + } else { + text += '' + curSet.species; + } + if (curSet.gender === 'M') text += ' (M)'; + if (curSet.gender === 'F') text += ' (F)'; + if (curSet.item) { + text += ' @ ' + curSet.item; + } + text += " \n"; + if (curSet.ability) { + text += 'Ability: ' + curSet.ability + " \n"; + } + if (curSet.level && curSet.level !== 100) { + text += 'Level: ' + curSet.level + " \n"; + } + if (curSet.shiny) { + text += 'Shiny: Yes \n'; + } + if (typeof curSet.happiness === 'number' && curSet.happiness !== 255 && !isNaN(curSet.happiness)) { + text += 'Happiness: ' + curSet.happiness + " \n"; + } + if (curSet.pokeball) { + text += 'Pokeball: ' + curSet.pokeball + " \n"; + } + if (curSet.hpType) { + text += 'Hidden Power: ' + curSet.hpType + " \n"; + } + if (typeof curSet.dynamaxLevel === 'number' && curSet.dynamaxLevel !== 10 && !isNaN(curSet.dynamaxLevel)) { + text += 'Dynamax Level: ' + curSet.dynamaxLevel + " \n"; + } + if (curSet.gigantamax) { + text += 'Gigantamax: Yes \n'; + } + if (gen === 9) { + const species = Dex.species.get(curSet.species); + text += 'Tera Type: ' + (species.forceTeraType || curSet.teraType || species.types[0]) + " \n"; + } + if (!hidestats) { + let first = true; + if (curSet.evs) { + let j: StatName; + for (j in BattleStatNames) { + if (!curSet.evs[j]) continue; + if (first) { + text += 'EVs: '; + first = false; + } else { + text += ' / '; + } + text += '' + curSet.evs[j] + ' ' + BattleStatNames[j]; + } + } + if (!first) { + text += " \n"; + } + if (curSet.nature) { + text += '' + curSet.nature + ' Nature' + " \n"; + } + first = true; + if (curSet.ivs) { + let defaultIvs = true; + let hpType = ''; + for (const move of curSet.moves) { + if (move.substr(0, 13) === 'Hidden Power ' && move.substr(0, 14) !== 'Hidden Power [') { + hpType = move.substr(13); + if (!Dex.types.isName(hpType)) { + alert(move + " is not a valid Hidden Power type."); + continue; + } + let stat: StatName; + for (stat in BattleStatNames) { + if ((curSet.ivs[stat] === undefined ? 31 : curSet.ivs[stat]) !== (Dex.types.get(hpType).HPivs?.[stat] || 31)) { + defaultIvs = false; + break; + } + } + } + } + if (defaultIvs && !hpType) { + let stat: StatName; + for (stat in BattleStatNames) { + if (curSet.ivs[stat] !== 31 && curSet.ivs[stat] !== undefined) { + defaultIvs = false; + break; + } + } + } + if (!defaultIvs) { + let stat: StatName; + for (stat in BattleStatNames) { + if (typeof curSet.ivs[stat] === 'undefined' || isNaN(curSet.ivs[stat]) || curSet.ivs[stat] === 31) continue; + if (first) { + text += 'IVs: '; + first = false; + } else { + text += ' / '; + } + text += '' + curSet.ivs[stat] + ' ' + BattleStatNames[stat]; + } + } + } + if (!first) { + text += " \n"; + } + } + if (curSet.moves) { + for (let move of curSet.moves) { + if (move.substr(0, 13) === 'Hidden Power ') { + move = move.substr(0, 13) + '[' + move.substr(13) + ']'; + } + if (move) { + text += '- ' + move + " \n"; + } + } + } + text += "\n"; + } + return text; + } + add(command?: string) { if (command) this.stepQueue.push(command); @@ -3577,12 +3827,8 @@ export class Battle { break; } case 'showteam': { - if (this.turn !== 0) return; - // @ts-ignore - if (!window.Storage?.unpackTeam || !window.Storage?.exportTeam) return; - // @ts-ignore - const team: PokemonSet[] = Storage.unpackTeam(args[2]); - if (!team) return; + const team = this.unpackTeam(args[2]); + if (!team.length) return; const side = this.getSide(args[1]); side.clearPokemon(); for (const set of team) { @@ -3596,20 +3842,7 @@ export class Battle { } if (set.teraType) pokemon.teraType = set.teraType; } - const exportedTeam = team.map(set => { - // @ts-ignore - let buf = Storage.exportTeam([set], this.gen).replace(/\n/g, '
'); - if (set.name && set.name !== set.species) { - buf = buf.replace(set.name, BattleLog.sanitizeHTML(`
${set.name}`)); - } else { - buf = buf.replace(set.species, `
${set.species}`); - } - if (set.item) { - buf = buf.replace(set.item, `${set.item} `); - } - return buf; - }).join(''); - this.add(`|raw|
Open Team Sheet for ${side.name}${exportedTeam}
`); + this.log(args, kwArgs); break; } case 'switch': case 'drag': case 'replace': { From d6efeb82bd6c8a0a4b203d11641240adc7b3eded Mon Sep 17 00:00:00 2001 From: Karthik <32044378+Karthik99999@users.noreply.github.com> Date: Thu, 19 Oct 2023 22:13:17 -0700 Subject: [PATCH 3/7] Fix speed buttons on downloaded replays (#2162) --- js/replay-embed.template.js | 24 +++++++++++++++++------- replays/battle.php | 2 +- src/battle-log.ts | 3 --- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/js/replay-embed.template.js b/js/replay-embed.template.js index 4a896900a31..74ea7974468 100644 --- a/js/replay-embed.template.js +++ b/js/replay-embed.template.js @@ -81,7 +81,7 @@ var Replays = { paused: true, }); - this.$('.replay-controls-2').html('
Speed:
Color scheme:
'); + this.$('.replay-controls-2').html('
Speed:
Color scheme:
'); // this works around a WebKit/Blink bug relating to float layout var rc2 = this.$('.replay-controls-2')[0]; @@ -138,13 +138,23 @@ var Replays = { break; case 'speed': - var speedTable = { - fast: 8, - normal: 800, - slow: 2500, - reallyslow: 5000 + var fadeTable = { + hyperfast: 40, + fast: 50, + normal: 300, + slow: 500, + reallyslow: 1000 }; - this.battle.messageDelay = speedTable[value]; + var delayTable = { + hyperfast: 1, + fast: 1, + normal: 1, + slow: 1000, + reallyslow: 3000 + }; + this.battle.messageShownTime = delayTable[value]; + this.battle.messageFadeTime = fadeTable[value]; + this.battle.scene.updateAcceleration(); break; } }, diff --git a/replays/battle.php b/replays/battle.php index d369f72d704..6e7e0b0c1c0 100644 --- a/replays/battle.php +++ b/replays/battle.php @@ -113,7 +113,7 @@ function userid($username) {
Speed: -
+
Color scheme: diff --git a/src/battle-log.ts b/src/battle-log.ts index 31decf131b6..99dffa486d1 100644 --- a/src/battle-log.ts +++ b/src/battle-log.ts @@ -1161,9 +1161,6 @@ export class BattleLog { buf += '\n'; buf += '\n'; buf += `${BattleLog.escapeHTML(battle.tier)} replay: ${BattleLog.escapeHTML(battle.p1.name)} vs. ${BattleLog.escapeHTML(battle.p2.name)}\n`; - buf += '\n'; buf += '
\n'; buf += '\n'; buf += '
\n'; From 0647322d4b2e072ed358c2648069bce2e6cc938d Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Sun, 22 Oct 2023 17:43:20 -0500 Subject: [PATCH 4/7] Storage: Improve handling of local teams versus remote teams This should help differentiate local teams and remote-loaded teams. When they're initially requested, the client now compares each new team against all the existing teams - if they appear to be similar, it doesn't add it to the builder (so that dupe teams don't show up). It considers them similar if they have the same mons, title, and format. Otherwise, if it finds one that's close, it'll add it but marked as (server version) so people can tell which is which. --- js/storage.js | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/js/storage.js b/js/storage.js index f9794cf3c9d..359299f2632 100644 --- a/js/storage.js +++ b/js/storage.js @@ -585,6 +585,32 @@ Storage.loadTeams = function () { } catch (e) {} }; +/** returns false to add the team, true to not add it, 'rename' to add it under a diff name */ +Storage.compareTeams = function (serverTeam, localTeam) { + // if titles match exactly and mons are the same, assume they're the same team + // if they don't match, it might be edited, but we'll go ahead and add it to the user's + // teambuilder since they may want that old version around. just go ahead and edit the name + var mons = serverTeam.team.split(','); + var matches = 0; + var otherMons = Storage.unpackTeam(localTeam.team); + for (var i = 0; i < mons.length; i++) { + for (var j = 0; j < otherMons.length; j++) { + if (toID(otherMons[j].species) === toID(mons[i])) { + matches++; + } + } + } + var localTeamName = localTeam.name.replace(' (server version)', ''); + if (!(serverTeam.name === localTeamName && serverTeam.format === localTeam.format)) { + return false; + } + // if it's been edited since, invalidate the team id on this one (count it as new) + // and load from server + if (mons.length !== otherMons.length || matches !== otherMons.length) return 'rename'; + if (serverTeam.teamid === localTeam.teamid && localTeam.teamid) return true; + return true; +}; + Storage.loadRemoteTeams = function (after) { $.get(app.user.getActionPHP(), {act: 'getteams'}, Storage.safeJSON(function (data) { if (data.actionerror) { @@ -595,12 +621,17 @@ Storage.loadRemoteTeams = function (after) { var matched = false; for (var j = 0; j < Storage.teams.length; j++) { var curTeam = Storage.teams[j]; - if (curTeam.teamid === team.teamid) { + var match = Storage.compareTeams(team, curTeam); + if (match === true) { // prioritize locally saved teams over remote // as so to not overwrite changes matched = true; break; } + if (match === 'rename') { + delete curTeam.teamid; + team.name += ' (server version)'; + } } team.loaded = false; if (!matched) { From 68a9694b3ac41c2f83bf431dbd48704ce4a66293 Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Sun, 22 Oct 2023 18:43:41 -0500 Subject: [PATCH 5/7] Storage: Properly compare team names --- js/storage.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/js/storage.js b/js/storage.js index 359299f2632..20d8ca5783b 100644 --- a/js/storage.js +++ b/js/storage.js @@ -600,8 +600,11 @@ Storage.compareTeams = function (serverTeam, localTeam) { } } } - var localTeamName = localTeam.name.replace(' (server version)', ''); - if (!(serverTeam.name === localTeamName && serverTeam.format === localTeam.format)) { + var sanitize = function (name) { + return (name || "").replace(' (server version)', '').trim(); + }; + var nameMatches = sanitize(serverTeam.name) === sanitize(localTeam.name); + if (!(nameMatches && serverTeam.format === localTeam.format)) { return false; } // if it's been edited since, invalidate the team id on this one (count it as new) From afbb31da920711e7011a80571504fee049edb4b5 Mon Sep 17 00:00:00 2001 From: Leonard Craft III Date: Mon, 23 Oct 2023 00:00:42 -0500 Subject: [PATCH 6/7] Teambuilder: Fix empty learnsets in oldgen VGC formats again --- src/battle-dex-search.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/battle-dex-search.ts b/src/battle-dex-search.ts index 81807f51ba8..52e5eb1305c 100644 --- a/src/battle-dex-search.ts +++ b/src/battle-dex-search.ts @@ -1501,8 +1501,8 @@ class BattleMoveSearch extends BattleTypedSearch<'move'> { const isSTABmons = (format.includes('stabmons') || format === 'staaabmons'); const isTradebacks = format.includes('tradebacks'); const regionBornLegality = dex.gen >= 6 && - /^battle(spot|stadium|festival)/.test(format) || format.startsWith('vgc') || - (dex.gen === 9 && this.formatType !== 'natdex'); + (/^battle(spot|stadium|festival)/.test(format) || format.startsWith('vgc') || + (dex.gen === 9 && this.formatType !== 'natdex')); let learnsetid = this.firstLearnsetid(species.id); let moves: string[] = []; From 65d387a82346b73d14bfa16d236dc7a4260efd0a Mon Sep 17 00:00:00 2001 From: Mia <49593536+mia-pi-git@users.noreply.github.com> Date: Mon, 23 Oct 2023 14:06:49 -0500 Subject: [PATCH 7/7] Storage: Account for malformed team names in loading comparisons --- js/storage.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/js/storage.js b/js/storage.js index 20d8ca5783b..7c8457b2180 100644 --- a/js/storage.js +++ b/js/storage.js @@ -601,7 +601,7 @@ Storage.compareTeams = function (serverTeam, localTeam) { } } var sanitize = function (name) { - return (name || "").replace(' (server version)', '').trim(); + return (name || "").replace(/\s+\(server version\)/g, '').trim(); }; var nameMatches = sanitize(serverTeam.name) === sanitize(localTeam.name); if (!(nameMatches && serverTeam.format === localTeam.format)) { @@ -633,7 +633,9 @@ Storage.loadRemoteTeams = function (after) { } if (match === 'rename') { delete curTeam.teamid; - team.name += ' (server version)'; + if (!team.name.endsWith(' (server version)')) { + team.name += ' (server version)'; + } } } team.loaded = false;