diff --git a/app.go b/app.go index 90aa879..13edb4d 100644 --- a/app.go +++ b/app.go @@ -463,6 +463,17 @@ func (a *App) PickRLBotToml() (string, error) { return "", fmt.Errorf("invalid file name") } +func (a *App) PickToml() (string, error) { + path, err := zenity.SelectFile(zenity.FileFilter{ + Name: ".toml files", + Patterns: []string{"*.toml"}, + }) + if err != nil { + return "", nil + } + return path, nil +} + func (a *App) ShowPathInExplorer(path string) error { fileInfo, err := os.Stat(path) if err != nil { diff --git a/frontend/src/assets/cannot_play.svg b/frontend/src/assets/cannot_play.svg new file mode 100644 index 0000000..942cd3c --- /dev/null +++ b/frontend/src/assets/cannot_play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/base-players.ts b/frontend/src/base-players.ts index 7c14dbe..8ab8d03 100644 --- a/frontend/src/base-players.ts +++ b/frontend/src/base-players.ts @@ -9,41 +9,50 @@ export const BASE_PLAYERS: DraggablePlayer[] = [ id: crypto.randomUUID(), player: new HumanInfo(), tags: ["human"], + overrides: { name: "Human", loadout: null, autoStart: true }, }, { displayName: "Psyonix Beginner", icon: "", id: crypto.randomUUID(), player: new PsyonixBotInfo({ + name: "", skill: 0, }), tags: ["psyonix"], + overrides: { name: "", loadout: null, autoStart: true }, }, { displayName: "Psyonix Rookie", icon: "", id: crypto.randomUUID(), player: new PsyonixBotInfo({ + name: "", skill: 1, }), tags: ["psyonix"], + overrides: { name: "", loadout: null, autoStart: true }, }, { displayName: "Psyonix Pro", icon: "", id: crypto.randomUUID(), player: new PsyonixBotInfo({ + name: "", skill: 2, }), tags: ["psyonix"], + overrides: { name: "", loadout: null, autoStart: true }, }, { displayName: "Psyonix Allstar", icon: "", id: crypto.randomUUID(), player: new PsyonixBotInfo({ + name: "", skill: 3, }), tags: ["psyonix"], + overrides: { name: "", loadout: null, autoStart: true }, }, ]; diff --git a/frontend/src/components/Teams/Main.svelte b/frontend/src/components/Teams/Main.svelte index f0624eb..018cff4 100644 --- a/frontend/src/components/Teams/Main.svelte +++ b/frontend/src/components/Teams/Main.svelte @@ -5,10 +5,12 @@ let { bluePlayers = $bindable(), orangePlayers = $bindable(), selectedTeam = $bindable(null), + globalAutoStart = $bindable(true), }: { bluePlayers: any[]; orangePlayers: any[]; selectedTeam: "blue" | "orange" | null; + globalAutoStart: boolean; } = $props(); function toggleTeam(team: "blue" | "orange") { @@ -29,7 +31,7 @@ function toggleTeam(team: "blue" | "orange") {

{bluePlayers?.length || 0} bots

- +
toggleTeam('orange')} class:selected={selectedTeam === 'orange'}>
@@ -37,7 +39,7 @@ function toggleTeam(team: "blue" | "orange") {

{orangePlayers?.length || 0} bots

- +
diff --git a/frontend/src/components/Teams/PlayerOverridesModal.svelte b/frontend/src/components/Teams/PlayerOverridesModal.svelte new file mode 100644 index 0000000..d00a40d --- /dev/null +++ b/frontend/src/components/Teams/PlayerOverridesModal.svelte @@ -0,0 +1,77 @@ + + +{#if player} + +

In-game name:

+ +
+
+ {#if player.player instanceof BotInfo || player.player instanceof PsyonixBotInfo} +

Loadout: {player.overrides.loadout ? "Customized" : ""}

+ +
+
+ {/if} + {#if player.player instanceof BotInfo} + + + {/if} +
+
+ +
+
+{/if} + + \ No newline at end of file diff --git a/frontend/src/components/Teams/TeamBotList.svelte b/frontend/src/components/Teams/TeamBotList.svelte index 89ddcfb..0b84828 100644 --- a/frontend/src/components/Teams/TeamBotList.svelte +++ b/frontend/src/components/Teams/TeamBotList.svelte @@ -5,14 +5,21 @@ import { untrack } from "svelte"; import { flip } from "svelte/animate"; import { fade } from "svelte/transition"; import type { DraggablePlayer } from "../.."; -import { BotInfo } from "../../../bindings/gui"; +import {BotInfo, PsyonixBotInfo} from "../../../bindings/gui"; import closeIcon from "../../assets/close.svg"; import duplicateIcon from "../../assets/duplicate.svg"; import editIcon from "../../assets/edit.svg"; +import cannotAutoRunIcon from "../../assets/cannot_play.svg"; import defaultIcon from "../../assets/rlbot_mono.png"; -import ModalPrompt from "../ModalPrompt.svelte"; +import PlayerOverridesModal from "./PlayerOverridesModal.svelte"; -let { items = $bindable() }: { items: DraggablePlayer[] } = $props(); +let { + items = $bindable(), + globalAutoStart = $bindable(true) +}: { + items: DraggablePlayer[], + globalAutoStart: boolean, +} = $props(); function remove(id: string): any { items = items.filter((x) => x.id !== id); @@ -24,61 +31,41 @@ function dupe(id: string): any { // Create a copy with a new ID const newItem = { ...items[index], - id: crypto.randomUUID(), // Generate a new unique ID + overrides: {...items[index].overrides}, + id: crypto.randomUUID() // Generate a new unique ID }; // Insert the new item after the original items = [...items.slice(0, index + 1), newItem, ...items.slice(index + 1)]; } } -let editPrompts: { [key: string]: ModalPrompt } = {}; -let editDataNames: { [key: string]: string } = $state({}); +let editedPlayer: DraggablePlayer | undefined = $state(undefined) +let showEditPrompt = $state(false) -// Update editDataNames once items updates, but try to keep as much state as possible -// Once editing more stuff is supported, this approach should probably change -$effect(() => { - let localEditDataNames = untrack(() => { - return editDataNames; - }); - let updated = Object.fromEntries( - items - .filter((x) => { - return x.player instanceof BotInfo; - }) - .map((bot) => { - return [ - bot.id, - // use ! + ?: instead of ?? so that "" is treated as null - // this is due to the input binding value to "" before this is ran - !localEditDataNames[bot.id] - ? (bot.player).config.settings.name - : localEditDataNames[bot.id], - ]; - }), - ); - untrack(() => { - editDataNames = updated; - }); -}); - -async function edit_custom_bot(id: string): Promise { - let modal = editPrompts[id]; +async function showEditModal(d: DraggablePlayer) { + if (d) { + editedPlayer = d + showEditPrompt = true; + } +} - let modified = await modal.prompt(); - if (modified) { - const index = items.findIndex((x) => x.id === id); - let copy = { - ...items[index], - // We need to deepclone here to make sure we don't modify the player globally. - // We also need to do BotInfo.createFrom to make sure that instanceof BotInfo == true still. - player: BotInfo.createFrom(structuredClone(items[index].player)), - }; - copy.modified = true; - if (copy.player instanceof BotInfo) { - copy.player.config.settings.name = editDataNames[id]; - } - items[index] = copy; +function reasonsForManualStart(d: DraggablePlayer): string[] { + let reasons: string[] = [] + if (d.player instanceof BotInfo) { + if (!globalAutoStart) reasons.push("Auto-start disabled in Extra"); + if (!d.player.config.settings.runCommand) reasons.push("No run_command declared"); + if (!d.overrides.autoStart) reasons.push("Auto-start disabled for bot"); } + return reasons +} + +function canAutoStart(d: DraggablePlayer): boolean { + return reasonsForManualStart(d).length == 0; +} + +function hasOverrides(d: DraggablePlayer): boolean { + let expectedName = (d.player instanceof BotInfo) ? d.displayName : ""; + return d.overrides.name !== expectedName || !d.overrides.autoStart || d.overrides.loadout != null } // :fire: :fire: :fire: @@ -157,7 +144,7 @@ const dnd_container_namespace = `team_${crypto.randomUUID()}`; }} animate:flip={{ duration: 100 }} in:fade={{ duration: 100 }} - out:fade={{ duration: 100 }} + out:fade={{ duration: 100 }} >
e.stopPropagation()} > icon -

{bot.displayName}

+

{bot.displayName === bot.overrides.name || (bot.player instanceof PsyonixBotInfo && bot.overrides.name === "") ? bot.displayName : `${bot.overrides.name}`}

{#if bot.uniquePathSegment} ({bot.uniquePathSegment}) {/if} -
- {#if bot.player instanceof BotInfo} - + {#if !canAutoStart(bot)} + No auto-run {/if} - +
{#if !bot.tags.includes("human")} {/if} + {#if bot.player instanceof BotInfo || bot.player instanceof PsyonixBotInfo} + + {/if} @@ -202,21 +188,7 @@ const dnd_container_namespace = `team_${crypto.randomUUID()}`;
-{#each items as bot (bot.id)} - -
- - - - -
-
-{/each} +