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,
dragging,
isDragging,
draggingOver
draggingOver,
shake
}"
draggable="true"
:ondragstart="() => (dragging = true)"
@ -105,6 +106,7 @@ defineProps<{
isShop?: boolean;
isDragging?: boolean;
selected?: Character | null;
shake?: boolean;
}>();
const dragging = ref(false);
@ -139,6 +141,10 @@ watch(dragging, dragging => {
cursor: pointer;
}
.character.shake {
animation: shake 0.5s infinite;
}
.character * {
pointer-events: none;
}
@ -232,8 +238,9 @@ watch(dragging, dragging => {
.level-display {
position: absolute;
bottom: -15%;
bottom: -5%;
right: -5%;
z-index: 1;
transform: translate(50%, 50%);
}
@ -270,4 +277,40 @@ watch(dragging, dragging => {
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>

View file

@ -15,8 +15,7 @@
}
.resource-box {
margin-right: 20px !important;
font-size: large;
display: flex;
border: solid 2px var(--bought);
padding: 0 4px;
border-radius: 4px;
@ -24,6 +23,10 @@
align-items: center;
}
.resource-box:not(:last-child) {
margin-right: 20px !important;
}
.resource-box .material-icons {
margin-right: 6px;
font-size: 1em;
@ -56,3 +59,144 @@
display: flex;
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 { createLayer } from "game/layers";
import type { Player } from "game/player";
import { computed, ref } from "vue";
import { computed, ref, Transition, TransitionGroup } from "vue";
import CharacterSlot from "./CharacterSlot.vue";
import "./socket";
import "./common.css";
@ -20,6 +20,7 @@ import vespa from "../../public/Vespa Coots.png";
import heart from "../../public/Heart.png";
import startStream from "../../public/start stream.png";
import { createReset } from "features/reset";
import settings from "game/settings";
export const characters: Record<string, CharacterInfo> = {
coots: {
@ -85,6 +86,49 @@ export const main = createLayer("main", function (this: BaseLayer) {
const selectedCharacter = ref<number | null>(null);
const selectedShopItem = ref<number | null>(null);
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(() => ({
onReset() {
@ -97,128 +141,331 @@ export const main = createLayer("main", function (this: BaseLayer) {
selectedCharacter.value = null;
selectedShopItem.value = null;
findingMatch.value = false;
battle.value = null;
outcome.value = "";
showingOutcome.value = false;
playClicked.value = false;
queue.value = [];
}
}));
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 {
name: "Game",
minimizable: false,
display: jsx(() => (
<div
class="game-container"
style={findingMatch.value ? "pointer-events: none" : ""}
onClick={() => {
selectedCharacter.value = null;
selectedShopItem.value = null;
}}
>
<Row style="position: absolute; top: 10px; left: -5px">
<div class="resource-box">
<span class="material-icons">credit_card</span>
{gold.value}
display: jsx(() => {
if (battle.value != null) {
return (
<div class={{ ["battle-container"]: true, fast: settings.fast }}>
<div class="battle-controls">
<button
class="button"
onClick={() => {
playClicked.value = true;
prepareMove();
}}
>
<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 class="resource-box">
<img src={heart} />
{lives.value}
</div>
<div class="resource-box">
<span class="material-icons">emoji_events</span>
{wins.value}/5
</div>
</Row>
<h2 style="font-size: 3vmin">{nickname.value}</h2>
<Row style="margin-top: 10vh">
{new Array(3).fill(0).map((_, i) => (
<CharacterSlot
character={team.value[i]}
isSelected={selectedCharacter.value === i}
selected={
selectedCharacter.value == null
? selectedShopItem.value == null ||
(team.value[i] != null &&
shop.value[selectedShopItem.value]?.type !==
team.value[i]?.type) ||
gold.value < 3
? null
: shop.value[selectedShopItem.value]
: team.value[selectedCharacter.value]
}
isDragging={isDragging.value}
onClick={clickCharacter(i)}
onDragstart={() => {
isDragging.value = true;
selectedCharacter.value = i;
selectedShopItem.value = null;
}}
onDragend={() => {
isDragging.value = false;
selectedCharacter.value = null;
selectedShopItem.value = null;
}}
onDrop={() => clickCharacter(i)()}
/>
))}
</Row>
<Row style="margin-top: 10vh">
<div
class="reroll"
style={gold.value > 0 ? "" : "color: var(--locked); cursor: not-allowed"}
onClick={() => {
if (gold.value > 0) {
emit("reroll");
}
}}
>
<span class="material-icons" style="font-size: 8vmin">
casino
</span>
<span style="font-size: 2vmin">Roll</span>
</div>
{shop.value.map((item, i) => (
<CharacterSlot
character={item == null ? undefined : item}
isSelected={selectedShopItem.value === i}
isShop={true}
isDragging={isDragging.value}
onClick={(e: MouseEvent) => {
if (item == null) {
return;
);
}
return (
<div
class="game-container"
style={findingMatch.value ? "pointer-events: none" : ""}
onClick={() => {
selectedCharacter.value = null;
selectedShopItem.value = null;
}}
>
<Row style="position: absolute; top: 10px; left: -5px">
<div class="resource-box">
<span class="material-icons">credit_card</span>
{gold.value}
</div>
<div class="resource-box">
<img src={heart} />
{lives.value}
</div>
<div class="resource-box">
<span class="material-icons">emoji_events</span>
{wins.value}/5
</div>
</Row>
<h2 style="font-size: 3vmin">{nickname.value}</h2>
<Row style="margin-top: 10vh">
{new Array(3).fill(0).map((_, i) => (
<CharacterSlot
character={team.value[i]}
isSelected={selectedCharacter.value === i}
selected={
selectedCharacter.value == null
? selectedShopItem.value == null ||
(team.value[i] != null &&
shop.value[selectedShopItem.value]?.type !==
team.value[i]?.type) ||
gold.value < 3
? null
: shop.value[selectedShopItem.value]
: team.value[selectedCharacter.value]
}
isDragging={isDragging.value}
onClick={clickCharacter(i)}
onDragstart={() => {
isDragging.value = true;
selectedCharacter.value = i;
selectedShopItem.value = null;
}}
onDragend={() => {
isDragging.value = false;
selectedCharacter.value = null;
selectedShopItem.value = null;
}}
onDrop={() => clickCharacter(i)()}
/>
))}
</Row>
<Row style="margin-top: 10vh">
<div
class="reroll"
style={
gold.value > 0 ? "" : "color: var(--locked); cursor: not-allowed"
}
onClick={() => {
if (gold.value > 0) {
emit("reroll");
}
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;
>
<span class="material-icons" style="font-size: 8vmin">
casino
</span>
<span style="font-size: 2vmin">Roll</span>
</div>
{shop.value.map((item, i) => (
<CharacterSlot
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>
<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>
)),
)}
</div>
);
}),
lives,
wins,
turn,
@ -228,7 +475,12 @@ export const main = createLayer("main", function (this: BaseLayer) {
selectedCharacter,
selectedShopItem,
findingMatch,
reset
showingOutcome,
outcome,
reset,
battle,
playClicked,
prepareMove
};
});

View file

@ -105,6 +105,7 @@ function setupSocket(socket: Socket<ServerToClientEvents, ClientToServerEvents>)
socket.on("newTurn", shop => {
main.gold.value = 10;
main.turn.value++;
main.battle.value = null;
main.shop.value = shop.map(item => ({
type: item,
relevancy: characters[item].initialRelevancy,
@ -135,15 +136,22 @@ function setupSocket(socket: Socket<ServerToClientEvents, ClientToServerEvents>)
main.team.value[index] = null;
main.team.value[otherIndex] = char;
});
socket.on("stream", (enemyTeam, enemyNickname, outcome) => {
if (outcome === "Victory") {
main.wins.value++;
} else if (outcome === "Defeat") {
main.lives.value--;
}
socket.on("stream", (enemy, outcome) => {
main.findingMatch.value = false;
// TODO display combat
emit("newTurn");
main.battle.value = {
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;
move: (index: number, otherIndex: number) => 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 {

View file

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