diff --git a/js/pokedex-pokemon.js b/js/pokedex-pokemon.js index e3e7448..ce73d83 100644 --- a/js/pokedex-pokemon.js +++ b/js/pokedex-pokemon.js @@ -76,7 +76,10 @@ var PokedexPokemonPanel = PokedexResultPanel.extend({ buf += ''; buf += '
'; - buf += '
Base stats:
'; + buf += '
Defensive effectiveness:
'; + buf += ''; + + buf += '
Base stats:
'; var StatTitles = { hp: "HP", @@ -299,10 +302,23 @@ var PokedexPokemonPanel = PokedexResultPanel.extend({ }, events: { 'click .tabbar button': 'selectTab', + 'click .toggle-eff': 'toggleEffectiveness', 'input input[name=level]': 'updateLevel', 'keyup input[name=level]': 'updateLevel', 'change input[name=level]': 'updateLevel', }, + toggleEffectiveness: function(e) { + var btn = $(e.currentTarget); + var targetId = btn.data('target-eff'); + var content = this.$('#' + targetId); + if (content.is(':visible')) { + content.hide(); + btn.text('Show'); + } else { + content.show(); + btn.text('Hide'); + } + }, updateLevel: function(e) { var val = this.$('input[name=level]').val(); var level = val === '' ? 100 : parseInt(val, 10); diff --git a/js/pokedex.js b/js/pokedex.js index e27a1fd..ef7004b 100644 --- a/js/pokedex.js +++ b/js/pokedex.js @@ -123,40 +123,267 @@ var PokedexTypePanel = PokedexResultPanel.extend({ var buf = '
'; buf += ' Pokédex'; buf += '

'+this.type+'

'; + buf += '
'; - var atLeastOne = false; + buf += '
Defensive effectiveness:
'; + buf += '
Offensive effectiveness:
'; + buf += '
'; - buf += '
Weaknesses:
'; - for (var attackType in type.damageTaken) { - if (type.damageTaken[attackType] == 1) { - buf += ''+Dex.getTypeIcon(attackType)+' '; - atLeastOne = true; + // move list + buf += '
'; + buf += '
    '; + buf += '
'; + + buf += '
'; + + this.html(buf); + + setTimeout(this.renderMoveList.bind(this)); + }, + getTypeEffectiveness: function(attacking, defending) { + var attackingName = typeof attacking === 'string' ? attacking : attacking.name; + if (Array.isArray(defending)) { + var total = 0; + for (var i = 0; i < defending.length; i++) { + var defendingType = typeof defending[i] === 'string' ? Dex.types.get(defending[i]) : defending[i]; + switch (defendingType.damageTaken[attackingName]) { + case 1: // super effective + ++total; + break; + case 2: // not very effective + --total; + break; + case 3: // immune + return null; + } } + return total; } - if (!atLeastOne) { - buf += 'No weaknesses'; + var defendingType = typeof defending === 'string' ? Dex.types.get(defending) : defending; + switch (defendingType.damageTaken[attackingName]) { + case 1: // super effective + return 1; + case 2: // not very effective + return -1; + case 3: // immune + return null; + default: + return 0; + } + }, + effectTypes: ['hail', 'sandstorm', 'powder', 'frz', 'brn', 'psn', 'par'], + getDefensiveEffectivenessData: function(defending) { + var immsMap = {}; + var addImm = function(i) { immsMap[i] = true; }; + if (Array.isArray(defending)) { + for (var i = 0; i < defending.length; i++) { + var defendingType = typeof defending[i] === 'string' ? Dex.types.get(defending[i]) : defending[i]; + for (var j = 0; j < this.effectTypes.length; j++) { + if (defendingType.damageTaken[this.effectTypes[j]] === 3) { + addImm(this.effectTypes[j]); + } + } + } + } else { + var defendingType = typeof defending === 'string' ? Dex.types.get(defending) : defending; + for (var j = 0; j < this.effectTypes.length; j++) { + if (defendingType.damageTaken[this.effectTypes[j]] === 3) { + addImm(this.effectTypes[j]); + } + } } - buf += ''; - buf += '
Resistances:
'; - atLeastOne = false; - for (var attackType in type.damageTaken) { - if (type.damageTaken[attackType] == 2) { - buf += ''+Dex.getTypeIcon(attackType)+' '; - atLeastOne = true; + // baseEffMap: typeName -> effectiveness level (-2 to +2) or null for immune + var baseEffMap = {}; + var allTypes = Dex.types.all(); + for (var i = 0; i < allTypes.length; i++) { + var eff = this.getTypeEffectiveness(allTypes[i], defending); + if (eff === null) { + addImm(allTypes[i].name); + baseEffMap[allTypes[i].name] = null; + } else { + baseEffMap[allTypes[i].name] = eff; } } - if (!atLeastOne) { - buf += 'No resistances'; + + var effs = [[], [], [], [], []]; + for (var typeName in baseEffMap) { + if (baseEffMap[typeName] !== null) { + effs[baseEffMap[typeName] + 2].push(typeName); + } } - buf += '
'; - buf += '
Immunities:
'; - atLeastOne = false; - for (var attackType in type.damageTaken) { - if (type.damageTaken[attackType] == 3) { - if (attackType === attackType.toLowerCase()) { - switch (attackType) { + var imms = Object.keys(immsMap); + return {effs: effs, imms: imms, baseEffMap: baseEffMap}; + }, + renderDefensiveEffectiveness: function(defending) { + var data = this.getDefensiveEffectivenessData(defending); + return this.renderEffectivenessTable(data.effs, data.imms, 'defense'); + }, + abilityImmunities: { + 'Levitate': ['Ground'], + 'Flash Fire': ['Fire'], + 'Volt Absorb': ['Electric'], + 'Water Absorb': ['Water'], + 'Sap Sipper': ['Grass'], + 'Dry Skin': ['Water'], + 'Storm Drain': ['Water'], + 'Lightning Rod': ['Electric'], + 'Motor Drive': ['Electric'] + }, + // abilities that shift effectiveness of specific types by a number of steps + // negative = less damage (good for defender), positive = more damage (bad for defender) + abilityTypeModifiers: { + 'Thick Fat': {'Fire': -1, 'Ice': -1}, + 'Heatproof': {'Fire': -1}, + 'Water Bubble': {'Fire': -1}, + 'Dry Skin': {'Fire': 1}, + 'Fluffy': {'Fire': 1} + }, + // abilities that reduce all super-effective damage (displayed as a note) + abilitySEReduction: ['Filter', 'Solid Rock', 'Prism Armor'], + renderDefensiveEffectivenessWithAbilities: function(defending, abilities) { + var data = this.getDefensiveEffectivenessData(defending); + var buf = this.renderEffectivenessTable(data.effs, data.imms, 'defense'); + + // Now compute ability-modified effectiveness for each ability the Pokémon can have + var abilityChanges = []; + var seReductionAbilities = []; + var wonderGuardAbilities = []; + + for (var slot in abilities) { + var abilityName = abilities[slot]; + if (!abilityName) continue; + + var isWonderGuard = (abilityName === 'Wonder Guard'); + var immuneTypes = this.abilityImmunities[abilityName] || []; + var typeMods = this.abilityTypeModifiers[abilityName] || {}; + var isSEReducer = this.abilitySEReduction.indexOf(abilityName) >= 0; + + if (isWonderGuard) { + wonderGuardAbilities.push({name: abilityName, slot: slot}); + continue; + } + + if (isSEReducer) { + seReductionAbilities.push({name: abilityName, slot: slot}); + continue; + } + + if (immuneTypes.length === 0 && Object.keys(typeMods).length === 0) continue; + + var changes = []; + // Check immunity changes + for (var k = 0; k < immuneTypes.length; k++) { + var iType = immuneTypes[k]; + if (data.baseEffMap[iType] !== null) { + // Not already immune by type, so this ability grants immunity + changes.push({type: iType, from: data.baseEffMap[iType], to: 'immune'}); + } + } + // Check type modifier changes + for (var modType in typeMods) { + if (data.baseEffMap[modType] === null) continue; // already immune + if (immuneTypes.indexOf(modType) >= 0) continue; // already handled as immune + var newEff = data.baseEffMap[modType] + typeMods[modType]; + if (newEff < -2) newEff = -2; + if (newEff > 2) newEff = 2; + if (newEff !== data.baseEffMap[modType]) { + changes.push({type: modType, from: data.baseEffMap[modType], to: newEff}); + } + } + + if (changes.length > 0) { + abilityChanges.push({name: abilityName, slot: slot, changes: changes}); + } + } + + if (abilityChanges.length > 0 || seReductionAbilities.length > 0 || wonderGuardAbilities.length > 0) { + buf += '
'; + + var effLabels = {'-2': '.25x', '-1': '.5x', '0': '1x', '1': '2x', '2': '4x', 'immune': '0x'}; + + for (var a = 0; a < abilityChanges.length; a++) { + var ac = abilityChanges[a]; + var slotLabel = ac.slot === 'H' ? ' (Hidden)' : (ac.slot === 'S' ? ' (Special)' : ''); + buf += '
'+ac.name+''+slotLabel+': '; + var parts = []; + for (var c = 0; c < ac.changes.length; c++) { + var ch = ac.changes[c]; + var fromLabel = effLabels[String(ch.from)]; + var toLabel = effLabels[String(ch.to)]; + var typeIcon = ''+Dex.getTypeIcon(ch.type)+''; + parts.push(typeIcon + ' ' + fromLabel + ' ' + toLabel); + } + buf += parts.join(', '); + buf += '
'; + } + + for (var a = 0; a < seReductionAbilities.length; a++) { + var sa = seReductionAbilities[a]; + var slotLabel = sa.slot === 'H' ? ' (Hidden)' : (sa.slot === 'S' ? ' (Special)' : ''); + buf += '
'+sa.name+''+slotLabel+': reduces super-effective damage by 25%
'; + } + + for (var a = 0; a < wonderGuardAbilities.length; a++) { + var wa = wonderGuardAbilities[a]; + var slotLabel = wa.slot === 'H' ? ' (Hidden)' : (wa.slot === 'S' ? ' (Special)' : ''); + buf += '
'+wa.name+''+slotLabel+': immune to all non-super-effective moves
'; + } + + buf += '
'; + } + + return buf; + }, + renderOffensiveEffectiveness: function(attacking) { + var effs = [[], [], [], [], []]; + var imms = []; + for (var types = Dex.types.all(), i = 0; i < types.length; i++) { + var eff = this.getTypeEffectiveness(attacking, types[i]); + if (eff === null) { + imms.push(types[i].name); + } else { + effs[eff + 2].push(types[i].name); + } + } + return this.renderEffectivenessTable(effs, imms, 'offense'); + }, + renderEffectivenessTable: function(effs, imms, cls) { + var hasDoubles = effs[0].length > 0 || effs[4].length > 0; // any .25x or 4x? + var buf = '
'; + + var i, max; + if (hasDoubles) { + buf += ''; + i = 0; + max = 4; + } else { + buf += ''; + i = 1; + max = 3; + } + + buf += ''; + for (; i <= max; i++) { + if (i === 2) continue; // don't show neutral effectiveness + buf += ''; + } + buf += ''; + + if (imms.length > 0) { + buf += ''; } - buf += ''; - - buf += ''; - - // move list - buf += ''; - buf += ''; - - buf += ''; - - this.html(buf); - setTimeout(this.renderMoveList.bind(this)); + buf += '
.25x.5x2x4x
.5x2x
'; + var atLeastOne = false; + for (let j = 0; j < effs[i].length; j++) { + buf += ''+Dex.getTypeIcon(effs[i][j])+' '; + atLeastOne = true; + } + if (!atLeastOne) buf += '-'; + buf += '
'; + buf += '
Immune:
'; + for (var i = 0; i < imms.length; i++) { + if (imms[i] === imms[i].toLowerCase()) { + switch (imms[i]) { case 'hail': buf += '
Hail damage
'; break; @@ -179,37 +406,31 @@ var PokedexTypePanel = PokedexResultPanel.extend({ buf += '
PAR status
'; break; } - if (!atLeastOne) atLeastOne = null; continue; } - buf += ''+Dex.getTypeIcon(attackType)+' '; - atLeastOne = true; - } - } - if (!atLeastOne) { - if (atLeastOne === null) { - buf += '
No type immunities
'; - } else { - buf += 'No immunities'; + buf += ''+Dex.getTypeIcon(imms[i])+' '; } + buf += '
'; + return buf; }, events: { - 'click .tabbar button': 'selectTab' + 'click .tabbar button': 'selectTab', + 'click .toggle-eff': 'toggleEffectiveness' + }, + toggleEffectiveness: function(e) { + var btn = $(e.currentTarget); + var targetId = btn.data('target-eff'); + var content = this.$('#' + targetId); + if (content.is(':visible')) { + content.hide(); + btn.text('Show'); + } else { + content.show(); + btn.text('Hide'); + } }, selectTab: function(e) { this.$('.tabbar button').removeClass('cur'); diff --git a/theme/pokedex.css b/theme/pokedex.css index af9567f..bf6e26a 100644 --- a/theme/pokedex.css +++ b/theme/pokedex.css @@ -409,6 +409,59 @@ a.subtle:hover { .type.special{background-color:#ADB1BD;background:linear-gradient(#ADB1BD,#7D828D);border-color:#A1A5AD;color:#E0E2E4;} .type.status{background-color:#CBC9CB;background:linear-gradient(#CBC9CB,#AAA6AA);border-color:#A99890;color:#F5F4F5;} +.typeeff { + width: 100%; + table-layout: fixed; + border-collapse: collapse; + font-size: 9pt; +} +.typeeff tr { + line-height: 1.25; +} +.typeeff th { + font-size: 7pt; + font-weight: bold; +} +.typeeff td { + padding: 2px 6px 0; + border: 1px solid #AAAAAA; +} +.typeeff.defense td.eff4, .typeeff.offense td.eff0 { + background-color: rgba(255,128,128,.2); +} +.typeeff.defense td.eff3, .typeeff.offense td.eff1 { + background-color: rgba(255,128,128,.1); +} +.typeeff.defense td.eff1, .typeeff.offense td.eff3 { + background-color: rgba(128,255,128,.1); +} +.typeeff.defense td.eff0, .typeeff.offense td.eff4 { + background-color: rgba(128,255,128,.2); +} +.typeeff td.immune { + padding-bottom: 2px; + background-color: rgba(128,128,128,.1); +} + +.dexentry dt .toggle-eff { + font-size: 7pt; + padding: 0 6px; + margin-left: 4px; + vertical-align: middle; +} + +.typeeff-abilities { + margin-top: 4px; + font-size: 8pt; +} +.typeeff-abilities .ability-note { + padding: 2px 0; + color: #555; +} +.typeeff-abilities .ability-note img.pixelated { + vertical-align: middle; +} + .pokemonicon, .picon { display: inline-block; vertical-align: -6px;