diff options
| -rw-r--r-- | server/bot/src/algos/customer.rs | 22 | ||||
| -rw-r--r-- | server/game-core/src/interaction.rs | 10 | ||||
| -rw-r--r-- | server/protocol/src/lib.rs | 17 | ||||
| -rw-r--r-- | server/src/commands.rs | 14 | ||||
| -rw-r--r-- | server/src/entity/ctf_minigame.rs | 13 | ||||
| -rw-r--r-- | server/src/entity/demand_sink.rs | 12 | ||||
| -rw-r--r-- | server/src/lib.rs | 9 | ||||
| -rw-r--r-- | server/src/server.rs | 10 | ||||
| -rw-r--r-- | server/src/state.rs | 4 | ||||
| -rw-r--r-- | test-client/main.ts | 5 | ||||
| -rw-r--r-- | test-client/map/effect.ts | 85 | ||||
| -rw-r--r-- | test-client/protocol.ts | 7 |
12 files changed, 160 insertions, 48 deletions
diff --git a/server/bot/src/algos/customer.rs b/server/bot/src/algos/customer.rs index 81a813e7..cc9f9c60 100644 --- a/server/bot/src/algos/customer.rs +++ b/server/bot/src/algos/customer.rs @@ -22,7 +22,8 @@ use crate::{ }; use hurrycurry_game_core::Game; use hurrycurry_protocol::{ - DemandIndex, Hand, ItemLocation, Message, PacketS, PlayerClass, PlayerID, Score, TileIndex, + DemandIndex, Effect, Hand, ItemLocation, Message, PacketS, PlayerClass, PlayerID, Score, + TileIndex, glam::{IVec2, Vec2}, }; use log::debug; @@ -213,14 +214,19 @@ impl CustomerState { player: me, pin: Some(false), }); + let points = -1; out.push(PacketS::ApplyScore(Score { - points: -1, + points, demands_failed: 1, ..Default::default() })); out.push(PacketS::Effect { - name: "angry".to_string(), - player: me, + effect: Effect::Angry, + location: ItemLocation::Player(me, Hand(0)), + }); + out.push(PacketS::Effect { + effect: Effect::Points { amount: points }, + location: ItemLocation::Player(me, Hand(0)), }); *self = CustomerState::Exiting { path }; return; @@ -309,8 +315,12 @@ impl CustomerState { pin: Some(true), }); out.push(PacketS::Effect { - name: "satisfied".to_string(), - player: me, + effect: Effect::Satisfied, + location: ItemLocation::Player(me, Hand(0)), + }); + out.push(PacketS::Effect { + effect: Effect::Points { amount: points }, + location: ItemLocation::Player(me, Hand(0)), }); out.push(PacketS::Interact { target: Some(ItemLocation::Tile(table)), diff --git a/server/game-core/src/interaction.rs b/server/game-core/src/interaction.rs index 1ca451d4..fd47a7f5 100644 --- a/server/game-core/src/interaction.rs +++ b/server/game-core/src/interaction.rs @@ -17,7 +17,7 @@ */ use crate::{Game, Involvement, Item}; use hurrycurry_locale::{TrError, tre}; -use hurrycurry_protocol::{ItemLocation, PacketC, Recipe}; +use hurrycurry_protocol::{Effect, ItemLocation, PacketC, Recipe}; use log::info; use std::collections::{BTreeSet, VecDeque}; @@ -224,7 +224,13 @@ impl Game { } self.score.points += pd; self.score.instant_recipes += 1; - self.events.push_back(PacketC::Score(self.score.clone())); + if *pd != 0 { + self.events.push_back(PacketC::Effect { + effect: Effect::Points { amount: *pd }, + location: this_loc, + }); + self.events.push_back(PacketC::Score(self.score.clone())); + } produce_events( &mut self.events, this_had_item, diff --git a/server/protocol/src/lib.rs b/server/protocol/src/lib.rs index d24b4973..9dd7e2f4 100644 --- a/server/protocol/src/lib.rs +++ b/server/protocol/src/lib.rs @@ -183,8 +183,8 @@ pub enum PacketS { /// For internal use only (customers) #[serde(skip)] Effect { - player: PlayerID, - name: String, + effect: Effect, + location: ItemLocation, }, /// Used internally and only used when built with debug_events @@ -287,8 +287,9 @@ pub enum PacketC { message: Option<Message>, timeout: Option<MessageTimeout>, }, - Effect2 { - name: String, + Effect { + #[serde(flatten)] + effect: Effect, location: ItemLocation, }, ServerMessage { @@ -369,6 +370,14 @@ pub struct GameConfig { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case", tag = "effect")] +pub enum Effect { + Satisfied, + Angry, + Points { amount: i64 }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case", tag = "menu", content = "data")] pub enum Menu { Score(Score), diff --git a/server/src/commands.rs b/server/src/commands.rs index 1d260631..05384849 100644 --- a/server/src/commands.rs +++ b/server/src/commands.rs @@ -23,9 +23,7 @@ use anyhow::Result; use clap::{Parser, ValueEnum}; use hurrycurry_bot::algos::ALGO_CONSTRUCTORS; use hurrycurry_locale::{TrError, tre, trm}; -use hurrycurry_protocol::{ - GameConfig, Hand, ItemLocation, Menu, Message, PacketC, PacketS, PlayerID, VoteSubject, -}; +use hurrycurry_protocol::{GameConfig, Menu, Message, PacketC, PacketS, PlayerID, VoteSubject}; use std::{fmt::Write, str::FromStr}; #[derive(Parser)] @@ -68,8 +66,6 @@ enum Command { }, /// Abort the current game End, - /// Send an effect - Effect { name: String }, /// Send an item message Item { name: String }, /// Send a tile message @@ -216,14 +212,6 @@ impl Server { Command::Book => { replies.push(PacketC::Menu(Menu::Book(self.priv_gamedata.book.clone()))) } - Command::Effect { name } => { - self.broadcast - .send(PacketC::Effect2 { - name, - location: ItemLocation::Player(player, Hand(0)), - }) - .ok(); - } Command::Item { name } => { let item = self .game diff --git a/server/src/entity/ctf_minigame.rs b/server/src/entity/ctf_minigame.rs index 03261e41..d2b23aee 100644 --- a/server/src/entity/ctf_minigame.rs +++ b/server/src/entity/ctf_minigame.rs @@ -18,6 +18,7 @@ use super::{Entity, EntityContext}; use anyhow::Result; use hurrycurry_locale::TrError; +use hurrycurry_protocol::{Effect, Hand}; use hurrycurry_protocol::{ItemIndex, ItemLocation, Message, PacketC, PlayerID, glam::IVec2}; use std::collections::{HashMap, HashSet}; use std::fmt::Write; @@ -36,11 +37,11 @@ struct TeamData { } impl TeamData { - fn effect(&self, c: &mut EntityContext, name: &str) -> Result<(), TrError> { + fn effect(&self, c: &mut EntityContext, effect: Effect) -> Result<(), TrError> { for pid in self.players.iter() { - c.packet_out.push_back(PacketC::Effect2 { - name: name.to_string(), - location: ItemLocation::Player(*pid, hurrycurry_protocol::Hand(0)), + c.packet_out.push_back(PacketC::Effect { + effect: effect.clone(), + location: ItemLocation::Player(*pid, Hand(0)), }); } Ok(()) @@ -257,14 +258,14 @@ impl Entity for CtfMinigame { .is_some_and(|a| a == from_team_idx) { for i in self.player_flags(&mut c, from)? { - self.teams.get(&i).unwrap().effect(&mut c, "angry")?; + self.teams.get(&i).unwrap().effect(&mut c, Effect::Angry)?; self.return_flag(&mut c, i)?; self.teams.get_mut(&from_team_idx).unwrap().score += 10; } self.teams .get(&from_team_idx) .unwrap() - .effect(&mut c, "satisfied")?; + .effect(&mut c, Effect::Satisfied)?; self.send_table(&mut c)?; } Ok(true) diff --git a/server/src/entity/demand_sink.rs b/server/src/entity/demand_sink.rs index 6fcfa643..45244e91 100644 --- a/server/src/entity/demand_sink.rs +++ b/server/src/entity/demand_sink.rs @@ -19,7 +19,7 @@ use crate::entity::{Entity, EntityContext}; use anyhow::Result; use hurrycurry_locale::TrError; -use hurrycurry_protocol::{ItemLocation, PacketC, glam::IVec2}; +use hurrycurry_protocol::{Effect, ItemLocation, PacketC, glam::IVec2}; pub struct DemandSink { pub pos: IVec2, @@ -36,8 +36,14 @@ impl Entity for DemandSink { c.game.score.demands_completed += 1; c.game.score.points += demand.points; *c.score_changed = true; - c.packet_out.push_back(PacketC::Effect2 { - name: "satisfied".to_string(), + c.packet_out.push_back(PacketC::Effect { + effect: Effect::Satisfied, + location: ItemLocation::Tile(self.pos), + }); + c.packet_out.push_back(PacketC::Effect { + effect: Effect::Points { + amount: demand.points, + }, location: ItemLocation::Tile(self.pos), }); } else { diff --git a/server/src/lib.rs b/server/src/lib.rs index b5dec60d..b0e93d05 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -62,7 +62,7 @@ mod test { ConnectionID, server::{Server, ServerConfig}, }; - use hurrycurry_protocol::{Character, GameConfig, PacketS, PlayerClass, PlayerID}; + use hurrycurry_protocol::{Character, GameConfig, Hand, PacketS, PlayerClass, PlayerID}; use tokio::sync::broadcast; fn server() -> Server { @@ -122,12 +122,13 @@ mod test { let mut s = server(); for (conn, p) in [ - PacketS::Effect { + PacketS::Leave { player: PlayerID(0), - name: "test".to_owned(), }, - PacketS::Leave { + PacketS::Interact { + hand: Hand(0), player: PlayerID(0), + target: None, }, PacketS::ReplayTick { dt: 1. }, ] diff --git a/server/src/server.rs b/server/src/server.rs index 9e91ff9c..9066475c 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -30,7 +30,7 @@ use hurrycurry_locale::{ tre, trm, }; use hurrycurry_protocol::{ - Character, GameConfig, Gamedata, Hand, ItemLocation, Menu, MessageTimeout, PacketC, PacketS, + Character, GameConfig, Gamedata, ItemLocation, Menu, MessageTimeout, PacketC, PacketS, PlayerClass, PlayerID, Score, Serverdata, VoteSubject, glam::Vec2, movement::MovementBase, }; use log::{info, warn}; @@ -353,11 +353,9 @@ impl Server { self.last_movement_update.remove(&player); self.vote_state.leave(player); } - PacketS::Effect { player, name } => { - self.packet_out.push_back(PacketC::Effect2 { - name, - location: ItemLocation::Player(player, Hand(0)), - }); + PacketS::Effect { effect, location } => { + self.packet_out + .push_back(PacketC::Effect { effect, location }); } PacketS::Movement { pos, diff --git a/server/src/state.rs b/server/src/state.rs index 12f6f6e4..150214ba 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -331,12 +331,12 @@ fn get_packet_player(packet: &PacketS) -> Option<PlayerID> { | PacketS::Communicate { player, .. } | PacketS::ReplaceHand { player, .. } | PacketS::InitiateVote { player, .. } - | PacketS::CastVote { player, .. } - | PacketS::Effect { player, .. } => Some(*player), + | PacketS::CastVote { player, .. } => Some(*player), PacketS::Join { .. } | PacketS::Idle { .. } | PacketS::Ready | PacketS::ApplyScore(_) + | PacketS::Effect { .. } | PacketS::ReplayTick { .. } | PacketS::Debug(_) | PacketS::Keepalive => None, diff --git a/test-client/main.ts b/test-client/main.ts index 0926ad0e..aea1713a 100644 --- a/test-client/main.ts +++ b/test-client/main.ts @@ -38,6 +38,7 @@ import { GridLayer } from "./map/mod.ts"; import { SConnectScreen } from "./connect_screen.ts"; import { SAnnouncement } from "./announcement.ts"; import { load_sprites, SSprites } from "./map/sprites.ts"; +import { SEffect } from "./map/effect.ts"; let ctx: CanvasRenderingContext2D; export let canvas: HTMLCanvasElement; @@ -99,13 +100,14 @@ const pinned_messages = new SPinnedMessages(sprites, players) const connect_screen = new SConnectScreen() export const camera = new SCamera(player_controller) const announcement = new SAnnouncement() +const effect = new SEffect(map_items) const systems: System[] = [ vote, chat, debug_events, server_hints, map_tiles, map_items, quick_commands, players, player_controller, camera, particles, server_message, score_display, pinned_messages, connect_screen, players_overlay, - announcement, sprites, + announcement, sprites, effect, ] const map_layers = [ new GridLayer(), @@ -116,6 +118,7 @@ const map_layers = [ player_controller, server_hints, players_overlay, + effect, debug_events, ] const ui_layers = [ diff --git a/test-client/map/effect.ts b/test-client/map/effect.ts new file mode 100644 index 00000000..5724c0ba --- /dev/null +++ b/test-client/map/effect.ts @@ -0,0 +1,85 @@ +/* + Hurry Curry! - a game about cooking + Copyright (C) 2026 Hurry Curry! Contributors + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, version 3 of the License only. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + +*/ + +import { int, PacketC } from "../protocol.ts"; +import { System } from "../system.ts"; +import { V2 } from "../util.ts"; +import { SMapItems } from "./item.ts"; +import { MapLayer } from "./mod.ts"; + + +export class SEffect extends System implements MapLayer { + effects: Set<EffectRenderer> = new Set() + + constructor(public is: SMapItems) { super() } + + override packet(p: PacketC): void { + if (p.type != "effect") return + const pos = { ...this.is.get_item_location_tracking(p.location) } + if (p.effect == "points") this.effects.add(new PointsEffect(pos, p.amount)) + } + + override tick(dt: number): void { + const remove = [] + for (const e of this.effects) + if (!e.tick(dt)) remove.push(e) + for (const e of remove) + this.effects.delete(e) + } + + draw_map_layer(ctx: CanvasRenderingContext2D): void { + for (const e of this.effects) + e.draw(ctx) + } +} + +abstract class EffectRenderer { + timer = 0 + constructor(public pos: V2) { } + abstract draw(ctx: CanvasRenderingContext2D): void; + tick(dt: number): boolean { + this.timer += dt + return this.timer < 3 + } +} + +class PointsEffect extends EffectRenderer { + constructor(pos: V2, public amount: int) { super(pos) } + override draw(ctx: CanvasRenderingContext2D): void { + ctx.save() + ctx.translate(this.pos.x, this.pos.y) + + ctx.translate(0, this.timer * -0.5) + + ctx.scale(0.03, 0.03) + if (this.timer < 0.5) ctx.scale(this.timer * 2, this.timer * 2) + ctx.globalAlpha = this.timer > 2 ? 3 - this.timer : 1 + ctx.font = "10px sans-serif" + ctx.fillStyle = this.amount < 0 ? "rgb(255, 106, 106)" : "rgb(157, 255, 124)" + ctx.strokeStyle = "black" + ctx.lineWidth = 2 + ctx.lineJoin = "round" + ctx.lineCap = "round" + const text = this.amount < 0 ? `${this.amount}` : `+${this.amount}` + ctx.strokeText(text, 0, 0) + ctx.fillText(text, 0, 0) + + + ctx.restore() + } +}
\ No newline at end of file diff --git a/test-client/protocol.ts b/test-client/protocol.ts index 465071b6..afa5101f 100644 --- a/test-client/protocol.ts +++ b/test-client/protocol.ts @@ -75,7 +75,7 @@ export type PacketC = | { type: "clear_progress", item: ItemLocation } | { type: "update_map", changes: [IVec2, TileIndex[]][] } // List of position-tiles-pairs of the map that changed | { type: "communicate", player: PlayerID, message?: Message, timeout?: MessageTimeout } // A player wants to communicate something, message is null when cleared - | { type: "effect2", location: ItemLocation, name: string } // Player sent an effect + | { type: "effect", location: ItemLocation } & Effect | { type: "server_message", message: Message, error: boolean } // Text message from the server | { type: "server_hint", message?: Message, position?: IVec2, player: PlayerID } // Hint message from server with optional position. Message is unset to clear previous message | { type: "vote_started", initiated_by: PlayerID, subject: VoteSubject, message: Message, timeout: float } @@ -97,6 +97,11 @@ export interface Character { hairstyle: int } +export type Effect = + { effect: "angry" } + | { effect: "satisfied" } + | { effect: "points", amount: int } + export type VoteSubject = { action: "start_game", config: GameConfig } | { action: "end_game" } |