send fire emoji to other players

This commit is contained in:
evanpelle
2024-10-04 13:08:20 -07:00
parent b35e78f2f8
commit e795b22220
13 changed files with 182 additions and 27 deletions
+3 -3
View File
@@ -157,17 +157,17 @@
* BUG: boat icon appears when click inland DONE 10/2/2024
* Auto deploy to dev on commit DONE 10/2/2024
* add emoji button
* make disabled icon crossed out icon
* donate troops button
* Make fake humans spawn by their country
* BUG: double tap zooms on mobile
* fake humans target enemies
* make year clock
* create private lobby menu
* block user inputs if too far behind server
* BUG: FakeHuman don't be enemy if don't share border (or send boat)
* store cookies
* BUG: names dissapear on bottom of screen
* UI: leader board
* UI: boats
* UI: current attacks
* Load terrain dataImage in background
* BUG: half encircle Antartica causes capture
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+17 -1
View File
@@ -1,5 +1,5 @@
import {EventBus, GameEvent} from "../core/EventBus"
import {AllianceRequest, Cell, Game, Player, PlayerID, PlayerType} from "../core/game/Game"
import {AllianceRequest, AllPlayers, Cell, Emoji, Player, PlayerID, PlayerType} from "../core/game/Game"
import {ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema} from "../core/Schemas"
@@ -9,6 +9,7 @@ export class SendAllianceRequestIntentEvent implements GameEvent {
public readonly recipient: Player
) { }
}
export class SendBreakAllianceIntentEvent implements GameEvent {
constructor(
public readonly requestor: Player,
@@ -49,6 +50,10 @@ export class SendTargetPlayerIntentEvent implements GameEvent {
) { }
}
export class SendEmojiIntentEvent implements GameEvent {
constructor(public readonly recipient: Player | typeof AllPlayers, public readonly emoji: Emoji) { }
}
export class Transport {
public onconnect: () => {}
@@ -68,6 +73,7 @@ export class Transport {
this.eventBus.on(SendAttackIntentEvent, (e) => this.onSendAttackIntent(e))
this.eventBus.on(SendBoatAttackIntentEvent, (e) => this.onSendBoatAttackIntent(e))
this.eventBus.on(SendTargetPlayerIntentEvent, (e) => this.onSendTargetPlayerIntent(e))
this.eventBus.on(SendEmojiIntentEvent, (e) => this.onSendEmojiIntent(e))
}
connect(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) {
@@ -200,6 +206,16 @@ export class Transport {
})
}
private onSendEmojiIntent(event: SendEmojiIntentEvent) {
this.sendIntent({
type: "emoji",
clientID: this.clientID,
sender: this.playerID,
recipient: event.recipient == AllPlayers ? AllPlayers : event.recipient.id(),
emoji: event.emoji
})
}
private sendIntent(intent: Intent) {
if (this.socket.readyState === WebSocket.OPEN) {
const msg = ClientIntentMessageSchema.parse({
+17 -1
View File
@@ -1,6 +1,6 @@
import {nullable} from "zod";
import {EventBus, GameEvent} from "../../../core/EventBus";
import {AllianceExpiredEvent, AllianceRequestEvent, AllianceRequestReplyEvent, BrokeAllianceEvent, Game, Player, PlayerID, TargetPlayerEvent} from "../../../core/game/Game";
import {AllianceExpiredEvent, AllianceRequestEvent, AllianceRequestReplyEvent, BrokeAllianceEvent, EmojiMessageEvent, Game, Player, PlayerID, TargetPlayerEvent} from "../../../core/game/Game";
import {ClientID} from "../../../core/Schemas";
import {Layer} from "./Layer";
import {SendAllianceReplyIntentEvent} from "../../Transport";
@@ -53,6 +53,7 @@ export class EventsDisplay implements Layer {
this.eventBus.on(BrokeAllianceEvent, e => this.onBrokeAllianceEvent(e))
this.eventBus.on(AllianceExpiredEvent, e => this.onAllianceExpiredEvent(e))
this.eventBus.on(TargetPlayerEvent, e => this.onTargetPlayerEvent(e))
this.eventBus.on(EmojiMessageEvent, e => this.onEmojiMessageEvent(e))
this.renderTable()
}
@@ -226,6 +227,21 @@ export class EventsDisplay implements Layer {
}
}
onEmojiMessageEvent(event: EmojiMessageEvent) {
const myPlayer = this.game.playerByClientID(this.clientID)
if (myPlayer == null) {
return
}
if (event.message.recipient == myPlayer) {
this.addEvent({
description: `${event.message.sender.name()}:${event.message.emoji}`,
type: MessageType.INFO,
highlight: true,
createdAt: this.game.ticks(),
})
}
}
addEvent(event: Event): void {
this.events.push(event);
this.renderTable()
+14 -11
View File
@@ -1,9 +1,9 @@
import {EventBus} from "../../../core/EventBus";
import {Cell, Game, Player, PlayerID} from "../../../core/game/Game";
import {AllPlayers, Cell, Emoji, Game, Player, PlayerID} from "../../../core/game/Game";
import {ClientID} from "../../../core/Schemas";
import {and, bfs, dist, manhattanDist, manhattanDistWrapped, sourceDstOceanShore} from "../../../core/Util";
import {ContextMenuEvent, MouseUpEvent} from "../../InputHandler";
import {SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBoatAttackIntentEvent, SendBreakAllianceIntentEvent, SendSpawnIntentEvent, SendTargetPlayerIntentEvent} from "../../Transport";
import {SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBoatAttackIntentEvent, SendBreakAllianceIntentEvent, SendEmojiIntentEvent, SendSpawnIntentEvent, SendTargetPlayerIntentEvent} from "../../Transport";
import {TransformHandler} from "../TransformHandler";
import {MessageType} from "./EventsDisplay";
import {Layer} from "./Layer";
@@ -13,20 +13,14 @@ import allianceIcon from '../../../../resources/images/AllianceIconWhite.png';
import boatIcon from '../../../../resources/images/BoatIconWhite.png';
import swordIcon from '../../../../resources/images/SwordIconWhite.png';
import targetIcon from '../../../../resources/images/TargetIconWhite.png';
import emojiIcon from '../../../../resources/images/EmojiIconWhite.png';
enum RadialElement {
RequestAlliance,
BreakAlliance,
BoatAttack,
Target,
}
enum Slot {
Alliance,
Boat,
Target,
FOURTH
Emoji
}
export class RadialMenu implements Layer {
@@ -38,7 +32,7 @@ export class RadialMenu implements Layer {
[Slot.Alliance, {name: "alliance", disabled: true, action: () => { }, color: null, icon: null, defaultIcon: allianceIcon}],
[Slot.Boat, {name: "boat", disabled: true, action: () => { }, color: null, icon: null, defaultIcon: boatIcon}],
[Slot.Target, {name: "target", disabled: true, action: () => { }, defaultIcon: targetIcon}],
[Slot.Emoji, {name: "emoji", disabled: true, action: () => { }, defaultIcon: emojiIcon}],
]);
private readonly menuSize = 190;
@@ -230,6 +224,15 @@ export class RadialMenu implements Layer {
return
}
if (tile.hasOwner()) {
const target = tile.owner() == myPlayer ? AllPlayers : (tile.owner() as Player)
this.activateMenuElement(Slot.Emoji, "#ebe250", emojiIcon, () => {
this.eventBus.emit(
new SendEmojiIntentEvent(target, Emoji.Fire)
)
})
}
if (tile.owner() != myPlayer && tile.isLand() && myPlayer.sharesBorderWith(other)) {
if (other.isPlayer()) {
if (!myPlayer.isAlliedWith(other)) {
+16 -3
View File
@@ -1,5 +1,5 @@
import {z} from 'zod';
import {PlayerType} from './game/Game';
import {Emoji, PlayerType} from './game/Game';
export type GameID = string
export type ClientID = string
@@ -12,6 +12,7 @@ export type Intent = SpawnIntent
| AllianceRequestReplyIntent
| BreakAllianceIntent
| TargetPlayerIntent
| EmojiIntent
export type AttackIntent = z.infer<typeof AttackIntentSchema>
export type SpawnIntent = z.infer<typeof SpawnIntentSchema>
@@ -21,7 +22,7 @@ export type AllianceRequestIntent = z.infer<typeof AllianceRequestIntentSchema>
export type AllianceRequestReplyIntent = z.infer<typeof AllianceRequestReplyIntentSchema>
export type BreakAllianceIntent = z.infer<typeof BreakAllianceIntentSchema>
export type TargetPlayerIntent = z.infer<typeof TargetPlayerIntentSchema>
export type EmojiIntent = z.infer<typeof EmojiIntentSchema>
export type Turn = z.infer<typeof TurnSchema>
@@ -37,6 +38,8 @@ export type ClientJoinMessage = z.infer<typeof ClientJoinMessageSchema>
export type ClientLeaveMessage = z.infer<typeof ClientLeaveMessageSchema>
const PlayerTypeSchema = z.nativeEnum(PlayerType);
const EmojiSchema = z.nativeEnum(Emoji);
// TODO: create Cell schema
@@ -46,9 +49,11 @@ export interface Lobby {
numClients: number;
}
// Zod schemas
const BaseIntentSchema = z.object({
type: z.enum(['attack', 'spawn', 'boat', 'name']),
type: z.enum(['attack', 'spawn', 'boat', 'name', 'targetPlayer', 'emoji']),
clientID: z.string(),
});
@@ -112,6 +117,13 @@ export const TargetPlayerIntentSchema = BaseIntentSchema.extend({
target: z.string(),
})
export const EmojiIntentSchema = BaseIntentSchema.extend({
type: z.literal('emoji'),
sender: z.string(),
recipient: z.string(),
emoji: EmojiSchema,
})
const IntentSchema = z.union([
AttackIntentSchema,
SpawnIntentSchema,
@@ -121,6 +133,7 @@ const IntentSchema = z.union([
AllianceRequestReplyIntentSchema,
BreakAllianceIntentSchema,
TargetPlayerIntentSchema,
EmojiIntentSchema,
]);
const TurnSchema = z.object({
+2
View File
@@ -51,6 +51,8 @@ export interface Config {
allianceRequestCooldown(): Tick
targetDuration(): Tick
targetCooldown(): Tick
emojiMessageCooldown(): Tick
emojiMessageDuration(): Tick
}
export interface Theme {
+6
View File
@@ -7,6 +7,12 @@ import {pastelTheme} from "./PastelTheme";
export class DefaultConfig implements Config {
emojiMessageDuration(): Tick {
return 5 * 10
}
emojiMessageCooldown(): Tick {
return 15 * 10
}
targetDuration(): Tick {
return 10 * 10
}
+7 -7
View File
@@ -5,14 +5,14 @@ export const devConfig = new class extends DefaultConfig {
return 95
}
numSpawnPhaseTurns(): number {
return 40
}
gameCreationRate(): number {
return 2 * 1000
}
lobbyLifetime(): number {
return 2 * 1000
return 80
}
// gameCreationRate(): number {
// return 2 * 1000
// }
// lobbyLifetime(): number {
// return 2 * 1000
// }
turnIntervalMs(): number {
return 100
}
+43
View File
@@ -0,0 +1,43 @@
import {AllPlayers, Emoji, Execution, MutableGame, MutablePlayer, PlayerID} from "../game/Game";
export class EmojiExecution implements Execution {
private requestor: MutablePlayer
private recipient: MutablePlayer | typeof AllPlayers
private active = true
constructor(
private senderID: PlayerID,
private recipientID: PlayerID | typeof AllPlayers,
private emoji: Emoji
) { }
init(mg: MutableGame, ticks: number): void {
this.requestor = mg.player(this.senderID)
this.recipient = this.recipientID == AllPlayers ? AllPlayers : mg.player(this.recipientID)
}
tick(ticks: number): void {
if (this.requestor.canSendEmoji(this.recipient)) {
this.requestor.sendEmoji(this.recipient, this.emoji)
} else {
console.warn(`cannot send emoji from ${this.requestor} to ${this.recipient}`)
}
this.active = false
}
owner(): MutablePlayer {
return null
}
isActive(): boolean {
return this.active
}
activeDuringSpawnPhase(): boolean {
return false
}
}
+3
View File
@@ -13,6 +13,7 @@ import {AllianceRequestExecution} from "./alliance/AllianceRequestExecution";
import {AllianceRequestReplyExecution} from "./alliance/AllianceRequestReplyExecution";
import {BreakAllianceExecution} from "./alliance/BreakAllianceExecution";
import {TargetPlayerExecution} from "./TargetPlayerExecution";
import {EmojiExecution} from "./EmojiExecution";
@@ -67,6 +68,8 @@ export class Executor {
return new BreakAllianceExecution(intent.requestor, intent.recipient)
} else if (intent.type == "targetPlayer") {
return new TargetPlayerExecution(intent.requestor, intent.target)
} else if (intent.type == "emoji") {
return new EmojiExecution(intent.sender, intent.recipient, intent.emoji)
}
else {
throw new Error(`intent type ${intent} not found`)
+28
View File
@@ -8,6 +8,26 @@ import {BreakAllianceExecution} from "../execution/alliance/BreakAllianceExecuti
export type PlayerID = string
export type Tick = number
export enum Emoji {
ThumbsUp = "👍",
ThumbsDown = "👎",
Smile = "😊",
Sad = "😢",
Heart = "❤️",
Fire = "🔥",
}
export const AllPlayers = "AllPlayers" as const;
export class EmojiMessage {
constructor(
public readonly sender: Player,
public readonly recipient: Player | typeof AllPlayers,
public readonly emoji: Emoji,
public readonly createdAt: Tick
) { }
}
export class Cell {
private strRepr: string
@@ -156,6 +176,8 @@ export interface Player {
// Targets of player and all allies.
transitiveTargets(): Player[]
toString(): string
canSendEmoji(recipient: Player | typeof AllPlayers): boolean
outgoingEmojis(): EmojiMessage[]
}
export interface MutablePlayer extends Player {
@@ -178,6 +200,8 @@ export interface MutablePlayer extends Player {
target(other: Player): void
targets(): MutablePlayer[]
transitiveTargets(): MutablePlayer[]
// Null means send to all Players
sendEmoji(recipient: Player | typeof AllPlayers, emoji: Emoji): void
}
export interface Game {
@@ -241,4 +265,8 @@ export class AllianceExpiredEvent implements GameEvent {
export class TargetPlayerEvent implements GameEvent {
constructor(public readonly player: Player, public readonly target: Player) { }
}
export class EmojiMessageEvent implements GameEvent {
constructor(public readonly message: EmojiMessage) { }
}
+26 -1
View File
@@ -1,4 +1,4 @@
import {MutablePlayer, Tile, PlayerInfo, PlayerID, PlayerType, Player, TerraNullius, Cell, MutableGame, Execution, AllianceRequest, MutableAllianceRequest, MutableAlliance, Alliance, Tick, TargetPlayerEvent} from "./Game";
import {MutablePlayer, Tile, PlayerInfo, PlayerID, PlayerType, Player, TerraNullius, Cell, MutableGame, Execution, AllianceRequest, MutableAllianceRequest, MutableAlliance, Alliance, Tick, TargetPlayerEvent, Emoji, EmojiMessage, EmojiMessageEvent, AllPlayers} from "./Game";
import {ClientID} from "../Schemas";
import {simpleHash} from "../Util";
import {CellString, GameImpl} from "./GameImpl";
@@ -26,6 +26,8 @@ export class PlayerImpl implements MutablePlayer {
private targets_: Target[] = []
private outgoingEmojis_: EmojiMessage[] = []
constructor(private gs: GameImpl, private readonly playerInfo: PlayerInfo, private _troops) {
this._name = playerInfo.name;
}
@@ -207,6 +209,29 @@ export class PlayerImpl implements MutablePlayer {
return [...new Set(ts)]
}
sendEmoji(recipient: Player | typeof AllPlayers, emoji: Emoji): void {
if (recipient == this) {
throw Error(`Cannot send emoji to oneself: ${this}`)
}
const msg = new EmojiMessage(this, recipient, emoji, this.gs.ticks())
this.outgoingEmojis_.push(msg)
this.gs.eventBus.emit(new EmojiMessageEvent(msg))
}
outgoingEmojis(): EmojiMessage[] {
return null
}
canSendEmoji(recipient: Player | null): boolean {
const prevMsgs = this.outgoingEmojis_.filter(msg => msg.recipient == recipient)
for (const msg of prevMsgs) {
if (this.gs.ticks() - msg.createdAt < this.gs.config().emojiMessageCooldown()) {
return false
}
}
return true
}
hash(): number {
return simpleHash(this.id()) * (this.troops() + this.numTilesOwned());
}