Compare commits

...

3 Commits

Author SHA1 Message Date
zephyr 6a30b1d6f0 zenno/doc/frbm: mention gold skills bad for lane combo 2026-05-25 22:02:01 -04:00
zephyr 7b45b8761b zenno: tooltips on skills 2026-05-25 15:36:25 -04:00
zephyr a89ef8fc4c zenno/doc/frbm: lucky 7 is not a gate skill 2026-05-24 18:21:35 -04:00
4 changed files with 262 additions and 9 deletions
+70 -1
View File
@@ -1,5 +1,16 @@
<script lang="ts">
import { skills, ZERO_SKILL, type Skill } from './data/skill';
import {
ABILITY_SCALE_NAME,
ABILITY_TYPE_FORMAT,
DURATION_SCALE_NAME,
skills,
Target,
TARGET_FORMAT,
tenThousandths,
ZERO_SKILL,
type Ability,
type Skill,
} from './data/skill';
interface CommonProps {
hint?: string;
@@ -10,6 +21,10 @@
let { hint, mention, skill, name }: Props = $props();
function splitCond(c: string): string[] {
return c.replaceAll('&', ' & ').split('@');
}
const s: Readonly<Skill> = $derived.by(() => {
const l =
skill != null ? skills.global.filter((s) => s.skill_id === skill) : skills.global.filter((s) => s.name.includes(name!));
@@ -21,8 +36,62 @@
}
return l[0];
});
const activationClass = $derived(s.activations.length === 1 ? null : 'm2 border rounded-md');
const spanClass = $derived(mention ? 'italic' : 'font-bold');
</script>
{#snippet condition(disj: string[])}
{#each disj as conj, i (conj)}
<div class="font-mono text-sm">{conj}</div>
{#if i !== disj.length - 1}
<div class="text-xs italic">or</div>
{/if}
{/each}
{/snippet}
{#snippet abilities(abils: Ability[])}
{#each abils as a (a.type)}
<div class="text-sm">
{ABILITY_TYPE_FORMAT[a.type](a.value)}
{ABILITY_SCALE_NAME[a.value_usage]}
{#if a.target && a.target !== Target.Self}
{TARGET_FORMAT[a.target](a.target_value)}
{/if}
</div>
{/each}
{/snippet}
<div class="group relative inline-block">
<div
class="skill-tip invisible absolute bottom-4 -left-1/2 flex w-80 flex-col rounded-lg border px-2 text-center opacity-0 shadow-lg transition-all group-hover:visible group-hover:bottom-6 group-hover:opacity-100"
role="tooltip"
>
<div class="border-b py-1">
{s.description}
</div>
{#each s.activations as act, i (i)}
<div class={activationClass}>
{#if act.precondition}
<div class="border-b py-1">
<div class="text-xs italic">Precondition</div>
{@render condition(splitCond(act.precondition))}
</div>
{/if}
<div class="border-b py-1">
{@render condition(splitCond(act.condition))}
</div>
<div class="py-1">
{@render abilities(act.abilities)}
{#if act.duration}
<div class="text-sm">
for {tenThousandths(act.duration)}s
{DURATION_SCALE_NAME[act.dur_scale]}
</div>
{/if}
</div>
</div>
{/each}
</div>
<span class={spanClass}>{s.name}</span>
</div>
+175 -4
View File
@@ -57,6 +57,24 @@ export interface Skill {
icon_id: number;
}
export enum DurationScale {
Direct = 1,
FrontDistance = 2,
RemainingHP = 3,
IncrementPass = 4,
MidSideBlock = 5,
RemainingHP2 = 7,
}
export const DURATION_SCALE_NAME: Readonly<Record<DurationScale, string>> = {
[DurationScale.Direct]: '',
[DurationScale.FrontDistance]: 'scaling with distance from the front',
[DurationScale.RemainingHP]: 'scaling with remaining HP',
[DurationScale.IncrementPass]: 'increasing with each pass while active',
[DurationScale.MidSideBlock]: 'scaling with mid-race phase blocked side time',
[DurationScale.RemainingHP2]: 'scaling with remaining HP',
} as const;
/**
* Conditions and results of skill activation.
*/
@@ -77,7 +95,7 @@ export interface Activation {
/**
* Special skill duration scaling mode.
*/
dur_scale: 1 | 2 | 3 | 4 | 5 | 7;
dur_scale: DurationScale;
/**
* Skill cooldown in ten thousandths of a second.
* A value of 5000000 indicates that the cooldown is forever.
@@ -90,6 +108,159 @@ export interface Activation {
abilities: Ability[];
}
export function tenThousandths(v: number): string {
const q = v / 10000;
if (v % 10000 === 0) {
return q.toString();
}
const s = q.toFixed(4);
const j = /\.?0+$/.exec(s);
return s.substring(0, j?.index ?? undefined);
}
function plusBonus(v: number): string {
const s = tenThousandths(v);
return v < 0 ? s : '+' + s;
}
export enum AbilityType {
Speed = 1,
Stamina = 2,
Power = 3,
Guts = 4,
Wit = 5,
GreatEscape = 6,
Vision = 8,
HP = 9,
GateDelay = 10,
Frenzy = 13,
AddGateDelay = 14,
CurrentSpeed = 21,
TargetSpeed = 27,
LaneSpeed = 28,
Accel = 31,
LaneChange = 35,
}
export const ABILITY_TYPE_FORMAT: Readonly<Record<AbilityType, (v: number) => string>> = {
[AbilityType.Speed]: (v) => 'Speed ' + plusBonus(v),
[AbilityType.Stamina]: (v) => 'Stamina ' + plusBonus(v),
[AbilityType.Power]: (v) => 'Power ' + plusBonus(v),
[AbilityType.Guts]: (v) => 'Guts ' + plusBonus(v),
[AbilityType.Wit]: (v) => 'Wit ' + plusBonus(v),
[AbilityType.GreatEscape]: () => 'Enable Great Escape',
[AbilityType.Vision]: (v) => plusBonus(v) + 'm Vision',
[AbilityType.HP]: (v) => (v > 0 ? tenThousandths(v * 100) + '% Recovery' : tenThousandths(v * 100) + '% HP Drain'),
[AbilityType.GateDelay]: (v) => tenThousandths(v) + '× Gate Delay',
[AbilityType.Frenzy]: (v) => tenThousandths(v) + 's Frenzy',
[AbilityType.AddGateDelay]: (v) => plusBonus(v) + 's Gate Delay',
[AbilityType.CurrentSpeed]: (v) => plusBonus(v) + ' m/s Current Speed',
[AbilityType.TargetSpeed]: (v) => plusBonus(v) + ' m/s Target Speed',
[AbilityType.LaneSpeed]: (v) => plusBonus(v) + ' CW/s Lane Change Speed',
[AbilityType.Accel]: (v) => plusBonus(v) + ' m/s² Acceleration',
[AbilityType.LaneChange]: (v) => 'Target Lane = ' + tenThousandths(v) + ' CW',
} as const;
export enum AbilityValueUsage {
Direct = 1,
SkillCount = 2,
TeamSpeed = 3,
TeamStamina = 4,
TeamPower = 5,
TeamGuts = 6,
TeamWit = 7,
Random1 = 8,
Random2 = 9,
Climax = 10,
MaxStat = 13,
GreenCount = 14,
DistAdd = 19,
MidRaceSideBlock = 20,
Speed1 = 22,
Speed2 = 23,
ArcPotential = 24,
MaxLead = 25,
}
export const ABILITY_SCALE_NAME: Readonly<Record<AbilityValueUsage, string>> = {
[AbilityValueUsage.Direct]: '',
[AbilityValueUsage.SkillCount]: 'scaling with the number of skills',
[AbilityValueUsage.TeamSpeed]: 'scaling with team Speed',
[AbilityValueUsage.TeamStamina]: 'scaling with team Stamina',
[AbilityValueUsage.TeamPower]: 'scaling with team Power',
[AbilityValueUsage.TeamGuts]: 'scaling with team Guts',
[AbilityValueUsage.TeamWit]: 'scaling with team Wit',
[AbilityValueUsage.Random1]: 'with a random 0× to 0.04× multiplier',
[AbilityValueUsage.Random2]: 'with a random 0× to 0.04× mulitplier',
[AbilityValueUsage.Climax]: 'scaling with the number of races won in training',
[AbilityValueUsage.MaxStat]: 'scaling with the highest raw stat',
[AbilityValueUsage.GreenCount]: 'scaling with the number of Passive skills activated',
[AbilityValueUsage.DistAdd]: 'plus extra when far from the lead',
[AbilityValueUsage.MidRaceSideBlock]: 'scaling with mid-race phase blocked side time',
[AbilityValueUsage.Speed1]: 'scaling with overall speed',
[AbilityValueUsage.Speed2]: 'scaling with overall speed',
[AbilityValueUsage.ArcPotential]: "scaling with L'Arc global potential",
[AbilityValueUsage.MaxLead]: 'scaling with the longest lead obtained in the first ⅔',
};
export enum Target {
Self = 1,
Sympathizers = 2,
InView = 4,
Frontmost = 7,
Ahead = 9,
Behind = 10,
AllTeammates = 11,
Style = 18,
RushingAhead = 19,
RushingBehind = 20,
RushingStyle = 21,
Character = 22,
Triggering = 23,
}
function targetn(v: number, n: string, one?: string): string {
switch (v) {
case 1:
return one != null ? one : `to ${n}`;
case 18:
return `to all ${n}`;
default:
return `to ${v} others ${n}`;
}
}
function stylename(v: number, infix: string): string {
switch (v) {
case 1:
return `to ${infix} Front Runners`;
case 2:
return `to ${infix} Pace Chasers`;
case 3:
return `to ${infix} Late Surgers`;
case 4:
return `to ${infix} End Closers`;
default:
return `to all running unknown style ${v}`;
}
}
export const TARGET_FORMAT: Readonly<Record<Target, (v?: number) => string>> = {
[Target.Self]: () => '',
[Target.Sympathizers]: () => 'to others with Sympathy',
[Target.InView]: (v) => targetn(v!, 'in view'),
[Target.Frontmost]: (v) => targetn(v!, 'at the front', 'the frontmost'),
[Target.Ahead]: (v) => targetn(v!, 'ahead'),
[Target.Behind]: (v) => targetn(v!, 'behind'),
[Target.AllTeammates]: () => 'to all teammates',
[Target.Style]: (v) => stylename(v!, 'all'),
[Target.RushingAhead]: (v) => targetn(v!, 'rushing ahead'),
[Target.RushingBehind]: (v) => targetn(v!, 'rushing behind'),
[Target.RushingStyle]: (v) => stylename(v!, 'all rushing'),
[Target.Character]: (v) => `to character ${v}`,
[Target.Triggering]: () => 'to whosoever triggered this skill',
} as const;
/**
* Effects applied when a skill activates.
*/
@@ -97,11 +268,11 @@ export interface Ability {
/**
* Race mechanic affected by the ability.
*/
type: 1 | 2 | 3 | 4 | 5 | 6 | 8 | 9 | 10 | 13 | 21 | 27 | 28 | 31 | 35;
type: AbilityType;
/**
* Special scaling type of the skill value.
*/
value_usage: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 19 | 20 | 22 | 23 | 24 | 25;
value_usage: AbilityValueUsage;
/**
* Amount that the skill modifies the race mechanic in ten thousandths of
* whatever is the appropriate unit.
@@ -110,7 +281,7 @@ export interface Ability {
/**
* Selector for horses targeted by the ability.
*/
target: 1 | 2 | 4 | 7 | 9 | 10 | 11 | 18 | 19 | 20 | 21 | 22 | 23;
target: Target;
/**
* Argument value for the ability target, when appropriate.
*/
+14 -2
View File
@@ -305,8 +305,11 @@
Gate skills are <Skill skill={201601} hint="gw" /> (GW), <Skill skill={200531} hint="ttl" /> (TTL), and <Skill
skill={200431}
hint="conc"
/> (Conc), as well as all green skills including <Skill skill={202051} hint="runaway" mention />. These skills activate the
moment the race starts.
/> (Conc), as well as all green skills including <Skill skill={202051} hint="runaway" mention /> (but excluding <Skill
skill={201562}
hint="lucky 7"
mention
/>). These skills activate the moment the race starts.
</p>
<p>
GW is an absolutely mandatory skill for all front runners. Even runaway blockers should have it, otherwise they will be passed
@@ -403,6 +406,15 @@
sufficiently confident in my Japanese to try to guide readers through it.
<!-- TODO(zeph): i could totally annotate a picture though -->
</p>
<p>
The gold versions of lane combo skills, <Skill skill={201261} hint="gold dd" mention />, <Skill
skill={200451}
hint="gold pp"
mention
/>, <Skill skill={210051} hint="burning wit" mention />, are excellent to take on parents, but they make lane combo itself
less effective. They have stronger lane change movement boosts, which does not affect the forward speed boost and is likely to
make it last a shorter time, since the horse will return to the rail more quickly.
</p>
<Sec h="2" id="slopes">Slopes</Sec>
<p>
+2 -1
View File
@@ -15,7 +15,8 @@ html {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
}
body {
body,
.skill-tip {
background-color: light-dark(var(--color-mist-200), var(--color-mist-800));
color: light-dark(var(--color-amber-950), var(--color-amber-50));
}