can donate to allies

This commit is contained in:
evanpelle
2024-10-07 20:17:15 -07:00
parent 10e3361966
commit db7a259587
10 changed files with 141 additions and 9 deletions
+1
View File
@@ -161,6 +161,7 @@
* disable select on mobile DONE 10/6/2024
* disable double tap on mobile DONE 10/6/2024
* donate troops button
* rewrite mobile input handling
* Make fake humans spawn by their country
* fake humans target enemies
* create private lobby menu
+23 -1
View File
@@ -51,7 +51,18 @@ export class SendTargetPlayerIntentEvent implements GameEvent {
}
export class SendEmojiIntentEvent implements GameEvent {
constructor(public readonly recipient: Player | typeof AllPlayers, public readonly emoji: string) { }
constructor(
public readonly recipient: Player | typeof AllPlayers,
public readonly emoji: string
) { }
}
export class SendDonateIntentEvent implements GameEvent {
constructor(
public readonly sender: Player,
public readonly recipient: Player,
public readonly troops: number | null,
) { }
}
export class Transport {
@@ -74,6 +85,7 @@ export class Transport {
this.eventBus.on(SendBoatAttackIntentEvent, (e) => this.onSendBoatAttackIntent(e))
this.eventBus.on(SendTargetPlayerIntentEvent, (e) => this.onSendTargetPlayerIntent(e))
this.eventBus.on(SendEmojiIntentEvent, (e) => this.onSendEmojiIntent(e))
this.eventBus.on(SendDonateIntentEvent, (e) => this.onSendDonateIntent(e))
}
connect(onconnect: () => void, onmessage: (message: ServerMessage) => void, isActive: () => boolean) {
@@ -216,6 +228,16 @@ export class Transport {
})
}
private onSendDonateIntent(event: SendDonateIntentEvent) {
this.sendIntent({
type: "donate",
clientID: this.clientID,
sender: event.sender.id(),
recipient: event.recipient.id(),
troops: event.troops,
})
}
private sendIntent(intent: Intent) {
if (this.socket.readyState === WebSocket.OPEN) {
const msg = ClientIntentMessageSchema.parse({
+11 -1
View File
@@ -3,7 +3,7 @@ import {AllPlayers, Cell, Game, Player} 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, SendEmojiIntentEvent, SendSpawnIntentEvent, SendTargetPlayerIntentEvent} from "../../Transport";
import {SendAllianceRequestIntentEvent, SendAttackIntentEvent, SendBoatAttackIntentEvent, SendBreakAllianceIntentEvent, SendDonateIntentEvent, SendEmojiIntentEvent, SendSpawnIntentEvent, SendTargetPlayerIntentEvent} from "../../Transport";
import {TransformHandler} from "../TransformHandler";
import {Layer} from "./Layer";
import * as d3 from 'd3';
@@ -14,6 +14,8 @@ import swordIcon from '../../../../resources/images/SwordIconWhite.png';
import targetIcon from '../../../../resources/images/TargetIconWhite.png';
import emojiIcon from '../../../../resources/images/EmojiIconWhite.png';
import disabledIcon from '../../../../resources/images/DisabledIcon.png';
import donateIcon from '../../../../resources/images/DonateIconWhite.png';
import {MessageType} from "./EventsDisplay";
enum Slot {
@@ -298,6 +300,14 @@ export class RadialMenu implements Layer {
return
}
if (myPlayer.canDonate(other)) {
this.activateMenuElement(Slot.Target, "#53ac75", donateIcon, () => {
this.eventBus.emit(
new SendDonateIntentEvent(myPlayer, other, null)
)
})
}
if (myPlayer.isAlliedWith(other)) {
this.activateMenuElement(Slot.Alliance, "#c74848", traitorIcon, () => {
this.eventBus.emit(
+10
View File
@@ -13,6 +13,7 @@ export type Intent = SpawnIntent
| BreakAllianceIntent
| TargetPlayerIntent
| EmojiIntent
| DonateIntent
export type AttackIntent = z.infer<typeof AttackIntentSchema>
export type SpawnIntent = z.infer<typeof SpawnIntentSchema>
@@ -23,6 +24,7 @@ export type AllianceRequestReplyIntent = z.infer<typeof AllianceRequestReplyInte
export type BreakAllianceIntent = z.infer<typeof BreakAllianceIntentSchema>
export type TargetPlayerIntent = z.infer<typeof TargetPlayerIntentSchema>
export type EmojiIntent = z.infer<typeof EmojiIntentSchema>
export type DonateIntent = z.infer<typeof DonateIntentSchema>
export type Turn = z.infer<typeof TurnSchema>
@@ -128,6 +130,13 @@ export const EmojiIntentSchema = BaseIntentSchema.extend({
emoji: EmojiSchema,
})
export const DonateIntentSchema = BaseIntentSchema.extend({
type: z.literal('donate'),
sender: z.string(),
recipient: z.string(),
troops: z.number().nullable(),
})
const IntentSchema = z.union([
AttackIntentSchema,
SpawnIntentSchema,
@@ -138,6 +147,7 @@ const IntentSchema = z.union([
BreakAllianceIntentSchema,
TargetPlayerIntentSchema,
EmojiIntentSchema,
DonateIntentSchema,
]);
const TurnSchema = z.object({
+2
View File
@@ -53,6 +53,8 @@ export interface Config {
targetCooldown(): Tick
emojiMessageCooldown(): Tick
emojiMessageDuration(): Tick
donateCooldown(): Tick
defaultDonationAmount(sender: Player): number
}
export interface Theme {
+7 -1
View File
@@ -7,6 +7,12 @@ import {pastelTheme} from "./PastelTheme";
export class DefaultConfig implements Config {
defaultDonationAmount(sender: Player): number {
return Math.floor(sender.troops() / 3)
}
donateCooldown(): Tick {
return 10 * 10
}
emojiMessageDuration(): Tick {
return 5 * 10
}
@@ -114,7 +120,7 @@ export class DefaultConfig implements Config {
}
boatAttackAmount(attacker: Player, defender: Player | TerraNullius): number {
return attacker.troops() / 5
return Math.floor(attacker.troops() / 5)
}
attackAmount(attacker: Player, defender: Player | TerraNullius) {
+46
View File
@@ -0,0 +1,46 @@
import {AllPlayers, Execution, MutableGame, MutablePlayer, PlayerID} from "../game/Game";
export class DonateExecution implements Execution {
private sender: MutablePlayer
private recipient: MutablePlayer
private active = true
constructor(
private senderID: PlayerID,
private recipientID: PlayerID,
private troops: number | null
) { }
init(mg: MutableGame, ticks: number): void {
this.sender = mg.player(this.senderID)
this.recipient = mg.player(this.recipientID)
if (this.troops == null) {
this.troops = mg.config().defaultDonationAmount(this.sender)
}
}
tick(ticks: number): void {
if (this.sender.canDonate(this.recipient)) {
this.sender.donate(this.recipient, this.troops)
} else {
console.warn(`cannot send tropps from ${this.sender} to ${this.recipient}`)
}
this.active = false
}
owner(): MutablePlayer {
return null
}
isActive(): boolean {
return this.active
}
activeDuringSpawnPhase(): boolean {
return false
}
}
+3
View File
@@ -14,6 +14,7 @@ import {AllianceRequestReplyExecution} from "./alliance/AllianceRequestReplyExec
import {BreakAllianceExecution} from "./alliance/BreakAllianceExecution";
import {TargetPlayerExecution} from "./TargetPlayerExecution";
import {EmojiExecution} from "./EmojiExecution";
import {DonateExecution} from "./DonateExecution";
@@ -70,6 +71,8 @@ export class Executor {
return new TargetPlayerExecution(intent.requestor, intent.target)
} else if (intent.type == "emoji") {
return new EmojiExecution(intent.sender, intent.recipient, intent.emoji)
} else if (intent.type == "donate") {
return new DonateExecution(intent.sender, intent.recipient, intent.troops)
}
else {
throw new Error(`intent type ${intent} not found`)
+5 -2
View File
@@ -4,6 +4,7 @@ import {GameEvent} from "../EventBus"
import {ClientID, GameID} from "../Schemas"
import {DisplayMessageEvent, MessageType} from "../../client/graphics/layers/EventsDisplay"
import {BreakAllianceExecution} from "../execution/alliance/BreakAllianceExecution"
import {DonateExecution} from "../execution/DonateExecution"
export type PlayerID = string
export type Tick = number
@@ -159,6 +160,7 @@ export interface Player {
isAlliedWith(other: Player): boolean
allianceWith(other: Player): Alliance | null
// Includes recent requests that are in cooldown
// TODO: why can't I have "canSendAllyRequest" function instead?
recentOrPendingAllianceRequestWith(other: Player): boolean
isTraitor(): boolean
canTarget(other: Player): boolean
@@ -169,13 +171,14 @@ export interface Player {
toString(): string
canSendEmoji(recipient: Player | typeof AllPlayers): boolean
outgoingEmojis(): EmojiMessage[]
canDonate(recipient: Player): boolean
}
export interface MutablePlayer extends Player {
setName(name: string): void
setTroops(troops: number): void
addTroops(troops: number): void
removeTroops(troops: number): void
removeTroops(troops: number): number
conquer(tile: Tile): void
relinquish(tile: Tile): void
executions(): Execution[]
@@ -191,8 +194,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: string): void
donate(recipient: MutablePlayer, troops: number): void
}
export interface Game {
+33 -4
View File
@@ -5,13 +5,18 @@ import {CellString, GameImpl} from "./GameImpl";
import {BoatImpl} from "./BoatImpl";
import {TileImpl} from "./TileImpl";
import {TerraNulliusImpl} from "./TerraNulliusImpl";
import {threadId} from "worker_threads";
import {MessageType} from "../../client/graphics/layers/EventsDisplay";
import {renderTroops} from "../../client/graphics/Utils";
interface Target {
tick: Tick
target: Player
}
class Donation {
constructor(public readonly recipient: Player, public readonly tick: Tick) { }
}
export class PlayerImpl implements MutablePlayer {
isTraitor_ = false
@@ -28,6 +33,8 @@ export class PlayerImpl implements MutablePlayer {
private outgoingEmojis_: EmojiMessage[] = []
private sentDonations: Donation[] = []
constructor(private gs: GameImpl, private readonly playerInfo: PlayerInfo, private _troops) {
this._name = playerInfo.name;
}
@@ -99,9 +106,10 @@ export class PlayerImpl implements MutablePlayer {
addTroops(troops: number): void {
this._troops += Math.floor(troops);
}
removeTroops(troops: number): void {
this._troops -= Math.floor(troops);
this._troops = Math.max(this._troops, 0);
removeTroops(troops: number): number {
const toRemove = Math.floor(Math.min(this._troops, troops))
this._troops -= toRemove;
return toRemove
}
isPlayer(): this is MutablePlayer {return true as const;}
@@ -234,6 +242,27 @@ export class PlayerImpl implements MutablePlayer {
return true
}
canDonate(recipient: Player): boolean {
if (!this.isAlliedWith(recipient)) {
return false
}
for (const donation of this.sentDonations) {
if (donation.recipient == recipient) {
if (this.gs.ticks() - donation.tick < this.gs.config().donateCooldown()) {
return false
}
}
}
return true
}
donate(recipient: MutablePlayer, troops: number): void {
this.sentDonations.push(new Donation(recipient, this.gs.ticks()))
recipient.addTroops(this.removeTroops(troops))
this.gs.displayMessage(`Sent ${renderTroops(troops)} troops to ${recipient.name()}`, MessageType.INFO, this.id())
this.gs.displayMessage(`Recieved ${renderTroops(troops)} troops from ${this.name()}`, MessageType.SUCCESS, recipient.id())
}
hash(): number {
return simpleHash(this.id()) * (this.troops() + this.numTilesOwned());
}