zenno: tooltips on skills
This commit is contained in:
@@ -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>
|
||||
|
||||
<span class={spanClass}>{s.name}</span>
|
||||
{#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>
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user