A Deno-powered backend service for Plants vs. Zombies: MODDED. [Read-only GitHub mirror] docs.pvzm.net
express typescript expressjs plant deno jspvz pvzm game online backend plants-vs-zombies zombie javascript plants modded vs plantsvszombies openapi pvz noads
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

v0.4.0: zombie picker support, profanity filter

Clay 5bcfcab3 a5349568

+77 -2
+1 -1
README.md
··· 1 - # PVZM Backend ![v0.3.0](https://img.shields.io/badge/version-v0.2.2-darklime) 1 + # PVZM Backend ![v0.4.0](https://img.shields.io/badge/version-v0.2.2-darklime) 2 2 3 3 > A Deno-powered backend service for [Plants vs. Zombies: MODDED](https://github.com/roblnet13/pvz). This service provides APIs for uploading, downloading, listing, favoriting, and reporting user-created _I, Zombie_ levels. 4 4
+1
deno.jsonc
··· 36 36 "@types/express": "npm:@types/express@^5.0.3", 37 37 "@types/node": "npm:@types/node@^24.3.1", 38 38 "@types/pako": "npm:@types/pako@^2.0.4", 39 + "bad-words": "npm:bad-words@^4.0.0", 39 40 "cors": "npm:cors@^2.8.5", 40 41 "discord.js": "npm:discord.js@^14.25.1", 41 42 "express-msgpack": "npm:express-msgpack@^6.0.0",
+11
deno.lock
··· 20 20 "npm:@types/express@^5.0.3": "5.0.6", 21 21 "npm:@types/node@^24.3.1": "24.10.4", 22 22 "npm:@types/pako@^2.0.4": "2.0.4", 23 + "npm:bad-words@4": "4.0.0", 23 24 "npm:cors@^2.8.5": "2.8.5", 24 25 "npm:discord.js@^14.25.1": "14.25.1", 25 26 "npm:express-msgpack@6": "6.0.0_express@5.2.1", ··· 249 250 "mime-types", 250 251 "negotiator" 251 252 ] 253 + }, 254 + "bad-words@4.0.0": { 255 + "integrity": "sha512-fLjG/I0N3I7xhurqGnGitSRD10UeEE63a7hyXtutQDpxo4+Eal+i7veWeZxZJPNtsl6X1mUIoWPwt8gQ7NMQUw==", 256 + "dependencies": [ 257 + "badwords-list" 258 + ] 259 + }, 260 + "badwords-list@2.0.1-4": { 261 + "integrity": "sha512-FxfZUp7B9yCnesNtFQS9v6PvZdxTYa14Q60JR6vhjdQdWI4naTjJIyx22JzoER8ooeT8SAAKoHLjKfCV7XgYUQ==" 252 262 }, 253 263 "base64url@3.0.1": { 254 264 "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==" ··· 804 814 "npm:@types/express@^5.0.3", 805 815 "npm:@types/node@^24.3.1", 806 816 "npm:@types/pako@^2.0.4", 817 + "npm:bad-words@4", 807 818 "npm:cors@^2.8.5", 808 819 "npm:discord.js@^14.25.1", 809 820 "npm:express-msgpack@6",
+19 -1
modules/levels_io.ts
··· 14 14 15 15 export interface Clone { 16 16 plants: Plant[]; 17 + selectedZombies: string[]; 17 18 music: string; 18 19 sun: number; 19 20 name: string; ··· 60 61 eleTop: 13, 61 62 eleWidth: 14, 62 63 eleHeight: 15, 64 + // zombies 65 + selectedZombies: 16, 63 66 }; 64 67 65 68 const REVERSE_TINYIFIER_MAP: Record<number, string> = Object.fromEntries(Object.entries(TINYIFIER_MAP).map(([key, value]) => [value, key])); ··· 118 121 "oFlamesMushroom", 119 122 ]; 120 123 121 - // Client name for this list (kept as alias for API familiarity) 124 + export const iZombies = [ 125 + "oIImp", 126 + "oIConeheadZombie", 127 + "oIPoleVaultingZombie", 128 + "oIBucketheadZombie", 129 + "oIFootballZombie", 130 + "oIJackinTheBoxZombie", 131 + "oIScreenDoorZombie", 132 + "oIZombie", 133 + "oIDuckyTubeZombie1", 134 + "oIDuckyTubeZombie2", 135 + "oIDuckyTubeZombie3", 136 + "oIBalloonZombie", 137 + ]; 138 + 139 + // client name for this list (kept as alias for API familiarity) 122 140 export const izombiePlantsMap = allPlantsStringArray; 123 141 124 142 function packFromArray(arr: number[]): number {
+7
modules/moderation.ts
··· 1 1 import OpenAI from "@openai/openai"; 2 + import { Filter } from "bad-words"; 2 3 3 4 import type { ServerConfig } from "./config.ts"; 5 + 6 + const filter = new Filter(); 4 7 5 8 export interface ModerationCategories { 6 9 [key: string]: boolean; ··· 29 32 } 30 33 31 34 return async function moderateContent(text: string): Promise<ModerationResult> { 35 + if (filter.isProfane(text)) { 36 + return { flagged: true }; 37 + } 38 + 32 39 if (!config.useOpenAIModeration || !openai) return { flagged: false }; 33 40 34 41 try {
+38
modules/validate.ts
··· 1 + import { iZombies } from "./levels_io.ts"; 2 + 1 3 import type { decodeFile } from "./levels_io.ts"; 2 4 3 5 type Clone = ReturnType<typeof decodeFile>; ··· 112 114 return true; 113 115 } 114 116 117 + function noDuplicateZombies(clone: Clone): boolean { 118 + const zombieSet = new Set<string>(); 119 + 120 + for (const zombie of clone.selectedZombies) { 121 + if (zombieSet.has(zombie)) { 122 + return false; 123 + } 124 + zombieSet.add(zombie); 125 + } 126 + 127 + return true; 128 + } 129 + 115 130 function noInvalidPlants(clone: Clone): boolean { 116 131 const validPlants = [ 117 132 "oPeashooter", ··· 155 170 return true; 156 171 } 157 172 173 + function noInvalidZombies(clone: Clone): boolean { 174 + const validZombies = new Set(iZombies); 175 + 176 + for (const zombie of clone.selectedZombies) { 177 + if (!validZombies.has(zombie)) { 178 + console.log(zombie, "is an invalid zombie type"); 179 + return false; 180 + } 181 + } 182 + 183 + return true; 184 + } 185 + 158 186 function cloneHasAllRequiredFields(clone: Clone): [boolean, string?] { 159 187 const requiredFields = ["plants", "music", "sun", "name", "lfValue", "stripeCol"]; 160 188 ··· 243 271 return [false, "Duplicate plant types in the same tile."]; 244 272 } 245 273 274 + if (!noDuplicateZombies(clone)) { 275 + console.error("Duplicate zombies in selected zombies."); 276 + return [false, "Duplicate zombies in selected zombies."]; 277 + } 278 + 246 279 if (!noInvalidPlants(clone)) { 247 280 console.error("Clone contains invalid plants."); 248 281 return [false, "Clone contains invalid plants."]; 282 + } 283 + 284 + if (!noInvalidZombies(clone)) { 285 + console.error("Clone contains invalid zombies."); 286 + return [false, "Clone contains invalid zombies."]; 249 287 } 250 288 251 289 if (!hasAllRequiredProperties(clone)) {