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()}
>

-
{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)}
+

{/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}
+