mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-27 21:58:08 -05:00
@@ -1,6 +1,15 @@
|
||||
import type { DamageType, TenacityPlayerDeficit } from "./analyzer-types";
|
||||
|
||||
export const MAX_LDE_INTENSITY = 21;
|
||||
/** Opponent's remaining points when Last-Ditch Effort starts ramping up in ranked modes. */
|
||||
export const LDE_START_POINTS = 50;
|
||||
|
||||
/** Opponent's remaining points when Last-Ditch Effort reaches its maximum. */
|
||||
const LDE_MAX_POINTS = 30;
|
||||
|
||||
const MAX_LDE_INTENSITY = LDE_START_POINTS - LDE_MAX_POINTS + 1;
|
||||
|
||||
/** Intensity standing for the clock based activation, which always grants the maximum boost. */
|
||||
export const LDE_CLOCK_INTENSITY = MAX_LDE_INTENSITY + 1;
|
||||
|
||||
/** Every ability point total reachable by some main/sub slot combination, ascending. */
|
||||
export function possibleApValues() {
|
||||
|
||||
@@ -25,7 +25,7 @@ describe("analyzerSearchParams", () => {
|
||||
weapon: [0, 10, 8000],
|
||||
build: [EMPTY_BUILD, FULL_BUILD, PARTIAL_BUILD],
|
||||
build2: [EMPTY_BUILD, FULL_BUILD],
|
||||
lde: [0, 21, 10],
|
||||
lde: [0, 22, 10],
|
||||
effect: [[], ["LDE"], ["OG", "TACTICOOLER"]],
|
||||
focused: [1, 2, 3],
|
||||
});
|
||||
@@ -43,7 +43,7 @@ describe("analyzerSearchParams", () => {
|
||||
["XX,XX,XX,XX,XX,XX,XX,XX,XX,XX,XX,XX"],
|
||||
]);
|
||||
assertDecodesToDefault(analyzerSearchParams, "lde", [
|
||||
["22"],
|
||||
["23"],
|
||||
["-1"],
|
||||
["x"],
|
||||
]);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { mainWeaponIds } from "~/modules/in-game-lists/weapon-ids";
|
||||
import * as SearchParams from "~/modules/search-params/search-params";
|
||||
import { codec, SP } from "~/modules/search-params/search-params";
|
||||
import { numericEnum } from "~/utils/schema";
|
||||
import { MAX_LDE_INTENSITY } from "./analyzer-constants";
|
||||
import { LDE_CLOCK_INTENSITY } from "./analyzer-constants";
|
||||
import type { SpecialEffectType } from "./analyzer-types";
|
||||
import { deserializeBuild, serializeBuild } from "./core/serializer";
|
||||
import { SPECIAL_EFFECTS } from "./core/specialEffects";
|
||||
@@ -43,7 +43,7 @@ export const analyzerSearchParams = SearchParams.define({
|
||||
v.number(),
|
||||
v.integer(),
|
||||
v.minValue(0),
|
||||
v.maxValue(MAX_LDE_INTENSITY),
|
||||
v.maxValue(LDE_CLOCK_INTENSITY),
|
||||
),
|
||||
{
|
||||
default: 0,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { applySpecialEffects } from "./specialEffects";
|
||||
import {
|
||||
applySpecialEffects,
|
||||
lastDitchEffortIntensityToOpponentPoints,
|
||||
} from "./specialEffects";
|
||||
|
||||
describe("applySpecialEffects()", () => {
|
||||
test("Adds an effect to empty build", () => {
|
||||
@@ -84,3 +87,13 @@ describe("applySpecialEffects()", () => {
|
||||
expect(aps.get("ISM")).toBe(13);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lastDitchEffortIntensityToOpponentPoints()", () => {
|
||||
test.each([
|
||||
{ why: "first step of the ramp", intensity: 1, points: 50 },
|
||||
{ why: "middle of the ramp", intensity: 15, points: 36 },
|
||||
{ why: "maximum boost", intensity: 21, points: 30 },
|
||||
])("$why", ({ intensity, points }) => {
|
||||
expect(lastDitchEffortIntensityToOpponentPoints(intensity)).toBe(points);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MAX_AP } from "../analyzer-constants";
|
||||
import { LDE_START_POINTS, MAX_AP } from "../analyzer-constants";
|
||||
import type { AbilityPoints } from "../analyzer-types";
|
||||
|
||||
export const SPECIAL_EFFECTS = [
|
||||
@@ -135,6 +135,11 @@ export function lastDitchEffortIntensityToAp(intensity: number) {
|
||||
return Math.floor((18 / 21) * intensity);
|
||||
}
|
||||
|
||||
/** Opponent's remaining points in an Anarchy Battle that the given Last-Ditch Effort intensity corresponds to. */
|
||||
export function lastDitchEffortIntensityToOpponentPoints(intensity: number) {
|
||||
return LDE_START_POINTS + 1 - intensity;
|
||||
}
|
||||
|
||||
function lastDitchEffortValues(intensity: number) {
|
||||
const ap = lastDitchEffortIntensityToAp(intensity);
|
||||
|
||||
|
||||
@@ -56,8 +56,9 @@
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
/* Fixed width so picking a longer option doesn't resize the column */
|
||||
.ldeIntensitySelect {
|
||||
font-size: var(--font-2xs);
|
||||
width: 12.5rem;
|
||||
}
|
||||
|
||||
.apSummary {
|
||||
|
||||
@@ -8,6 +8,7 @@ import * as R from "remeda";
|
||||
import { AbilitiesSelector } from "~/components/AbilitiesSelector";
|
||||
import { Ability } from "~/components/Ability";
|
||||
import { Chart } from "~/components/Chart";
|
||||
import { SendouSelect, SendouSelectItem } from "~/components/elements/Select";
|
||||
import { SendouSwitch } from "~/components/elements/Switch";
|
||||
import {
|
||||
SendouTab,
|
||||
@@ -59,8 +60,8 @@ import { SendouPopover } from "../../../components/elements/Popover";
|
||||
import { metaTags, ogPageImage } from "../../../utils/remix";
|
||||
import {
|
||||
damageTypeToWeaponType,
|
||||
LDE_CLOCK_INTENSITY,
|
||||
MAX_AP,
|
||||
MAX_LDE_INTENSITY,
|
||||
TENACITY_PLAYER_DEFICITS,
|
||||
} from "../analyzer-constants";
|
||||
import { useAnalyzeBuild } from "../analyzer-hooks";
|
||||
@@ -82,6 +83,7 @@ import {
|
||||
} from "../core/abilityChunksCalc";
|
||||
import {
|
||||
lastDitchEffortIntensityToAp,
|
||||
lastDitchEffortIntensityToOpponentPoints,
|
||||
SPECIAL_EFFECTS,
|
||||
} from "../core/specialEffects";
|
||||
import { buildStats } from "../core/stats";
|
||||
@@ -1332,8 +1334,6 @@ function EffectsSelector({
|
||||
handleAddEffect: (effect: SpecialEffectType) => void;
|
||||
handleRemoveEffect: (effect: SpecialEffectType) => void;
|
||||
}) {
|
||||
const { t } = useTranslation(["weapons", "analyzer"]);
|
||||
|
||||
const effectsToShow = SPECIAL_EFFECTS.filter(
|
||||
(effect) =>
|
||||
!isAbility(effect.type) ||
|
||||
@@ -1357,26 +1357,10 @@ function EffectsSelector({
|
||||
</div>
|
||||
<div>
|
||||
{effect.type === "LDE" ? (
|
||||
<select
|
||||
value={ldeIntensity}
|
||||
onChange={(e) =>
|
||||
handleLdeIntensityChange(Number(e.target.value))
|
||||
}
|
||||
className={styles.ldeIntensitySelect}
|
||||
>
|
||||
{new Array(MAX_LDE_INTENSITY + 1).fill(null).map((_, i) => {
|
||||
const percentage = ((i / MAX_LDE_INTENSITY) * 100)
|
||||
.toFixed(2)
|
||||
.replace(".00", "");
|
||||
|
||||
return (
|
||||
<option key={i} value={i}>
|
||||
{percentage}% (+{lastDitchEffortIntensityToAp(i)}{" "}
|
||||
{t("analyzer:abilityPoints.short")})
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<LdeIntensitySelect
|
||||
ldeIntensity={ldeIntensity}
|
||||
onChange={handleLdeIntensityChange}
|
||||
/>
|
||||
) : (
|
||||
<SendouSwitch
|
||||
isSelected={effects.includes(effect.type)}
|
||||
@@ -1395,6 +1379,45 @@ function EffectsSelector({
|
||||
);
|
||||
}
|
||||
|
||||
function LdeIntensitySelect({
|
||||
ldeIntensity,
|
||||
onChange,
|
||||
}: {
|
||||
ldeIntensity: number;
|
||||
onChange: (newLdeIntensity: number) => void;
|
||||
}) {
|
||||
const { t } = useTranslation(["analyzer"]);
|
||||
|
||||
const intensityLabel = (intensity: number) => {
|
||||
if (intensity === 0) return t("analyzer:lde.inactive");
|
||||
|
||||
const activation =
|
||||
intensity === LDE_CLOCK_INTENSITY
|
||||
? t("analyzer:lde.secondsLeft")
|
||||
: t("analyzer:lde.points", {
|
||||
points: lastDitchEffortIntensityToOpponentPoints(intensity),
|
||||
});
|
||||
const ap = lastDitchEffortIntensityToAp(intensity);
|
||||
|
||||
return `${activation} (+${ap} ${t("analyzer:abilityPoints.short")})`;
|
||||
};
|
||||
|
||||
return (
|
||||
<SendouSelect
|
||||
selectedKey={ldeIntensity}
|
||||
onSelectionChange={(intensity) => onChange(Number(intensity))}
|
||||
aria-label="Last-Ditch Effort intensity"
|
||||
className={styles.ldeIntensitySelect}
|
||||
>
|
||||
{nullFilledArray(LDE_CLOCK_INTENSITY + 1).map((_, intensity) => (
|
||||
<SendouSelectItem key={intensity} id={intensity}>
|
||||
{intensityLabel(intensity)}
|
||||
</SendouSelectItem>
|
||||
))}
|
||||
</SendouSelect>
|
||||
);
|
||||
}
|
||||
|
||||
function AbilityChunksRequired({
|
||||
build,
|
||||
}: {
|
||||
|
||||
5
changelog/2026-09-25-lde-activation-explained.md
Normal file
5
changelog/2026-09-25-lde-activation-explained.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
navItem: analyzer
|
||||
type: feature
|
||||
---
|
||||
Last-Ditch Effort dropdown shows what activates the ability (the opposing team's points left, or the last 30 seconds) instead of a percentage
|
||||
@@ -169,6 +169,9 @@
|
||||
"patch": "Patch:",
|
||||
"abilityPoints.short": "AP",
|
||||
"abilityChunks": "Pris i Egenskabs-fragmenter",
|
||||
"lde.inactive": "",
|
||||
"lde.points": "",
|
||||
"lde.secondsLeft": "",
|
||||
"torpedoExplanation": "Man kan kun have én aktiv torpedo ad gangen.",
|
||||
"trackingSubDefExplanation": "Point, sensor, Ink Mine og Angle shooter sporingsvarighedsværdier er udregnet for fjender, der bruger 0AP af Sub power up. (Opgraderet sub-våben)",
|
||||
"damageSubDefExplanation": "Sprinker og Splash-vægge er ikke vist i tabellen, men skaden fra dem kan også reduceres via ”Sub Resistance Up” (forsvar mod sub-våben)",
|
||||
|
||||
@@ -169,6 +169,9 @@
|
||||
"patch": "Patch:",
|
||||
"abilityPoints.short": "EP",
|
||||
"abilityChunks": "Effekt-Fragmente erforderlich",
|
||||
"lde.inactive": "",
|
||||
"lde.points": "",
|
||||
"lde.secondsLeft": "",
|
||||
"torpedoExplanation": "Es ist unmöglich, mehr als einen Torpedo gleichzeitig abzufeuern (wegen der Spielmechanik).",
|
||||
"trackingSubDefExplanation": "Die Markierungsdauern von Detektor, Tintenmine and Winkelmarker basieren auf einem Gegner mit 0 EP Sekundärstärke +.",
|
||||
"damageSubDefExplanation": "",
|
||||
|
||||
@@ -169,6 +169,9 @@
|
||||
"patch": "Patch:",
|
||||
"abilityPoints.short": "AP",
|
||||
"abilityChunks": "Ability Chunks Required",
|
||||
"lde.inactive": "Not active",
|
||||
"lde.points": "{{points}} points",
|
||||
"lde.secondsLeft": "30 sec left",
|
||||
"torpedoExplanation": "You can't launch more than one torpedo at a time due to game mechanics.",
|
||||
"trackingSubDefExplanation": "Point Sensor, Ink Mine and Angle Shooter tracking times are calculated against an opponent with 0AP of Sub Power Up.",
|
||||
"damageSubDefExplanation": "Sprinkler and Splash Wall are not shown in the table but the damage caused by them can also be lowered by Sub Resistance Up",
|
||||
|
||||
@@ -171,6 +171,9 @@
|
||||
"patch": "Parche:",
|
||||
"abilityPoints.short": "AP",
|
||||
"abilityChunks": "Fragmentos de potenciador requeridos",
|
||||
"lde.inactive": "",
|
||||
"lde.points": "",
|
||||
"lde.secondsLeft": "",
|
||||
"torpedoExplanation": "No se puede lanzar más de un torpedo a la vez.",
|
||||
"trackingSubDefExplanation": "Los tiempos de rastreo del Rastreador, la Bomba trampa y el Tiralíneas se calculan contra un oponente con 0 AP de Resistencia secundaria.",
|
||||
"damageSubDefExplanation": "El daño debido al aspersor y al telón de tinta también puede ser reducido con Resistencia secundaria.",
|
||||
|
||||
@@ -171,6 +171,9 @@
|
||||
"patch": "Parche:",
|
||||
"abilityPoints.short": "AP",
|
||||
"abilityChunks": "Fragmentos de potenciador requeridos",
|
||||
"lde.inactive": "",
|
||||
"lde.points": "",
|
||||
"lde.secondsLeft": "",
|
||||
"torpedoExplanation": "No se puede lanzar más de un torpedo a la vez.",
|
||||
"trackingSubDefExplanation": "Tiempos de rastreo de Rastreador, Mina de Tinta y Dardo Resaltador son calculados contra un oponente con 0AP de Resistencia Secundaria.",
|
||||
"damageSubDefExplanation": "Daño debido al aspersor y barricada también puede ser reducido con Resistencia secundaria.",
|
||||
|
||||
@@ -171,6 +171,9 @@
|
||||
"patch": "Version:",
|
||||
"abilityPoints.short": " PC",
|
||||
"abilityChunks": "Fragments de bonus requis",
|
||||
"lde.inactive": "",
|
||||
"lde.points": "",
|
||||
"lde.secondsLeft": "",
|
||||
"torpedoExplanation": "Vous ne pouvez pas lancer plus d'une bentorpille à la fois à cause d'une mécanique du jeu.",
|
||||
"trackingSubDefExplanation": "Le temps de traçage du détecteur, de la mine et du ricocheur sont calculés contre un adversaire avec 0 PC d'arme secondaire +.",
|
||||
"damageSubDefExplanation": "La Fontaine et le Mur d'encre ne sont pas listés mais les dégâts qu'ils infligent peuvent aussi être réduits par Filtre à secondaires.",
|
||||
|
||||
@@ -171,6 +171,9 @@
|
||||
"patch": "Version:",
|
||||
"abilityPoints.short": " PC",
|
||||
"abilityChunks": "Fragments de bonus requis",
|
||||
"lde.inactive": "",
|
||||
"lde.points": "",
|
||||
"lde.secondsLeft": "",
|
||||
"torpedoExplanation": "Vous ne pouvez pas lancer plus d'une bentorpille à la fois à cause d'une mécanique du jeu.",
|
||||
"trackingSubDefExplanation": "Le temps de traçage du détecteur, de la mine et du ricocheur sont calculés contre un adversaire avec 0 PC d'arme secondaire +.",
|
||||
"damageSubDefExplanation": "La Fontaine et le Mur d'encre ne sont pas listés mais les dégâts qu'ils infligent peuvent aussi être réduits par Filtre à secondaires.",
|
||||
|
||||
@@ -171,6 +171,9 @@
|
||||
"patch": "עדכון:",
|
||||
"abilityPoints.short": "נ'י",
|
||||
"abilityChunks": "כמות צ'קאנס יכולות הנדרשת",
|
||||
"lde.inactive": "",
|
||||
"lde.points": "",
|
||||
"lde.secondsLeft": "",
|
||||
"torpedoExplanation": "לא ניתן לשגר יותר מטורפדו אחד בכל פעם בעקבות מכניקת המשחק.",
|
||||
"trackingSubDefExplanation": "זמני המעקב של Point Sensor, Ink Mine ו-Angle Shooter המחושבים מול יריב עם 0AP של Sub Power Up.",
|
||||
"damageSubDefExplanation": "Sprinkler ו-Splash Wall אינם מוצגים בטבלה אך ניתן להוריד את הנזק שנגרם מהם גם על ידי Sub Resistance Up.",
|
||||
|
||||
@@ -171,6 +171,9 @@
|
||||
"patch": "Versione gioco:",
|
||||
"abilityPoints.short": "AP",
|
||||
"abilityChunks": "Frammenti abilità necessari",
|
||||
"lde.inactive": "",
|
||||
"lde.points": "",
|
||||
"lde.secondsLeft": "",
|
||||
"torpedoExplanation": "Non puoi lanciare più di un Torpedinatore alla volta.",
|
||||
"trackingSubDefExplanation": "I tempi di localizzazione della cimice, la mina e lo sparalinee sono calcolati contro un nemico con 0AP di Arma sec. impermeabile.",
|
||||
"damageSubDefExplanation": "Lo Spruzzatore e il Muro di Colore non sono mostrati nella tabella ma il danno causato da essi può anch'esso essere diminuito da Arma sec. impermeabile",
|
||||
|
||||
@@ -165,6 +165,9 @@
|
||||
"patch": "バージョン:",
|
||||
"abilityPoints.short": "GP",
|
||||
"abilityChunks": "必要なギアパワーのかけら",
|
||||
"lde.inactive": "",
|
||||
"lde.points": "",
|
||||
"lde.secondsLeft": "",
|
||||
"torpedoExplanation": "ゲームの仕様上、1度に1つ以上のトーピードを使用することはできません。",
|
||||
"trackingSubDefExplanation": "ポイントセンサー、トラップ、ラインマーカーのマーキング時間は、サブ性能アップのギアなしで計算しています。",
|
||||
"damageSubDefExplanation": "スプリンクラーとスプラッシュシールドはテーブルに表示されていませんが、これらによるダメージはサブ影響軽減によって減少されます。",
|
||||
|
||||
@@ -165,6 +165,9 @@
|
||||
"patch": "패치:",
|
||||
"abilityPoints.short": "AP",
|
||||
"abilityChunks": "필요 기어 조각",
|
||||
"lde.inactive": "",
|
||||
"lde.points": "",
|
||||
"lde.secondsLeft": "",
|
||||
"torpedoExplanation": "게임 시스템 상 토피도를 한 번에 하나밖에 던지지 못합니다.",
|
||||
"trackingSubDefExplanation": "포인트 센서, 트랩 그리고 라인 마커 추적 시간은 서브 성능 업을 착용하지 않은 상대를 대상으로 계산됐습니다.",
|
||||
"damageSubDefExplanation": "스프링클러와 스플래시 실드는 표에 나타나지 않지만 마찬가지로 서브 영향 감소로 대미지를 줄일 수 있습니다.",
|
||||
|
||||
@@ -169,6 +169,9 @@
|
||||
"patch": "Softwareversie:",
|
||||
"abilityPoints.short": "EP",
|
||||
"abilityChunks": "",
|
||||
"lde.inactive": "",
|
||||
"lde.points": "",
|
||||
"lde.secondsLeft": "",
|
||||
"torpedoExplanation": "",
|
||||
"trackingSubDefExplanation": "Detector, Inktmijn en Ketskogel markeringstijden worden berekend voor een tegenstander met 0 Subtopper EP.",
|
||||
"damageSubDefExplanation": "",
|
||||
|
||||
@@ -173,6 +173,9 @@
|
||||
"patch": "Patch:",
|
||||
"abilityPoints.short": "AP",
|
||||
"abilityChunks": "Ilość potrzebnych chunków",
|
||||
"lde.inactive": "",
|
||||
"lde.points": "",
|
||||
"lde.secondsLeft": "",
|
||||
"torpedoExplanation": "Nie możesz strzelić więcej niż jedno torpedo na raz ze względu na mechanizmy gry.",
|
||||
"trackingSubDefExplanation": "Czas śledzenia przeciwnika bronią Point Sensor, Ink Mine i Angle Shooter z użyciem 0AP efektu Sub Power Up.",
|
||||
"damageSubDefExplanation": "",
|
||||
|
||||
@@ -171,6 +171,9 @@
|
||||
"patch": "Patch (versão):",
|
||||
"abilityPoints.short": "AP",
|
||||
"abilityChunks": "Ability Chunks necessários",
|
||||
"lde.inactive": "",
|
||||
"lde.points": "",
|
||||
"lde.secondsLeft": "",
|
||||
"torpedoExplanation": "Você não pode lançar mais de um torpedo de uma vez devido à mecânicas do jogo.",
|
||||
"trackingSubDefExplanation": "O tempo de rastreamento do Point Sensor, Ink Mine e Angle Shooter são calculados contra um oponente com 0AP de Sub Power Up.",
|
||||
"damageSubDefExplanation": "O Sprinkler e a Splash Wall não são mostrados na tabela mas o dano causado por eles também pode ser diminuído pelo Sub Resistance Up.",
|
||||
|
||||
@@ -173,6 +173,9 @@
|
||||
"patch": "Патч:",
|
||||
"abilityPoints.short": "AP",
|
||||
"abilityChunks": "Требуется чанков свойства",
|
||||
"lde.inactive": "",
|
||||
"lde.points": "",
|
||||
"lde.secondsLeft": "",
|
||||
"torpedoExplanation": "Из-за внутриигровых ограничений невозможно бросить 2 и более торпеды подряд.",
|
||||
"trackingSubDefExplanation": "В расчётах продолжительности маркировки Маркера Движения, Мины и Углострела используется игрок с 0AP Стойкости Запаса.",
|
||||
"damageSubDefExplanation": "Распылятор и Чернильный Занавес не указаны в таблице, но их урон можно снизить Стойкостью Запаса.",
|
||||
|
||||
@@ -166,6 +166,9 @@
|
||||
"patch": "游戏版本:",
|
||||
"abilityPoints.short": "AP",
|
||||
"abilityChunks": "所需碎片",
|
||||
"lde.inactive": "",
|
||||
"lde.points": "",
|
||||
"lde.secondsLeft": "",
|
||||
"torpedoExplanation": "同时只能使用一个鱼雷。",
|
||||
"trackingSubDefExplanation": "定点侦测器、墨汁陷阱和标线器的索敌时间是根据没有携带提升次要武器性能的敌人计算得出。",
|
||||
"damageSubDefExplanation": "洒墨器和斯普拉防护墙未在表内显示,但是它们所造成的伤害也会被减轻次要武器影响的效果降低。",
|
||||
|
||||
Reference in New Issue
Block a user