Compare commits
10 Commits
e139eae06d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 886dccb6b8 | |||
| 57e8a06383 | |||
| ab14f58079 | |||
| 4106215180 | |||
| cdea376f94 | |||
| d157dfc9b6 | |||
| 773625b842 | |||
| 22ca5c98f3 | |||
| 08deedea8f | |||
| 86b769d7ed |
@@ -3,11 +3,14 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
@@ -26,20 +29,23 @@ import (
|
||||
|
||||
func main() {
|
||||
var (
|
||||
dataDir string
|
||||
// public site
|
||||
addr string
|
||||
dataDir string
|
||||
public string
|
||||
// discord
|
||||
tokenFile string
|
||||
// http api options
|
||||
addr string
|
||||
route string
|
||||
pubkey string
|
||||
// logging options
|
||||
apiRoute string
|
||||
pubkey string
|
||||
// logging
|
||||
level slog.Level
|
||||
textfmt string
|
||||
)
|
||||
flag.StringVar(&addr, "http", ":80", "`address` to bind HTTP server")
|
||||
flag.StringVar(&dataDir, "data", "", "`dir`ectory containing exported json data")
|
||||
flag.StringVar(&public, "public", "", "`dir`ectory containing the website to serve")
|
||||
flag.StringVar(&tokenFile, "token", "", "`file` containing the Discord bot token")
|
||||
flag.StringVar(&addr, "http", "", "`address` to bind HTTP API server")
|
||||
flag.StringVar(&route, "route", "/interactions/callback", "`path` to serve HTTP API calls")
|
||||
flag.StringVar(&apiRoute, "route", "/interactions/callback", "`path` to serve Discord HTTP API calls")
|
||||
flag.StringVar(&pubkey, "key", "", "Discord public key")
|
||||
flag.TextVar(&level, "log", slog.LevelInfo, "slog logging `level`")
|
||||
flag.StringVar(&textfmt, "log-format", "text", "slog logging `format`, text or json")
|
||||
@@ -57,6 +63,16 @@ func main() {
|
||||
}
|
||||
slog.SetDefault(slog.New(lh))
|
||||
|
||||
stat, err := os.Stat(public)
|
||||
if err != nil {
|
||||
slog.Error("public", slog.Any("err", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
if !stat.IsDir() {
|
||||
slog.Error("public", slog.String("err", "not a directory"))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
skills, err := loadSkills(filepath.Join(dataDir, "skill.json"))
|
||||
slog.Info("loaded skills", slog.Int("count", len(skills)))
|
||||
groups, err2 := loadSkillGroups(filepath.Join(dataDir, "skill-group.json"))
|
||||
@@ -87,38 +103,50 @@ func main() {
|
||||
r.SelectMenuComponent("/swap", skillSrv.menu)
|
||||
r.ButtonComponent("/swap/{id}", skillSrv.button)
|
||||
})
|
||||
|
||||
opts := []bot.ConfigOpt{bot.WithDefaultGateway(), bot.WithEventListeners(r)}
|
||||
if addr != "" {
|
||||
if pubkey == "" {
|
||||
slog.Error("Discord public key must be provided when using HTTP API")
|
||||
os.Exit(1)
|
||||
}
|
||||
opts = append(opts, bot.WithHTTPServerConfigOpts(pubkey,
|
||||
httpserver.WithAddress(addr),
|
||||
httpserver.WithURL(route),
|
||||
))
|
||||
}
|
||||
|
||||
slog.Info("connect", slog.String("disgo", disgo.Version))
|
||||
client, err := disgo.New(string(token), opts...)
|
||||
client, err := disgo.New(string(token), bot.WithDefaultGateway(), bot.WithEventListeners(r))
|
||||
if err != nil {
|
||||
slog.Error("building bot", slog.Any("err", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("GET /", http.FileServerFS(os.DirFS(public)))
|
||||
if pubkey != "" {
|
||||
pk, err := hex.DecodeString(pubkey)
|
||||
if err != nil {
|
||||
slog.Error("pubkey", slog.Any("err", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
mux.Handle("POST "+apiRoute, httpserver.HandleInteraction(httpserver.DefaultVerifier{}, pk, slog.Default(), client.EventManager.HandleHTTPEvent))
|
||||
slog.Info("Discord HTTP API enabled", slog.String("pubkey", pubkey))
|
||||
}
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
slog.Error("listen", slog.String("addr", addr), slog.Any("err", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
srv := http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
BaseContext: func(l net.Listener) context.Context { return ctx },
|
||||
}
|
||||
go func() {
|
||||
slog.Info("HTTP", slog.Any("addr", l.Addr()))
|
||||
err := srv.Serve(l)
|
||||
if err == http.ErrServerClosed {
|
||||
return
|
||||
}
|
||||
slog.Error("HTTP server closed", slog.Any("err", err))
|
||||
}()
|
||||
|
||||
if err := handler.SyncCommands(client, commands, nil, rest.WithCtx(ctx)); err != nil {
|
||||
slog.Error("syncing commands", slog.Any("err", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if addr != "" {
|
||||
slog.Info("start HTTP server", slog.String("address", addr), slog.String("route", route))
|
||||
if err := client.OpenHTTPServer(); err != nil {
|
||||
slog.Error("starting HTTP server", slog.Any("err", err))
|
||||
stop()
|
||||
}
|
||||
}
|
||||
slog.Info("start gateway")
|
||||
if err := client.OpenGateway(ctx); err != nil {
|
||||
slog.Error("starting gateway", slog.Any("err", err))
|
||||
@@ -131,6 +159,10 @@ func main() {
|
||||
ctx, stop = context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer stop()
|
||||
client.Close(ctx)
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
slog.Error("HTTP API shutdown", slog.Any("err", err))
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var commands = []discord.ApplicationCommandCreate{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 130,
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
@@ -22,8 +22,8 @@ export default defineConfig(
|
||||
rules: {
|
||||
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
||||
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||
'no-undef': 'off'
|
||||
}
|
||||
'no-undef': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
|
||||
@@ -32,8 +32,8 @@ export default defineConfig(
|
||||
projectService: true,
|
||||
extraFileExtensions: ['.svelte'],
|
||||
parser: ts.parser,
|
||||
svelteConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
svelteConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
11
zenno/package-lock.json
generated
11
zenno/package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"@eslint/compat": "^2.0.3",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.50.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
@@ -1134,6 +1135,16 @@
|
||||
"@sveltejs/kit": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/adapter-static": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz",
|
||||
"integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@sveltejs/kit": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.55.0.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@eslint/compat": "^2.0.3",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@sveltejs/adapter-auto": "^7.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.50.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { character } from '$lib/data/character'
|
||||
import { character } from '$lib/data/character';
|
||||
import type { ClassValue } from 'svelte/elements';
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
value: number
|
||||
label?: string
|
||||
region?: keyof typeof character
|
||||
required?: boolean
|
||||
}
|
||||
interface Props {
|
||||
id: string;
|
||||
value: number;
|
||||
label?: string;
|
||||
class?: ClassValue | null;
|
||||
optionClass?: ClassValue | null;
|
||||
labelClass?: ClassValue | null;
|
||||
region?: keyof typeof character;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
let { id, value = $bindable(), label, region = 'global', required = false }: Props = $props()
|
||||
let {
|
||||
id,
|
||||
value = $bindable(),
|
||||
label,
|
||||
class: className,
|
||||
optionClass,
|
||||
labelClass,
|
||||
region = 'global',
|
||||
required = false,
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if label}
|
||||
<label for={id}>{label}</label>
|
||||
<label for={id} class={labelClass}>{label}</label>
|
||||
{/if}
|
||||
<select id={id} bind:value={value} required={required}>
|
||||
{#if !required}
|
||||
<option value=0></option>
|
||||
{/if}
|
||||
{#each character[region] as c}
|
||||
<option value={c.chara_id}>{c.name}</option>
|
||||
{/each}
|
||||
<select {id} class={className} bind:value {required}>
|
||||
{#if !required}
|
||||
<option value="0" class={optionClass}></option>
|
||||
{/if}
|
||||
{#each character[region] as c}
|
||||
<option value={c.chara_id} class={optionClass}>{c.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
BIN
zenno/src/lib/assets/favicon.png
Executable file
BIN
zenno/src/lib/assets/favicon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,5 +1,5 @@
|
||||
import type { RegionalName } from '$lib/regional-name'
|
||||
import globalJSON from '../../../../global/character.json'
|
||||
import type { RegionalName } from '$lib/regional-name';
|
||||
import globalJSON from '../../../../global/character.json';
|
||||
|
||||
/**
|
||||
* Character definitions.
|
||||
@@ -8,16 +8,19 @@ export interface Character {
|
||||
/**
|
||||
* Character ID.
|
||||
*/
|
||||
chara_id: number
|
||||
chara_id: number;
|
||||
/**
|
||||
* Regional name of the character.
|
||||
* E.g., Special Week for Global, or スペシャルウィーク for JP.
|
||||
*/
|
||||
name: string
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const character = {
|
||||
global: globalJSON as Character[],
|
||||
}
|
||||
global: globalJSON as Character[],
|
||||
};
|
||||
|
||||
export const charaNames = globalJSON.reduce((m, c) => m.set(c.chara_id, {en: c.name}), new Map<Character['chara_id'], RegionalName>());
|
||||
export const charaNames = globalJSON.reduce(
|
||||
(m, c) => m.set(c.chara_id, { en: c.name }),
|
||||
new Map<Character['chara_id'], RegionalName>(),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import globalJSON from '../../../../global/conversation.json'
|
||||
import type { RegionalName } from '$lib/regional-name';
|
||||
import globalJSON from '../../../../global/conversation.json';
|
||||
|
||||
/**
|
||||
* Lobby conversation data.
|
||||
@@ -41,8 +42,44 @@ export interface Conversation {
|
||||
|
||||
export const conversation = {
|
||||
global: globalJSON as Conversation[],
|
||||
}
|
||||
};
|
||||
|
||||
export const byChara = {
|
||||
global: globalJSON.reduce((m, c) => m.set(c.chara_id, (m.get(c.chara_id) ?? []).concat(c as Conversation)), new Map<Conversation['chara_id'], Conversation[]>()),
|
||||
global: globalJSON.reduce(
|
||||
(m, c) => m.set(c.chara_id, (m.get(c.chara_id) ?? []).concat(c as Conversation)),
|
||||
new Map<Conversation['chara_id'], Conversation[]>(),
|
||||
),
|
||||
};
|
||||
|
||||
export const locations: Record<Conversation['location'], { name: RegionalName; group: 1 | 2 | 3 | 4 | 5 }> = {
|
||||
110: { name: { en: 'right side front' }, group: 1 },
|
||||
120: { name: { en: 'right side front' }, group: 1 },
|
||||
130: { name: { en: 'right side front' }, group: 1 },
|
||||
210: { name: { en: 'left side table' }, group: 2 },
|
||||
220: { name: { en: 'left side table' }, group: 2 },
|
||||
310: { name: { en: 'center back seat' }, group: 3 },
|
||||
410: { name: { en: 'center posters' }, group: 4 },
|
||||
420: { name: { en: 'center posters' }, group: 4 },
|
||||
430: { name: { en: 'center posters' }, group: 4 },
|
||||
510: { name: { en: 'left side school map' }, group: 5 },
|
||||
520: { name: { en: 'left side school map' }, group: 5 },
|
||||
530: { name: { en: 'left side school map' }, group: 5 },
|
||||
};
|
||||
|
||||
function locCharas(convos: Conversation[], locGroup: 1 | 2 | 3 | 4 | 5) {
|
||||
const m = convos
|
||||
.filter((c) => locations[c.location].group === locGroup)
|
||||
.flatMap((c) => [c.chara_1, c.chara_2, c.chara_3].filter((x) => x != null))
|
||||
.reduce((m, id) => m.set(id, 1 + (m.get(id) ?? 0)), new Map<number, number>());
|
||||
return [...m].toSorted((a, b) => b[1] - a[1]); // descending
|
||||
}
|
||||
|
||||
export const groupPopulars = {
|
||||
global: {
|
||||
1: locCharas(conversation.global, 1),
|
||||
2: locCharas(conversation.global, 2),
|
||||
3: locCharas(conversation.global, 3),
|
||||
4: locCharas(conversation.global, 4),
|
||||
5: locCharas(conversation.global, 5),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
* Currently English is the only supported language.
|
||||
*/
|
||||
export interface RegionalName {
|
||||
en: string
|
||||
en: string;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,7 @@ describe('Welcome.svelte', () => {
|
||||
it('renders greetings for host and guest', async () => {
|
||||
render(Welcome, { host: 'SvelteKit', guest: 'Vitest' });
|
||||
|
||||
await expect
|
||||
.element(page.getByRole('heading', { level: 1 }))
|
||||
.toHaveTextContent('Hello, SvelteKit!');
|
||||
await expect.element(page.getByRole('heading', { level: 1 })).toHaveTextContent('Hello, SvelteKit!');
|
||||
await expect.element(page.getByText('Hello, Vitest!')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
<script lang="ts">
|
||||
import './layout.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import favicon from '$lib/assets/favicon.png';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||
<svelte:head>
|
||||
<title>Zenno Rob Roy</title>
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
<nav class="flex min-w-full p-4 mb-4 shadow-md">
|
||||
<span class="hidden md:inline flex-1">
|
||||
<a href="/" class="text-7xl">Zenno Rob Roy</a>
|
||||
</span>
|
||||
<span class="flex-1 text-center">
|
||||
<a href="/" class="md:hidden block mx-8 my-1 font-semibold">Zenno Rob Roy</a>
|
||||
<a href="/inherit" class="inline-block mx-8 my-1">Inheritance Chance</a>
|
||||
<a href="/spark" class="inline-block mx-8 my-1">Spark Chance</a>
|
||||
<a href="/vet" class="inline-block mx-8 my-1">My Veterans</a>
|
||||
<a href="/convo" class="inline-block mx-8 my-1">Lobby Conversations</a>
|
||||
</span>
|
||||
</nav>
|
||||
<div class="md:min-w-7xl md:max-w-7xl md:m-auto">
|
||||
{@render children()}
|
||||
<div class="flex h-screen flex-col">
|
||||
<nav class="mb-4 flex min-w-full bg-mist-300 p-4 shadow-md dark:bg-mist-900">
|
||||
<span class="hidden flex-1 md:inline">
|
||||
<a href="/" class="text-4xl">Zenno Rob Roy</a>
|
||||
</span>
|
||||
<span class="flex-1 text-center">
|
||||
<a href="/" class="mx-8 my-1 block font-semibold md:hidden">Zenno Rob Roy</a>
|
||||
<a href="/inherit" class="mx-8 my-1 inline-block">Inheritance Chance</a>
|
||||
<a href="/spark" class="mx-8 my-1 inline-block">Spark Chance</a>
|
||||
<a href="/vet" class="mx-8 my-1 inline-block">My Veterans</a>
|
||||
<a href="/convo" class="mx-8 my-1 inline-block">Lobby Conversations</a>
|
||||
</span>
|
||||
</nav>
|
||||
<div class="mx-4 grow lg:m-auto lg:max-w-7xl lg:min-w-7xl">
|
||||
{@render children()}
|
||||
</div>
|
||||
<footer class="inset-x-0 bottom-0 mt-8 border-t bg-mist-300 p-4 text-center text-sm md:mt-20 dark:border-none dark:bg-mist-900">
|
||||
Umamusume: Pretty Derby tools by <a href="https://zephyrtronium.date/" target="_blank" rel="noopener noreferrer">zephyrtronium</a>.<br />
|
||||
All game data is auto-generated from the <a href="https://git.sunturtle.xyz/zephyr/horse/src/branch/main/doc/README.md" target="_blank" rel="noopener noreferrer">game's local database</a>.
|
||||
</footer>
|
||||
</div>
|
||||
<footer class="p-4 mt-32 inset-x-0 bottom-0 border-t text-center text-[14px]">
|
||||
Umamusume: Pretty Derby tools by <a href="https://zephyrtronium.date/">zephyrtronium</a>.<br>
|
||||
All data is generated from the game's local database.
|
||||
</footer>
|
||||
|
||||
2
zenno/src/routes/+layout.ts
Normal file
2
zenno/src/routes/+layout.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const prerender = true;
|
||||
export const trailingSlash = 'always';
|
||||
@@ -1,11 +1,34 @@
|
||||
<script lang="ts">
|
||||
import CharaPick from "$lib/CharaPick.svelte";
|
||||
|
||||
let selChara = $state(0);
|
||||
</script>
|
||||
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<CharaPick id="test-chara" bind:value={selChara} />
|
||||
<p>selected character is id {selChara}</p>
|
||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
||||
<p>Lorem ipsum (/ ˌ l ɔː. r ə m ˈ ɪ p. s ə m/ LOR-əm IP-səm) is a dummy or placeholder text commonly used in graphic design, publishing, and web development. It is typically a corrupted version of De finibus bonorum et malorum, a 1st-century BC text by the Roman statesman and philosopher Cicero, with words altered, added, and removed to make it nonsensical and improper Latin. The first two words are the truncation of dolorem ipsum ("pain itself"). Lorem ipsum's purpose is to permit a page layout to be designed, independently of the copy that will subsequently populate it, or to demonstrate various fonts of a typeface without meaningful text that could be distracting. Versions of the Lorem ipsum text have been used in typesetting since the 1960s, when advertisements for Letraset transfer sheets popularized it. Lorem ipsum was introduced to the digital world in the mid-1980s, when Aldus employed it in graphic and word-processing templates for its desktop publishing program PageMaker. Other popular word processors, including Pages and Microsoft Word, have since adopted Lorem ipsum, as have many LaTeX packages, web content</p>
|
||||
<h1 class="m-8 text-center text-7xl">Zenno Rob Roy</h1>
|
||||
<p>She's read all about Umamusume, and she's always happy to share her knowledge and give recommendations!</p>
|
||||
<h2 class="mt-8 mb-4 text-4xl">Tools</h2>
|
||||
<ul class="list-disc pl-4">
|
||||
<li>
|
||||
<a href="/inherit">Inheritance Chance</a> — <i>Not yet implemented</i> — Given a legacy, calculate the probability distribution
|
||||
of activation counts for each spark.
|
||||
</li>
|
||||
<li>
|
||||
<a href="/spark">Spark Chance</a> — <i>Not yet implemented</i> — Given a legacy, calculate the chance of generating each spark if
|
||||
you fulfill the conditions to do so, and the distribution of total spark counts.
|
||||
</li>
|
||||
<li>
|
||||
<a href="/vet">My Veterans</a> — <i>Not yet implemented</i> — Set up and track your veterans for Zenno Rob Roy's inspiration and
|
||||
spark calculators.
|
||||
</li>
|
||||
<li>
|
||||
<a href="/convo">Lobby Conversations</a> — Check participants in lobby conversations and get recommendations on unlocking them quickly.
|
||||
</li>
|
||||
</ul>
|
||||
<h2 class="mt-8 mb-4 text-4xl">About</h2>
|
||||
<p>Tools to fill some gaps I've felt in Umamusume optimization.</p>
|
||||
<p>This site is very under construction. To demonstrate just how under construction it is, here is lorem ipsum:</p>
|
||||
<p>
|
||||
Lorem ipsum (/ ˌ l ɔː. r ə m ˈ ɪ p. s ə m/ LOR-əm IP-səm) is a dummy or placeholder text commonly used in graphic design,
|
||||
publishing, and web development. It is typically a corrupted version of De finibus bonorum et malorum, a 1st-century BC text by
|
||||
the Roman statesman and philosopher Cicero, with words altered, added, and removed to make it nonsensical and improper Latin.
|
||||
The first two words are the truncation of dolorem ipsum ("pain itself"). Lorem ipsum's purpose is to permit a page layout to be
|
||||
designed, independently of the copy that will subsequently populate it, or to demonstrate various fonts of a typeface without
|
||||
meaningful text that could be distracting. Versions of the Lorem ipsum text have been used in typesetting since the 1960s, when
|
||||
advertisements for Letraset transfer sheets popularized it. Lorem ipsum was introduced to the digital world in the mid-1980s,
|
||||
when Aldus employed it in graphic and word-processing templates for its desktop publishing program PageMaker. Other popular word
|
||||
processors, including Pages and Microsoft Word, have since adopted Lorem ipsum, as have many LaTeX packages, web content
|
||||
</p>
|
||||
|
||||
@@ -1,11 +1,69 @@
|
||||
<script lang="ts">
|
||||
import CharaPick from "$lib/CharaPick.svelte";
|
||||
import { charaNames } from '$lib/data/character';
|
||||
import { byChara, locations, groupPopulars } from '$lib/data/convo';
|
||||
import CharaPick from '$lib/CharaPick.svelte';
|
||||
|
||||
let charaID = $state(1001)
|
||||
let convo = $state(1)
|
||||
const minSuggest = 8;
|
||||
|
||||
let charaID = $state(1001);
|
||||
let convo = $state(1);
|
||||
|
||||
let options = $derived(byChara.global.get(charaID) ?? []);
|
||||
let cur = $derived(options.find((c) => c.number === convo));
|
||||
let suggested = $derived.by(() => {
|
||||
if (cur == null) {
|
||||
return [];
|
||||
}
|
||||
const u = groupPopulars.global[locations[cur.location].group].filter(
|
||||
(s) => charaNames.get(s[0]) != null && s[0] !== cur.chara_1 && s[0] !== cur.chara_2 && s[0] !== cur.chara_3,
|
||||
);
|
||||
const r = u.length <= minSuggest ? u : u.filter((s) => s[1] >= u[minSuggest][1]);
|
||||
return r.map(([chara_id, count]) => ({ chara_id, count }));
|
||||
});
|
||||
</script>
|
||||
|
||||
<h1>Lobby Conversations</h1>
|
||||
<p>Find which horses are in a given lobby conversation, and get recommendations on which ones to assign to fixed locations to maximize the chance of getting it.</p>
|
||||
<hr>
|
||||
<CharaPick id="chara" label="Select a character" bind:value={charaID} required />
|
||||
<h1 class="text-4xl">Lobby Conversations</h1>
|
||||
<div class="mx-auto mt-8 flex flex-col rounded-md text-center shadow-md ring md:max-w-xl md:flex-row">
|
||||
<div class="m-4 flex-1 md:mt-3">
|
||||
<CharaPick id="chara" class="w-full" label="Character" labelClass="hidden md:inline" bind:value={charaID} required />
|
||||
</div>
|
||||
<div class="m-4 flex-1 md:mt-3">
|
||||
<label for="convo" class="hidden md:inline">Conversation</label>
|
||||
<select id="convo" bind:value={convo} class="w-full">
|
||||
{#each options as opt}
|
||||
<option value={opt.number}>Slice of Life {opt.number}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{#if cur}
|
||||
<div class="shadow-sm transition-shadow hover:shadow-md">
|
||||
<div class="mt-8 flex text-center text-lg">
|
||||
<span class="flex-1"
|
||||
>{charaNames.get(cur.chara_1)?.en ?? 'someone not a trainee'}{(cur.chara_2 ?? cur.chara_3) == null ? ' alone' : ''}</span
|
||||
>
|
||||
{#if cur.chara_2}
|
||||
<span class="flex-1">{charaNames.get(cur.chara_2)?.en ?? 'someone not a trainee'}</span>
|
||||
{/if}
|
||||
{#if cur.chara_3}
|
||||
<span class="flex-1">{charaNames.get(cur.chara_3)?.en ?? 'someone not a trainee'}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex w-full text-center text-lg">
|
||||
<span class="flex-1">at {locations[cur.location].name.en}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 block text-center">
|
||||
<span>Other characters who appear here most often:</span>
|
||||
</div>
|
||||
<div class="mt-4 grid text-center shadow-sm transition-shadow ease-in hover:shadow-md hover:ease-out md:grid-cols-4">
|
||||
{#each suggested as s}
|
||||
<span>{charaNames.get(s.chara_id)?.en}: {s.count}×</span>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="mt-4 block text-center">
|
||||
<span>
|
||||
Set these characters to fixed positions (main, upgrades, story, races) to maximize the chance of getting this conversation.
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,42 @@
|
||||
@import 'tailwindcss';
|
||||
@import './sakura-vars.css';
|
||||
|
||||
@theme {
|
||||
--text-sm: 1.25rem;
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: light-dark(var(--color-mist-200), var(--color-mist-800));
|
||||
color: light-dark(var(--color-amber-950), var(--color-amber-50));
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: calc(var(--spacing) * 4);
|
||||
}
|
||||
|
||||
a {
|
||||
color: light-dark(var(--color-sky-900), var(--color-sky-100));
|
||||
}
|
||||
|
||||
nav > span > a {
|
||||
color: light-dark(var(--color-amber-950), var(--color-amber-50));
|
||||
}
|
||||
|
||||
a:hover {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
select {
|
||||
background-color: light-dark(var(--color-mist-300), var(--color-mist-900));
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
@@ -1,401 +0,0 @@
|
||||
/* Sakura.css v1.5.0
|
||||
* ================
|
||||
* Minimal css theme.
|
||||
* Project: https://github.com/oxalorg/sakura/
|
||||
*/
|
||||
|
||||
/* data-theme="taiyō" */
|
||||
:root {
|
||||
--blossom: #292722;
|
||||
--fade: #7d7768;
|
||||
--bg: #ffecec;
|
||||
--bg-alt: #ffecec;
|
||||
--text: #292222;
|
||||
}
|
||||
|
||||
[data-theme="iron goddess"] {
|
||||
--blossom: #424b51;
|
||||
--fade: #64707a;
|
||||
--bg: #fff2e2;
|
||||
--bg-alt: #fffce2;
|
||||
--text: #2c2923;
|
||||
}
|
||||
|
||||
[data-theme="main sequence"] {
|
||||
--blossom: #3a5425;
|
||||
--fade: #698650;
|
||||
--bg: #fffde5;
|
||||
--bg-alt: #fff4e5;
|
||||
--text: #5e592a;
|
||||
}
|
||||
|
||||
[data-theme="sorcery"] {
|
||||
--blossom: #5a5a69;
|
||||
--fade: #868698;
|
||||
--bg: #e5f4e5;
|
||||
--bg-alt: #e6f4e6;
|
||||
--text: #323932;
|
||||
}
|
||||
|
||||
[data-theme="cirrus"] {
|
||||
--blossom: #565a4b;
|
||||
--fade: #9da587;
|
||||
--bg: #e5f6fa;
|
||||
--bg-alt: #e5f6fa;
|
||||
--text: #31393b;
|
||||
}
|
||||
|
||||
[data-theme="oxygen"] {
|
||||
--blossom: #162011;
|
||||
--fade: #343932;
|
||||
--bg: #e1e2e4;
|
||||
--bg-alt: #e3e0e3;
|
||||
--text: #27282c;
|
||||
}
|
||||
|
||||
[data-theme="dauphin"] {
|
||||
--blossom: #171e1c;
|
||||
--fade: #485b58;
|
||||
--bg: #ebe5f8;
|
||||
--bg-alt: #ebe5f8;
|
||||
--text: #1c1a20;
|
||||
}
|
||||
|
||||
[data-theme="diamond-burned"] {
|
||||
--blossom: #0f0d0b;
|
||||
--fade: #4d4743;
|
||||
--bg: #f8ebf2;
|
||||
--bg-alt: #ebe8f4;
|
||||
--text: #3e363a;
|
||||
}
|
||||
|
||||
[data-theme="chi"] {
|
||||
--blossom: #908975;
|
||||
--fade: #fff8e5;
|
||||
--bg: #110c0c;
|
||||
--bg-alt: #0a090c;
|
||||
--text: #cfa9a9;
|
||||
}
|
||||
|
||||
[data-theme="darjeeling"] {
|
||||
--blossom: #ba949c;
|
||||
--fade: #f8e1e6;
|
||||
--bg: #1c160d;
|
||||
--bg-alt: #1c160d;
|
||||
--text: #c9b9a0;
|
||||
}
|
||||
|
||||
[data-theme="subgiant"] {
|
||||
--blossom: #9fad8a;
|
||||
--fade: #e8f2d7;
|
||||
--bg: #16130b;
|
||||
--bg-alt: #16130b;
|
||||
--text: #bbb396;
|
||||
}
|
||||
|
||||
[data-theme="goblin"] {
|
||||
--blossom: #7a808e;
|
||||
--fade: #dae1ef;
|
||||
--bg: #070905;
|
||||
--bg-alt: #0a0906;
|
||||
--text: #acbd9f;
|
||||
}
|
||||
|
||||
[data-theme="altostratus"] {
|
||||
--blossom: #a8a0b7;
|
||||
--fade: #e5dbf7;
|
||||
--bg: #0c0f0f;
|
||||
--bg-alt: #1a1614;
|
||||
--text: #8da4a4;
|
||||
}
|
||||
|
||||
[data-theme="silicon"] {
|
||||
--blossom: #717f63;
|
||||
--fade: #c4d4b3;
|
||||
--bg: #050a0f;
|
||||
--bg-alt: #050a0f;
|
||||
--text: #838e9a;
|
||||
}
|
||||
|
||||
[data-theme="imperator"] {
|
||||
--blossom: #93a0a3;
|
||||
--fade: #f3fbfd;
|
||||
--bg: #0e0c12;
|
||||
--bg-alt: #0e0c12;
|
||||
--text: #a8a1b1;
|
||||
}
|
||||
|
||||
[data-theme="mædi"] {
|
||||
--blossom: #ccd3b6;
|
||||
--fade: #fdfbf3;
|
||||
--bg: #10090f;
|
||||
--bg-alt: #2f282e;
|
||||
--text: #9e889a;
|
||||
}
|
||||
|
||||
/* Body */
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 1.8rem;
|
||||
line-height: 1.618;
|
||||
/* max-width: 38em; */
|
||||
margin: auto;
|
||||
color: var(--text);
|
||||
background-color: var(--bg);
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 684px) {
|
||||
body {
|
||||
font-size: 1.53rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 382px) {
|
||||
body {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: 1.1;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
font-weight: 700;
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 1.5rem;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
-ms-word-break: break-all;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.35em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.75em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
small,
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: var(--blossom);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--blossom);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--fade);
|
||||
border-bottom: 2px solid var(--text);
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 1.4em;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
padding-left: 1em;
|
||||
padding-top: 0.8em;
|
||||
padding-bottom: 0.8em;
|
||||
padding-right: 0.8em;
|
||||
border-left: 5px solid var(--blossom);
|
||||
margin-bottom: 2.5rem;
|
||||
background-color: var(--bg-alt);
|
||||
}
|
||||
|
||||
blockquote p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
/* Pre and Code */
|
||||
pre {
|
||||
background-color: var(--bg-alt);
|
||||
display: block;
|
||||
padding: 1em;
|
||||
overflow-x: auto;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 2.5rem;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-size: 0.9em;
|
||||
padding: 0 0.5em;
|
||||
background-color: var(--bg-alt);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
pre>code {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
white-space: pre;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
text-align: justify;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
padding: 0.5em;
|
||||
border-bottom: 1px solid var(--bg-alt);
|
||||
}
|
||||
|
||||
/* Buttons, forms and input */
|
||||
input,
|
||||
textarea {
|
||||
border: 1px solid var(--text);
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus {
|
||||
border: 1px solid var(--blossom);
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button,
|
||||
button,
|
||||
input[type=submit],
|
||||
input[type=reset],
|
||||
input[type=button],
|
||||
input[type=file]::file-selector-button {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background-color: var(--blossom);
|
||||
color: var(--bg);
|
||||
border-radius: 1px;
|
||||
border: 1px solid var(--blossom);
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.button[disabled],
|
||||
button[disabled],
|
||||
input[type=submit][disabled],
|
||||
input[type=reset][disabled],
|
||||
input[type=button][disabled],
|
||||
input[type=file]::file-selector-button[disabled] {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.button:hover,
|
||||
button:hover,
|
||||
input[type=submit]:hover,
|
||||
input[type=reset]:hover,
|
||||
input[type=button]:hover,
|
||||
input[type=file]::file-selector-button:hover {
|
||||
background-color: var(--fade);
|
||||
color: var(--bg);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.button:focus-visible,
|
||||
button:focus-visible,
|
||||
input[type=submit]:focus-visible,
|
||||
input[type=reset]:focus-visible,
|
||||
input[type=button]:focus-visible,
|
||||
input[type=file]::file-selector-button:focus-visible {
|
||||
outline-style: solid;
|
||||
outline-width: 2px;
|
||||
}
|
||||
|
||||
textarea,
|
||||
select,
|
||||
input {
|
||||
color: var(--text);
|
||||
padding: 6px 10px;
|
||||
/* The 6px vertically centers text on FF, ignored by Webkit */
|
||||
margin-bottom: 10px;
|
||||
background-color: var(--bg-alt);
|
||||
border: 1px solid var(--bg-alt);
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
textarea:focus,
|
||||
select:focus,
|
||||
input:focus {
|
||||
border: 1px solid var(--blossom);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input[type=checkbox]:focus {
|
||||
outline: 1px dotted var(--blossom);
|
||||
}
|
||||
|
||||
label,
|
||||
legend,
|
||||
fieldset {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
<h1>Spark Generation Chance</h1>
|
||||
<p>Given a legacy, calculate the chance of generating each spark if you fulfill the conditions to do so, and the distribution of total spark counts.</p>
|
||||
<p>
|
||||
Given a legacy, calculate the chance of generating each spark if you fulfill the conditions to do so, and the distribution of
|
||||
total spark counts.
|
||||
</p>
|
||||
<p>TODO</p>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import adapter from '@sveltejs/adapter-static';
|
||||
import { relative, sep } from 'node:path';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
@@ -11,14 +11,11 @@ const config = {
|
||||
const isExternalLibrary = pathSegments.includes('node_modules');
|
||||
|
||||
return isExternalLibrary ? undefined : true;
|
||||
}
|
||||
},
|
||||
},
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
adapter: adapter(),
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -15,11 +15,11 @@ export default defineConfig({
|
||||
browser: {
|
||||
enabled: true,
|
||||
provider: playwright(),
|
||||
instances: [{ browser: 'chromium', headless: true }]
|
||||
instances: [{ browser: 'chromium', headless: true }],
|
||||
},
|
||||
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
|
||||
exclude: ['src/lib/server/**']
|
||||
}
|
||||
exclude: ['src/lib/server/**'],
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
@@ -28,9 +28,9 @@ export default defineConfig({
|
||||
name: 'server',
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user