From d0fcad0a72ee3405fd88ec8bf1fb586aef1b6075 Mon Sep 17 00:00:00 2001 From: Nick Kosarev Date: Tue, 14 May 2024 11:45:39 +0200 Subject: [PATCH] scripts! --- EVENTS.md | 4 + apps/api/src/bot/bot.controller.ts | 34 ++-- apps/api/src/bot/bot.service.ts | 73 ++++---- apps/api/src/config.ts | 7 +- apps/api/src/game/chunks/gameChunk.ts | 7 +- apps/api/src/game/chunks/village.ts | 148 +++++++++++++++- apps/api/src/game/common/event.ts | 5 +- apps/api/src/game/common/index.ts | 1 + apps/api/src/game/common/inventory.ts | 5 - apps/api/src/game/objects/gameObject.ts | 18 +- apps/api/src/game/objects/rabbit.ts | 5 +- apps/api/src/game/objects/units/mechanic.ts | 3 + apps/api/src/game/objects/units/player.ts | 101 ++++------- apps/api/src/game/objects/units/raider.ts | 19 +- apps/api/src/game/objects/units/unit.ts | 43 ++++- .../src/game/objects/units/villageCourier.ts | 100 +---------- .../src/game/objects/units/villageFarmer.ts | 65 +------ apps/api/src/game/objects/wagon.ts | 55 +++++- apps/api/src/game/objects/wolf.ts | 2 +- apps/api/src/game/scenes/defenceScene.ts | 20 --- apps/api/src/game/scenes/gameScene.ts | 162 +++++++++++------- apps/api/src/game/scripts/chopTreeScript.ts | 37 ++++ .../game/scripts/moveToRandomTargetScript.ts | 16 ++ .../scripts/placeItemInWarehouseScript.ts | 39 +++++ .../src/game/scripts/plantNewTreeScript.ts | 39 +++++ apps/api/src/game/scripts/script.ts | 87 ++++++++++ apps/client/index.html | 2 +- apps/client/src/components/eventCard.tsx | 81 +++++++-- apps/client/src/components/loader.tsx | 2 +- apps/client/src/game/objects/area.ts | 2 - apps/client/src/game/objects/units/player.ts | 23 +-- apps/client/src/game/objects/units/unit.ts | 24 +++ .../src/game/utils/generators/background.ts | 5 +- bun.lockb | Bin 121131 -> 121943 bytes package.json | 6 +- packages/api-sdk/src/lib/date.ts | 5 + packages/api-sdk/src/lib/random.ts | 7 +- packages/api-sdk/src/lib/types.ts | 20 ++- 38 files changed, 840 insertions(+), 432 deletions(-) create mode 100644 apps/api/src/game/scripts/chopTreeScript.ts create mode 100644 apps/api/src/game/scripts/moveToRandomTargetScript.ts create mode 100644 apps/api/src/game/scripts/placeItemInWarehouseScript.ts create mode 100644 apps/api/src/game/scripts/plantNewTreeScript.ts create mode 100644 apps/api/src/game/scripts/script.ts diff --git a/EVENTS.md b/EVENTS.md index 24a27d30..21f8ae5b 100644 --- a/EVENTS.md +++ b/EVENTS.md @@ -1,3 +1,7 @@ +## 2024-05-13 + +- Сценарии! Теперь юниты выполняют подготовленные наборы задач, а не "думают что им делать" + ## 2024-05-09 - В деревне теперь появляются доступные Приключения. Зрители голосуют командой "!го" diff --git a/apps/api/src/bot/bot.controller.ts b/apps/api/src/bot/bot.controller.ts index bf6db147..d54f9d56 100644 --- a/apps/api/src/bot/bot.controller.ts +++ b/apps/api/src/bot/bot.controller.ts @@ -1,5 +1,5 @@ import { RefreshingAuthProvider } from "@twurple/auth" -import { Bot } from "@twurple/easy-bot" +import { Bot, type BotCommand } from "@twurple/easy-bot" import { PubSubClient } from "@twurple/pubsub" import type { Game } from "../game/game" import { BotService } from "./bot.service" @@ -35,21 +35,22 @@ export class BotController { return authProvider } - prepareBotCommands() { + prepareBotCommands(): BotCommand[] { return [ - this.service.commandStartGroupBuild(), - this.service.commandJoinGroup(), - this.service.commandDisbandGroup(), - this.service.commandStartChangingScene(), - this.service.commandStartCreatingNewAdventure(), - this.service.commandRefuel(), - this.service.commandChop(), - this.service.commandMine(), - this.service.commandGift(), - this.service.commandSell(), - this.service.commandBuy(), - this.service.commandHelp(), - this.service.commandDonate(), + ...this.service.commandStartGroupBuild(), + ...this.service.commandJoinGroup(), + ...this.service.commandDisbandGroup(), + ...this.service.commandStartChangingScene(), + ...this.service.commandStartCreatingNewAdventure(), + ...this.service.commandRefuel(), + ...this.service.commandChop(), + ...this.service.commandMine(), + ...this.service.commandGift(), + ...this.service.commandSell(), + ...this.service.commandBuy(), + ...this.service.commandHelp(), + ...this.service.commandDonate(), + ...this.service.commandGithub(), ] } @@ -83,8 +84,7 @@ export class BotController { console.log("raid canceled!", event) }) - bot.onMessage(({ userId, userName, isAction, text }) => { - console.log("message", userId, isAction, text) + bot.onMessage(({ userId, userName, text }) => { void this.service.reactOnMessage({ userName, userId, text }) }) diff --git a/apps/api/src/bot/bot.service.ts b/apps/api/src/bot/bot.service.ts index d0c9c8ea..49450a0b 100644 --- a/apps/api/src/bot/bot.service.ts +++ b/apps/api/src/bot/bot.service.ts @@ -1,4 +1,4 @@ -import { createBotCommand } from "@twurple/easy-bot" +import { type BotCommand, createBotCommand } from "@twurple/easy-bot" import type { IGameSceneAction } from "../../../../packages/api-sdk/src" import { TWITCH_CHANNEL_REWARDS } from "../config" import type { Game } from "../game/game" @@ -10,73 +10,88 @@ export class BotService { this.game = game } - private buildCommand(commandName: string, action: IGameSceneAction) { - return createBotCommand( - commandName, - async (params, { userId, userName, reply }) => { - const result = await this.game.handleChatCommand({ - action, - userId, - userName, - params, - }) - if (result.message) { - void reply(result.message) - } - }, - ) + private buildCommand( + commandName: string[], + action: IGameSceneAction, + ): BotCommand[] { + const commands = [] + + for (const command of commandName) { + commands.push( + createBotCommand( + command, + async (params, { userId, userName, reply }) => { + const result = await this.game.handleChatCommand({ + action, + userId, + userName, + params, + }) + if (result.message) { + void reply(result.message) + } + }, + ), + ) + } + + return commands } public commandStartChangingScene() { - return this.buildCommand("вернуться", "START_CHANGING_SCENE") + return this.buildCommand(["вернуться"], "START_CHANGING_SCENE") } public commandStartGroupBuild() { - return this.buildCommand("собрать", "START_GROUP_BUILD") + return this.buildCommand(["собрать"], "START_GROUP_BUILD") } public commandJoinGroup() { - return this.buildCommand("го", "JOIN_GROUP") + return this.buildCommand(["го"], "JOIN_GROUP") } public commandDisbandGroup() { - return this.buildCommand("расформировать", "DISBAND_GROUP") + return this.buildCommand(["расформировать"], "DISBAND_GROUP") } public commandStartCreatingNewAdventure() { - return this.buildCommand("путешествовать", "START_CREATING_NEW_ADVENTURE") + return this.buildCommand(["путешествовать"], "START_CREATING_NEW_ADVENTURE") } public commandRefuel() { - return this.buildCommand("заправить", "REFUEL") + return this.buildCommand(["заправить", "з"], "REFUEL") } public commandChop() { - return this.buildCommand("рубить", "CHOP") + return this.buildCommand(["рубить", "р"], "CHOP") } public commandMine() { - return this.buildCommand("добыть", "MINE") + return this.buildCommand(["добыть", "добывать", "д"], "MINE") } public commandGift() { - return this.buildCommand("подарить", "GIFT") + return this.buildCommand(["подарить"], "GIFT") } public commandSell() { - return this.buildCommand("продать", "SELL") + return this.buildCommand(["продать"], "SELL") } public commandBuy() { - return this.buildCommand("купить", "BUY") + return this.buildCommand(["купить"], "BUY") } public commandHelp() { - return this.buildCommand("помощь", "HELP") + return this.buildCommand(["помощь"], "HELP") } public commandDonate() { - return this.buildCommand("донат", "DONATE") + return this.buildCommand(["донат"], "DONATE") + } + + public commandGithub() { + return this.buildCommand(["github"], "GITHUB") } public reactOnMessage({ diff --git a/apps/api/src/config.ts b/apps/api/src/config.ts index 091bbf29..4c78f98a 100644 --- a/apps/api/src/config.ts +++ b/apps/api/src/config.ts @@ -6,13 +6,10 @@ export const MAX_X = 2520 export const MIN_Y = 120 export const MAX_Y = 1190 -export const RAIDER_CAMP_MIN_X = 3000 -export const RAIDER_CAMP_MAX_X = 3500 -export const RAIDER_CAMP_MIN_Y = 120 -export const RAIDER_CAMP_MAX_Y = 1190 - export const DONATE_URL = "https://www.donationalerts.com/r/hmbanan666" export const DISCORD_SERVER_INVITE_URL = "https://discord.gg/B6etUajrGZ" +export const GITHUB_REPO_URL = + "https://github.com/hmbanan666/royal-madness-twitch-game" export const TWITCH_CHANNEL_REWARDS = { add150ViewerPointsId: "d8237822-c943-434f-9d7e-87a9f549f4c4", diff --git a/apps/api/src/game/chunks/gameChunk.ts b/apps/api/src/game/chunks/gameChunk.ts index 91ed56dd..50463148 100644 --- a/apps/api/src/game/chunks/gameChunk.ts +++ b/apps/api/src/game/chunks/gameChunk.ts @@ -84,9 +84,14 @@ export class GameChunk implements IGameChunk { } public getRandomOutPointOnRight() { + const height = this.area.area.endY - this.area.area.startY + return { x: this.area.area.endX, - y: getRandomInRange(this.area.area.startY, this.area.area.endY), + y: getRandomInRange( + this.area.area.startY + Math.round(height / 2), + this.area.area.endY, + ), } } diff --git a/apps/api/src/game/chunks/village.ts b/apps/api/src/game/chunks/village.ts index 40f0ed23..74d0d215 100644 --- a/apps/api/src/game/chunks/village.ts +++ b/apps/api/src/game/chunks/village.ts @@ -5,10 +5,15 @@ import { getRandomInRange, } from "../../../../../packages/api-sdk/src" import { Flag, Stone, Tree } from "../objects" +import { Building } from "../objects/buildings/building" import { Campfire } from "../objects/buildings/campfire" import { WagonStop } from "../objects/buildings/wagonStop" import { Warehouse } from "../objects/buildings/warehouse" import { VillageCourier, VillageFarmer } from "../objects/units" +import { ChopTreeScript } from "../scripts/chopTreeScript" +import { MoveToRandomTargetScript } from "../scripts/moveToRandomTargetScript" +import { PlaceItemInWarehouseScript } from "../scripts/placeItemInWarehouseScript" +import { PlantNewTreeScript } from "../scripts/plantNewTreeScript" import { GameChunk } from "./gameChunk" interface IVillageOptions { @@ -37,8 +42,112 @@ export class Village extends GameChunk implements IGameVillageChunk { live() { super.live() - for (const obj of this.objects) { - void obj.live() + for (const object of this.objects) { + // Check if NPC is without Script + if (object.entity === "FARMER" && !object.script) { + const target = this.checkIfNeedToPlantTree() + if (target) { + const plantNewTreeFunc = () => { + this.plantNewTree(target) + } + + object.script = new PlantNewTreeScript({ + object, + target, + plantNewTreeFunc, + }) + continue + } + + // No Trees needed? + const random = getRandomInRange(1, 300) + if (random <= 1) { + const target = this.getRandomMovementFlagInVillage() + if (!target) { + return + } + object.script = new MoveToRandomTargetScript({ + object, + target, + }) + continue + } + } + + if ( + object instanceof VillageCourier && + object.entity === "COURIER" && + !object.script + ) { + const random = getRandomInRange(1, 200) + if (random !== 1) { + continue + } + + // If unit have smth in inventory + const item = object.inventory.checkIfAlreadyHaveItem("WOOD") + if (item) { + const target = this.getWarehouse() + if (!target) { + continue + } + + const placeItemFunc = () => { + if (object.target instanceof Building) { + void object.target.inventory.addOrCreateItem( + item.type, + item.amount, + ) + void object.inventory.destroyItem(item.id) + } + } + object.script = new PlaceItemInWarehouseScript({ + object, + target, + placeItemFunc, + }) + + continue + } + + // If there is an available tree + const availableTree = this.getAvailableTree() + if (availableTree) { + const chopTreeFunc = (): boolean => { + object.chopTree() + if (!object.target || object.target.state === "DESTROYED") { + object.state = "IDLE" + if (object.target instanceof Tree) { + void object.inventory.addOrCreateItem( + "WOOD", + object.target?.resource, + ) + } + return true + } + return false + } + + object.script = new ChopTreeScript({ + object, + target: availableTree, + chopTreeFunc, + }) + + continue + } + + const target = this.getRandomMovementFlagInVillage() + if (!target) { + return + } + object.script = new MoveToRandomTargetScript({ + object, + target, + }) + } + + void object.live() } } @@ -87,7 +196,6 @@ export class Village extends GameChunk implements IGameVillageChunk { const randomPoint = this.getRandomPoint() this.objects.push( new VillageCourier({ - village: this, x: randomPoint.x, y: randomPoint.y, }), @@ -98,7 +206,6 @@ export class Village extends GameChunk implements IGameVillageChunk { const randomPoint = this.getRandomPoint() this.objects.push( new VillageFarmer({ - village: this, x: randomPoint.x, y: randomPoint.y, }), @@ -126,6 +233,12 @@ export class Village extends GameChunk implements IGameVillageChunk { ) } + getWarehouse() { + return this.objects.find((b) => b instanceof Warehouse) as + | Warehouse + | undefined + } + public getWagonStopPoint() { for (const object of this.objects) { if (object instanceof WagonStop) { @@ -140,7 +253,7 @@ export class Village extends GameChunk implements IGameVillageChunk { (f) => f instanceof Flag && f.type === "RESOURCE" && !f.target, ) return flags.length > 0 - ? flags[Math.floor(Math.random() * flags.length)] + ? (flags[Math.floor(Math.random() * flags.length)] as Flag) : undefined } @@ -178,8 +291,31 @@ export class Village extends GameChunk implements IGameVillageChunk { } } - plantNewTree(flag: Flag, tree: Tree) { + plantNewTree(flag: Flag) { + const tree = new Tree({ + x: flag.x, + y: flag.y, + resource: 1, + size: 12, + variant: this.area.theme, + }) + flag.target = tree this.objects.push(tree) } + + getAvailableTree(): Tree | undefined { + const trees = this.objects.filter( + (obj) => + obj instanceof Tree && + obj.state !== "DESTROYED" && + !obj.isReserved && + obj.isReadyToChop, + ) + if (!trees || !trees.length) { + return undefined + } + + return trees[Math.floor(Math.random() * trees.length)] as Tree + } } diff --git a/apps/api/src/game/common/event.ts b/apps/api/src/game/common/event.ts index 99ac143d..2c263d8b 100644 --- a/apps/api/src/game/common/event.ts +++ b/apps/api/src/game/common/event.ts @@ -2,6 +2,7 @@ import { createId } from "@paralleldrive/cuid2" import { type GameSceneType, type IGameEvent, + type IGameObjectPlayer, type IGamePoll, getDatePlusSeconds, } from "../../../../../packages/api-sdk/src" @@ -89,7 +90,7 @@ export class Event implements IGameEvent { } } - public vote(player: { id: string }): boolean { + public vote(player: IGameObjectPlayer): boolean { if (!this.poll) { return false } @@ -98,7 +99,7 @@ export class Event implements IGameEvent { return false } - this.poll.votes.push({ id: player.id }) + this.poll.votes.push({ id: player.id, userName: player.userName }) return true } } diff --git a/apps/api/src/game/common/index.ts b/apps/api/src/game/common/index.ts index ce8cc0b2..9b62d77f 100644 --- a/apps/api/src/game/common/index.ts +++ b/apps/api/src/game/common/index.ts @@ -3,3 +3,4 @@ export { Skill } from "./skill" export { Event } from "./event" export { Group } from "./group" export { Route } from "./route" +export { Script } from "../scripts/script" diff --git a/apps/api/src/game/common/inventory.ts b/apps/api/src/game/common/inventory.ts index be913b84..eca6fcc4 100644 --- a/apps/api/src/game/common/inventory.ts +++ b/apps/api/src/game/common/inventory.ts @@ -28,17 +28,12 @@ export class Inventory implements IGameInventory { await this.updateFromDB() } - public transferItemWithType(type: ItemType) { - return this.items.find((i) => i.type === type) - } - public async destroyItem(id: string) { const itemIndex = this.items.findIndex((i) => i.id === id) if (itemIndex < 0) { return } - await this.destroyItemInDB(id) this.items.splice(itemIndex, 1) } diff --git a/apps/api/src/game/objects/gameObject.ts b/apps/api/src/game/objects/gameObject.ts index 2d6de593..21754df4 100644 --- a/apps/api/src/game/objects/gameObject.ts +++ b/apps/api/src/game/objects/gameObject.ts @@ -2,6 +2,7 @@ import type { IGameObject, IGameObjectDirection, IGameObjectState, + IGameScript, } from "../../../../../packages/api-sdk/src" import { sendMessage } from "../../websocket/websocket.server" @@ -18,12 +19,15 @@ export class GameObject implements IGameObject { public x: number public y: number public health = 100 + public speed = 1 public isVisibleOnClient: boolean public entity: IGameObject["entity"] public direction: IGameObjectDirection = "RIGHT" public state: IGameObjectState = "IDLE" - public target: IGameObject | undefined + public target: IGameObject["target"] + public script: IGameScript | undefined + public minDistance = 1 public needToSendDataToClient: boolean public isOnWagonPath = false @@ -39,8 +43,8 @@ export class GameObject implements IGameObject { live(): void {} - move(speed: number, minDistance?: number) { - const isOnTarget = this.checkIfIsOnTarget(minDistance) + move() { + const isOnTarget = this.checkIfIsOnTarget() if (isOnTarget) { this.stop() return false @@ -55,7 +59,8 @@ export class GameObject implements IGameObject { const distanceToY = this.getDistanceToTargetY() // Fix diagonal speed - const finalSpeed = distanceToX > 0 && distanceToY > 0 ? speed * 0.75 : speed + const finalSpeed = + distanceToX > 0 && distanceToY > 0 ? this.speed * 0.75 : this.speed this.moveX(finalSpeed > distanceToX ? distanceToX : finalSpeed) this.moveY(finalSpeed > distanceToY ? distanceToY : finalSpeed) @@ -94,9 +99,10 @@ export class GameObject implements IGameObject { this.state = "IDLE" } - checkIfIsOnTarget(minDistance = 1) { + checkIfIsOnTarget() { return ( - this.getDistanceToTargetX() + this.getDistanceToTargetY() <= minDistance + this.getDistanceToTargetX() + this.getDistanceToTargetY() <= + this.minDistance ) } diff --git a/apps/api/src/game/objects/rabbit.ts b/apps/api/src/game/objects/rabbit.ts index e225c1de..124994af 100644 --- a/apps/api/src/game/objects/rabbit.ts +++ b/apps/api/src/game/objects/rabbit.ts @@ -13,6 +13,9 @@ export class Rabbit extends GameObject implements IGameObjectRabbit { const y = getRandomInRange(MIN_Y, MAX_Y) super({ id, x, y, entity: "RABBIT" }) + + this.speed = 0.5 + this.minDistance = 5 } live() { @@ -21,7 +24,7 @@ export class Rabbit extends GameObject implements IGameObjectRabbit { } if (this.state === "MOVING") { - const isMoving = this.move(1) + const isMoving = this.move() this.handleChange() if (!isMoving) { diff --git a/apps/api/src/game/objects/units/mechanic.ts b/apps/api/src/game/objects/units/mechanic.ts index 2936b5bb..673ce687 100644 --- a/apps/api/src/game/objects/units/mechanic.ts +++ b/apps/api/src/game/objects/units/mechanic.ts @@ -25,12 +25,15 @@ export class Mechanic extends Unit implements IGameObjectMechanic { } live() { + super.live() this.handleChange() } handleChange() { const prepared = { ...this, + script: undefined, + live: undefined, } this.sendMessageObjectUpdated(prepared) diff --git a/apps/api/src/game/objects/units/player.ts b/apps/api/src/game/objects/units/player.ts index fbd2fb6e..eca4f224 100644 --- a/apps/api/src/game/objects/units/player.ts +++ b/apps/api/src/game/objects/units/player.ts @@ -5,7 +5,6 @@ import { type ItemType, getRandomInRange, } from "../../../../../../packages/api-sdk/src" -import { MAX_X, MAX_Y, MIN_X, MIN_Y } from "../../../config" import { db } from "../../../db/db.client" import { Inventory, Skill } from "../../common" import { Stone } from "../stone" @@ -14,8 +13,8 @@ import { Unit } from "./unit" interface IPlayerOptions { id?: string - x?: number - y?: number + x: number + y: number } export class Player extends Unit implements IGameObjectPlayer { @@ -23,7 +22,8 @@ export class Player extends Unit implements IGameObjectPlayer { public reputation = 0 public villainPoints = 0 public refuellerPoints = 0 - public userName = "NPC" + public userName = "" + public lastActionAt: IGameObjectPlayer["lastActionAt"] = new Date() public health = 100 public inventoryId?: string @@ -33,10 +33,9 @@ export class Player extends Unit implements IGameObjectPlayer { constructor({ id, x, y }: IPlayerOptions) { const objectId = id ?? createId() - const finalX = x ?? getRandomInRange(MIN_X, MAX_X) - const finalY = y ?? getRandomInRange(MIN_Y, MAX_Y) + super({ id: objectId, x, y, entity: "PLAYER" }) - super({ id: objectId, x: finalX, y: finalY, entity: "PLAYER" }) + this.speed = 2 } async init() { @@ -52,58 +51,20 @@ export class Player extends Unit implements IGameObjectPlayer { live() { this.handleMessages() - if (this.state === "IDLE") { - this.handleChange() - return - } - - if (this.state === "MOVING") { - const isMoving = this.move(2) - this.handleChange() - - if (!isMoving && this.target) { - if (this.target instanceof Tree) { - void this.startChopping() - return - } - if (this.target instanceof Stone) { - void this.startMining() - return - } - } - - return - } + super.live() + this.handleChange() if (this.state === "CHOPPING") { if (this.target instanceof Tree) { - // Skill up on random - const random = getRandomInRange(1, 200) - if (random <= 1) { - const skill = this.skills.find((skill) => skill.type === "WOODSMAN") - if (skill) { - void skill.addXp() - } - } - - // Check instrument - const axe = this.inventory.items.find((item) => item.type === "AXE") - if (axe) { - this.target.health -= 0.16 - const random = getRandomInRange(1, 40) - if (random <= 1) { - void this.inventory.checkAndBreakItem(axe, 1) - } - } - - this.target.chop() - this.handleChange() - - if (this.target.health <= 0) { - void this.stopChopping(this.target) + this.chopTree() + this.upSkillWoodsman() + } + if (this.target?.state === "DESTROYED") { + this.state = "IDLE" + if (this.target instanceof Tree) { + void this.inventory.addOrCreateItem("WOOD", this.target?.resource) } } - return } @@ -143,7 +104,13 @@ export class Player extends Unit implements IGameObjectPlayer { } handleChange() { - this.sendMessageObjectUpdated() + const prepared = { + ...this, + script: undefined, + live: undefined, + } + + this.sendMessageObjectUpdated(prepared) } async startChopping() { @@ -152,14 +119,7 @@ export class Player extends Unit implements IGameObjectPlayer { await this.findOrCreateSkillInDB("WOODSMAN") - await this.updateInDB() - this.handleChange() - } - - async stopChopping(tree: Tree) { - this.state = "IDLE" - // Reward - await this.inventory.addOrCreateItem("WOOD", tree.resource) + await this.updateLastActionAt() this.handleChange() } @@ -169,7 +129,7 @@ export class Player extends Unit implements IGameObjectPlayer { await this.findOrCreateSkillInDB("MINER") - await this.updateInDB() + await this.updateLastActionAt() this.handleChange() } @@ -265,7 +225,8 @@ export class Player extends Unit implements IGameObjectPlayer { this.inventoryId = player.inventoryId } - public updateInDB() { + public updateLastActionAt() { + this.lastActionAt = new Date() return db.player.update({ where: { id: this.id }, data: { @@ -308,4 +269,14 @@ export class Player extends Unit implements IGameObjectPlayer { return skill } + + public upSkillWoodsman() { + const random = getRandomInRange(1, 200) + if (random <= 1) { + const skill = this.skills.find((skill) => skill.type === "WOODSMAN") + if (skill) { + void skill.addXp() + } + } + } } diff --git a/apps/api/src/game/objects/units/raider.ts b/apps/api/src/game/objects/units/raider.ts index 5186f876..a28fc6fa 100644 --- a/apps/api/src/game/objects/units/raider.ts +++ b/apps/api/src/game/objects/units/raider.ts @@ -26,18 +26,23 @@ export class Raider extends Unit implements IGameObjectRaider { top: "BLACK_SHIRT", }, }) + + this.speed = 1.5 } live() { - if (this.state === "IDLE") { - this.sendMessageObjectUpdated() - return - } + super.live() + this.handleChange() + } - if (this.state === "MOVING") { - this.sendMessageObjectUpdated() - return + handleChange() { + const prepared = { + ...this, + script: undefined, + live: undefined, } + + this.sendMessageObjectUpdated(prepared) } moveOutOfScene(target: GameObject) { diff --git a/apps/api/src/game/objects/units/unit.ts b/apps/api/src/game/objects/units/unit.ts index af1b7a3a..77b33117 100644 --- a/apps/api/src/game/objects/units/unit.ts +++ b/apps/api/src/game/objects/units/unit.ts @@ -4,15 +4,15 @@ import { type IGameObjectUnit, getRandomInRange, } from "../../../../../../packages/api-sdk/src" -import { MAX_X, MAX_Y, MIN_X, MIN_Y } from "../../../config" import { Inventory } from "../../common" import { GameObject } from "../gameObject" +import { Tree } from "../tree" interface IUnitOptions { entity: IGameObject["entity"] id?: string - x?: number - y?: number + x: number + y: number visual?: IGameObjectUnit["visual"] } @@ -25,10 +25,7 @@ export class Unit extends GameObject implements IGameObjectUnit { constructor({ entity, id, x, y, visual }: IUnitOptions) { const objectId = id ?? createId() - const finalX = x ?? getRandomInRange(MIN_X, MAX_X) - const finalY = y ?? getRandomInRange(MIN_Y, MAX_Y) - - super({ id: objectId, x: finalX, y: finalY, entity }) + super({ id: objectId, x, y, entity }) this.initInventory() this.initVisual(visual) @@ -36,6 +33,13 @@ export class Unit extends GameObject implements IGameObjectUnit { this.coins = 0 } + live() { + if (this.script) { + this.script.live() + return + } + } + public initInventory() { this.inventory = new Inventory({ objectId: this.id, @@ -76,4 +80,29 @@ export class Unit extends GameObject implements IGameObjectUnit { this.dialogue.messages.splice(0, 1) } } + + public chopTree() { + if (this.target instanceof Tree && this.target.state !== "DESTROYED") { + this.state = "CHOPPING" + this.checkAndBreakAxe() + + this.target.chop() + } + } + + async stopChopping(tree: Tree) { + this.state = "IDLE" + await this.inventory.addOrCreateItem("WOOD", tree.resource) + } + + checkAndBreakAxe() { + const axe = this.inventory.items.find((item) => item.type === "AXE") + if (axe) { + //this.target.health -= 0.16 + const random = getRandomInRange(1, 40) + if (random <= 1) { + void this.inventory.checkAndBreakItem(axe, 1) + } + } + } } diff --git a/apps/api/src/game/objects/units/villageCourier.ts b/apps/api/src/game/objects/units/villageCourier.ts index 1b0e5b04..8d3611b3 100644 --- a/apps/api/src/game/objects/units/villageCourier.ts +++ b/apps/api/src/game/objects/units/villageCourier.ts @@ -1,23 +1,14 @@ import { createId } from "@paralleldrive/cuid2" -import { - type IGameObjectCourier, - type ItemType, - getRandomInRange, -} from "../../../../../../packages/api-sdk/src" -import type { Village } from "../../chunks" -import { Building } from "../buildings/building" +import type { IGameObjectCourier } from "../../../../../../packages/api-sdk/src" import { Unit } from "./unit" interface ICourierOptions { - village: Village x: number y: number } export class VillageCourier extends Unit implements IGameObjectCourier { - private village: Village - - constructor({ village, x, y }: ICourierOptions) { + constructor({ x, y }: ICourierOptions) { const id = createId() super({ @@ -32,95 +23,22 @@ export class VillageCourier extends Unit implements IGameObjectCourier { }, }) - this.village = village + this.speed = 2.5 + this.minDistance = 15 } - async live() { - if (this.state === "IDLE") { - // const playerWithWood = scene.findUnitWithItem("WOOD"); - // if (playerWithWood) { - // this.setTarget(playerWithWood); - // return; - // } - // - // const playerWithStone = scene.findUnitWithItem("STONE"); - // if (playerWithStone) { - // this.setTarget(playerWithStone); - // return; - // } - - const random = getRandomInRange(1, 100) - if (random <= 1) { - const randObj = this.village.getRandomMovementFlagInVillage() - if (!randObj) { - return - } - this.setTarget(randObj) - } - - this.handleChange() - return - } - - if (this.state === "MOVING") { - const isMoving = this.move(2, 12) - this.handleChange() - - if (!isMoving && this.target) { - // if (this.target instanceof Player) { - // await this.takeItemFromUnit("WOOD"); - // await this.takeItemFromUnit("STONE"); - // - // const warehouse = scene.findBuildingByType("WAREHOUSE"); - // if (warehouse) { - // return this.setTarget(warehouse); - // } - // } - // - // if (this.target instanceof Building) { - // await this.placeItemInBuilding("WOOD"); - // await this.placeItemInBuilding("STONE"); - // } - } - - return - } + live() { + super.live() + this.handleChange() } handleChange() { const prepared = { ...this, - village: undefined, + script: undefined, + live: undefined, } this.sendMessageObjectUpdated(prepared) } - - async takeItemFromUnit(type: ItemType) { - if (this.target instanceof Unit) { - const item = this.target.inventory.transferItemWithType(type) - if (item) { - await this.inventory.addOrCreateItem(item.type, item.amount) - this.target.inventory.destroyItem(item.id) - - return true - } - } - - return false - } - - async placeItemInBuilding(type: ItemType) { - if (this.target instanceof Building) { - const item = this.inventory.transferItemWithType(type) - if (item) { - await this.target.inventory.addOrCreateItem(item.type, item.amount) - this.inventory.destroyItem(item.id) - - return true - } - } - - return false - } } diff --git a/apps/api/src/game/objects/units/villageFarmer.ts b/apps/api/src/game/objects/units/villageFarmer.ts index 799638bb..13d5d90a 100644 --- a/apps/api/src/game/objects/units/villageFarmer.ts +++ b/apps/api/src/game/objects/units/villageFarmer.ts @@ -1,23 +1,14 @@ import { createId } from "@paralleldrive/cuid2" -import { - type IGameObjectFarmer, - getRandomInRange, -} from "../../../../../../packages/api-sdk/src" -import type { Village } from "../../chunks" -import { Flag } from "../flag" -import { Tree } from "../tree" +import type { IGameObjectFarmer } from "../../../../../../packages/api-sdk/src" import { Unit } from "./unit" interface IVillageFarmerOptions { - village: Village x: number y: number } export class VillageFarmer extends Unit implements IGameObjectFarmer { - private village: Village - - constructor({ village, x, y }: IVillageFarmerOptions) { + constructor({ x, y }: IVillageFarmerOptions) { const id = createId() super({ @@ -31,62 +22,18 @@ export class VillageFarmer extends Unit implements IGameObjectFarmer { top: "GREEN_SHIRT", }, }) - - this.village = village } live() { - if (this.state === "IDLE") { - const flagToPlantNewTree = this.village.checkIfNeedToPlantTree() - if (flagToPlantNewTree) { - this.target = flagToPlantNewTree - this.state = "MOVING" - - this.handleChange() - return - } - - const random = getRandomInRange(1, 300) - if (random <= 1) { - const randomObj = this.village.getRandomMovementFlagInVillage() - if (!randomObj) { - return - } - this.setTarget(randomObj) - } - - this.handleChange() - return - } - - if (this.state === "MOVING") { - const isMoving = this.move(1, 4) - - if (!isMoving && this.target) { - if (this.target instanceof Flag && this.target.type === "RESOURCE") { - const tree = new Tree({ - x: this.target.x, - y: this.target.y, - resource: 1, - size: 12, - variant: this.village.area.theme, - }) - this.village.plantNewTree(this.target, tree) - } - - this.state = "IDLE" - this.target = undefined - } - - this.handleChange() - return - } + super.live() + this.handleChange() } handleChange() { const prepared = { ...this, - village: undefined, + script: undefined, + live: undefined, } this.sendMessageObjectUpdated(prepared) diff --git a/apps/api/src/game/objects/wagon.ts b/apps/api/src/game/objects/wagon.ts index 7b9a2c75..13148840 100644 --- a/apps/api/src/game/objects/wagon.ts +++ b/apps/api/src/game/objects/wagon.ts @@ -1,5 +1,9 @@ import { createId } from "@paralleldrive/cuid2" -import type { IGameObjectWagon } from "../../../../../packages/api-sdk/src" +import { + type IGameObjectWagon, + getMinusOrPlus, + getRandomInRange, +} from "../../../../../packages/api-sdk/src" import { Flag } from "./flag" import { GameObject } from "./gameObject" import { Mechanic } from "./units" @@ -10,7 +14,6 @@ interface IWagonOptions { } export class Wagon extends GameObject implements IGameObjectWagon { - public speed: number public fuel: number public visibilityArea!: IGameObjectWagon["visibilityArea"] @@ -18,6 +21,7 @@ export class Wagon extends GameObject implements IGameObjectWagon { public serverDataArea!: IGameObjectWagon["visibilityArea"] public collisionArea!: IGameObjectWagon["visibilityArea"] public nearFlags: Flag[] = [] + public outFlags: Flag[] = [] constructor({ x, y }: IWagonOptions) { const finalId = createId() @@ -29,6 +33,7 @@ export class Wagon extends GameObject implements IGameObjectWagon { this.updateVisibilityArea() this.updateServerDataArea() this.initNearFlags() + this.initOutFlags(10) this.initMechanic() } @@ -36,7 +41,7 @@ export class Wagon extends GameObject implements IGameObjectWagon { this.updateVisibilityArea() this.updateServerDataArea() this.updateCollisionArea() - this.updateNearFlags() + this.updateFlags() this.updateMechanic() this.consumeFuel() @@ -170,10 +175,46 @@ export class Wagon extends GameObject implements IGameObjectWagon { this.nearFlags.push(flag1, flag2, flag3, flag4) } - updateNearFlags() { - for (const nearFlag of this.nearFlags) { - nearFlag.x = this.x + nearFlag.offsetX - nearFlag.y = this.y + nearFlag.offsetY + updateFlags() { + for (const flag of this.nearFlags) { + flag.x = this.x + flag.offsetX + flag.y = this.y + flag.offsetY } + for (const flag of this.outFlags) { + flag.x = this.x + flag.offsetX + flag.y = this.y + flag.offsetY + } + } + + public findRandomNearFlag() { + return this.nearFlags[Math.floor(Math.random() * this.nearFlags.length)] + } + + initOutFlags(count: number) { + for (let i = 0; i < count; i++) { + this.outFlags.push(this.generateRandomOutFlag()) + } + } + + generateRandomOutFlag() { + const minOffsetX = 1800 + const minOffsetY = 1200 + + const offsetX = + getRandomInRange(minOffsetX, minOffsetX * 1.5) * getMinusOrPlus() + const offsetY = + getRandomInRange(minOffsetY, minOffsetY * 1.5) * getMinusOrPlus() + + return new Flag({ + type: "OUT_OF_SCREEN", + x: this.x + offsetX, + y: this.y + offsetY, + offsetX, + offsetY, + }) + } + + public findRandomOutFlag() { + return this.outFlags[Math.floor(Math.random() * this.outFlags.length)] } } diff --git a/apps/api/src/game/objects/wolf.ts b/apps/api/src/game/objects/wolf.ts index 3f9a5386..298266bf 100644 --- a/apps/api/src/game/objects/wolf.ts +++ b/apps/api/src/game/objects/wolf.ts @@ -22,7 +22,7 @@ export class Wolf extends GameObject implements IGameObjectWolf { } if (this.state === "MOVING") { - this.move(1) + this.move() this.sendMessageObjectUpdated() return diff --git a/apps/api/src/game/scenes/defenceScene.ts b/apps/api/src/game/scenes/defenceScene.ts index cb627411..1ac1e2ad 100644 --- a/apps/api/src/game/scenes/defenceScene.ts +++ b/apps/api/src/game/scenes/defenceScene.ts @@ -2,7 +2,6 @@ import { getRandomInRange } from "../../../../../packages/api-sdk/src" import type { Group } from "../common" import type { Game } from "../game" import { Stone, Tree } from "../objects" -import { Player } from "../objects/units" import { GameScene } from "./gameScene" interface IDefenceSceneOptions { @@ -59,25 +58,6 @@ export class DefenceScene extends GameScene { } } - async initPlayer(id: string) { - const instance = new Player({ id }) - await instance.init() - - const spawnFlag = this.findSpawnFlag("SPAWN_LEFT") - if (spawnFlag) { - instance.x = spawnFlag.x - instance.y = spawnFlag.y - } - - const targetFlag = this.findRandomMovementFlag() - if (targetFlag) { - instance.target = targetFlag - instance.state = "MOVING" - } - - return instance - } - private initTrees(count: number) { for (let i = 0; i < count; i++) { const flag = this.findRandomEmptyResourceFlag() diff --git a/apps/api/src/game/scenes/gameScene.ts b/apps/api/src/game/scenes/gameScene.ts index fd0b266e..0492711f 100644 --- a/apps/api/src/game/scenes/gameScene.ts +++ b/apps/api/src/game/scenes/gameScene.ts @@ -9,12 +9,14 @@ import { type IGameRoute, type IGameSceneAction, type ItemType, + getDateMinusMinutes, getRandomInRange, } from "../../../../../packages/api-sdk/src" import { ADMIN_PLAYER_ID, DISCORD_SERVER_INVITE_URL, DONATE_URL, + GITHUB_REPO_URL, SERVER_TICK_MS, } from "../../config" import { Forest, type GameChunk, LakeChunk, Village } from "../chunks" @@ -53,8 +55,6 @@ export class GameScene { this.game = game this.group = group this.possibleActions = possibleActions - - this.initSpawnFlags() } public async play() { @@ -143,6 +143,9 @@ export class GameScene { if (action === "HELP") { return this.helpAction(player) } + if (action === "GITHUB") { + return this.githubAction(player) + } if (action === "DONATE") { return this.donateAction(player) } @@ -248,6 +251,7 @@ export class GameScene { } updateObjects() { + this.removeInactivePlayers() const wagon = this.getWagon() for (const obj of this.objects) { @@ -324,7 +328,7 @@ export class GameScene { updatePolls() { if (this.chunkNow instanceof Village) { - this.generateNewPollForNewAdventure() + this.generatePollForNewAdventure() } for (const event of this.events) { if (event.type === "VOTING_FOR_NEW_ADVENTURE_STARTED") { @@ -385,7 +389,7 @@ export class GameScene { } if (object.state === "MOVING") { object.speed = 0.5 - const isMoving = object.move(object.speed) + const isMoving = object.move() object.handleChange() if (!isMoving) { @@ -408,14 +412,41 @@ export class GameScene { object.live() if (object.state === "IDLE") { - const random = getRandomInRange(1, 120) + const random = getRandomInRange(1, 150) if (random <= 1) { - const randObj = this.findRandomNearWagonFlag() + const randObj = this.getWagon().findRandomNearFlag() if (!randObj) { return } object.setTarget(randObj) } + object.handleChange() + return + } + if (object.state === "MOVING") { + const isMoving = object.move() + object.handleChange() + + if (!isMoving && object.target) { + if (object.target instanceof Tree) { + void object.startChopping() + return + } + if (object.target instanceof Stone) { + void object.startMining() + return + } + if ( + object.target instanceof Flag && + object.target.type === "OUT_OF_SCREEN" + ) { + this.removeObject(object) + return + } + + object.state = "IDLE" + return + } } } @@ -464,12 +495,13 @@ export class GameScene { } if (object.state === "MOVING") { - const isMoving = object.move(1) + const isMoving = object.move() if (!isMoving) { - if (object.target?.id === "SPAWN_LEFT") { - // Destroy - const index = this.objects.indexOf(object) - this.objects.splice(index, 1) + if ( + object.target instanceof Flag && + object.target.type === "OUT_OF_SCREEN" + ) { + this.removeObject(object) } object.state = "IDLE" @@ -478,6 +510,11 @@ export class GameScene { } } + removeObject(object: GameObject) { + const index = this.objects.indexOf(object) + this.objects.splice(index, 1) + } + getAvailableCommands() { const commands: string[] = [] for (const action of this.possibleActions) { @@ -525,16 +562,36 @@ export class GameScene { } } + public findActivePlayers() { + return this.objects.filter((obj) => obj instanceof Player) as Player[] + } + + public removeInactivePlayers() { + const players = this.findActivePlayers() + for (const player of players) { + const checkTime = getDateMinusMinutes(8) + if (player.lastActionAt.getTime() <= checkTime.getTime()) { + if ( + player.target instanceof Flag && + player.target.type === "OUT_OF_SCREEN" + ) { + continue + } + + player.target = this.getWagon().findRandomOutFlag() + player.state = "MOVING" + } + } + } + async initPlayer(id: string) { - const instance = new Player({ id }) + const instance = new Player({ id, x: -100, y: -100 }) await instance.init() await instance.initInventoryFromDB() - const wagon = this.getWagon() - if (wagon) { - instance.x = wagon.x - 250 - instance.y = wagon.y - } + const flag = this.getWagon().findRandomOutFlag() + instance.x = flag.x + instance.y = flag.y return instance } @@ -959,13 +1016,9 @@ export class GameScene { const wagon = this.getWagon() for (let i = 0; i < count; i++) { - const x = wagon.visibilityArea.endX - const y = getRandomInRange( - wagon.visibilityArea.startY, - wagon.visibilityArea.endY, - ) + const flag = wagon.findRandomOutFlag() - this.objects.push(new Raider({ x, y })) + this.objects.push(new Raider({ x: flag.x, y: flag.y })) } } @@ -984,10 +1037,7 @@ export class GameScene { } public stopRaid() { - const flag = this.findSpawnFlag("SPAWN_LEFT") - if (!flag) { - return - } + const flag = this.getWagon().findRandomOutFlag() for (const obj of this.objects) { if (obj instanceof Raider) { @@ -1005,7 +1055,20 @@ export class GameScene { } return { ok: true, - message: `${player.userName}, это интерактивная игра-чат, в которой может участвовать любой зритель! Пиши команды (примеры на экране) для управления своим героем. Вступай в наше комьюнити: ${DISCORD_SERVER_INVITE_URL}`, + message: `${player.userName}, это интерактивная игра-чат, в которой может участвовать любой зритель! Пиши команды (примеры на экране) для управления своим юнитом. Вступай в наше комьюнити: ${DISCORD_SERVER_INVITE_URL}`, + } + } + + public githubAction(player: Player) { + if (!this.checkIfActionIsPossible("GITHUB")) { + return { + ok: false, + message: null, + } + } + return { + ok: true, + message: `${player.userName}, код игры находится в репозитории: ${GITHUB_REPO_URL}`, } } @@ -1395,15 +1458,6 @@ export class GameScene { } } - findRandomNearWagonFlag() { - const wagon = this.getWagon() - if (!wagon) { - return undefined - } - - return wagon.nearFlags[Math.floor(Math.random() * wagon.nearFlags.length)] - } - findRandomMovementFlag() { const flags = this.chunkNow?.objects.filter( (f) => f instanceof Flag && f.type === "MOVEMENT", @@ -1426,31 +1480,16 @@ export class GameScene { : undefined } - initSpawnFlags() { - const spawnLeftFlag = new Flag({ - x: -300, - y: 620, - id: "SPAWN_LEFT", - type: "SPAWN_LEFT", - }) - const spawnRightFlag = new Flag({ - x: 2700, - y: 620, - id: "SPAWN_RIGHT", - type: "SPAWN_RIGHT", - }) - this.objects.push(spawnLeftFlag, spawnRightFlag) - } - - findSpawnFlag(id: "SPAWN_LEFT" | "SPAWN_RIGHT") { - return this.objects.find((f) => f.id === id) - } + generatePollForNewAdventure() { + const random = getRandomInRange(1, 1500) + if (random !== 1) { + return + } - generateNewPollForNewAdventure() { const votingEvents = this.events.filter( (e) => e.type === "VOTING_FOR_NEW_ADVENTURE_STARTED", ) - if (votingEvents.length >= 3) { + if (votingEvents.length >= 4) { return } @@ -1462,6 +1501,9 @@ export class GameScene { return } + const votesToSuccess = + this.findActivePlayers().length >= 2 ? this.findActivePlayers().length : 2 + this.initEvent({ type: "VOTING_FOR_NEW_ADVENTURE_STARTED", title: "Новый квест", @@ -1469,7 +1511,7 @@ export class GameScene { poll: { votes: [], status: "STARTED", - votesToSuccess: 4, + votesToSuccess, id: this.generatePollId(), }, }) @@ -1487,7 +1529,7 @@ export class GameScene { return id } - findActivePollAndVote(pollId: string, player: { id: string }) { + findActivePollAndVote(pollId: string, player: Player) { for (const event of this.events) { if (event.type === "VOTING_FOR_NEW_ADVENTURE_STARTED") { if (event.poll?.id === pollId) { diff --git a/apps/api/src/game/scripts/chopTreeScript.ts b/apps/api/src/game/scripts/chopTreeScript.ts new file mode 100644 index 00000000..107b33df --- /dev/null +++ b/apps/api/src/game/scripts/chopTreeScript.ts @@ -0,0 +1,37 @@ +import type { + IGameObject, + IGameTask, +} from "../../../../../packages/api-sdk/src" +import type { GameObject } from "../objects" +import { Script } from "./script" + +interface IPlantNewTreeScriptOptions { + object: GameObject + target: IGameObject + chopTreeFunc: () => boolean +} + +export class ChopTreeScript extends Script { + constructor({ target, object, chopTreeFunc }: IPlantNewTreeScriptOptions) { + super({ object }) + + this.tasks = [ + this.setTarget(target), + this.runToTarget(), + this.chopTree(chopTreeFunc), + ] + } + + chopTree(func: () => boolean): IGameTask { + return { + id: "3", + status: "IDLE", + live: () => { + const isFinished = func() + if (isFinished) { + this.markTaskAsDone() + } + }, + } + } +} diff --git a/apps/api/src/game/scripts/moveToRandomTargetScript.ts b/apps/api/src/game/scripts/moveToRandomTargetScript.ts new file mode 100644 index 00000000..4e6332f7 --- /dev/null +++ b/apps/api/src/game/scripts/moveToRandomTargetScript.ts @@ -0,0 +1,16 @@ +import type { IGameObject } from "../../../../../packages/api-sdk/src" +import type { GameObject } from "../objects" +import { Script } from "./script" + +interface IMoveToRandomTargetScriptOptions { + object: GameObject + target: IGameObject +} + +export class MoveToRandomTargetScript extends Script { + constructor({ target, object }: IMoveToRandomTargetScriptOptions) { + super({ object }) + + this.tasks = [this.setTarget(target), this.runToTarget()] + } +} diff --git a/apps/api/src/game/scripts/placeItemInWarehouseScript.ts b/apps/api/src/game/scripts/placeItemInWarehouseScript.ts new file mode 100644 index 00000000..a4f5747c --- /dev/null +++ b/apps/api/src/game/scripts/placeItemInWarehouseScript.ts @@ -0,0 +1,39 @@ +import type { + IGameObject, + IGameTask, +} from "../../../../../packages/api-sdk/src" +import type { GameObject } from "../objects" +import { Script } from "./script" + +interface IPlaceItemInWarehouseScriptOptions { + object: GameObject + target: IGameObject + placeItemFunc: () => void +} + +export class PlaceItemInWarehouseScript extends Script { + constructor({ + target, + object, + placeItemFunc, + }: IPlaceItemInWarehouseScriptOptions) { + super({ object }) + + this.tasks = [ + this.setTarget(target), + this.runToTarget(), + this.placeItem(placeItemFunc), + ] + } + + placeItem(func: () => void): IGameTask { + return { + id: "3", + status: "IDLE", + live: () => { + func() + this.markTaskAsDone() + }, + } + } +} diff --git a/apps/api/src/game/scripts/plantNewTreeScript.ts b/apps/api/src/game/scripts/plantNewTreeScript.ts new file mode 100644 index 00000000..a6e5b7df --- /dev/null +++ b/apps/api/src/game/scripts/plantNewTreeScript.ts @@ -0,0 +1,39 @@ +import type { + IGameObject, + IGameTask, +} from "../../../../../packages/api-sdk/src" +import type { GameObject } from "../objects" +import { Script } from "./script" + +interface IPlantNewTreeScriptOptions { + object: GameObject + target: IGameObject + plantNewTreeFunc: () => void +} + +export class PlantNewTreeScript extends Script { + constructor({ + target, + object, + plantNewTreeFunc, + }: IPlantNewTreeScriptOptions) { + super({ object }) + + this.tasks = [ + this.setTarget(target), + this.runToTarget(), + this.plantNewTree(plantNewTreeFunc), + ] + } + + plantNewTree(func: () => void): IGameTask { + return { + id: "3", + status: "IDLE", + live: () => { + func() + this.markTaskAsDone() + }, + } + } +} diff --git a/apps/api/src/game/scripts/script.ts b/apps/api/src/game/scripts/script.ts new file mode 100644 index 00000000..00b56640 --- /dev/null +++ b/apps/api/src/game/scripts/script.ts @@ -0,0 +1,87 @@ +import { createId } from "@paralleldrive/cuid2" +import type { + IGameObject, + IGameScript, + IGameTask, +} from "../../../../../packages/api-sdk/src" +import type { GameObject } from "../objects" + +interface IScriptOptions { + object: GameObject +} + +export class Script implements IGameScript { + public id: string + public tasks!: IGameScript["tasks"] + + public object!: GameObject + + constructor({ object }: IScriptOptions) { + this.id = createId() + this.object = object + } + + live() { + const activeTask = this.getActiveTask() + if (!activeTask) { + const nextTask = this.getNextIdleTask() + if (!nextTask) { + return this.markScriptAsFinished() + } + + return this.markTaskAsActive(nextTask) + } + + return activeTask.live() + } + + getActiveTask() { + return this.tasks.find((t) => t.status === "ACTIVE") + } + + getNextIdleTask() { + return this.tasks.find((t) => t.status === "IDLE") + } + + markTaskAsActive(task: IGameTask) { + task.status = "ACTIVE" + } + + markTaskAsDone() { + const activeTask = this.getActiveTask() + if (!activeTask) { + return + } + + activeTask.status = "DONE" + } + + markScriptAsFinished() { + this.object.script = undefined + } + + setTarget(target: IGameObject): IGameTask { + return { + id: createId(), + status: "IDLE", + live: () => { + this.object.target = target + this.object.state = "MOVING" + this.markTaskAsDone() + }, + } + } + + runToTarget(): IGameTask { + return { + id: createId(), + status: "IDLE", + live: () => { + const isMoving = this.object.move() + if (!isMoving) { + this.markTaskAsDone() + } + }, + } + } +} diff --git a/apps/client/index.html b/apps/client/index.html index dc6b4001..607e89e2 100644 --- a/apps/client/index.html +++ b/apps/client/index.html @@ -5,7 +5,7 @@ Royal Madness: Twitch Chat Game - +
diff --git a/apps/client/src/components/eventCard.tsx b/apps/client/src/components/eventCard.tsx index 3d71cf56..ae164bae 100644 --- a/apps/client/src/components/eventCard.tsx +++ b/apps/client/src/components/eventCard.tsx @@ -1,4 +1,4 @@ -import type { IGameEvent } from "../../../../packages/api-sdk/src" +import type { IGameEvent, IGamePoll } from "../../../../packages/api-sdk/src" import { useCountdown } from "../hooks/useCountdown" export const EventCard = ({ event }: { event: IGameEvent }) => { @@ -12,10 +12,14 @@ export const EventCard = ({ event }: { event: IGameEvent }) => { const description = getEventDescriptionByType(event) return ( -
-

{event.title}

+
+

{event.title}

{description} -

+ + + +

Заканчивается через {minutes}:{secondsWithZero}

@@ -36,15 +40,72 @@ function getEventDescriptionByType(event: IGameEvent) { if (event.type === "VOTING_FOR_NEW_ADVENTURE_STARTED") { return (
-

+

Проголосуем за это приключение? Пишите в чат команду:

-

!го {event.poll?.id}

- -

- Голосов: {event.poll?.votes.length} из {event.poll?.votesToSuccess} -

+

!го {event.poll?.id}

) } } + +const PollProgressBar = ({ + poll, + }: { + poll: IGamePoll | undefined +}) => { + if (!poll) { + return null + } + + const pollProgressBarWidth = Math.round( + poll.votes.length / (poll.votesToSuccess / 100), + ) + + const thumbUpIcon = ( + + + + + ) + + const showUserNames = poll.votes.map((v) => { + return ( +
+ {thumbUpIcon} + {v.userName} +
+ ) + }) + + return ( + <> +
+
+ Голосов: {poll.votes.length} из {poll.votesToSuccess} +
+
+
+ +
{showUserNames}
+ + ) +} diff --git a/apps/client/src/components/loader.tsx b/apps/client/src/components/loader.tsx index 23c8271a..8be4a234 100644 --- a/apps/client/src/components/loader.tsx +++ b/apps/client/src/components/loader.tsx @@ -1,7 +1,7 @@ export const Loader = ({ isVisible }: { isVisible: boolean }) => { return (
diff --git a/apps/client/src/game/objects/area.ts b/apps/client/src/game/objects/area.ts index e809ff9b..23b390f6 100644 --- a/apps/client/src/game/objects/area.ts +++ b/apps/client/src/game/objects/area.ts @@ -25,8 +25,6 @@ export class Area extends GameObjectContainer implements IGameObjectArea { bg.width = this.area.endX - this.area.startX bg.height = this.area.endY - this.area.startY this.addChild(bg) - - console.log(bg, this.theme) } animate() { diff --git a/apps/client/src/game/objects/units/player.ts b/apps/client/src/game/objects/units/player.ts index ab847d04..89ad757f 100644 --- a/apps/client/src/game/objects/units/player.ts +++ b/apps/client/src/game/objects/units/player.ts @@ -17,6 +17,7 @@ export class Player extends Unit implements IGameObjectPlayer { refuellerPoints!: number userName!: string skills!: IGameSkill[] + lastActionAt!: IGameObjectPlayer["lastActionAt"] constructor({ game, object }: IPlayerOptions) { super({ game, object }) @@ -37,7 +38,6 @@ export class Player extends Unit implements IGameObjectPlayer { this.interface.animate() this.showToolInHand() - this.handleSoundByState() } showToolInHand() { @@ -61,26 +61,6 @@ export class Player extends Unit implements IGameObjectPlayer { } } - handleSoundByState() { - if (this.state === "CHOPPING") { - if (this.inventory?.items.find((item) => item.type === "AXE")) { - this.game.audio.playChopWithAxeSound() - return - } - - this.game.audio.playHandPunch() - } - - if (this.state === "MINING") { - if (this.inventory?.items.find((item) => item.type === "PICKAXE")) { - this.game.audio.playMineWithPickaxeSound() - return - } - - this.game.audio.playHandPunch() - } - } - update(object: IGameObjectPlayer) { super.update(object) @@ -89,5 +69,6 @@ export class Player extends Unit implements IGameObjectPlayer { this.refuellerPoints = object.refuellerPoints this.userName = object.userName this.skills = object.skills + this.lastActionAt = object.lastActionAt } } diff --git a/apps/client/src/game/objects/units/unit.ts b/apps/client/src/game/objects/units/unit.ts index 772fd25f..dd53b008 100644 --- a/apps/client/src/game/objects/units/unit.ts +++ b/apps/client/src/game/objects/units/unit.ts @@ -23,6 +23,7 @@ export class Unit extends GameObjectContainer implements IGameObjectUnit { public inventory!: IGameInventory public visual!: IGameObjectUnit["visual"] public coins = 0 + public speed = 0 public dialogue!: IGameObjectUnit["dialogue"] public interface!: UnitInterface @@ -120,6 +121,8 @@ export class Unit extends GameObjectContainer implements IGameObjectUnit { this.interface.animate() this.dialogueInterface.animate() + this.handleSoundByState() + if (this.target && this.target instanceof Flag) { this.target.visible = true } @@ -172,6 +175,27 @@ export class Unit extends GameObjectContainer implements IGameObjectUnit { this.inventory = object.inventory this.visual = object.visual this.coins = object.coins + this.speed = object.speed this.dialogue = object.dialogue } + + handleSoundByState() { + if (this.state === "CHOPPING") { + if (this.inventory?.items.find((item) => item.type === "AXE")) { + this.game.audio.playChopWithAxeSound() + return + } + + this.game.audio.playHandPunch() + } + + if (this.state === "MINING") { + if (this.inventory?.items.find((item) => item.type === "PICKAXE")) { + this.game.audio.playMineWithPickaxeSound() + return + } + + this.game.audio.playHandPunch() + } + } } diff --git a/apps/client/src/game/utils/generators/background.ts b/apps/client/src/game/utils/generators/background.ts index 6c3f1f65..0c7f97f9 100644 --- a/apps/client/src/game/utils/generators/background.ts +++ b/apps/client/src/game/utils/generators/background.ts @@ -98,8 +98,8 @@ export class BackgroundGenerator { if (theme === "VIOLET") { this.mainColor1 = "0x6b3e75" this.mainColor2 = "0x905ea9" - this.accentColor1 = "0xeaaded" - this.accentColor2 = "0xf57d4a" + this.accentColor1 = "0xfdcbb0" + this.accentColor2 = "0xfbb954" this.accentColor3 = "0x8fd3ff" } if (theme === "BLUE") { @@ -200,6 +200,7 @@ export class BackgroundGenerator { } } + // Result: array console.log(pixels) return imageData diff --git a/bun.lockb b/bun.lockb index 007e941dcff30dee0cafbcdf807563aaa5f030a2..b1b41417b337029c6a19425f6dc75e9589b71a22 100644 GIT binary patch delta 2706 zcmc&$eN>di8lOQHcNbRyQ5FG#ML8K$dO6oz#X=xULrBoLe$`#<+P#XI zz*JLarIi|ow2uuJ5F|2BifC?tBBa)}s~o+WL{c6T?{D7SZT+`%I_J*WXMVruH_!XN zGtcb%&Qt&C=yQ#ut79Ve*9ZKV+txpHc)~4h*VC(lOFItiKeoo}jS53y`sFhz*S_kf zM6|dXW1!1L-6!h?=n03AoWvd}(vhA_SvJSA>5?hSaJSDNV-&yDrge;S^tzg9x8v-kn*b~bL zSs}5LUC0u+85I#=LPApSArlo0wg^Uqm~`Occ3Wf{Fy{EL=_KHD;TT{o_JGrP!Cj>qSP?caN~y}TpzWK{L8sg2tP_Z-N%8CvbdPweTGnWHysS(5G68#Ht- z^UqQBg`t;w(Gxza;- z2M^r6RDV@jS~d9M0gri)wY(bHm#A!cePP$3@z1+2(Ug1~;##<DS7G&fE7 zVsdKA_wLVDS5=13Prp&~!fkf%tU7yr(NXo%V~HU*Tm~c8dw6^|-F^1h&hD50{Y`d5 z$g$+=)Hy$F{%J|sv`p`)h`foWoexL6^?uM{SM}>9A$|A09S%=^V0OXmle0#uVTyA` z8o~OV2ky1RCJ}$T<_5nN9L+=m?BZy6%WWc=Y}dpaE&~@26Z9;9_iw6sMO?HnQC=hDxGz*pMCI z;({={woVU%=AFpL>gYfDZXNXw_y$@RtQ#gidY{9@$FT|4Y&oW*@s!K;)ZeoM!bw;s zOnfa{EfIS959)G+@`DC?aqb~#t6^ffZm@DZX%$SIy>;+au)l$eC5a`78H*Xd46BA^ zB%(umB}2?k;~5QCq(Y=pq}bnbCysWx#fW{G1QY2L>6?OU!eB>XB26aXJlHhwD9W#- zQjN?2uIH*W>Zd%O0ghH&#*@-$nAjM;G>v9a9j{NLdaAR0mqy>FG|pnmp!Jb_xR~mF z9QDG+|1LkZ#p}Apg=%B8F$w&km2}c59Y{V1-Le}PW5=qhOOVt)1%9nV7= xXfQu>in{S#r|De$1+Jc^Gid_Xo~B{^<_7A4&DHWvYY}zNO2=3heocE6{{wO}k7)n^ delta 2322 zcmd5;Yfw{16y6gE1g5DK1p-k50aR3C!XqG13{(M$ysDV0GZey8sywRrsue_WM5{=* z&Z?g^;W z$prn%+7*MM;zS&a2`Py}wnskpE+KZvfjAzCJUx{V2jtc?LOhW7nRp$tJC0M4J&{9@ z$019Q<;c%*o*Qx>@>t|1WG=rNS%&OF2zi~5kdp7Rh7)R#r5K>d666n&osct-F}WlT z*%>*|6qgx%><2`W)|VXz_E;r2owo65(kjqu+=y(A z+>DH;PEK>d|CmMieVwN7MV`XH;9rhcj1BYCZj1g83s<~ip?ug0iE?{++s-<2qu%mJg4h6f*yf($mTV~Wes_U7b z{bT9$IeJ_Wkf9Mn$TI5Dt)p#SVp`U%1rOTMtphpr>Cu5FU5nO>_Mr8qs$L!K?h(`N zy;>MgulMR;0-btU2R^hC?L>MD?Iap;MF*4VPPD%C!4)0daYam%`?N5H?&;Hkl0HML zqDfbE;71Rk^*6vzHO_cnt;PC<`HS)DkjrT7=$@Mw4E0Z2L+mH0hIh}}j}7`k7?b;h zQ}IO8jym&&3}1*B!>33j=;JPk6=H$O=W5F1E=;y*uGHicqEBd=GsfhzL0_3Ek2^h1 z+9Echz*qutL&3d-yCIuVb3u1x{BXa$WQ)n?Ve)Y|v;sAkFE{y=ekky1R5Q1!AZIHA zAVbAn;~x-b5bcPwh;xYZhz&Sp2KGD{Tw zu5NG4^Fo|Na5Wbr#}oFp2INAnHV?K^15yWHl#>yu;b`#rdNHfkz&O^Xfml$nQ4tUT zDq~CpR09MU2O^$jNyLU>|;NC!M7>_T+-th&+DwVTv{GM3BNC%Zg*?FhOl@s!fMurj5}B@3thE z+wRS0{_pjBS_v!>n#J4-KoV|l)JJz^N}@l>J!#1>xANo)P3h;cKYLmtByac_Nj_Uz z0Fejk3jmyz`NK|X`deZjRumlWu3}$mKw=yygkB+YSOy;K_(~8NZ!d@3d2DM3#EMjk frP-_WMR~0DJVv1t&r@Fu?(BXSh>fM4aMA8>q^dOH diff --git a/package.json b/package.json index dc1f73e4..0b994c6d 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@twurple/easy-bot": "7.1.0", "@twurple/eventsub-http": "7.1.0", "@twurple/pubsub": "7.1.0", - "hono": "4.3.4", + "hono": "4.3.6", "howler": "2.2.4", "pixi.js": "8.1.1", "react": "18.3.1", @@ -44,9 +44,9 @@ "devDependencies": { "@biomejs/biome": "1.7.3", "@tailwindcss/vite": "4.0.0-alpha.15", - "@types/bun": "1.1.1", + "@types/bun": "1.1.2", "@types/howler": "2.2.11", - "@types/react": "18.3.1", + "@types/react": "18.3.2", "@types/react-dom": "18.3.0", "@vitejs/plugin-react": "4.2.1", "dependency-cruiser": "16.3.2", diff --git a/packages/api-sdk/src/lib/date.ts b/packages/api-sdk/src/lib/date.ts index 28406556..1170c50e 100644 --- a/packages/api-sdk/src/lib/date.ts +++ b/packages/api-sdk/src/lib/date.ts @@ -3,6 +3,11 @@ export function getDatePlusMinutes(minutes: number) { return new Date(new Date().getTime() + milliseconds) } +export function getDateMinusMinutes(minutes: number) { + const milliseconds = minutes * 60 * 1000 + return new Date(new Date().getTime() - milliseconds) +} + export function getDatePlusSeconds(seconds: number) { const milliseconds = seconds * 1000 return new Date(new Date().getTime() + milliseconds) diff --git a/packages/api-sdk/src/lib/random.ts b/packages/api-sdk/src/lib/random.ts index 535c25cd..81a8fdea 100644 --- a/packages/api-sdk/src/lib/random.ts +++ b/packages/api-sdk/src/lib/random.ts @@ -1,4 +1,9 @@ -export function getRandomInRange(min: number, max: number) { +export function getRandomInRange(min: number, max: number): number { const ceilMin = Math.ceil(min) return Math.floor(Math.random() * (max - ceilMin + 1)) + ceilMin } + +export function getMinusOrPlus(): number { + // -1 or 1 + return Math.round(Math.random()) * 2 - 1 +} diff --git a/packages/api-sdk/src/lib/types.ts b/packages/api-sdk/src/lib/types.ts index aa03fa1f..93a3a0e8 100644 --- a/packages/api-sdk/src/lib/types.ts +++ b/packages/api-sdk/src/lib/types.ts @@ -15,6 +15,7 @@ export type IGameSceneAction = | "CREATE_NEW_PLAYER" | "START_CREATING_NEW_ADVENTURE" | "SHOW_MESSAGE" + | "GITHUB" export type ItemType = "WOOD" | "STONE" | "AXE" | "PICKAXE" @@ -78,6 +79,7 @@ export interface IGameObject { entity: IGameObjectEntity target: IGameObject | undefined health: number + speed: number isVisibleOnClient: boolean } @@ -121,7 +123,6 @@ export interface WebSocketMessage { } export interface IGameObjectWagon extends IGameObject { - speed: number fuel: number visibilityArea: { startX: number @@ -151,6 +152,7 @@ export interface IGameObjectFlag extends IGameObject { | "RESOURCE" | "SPAWN_LEFT" | "SPAWN_RIGHT" + | "OUT_OF_SCREEN" } export interface IGameObjectWater extends IGameObject {} @@ -213,6 +215,7 @@ export interface IGameObjectPlayer extends IGameObjectUnit { refuellerPoints: number userName: string skills: IGameSkill[] + lastActionAt: Date } export interface IGameObjectRaider extends IGameObjectUnit { @@ -224,6 +227,19 @@ export interface IGameObjectRabbit extends IGameObject {} export interface IGameObjectWolf extends IGameObject {} +export interface IGameScript { + id: string + tasks: IGameTask[] + live: () => void +} + +export interface IGameTask { + id: string + status: "IDLE" | "ACTIVE" | "DONE" + target?: IGameObject + live: () => void +} + export interface IGameEvent { id: string title: string @@ -237,7 +253,7 @@ export interface IGamePoll { status: "STARTED" | "STOPPED" id: string votesToSuccess: number - votes: { id: string }[] + votes: { id: string; userName: string }[] } export type GameSceneType = "VILLAGE" | "DEFENCE" | "MOVING"