Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/assets/cannot_play.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions frontend/src/base-players.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
},
];
6 changes: 4 additions & 2 deletions frontend/src/components/Teams/Main.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand All @@ -29,15 +31,15 @@ function toggleTeam(team: "blue" | "orange") {
<div style="flex: 1;"></div>
<h3 class="dimmed">{bluePlayers?.length || 0} bots</h3>
</header>
<TeamBotList bind:items={bluePlayers} />
<TeamBotList bind:items={bluePlayers} bind:globalAutoStart />
</div>
<div class="team box orange" onclick={() => toggleTeam('orange')} class:selected={selectedTeam === 'orange'}>
<header class="orange">
<h3>Orange team</h3>
<div style="flex: 1;"></div>
<h3 class="dimmed">{orangePlayers?.length || 0} bots</h3>
</header>
<TeamBotList bind:items={orangePlayers} />
<TeamBotList bind:items={orangePlayers} bind:globalAutoStart />
</div>
</div>

Expand Down
77 changes: 77 additions & 0 deletions frontend/src/components/Teams/PlayerOverridesModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<script lang="ts">
import Modal from "../Modal.svelte";
import type {DraggablePlayer} from "../../index";
import {App, BotInfo, PsyonixBotInfo} from "../../../bindings/gui";

let {
player = $bindable(undefined),
visible = $bindable(false)
}: {
player?: DraggablePlayer | null
visible?: boolean
} = $props();

function clearOverrides() {
if (player) {
player.overrides.name = player.player instanceof PsyonixBotInfo ? "" : player.displayName;
player.overrides.loadout = null;
player.overrides.autoStart = true;
}
}

function hasNameOverride(): boolean {
if (!player) return false;
let expectedName = player.player instanceof BotInfo ? player.displayName : "";
return player.overrides.name !== expectedName;
}

async function pickLoadoutOverride() {
if (!player) return;
let path = await App.PickToml();
if (!path) return;
player.overrides.loadout = await App.GetLoadout(path)
}

</script>

{#if player}
<Modal title={`Edit ${player.displayName}`} bind:visible >
<p style={hasNameOverride() ? "color: orange;" : ""}>In-game name:</p>
<input
type="text"
placeholder="Bot name"
id={`edit-name-${player.id}`}
bind:value={player.overrides.name}
>
<br />
<br />
{#if player.player instanceof BotInfo || player.player instanceof PsyonixBotInfo}
<p style={player.overrides.loadout ? "color: orange;" : ""}>Loadout: {player.overrides.loadout ? "Customized" : ""}</p>
<button onclick={pickLoadoutOverride}>Pick TOML</button>
<br />
<br />
{/if}
{#if player.player instanceof BotInfo}
<input
type="checkbox"
id={`edit-auto-start-${player.id}`}
bind:checked={player.overrides.autoStart}
>
<label for={`edit-auto-start-${player.id}`} style={player.overrides.autoStart ? "" : "color: orange;"}>Auto start</label>
{/if}
<hr>
<div class="buttons">
<button onclick={clearOverrides}>Clear Overrides</button>
</div>
</Modal>
{/if}

<style>
.buttons {
display: flex;
width: 100%;
justify-content: left;
gap: .5rem;
padding-top: 1rem;
}
</style>
138 changes: 62 additions & 76 deletions frontend/src/components/Teams/TeamBotList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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]
? (<BotInfo>bot.player).config.settings.name
: localEditDataNames[bot.id],
];
}),
);
untrack(() => {
editDataNames = updated;
});
});

async function edit_custom_bot(id: string): Promise<void> {
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:
Expand Down Expand Up @@ -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 }}
>
<div
class="bot"
Expand All @@ -170,25 +157,24 @@ const dnd_container_namespace = `team_${crypto.randomUUID()}`;
onclick={e => e.stopPropagation()}
>
<img src={bot.icon || defaultIcon} alt="icon" />
<p
style={bot.modified ? "color: orange" : ""}
title={bot.modified ? "(modified)" : undefined}
>{bot.displayName}</p>
<p>{bot.displayName === bot.overrides.name || (bot.player instanceof PsyonixBotInfo && bot.overrides.name === "") ? bot.displayName : `${bot.overrides.name}`}</p>
{#if bot.uniquePathSegment}
<span class="unique-bot-identifier">({bot.uniquePathSegment})</span>
{/if}
<div style="flex: 1;"></div>
{#if bot.player instanceof BotInfo}
<button class="edit" title="edit" onclick={edit_custom_bot.bind(null, bot.id)}>
<img src={editIcon} alt="Dupe">
</button>
{#if !canAutoStart(bot)}
<img class="bot-icon" src={cannotAutoRunIcon} alt="No auto-run" title={`Must be launched manually (${reasonsForManualStart(bot).join("; ")})`}>
{/if}
<!-- TODO: support editing psyonix bots too, skill level (and name?) -->
<div style="flex: 1;"></div>
{#if !bot.tags.includes("human")}
<button class="duplicate" title="Duplicate" onclick={dupe.bind(null, bot.id)}>
<img src={duplicateIcon} alt="Dupe">
</button>
{/if}
{#if bot.player instanceof BotInfo || bot.player instanceof PsyonixBotInfo}
<button class={hasOverrides(bot) ? "edit has-overrides" : "edit"} title="Edit" onclick={() => showEditModal(bot)}>
<img src={editIcon} alt="Edit">
</button>
{/if}
<button class="close" onclick={remove.bind(null, bot.id)}>
<img src={closeIcon} alt="X" />
</button>
Expand All @@ -202,21 +188,7 @@ const dnd_container_namespace = `team_${crypto.randomUUID()}`;
</div>
</div>

{#each items as bot (bot.id)}
<ModalPrompt title={"Edit " + bot.displayName} bind:this={editPrompts[bot.id]}>
<div style="display: flex; flex-direction: column;">
<label for={`edit-name-${bot.id}`}>Bot name (note: only in-game)</label>
<input
type="text"
placeholder="Bot name"
id={`edit-name-${bot.id}`}
bind:value={editDataNames[bot.id]}
>
<!-- TODO: Add a bunch of more stuff to edit, loadout etc -->
<!-- TODO: Perhaps add a way to save mods to bots as new tomls? -->
</div>
</ModalPrompt>
{/each}
<PlayerOverridesModal bind:player={editedPlayer} bind:visible={showEditPrompt} />

<style>
* {
Expand Down Expand Up @@ -282,19 +254,33 @@ const dnd_container_namespace = `team_${crypto.randomUUID()}`;
.bot:not(:hover) :is(.duplicate, .edit) {
visibility: hidden;
}
.has-overrides img {
visibility: visible;
}
.close, .duplicate, .edit {
background-color: transparent;
height: 100%;
padding: 0;
}
.duplicate {
padding: 0 .2rem;
padding: 0 .1rem;
}
:is(.close, .duplicate, .edit) img {
height: 100%;
width: auto;
filter: invert();
}
:is(.has-overrides) img {
filter: invert(68%) sepia(66%) saturate(2755%) hue-rotate(360deg) brightness(103%) contrast(104%);
}
.bot-icon {
background-color: transparent;
padding: 0 .1rem;
margin: .4rem 0;
height: 100%;
width: auto;
filter: invert(60%);
}
.unique-bot-identifier {
color: #868686;
}
Expand Down
Loading