Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(GameScreen): add "follow my bot" button #89

Merged
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
86d754c
added button to follow bot
Nov 27, 2024
1c24502
use the right bot coordinate for follow
Nov 29, 2024
07b2fb3
refactored zoom functions to be generally used
Nov 30, 2024
17dd145
constraint followBot zoom in to map borders
Nov 30, 2024
ad1d859
refactored zoomIn and zoomOut functions to be just 1 function
Dec 1, 2024
0e7c08f
disable panning and mouse grab function when following bot
Dec 1, 2024
c17db6a
fix typoes and follow coding standards
Dec 2, 2024
e135f84
Merge branch 'main' into followButton
Dec 2, 2024
d53c450
changed button label
Dec 2, 2024
c13bfeb
fixed error of follow function on load
Dec 3, 2024
ff72b58
code cleanup
Dec 3, 2024
c76fed5
more code clean up
Dec 3, 2024
9f501e9
disabled zoom in and out with mouse wheen when following
Dec 3, 2024
0d76284
fixed follow issues, put indication if following or not
Dec 8, 2024
5bc6fa6
Merge branch 'main' into followButton
Dec 8, 2024
06ddebe
code cleanup
Dec 8, 2024
8b909e0
removed username scale adjustment
Dec 8, 2024
e08cd40
moved username check, used @onclick, followBot zoom in is based on bo…
Dec 9, 2024
83c79f9
Merge branch 'main' into followButton
Dec 9, 2024
c560264
change followBot to toggle to toggleFollowMeMode
Dec 10, 2024
6d5100d
change follow function name to zoomInToPlayer
Dec 10, 2024
52161a0
Move zoom function to utils
Dec 11, 2024
d5a232d
hide the "follow my bot" button if the user isn't logged in.
Dec 17, 2024
1bc43bf
feat(login): view current game without login (#92)
arilloid Dec 12, 2024
89f4ea4
fix hiding follow button after merge
Dec 17, 2024
13ddd8d
Merge branch 'main' into followButton
Dec 17, 2024
a64d2c2
clean up
Dec 18, 2024
6003bdf
added tests for getZoomScale function
Dec 19, 2024
7ac1c3a
fix test prompt
aldrin312 Dec 19, 2024
8d98688
Made getZoomScale tests prompts more specific.
Dec 20, 2024
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
151 changes: 74 additions & 77 deletions components/GameScreen.vue
aldrin312 marked this conversation as resolved.
Show resolved Hide resolved
yurijmikhalevich marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<script setup lang="ts">
import { Application, Graphics, Text, FillGradient, Assets, type Texture, Sprite } from "pixi.js";
import { getSmoothZoomScreen } from "~/other/zoomUtils";

const { data: user } = await useFetch("/api/auth/user");
const refreshIntervalMs = 1000;
const zoomSpeed = 0.05;
const zoomSpeed = 0.1;
const minZoom = 1;
const maxZoom = 3;
const isFollowing = ref<boolean>(false);
const zoomInToPlayerFn = ref<(() => void) | null>(null);
aldrin312 marked this conversation as resolved.
Show resolved Hide resolved

const { data: gameState, refresh } = await useFetch("/api/state");
const intervalRef = ref<number | null>(null);
Expand Down Expand Up @@ -34,11 +38,14 @@ onBeforeUnmount(() => {
});

const canvas = ref<HTMLCanvasElement | null>(null);
const appRef = ref<Application | null>(null);
// const appRef = ref<Application | null>(null);
aldrin312 marked this conversation as resolved.
Show resolved Hide resolved
const appRef: Ref<Application | null> = ref(null);
const foodRef = ref<{ x: number; y: number; graphics: Graphics }[]>([]);
const botSpawnsRef = ref<Record<string, Graphics>>({});
const gameScreen = ref<HTMLDivElement | null>(null);

const smoothZoomScreen = getSmoothZoomScreen(appRef);

const tickFnRef = ref<() => void>();

type DrawBotArgs = {
Expand All @@ -56,6 +63,29 @@ type DrawBotArgs = {
graphics: Graphics;
};

function followPlayerBot(x: number, y: number) {
aldrin312 marked this conversation as resolved.
Show resolved Hide resolved
if (!appRef.value || !gameState.value) {
throw new Error("unexpected: game not initialized");
}
const currentPos = appRef.value.stage.position;
const targetPos = {
x: -x * appRef.value.stage.scale.x + appRef.value.screen.width / 2,
y: -y * appRef.value.stage.scale.y + appRef.value.screen.height / 2,
};
const newPosX = currentPos.x + (targetPos.x - currentPos.x) * 0.1;
const newPosY = currentPos.y + (targetPos.y - currentPos.y) * 0.1;

appRef.value.stage.position.set(
Math.min(0, Math.max(newPosX, appRef.value.screen.width - appRef.value.screen.width * appRef.value.stage.scale.x)),
Math.min(0, Math.max(newPosY, appRef.value.screen.height - appRef.value.screen.height * appRef.value.stage.scale.y)),
);
}

function toggleFollowMeMode() {
isFollowing.value = !isFollowing.value;
zoomInToPlayerFn.value?.();
}

function setSpritePositionAndSize({
sprite,
bot: { x, y, radius },
Expand Down Expand Up @@ -179,87 +209,43 @@ watch(gameState, async (newState, prevState) => {
autoDensity: true,
});

let isZoomingOut = false;
const zoomDuration = 500; // Duration of the zoom-out effect in milliseconds
let startZoomTime: number | null = null;
let startMousePos = { x: 0, y: 0 };

// Functions to handle smooth zoom out
function smoothZoomOut(mousePos: { x: number; y: number }) {
if (!appRef.value) {
return;
// Follow player bot
function zoomInToPlayer() {
if (!gameState.value) {
throw new Error("unexpected: game not initialized");
}

isZoomingOut = true;
startZoomTime = performance.now();
startMousePos = mousePos;
requestAnimationFrame(animateZoomOut);
}

function animateZoomOut(currentTime: number) {
if (!appRef.value || !startZoomTime) {
return;
};

const elapsedTime = currentTime - startZoomTime;
const progress = Math.min(elapsedTime / zoomDuration, 1);
const newScale = minZoom + (appRef.value.stage.scale.x - minZoom) * (1 - progress);

const worldPos = {
x: (startMousePos.x - appRef.value.stage.position.x) / appRef.value.stage.scale.x,
y: (startMousePos.y - appRef.value.stage.position.y) / appRef.value.stage.scale.y,
};

appRef.value.stage.scale.set(newScale);

const newScreenPos = {
x: worldPos.x * newScale + appRef.value.stage.position.x,
y: worldPos.y * newScale + appRef.value.stage.position.y,
};

appRef.value.stage.position.set(
Math.min(0, Math.max(appRef.value.stage.position.x - (newScreenPos.x - startMousePos.x), appRef.value.screen.width - appRef.value.screen.width * newScale)),
Math.min(0, Math.max(appRef.value.stage.position.y - (newScreenPos.y - startMousePos.y), appRef.value.screen.height - appRef.value.screen.height * newScale)),
);
if (progress < 1) {
requestAnimationFrame(animateZoomOut);
} else {
isZoomingOut = false;
let playerBot = null;
for (const bot of Object.values(gameState.value.bots)) {
if (bot.username == user.value?.body.username) {
playerBot = bot;
}
}
if (playerBot) {
const newScale = (Math.max(minZoom, Math.min(maxZoom, ((app.stage.scale.x + 3) - ((app.stage.scale.x + 3) * ((playerBot?.radius + 10) / 100))))));
aldrin312 marked this conversation as resolved.
Show resolved Hide resolved
const botPos = { x: playerBot.x, y: playerBot.y };
if (isFollowing.value) {
smoothZoomScreen({ pos: botPos, scale: newScale });
} else {
smoothZoomScreen({ pos: botPos, scale: 1 });
}
}
}
zoomInToPlayerFn.value = zoomInToPlayer;

// Mouse wheel event listener
// Event listeners can be potentially called multiple times.
// TODO: add .removeEventListener later for refactoring.
canvas.value?.addEventListener("wheel", (event) => {
if (isFollowing.value) {
return;
}
event.preventDefault();
const mousePos = { x: event.offsetX, y: event.offsetY };
if (event.deltaY > 0) {
// Zoom out when scrolling down
if (!isZoomingOut) {
smoothZoomOut(mousePos);
}
} else {
// Existing zoom-in functionality
const zoomFactor = event.deltaY * -zoomSpeed;
const newScale = Math.max(minZoom, Math.min(maxZoom, app.stage.scale.x + zoomFactor));

const worldPos = {
x: (mousePos.x - app.stage.position.x) / app.stage.scale.x,
y: (mousePos.y - app.stage.position.y) / app.stage.scale.y,
};

app.stage.scale.set(newScale);

const newScreenPos = {
x: worldPos.x * newScale + app.stage.position.x,
y: worldPos.y * newScale + app.stage.position.y,
};

app.stage.position.set(
Math.min(0, Math.max(app.stage.position.x - (newScreenPos.x - mousePos.x), app.screen.width - app.screen.width * newScale)),
Math.min(0, Math.max(app.stage.position.y - (newScreenPos.y - mousePos.y), app.screen.height - app.screen.height * newScale)),
);
const zoomFactor = (event.deltaY * -zoomSpeed) * 100;
const newScale = Math.max(minZoom, Math.min(maxZoom, app.stage.scale.x + zoomFactor));
smoothZoomScreen({ pos: mousePos, scale: newScale });
if (!isFollowing.value) {
if (newScale > 1) {
gameScreen.value?.classList.add("cursor-grab");
} else {
Expand All @@ -273,15 +259,17 @@ watch(gameState, async (newState, prevState) => {
let startDragPos = { x: 0, y: 0 };

canvas.value?.addEventListener("mousedown", (event) => {
if (app.stage.scale.x > 1) {
gameScreen.value?.classList.add("cursor-grabbing");
if (!isFollowing.value) {
if (app.stage.scale.x > 1) {
gameScreen.value?.classList.add("cursor-grabbing");
}
isDragging = true;
startDragPos = { x: event.offsetX, y: event.offsetY };
}
isDragging = true;
startDragPos = { x: event.offsetX, y: event.offsetY };
});

canvas.value?.addEventListener("mousemove", (event) => {
if (isDragging && canvas.value) {
if (isDragging && canvas.value && !isFollowing.value) {
const dx = event.offsetX - startDragPos.x;
const dy = event.offsetY - startDragPos.y;
const newPosX = app.stage.position.x + dx;
Expand Down Expand Up @@ -417,6 +405,10 @@ watch(gameState, async (newState, prevState) => {
const x = prevBot.x + (bot.x - prevBot.x) * progress;
const y = prevBot.y + (bot.y - prevBot.y) * progress;
const botDirection = getDirection(prevBot, bot);
// Follow players bot
if (isFollowing.value && bot.username == user.value?.body.username) {
followPlayerBot(x, y);
}

drawBot({ bot: { ...bot, x, y }, graphics: existingBot, botDirection });
}
Expand All @@ -436,6 +428,11 @@ watch(gameState, async (newState, prevState) => {
class="flex flex-col shadow ml-2"
:style="{ maxWidth: gameState?.width + 'px' }"
>
<div class="flex flex-row justify-end mb-2 mt-1 mx-4 gap-6">
<ButtonLink @click="toggleFollowMeMode">
{{ isFollowing ? "stop following my bot" : "follow my bot" }}
</ButtonLink>
</div>
aldrin312 marked this conversation as resolved.
Show resolved Hide resolved
<canvas ref="canvas" />
</div>
</div>
Expand Down
64 changes: 64 additions & 0 deletions other/zoomUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Application } from "pixi.js";

export function getSmoothZoomScreen(appRef: Ref<Application | null>) {
let startZoomTime: number;
const zoomDuration = 500;
let targetScale: number;
let targetPos: { x: number; y: number };
let isZooming = false;
function smoothZoom({ pos, scale }: { pos: { x: number; y: number }; scale: number }) {
if (!appRef.value) {
return;
}

if (isZooming) {
return; // Prevent multiple simultaneous zooms
}
isZooming = true;
targetScale = scale;
targetPos = pos;
startZoomTime = performance.now();

requestAnimationFrame(animateZoom);
}

function animateZoom(currentTime: number) {
if (!appRef.value || !appRef.value.stage) {
console.error("AppRef or stage is not initialized");
return;
}

const elapsedTime = currentTime - startZoomTime;
const progress = Math.min(elapsedTime / zoomDuration, 1);

// Get the current scale and the scale change based on progress
const currentScale = appRef.value.stage.scale.x;
const newScale = currentScale + (targetScale - currentScale) * progress;

// Calculate the world position relative to the current scale
const worldPos = {
x: (targetPos.x - appRef.value.stage.position.x) / appRef.value.stage.scale.x,
y: (targetPos.y - appRef.value.stage.position.y) / appRef.value.stage.scale.y,
};
appRef.value.stage.scale.set(newScale);

const newScreenPos = {
x: worldPos.x * newScale + appRef.value.stage.position.x,
y: worldPos.y * newScale + appRef.value.stage.position.y,
};

// Adjust the stage position to follow the zoom focus point
appRef.value.stage.position.set(
Math.min(0, Math.max(appRef.value.stage.position.x - (newScreenPos.x - targetPos.x), appRef.value.screen.width - appRef.value.screen.width * newScale)),
Math.min(0, Math.max(appRef.value.stage.position.y - (newScreenPos.y - targetPos.y), appRef.value.screen.height - appRef.value.screen.height * newScale)),
);

if (progress < 1) {
requestAnimationFrame(animateZoom);
} else {
isZooming = false;
}
}

return smoothZoom;
}