Implement battles

This commit is contained in:
thepaperpilot 2023-02-19 17:39:40 -06:00
parent eb08ec9976
commit 75e2e3e2ae
6 changed files with 588 additions and 126 deletions

View file

@ -14,7 +14,8 @@
empty: character == null && selected == null, empty: character == null && selected == null,
dragging, dragging,
isDragging, isDragging,
draggingOver draggingOver,
shake
}" }"
draggable="true" draggable="true"
:ondragstart="() => (dragging = true)" :ondragstart="() => (dragging = true)"
@ -105,6 +106,7 @@ defineProps<{
isShop?: boolean; isShop?: boolean;
isDragging?: boolean; isDragging?: boolean;
selected?: Character | null; selected?: Character | null;
shake?: boolean;
}>(); }>();
const dragging = ref(false); const dragging = ref(false);
@ -139,6 +141,10 @@ watch(dragging, dragging => {
cursor: pointer; cursor: pointer;
} }
.character.shake {
animation: shake 0.5s infinite;
}
.character * { .character * {
pointer-events: none; pointer-events: none;
} }
@ -232,8 +238,9 @@ watch(dragging, dragging => {
.level-display { .level-display {
position: absolute; position: absolute;
bottom: -15%; bottom: -5%;
right: -5%; right: -5%;
z-index: 1;
transform: translate(50%, 50%); transform: translate(50%, 50%);
} }
@ -270,4 +277,40 @@ watch(dragging, dragging => {
transform: translateX(-50%) rotate(180deg) translateY(0%); transform: translateX(-50%) rotate(180deg) translateY(0%);
} }
} }
@keyframes shake {
0% {
transform: translate(1px, 1px) rotate(0deg);
}
10% {
transform: translate(-1px, -2px) rotate(-1deg);
}
20% {
transform: translate(-3px, 0px) rotate(1deg);
}
30% {
transform: translate(3px, 2px) rotate(0deg);
}
40% {
transform: translate(1px, -1px) rotate(1deg);
}
50% {
transform: translate(-1px, 2px) rotate(-1deg);
}
60% {
transform: translate(-3px, 1px) rotate(0deg);
}
70% {
transform: translate(3px, 1px) rotate(-1deg);
}
80% {
transform: translate(-1px, -1px) rotate(1deg);
}
90% {
transform: translate(1px, 2px) rotate(0deg);
}
100% {
transform: translate(1px, -2px) rotate(-1deg);
}
}
</style> </style>

View file

@ -15,8 +15,7 @@
} }
.resource-box { .resource-box {
margin-right: 20px !important; display: flex;
font-size: large;
border: solid 2px var(--bought); border: solid 2px var(--bought);
padding: 0 4px; padding: 0 4px;
border-radius: 4px; border-radius: 4px;
@ -24,6 +23,10 @@
align-items: center; align-items: center;
} }
.resource-box:not(:last-child) {
margin-right: 20px !important;
}
.resource-box .material-icons { .resource-box .material-icons {
margin-right: 6px; margin-right: 6px;
font-size: 1em; font-size: 1em;
@ -56,3 +59,144 @@
display: flex; display: flex;
align-items: center; align-items: center;
} }
.battle-container {
display: flex;
flex-direction: column;
height: 100%
}
.battle-container:not(.fast) * {
transition-duration: 1s;
}
.teams-container {
display: flex;
height: 100%;
width: 100%;
}
.team-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
margin: 0 4vmin;
}
.stream-container {
aspect-ratio: 16/9;
width: calc(100% - 4px);
margin: 0 auto;
border: solid 2px var(--accent1);
position: relative;
background: var(--background);
filter: drop-shadow(2px 4px 6px black);
}
.stream-details {
position: absolute;
top: 1vmin;
display: flex;
flex-direction: column;
}
.stream-details .stats {
display: flex;
margin-top: 1vmin;
}
.view-counter {
font-size: 2vmin;
position: absolute;
top: 1vmin;
}
.streamers-container {
width: 90%;
height: 18vmin;
position: absolute;
left: 5%;
bottom: 5%;
overflow: hidden;
}
.members-container {
height: 18vmin;
overflow: hidden;
flex-shrink: 0;
}
.streamers-container .row,
.members-container .row {
flex-flow: row
}
.streamers-container .character {
margin: 4vmin 1vmin;
}
.battle-controls {
margin-bottom: 2vmin;
display: flex;
}
.battle-controls .button {
display: flex;
flex-direction: column;
font-size: 2vmin;
margin: 1vmin;
border: solid 2px var(--link);
border-radius: var(--border-radius);
}
.battle-controls .button.active {
background: var(--link);
color: var(--feature-foreground);
}
.outcome {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 2vmin 4vmin;
background: var(--tooltip-background);
border-radius: 4vmin;
font-size: 6vmin;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
}
.outcome span {
margin: 1vmin;
}
.character-transition-enter-from {
transform: translateY(100%);
}
.character-transition-leave-to {
transform: translateY(-100%);
}
.character-trensition-active {
position: absolute;
}
@media (orientation: portrait) {
.teams-container {
flex-direction: column;
}
.team-container {
margin: 4vmin 0;
}
.stream-container {
width: unset;
height: 100%;
}
}

View file

@ -4,7 +4,7 @@ import { jsx } from "features/feature";
import type { BaseLayer, GenericLayer } from "game/layers"; import type { BaseLayer, GenericLayer } from "game/layers";
import { createLayer } from "game/layers"; import { createLayer } from "game/layers";
import type { Player } from "game/player"; import type { Player } from "game/player";
import { computed, ref } from "vue"; import { computed, ref, Transition, TransitionGroup } from "vue";
import CharacterSlot from "./CharacterSlot.vue"; import CharacterSlot from "./CharacterSlot.vue";
import "./socket"; import "./socket";
import "./common.css"; import "./common.css";
@ -20,6 +20,7 @@ import vespa from "../../public/Vespa Coots.png";
import heart from "../../public/Heart.png"; import heart from "../../public/Heart.png";
import startStream from "../../public/start stream.png"; import startStream from "../../public/start stream.png";
import { createReset } from "features/reset"; import { createReset } from "features/reset";
import settings from "game/settings";
export const characters: Record<string, CharacterInfo> = { export const characters: Record<string, CharacterInfo> = {
coots: { coots: {
@ -85,6 +86,49 @@ export const main = createLayer("main", function (this: BaseLayer) {
const selectedCharacter = ref<number | null>(null); const selectedCharacter = ref<number | null>(null);
const selectedShopItem = ref<number | null>(null); const selectedShopItem = ref<number | null>(null);
const findingMatch = ref<boolean>(false); const findingMatch = ref<boolean>(false);
const outcome = ref<BattleOutcome | "">("");
const showingOutcome = ref<boolean>(false);
const previewing = ref<boolean>(false);
const playClicked = ref<boolean>(false);
const queue = ref<
{
action: "join";
}[]
>([]);
const battle = ref<{
team: Character[];
streamers: Character[];
enemyTeam: Character[];
enemyStreamers: Character[];
enemyNickname: string;
enemyLives: number;
enemyWins: number;
enemyTurn: number;
} | null>(null);
const views = computed(() => {
if (battle.value == null) {
return 0;
}
return (
battle.value.streamers.reduce(
(acc, curr) => acc + Math.max(0, curr.presence) * Math.max(0, curr.relevancy),
0
) * 100
);
});
const enemyViews = computed(() => {
if (battle.value == null) {
return 0;
}
return (
battle.value.enemyStreamers.reduce(
(acc, curr) => acc + Math.max(0, curr.presence) * Math.max(0, curr.relevancy),
0
) * 100
);
});
const reset = createReset(() => ({ const reset = createReset(() => ({
onReset() { onReset() {
@ -97,128 +141,331 @@ export const main = createLayer("main", function (this: BaseLayer) {
selectedCharacter.value = null; selectedCharacter.value = null;
selectedShopItem.value = null; selectedShopItem.value = null;
findingMatch.value = false; findingMatch.value = false;
battle.value = null;
outcome.value = "";
showingOutcome.value = false;
playClicked.value = false;
queue.value = [];
} }
})); }));
const isDragging = ref(false); const isDragging = ref(false);
function prepareMove() {
if (battle.value == null) {
throw "Preparing move while not in battle";
}
if (
queue.value.length === 0 &&
battle.value.team.length === 0 &&
battle.value.enemyTeam.length === 0
) {
if (outcome.value === "Victory") {
wins.value++;
} else if (outcome.value === "Defeat") {
lives.value--;
}
showingOutcome.value = true;
return;
}
if (queue.value.length === 0) {
queue.value.push({
action: "join"
});
}
if (settings.autoplay === false && playClicked.value === false) {
previewing.value = true;
} else {
previewing.value = false;
const action = queue.value.shift()!;
switch (action.action) {
case "join":
if ((battle.value.team.length ?? 0) > 0) {
battle.value.streamers.push(battle.value.team.pop()!);
}
if ((battle.value.enemyTeam.length ?? 0) > 0) {
battle.value.enemyStreamers.push(battle.value.enemyTeam.pop()!);
}
break;
}
playClicked.value = false;
setTimeout(prepareMove, settings.fast ? 750 : 1250);
}
}
return { return {
name: "Game", name: "Game",
minimizable: false, minimizable: false,
display: jsx(() => ( display: jsx(() => {
<div if (battle.value != null) {
class="game-container" return (
style={findingMatch.value ? "pointer-events: none" : ""} <div class={{ ["battle-container"]: true, fast: settings.fast }}>
onClick={() => { <div class="battle-controls">
selectedCharacter.value = null; <button
selectedShopItem.value = null; class="button"
}} onClick={() => {
> playClicked.value = true;
<Row style="position: absolute; top: 10px; left: -5px"> prepareMove();
<div class="resource-box"> }}
<span class="material-icons">credit_card</span> >
{gold.value} <span class="material-icons">play_arrow</span>
<span>Play</span>
</button>
<button
class={{ button: true, active: settings.autoplay }}
onClick={() => {
settings.autoplay = !settings.autoplay;
if (previewing.value) {
prepareMove();
}
}}
>
<span class="material-icons">all_inclusive</span>
<span>Autoplay</span>
</button>
<button
class={{ button: true, active: settings.fast }}
onClick={() => (settings.fast = !settings.fast)}
>
<span class="material-icons">fast_forward</span>
<span>Fast</span>
</button>
</div>
<div
class="teams-container"
style={showingOutcome.value ? "pointer-events: none;" : ""}
>
<div class="team-container">
<div class="stream-container">
<div class="stream-details" style="left: 1vmin">
<span style="margin-left: 0">{nickname.value} (YOU)</span>
<div class="stats" style="margin-left: 0">
<div class="resource-box">
<img src={heart} />
<span>{lives.value}</span>
</div>
<div class="resource-box">
<span class="material-icons">emoji_events</span>
<span>{wins.value}/5</span>
</div>
</div>
</div>
<div class="view-counter" style="right: 1vmin">
{views.value} Views
</div>
<Row class="streamers-container">
<TransitionGroup name="character-transition">
{battle.value.streamers
.slice()
.reverse()
.map((streamer, i) => (
<CharacterSlot
key={battle.value!.streamers.length - i}
character={streamer}
/>
))}
</TransitionGroup>
</Row>
</div>
<Row class="members-container" style="margin-left: 0">
<TransitionGroup name="character-transition">
{battle.value.team.map((member, i) => (
<CharacterSlot
character={member}
key={i}
shake={
previewing.value &&
queue.value[0]?.action === "join" &&
member ===
battle.value?.team[
(battle.value?.team.length ?? 0) - 1
]
}
/>
))}
</TransitionGroup>
</Row>
</div>
<div class="team-container">
<div class="stream-container">
<div class="stream-details" style="right: 1vmin">
<span style="margin-right: 0">
{battle.value.enemyNickname}
</span>
<div class="stats" style="margin-right: 0">
<div class="resource-box">
<img src={heart} />
<span>{battle.value.enemyLives}</span>
</div>
<div class="resource-box">
<span class="material-icons">emoji_events</span>
<span>{battle.value.enemyWins}/5</span>
</div>
</div>
</div>
<div class="view-counter" style="left: 1vmin">
{enemyViews.value} Views
</div>
<Row class="streamers-container">
<TransitionGroup name="character-transition">
{battle.value.enemyStreamers.map((streamer, i) => (
<CharacterSlot key={i} character={streamer} />
))}
</TransitionGroup>
</Row>
</div>
<Row class="members-container" style="margin-right: 0">
<TransitionGroup name="character-transition">
{battle.value.enemyTeam
.slice()
.reverse()
.map((member, i) => (
<CharacterSlot
character={member}
key={battle.value!.enemyStreamers.length + i}
shake={
previewing.value &&
queue.value[0]?.action === "join" &&
member ===
battle.value?.enemyTeam[
(battle.value?.enemyTeam.length ??
0) - 1
]
}
/>
))}
</TransitionGroup>
</Row>
</div>
</div>
{showingOutcome.value ? (
<div class="outcome" onClick={() => emit("newTurn")}>
<span>{outcome.value}</span>
<span style="font-size: 2vmin">Next Turn</span>
</div>
) : null}
</div> </div>
<div class="resource-box"> );
<img src={heart} /> }
{lives.value}
</div> return (
<div class="resource-box"> <div
<span class="material-icons">emoji_events</span> class="game-container"
{wins.value}/5 style={findingMatch.value ? "pointer-events: none" : ""}
</div> onClick={() => {
</Row> selectedCharacter.value = null;
<h2 style="font-size: 3vmin">{nickname.value}</h2> selectedShopItem.value = null;
<Row style="margin-top: 10vh"> }}
{new Array(3).fill(0).map((_, i) => ( >
<CharacterSlot <Row style="position: absolute; top: 10px; left: -5px">
character={team.value[i]} <div class="resource-box">
isSelected={selectedCharacter.value === i} <span class="material-icons">credit_card</span>
selected={ {gold.value}
selectedCharacter.value == null </div>
? selectedShopItem.value == null || <div class="resource-box">
(team.value[i] != null && <img src={heart} />
shop.value[selectedShopItem.value]?.type !== {lives.value}
team.value[i]?.type) || </div>
gold.value < 3 <div class="resource-box">
? null <span class="material-icons">emoji_events</span>
: shop.value[selectedShopItem.value] {wins.value}/5
: team.value[selectedCharacter.value] </div>
} </Row>
isDragging={isDragging.value} <h2 style="font-size: 3vmin">{nickname.value}</h2>
onClick={clickCharacter(i)} <Row style="margin-top: 10vh">
onDragstart={() => { {new Array(3).fill(0).map((_, i) => (
isDragging.value = true; <CharacterSlot
selectedCharacter.value = i; character={team.value[i]}
selectedShopItem.value = null; isSelected={selectedCharacter.value === i}
}} selected={
onDragend={() => { selectedCharacter.value == null
isDragging.value = false; ? selectedShopItem.value == null ||
selectedCharacter.value = null; (team.value[i] != null &&
selectedShopItem.value = null; shop.value[selectedShopItem.value]?.type !==
}} team.value[i]?.type) ||
onDrop={() => clickCharacter(i)()} gold.value < 3
/> ? null
))} : shop.value[selectedShopItem.value]
</Row> : team.value[selectedCharacter.value]
<Row style="margin-top: 10vh"> }
<div isDragging={isDragging.value}
class="reroll" onClick={clickCharacter(i)}
style={gold.value > 0 ? "" : "color: var(--locked); cursor: not-allowed"} onDragstart={() => {
onClick={() => { isDragging.value = true;
if (gold.value > 0) { selectedCharacter.value = i;
emit("reroll"); selectedShopItem.value = null;
} }}
}} onDragend={() => {
> isDragging.value = false;
<span class="material-icons" style="font-size: 8vmin"> selectedCharacter.value = null;
casino selectedShopItem.value = null;
</span> }}
<span style="font-size: 2vmin">Roll</span> onDrop={() => clickCharacter(i)()}
</div> />
{shop.value.map((item, i) => ( ))}
<CharacterSlot </Row>
character={item == null ? undefined : item} <Row style="margin-top: 10vh">
isSelected={selectedShopItem.value === i} <div
isShop={true} class="reroll"
isDragging={isDragging.value} style={
onClick={(e: MouseEvent) => { gold.value > 0 ? "" : "color: var(--locked); cursor: not-allowed"
if (item == null) { }
return; onClick={() => {
if (gold.value > 0) {
emit("reroll");
} }
selectedShopItem.value = selectedShopItem.value === i ? null : i;
selectedCharacter.value = null;
e.stopPropagation();
}} }}
onDragstart={() => { >
isDragging.value = true; <span class="material-icons" style="font-size: 8vmin">
selectedCharacter.value = null; casino
selectedShopItem.value = i; </span>
}} <span style="font-size: 2vmin">Roll</span>
onDragend={() => { </div>
isDragging.value = false; {shop.value.map((item, i) => (
selectedCharacter.value = null; <CharacterSlot
selectedShopItem.value = null; character={item == null ? undefined : item}
isSelected={selectedShopItem.value === i}
isShop={true}
isDragging={isDragging.value}
onClick={(e: MouseEvent) => {
if (item == null) {
return;
}
selectedShopItem.value =
selectedShopItem.value === i ? null : i;
selectedCharacter.value = null;
e.stopPropagation();
}}
onDragstart={() => {
isDragging.value = true;
selectedCharacter.value = null;
selectedShopItem.value = i;
}}
onDragend={() => {
isDragging.value = false;
selectedCharacter.value = null;
selectedShopItem.value = null;
}}
/>
))}
</Row>
<Spacer height="4vh" />
{findingMatch.value ? (
<div class="waiting">Finding opposing team...</div>
) : (
<img
class="startStream"
draggable="false"
onClick={() => {
emit("stream");
findingMatch.value = true;
}} }}
src={startStream}
/> />
))} )}
</Row> </div>
<Spacer height="4vh" /> );
{findingMatch.value ? ( }),
<div class="waiting">Finding opposing team...</div>
) : (
<img
class="startStream"
draggable="false"
onClick={() => {
emit("stream");
findingMatch.value = true;
}}
src={startStream}
/>
)}
</div>
)),
lives, lives,
wins, wins,
turn, turn,
@ -228,7 +475,12 @@ export const main = createLayer("main", function (this: BaseLayer) {
selectedCharacter, selectedCharacter,
selectedShopItem, selectedShopItem,
findingMatch, findingMatch,
reset showingOutcome,
outcome,
reset,
battle,
playClicked,
prepareMove
}; };
}); });

View file

@ -105,6 +105,7 @@ function setupSocket(socket: Socket<ServerToClientEvents, ClientToServerEvents>)
socket.on("newTurn", shop => { socket.on("newTurn", shop => {
main.gold.value = 10; main.gold.value = 10;
main.turn.value++; main.turn.value++;
main.battle.value = null;
main.shop.value = shop.map(item => ({ main.shop.value = shop.map(item => ({
type: item, type: item,
relevancy: characters[item].initialRelevancy, relevancy: characters[item].initialRelevancy,
@ -135,15 +136,22 @@ function setupSocket(socket: Socket<ServerToClientEvents, ClientToServerEvents>)
main.team.value[index] = null; main.team.value[index] = null;
main.team.value[otherIndex] = char; main.team.value[otherIndex] = char;
}); });
socket.on("stream", (enemyTeam, enemyNickname, outcome) => { socket.on("stream", (enemy, outcome) => {
if (outcome === "Victory") {
main.wins.value++;
} else if (outcome === "Defeat") {
main.lives.value--;
}
main.findingMatch.value = false; main.findingMatch.value = false;
// TODO display combat main.battle.value = {
emit("newTurn"); team: JSON.parse(JSON.stringify(main.team.value.filter(m => m != null))),
streamers: [],
enemyTeam: enemy.team.filter(m => m != null) as Character[],
enemyStreamers: [],
enemyNickname: enemy.nickname,
enemyLives: enemy.lives,
enemyWins: enemy.wins,
enemyTurn: enemy.turn
};
main.outcome.value = outcome;
main.showingOutcome.value = false;
main.playClicked.value = false;
setTimeout(main.prepareMove, 1000);
}); });
} }

11
src/data/types.d.ts vendored
View file

@ -23,7 +23,16 @@ interface ServerToClientEvents {
buy: (shopIndex: number, teamIndex: number, char: Character) => void; buy: (shopIndex: number, teamIndex: number, char: Character) => void;
move: (index: number, otherIndex: number) => void; move: (index: number, otherIndex: number) => void;
merge: (shopIndex: number, teamIndex: number, char: Character) => void; merge: (shopIndex: number, teamIndex: number, char: Character) => void;
stream: (enemyTeam: (Character | null)[], nickname: string, outcome: BattleOutcome) => void; stream: (
enemy: {
team: (Character | null)[];
nickname: string;
lives: number;
wins: number;
turn: number;
},
outcome: BattleOutcome
) => void;
} }
interface ClientToServerEvents { interface ClientToServerEvents {

View file

@ -20,6 +20,8 @@ export interface Settings {
unthrottled: boolean; unthrottled: boolean;
/** Whether to align modifiers to the unit. */ /** Whether to align modifiers to the unit. */
alignUnits: boolean; alignUnits: boolean;
autoplay: boolean;
fast: boolean;
} }
const state = reactive<Partial<Settings>>({ const state = reactive<Partial<Settings>>({
@ -28,7 +30,9 @@ const state = reactive<Partial<Settings>>({
showTPS: true, showTPS: true,
theme: Themes.Nordic, theme: Themes.Nordic,
unthrottled: false, unthrottled: false,
alignUnits: false alignUnits: false,
autoplay: false,
fast: false
}); });
watch( watch(
@ -61,7 +65,9 @@ export const hardResetSettings = (window.hardResetSettings = () => {
saves: [], saves: [],
showTPS: true, showTPS: true,
theme: Themes.Nordic, theme: Themes.Nordic,
alignUnits: false alignUnits: false,
autoplay: false,
fast: false
}; };
globalBus.emit("loadSettings", settings); globalBus.emit("loadSettings", settings);
Object.assign(state, settings); Object.assign(state, settings);