112 Commits

Author SHA1 Message Date
0d810f9a7e Merge pull request 'Update 2025 Halloween event deadlines' (#18) from update-2025-hallowen-deadlines into master
Some checks failed
SteamWarCI Build failed
Reviewed-on: #18
Reviewed-by: Chaoscaot <max@chaoscaot.de>
2025-11-02 00:25:57 +01:00
5d384bc336 Update 2025 Halloween event deadlines
Some checks failed
SteamWarCI Build failed
2025-11-01 23:35:38 +01:00
f95cf6cbfa Fix pro-wargear.md
Some checks failed
SteamWarCI Build failed
2025-10-30 16:06:13 +01:00
972b8da9e6 Enhance EventFight handling by adding conditional relation names and improving group button visibility
Some checks failed
SteamWarCI Build failed
2025-10-30 12:06:44 +01:00
cb41356351 Fix date in 2025-halloween.md
All checks were successful
SteamWarCI Build successful
2025-10-27 19:32:08 +01:00
276dc56627 Add 2025-halloween.md
All checks were successful
SteamWarCI Build successful
2025-10-27 13:21:07 +01:00
0edec9cdf0 Add 2025-halloween.md
All checks were successful
SteamWarCI Build successful
2025-10-27 11:03:24 +01:00
4703fde5a3 src/content/downloads/advancedscripts.json aktualisiert
All checks were successful
SteamWarCI Build successful
2025-10-07 23:09:54 +02:00
954a8cc318 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 18:29:49 +02:00
1229edbf51 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 18:16:46 +02:00
00bce50a49 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 18:06:44 +02:00
5a44f2160c Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 17:55:25 +02:00
9b65d5d730 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 17:33:42 +02:00
8397aace8d Update Eventplan MWGL2025 2025-09-28 17:33:36 +02:00
c2b0bcc54e Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 17:21:46 +02:00
5c48f0cb85 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 17:04:14 +02:00
d30cceaad0 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 16:52:46 +02:00
41be843be4 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 16:47:39 +02:00
3768788f32 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 16:34:31 +02:00
7e6f953e44 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 16:32:28 +02:00
cad3a795a7 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 16:20:02 +02:00
48e8165417 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 16:10:34 +02:00
b11534490d Refactor EventFight handling to include team relation names and update type definitions
All checks were successful
SteamWarCI Build successful
2025-09-28 14:11:58 +02:00
c0f4a852b5 Refactor event handling and introduce TeamSelector component for improved fight management
All checks were successful
SteamWarCI Build successful
2025-09-28 10:26:08 +02:00
54d49cca5b Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 19:41:56 +02:00
831ea3af11 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 19:14:21 +02:00
b6a0692c50 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 19:13:48 +02:00
01394953d4 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 19:12:09 +02:00
c515b19e74 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 19:09:57 +02:00
98199cc9a0 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 19:08:31 +02:00
3f61564067 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 18:53:59 +02:00
7b0f18f65d Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 18:40:17 +02:00
4ac5d2d2b2 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 18:37:20 +02:00
8fd3e04116 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 18:10:17 +02:00
3180ad1263 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 18:09:56 +02:00
f689415b98 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 18:08:59 +02:00
894d0f8a05 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 17:56:24 +02:00
16d377e3e4 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 17:46:19 +02:00
1b2a05c204 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 17:11:35 +02:00
04969e79c3 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 16:45:08 +02:00
a949237334 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 16:09:41 +02:00
01a59d6de4 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 13:48:50 +02:00
3daeb8b62d Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 12:45:30 +02:00
aa72de70ef Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 12:06:12 +02:00
324025dd57 Add Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 12:02:51 +02:00
41b847b3e4 Refactor colorCode validation in PrefixSchema to allow any length string starting with "§"
All checks were successful
SteamWarCI Build successful
2025-09-16 18:07:39 +02:00
a3b4a6d0c2 Refactor event and fight repositories to use numeric IDs for groups; update datetime picker input handling; add new generator components for event fights and group phases.
All checks were successful
SteamWarCI Build successful
2025-09-16 18:03:29 +02:00
5f12a0cc7a Update kuerzel max length in TeamSchema to 16 characters
All checks were successful
SteamWarCI Build successful
2025-08-13 21:06:35 +02:00
7166575806 Fix section number for cannon count in WarShip rules
All checks were successful
SteamWarCI Build successful
2025-08-13 20:58:50 +02:00
0055e9fb9c Update WarShip rules to clarify restrictions on protective materials
All checks were successful
SteamWarCI Build successful
2025-08-13 20:55:49 +02:00
fc5a209638 Refactor WarShip rules for clarity and structure; added section numbers and improved definitions.
All checks were successful
SteamWarCI Build successful
2025-08-13 20:54:07 +02:00
c7cdc19102 Fix typo in WarGear Event announcement text
All checks were successful
SteamWarCI Build successful
2025-08-12 20:57:09 +02:00
c6bbe8c9c8 Add team size information to WarGear Event announcement
All checks were successful
SteamWarCI Build successful
2025-08-11 22:46:26 +02:00
1cec1b917e Add note about new Schematic type for WarGear Event
All checks were successful
SteamWarCI Build successful
2025-08-11 22:45:40 +02:00
13805c7f3f Add WarGear Event announcement for November 2025
All checks were successful
SteamWarCI Build successful
2025-08-11 22:41:15 +02:00
da668c574a Updated mwgl.md
All checks were successful
SteamWarCI Build successful
2025-07-28 13:42:19 +02:00
2aab86573a Add Image generated-image(8).png
All checks were successful
SteamWarCI Build successful
2025-07-28 13:41:51 +02:00
5d7eb3b8fb Merge pull request 'Merge branch mwgl' (#16) from mwgl into master
All checks were successful
SteamWarCI Build successful
Reviewed-on: #16
2025-07-28 13:02:14 +02:00
6933af1554 Updated mwgl.md
All checks were successful
SteamWarCI Build successful
2025-07-28 13:01:56 +02:00
e607ea1343 Updated mwgl.md
All checks were successful
SteamWarCI Build successful
2025-07-28 13:00:51 +02:00
b0ae4e978e Create page announcements/de/mwgl.md
All checks were successful
SteamWarCI Build successful
2025-07-28 12:57:57 +02:00
8fe273f3e0 Add Open-Source section to documentation
All checks were successful
SteamWarCI Build successful
2025-07-10 17:57:42 +02:00
1b48cbe1f4 Update edit link base URL to point to the master branch
All checks were successful
SteamWarCI Build successful
2025-07-10 13:51:09 +02:00
7276552ed1 Merge branch 'master' of https://git.steamwar.de/SteamWar/Website
All checks were successful
SteamWarCI Build successful
2025-07-10 13:49:15 +02:00
a2ef92aaad Add Docs 2025-07-10 13:49:00 +02:00
8b85cd0729 src/content/modes/spacecraft.json aktualisiert
All checks were successful
SteamWarCI Build successful
2025-06-29 22:51:13 +02:00
2d024cf64b Create page modes/spacecraft.json
Some checks failed
SteamWarCI Build failed
2025-06-29 22:49:58 +02:00
13d76d0a97 Updated SC-Eventplan.md
All checks were successful
SteamWarCI Build successful
2025-06-29 20:34:55 +02:00
e65fadb65c Updated SC-Eventplan.md
All checks were successful
SteamWarCI Build successful
2025-06-29 20:32:23 +02:00
6b4693b7f1 Updated SC-Eventplan.md
All checks were successful
SteamWarCI Build successful
2025-06-29 20:29:54 +02:00
92282006fe Add Image SpaceCraftWinners3.png
All checks were successful
SteamWarCI Build successful
2025-06-29 20:29:16 +02:00
5457632598 Fix formatting of teamPoints calculation in GroupTable component for improved readability
All checks were successful
SteamWarCI Build successful
2025-06-29 19:47:35 +02:00
bed134f8e0 Fix group points retrieval in GroupTable component to ensure correct mapping of event groups
All checks were successful
SteamWarCI Build successful
2025-06-29 19:45:18 +02:00
353a415990 Refactor GroupTable component to use $props for event, group, and rows; simplify teamPoints calculation with derived state
All checks were successful
SteamWarCI Build successful
2025-06-29 19:41:43 +02:00
3c6d0f8528 Fix
All checks were successful
SteamWarCI Build successful
2025-06-29 14:00:09 +02:00
887235dc86 Enhance caching mechanism by adding future promise to cached function and updating maps retrieval logic to use it
All checks were successful
SteamWarCI Build successful
2025-06-29 13:54:03 +02:00
a99a066f0d Merge branch 'master' of https://git.steamwar.de/SteamWar/Website
All checks were successful
SteamWarCI Build successful
2025-06-29 11:24:07 +02:00
e5e3c15b07 Add refresh functionality and duplicate fight feature in FightEditRow component 2025-06-29 11:23:51 +02:00
fb74689c39 Updated SC-Eventplan.md
All checks were successful
SteamWarCI Build successful
2025-06-29 09:15:11 +02:00
18b1f97a84 src/content/announcements/de/SC-Eventplan.md aktualisiert
All checks were successful
SteamWarCI Build successful
2025-06-29 03:07:37 +02:00
53b81db2c4 Updated SC-Eventplan.md
Some checks failed
SteamWarCI Build failed
2025-06-29 02:44:17 +02:00
2314b4c5b5 Updated SC-Eventplan.md
Some checks failed
SteamWarCI Build failed
2025-06-28 17:53:43 +02:00
6a81936f77 Updated SC-Eventplan.md
Some checks failed
SteamWarCI Build failed
2025-06-28 17:32:32 +02:00
a128de3213 Updated SC-Eventplan.md
Some checks failed
SteamWarCI Build failed
2025-06-28 17:31:58 +02:00
6df661f885 Create page announcements/de/SC-Eventplan.md
Some checks failed
SteamWarCI Build failed
2025-06-28 17:21:11 +02:00
a32d84ed86 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-27 16:12:51 +02:00
e60cebc9a3 Updated sw-arcade-fightplan.md
All checks were successful
SteamWarCI Build successful
2025-06-27 00:31:58 +02:00
3576d5e034 Refactor save method to remove base64 encoding for page content
All checks were successful
SteamWarCI Build successful
2025-06-27 00:31:08 +02:00
d5c7d8fc27 Updated sw-arcade-fightplan.md
Some checks failed
SteamWarCI Build failed
2025-06-27 00:29:26 +02:00
ce895e9297 Add default value to prompt for change description in page update
All checks were successful
SteamWarCI Build successful
2025-06-27 00:28:37 +02:00
7c83ad0937 Add prompt for change description in page update
All checks were successful
SteamWarCI Build successful
2025-06-27 00:28:16 +02:00
5e0a9d89b3 Fixing
All checks were successful
SteamWarCI Build successful
2025-06-27 00:24:13 +02:00
2a8b98ce5b Update copyright year to 2025 in table.css
All checks were successful
SteamWarCI Build successful
2025-06-26 23:57:55 +02:00
427818d6bf Fixing
Some checks failed
SteamWarCI Build failed
2025-06-26 23:56:45 +02:00
8424c14ca9 Remove unused import of ExtendedEvent from TeamTable.svelte
Some checks failed
SteamWarCI Build failed
2025-06-26 23:54:21 +02:00
602a7e1453 Remove unused import of Team from TeamTable.svelte
Some checks failed
SteamWarCI Build failed
2025-06-26 23:53:03 +02:00
9f31c5ff0c Remove unused import of Team from EventFightList.svelte
Some checks failed
SteamWarCI Build failed
2025-06-26 23:50:28 +02:00
8a41b98c58 Remove unused import of ExtendedEvent from EventFightList.svelte
Some checks failed
SteamWarCI Build failed
2025-06-26 23:49:09 +02:00
9fc5c500f5 Merge pull request 'Event Brackets' (#11) from event-brackets into master
All checks were successful
SteamWarCI Build successful
Reviewed-on: #11
2025-06-26 23:40:59 +02:00
bc879d7cad Add login.page to de.json
All checks were successful
SteamWarCI Build successful
2025-06-26 15:02:08 +02:00
96f0019dc1 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-22 16:55:31 +02:00
7418b608ab Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-21 23:33:31 +02:00
3802b9bc26 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-13 14:21:16 +02:00
03effd2fd2 src/content/downloads/advancedscripts.json aktualisiert
All checks were successful
SteamWarCI Build successful
2025-06-12 20:35:23 +02:00
a4669a897b Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-06 19:20:33 +02:00
eac0d5592d src/i18n/common/de.json aktualisiert
All checks were successful
SteamWarCI Build successful
2025-06-03 14:11:22 +02:00
bd9aea8f35 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-03 09:53:14 +02:00
6e715cee07 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-03 07:11:00 +02:00
4147a1d243 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-02 23:46:46 +02:00
46dba2a6f9 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-02 23:44:12 +02:00
3d8ad3a129 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-02 23:40:31 +02:00
7d50a4db12 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-02 23:30:42 +02:00
54 changed files with 1439 additions and 460 deletions

View File

@ -1,4 +1,4 @@
import {defineConfig, sharpImageService} from "astro/config";
import { defineConfig, sharpImageService } from "astro/config";
import svelte from "@astrojs/svelte";
import tailwind from "@astrojs/tailwind";
import configureI18n from "./astro-i18n.adapter";
@ -8,6 +8,8 @@ import robotsTxt from "astro-robots-txt";
import path from "node:path";
import mdx from "@astrojs/mdx";
import starlight from "@astrojs/starlight";
// https://astro.build/config
export default defineConfig({
output: "static",
@ -18,14 +20,40 @@ export default defineConfig({
site: "https://steamwar.de",
integrations: [
svelte(),
starlight({
disable404Route: true,
title: "SteamWar Docs",
defaultLocale: "de",
logo: {
src: "./src/images/logo.png",
},
social: [
{ icon: "discord", label: "Discord", href: "https://steamwar.de/discord" },
{ icon: "document", label: "Gitea", href: "https://git.steamwar.de" },
],
sidebar: [
{ label: "Startseite", slug: "docs" },
{ label: "Bau", badge: "WIP", items: ["docs/bausystem", { label: "Script System", items: ["docs/bausystem/script"] }] },
{ label: "Kampfsystem", badge: "WIP", items: ["docs/fightsystem"] },
{ label: "Minigames", badge: "WIP", items: ["docs/minigames"] },
{ label: "Schematicsystem", badge: "WIP", items: ["docs/schematicsystem"] },
{ label: "API", badge: "WIP", items: ["docs/api"] },
],
editLink: {
baseUrl: "https://git.steamwar.de/SteamWar/Website/src/branch/master/",
},
}),
tailwind({
configFile: "./tailwind.config.js",
applyBaseStyles: false,
}),
configureI18n(),
sitemap({
i18n: {
defaultLocale: "en", locales: {
en: "en-US", de: "de-DE",
defaultLocale: "en",
locales: {
en: "en-US",
de: "de-DE",
},
},
}),
@ -49,7 +77,7 @@ export default defineConfig({
{ userAgent: "omgili", disallow: "/" },
{ userAgent: "OmigliBot", disallow: "/" },
{ userAgent: "PerplexityBot", disallow: "/" },
{ userAgent: "Timpibot", disallow: "/" }
{ userAgent: "Timpibot", disallow: "/" },
],
}),
mdx(),
@ -66,7 +94,7 @@ export default defineConfig({
"@layouts": path.resolve("./src/layouts"),
"@repo": path.resolve("./src/components/repo"),
"@stores": path.resolve("./src/components/stores"),
"$lib": path.resolve("./src"),
$lib: path.resolve("./src"),
},
},
},

View File

@ -58,6 +58,8 @@
"dependencies": {
"@astrojs/mdx": "^4.3.0",
"@astrojs/sitemap": "^3.4.0",
"@astrojs/starlight": "^0.34.4",
"@astrojs/starlight-tailwind": "^4.0.1",
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/view": "^6.36.8",
@ -84,11 +86,9 @@
"svelte-spa-router": "^4.0.1"
},
"pnpm": {
"ignoredBuiltDependencies": [
"esbuild"
],
"onlyBuiltDependencies": [
"@parcel/watcher",
"esbuild",
"sharp"
]
}

View File

@ -24,35 +24,24 @@
import type { ExtendedEvent } from "@type/event.ts";
import "@styles/table.css";
export let event: ExtendedEvent;
export let group: number;
export let rows: number = 1;
let {
event,
group,
rows = 1,
}: {
event: ExtendedEvent;
group: number;
rows?: number;
} = $props();
$: teamPoints = event.teams
.map((team) => {
let fights = event.fights.filter((fight) => fight.blueTeam.id === team.id || fight.redTeam.id === team.id);
if (group !== undefined) {
fights = fights.filter((fight) => fight.group?.id === group);
}
const points = fights.reduce((acc, fight) => {
if (fight.ergebnis === 1 && fight.blueTeam.id === team.id) {
return acc + (fight.group?.pointsPerWin ?? 3);
} else if (fight.ergebnis === 2 && fight.redTeam.id === team.id) {
return acc + (fight.group?.pointsPerWin ?? 3);
} else if (fight.ergebnis === 3) {
return acc + (fight.group?.pointsPerDraw ?? 1);
} else {
return acc + (fight.group?.pointsPerLoss ?? 0);
}
}, 0);
return {
team,
points,
};
})
.sort((a, b) => b.points - a.points);
let teamPoints = $derived(
Object.entries(event.groups.find((g) => g.id === group)?.points ?? {})
.map(([teamId, points]) => ({
team: event.teams.find((t) => t.id === Number(teamId))!!,
points: points,
}))
.sort((a, b) => b.points - a.points)
);
</script>
<div class="p-3 bg-gray-200 dark:bg-neutral-800 rounded-2xl w-3/4 mx-auto">

View File

@ -18,12 +18,14 @@
-->
<script lang="ts">
import {createEventDispatcher} from "svelte";
interface Props {
children?: import('svelte').Snippet;
}
import { Card } from "@components/ui/card";
let { children }: Props = $props();
interface Props {
children?: import("svelte").Snippet;
ondrop: (event: DragEvent) => void;
}
let { children, ondrop }: Props = $props();
let dragover = $state(false);
@ -32,19 +34,16 @@
dragover = true;
}
const dispatch = createEventDispatcher();
function handleDrop(ev: DragEvent) {
ev.preventDefault();
dragover = false;
dispatch("drop", ev);
ondrop(ev);
}
</script>
<div class="w-56 bg-gray-800 p-4 rounded" class:border={dragover} class:m-px={!dragover} ondrop={handleDrop}
ondragover={handleDragOver} ondragleave={() => dragover = false} role="none">
<Card class="w-56 p-4 rounded m-px {dragover ? 'border-white' : ''}" ondrop={handleDrop} ondragover={handleDragOver} ondragleave={() => (dragover = false)} role="none">
{@render children?.()}
</div>
</Card>
<style>
div {

View File

@ -18,28 +18,28 @@
-->
<script lang="ts">
import { createBubbler } from 'svelte/legacy';
import type { Team } from "@type/team.ts";
import { brightness, colorFromTeam, lighten } from "../../util";
const bubble = createBubbler();
import type {Team} from "@type/team.ts";
import {brightness, colorFromTeam, lighten} from "../../util";
interface Props {
team: Team;
ondragstart: (event: DragEvent) => void;
}
interface Props {
team: Team;
}
let { team }: Props = $props();
let { team, ondragstart }: Props = $props();
let hover = $state(false);
</script>
<div class="rounded w-fit p-2 border-gray-600 border cursor-grab select-none m-1 flex place-items-center"
style:background-color={hover ? lighten(colorFromTeam(team)) : colorFromTeam(team)}
class:text-black={brightness(colorFromTeam(team))} draggable="true"
ondragstart={bubble('dragstart')}
onmouseenter={() => hover = true}
onmouseleave={() => hover = false}
role="figure">
<div
class="rounded w-fit p-2 border-gray-600 border cursor-grab select-none m-1 flex place-items-center"
style:background-color={hover ? lighten(colorFromTeam(team)) : colorFromTeam(team)}
class:text-black={brightness(colorFromTeam(team))}
draggable="true"
{ondragstart}
onmouseenter={() => (hover = true)}
onmouseleave={() => (hover = false)}
role="figure"
>
<span>{team.name}</span>
</div>

View File

@ -21,21 +21,23 @@
import type { RouteDefinition } from "svelte-spa-router";
import Router from "svelte-spa-router";
import NavLinks from "@components/moderator/layout/NavLinks.svelte";
import { Switch } from "@components/ui/switch";
import { Label } from "@components/ui/label";
import { navigate } from "astro:transitions/client";
import Players from "@components/moderator/pages/players/Players.svelte";
import Events from "@components/moderator/pages/events/Events.svelte";
import Dashboard from "@components/moderator/pages/dashboard/Dashboard.svelte";
import Event from "@components/moderator/pages/event/Event.svelte";
import Pages from "@components/moderator/pages/pages/Pages.svelte";
import Generator from "@components/moderator/pages/generators/Generator.svelte";
import Schematics from "@components/moderator/pages/schems/Schematics.svelte";
import { Tooltip } from "bits-ui";
const routes: RouteDefinition = {
"/": Dashboard,
"/events": Events,
"/players": Players,
"/event/:id": Event,
"/event/:id/generate": Generator,
"/pages": Pages,
"/schems": Schematics,
};
</script>
@ -44,11 +46,10 @@
<div class="flex h-16 items-center px-4">
<a href="/" class="text-sm font-bold transition-colors text-primary"> SteamWar </a>
<NavLinks />
<div class="ml-auto flex items-center space-x-4">
<Switch id="new-ui-switch" checked={true} onclick={() => navigate("/admin")} />
<Label for="new-ui-switch">New UI!</Label>
</div>
</div>
</div>
<Router {routes} />
<Tooltip.Provider>
<Router {routes} />
</Tooltip.Provider>
</div>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import GroupSelector from "./GroupSelector.svelte";
import type { EventFight, EventFightEdit, ResponseGroups, SWEvent } from "@type/event";
import type { EventFight, EventFightEdit, ResponseGroups, ResponseRelation, SWEvent } from "@type/event";
import { fromAbsolute } from "@internationalized/date";
import { Label } from "@components/ui/label";
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
@ -11,46 +11,36 @@
import { ChevronsUpDown, Check } from "lucide-svelte";
import { Button } from "@components/ui/button";
import { cn } from "@components/utils";
import type { Team } from "@components/types/team";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
import type { Snippet } from "svelte";
import { Input } from "@components/ui/input";
import TeamSelector from "./TeamSelector.svelte";
import type { EventModel } from "../pages/event/eventmodel.svelte";
let {
fight,
teams,
event,
actions,
onSave,
groups = $bindable(),
data,
}: {
fight: EventFight | null;
teams: Team[];
event: SWEvent;
groups: ResponseGroups[];
actions: Snippet<[boolean, () => void]>;
onSave: (fight: EventFightEdit) => void;
data: EventModel;
} = $props();
let fightModus = $state(fight?.spielmodus);
let fightMap = $state(fight?.map);
let fightBlueTeam = $state(fight?.blueTeam);
let fightRedTeam = $state(fight?.redTeam);
let fightStart = $state(fight?.start ? fromAbsolute(fight.start, "Europe/Berlin") : fromAbsolute(event.start, "Europe/Berlin"));
let fightStart = $state(fight?.start ? fromAbsolute(fight.start, "Europe/Berlin") : fromAbsolute(data.event.start, "Europe/Berlin"));
let fightErgebnis = $state(fight?.ergebnis ?? 0);
let fightSpectatePort = $state(fight?.spectatePort?.toString() ?? null);
let fightGroup = $state(fight?.group?.id ?? null);
let selectedGroup = $derived(groups.find((group) => group.id === fightGroup));
let mapsStore = $derived(maps(fightModus ?? "null"));
let gamemodeSelectOpen = $state(false);
let mapSelectOpen = $state(false);
let blueTeamSelectOpen = $state(false);
let redTeamSelectOpen = $state(false);
let createOpen = $state(false);
let groupSelectOpen = $state(false);
let dirty = $derived(
fightModus !== fight?.spielmodus ||
@ -151,128 +141,10 @@
</Command>
</PopoverContent>
</Popover>
<Label for="fight-blue-team">Blue Team</Label>
<Popover bind:open={blueTeamSelectOpen}>
<PopoverTrigger>
{#snippet child({ props })}
<Button variant="outline" class="justify-between" {...props} role="combobox">
{teams.find((value) => value.id === fightBlueTeam?.id)?.name || fightBlueTeam?.name || "Select a team..."}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search Teams..." />
<CommandList>
<CommandEmpty>No team found.</CommandEmpty>
<CommandGroup>
<CommandItem
value={"-1"}
onSelect={() => {
fightBlueTeam = {
id: -1,
name: "?",
color: "7",
kuerzel: "?",
};
blueTeamSelectOpen = false;
}}
keywords={["?"]}>???</CommandItem
>
<CommandItem
value={"0"}
onSelect={() => {
fightBlueTeam = {
id: 0,
name: "Public",
color: "7",
kuerzel: "PUB",
};
blueTeamSelectOpen = false;
}}
keywords={["PUB", "Public"]}>PUB</CommandItem
>
</CommandGroup>
<CommandGroup heading="Teams">
{#each teams as team}
<CommandItem
value={team.name}
onSelect={() => {
fightBlueTeam = team;
blueTeamSelectOpen = false;
}}
>
<Check class={cn("mr-2 size-4", team.id !== fightBlueTeam?.id && "text-transparent")} />
{team.name}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Label for="fight-red-team">Red Team</Label>
<Popover bind:open={redTeamSelectOpen}>
<PopoverTrigger>
{#snippet child({ props })}
<Button variant="outline" class="justify-between" {...props} role="combobox">
{teams.find((value) => value.id === fightRedTeam?.id)?.name || fightRedTeam?.name || "Select a team..."}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search Teams..." />
<CommandList>
<CommandEmpty>No team found.</CommandEmpty>
<CommandGroup>
<CommandItem
value={"-1"}
onSelect={() => {
fightRedTeam = {
id: -1,
name: "?",
color: "7",
kuerzel: "?",
};
redTeamSelectOpen = false;
}}
keywords={["?"]}>???</CommandItem
>
<CommandItem
value={"0"}
onSelect={() => {
fightRedTeam = {
id: 0,
name: "Public",
color: "7",
kuerzel: "PUB",
};
redTeamSelectOpen = false;
}}
keywords={["PUB", "Public"]}>PUB</CommandItem
>
</CommandGroup>
<CommandGroup heading="Teams">
{#each teams as team}
<CommandItem
value={team.name}
onSelect={() => {
fightRedTeam = team;
redTeamSelectOpen = false;
}}
>
<Check class={cn("mr-2 size-4", team.id !== fightRedTeam?.id && "text-transparent")} />
{team.name}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Label>Blue Team</Label>
<TeamSelector bind:selectedTeam={fightBlueTeam} {data} fightId={fight?.id} team="BLUE" />
<Label>Red Team</Label>
<TeamSelector bind:selectedTeam={fightRedTeam} {data} fightId={fight?.id} team="RED" />
<Label>Start</Label>
<DateTimePicker bind:value={fightStart} />
{#if fight !== null}
@ -290,7 +162,7 @@
{/if}
<Label for="fight-group">Gruppe</Label>
<GroupSelector {event} bind:value={fightGroup} bind:groups></GroupSelector>
<GroupSelector event={data.event} bind:value={fightGroup} bind:groups={data.groups}></GroupSelector>
<Label for="spectate-port">Spectate Port</Label>
<Input id="spectate-port" bind:value={fightSpectatePort} type="number" placeholder="2001" />
</div>

View File

@ -25,7 +25,7 @@
let groupSelectOpen = $state(false);
async function handleGroupSave(group: GroupUpdateEdit) {
let g = await $eventRepo.createGroup(event.id.toString(), group);
let g = await $eventRepo.createGroup(event.id, group);
groups.push(g);
value = g.id;
createOpen = false;

View File

@ -0,0 +1,253 @@
<script lang="ts">
import type { ResponseRelation } from "@components/types/event";
import type { Team } from "@components/types/team";
import { Button } from "@components/ui/button";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/ui/tabs";
import { Tooltip, TooltipContent, TooltipTrigger } from "@components/ui/tooltip";
import { cn } from "@components/utils";
import { Check, ChevronsUpDown, GitPullRequestArrow, Plus } from "lucide-svelte";
import type { EventModel } from "../pages/event/eventmodel.svelte";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
import { Label } from "@components/ui/label";
import { eventRepo } from "@components/repo/event";
interface Props {
selectedTeam: Team | undefined;
open?: boolean;
team: "BLUE" | "RED";
data: EventModel;
fightId?: number;
onSelect?: (team: Team) => void;
}
let { selectedTeam = $bindable(), data, team, open = $bindable(false), fightId, onSelect }: Props = $props();
const currentRelation = $derived(data.relations.find((r) => r.fight === fightId && r.team === team));
let fromType = $state<"FIGHT" | "GROUP">(currentRelation?.type ?? "FIGHT");
let fromFight = $state<string | undefined>(currentRelation?.fromFight?.id?.toString());
let fromFightData = $derived(data.fights.find((f) => f.id.toString() === fromFight));
let fromGroup = $state<string | undefined>(currentRelation?.fromGroup?.id?.toString());
let fromGroupData = $derived(data.groups.find((g) => g.id.toString() === fromGroup));
let fromPlace = $state<string | undefined>(currentRelation?.fromPlace?.toString());
let relationOpen = $state(false);
async function saveRelation() {
relationOpen = false;
if (currentRelation === undefined) {
await $eventRepo.createRelation(data.event.id, {
fightId: fightId!,
team,
fromType,
fromId: fromType === "FIGHT" ? parseInt(fromFight!) : parseInt(fromGroup!),
fromPlace: parseInt(fromPlace!),
});
} else {
await $eventRepo.updateRelation(data.event.id, currentRelation.id, {
from: {
fromType,
fromId: fromType === "FIGHT" ? parseInt(fromFight!) : parseInt(fromGroup!),
fromPlace: parseInt(fromPlace!),
},
});
}
data.relations = await $eventRepo.listRelations(data.event.id);
reset();
}
async function clearRelation() {
relationOpen = false;
if (currentRelation !== undefined) {
await $eventRepo.deleteRelation(data.event.id, currentRelation.id);
data.relations = await $eventRepo.listRelations(data.event.id);
}
reset();
}
function reset() {
fromType = currentRelation?.type ?? "FIGHT";
fromFight = currentRelation?.fromFight?.id.toString();
fromGroup = currentRelation?.fromGroup?.id.toString();
fromPlace = currentRelation?.fromPlace.toString();
}
let canSave = $derived(
(fromType !== currentRelation?.type ||
fromFight !== (currentRelation?.fromFight?.id.toString() ?? "") ||
fromGroup !== (currentRelation?.fromGroup?.id.toString() ?? "") ||
fromPlace !== (currentRelation?.fromPlace.toString() ?? "")) &&
((fromType === "FIGHT" && fromFight !== "" && fromPlace !== "") || (fromType === "GROUP" && fromGroup !== "" && fromPlace !== ""))
);
</script>
<div class="flex gap-2">
<Popover bind:open>
<PopoverTrigger>
{#snippet child({ props })}
<Button variant="outline" class="justify-between flex-1" {...props} role="combobox">
{#if selectedTeam?.id === -1}
???
{:else if selectedTeam?.id === 0}
PUB
{:else}
{data.teams.find((v) => v.id === selectedTeam?.id)?.name || selectedTeam?.name || "Select a team..."}
{/if}
{#if currentRelation !== undefined}
({#if currentRelation.type === "FIGHT"}
{currentRelation.fromPlace === 0 ? "Gewinner" : "Verlierer"} von {currentRelation.fromFight?.blueTeam.name} vs {currentRelation.fromFight?.redTeam.name} ({new Date(
currentRelation.fromFight?.start ?? 0
).toLocaleTimeString("de-DE", {
timeStyle: "short",
})})
{:else}
{currentRelation.fromPlace + 1}. Platz von {currentRelation.fromGroup?.name}
{/if})
{/if}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search Teams..." />
<CommandList>
<CommandEmpty>No team found.</CommandEmpty>
<CommandGroup>
<CommandItem
value={"-1"}
onSelect={() => {
selectedTeam = {
id: -1,
name: "?",
color: "7",
kuerzel: "?",
};
onSelect?.(selectedTeam);
open = false;
}}
keywords={["?"]}>???</CommandItem
>
<CommandItem
value={"0"}
onSelect={() => {
selectedTeam = {
id: 0,
name: "Public",
color: "7",
kuerzel: "PUB",
};
onSelect?.(selectedTeam);
open = false;
}}
keywords={["PUB", "Public"]}>PUB</CommandItem
>
</CommandGroup>
<CommandGroup heading="Teams">
{#each data.teams as team}
<CommandItem
value={team.name}
onSelect={() => {
selectedTeam = team;
onSelect?.(selectedTeam);
open = false;
}}
>
<Check class={cn("mr-2 size-4", team.id !== selectedTeam?.id && "text-transparent")} />
{team.name}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Popover bind:open={relationOpen}>
<PopoverTrigger>
{#snippet child({ props })}
<Tooltip>
<TooltipTrigger>
<Button {...props} size="icon" variant={currentRelation !== undefined ? "default" : "outline"} disabled={fightId === undefined}>
<GitPullRequestArrow />
</Button>
</TooltipTrigger>
<TooltipContent>Kampfverbindung</TooltipContent>
</Tooltip>
{/snippet}
</PopoverTrigger>
<PopoverContent>
<Tabs bind:value={fromType}>
<TabsList>
<TabsTrigger value="FIGHT">Kampf</TabsTrigger>
<TabsTrigger value="GROUP">Gruppe</TabsTrigger>
</TabsList>
<TabsContent value="FIGHT">
<Label>Kampf</Label>
<Select bind:value={fromFight} type="single" disabled={data.fights.length === 0}>
<SelectTrigger>
{fromFightData
? `${new Date(fromFightData.start).toLocaleString("de-DE", { timeStyle: "short" })}: ${fromFightData.blueTeam.kuerzel} vs. ${fromFightData.redTeam.kuerzel}`
: "Kampf auswählen..."}
</SelectTrigger>
<SelectContent>
{#each data.fights.filter((v) => v.id !== fightId) as fight (fight.id)}
<SelectItem value={fight.id.toString()}
>{new Date(fight.start).toLocaleString("de-DE", {
timeStyle: "short",
})}: {fight.blueTeam.kuerzel} vs. {fight.redTeam.kuerzel}</SelectItem
>
{/each}
</SelectContent>
</Select>
<Label>Team</Label>
<Select bind:value={fromPlace} type="single" disabled={data.fights.length === 0}>
<SelectTrigger>
{fromPlace ? (fromPlace === "0" ? "Gewinner" : "Verlierer") : "Platz auswählen..."}
</SelectTrigger>
<SelectContent>
<SelectItem value={"0"}>Gewinner</SelectItem>
<SelectItem value={"1"}>Verlierer</SelectItem>
</SelectContent>
</Select>
</TabsContent>
<TabsContent value="GROUP">
<Label>Gruppe</Label>
<Select bind:value={fromGroup} type="single" disabled={data.groups.length === 0}>
<SelectTrigger>
{fromGroupData ? fromGroupData.name : "Kampf auswählen..."}
</SelectTrigger>
<SelectContent>
{#each data.groups as group (group.id)}
<SelectItem value={group.id.toString()}>{group.name}</SelectItem>
{/each}
</SelectContent>
</Select>
<Label>Platz</Label>
<Select bind:value={fromPlace} type="single" disabled={data.fights.length === 0}>
<SelectTrigger>
{fromPlace ? `${parseInt(fromPlace) + 1}. Platz` : "Platz auswählen..."}
</SelectTrigger>
<SelectContent>
{#each Array(32) as _, i}
<SelectItem value={i.toString()}>{i + 1}. Platz</SelectItem>
{/each}
</SelectContent>
</Select>
</TabsContent>
</Tabs>
<div class="flex justify-end gap-2 mt-2">
<Button onclick={clearRelation} variant="destructive">Löschen</Button>
<Button onclick={saveRelation} disabled={!canSave}>Übernehmen</Button>
</div>
</PopoverContent>
</Popover>
</div>

View File

@ -20,7 +20,7 @@
<script lang="ts">
import FightEditRow from "./FightEditRow.svelte";
import type { EventFight, EventFightEdit, ExtendedEvent } from "@type/event";
import type { EventFightEdit } from "@type/event";
import { createSvelteTable, FlexRender } from "@components/ui/data-table";
import { type ColumnFiltersState, getCoreRowModel, getFilteredRowModel, getGroupedRowModel, getSortedRowModel, type RowSelectionState, type SortingState } from "@tanstack/table-core";
import { columns } from "./columns";
@ -38,7 +38,6 @@
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@components/ui/dropdown-menu";
import GroupSelector from "@components/moderator/components/GroupSelector.svelte";
import { fightRepo } from "@components/repo/fight";
import type { Team } from "@components/types/team";
import type { EventModel } from "./eventmodel.svelte";
let { data = $bindable(), refresh }: { data: EventModel; refresh: () => void } = $props();
@ -135,7 +134,7 @@
<DialogTitle>Fight Erstellen</DialogTitle>
<DialogDescription>Hier kannst du einen neuen Fight erstellen</DialogDescription>
</DialogHeader>
<FightEdit fight={null} teams={data.teams} event={data.event} groups={data.groups} onSave={handleSave}>
<FightEdit fight={null} {data} onSave={handleSave}>
{#snippet actions(dirty, submit)}
<DialogFooter>
<Button disabled={!dirty} onclick={submit}>Speichern</Button>
@ -203,8 +202,9 @@
<MenubarItem onclick={() => (createOpen = true)}>Fight Erstellen</MenubarItem>
<MenubarGroup>
<MenubarGroupHeading>Generatoren</MenubarGroupHeading>
<MenubarItem disabled>Gruppenphase</MenubarItem>
<MenubarItem disabled>K.O. Phase</MenubarItem>
<a href="#/event/{data.event.id}/generate">
<MenubarItem>Gruppenphase</MenubarItem>
</a>
</MenubarGroup>
</MenubarContent>
</MenubarMenu>
@ -258,12 +258,14 @@
{group?.name ?? "Keine Gruppe"}
</TableCell>
<TableCell class="text-right">
<Button variant="ghost" size="icon" onclick={() => openGroupEditDialog(group)}>
<EditIcon />
</Button>
<Button variant="ghost" size="icon" onclick={() => openGroupResultsDialog(group)}>
<GroupIcon />
</Button>
{#if group}
<Button variant="ghost" size="icon" onclick={() => openGroupEditDialog(group!)}>
<EditIcon />
</Button>
<Button variant="ghost" size="icon" onclick={() => openGroupResultsDialog(group!)}>
<GroupIcon />
</Button>
{/if}
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant="ghost" size="icon">
@ -291,13 +293,7 @@
</TableCell>
{/each}
<TableCell class="text-right">
<FightEditRow
fight={row.original}
teams={data.teams}
bind:groups={data.groups}
event={data.event}
onupdate={(update) => (data.fights = data.fights.map((v) => (v.id === update.id ? update : v)))}
></FightEditRow>
<FightEditRow fight={row.original} {data} onupdate={(update) => (data._fights = data._fights.map((v) => (v.id === update.id ? update : v)))} {refresh}></FightEditRow>
</TableCell>
</TableRow>
{/each}

View File

@ -1,18 +1,21 @@
<script lang="ts">
import type { EventFight, EventFightEdit, ResponseGroups, SWEvent } from "@type/event";
import type { EventFight, EventFightEdit, ResponseGroups, ResponseRelation, SWEvent } from "@type/event";
import { Button } from "@components/ui/button";
import { EditIcon, MenuIcon, GroupIcon } from "lucide-svelte";
import { EditIcon, CopyIcon } from "lucide-svelte";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@components/ui/dialog";
import FightEdit from "@components/moderator/components/FightEdit.svelte";
import type { Team } from "@components/types/team";
import { fightRepo } from "@components/repo/fight";
import { eventRepo } from "@components/repo/event";
import type { EventModel } from "./eventmodel.svelte";
let { fight, teams, groups = $bindable(), event, onupdate }: { fight: EventFight; teams: Team[]; groups: ResponseGroups[]; event: SWEvent; onupdate: (update: EventFight) => void } = $props();
let { fight, onupdate, refresh, data }: { fight: EventFight; onupdate: (update: EventFight) => void; refresh: () => void; data: EventModel } = $props();
let editOpen = $state(false);
let duplicateOpen = $state(false);
async function handleSave(fightData: EventFightEdit) {
let f = await $fightRepo.updateFight(event.id, fight.id, {
let f = await $fightRepo.updateFight(data.event.id, fight.id, {
...fightData,
blueTeam: fightData.blueTeam.id,
redTeam: fightData.redTeam.id,
@ -23,6 +26,18 @@
editOpen = false;
}
async function handlyCopy(fightData: EventFightEdit) {
await $eventRepo.createFight(data.event.id.toString(), {
...fightData,
blueTeam: fightData.blueTeam.id,
redTeam: fightData.redTeam.id,
});
refresh();
duplicateOpen = false;
}
</script>
<div>
@ -37,7 +52,7 @@
<DialogTitle>Fight bearbeiten</DialogTitle>
<DialogDescription>Hier kannst du die Daten des Kampfes bearbeiten.</DialogDescription>
</DialogHeader>
<FightEdit {fight} {teams} bind:groups {event} onSave={handleSave}>
<FightEdit {fight} {data} onSave={handleSave}>
{#snippet actions(dirty, submit)}
<DialogFooter>
<Button disabled={!dirty} onclick={submit}>Speichern</Button>
@ -46,4 +61,24 @@
</FightEdit>
</DialogContent>
</Dialog>
<Dialog bind:open={duplicateOpen}>
<DialogTrigger>
<Button variant="ghost" size="icon">
<CopyIcon />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Fight duplizieren</DialogTitle>
<DialogDescription>Hier kannst du die Daten des duplizierten Fights ändern</DialogDescription>
</DialogHeader>
<FightEdit {fight} {data} onSave={handlyCopy}>
{#snippet actions(dirty, submit)}
<DialogFooter>
<Button onclick={submit}>Speichern</Button>
</DialogFooter>
{/snippet}
</FightEdit>
</DialogContent>
</Dialog>
</div>

View File

@ -73,7 +73,7 @@
<CommandEmpty>No Players found :(</CommandEmpty>
<CommandGroup heading="Players">
{#each $players
.filter((v) => v.name.includes(playerSearch))
.filter((v) => v.name.toLowerCase().includes(playerSearch.toLowerCase()))
.filter((v, i) => i < 50)
.filter((v) => !referees.some((k) => k.uuid === v.uuid)) as player (player.uuid)}
<CommandItem value={player.name} onSelect={() => addReferee(player.uuid)} keywords={[player.uuid]}>{player.name}</CommandItem>

View File

@ -20,12 +20,10 @@
<script lang="ts">
import { Button } from "@components/ui/button";
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell, TableCaption } from "@components/ui/table";
import type { ExtendedEvent } from "@type/event.ts";
import { eventRepo } from "@repo/event";
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
import { teams } from "@components/stores/stores";
import type { Team } from "@components/types/team";
import type { EventModel } from "./eventmodel.svelte";
let { event = $bindable() }: { event: EventModel } = $props();

View File

@ -20,9 +20,9 @@
import { Checkbox } from "@components/ui/checkbox";
import { renderComponent } from "@components/ui/data-table";
import type { ColumnDef } from "@tanstack/table-core";
import type { EventFight } from "@type/event.ts";
import type { EventFightModel } from "./eventmodel.svelte";
export const columns: ColumnDef<EventFight> = [
export const columns: ColumnDef<EventFightModel>[] = [
{
id: "auswahl",
header: ({ table }) => {
@ -32,7 +32,7 @@ export const columns: ColumnDef<EventFight> = [
onCheckedChange: () => {
if (!table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected()) {
const now = new Date();
const rows = table.getRowModel().rows.filter((row) => new Date(row.original.date) > now);
const rows = table.getRowModel().rows.filter((row) => new Date(row.original.start) > now);
if (rows.length > 0) {
rows.forEach((row) => {
@ -57,7 +57,7 @@ export const columns: ColumnDef<EventFight> = [
},
},
{
accessorFn: (r) => r.blueTeam.name + " vs " + r.redTeam.name,
accessorFn: (r) => r.blueTeam.nameWithRelation + " vs " + r.redTeam.nameWithRelation,
id: "begegnung",
header: "Begegnung",
},

View File

@ -1,21 +1,68 @@
import type { ResponseUser } from "@components/repo/event";
import type { EventFight, ExtendedEvent, ResponseGroups, ResponseRelation, SWEvent } from "@components/types/event";
import type { Team } from "@components/types/team";
import { derived } from "svelte/store";
export class EventModel {
public event: SWEvent = $state({} as SWEvent);
public teams: Array<Team> = $state([]);
public groups: Array<ResponseGroups> = $state([]);
public fights: Array<EventFight> = $state([]);
public _fights: Array<EventFight> = $state([]);
public referees: Array<ResponseUser> = $state([]);
public relations: Array<ResponseRelation> = $state([]);
public fights = $derived(this.remapFights(this._fights, this.relations));
constructor(data: ExtendedEvent) {
this.event = data.event;
this.relations = data.relations;
this.teams = data.teams;
this.groups = data.groups;
this.fights = data.fights;
this._fights = data.fights;
this.referees = data.referees;
this.relations = data.relations;
}
private remapFights(v: Array<EventFight>, rels: Array<ResponseRelation>) {
return v.map((fight) => {
let f = JSON.parse(JSON.stringify(fight)) as EventFight;
let blueTeamRelation = "";
let redTeamRelation = "";
let relations = rels.filter((relation) => relation.fight === f.id);
relations.forEach((relation) => {
let str = "";
if (relation.type === "FIGHT") {
str = `${relation.fromPlace === 0 ? "Gewinner" : "Verlierer"} von ${relation.fromFight?.blueTeam.name} vs ${relation.fromFight?.redTeam.name} (${new Date(
relation.fromFight?.start ?? 0
).toLocaleTimeString("de-DE", {
timeStyle: "short",
})})`;
} else {
str = `${relation.fromPlace + 1}. Platz von ${relation.fromGroup?.name}`;
}
if (relation.team === "BLUE") {
blueTeamRelation = str;
} else {
redTeamRelation = str;
}
});
return {
...f,
blueTeam: {
...f.blueTeam,
nameWithRelation: blueTeamRelation ? `${f.blueTeam.name} (${blueTeamRelation})` : f.blueTeam.name,
},
redTeam: {
...f.redTeam,
nameWithRelation: redTeamRelation ? `${f.redTeam.name} (${redTeamRelation})` : f.redTeam.name,
},
};
});
}
}
export type EventFightModel = (typeof EventModel.prototype.fights)[number];

View File

@ -0,0 +1,22 @@
<script lang="ts">
import type { ExtendedEvent } from "@components/types/event";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@components/ui/tabs";
import GroupPhaseGenerator from "./gens/group/GroupPhaseGenerator.svelte";
let {
data,
}: {
data: ExtendedEvent;
} = $props();
</script>
<div class="m-4">
<Tabs value="group">
<TabsList class="mb-4">
<TabsTrigger value="group">Gruppenphase</TabsTrigger>
<TabsTrigger value="ko">K.O. Phase</TabsTrigger>
</TabsList>
<TabsContent value="group">
<GroupPhaseGenerator {data} />
</TabsContent>
</Tabs>
</div>

View File

@ -0,0 +1,22 @@
<script lang="ts">
import { eventRepo } from "@components/repo/event";
import FightsGenerator from "./FightsGenerator.svelte";
interface Props {
params: { id: number };
}
let { params }: Props = $props();
let id = params.id;
let future = $eventRepo.getEvent(id.toString());
</script>
{#await future}
<p>Loading...</p>
{:then event}
<FightsGenerator data={event} />
{:catch error}
<p class="text-red-500">Error loading event: {error.message}</p>
{/await}

View File

@ -0,0 +1,306 @@
<script lang="ts">
import DragAcceptor from "@components/admin/pages/generate/DragAcceptor.svelte";
import TeamChip from "@components/admin/pages/generate/TeamChip.svelte";
import { eventRepo } from "@components/repo/event";
import { fightRepo } from "@components/repo/fight";
import { gamemodes, maps } from "@components/stores/stores";
import type { ExtendedEvent } from "@components/types/event";
import type { Team } from "@components/types/team";
import { Button } from "@components/ui/button";
import { Card } from "@components/ui/card";
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
import { Dialog } from "@components/ui/dialog";
import { Input } from "@components/ui/input";
import { Label } from "@components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
import { Slider } from "@components/ui/slider";
import { fromAbsolute, fromDate, parseDateTime, parseDuration } from "@internationalized/date";
import dayjs from "dayjs";
import { Plus } from "lucide-svelte";
import { replace } from "svelte-spa-router";
let {
data,
}: {
data: ExtendedEvent;
} = $props();
let teams = $derived(new Map<number, Team>(data.teams.map((team) => [team.id, team])));
let groups: number[][] = $state([]);
let teamsNotInGroup = $derived(data.teams.filter((team) => !groups.flat().includes(team.id)));
function dragToNewGroup(event: DragEvent) {
event.preventDefault();
let teamId = parseInt(event.dataTransfer!.getData("team"));
groups = [...groups.map((value) => value.filter((value1) => value1 != teamId)), [teamId]].filter((value) => value.length > 0);
}
function teamDragStart(ev: DragEvent, team: Team) {
ev.dataTransfer!.setData("team", team.id.toString());
}
let resetDragOver = $state(false);
function resetDragOverEvent(ev: DragEvent) {
resetDragOver = true;
ev.preventDefault();
}
function dropReset(ev: DragEvent) {
ev.preventDefault();
let teamId = parseInt(ev.dataTransfer!.getData("team"));
groups = groups.map((group) => group.filter((team) => team !== teamId)).filter((group) => group.length > 0);
resetDragOver = false;
}
function dropGroup(ev: DragEvent, groupIndex: number) {
ev.preventDefault();
let teamId = parseInt(ev.dataTransfer!.getData("team"));
groups = groups.map((group, i) => (i === groupIndex ? [...group.filter((value) => value != teamId), teamId] : group.filter((value) => value != teamId))).filter((group) => group.length > 0);
}
let startTime = $state(fromAbsolute(data.event.start, "Europe/Berlin"));
let gamemode = $state("");
let map = $state("");
let selectableGamemodes = $derived(
$gamemodes
.map((gamemode) => {
return {
name: gamemode,
value: gamemode,
};
})
.sort((a, b) => a.name.localeCompare(b.name))
);
let mapsStore = $derived(maps(gamemode));
let selectableMaps = $derived(
$mapsStore
.map((map) => {
return {
name: map,
value: map,
};
})
.sort((a, b) => a.name.localeCompare(b.name))
);
let roundTime = $state(30);
let startDelay = $state(30);
let showAutoGrouping = $state(false);
let groupCount = $state(Math.floor(data.teams.length / 2));
function createGroups() {
let teams = data.teams.map((team) => team.id).sort(() => Math.random() - 0.5);
groups = [];
for (let i = 0; i < groupCount; i++) {
groups.push([]);
}
while (teams.length > 0) {
groups[teams.length % groupCount].push(teams.pop() as number);
}
showAutoGrouping = false;
groups = groups.filter((group) => group.length > 0);
}
function generateGroups(groups: number[][]): number[][][][] {
const groupFights: number[][][][] = [];
groups.forEach((group) => {
let round = group.length + (group.length % 2) - 1;
let groupFight = [];
for (let i = 0; i < round; i++) {
let availableTeams = [...group];
if (group.length % 2 === 1) {
availableTeams = availableTeams.filter((team, index) => index !== i);
}
let roundFights = [];
while (availableTeams.length > 0) {
let team1 = availableTeams.pop() as number;
let team2 = availableTeams.at(i % availableTeams.length) as number;
availableTeams = availableTeams.filter((team) => team !== team2);
let fight = [team1, team2];
fight.sort(() => Math.random() - 0.5);
roundFights.push(fight);
}
groupFight.push(roundFights);
}
groupFights.push(groupFight);
});
return groupFights;
}
let groupsFights = $derived(generateGroups(groups));
let generateDisabled = $derived(groupsFights.length > 0 && groupsFights.every((value) => value.every((value1) => value1.length > 0)) && gamemode !== "" && map !== "");
async function generateFights() {
groupsFights.forEach((group, i) => {
$eventRepo
.createGroup(data.event.id, {
name: "Gruppe " + (i + 1),
type: "GROUP_STAGE",
})
.then((v) => {
group.forEach((round, j) => {
round.forEach(async (fight, k) => {
const blueTeam = teams.get(fight[0])!;
const redTeam = teams.get(fight[1])!;
let karte = map;
if (karte === "%random%") {
karte = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
}
await $fightRepo.createFight(data.event.id, {
blueTeam: blueTeam.id,
redTeam: redTeam.id,
group: v.id,
map: karte,
spectatePort: null,
spielmodus: gamemode,
start: dayjs(
startTime
.copy()
.add({
minutes: roundTime * j,
})
.add({
seconds: startDelay * (k + i * round.length),
})
.toDate()
),
});
});
});
});
});
await replace("#/event/" + data.event.id);
}
</script>
<div class="flex justify-between">
<Card
id="reseter"
class="flex w-fit p-2 border border-gray-700 rounded h-20 pt-6 relative {resetDragOver ? 'border-white' : ''}"
ondragover={resetDragOverEvent}
ondragleave={() => (resetDragOver = false)}
ondrop={dropReset}
role="group"
>
{#each teamsNotInGroup as team (team.id)}
<TeamChip {team} ondragstart={(ev) => teamDragStart(ev, team)} />
{/each}
</Card>
<div class="flex items-center mr-4">
<Button onclick={() => (showAutoGrouping = true)}>Automatische Gruppen</Button>
</div>
</div>
<div class="flex mt-4 gap-4 border-b border-gray-700 pb-4">
{#each groups as group, i (i)}
<DragAcceptor ondrop={(ev) => dropGroup(ev, i)}>
<h1>Gruppe {i + 1} ({group.length})</h1>
{#each group as teamId (teamId)}
<TeamChip team={teams.get(teamId)!} ondragstart={(ev) => teamDragStart(ev, teams.get(teamId)!)} />
{/each}
</DragAcceptor>
{/each}
<DragAcceptor ondrop={dragToNewGroup}>
<h1>Neue Gruppe</h1>
</DragAcceptor>
</div>
<div class="border-b mt-4 border-gray-700 pb-4">
<Label for="event-end">Startzeit</Label>
<DateTimePicker bind:value={startTime} />
<div class="mt-2">
<Label for="event-roundtime">Rundenzeit: {roundTime}m</Label>
<Slider id="event-roundtime" type="single" bind:value={roundTime} step={1} min={5} max={60} />
</div>
<div class="mt-2">
<Label for="event-member">Startverzögerung: {startDelay}</Label>
<Slider id="event-member" type="single" bind:value={startDelay} step={1} min={0} max={30} />
</div>
<div class="mt-2">
<Label for="fight-gamemode">Spielmodus</Label>
<Select type="single" bind:value={gamemode}>
<SelectTrigger id="fight-gamemode">{gamemode}</SelectTrigger>
<SelectContent>
{#each selectableGamemodes as gamemodeOption}
<SelectItem value={gamemodeOption.value}>{gamemodeOption.name}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<div class="mt-2">
<Label for="fight-maps">Map</Label>
<Select type="single" bind:value={map}>
<SelectTrigger id="fight-maps">{map}</SelectTrigger>
<SelectContent>
<SelectItem value="%random%">Zufällige Map</SelectItem>
{#each selectableMaps as mapOption}
<SelectItem value={mapOption.value}>{mapOption.name}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
</div>
<div class="text-center mx-2">
{#each groupsFights as fightsGroup, i}
<div>
<h1 class="text-4xl">Gruppe: {i + 1}</h1>
{#each fightsGroup as fightsRound, j}
<div class="border-b border-gray-700">
<h1 class="text-2xl">Runde: {j + 1}</h1>
{#each fightsRound as fightTeams, k}
<div class="text-left p-4">
<span class="p-2 border border-gray-700 rounded"
>{new Intl.DateTimeFormat("de-DE", {
day: "2-digit",
month: "2-digit",
year: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}).format(
startTime
.copy()
.add({
minutes: roundTime * j,
seconds: startDelay * (k + i * fightsRound.length),
})
.toDate()
)}</span
>
{teams.get(fightTeams[0])!.name} vs. {teams.get(fightTeams[1])!.name}
</div>
{/each}
</div>
{/each}
</div>
{/each}
</div>
<Button class="!p-4 fixed bottom-4 right-4" disabled={!generateDisabled} onclick={generateFights}>
<Plus />
</Button>
<style lang="scss">
:global(#reseter::before) {
content: "Reset";
position: absolute;
top: 0;
color: white;
}
:global(#reseter) {
min-width: 14rem;
}
</style>

View File

@ -37,10 +37,8 @@ export class OpenEditPage {
contentToSave += "---\n\n";
}
contentToSave += this.content;
const encodedContent = btoa(new TextEncoder().encode(contentToSave).reduce((data, byte) => data + String.fromCharCode(byte), ""));
console.log(encodedContent);
//await get(pageRepo).updatePage(this.pageId, this.sha, encodedContent, this.manager.branch);
await get(pageRepo).updatePage(this.pageId, contentToSave, this.sha, prompt("Was hast du geändert?", `Updated ${this.pageTitle}`) ?? `Updated ${this.pageTitle}`, this.manager.branch);
this.dirty = false;
this.manager.reloadImages();
}

View File

@ -150,7 +150,7 @@ export class EventRepo {
.then((value) => value.json())
.then((value) => z.array(ResponseGroupsSchema).parse(value));
}
public async createGroup(eventId: string, group: CreateEventGroup): Promise<ResponseGroups> {
public async createGroup(eventId: number, group: CreateEventGroup): Promise<ResponseGroups> {
CreateEventGroupSchema.parse(group);
return await fetchWithToken(this.token, `/events/${eventId}/groups`, {
method: "POST",
@ -186,12 +186,12 @@ export class EventRepo {
}
// Relations
public async listRelations(eventId: string): Promise<ResponseRelation[]> {
public async listRelations(eventId: number): Promise<ResponseRelation[]> {
return await fetchWithToken(this.token, `/events/${eventId}/relations`)
.then((value) => value.json())
.then((value) => z.array(ResponseRelationSchema).parse(value));
}
public async createRelation(eventId: string, relation: CreateEventRelation): Promise<ResponseRelation> {
public async createRelation(eventId: number, relation: CreateEventRelation): Promise<ResponseRelation> {
CreateEventRelationSchema.parse(relation);
return await fetchWithToken(this.token, `/events/${eventId}/relations`, {
method: "POST",
@ -206,7 +206,7 @@ export class EventRepo {
.then((value) => value.json())
.then(ResponseRelationSchema.parse);
}
public async updateRelation(eventId: string, relationId: string, relation: UpdateEventRelation): Promise<ResponseRelation> {
public async updateRelation(eventId: number, relationId: number, relation: UpdateEventRelation): Promise<ResponseRelation> {
UpdateEventRelationSchema.parse(relation);
return await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`, {
method: "PUT",
@ -216,7 +216,7 @@ export class EventRepo {
.then((value) => value.json())
.then(ResponseRelationSchema.parse);
}
public async deleteRelation(eventId: string, relationId: string): Promise<boolean> {
public async deleteRelation(eventId: number, relationId: number): Promise<boolean> {
const res = await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`, {
method: "DELETE",
});

View File

@ -31,7 +31,7 @@ export interface CreateFight {
redTeam: number;
start: Dayjs;
spectatePort: number | null;
group: string | null;
group: number | null;
}
export interface UpdateFight {
@ -57,7 +57,6 @@ export class FightRepo {
return await fetchWithToken(this.token, `/events/${eventId}/fights`, {
method: "POST",
body: JSON.stringify({
event: eventId,
spielmodus: fight.spielmodus,
map: fight.map,
blueTeam: fight.blueTeam,

View File

@ -17,20 +17,31 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {readonly, writable} from "svelte/store";
import { readonly, writable } from "svelte/store";
import type {Readable, Subscriber, Unsubscriber} from "svelte/store";
import type { Readable, Subscriber, Unsubscriber } from "svelte/store";
export interface Cached<T> extends Readable<T> {
reload: () => void;
future: Promise<T>;
}
export function cached<T>(normal: T, init: () => Promise<T>): Cached<T> {
const store = writable<T>(normal);
const future = new Promise<T>((resolve) => {
let f = false;
store.subscribe((value) => {
if (f) {
resolve(value);
} else {
f = true;
}
});
});
let first = true;
const reload = () => {
init().then(data => {
init().then((data) => {
store.set(data);
});
};
@ -45,6 +56,7 @@ export function cached<T>(normal: T, init: () => Promise<T>): Cached<T> {
return store.subscribe(run, invalidate);
},
reload,
future,
};
}
@ -58,7 +70,7 @@ export function cachedFamily<T, K>(normal: K, init: (arg0: T) => Promise<K>): (a
let first = true;
const reload = () => {
init(arg).then(data => {
init(arg).then((data) => {
store.set(data);
});
};

View File

@ -55,7 +55,7 @@ export const gamemodes = cached<string[]>([], async () => {
});
export const maps = cachedFamily<string, string[]>([], async (gamemode) => {
if (get(gamemodes).every((value) => value !== gamemode)) return [];
if ((await gamemodes.future).every((value) => value !== gamemode)) return [];
const res = await fetchWithToken(get(tokenStore), `/data/admin/gamemodes/${gamemode}/maps`);
if (!res.ok) {

View File

@ -60,10 +60,11 @@ export type ResponseGroups = z.infer<typeof ResponseGroupsSchema>;
export const ResponseRelationSchema = z.object({
id: z.number(),
fight: EventFightSchema,
fight: z.number(),
team: z.enum(["RED", "BLUE"]),
type: z.enum(["FIGHT", "GROUP"]),
fromFight: EventFightSchema.nullable(),
fromGroup: ResponseGroupsSchema.nullable(),
fromFight: EventFightSchema.optional(),
fromGroup: ResponseGroupsSchema.optional(),
fromPlace: z.number(),
});

View File

@ -17,11 +17,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {z} from "zod";
import { z } from "zod";
export const PrefixSchema = z.object({
name: z.string().startsWith("PREFIX_"),
colorCode: z.string().length(2).startsWith("§"),
colorCode: z.string().startsWith("§"),
chatPrefix: z.string(),
});

View File

@ -17,12 +17,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {z} from "zod";
import { z } from "zod";
export const TeamSchema = z.object({
id: z.number(),
name: z.string(),
kuerzel: z.string().min(1).max(4),
kuerzel: z.string().min(1).max(16),
color: z.string().max(1),
});

View File

@ -24,14 +24,15 @@
import { ScrollArea } from "$lib/components/ui/scroll-area";
import { CalendarIcon } from "lucide-svelte";
import { cn } from "@components/utils";
import type {ZonedDateTime} from "@internationalized/date";
import { fromDate, type ZonedDateTime } from "@internationalized/date";
import Input from "../input/input.svelte";
let {
value = $bindable(),
onChange
onChange,
}: {
value: ZonedDateTime
onChange?: ((date: ZonedDateTime | undefined) => void) | undefined
value: ZonedDateTime;
onChange?: ((date: ZonedDateTime | undefined) => void) | undefined;
} = $props();
let isOpen = $state(false);
@ -63,13 +64,7 @@
<Popover bind:open={isOpen}>
<PopoverTrigger>
<Button
variant="outline"
class={cn(
"w-full justify-start text-left font-normal",
!value && "text-muted-foreground"
)}
>
<Button variant="outline" class={cn("w-full justify-start text-left font-normal", !value && "text-muted-foreground")}>
<CalendarIcon class="mr-2 h-4 w-4" />
{#if value}
{new Intl.DateTimeFormat("de-DE", {
@ -86,23 +81,14 @@
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<Input type="datetime-local" value={value.toString().slice(0, 16)} onchange={(e) => handleDateSelect(fromDate(e.target.valueAsDate, "Europe/Berlin"))} />
<div class="sm:flex">
<Calendar
mode="single"
bind:value
onValueChange={(date) => handleDateSelect(date)}
initialFocus
/>
<Calendar mode="single" bind:value onValueChange={(date) => handleDateSelect(date)} initialFocus />
<div class="flex flex-col sm:flex-row sm:h-[300px] divide-y sm:divide-y-0 sm:divide-x">
<ScrollArea class="w-64 sm:w-auto">
<div class="flex sm:flex-col p-2">
{#each [...hours].reverse() as hour}
<Button
size="icon"
variant={value && value.hour === hour ? "default" : "ghost"}
class="sm:w-full shrink-0 aspect-square"
onclick={() => handleTimeChange("hour", hour)}
>
<Button size="icon" variant={value && value.hour === hour ? "default" : "ghost"} class="sm:w-full shrink-0 aspect-square" onclick={() => handleTimeChange("hour", hour)}>
{hour}
</Button>
{/each}
@ -113,12 +99,12 @@
<div class="flex sm:flex-col p-2">
{#each Array.from({ length: 60 }, (_, i) => i) as minute}
<Button
size="icon"
variant={value && value.minute === minute ? "default" : "ghost"}
class="sm:w-full shrink-0 aspect-square"
onclick={() => handleTimeChange("minute", minute)}
size="icon"
variant={value && value.minute === minute ? "default" : "ghost"}
class="sm:w-full shrink-0 aspect-square"
onclick={() => handleTimeChange("minute", minute)}
>
{minute.toString().padStart(2, '0')}
{minute.toString().padStart(2, "0")}
</Button>
{/each}
</div>

View File

@ -1,29 +1,6 @@
import Root from "./input.svelte";
export type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
};
export type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
mousemove: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
wheel: FormInputEvent<WheelEvent>;
};
export {
Root,
//
Root as Input,
Root,
//
Root as Input,
};

View File

@ -1,42 +1,39 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import type { InputEvents } from "./index.js";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined;
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
import { cn } from "@components/utils";
import { type WithElementRef } from "bits-ui";
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
type Props = WithElementRef<Omit<HTMLInputAttributes, "type"> & ({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })>;
let { ref = $bindable(null), value = $bindable(), type, files = $bindable(), class: className, ...restProps }: Props = $props();
</script>
<input
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:mousemove
on:paste
on:input
on:wheel|passive
{...$$restProps}
/>
{#if type === "file"}
<input
bind:this={ref}
data-slot="input"
class={cn(
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
type="file"
bind:files
bind:value
{...restProps}
/>
{:else}
<input
bind:this={ref}
data-slot="input"
class={cn(
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{type}
bind:value
{...restProps}
/>
{/if}

View File

@ -0,0 +1,45 @@
---
title: WarShip Halloween Event 2025
key: 2025-halloween
description: Das WarShip Halloween Event 2025 für die Community
created: 2025-10-27T00:00:00.000Z
tags:
- event
- warship
---
Ahoi Community,
das diesjährige Halloween-Event nähert sich, die Tage werden langsam kürzer und die Nächte länger. Es geht auf dem Herbst zu und erinnert daran, dass das Jahr wieder halb vorbei ist. Dieses Mal im Spielmodus Warship. Das im Format 6 gegen 6 ausgetragen wird. Neben dem eigentlichen Turnier wird das Außendesign bewertet. Die Bewertung des Außendedigns wird zu 70% Das SW Builderteam übernehmen und 30% die Userbewertung. Die prozentuale Bewertung soll dazu dienen, dass große Teams Ihr eigenes Design nicht hoch puschen können.
Das Event findet am 08.11.2025 in der Version 1.21 mit dem aktuellen Regelwerk statt.
~~Anmelde + Einsendeschluss 03.11.2025~~
**Neue Fristen**:
Einsendeschluss: 06.11.2025 23:59 Uhr
Hotfixschluss: 07.11.2025 23:59 Uhr
Der Anmeldeschluss bleibt der 03.11.2025
zusätzlich wird es mit einem Designcontest begleitet.
Design Regel: Halloween
Arena: Lucifus
Design Bewertung
- Userbewertung (30%) wird über den Discord Community Server von SW organisiert. (Bilder vom Außendesign werden gepostet und per Abstimmung ausgelost)
- Builderbewertung (70%) läuft nach folgende Kriterien ab.
- Form des WS
- Farbgestaltung
- Muster
- Thematisierung: Thema Halloween / Grusel
Es wird also 3 Sieges- Plätze geben welch wie Folgt ermittelt wird.
- Gesamtsieger: Höchste Fight Platzierung und Design Platzierung im Durchschnitt
- Event- Sieger : Höchste Fight Platzierung
- Designsieger: Bestes Design
Das Warshipdesign vom Gesamtsieger wird bis zum nächsten Halloween in der Lobby ausgestellt. Wir freuen uns auf zahlreiche Anmeldungen und sind gespannt, welche Designs uns erwarten!
Das Serverteam

View File

@ -0,0 +1,39 @@
---
title: SpaceCraft Eventplan
description: Der Kampfplan für das SpaceCraft Event am 29.6.
key: SC-Eventplan
created: 2025-06-28T00:00:00.000Z
tags:
- SpaceCraft
- Event
image: ../../../images/SpaceCraftWinners3.png
---
### Infos:
Eventleitung: TheBreadBeard
Sollten fights zu schnell vorbei sein, ist eine vorverschiebung der folgenden fights möglich. In diesem Fall wird eine Pause von 10 Minuten garantiert.
## Gruppen
<group-table data-event="72" data-group="2"> </group-table>
<group-table data-event="72" data-group="3"> </group-table>
## Tabelle
![](https://cdn.discordapp.com/attachments/690535200865910794/1388779302782501014/SPaceship_Eventplan.png?ex=68623904&is=6860e784&hm=5f32d5f34449aee7e2b58b9c16d657f0d3b08e2cd5b341a7b31559a1b1b30f27&)
## Fights
### Gruppenphase
**Gruppe 1**
<fight-table data-event="72" data-group="2"> </group-table>
**Gruppe 2**
<fight-table data-event="72" data-group="3"> </group-table>
### KO-Phase
<fight-table data-event="72" data-group="4"> </group-table>

View File

@ -0,0 +1,25 @@
---
title: MiniWarGearLiga Ankündigung
description: Die MiniWargear-Liga 2025 findet am 27./28. September statt.
key: mwgl
created: 2025-07-28T00:00:00.000Z
tags:
- event
- miniwargear
image: ../../../images/generated-image(8).png
---
**Ahoi Liebe Community,**
Es ist wieder so weit die MiniWargear-Liga findet auch dieses Jahr wieder statt.
Infos zum Event:
* Die MWGL findet am Wochenende des **27./28.09.2025** um **16 Uhr** statt.
* Einsendeschluss ist der **22.09.2025** um 23:59 Uhr bis dahin muss ein MWG gemäß Regelwerk vorhanden sein.
* Hotfixes (geringe Änderungen wie z.B. das Fixen von Läufen usw.) dürfen bis zum **25.09.2025** um 23:59 Uhr nachgereicht werden.
* Max. **4 Kämpfer** pro Team
* Normales SW-MWG-Regelwerk mit automatischen Kanonen
* Es wird einen eigenen Schem-Typ geben.
* Der Schem-Name muss mit dem Teamkürzel enden.
* Gefightet wird mit getrenntem Spectate- und Fightserver (wie bei der WGS)

View File

@ -0,0 +1,98 @@
---
title: MiniWarGearLiga Eventplan
description: Der Eventplan für die MiniWargear-Liga 2025
key: mwgl
created: 2025-09-27T00:00:00.000Z
tags:
- event
- miniwargear
image: ../../../images/generated-image(8).png
---
## Spieltag 27.09.2025
### Gruppenphase 1
##### Gruppen
| Gruppe 1 | Gruppe 2 | Gruppe 3 |
|----------|----------|----------|
| BF | Borg | MLT! |
| KT | EXO | Salo |
| ED | PL | FK |
In nachfolgenden Tabellen wird `W` für `Winner`/`Gewinner` stehen und `L` für `Loser`/`Verlierer`.
| 27.09.2025 | Arena 1 | Ergebnis | Arena 2 | Ergebnis | Arena 3 | Ergebnis |
|------------|----------|:--------:|-------------|:--------:|--------------|:--------:|
| 16:00 | BF vs KT | BF | Borg vs EXO | Borg | MLT! vs Salo | MLT! |
| 16:30 | BF vs ED | ED | Borg vs PL | Borg | MLT! vs FK | FK |
| 17:00 | KT vs BF | BF | EXO vs PL | PL | Salo vs MLT! | Salo |
| 17:30 | ED vs BF | BF | Borg vs PL | Borg | FK vs Salo | Salo |
### Gruppenphase 2
##### Gruppen
| Gruppe 1 | Gruppe 2 | Gruppe 3 |
|----------|----------|----------|
| BF | ED | MLT! |
| Borg | PL | EXO |
| Salo | FK | KT |
In nachfolgender Tabelle steht `Gr.` für `Gruppe` und das Wort `Platz` wird weggelassen.
| 27.09.2025 | Arena 1 | Ergebnis | Arena 2 | Ergebnis | Arena 3 | Ergebnis |
|------------|--------------|:--------:|----------|:--------:|-------------|:--------:|
| 18:00 | BF vs Borg | Borg | ED vs PL | PL | EXO vs KT | EXO |
| 18:30 | Borg vs Salo | Borg | PL vs FK | FK | EXO vs MLT! | MLT! |
| 19:00 | BF vs Salo | Salo | ED vs PL | PL | KT vs EXO | EXO |
| 19:30 | Borg vs Salo | Salo | FK vs PL | FK | MLT! vs EXO | EXO |
KT wird disqualifiziert und tritt morgen nicht mehr an. Das Team belegt somit den 9. und damit letzten Platz.
## Spieltag 28.09.2025
### Leiter
Die fights werden auf 5 Minuten an den vorherigen vorgezogen.
| 28.09.2025 | | Ergebnis |
|------------|-------------|:--------:|
| 16:00 | MLT! vs EXO | EXO |
| 16:15 | EXO vs ED | ED |
| 16:25 | ED vs PL | PL |
| 16:40 | PL vs FK | FK |
| 16:55 | FK vs BF | FK |
### Spiel um Platz 3
| 28.09.2025 | | Ergebnis |
|------------|------------|:--------:|
| 17:15 | FK vs Borg | Borg |
| 17:25 | FK vs Borg | Borg |
| Entfällt | FK vs Borg | / |
### Spiel um Platz 2 und 1
| 28.09.2025 | | Ergebnis |
|------------|--------------|:--------:|
| 17:45 | Borg vs Salo | Salo |
| 18:00 | Borg vs Salo | Borg |
| 18:10 | Borg vs Salo | Borg |
| 18:20 | Borg vs Salo | Borg |
| entfällt | Borg vs Salo | / |
## Endplatzierung
| Platz | Team |
|-------|------|
| 1. | Borg |
| 2. | Salo |
| 3. | FK |
| 4. | BF |
| 5. | PL |
| 6. | ED |
| 7. | EXO |
| 8. | MLT! |
| 9. | KT |

View File

@ -17,7 +17,7 @@ Die neue Physik nutzt ihr mit ***/nogravity*** auf dem Bau
### Alle Infos zum Event:
- Die Technik und das Design der Publics werden nochmal seperat Public gestellt. So könnt ihr, wenn ihr keine oder weniger eigene Technik bauen wollt, einfacher die Public technik verwenden.
- Das Event findet am 19.06.2025 um 16 Uhr statt
- Das Event findet am 29.06.2025 um 16 Uhr statt
- Es gibt kein Entern
- Kits können wie bei WG und MWG selbst erstellt werden
- es wird Redstone TKO verwendet
@ -37,12 +37,13 @@ Die neue Physik nutzt ihr mit ***/nogravity*** auf dem Bau
- Wasser darf in Kanonen verwendet werden
- Dadurch könnt ihr TNT einfacher komprimieren. Wasser muss nicht mehr den Zweck erfüllen, die Kanone vor eigenem Schaden zu bewahren
- 32 Projektile pro Seite
- Unterpunkt: Crossbows
- Es gibt keinen Techhider
# Regelwerk für das Event:
## Definitionen
Ein AirShip ist eine beidseitig bewaffnete Struktur in Minecraft 1.19 und sieht flugfähig aus.
Ein AirShip ist eine beidseitig bewaffnete Struktur in Minecraft 1.20 und sieht flugfähig aus.
## Maße
@ -57,16 +58,41 @@ Ein AirShip ist eine beidseitig bewaffnete Struktur in Minecraft 1.19 und sieht
- 51 breit
- 32 hoch
Technik, welche für die Funktion der Kanonen nicht relevant ist, darf sich in maßen auch im Design-Bereich befinden.
Entfernung von Mitte zu Mitte des Gegners: 100 Block
Es dürfen keine Blöcke über die Grenze des Designbereiches bewegt werden
Panzerung darf nur im Technikbereich verbaut werden.
## Projektile
Ein Projektil ist ein gezündetes TNT, welches in die gegnerische Hälfte wechselt. Eine Kanone ist eine Vorrichtung zum Beschleunigen von Projektilen. Es ist verboten, mehrere Kanonen als eine Einzige auszugeben.
Jeder Seite stehen bis zu 32 Projektile zur Verfügung. Sie müssen auf der dem Gegner zugewandten Seite oder, wenn beidseitig, in der Mitte gezündet werden
## Crossbows
Kanonen, welche, ohne nachgeladen werden zu müssen, mehrere Schüsse abgeben können, Müssen:
- Jeden Schuss das TNT an den selben Punkten zünden.
- Für jedes Projektil das die Kanone für einen Schuss maximal zünden könnte, mindestens 1,5 Redstone Tick zwischen den Schüssen warten.
##### Beispiele
| Projektile | Ticks |
|-------|------------|
| 1 | 1,5 |
| 2 | 3 |
| 3 | 4,5 |
| 4 | 6 |
| ... | ... |
| 8 | 12 |
| 12 | 18 |
| 16 | 24 |
| ... | ... |
| 32 | 48 |
## Siegesbedingung
- Ein AirShip wird zu 60% (nach prozentualer Redstonekomponenten) zerstört.
@ -76,10 +102,63 @@ Jeder Seite stehen bis zu 32 Projektile zur Verfügung. Sie müssen auf der dem
## Blöcke
Es dürfen maximal je 30 TNT und 32 Werfer verbaut werden. Verbaute Blöcke dürfen einen TNT-Widerstand von maximal 6 haben. Ausgenommen davon ist Wasser, welches ausschließlich in Kanonen verbaut werden darf.
Es dürfen maximal je 32 TNT und 32 Werfer verbaut werden. Verbaute Blöcke dürfen einen TNT-Widerstand von maximal 6 haben. Ausgenommen davon ist Wasser, welches ausschließlich in Kanonen verbaut werden darf.
Inventar-Blöcke dürfen nur mit Blumen, Honigflaschen und Pferderüstungen gefüllt sein. Zusätzlich dürfen sich in Kisten und Fässern TNT sowie in Werfern 2·64 Feuerbälle, 2·64 reguläre Pfeile oder 1 Eimer zum Aufheben von powdered Snow befinden.
Zusätzlich verboten: Monster Spawner, Eis, Nether Portal, Silberfischsteine, Leuchtfeuer, unsichtbare Blöcke (Ausnahme: structure_void) Das Duplizieren von Blöcken und Entities ist verboten.
Blöcke aus dem eigenen AS dürfen nicht in großer Menge entfernt werden.
Bug-Using ist nicht erwünscht.
# Versteckte Blöcke (Ersetzt durch Stein)
- WATER
- NOTE_BLOCK
- POWERED_RAIL
- DETECTOR_RAIL
- PISTON
- PISTON_HEAD
- STICKY_PISTON
- TNT
- CHEST
- TRAPPED_CHEST
- REDSTONE_WIRE
- STONE_PRESSURE_PLATE
- IRON_DOOR
- OAK_PRESSURE_PLATE
- SPRUCE_PRESSURE_PLATE
- BIRCH_PRESSURE_PLATE
- JUNGLE_PRESSURE_PLATE
- ACACIA_PRESSURE_PLATE
- DARK_OAK_PRESSURE_PLATE
- REDSTONE_TORCH
- REDSTONE_WALL_TORCH
- REPEATER
- BREWING_STAND
- TRIPWIRE_HOOK
- TRIPWIRE
- HEAVY_WEIGHTED_PRESSURE_PLATE
- LIGHT_WEIGHTED_PRESSURE_PLATE
- COMPARATOR
- REDSTONE_BLOCK
- HOPPER
- ACTIVATOR_RAIL
- DROPPER
- SLIME_BLOCK
- OBSERVER
- HONEY_BLOCK
- LEVER
# Zusatzinhalte folgender Blöcke (z.B. Text, Inhalt von Inventaren) werden mit versteckt:
- SIGN
- DISPENSER
- CHEST
- TRAPPED_CHEST
- FURNACE
- BREWING_STAND
- HOPPER
- DROPPER
- SHULKER_BOX
- JUKEBOX
- COMPARATOR

View File

@ -2,29 +2,31 @@
title: SteamWar Arcade Eventplan
key: sw-arcade-eventplan
description: Der Eventplan für SteamWar Arcade
created: 2025-05-14
created: 2025-05-14T00:00:00.000Z
tags:
- event
- towerrun
- misslewars
- tntleague
- arcade
- event
- towerrun
- misslewars
- tntleague
- arcade
image: ../../../images/sw-arcade-image.png
---
### Infos:
Eventleitung: Chaoscaot
Fights werden nach Möglichkeit mit einer Pause von 10 Minuten vorverschoben.
# Kämpfe
## Punkte aus der Gruppenphase
<group-table data-event="70"> </group-table>
### Fights
<fight-table data-event="70" data-group="Gruppe 1"> </fight-table>
<fight-table data-event="70" data-group="1"> </fight-table>
## Auswahl des Spielmodis

View File

@ -0,0 +1,43 @@
---
title: WarGear Event
description: "Das erste WarGear Event in der 1.21 auf SteamWar!"
key: steamwar-wg-event-21
created: 2025-08-12
tags:
- event
- wargear
image: ../../../images/generated-image(11).png
---
**Ahoi, liebe Community,**
lange ist es her seit dem letzten WarGear-Event. Nun ist es so weit: Am **29. und 30. November** findet ein neues WarGear-Event **mit** SFAs statt.
## Übersicht
- **Datum:** 29.11.: Gruppenphase, 30.11.: KO-Phase
- **Spielmodus:** Standard **und** Pro WarGear
- **Teamgröße**: 6
- **Anmeldeschluss:** 22. November
- **Einsendeschluss:** 24. November
- **Hotfix-Schluss:** 27. November
Bei der SFA muss sich an eines der Regelwerke gehalten werden. Standard- und Pro-WarGear treten gleichwertig gegeneinander an.
## Sonderregeln
**Version:** 1.21.6 (aktuellste Bau-Version)
Es wird einen eigenen Schematic-Typen geben.
### Windcharges
Werden beim Überfliegen der Mittellinie entfernt.
### Cobwebs & Powder Snow
Dürfen uneingeschränkt benutzt werden, jedoch nicht als Panzerung. Die Bewertung liegt im Ermessen des Prüfers.
**Verantwortlicher:** Chaoscaot
**Frohes Bauen!**

View File

@ -17,7 +17,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {defineCollection, reference, z} from "astro:content";
import { defineCollection, reference, z } from "astro:content";
import { docsLoader } from "@astrojs/starlight/loaders";
import { docsSchema } from "@astrojs/starlight/schema";
export const pagesSchema = z.object({
title: z.string().min(1).max(80),
@ -55,8 +57,7 @@ export const downloads = defineCollection({
schema: z.object({
name: z.string(),
description: z.string(),
url: z.string().url()
.or(z.record(z.string(), z.string())),
url: z.string().url().or(z.record(z.string(), z.string())),
sourceUrl: z.string().url().optional(),
}),
});
@ -71,45 +72,50 @@ export const rules = defineCollection({
export const announcements = defineCollection({
type: "content",
schema: ({image}) => z.object({
title: z.string(),
description: z.string(),
author: z.string().optional(),
image: image().optional(),
tags: z.array(z.string()),
created: z.date(),
key: z.string(),
}),
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
author: z.string().optional(),
image: image().optional(),
tags: z.array(z.string()),
created: z.date(),
key: z.string(),
}),
});
export const publics = defineCollection({
type: "data",
schema: ({image}) => z.object({
"name": z.string(),
"description": z.string(),
"id": z.number().positive(),
"creator": z.string().array().optional(),
"showcase": z.string().url().optional(),
"camera": z.object({
"fov": z.number().optional(),
"near": z.number().optional(),
"far": z.number().optional(),
"distance": z.number().optional(),
}).optional(),
"image": image(),
"alt": image().optional(),
"xray": image().optional(),
"gamemode": reference("modes"),
"3d": z.boolean().optional().default(true),
}),
schema: ({ image }) =>
z.object({
name: z.string(),
description: z.string(),
id: z.number().positive(),
creator: z.string().array().optional(),
showcase: z.string().url().optional(),
camera: z
.object({
fov: z.number().optional(),
near: z.number().optional(),
far: z.number().optional(),
distance: z.number().optional(),
})
.optional(),
image: image(),
alt: image().optional(),
xray: image().optional(),
gamemode: reference("modes"),
"3d": z.boolean().optional().default(true),
}),
});
export const collections = {
"pages": pages,
"help": help,
"modes": modes,
"rules": rules,
"downloads": downloads,
"announcements": announcements,
"publics": publics,
pages: pages,
help: help,
modes: modes,
rules: rules,
downloads: downloads,
announcements: announcements,
publics: publics,
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
};

View File

@ -0,0 +1,5 @@
---
title: Überblick
---
WIP

View File

@ -0,0 +1,5 @@
---
title: Überblick
---
WIP

View File

@ -0,0 +1,5 @@
---
title: Script Überblick
---
WIP

View File

@ -0,0 +1,5 @@
---
title: Überblick
---
WIP

View File

@ -0,0 +1,29 @@
---
title: Startseite
desciption: Startseite der SteamWar Dokumentation
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
Herzlich Willkommen in der SteamWar Dokumentation!
## SteamWar beitreten
SteamWar ist ein Minecraft Java Server.
<Tabs>
<TabItem label="Java Edition">
- IP: `steamwar.de`
- Empholene Version: `1.21.6`
</TabItem>
<TabItem label="Bedrock Edition">
- IP: `steamwar.de`
- Port: `19132`
- Version: `Aktuellste`
</TabItem>
</Tabs>
## Open-Source
Die SteamWar Software ist Open-Source und auf der internen [Gitea](https://git.steamwar.de) verfügbar. Jeglicher Code ist unter der [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.en.html) lizenziert. Das bedeutet, dass du den Code frei nutzen, modifizieren und verteilen kannst, solange du den Code ebenfalls unter der AGPL lizenziert verfügbar machst.
Bugs und Feature Requests können im [Gitea Issue Tracker](https://git.steamwar.de/SteamWar/SteamWar/issues) erstellt werden. Contributions sind immer willkommen! Bitte erstelle einen Pull Request im [SteamWar Repository](https://git.steamwar.de/SteamWar/SteamWar).

View File

@ -0,0 +1,5 @@
---
title: Überblick
---
WIP

View File

@ -0,0 +1,5 @@
---
title: Überblick
---
WIP

View File

@ -2,7 +2,9 @@
"name": "AdvancedScripts",
"description": "Ein Fabric-Mod, der für den BauServer von SteamWar Hotkeys für das ScriptSystem hinzufügt. Hierzu werden die einzelnen Zeichen an den Server gesendet und vom Server verarbeitet.",
"url": {
"1.21.6": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.2.3/AdvancedScripts-2.2.3.jar",
"1.21.4": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.2.0/AdvancedScripts-2.2.0.jar",
"1.21.3": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.2.1/AdvancedScripts-2.2.1.jar",
"1.20.6": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.1.0/AdvancedScripts-2.1.0.jar",
"1.19.3": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.0.0/AdvancedScripts-2.0.0.jar",
"Hotkey script": "https://git.steamwar.de/SteamWar/SteamWar/src/branch/main/BauSystem/hotkeys.lua"

View File

@ -0,0 +1,5 @@
{
"translationKey": "sc",
"main": false,
"ranked": false
}

View File

@ -62,7 +62,7 @@ Manuelle Kanonen dürfen vor dem Kampfgeschehen kein TNT beinhalten. Diese werde
### Automatische Kanonen
Automatische Kanonen sind Kanonen, welche vor dem Kampfgeschehen TNT beinhalten und ohne nachgeladen zu werden mehrere Schüsse abgeben können. Zu beachten ist, dass die Projektile aller Schüsse immer von dem/den exakt gleichen Punkt-/en aus abgeschossen werden müssen. Vor Fightbeginn dürfen automatische Kanonen vollständig leergeschossen werden. Automatische Kanonen müssen von der Kommandozentrale aus aktivierbar sein. Dies kann auch durch den Verbau des Autostarters innerhalb der Kommandozentrale erfolgen. Des weiteren muss eine Möglichkeit innerhalb der Kommandozentrale gegeben sein die automatische Kanone vollständig vor Fightbeginn leerschießen zu können.
Automatische Kanonen sind Kanonen, welche vor dem Kampfgeschehen TNT beinhalten und ohne nachgeladen zu werden mehrere Schüsse abgeben können. Zu beachten ist, dass die Projektile aller Schüsse immer von dem/den exakt gleichen Punkt-/en aus gezündet und abgeschossen werden müssen. Außerdem müssen alle Treibladungen am dem/den exakt gleichen Punkt-/en gezündet werden. Vor Fightbeginn dürfen automatische Kanonen vollständig leergeschossen werden. Automatische Kanonen müssen von der Kommandozentrale aus aktivierbar sein. Dies kann auch durch den Verbau des Autostarters innerhalb der Kommandozentrale erfolgen. Des weiteren muss eine Möglichkeit innerhalb der Kommandozentrale gegeben sein die automatische Kanone vollständig vor Fightbeginn leerschießen zu können.
Das wiederverwenden des Abschusswinkels einer Automatischen Kanone zählt immer als zweite Kanone. Auch das Nachladen der Automatischen Kanone zählt als zweite Kanone.

View File

@ -7,75 +7,115 @@ mode: warship
## Definitionen
### WarShip
### §1 WarShip
Ein WarShip ist eine bewaffnete, schwimmende Struktur in Minecraft mit der optischen Erscheinung eines Schiffes. Der Schwimmkörper muss dabei einen Großteil des WarShips ausmachen. Ein WarShip kann optional ein Design aufweisen, das andere im Wasser schwimmende/befindliche Dinge oder Tiere repräsentiert, sofern das gewählte Design gänzlich implementiert wird. Jedes WarShip muss beidseitig gleich bewaffnet sein.
1. Ein WarShip ist eine bewaffnete, schwimmende Struktur mit der optischen Erscheinung eines Schiffes. Der Schwimmkörper muss dabei einen Großteil des WarShips ausmachen.
2. Ein WarShip kann optional ein Design aufweisen, das andere im Wasser schwimmende/befindliche Dinge oder Tiere repräsentiert, sofern das gewählte Design gänzlich implementiert wird.
3. Jedes WarShip muss beidseitig gleich bewaffnet sein.
### Projektil
### §2 Projektil
Ein Projektil ist ein TNT, welches im gezündeten Zustand in die gegnerische Hälfte wechselt. Projektile müssen auf der dem Gegner zugewandten Schiffsseite gezündet werden. Unter Wasser gezündete Projektile müssen innerhalb des Technikbereiches, oberhalb der Wasserlinie gezündete Projektile innerhalb des Ausfahrbereichs gezündet werden.
1. Ein Projektil ist ein TNT, welches im gezündeten Zustand in die gegnerische Hälfte wechselt.
2. Projektile müssen auf der dem Gegner zugewandten Schiffsseite gezündet werden.
3. Die Zündung eines Projektils muss innerhalb des Technikbereiches stattfinden, über der Wasserlinie darf es auch innerhalb des Ausfahrbereichs gezündet werden.
### Kanone
### §3 Kanone
Eine Kanone ist eine Vorrichtung zum Beschleunigen von Projektilen. Eine Kanone darf maximal 2 Projektile verschießen. Eine Kanone muss manuell beladen werden. Eine Kanone darf maximal alle 2s schießen. Es dürfen maximal 32 Kanonen pro Seite verbaut werden. Kanonen sind der einzige Ort, an dem Wasser verbaut werden darf, wenn es keinen anderen Zweck hat, als diese vor selbst verursachten Schaden zu bewahren und TNT zu transportieren.
1. Eine Kanone ist eine Vorrichtung zum Beschleunigen von maximal 2 Projektilen.
2. Eine Kanone muss manuell beladen werden und darf maximal alle 2s schießen.
3. Kanonen dürfen nicht gezielt Projektile anderer Kanonen beeinflussen.
## Maße
### §4 Raketen und Flugmaschinen
- Länge: 230 Block
- Breite: 35 Block (+ 4 Block Design pro Seite)
- Höhe: 30 Block + 20 Block Design
- Tiefe: Bis zu 8 Block unter dem Meeresspiegel
1. Flugmaschinen sind automatisch bewegliche Blockkonstruktion, welche die Ausfahrmaße des WarShips verlassen.
2. Raketen sind mit TNT bestückte Flugmaschinen
3. Ein Raketenmagazin enthält mehrere Raketen und schickt sie auf nahezu gleicher Flugbahn zum Gegner.
4. Raketen und Flugmaschinen dürfen sich während des Fluges nicht in mehrere flugfähige Einheiten aufteilen.
Bei jedem WarShip müssen sich mindestens 10% der absoluten Blöcke (45.000 Blöcke) über der Wasserlinie befinden.
### §5 Brücke
Im Designbereich dürfen sich keine kampfrelevanten (Kanonen, Schleim/Honigfahrzeuge, Schilde) Techniken befinden. Eine Durchpanzerung des Designbereiches ist nicht zulässig. Der Designbereich ist ausschließlich für einzelne überstehende Designobjekte wie beispielsweise Kanonenrohre, Segel, Banner oder Bullaugen da.
1. Eine Brücke ist der optisch zentrale Kontroll- und Steuerbereich eines Schiffes.
2. Die Brücke muss folgende Anforderung erfüllen:
## Blöcke
- Mindestens 50 begehbare Blöcke.
- Mindesthöhe von 2 Blöcken im gesamten Brückenraum
- Steuerung von mindestens zwei zum Gegner gerichteten Scheinwerfern
- Optische Brückeneinrichtung (z. B. Instrumente, Anzeigen)
Es dürfen nur Blöcke mit einem TNT-Widerstand von maximal 6 verbaut werden. Vor und im Kampf dürfen sich in allen Blöcken mit Inventar nur Blumen, Honigflaschen und Pferderüstungen befinden, in Kisten und Fässer darf auch TNT sein. Es dürfen maximal 32 Werfer pro Seite verbaut werden. In Werfern dürfen sich 2×64 Feuerbälle oder 2×64 Pfeile (ohne Effekte) befinden.
### §6 Bereiche
Das Verbauen von unsichtbaren Mauern oder anderen unsichtbaren Blöcken mit Hitbox ist verboten.
Ebenso verboten sind: Monsterspawner, Eis, Netherportalblock, Alle Silberfischsteine.
1. Der Technikbereich ist der einzige Bereich, der im nicht ausgefahrenen Zustand, kampfrelevante Technik enthalten darf.
2. Der Designbereich ist ausschließlich für einzelne überstehende Designobjekte wie beispielsweise Kanonenrohre, Segel, Banner oder Bullaugen da.
3. Eine Durchpanzerung des Designbereiches ist nicht zulässig.
4. Ausfahrbereich wird in §8 Abs. 2 definiert.
Das Missbrauchen von unverschiebbaren Blöcken als Panzerung ist verboten.
Wasser darf nicht zum Schutz des eigenen WarShips missbraucht werden.
## Parameter
## Design
### §7 Ausstattung
Größere Hohlräume im Rumpf zum Ausweichen feindlicher Schüsse sind nicht gestattet, auch nicht während des Kampfes. Jedes WarShip braucht eine Flagge.
1. Es dürfen maximal,
Ein WarShip benötigt einen fortbewegungsfähigen Rumpf mit entsprechendem Antrieb (z.B. Segel, Schiffsschrauben).
Der Rumpf muss mindestens einen Block tief unter Wasser sowie mindestens 5 Block über dem Meeresspiegel sein (dies gilt auch während des Kampfes relativ zur Wasseroberfläche). Der Rumpf wird ab Wasserienie gemessen und darf max. 16 Blöcke hoch sein.
- 6 Flugmaschinen, (Flugmaschinen die dazu dienen, TNT von einigen Raketen zu zünden oder andere eigene Flugmaschinen zu stoppen, werden nicht gezählt)
- 8 Raketen (separat von den Flugmaschinen gezählt), wobei ein Raketenmagazin als 2 Raketen zählt,
- 28 Kanonen,
- 32 Werfer,
pro Seite verbaut werden.
Jedes WarShip benötigt eine Brücke, welche die folgenden Kriterien erfüllt:
2. Es dürften maximal 1000 der vorverbauten Slime-, Honig- & TNT-Blöcke den Ausfahrbereich, als Bestandteil von Flugmaschinen, verlassen.
3. Jedes WarShip benötigt eine Brücke.
4. Jedes WarShip braucht eine Flagge.
- Min. 50 begehbare Blöcke
- Min. 2 Block hoch im gesamten Brückenraum
- Ansteuerung von min. zwei zum Gegner gewandten Scheinwerfer
- Optische Brückeneinrichtung
### §8 Maße
## Anti-Lag-Regeln
1. Die Maße eines Schiffs dürfen folgende Werte nicht überschreiten:
Clocks müssen sich mit Ende ihres Einsatzzweckes selbst abschalten.
- Länge: 175 Blöcke
- Breite: 31 Blöcke + 4 Blöcke Designbereich pro Seite
- Höhe über dem Meeresspiegel: 30 Blöcke + 20 Blöcke Designbereich
- Tiefe unter dem Meeresspiegel: 8 Blöcke
Sämtliche Redstonetechnik zum Schutz des eigenen WarShips muss ihre Aktivität vor dem Verteilen der Kits eingestellt haben.
2. Ein WarShip darf sich maximal 12 Blöcke vom Technikbereich ausfahren. Ausgenommen davon sind Flugmaschinen. Dieser Bereich wird als Ausfahrbereich deklariert.
3. Bei jedem WarShip müssen sich mindestens 10% der absoluten Blöcke (30.000 Blöcke) über der Wasserlinie befinden.
## Raketen und Flugmaschinen
### §9 Blöcke
Ein WarShip darf sich maximal 12 Block vom Technikbereich an weit ausfahren, davon ausgenommen sind Raketen und Flugmaschinen. Raketen und Flugmaschinen dürfen sich im Flug nicht in mehrere Schleim/Honigfahrzeuge aufteilen.
1. Es dürfen nur Blöcke mit einem TNT-Widerstand von maximal 6 verbaut werden.
2. Inventarblöcke dürfen nur Honigflaschen, Pferderüstungen und Blumen enthalten.
1. In Kisten und Fässern darf sich auch TNT befinden.
1. Werfer dürfen maximal 2 Stacks (2 × 64) Feuerbälle und Pfeile (ohne Effekte) enthalten. (Eimer innerhalb von Werfern der in Powder Snow schaut)
3. Es ist verboten, Monsterspawner, Eis, Netherportalblock, alle Silberfischsteine und unsichtbare Blöcke mit Hitboxen zu verbauen.
4. Der Missbrauch von nicht verschiebbaren Blöcken als Panzerung ist nicht gestattet.
5. Wasser darf nur in Kanonen zum Schutz vor Selbstbeschädigung oder um TNT zu transporieren verbaut werden.
1. Wasser, Spinnennetze oder Pulverschnee darf nicht zum Schutz des eigenen WarShips missbraucht werden.
Flugmaschinen sind Schleim/Honigfahrzeuge, welche die Ausfahrmaße des WarShips verlassen und kein TNT zum Gegner transportieren. Flugmaschinen, welche TNT von Raketen zünden oder Flugmaschinen und/oder Raketen stoppen werden bei der Bestimmung der Anzahl nicht gezählt. Es dürfen maximal 8 Flugmaschinen pro Seite verbaut werden.
### §10 Design
Eine Rakete ist ein Schleim/Honigfahrzeug, das TNT zum Gegner transportiert. Es dürfen maximal 12 Raketen pro Seite verbaut werden. Ein Raketenmagazin ist in der Lage, mehrere Raketen auf der nahezu gleichen Flugbahn zum Gegner zu schicken. Ein Raketenmagazin wird wie 2 Raketen gewertet.
1. Ein WarShip benötigt einen fortbewegungsfähigen Rumpf mit entsprechendem Antrieb (z.B. Segel, Schiffsschrauben).
2. Der Rumpf muss mindestens 1 Block unter Wasser und zwischen 5 und maximal 16 Blöcken über dem Meeresspiegel liegen (auch während des Kampfes, relativ zur Wasseroberfläche).
3. Größere Hohlräume im Rumpf zum Ausweichen feindlicher Schüsse sind nicht gestattet, auch nicht während des Kampfes.
Es dürften maximal 1000 der vorverbauten Slime- + Honig- + TNT-Blöcke das WarShip in Flugmaschinen und Raketen verlassen.
### §11 Anti-Lag-Regeln
## Kampfablauf
1. Clocks müssen sich mit Ende ihres Einsatzzweckes selbst abschalten.
2. Sämtliche Redstone Technik zum Schutz des eigenen WarShips muss ihre Aktivität vor dem Verteilen der Kits eingestellt haben.
60 Sekunden vor Kampfbeginn können Flugmaschinen und Raketen das eigene WarShip verlassen. Mit Kampfbeginn dürfen Blöcke abgebaut und platziert werden; TNT-Schaden wird aktiviert. 10 Minuten nach Kampfbeginn wird das Entern des feindlichen WarShips erlaubt. Spieler mit einem Kit, welches TNT beladen kann und nicht dem Kapitän zugeordnet ist, dürfen erst 15 Minuten nach Kampfbeginn entern.
### §12 Kampfablauf
Der Kampf endet, wenn:
1. 60 Sekunden vor Kampfbeginn können Flugmaschinen und Raketen das eigene WarShip verlassen.
2. Mit Kampfbeginn dürfen Blöcke abgebaut und platziert werden. Der TNT-Schaden wird aktiviert.
3. 10 Minuten nach Kampfbeginn wird das Entern des feindlichen WarShips erlaubt. Spieler mit einem Kit, welches TNT beladen kann und nicht dem Kapitän zugeordnet ist, dürfen erst 15 Minuten nach Kampfbeginn entern.
4. Der Kampf endet,
- Der Kampf länger als 20 Minuten dauert
- Der Anführer eines Teams tot ist
- Ein WarShip zu 7% beschädigt wurde
- wenn er länger als 20 Minuten dauert.
- wenn der Kapitän eines Teams tot ist.
- wenn ein WarShip zu 7% beschädigt wurde.
### §13 Siegeskriterium
1. Folgende drei Szenarien führen zum Sieg:
- Das erfolgreiche eliminieren des gegnerischen Kapitäns.
- Eine Beschädigung des gegnerischen WarShips von mindestens 7%.
- Das nach Ablauf der Zeit das weniger beschädigte Schiff (prozentual).
2. Sollte keines dieser Szenarien eintreffen, endet der Kampf in einem Unentschieden.

View File

@ -209,6 +209,7 @@
}
},
"login": {
"page": "SteamWar - Login",
"title": "Login",
"placeholder": {
"username": "Nutzername...",
@ -219,7 +220,7 @@
"password": "Passwort",
"repeat": "Passwort Wiederholen"
},
"setPassword": "Wie setzte ich mein Passwort?",
"setPassword": "Wie setze ich mein Passwort?",
"submit": "Login",
"error": "Falscher Nutzername oder falsches Passwort"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@ -1,7 +1,7 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 SteamWar.de-Serverteam
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by

View File

@ -22,10 +22,7 @@ import { fontFamily } from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
const config = {
darkMode: ["class"],
content: [
"./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}",
"./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}",
],
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}", "./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}"],
safelist: ["dark"],
theme: {
container: {