diff --git a/app/admin/review/page.tsx b/app/admin/review/page.tsx index 2071686..ad5b201 100644 --- a/app/admin/review/page.tsx +++ b/app/admin/review/page.tsx @@ -2,6 +2,7 @@ import { useMutation, useQuery } from "convex/react"; import { Map } from "@/components/Map"; +import { PlayMapButton } from "@/components/PlayMapButton"; import { Button } from "@/components/ui/button"; import { Card, @@ -12,16 +13,17 @@ import { } from "@/components/ui/card"; import { api } from "@/convex/_generated/api"; -const Page = () => { +export default function AdminReviewPage() { const isAdmin = useQuery(api.users.isAdmin); const maps = useQuery(api.maps.getMaps, { isReviewed: false }); const adminApprovalMutation = useMutation(api.maps.approveMap); + const adminRejectMapMutation = useMutation(api.maps.rejectMap); if (isAdmin == true) { return (

Review Maps

-
+
{maps?.map((map) => ( @@ -32,7 +34,7 @@ const Page = () => { - + + + ))} @@ -50,6 +61,4 @@ const Page = () => { } return
Not an admin
; -}; - -export default Page; +} diff --git a/app/header.tsx b/app/header.tsx index 9e6720f..9238bab 100644 --- a/app/header.tsx +++ b/app/header.tsx @@ -45,7 +45,7 @@ export default function Header() { return (
- Logo + Logo SurviveTheNight @@ -99,11 +99,18 @@ export default function Header() {
{!isAuthenticated ? ( diff --git a/app/play/page.tsx b/app/play/page.tsx index 332da1e..bb8f0c5 100644 --- a/app/play/page.tsx +++ b/app/play/page.tsx @@ -1,7 +1,13 @@ "use client"; import { useEffect, useState } from "react"; -import { Authenticated, Unauthenticated, useQuery } from "convex/react"; +import { + Authenticated, + Unauthenticated, + useMutation, + useQuery, +} from "convex/react"; +import { TrashIcon } from "lucide-react"; import Link from "next/link"; import { Map as GameMap } from "@/components/Map"; import { Button } from "@/components/ui/button"; @@ -18,9 +24,11 @@ import { api } from "@/convex/_generated/api"; import { cn } from "@/lib/utils"; export default function PlayPage() { + const isAdmin = useQuery(api.users.isAdmin); const maps = useQuery(api.maps.getMaps, {}); const userMapResults = useQuery(api.playerresults.getUserMapStatus); const mapCountResults = useQuery(api.playerresults.getMapsWins); + const adminDeleteMapMutation = useMutation(api.maps.deleteMap); const [resMap, setResMap] = useState(new Map()); const [countMap, setCountMap] = useState(new Map()); @@ -128,9 +136,24 @@ export default function PlayPage() { )} > - - Night #{map.level} - +
+ + Night #{map.level} + + {isAdmin && ( + + )} +
diff --git a/app/playground/page.tsx b/app/playground/page.tsx index 7455877..41ece90 100644 --- a/app/playground/page.tsx +++ b/app/playground/page.tsx @@ -2,19 +2,19 @@ import * as React from "react"; import { useAction, useMutation, useQuery } from "convex/react"; -import { CircleAlertIcon, EraserIcon, SendIcon, UploadIcon, ChevronLeft } from "lucide-react"; -import { CopyMapButton } from "@/components/CopyMapButton"; -import { MapBuilder } from "@/components/MapBuilder"; +import { ChevronLeft, UploadIcon } from "lucide-react"; +import { useSearchParams } from "next/navigation"; +import { Map } from "@/components/Map"; import { MapStatus } from "@/components/MapStatus"; import { ModelSelector } from "@/components/ModelSelector"; import { Visualizer } from "@/components/Visualizer"; import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; import { useToast } from "@/components/ui/use-toast"; import { api } from "@/convex/_generated/api"; +import { Id } from "@/convex/_generated/dataModel"; import { errorMessage } from "@/lib/utils"; import { ZombieSurvival } from "@/simulators/zombie-survival"; -import { Card } from "@/components/ui/card"; -import { Map } from "@/components/Map"; const STORAGE_MAP_KEY = "playground-map"; @@ -22,6 +22,13 @@ export default function PlaygroundPage() { const isAdmin = useQuery(api.users.isAdmin); const submitMap = useMutation(api.maps.submitMap); const testMap = useAction(api.maps.testMap); + const searchParams = useSearchParams(); + const mapId = searchParams.get("map") as Id<"maps"> | null; + const adminMapMaybe = useQuery( + api.maps.adminGetMapById, + !isAdmin || mapId === null ? "skip" : { mapId }, + ); + const adminMap = adminMapMaybe ?? null; const { toast } = useToast(); const [map, setMap] = React.useState([ [" ", " ", " ", " ", " "], @@ -99,7 +106,9 @@ export default function PlaygroundPage() { function handleChangeMap(value: string[][]) { setMap(value); setError(null); - window.localStorage.setItem(STORAGE_MAP_KEY, JSON.stringify(value)); + if (adminMap === null) { + window.localStorage.setItem(STORAGE_MAP_KEY, JSON.stringify(value)); + } } function handleChangeModel(value: string) { @@ -112,10 +121,10 @@ export default function PlaygroundPage() { setReasoning(null); setUserPlaying(false); setVisualizingUserSolution(false); - + // Remove players and blocks from the map - const cleanedMap = map.map(row => - row.map(cell => (cell === "P" || cell === "B") ? " " : cell) + const cleanedMap = map.map((row) => + row.map((cell) => (cell === "P" || cell === "B" ? " " : cell)), ); setMap(cleanedMap); window.localStorage.setItem(STORAGE_MAP_KEY, JSON.stringify(cleanedMap)); @@ -169,6 +178,12 @@ export default function PlaygroundPage() { } }, []); + React.useEffect(() => { + if (adminMap !== null) { + setMap(adminMap.grid); + } + }, [adminMap]); + const visualizing = solution !== null || visualizingUserSolution; return ( @@ -180,16 +195,20 @@ export default function PlaygroundPage() {
{!visualizing && !userPlaying && ( -

- Click on the board to place or remove units. Use the buttons below to switch between unit types. +

+ Click on the board to place or remove units. Use the buttons + below to switch between unit types.

)} {!visualizing && userPlaying && ( -

- Place a player (P) and blocks (B) on the board to create your escape route. Click to toggle between empty, player, and block. +

+ Place a player (P) and blocks (B) on the board to create your + escape route. Click to toggle between empty, player, and block.

)} -
+
{visualizing && ( { @@ -223,8 +246,12 @@ export default function PlaygroundPage() { : [...map]; if (userPlaying) { // Count existing players and blocks - const playerCount = newMap.flat().filter(c => c === "P").length; - const blockCount = newMap.flat().filter(c => c === "B").length; + const playerCount = newMap + .flat() + .filter((c) => c === "P").length; + const blockCount = newMap + .flat() + .filter((c) => c === "B").length; // Toggle logic for play mode if (cell === " ") { @@ -252,7 +279,7 @@ export default function PlaygroundPage() { : handleChangeMap(newMap); }} /> - )) + )), )}
diff --git a/app/prompts/page.tsx b/app/prompts/page.tsx index 91b4dc3..466feeb 100644 --- a/app/prompts/page.tsx +++ b/app/prompts/page.tsx @@ -14,7 +14,7 @@ import { } from "@/components/ui/table"; import { api } from "@/convex/_generated/api"; -const Page = () => { +export default function PromptsPage() { const prompts = useQuery(api.prompts.getAllPrompts); const enablePrompt = useMutation(api.prompts.enablePrompt); const deletePrompt = useMutation(api.prompts.deletePrompt); @@ -91,6 +91,4 @@ const Page = () => {
); -}; - -export default Page; +} diff --git a/components/PlayMapButton.tsx b/components/PlayMapButton.tsx new file mode 100644 index 0000000..21ec6a2 --- /dev/null +++ b/components/PlayMapButton.tsx @@ -0,0 +1,16 @@ +"use client"; + +import * as React from "react"; +import { ExternalLinkIcon } from "lucide-react"; +import Link from "next/link"; +import { Button } from "./ui/button"; + +export function PlayMapButton({ mapId }: { mapId: string }) { + return ( + + ); +} diff --git a/convex/maps.ts b/convex/maps.ts index 003dc04..9841252 100644 --- a/convex/maps.ts +++ b/convex/maps.ts @@ -11,7 +11,11 @@ import { query, } from "./_generated/server"; import { Prompt } from "./prompts"; -import { adminMutationBuilder, authenticatedMutation } from "./users"; +import { + adminMutationBuilder, + adminQueryBuilder, + authenticatedMutation, +} from "./users"; const LEVELS = [ { @@ -244,6 +248,47 @@ export const approveMap = adminMutationBuilder({ }, }); +export const rejectMap = adminMutationBuilder({ + args: { + mapId: v.id("maps"), + }, + handler: async (ctx, args) => { + await ctx.db.delete(args.mapId); + }, +}); + +export const deleteMap = adminMutationBuilder({ + args: { + mapId: v.id("maps"), + }, + handler: async (ctx, args) => { + const map = await ctx.db.get(args.mapId); + + if (map === null) { + return; + } + + await ctx.db.delete(args.mapId); + + if (map.level === undefined) { + return; + } + + const higherLevelMaps = await ctx.db + .query("maps") + .withIndex("by_level", (q) => q.gt("level", map.level)) + .collect(); + + await Promise.all( + higherLevelMaps.map(async (higherLevelMap) => { + return await ctx.db.patch(higherLevelMap._id, { + level: higherLevelMap.level! - 1, + }); + }), + ); + }, +}); + export const getMapByLevel = query({ args: { level: v.number() }, handler: async (ctx, args) => { @@ -254,6 +299,15 @@ export const getMapByLevel = query({ }, }); +export const adminGetMapById = adminQueryBuilder({ + args: { + mapId: v.id("maps"), + }, + handler: async (ctx, args) => { + return await ctx.db.get(args.mapId); + }, +}); + export const playMapAction = internalAction({ args: { gameId: v.id("games"), diff --git a/public/entities/rocks.png b/public/entities/rocks.png deleted file mode 100644 index 1f2e53a..0000000 Binary files a/public/entities/rocks.png and /dev/null differ diff --git a/renderer/index.ts b/renderer/index.ts index f32827b..505efd8 100644 --- a/renderer/index.ts +++ b/renderer/index.ts @@ -8,6 +8,7 @@ export interface RendererAssets { player: HTMLImageElement | null; rock: HTMLImageElement | null; zombie: HTMLImageElement | null; + zombieHit: HTMLImageElement | null; } const assets: RendererAssets = { @@ -18,6 +19,7 @@ const assets: RendererAssets = { player: null, rock: null, zombie: null, + zombieHit: null, }; async function loadAssets() { @@ -27,12 +29,13 @@ async function loadAssets() { assets.loading = true; - const [bg, box, player, rock, zombie] = await Promise.all([ + const [bg, box, player, rock, zombie, zombieHit] = await Promise.all([ loadImage("/map.png"), loadImage("/entities/block.svg"), loadImage("/entities/player_alive_1.svg"), loadImage("/entities/rocks.svg"), loadImage("/entities/zombie_alive_1.svg"), + loadImage("/entities/zombie_alive_2.svg"), ]); assets.loaded = true; @@ -41,6 +44,7 @@ async function loadAssets() { assets.player = player; assets.rock = rock; assets.zombie = zombie; + assets.zombieHit = zombieHit; } async function loadImage(src: string): Promise { @@ -63,16 +67,49 @@ function getEntityImage(entity: Entity): HTMLImageElement | null { return assets.rock; } case EntityType.Zombie: { - return assets.zombie; + if (entity.getHealth() === 1) { + return assets.zombieHit; + } else { + return assets.zombie; + } } } } function getEntityOffset(entity: Entity): { x: number; y: number } { - return { - x: entity.getType() === EntityType.Zombie ? 16 : 0, - y: 0, - }; + switch (entity.getType()) { + case EntityType.Zombie: { + if (entity.getHealth() === 1) { + return { x: -2, y: 0 }; + } else { + return { x: 14, y: 0 }; + } + } + default: { + return { x: 0, y: 0 }; + } + } +} + +function getEntityRatio(entity: Entity): { width: number; height: number } { + switch (entity.getType()) { + case EntityType.Box: { + return { width: 0.87, height: 1 }; // 41x47 + } + case EntityType.Player: { + return { width: 1, height: 1 }; // 64x64 + } + case EntityType.Rock: { + return { width: 1, height: 0.76 }; // 67x51 + } + case EntityType.Zombie: { + if (entity.getHealth() === 1) { + return { width: 0.61, height: 1 }; // 40x65 + } else { + return { width: 1, height: 1 }; // 64x64 + } + } + } } export class Renderer { @@ -118,8 +155,6 @@ export class Renderer { for (const entity of entities) { this.drawEntity(entity); } - - this.ctx.globalAlpha = 1.0; } private drawBg() { @@ -158,6 +193,7 @@ export class Renderer { const entityPosition = entity.getPosition(); const entityOffset = getEntityOffset(entity); + const entityScale = getEntityRatio(entity); this.ctx.globalAlpha = entity.getType() === EntityType.Zombie && entity.getHealth() === 1 @@ -166,10 +202,16 @@ export class Renderer { this.ctx.drawImage( entityImage, - entityPosition.x * this.cellSize + entityOffset.x, - entityPosition.y * this.cellSize + entityOffset.y, - this.cellSize, - this.cellSize, + entityPosition.x * this.cellSize + + ((1 - entityScale.width) / 2) * this.cellSize + + entityOffset.x, + entityPosition.y * this.cellSize + + ((1 - entityScale.height) / 2) * this.cellSize + + entityOffset.y, + this.cellSize * entityScale.width, + this.cellSize * entityScale.height, ); + + this.ctx.globalAlpha = 1; } }