mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-23 00:15:23 +00:00
added chat sys
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"help": [
|
||||
{
|
||||
"key": "troops",
|
||||
"text": "Please give me troops!",
|
||||
"requiresPlayer": false
|
||||
},
|
||||
{ "key": "gold", "text": "Please give me golds!", "requiresPlayer": false },
|
||||
{
|
||||
"key": "no_attack",
|
||||
"text": "Please don't attack me!",
|
||||
"requiresPlayer": false
|
||||
}
|
||||
],
|
||||
"attack": [
|
||||
{ "key": "attack", "text": "Attack [P1]!", "requiresPlayer": true },
|
||||
{ "key": "mirv", "text": "Launch a MIRV at [P1]!", "requiresPlayer": true },
|
||||
{ "key": "focus", "text": "Focus fire on [P1]!", "requiresPlayer": true },
|
||||
{
|
||||
"key": "finish",
|
||||
"text": "Let's finish off [P1]!",
|
||||
"requiresPlayer": true
|
||||
}
|
||||
],
|
||||
"defend": [
|
||||
{ "key": "defend", "text": "Defend [P1]!", "requiresPlayer": true }
|
||||
],
|
||||
"greet": [{ "key": "hello", "text": "Hello!", "requiresPlayer": false }],
|
||||
"misc": [
|
||||
{ "key": "go", "text": "Let’s go!", "requiresPlayer": false },
|
||||
{ "key": "strategy", "text": "Nice strategy!", "requiresPlayer": false },
|
||||
{ "key": "fun", "text": "This game is fun!", "requiresPlayer": false },
|
||||
{
|
||||
"key": "pr",
|
||||
"text": "When will my PR finally get merged...?",
|
||||
"requiresPlayer": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -107,6 +107,15 @@ export class SendDonateTroopsIntentEvent implements GameEvent {
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendQuickChatEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly sender: PlayerView,
|
||||
public readonly recipient: PlayerView,
|
||||
public readonly quickChatKey: string,
|
||||
public readonly variables: { [key: string]: string },
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SendEmbargoIntentEvent implements GameEvent {
|
||||
constructor(
|
||||
public readonly sender: PlayerView,
|
||||
@@ -195,6 +204,7 @@ export class Transport {
|
||||
this.eventBus.on(SendDonateTroopsIntentEvent, (e) =>
|
||||
this.onSendDonateTroopIntent(e),
|
||||
);
|
||||
this.eventBus.on(SendQuickChatEvent, (e) => this.onSendQuickChatIntent(e));
|
||||
this.eventBus.on(SendEmbargoIntentEvent, (e) =>
|
||||
this.onSendEmbargoIntent(e),
|
||||
);
|
||||
@@ -454,6 +464,16 @@ export class Transport {
|
||||
});
|
||||
}
|
||||
|
||||
private onSendQuickChatIntent(event: SendQuickChatEvent) {
|
||||
this.sendIntent({
|
||||
type: "quick_chat",
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
recipient: event.recipient.id(),
|
||||
quickChatKey: event.quickChatKey,
|
||||
variables: event.variables,
|
||||
});
|
||||
}
|
||||
|
||||
private onSendEmbargoIntent(event: SendEmbargoIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "embargo",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { GameStartingModal } from "../gameStartingModal";
|
||||
import { TransformHandler } from "./TransformHandler";
|
||||
import { UIState } from "./UIState";
|
||||
import { BuildMenu } from "./layers/BuildMenu";
|
||||
import { ChatModal } from "./layers/ChatModal";
|
||||
import { ControlPanel } from "./layers/ControlPanel";
|
||||
import { EmojiTable } from "./layers/EmojiTable";
|
||||
import { EventsDisplay } from "./layers/EventsDisplay";
|
||||
@@ -120,6 +121,13 @@ export function createRenderer(
|
||||
playerPanel.eventBus = eventBus;
|
||||
playerPanel.emojiTable = emojiTable;
|
||||
|
||||
const chatModal = document.querySelector("chat-modal") as ChatModal;
|
||||
if (!(chatModal instanceof ChatModal)) {
|
||||
console.error("chat modal not found");
|
||||
}
|
||||
chatModal.g = game;
|
||||
chatModal.eventBus = eventBus;
|
||||
|
||||
const layers: Layer[] = [
|
||||
new TerrainLayer(game),
|
||||
new TerritoryLayer(game, eventBus),
|
||||
|
||||
@@ -1,31 +1,22 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, query } from "lit/decorators.js";
|
||||
|
||||
const quickChatPhrases: Record<
|
||||
string,
|
||||
Array<{ text: string; requiresPlayer: boolean }>
|
||||
> = {
|
||||
help: [
|
||||
{ text: "Please give me troops!", requiresPlayer: false },
|
||||
{ text: "Please give me golds!", requiresPlayer: false },
|
||||
{ text: "Please don't attack me!", requiresPlayer: false },
|
||||
],
|
||||
attack: [
|
||||
{ text: "Attack [P1]!", requiresPlayer: true },
|
||||
{ text: "Launch a MIRV at [P1]!", requiresPlayer: true },
|
||||
{ text: "Focus fire on [P1]!", requiresPlayer: true },
|
||||
{ text: "Let's finish off [P1]!", requiresPlayer: true },
|
||||
],
|
||||
defend: [{ text: "Defend [P1]!", requiresPlayer: true }],
|
||||
greet: [{ text: "Hello!", requiresPlayer: false }],
|
||||
misc: [
|
||||
{ text: "Let’s go!", requiresPlayer: false },
|
||||
{ text: "Nice strategy!", requiresPlayer: false },
|
||||
{ text: "This game is fun!", requiresPlayer: false },
|
||||
{ text: "When will my PR finally get merged...?", requiresPlayer: false },
|
||||
],
|
||||
import { GameView, PlayerView } from "../../../core/game/GameView";
|
||||
|
||||
import quickChatData from "../../../../resources/QuickChat.json";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { SendQuickChatEvent } from "../../Transport";
|
||||
|
||||
type QuickChatPhrase = {
|
||||
key: string;
|
||||
text: string;
|
||||
requiresPlayer: boolean;
|
||||
};
|
||||
|
||||
type QuickChatPhrases = Record<string, QuickChatPhrase[]>;
|
||||
|
||||
const quickChatPhrases: QuickChatPhrases = quickChatData;
|
||||
|
||||
@customElement("chat-modal")
|
||||
export class ChatModal extends LitElement {
|
||||
@query("o-modal") private modalEl!: HTMLElement & {
|
||||
@@ -61,6 +52,13 @@ export class ChatModal extends LitElement {
|
||||
private selectedPhraseText: string | null = null;
|
||||
private selectedPlayer: string | null = null;
|
||||
private selectedPhraseTemplate: string | null = null;
|
||||
private selectedQuickChatKey: string | null = null;
|
||||
|
||||
private recipient: PlayerView;
|
||||
private sender: PlayerView;
|
||||
public eventBus: EventBus;
|
||||
|
||||
public g: GameView;
|
||||
|
||||
quickChatPhrases: Record<
|
||||
string,
|
||||
@@ -195,9 +193,13 @@ export class ChatModal extends LitElement {
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private selectPhrase(phrase: { text: string; requiresPlayer: boolean }) {
|
||||
private selectPhrase(phrase: QuickChatPhrase) {
|
||||
this.selectedPhraseTemplate = phrase.text;
|
||||
this.selectedPhraseText = phrase.text;
|
||||
this.selectedQuickChatKey = this.getFullQuickChatKey(
|
||||
this.selectedCategory!,
|
||||
phrase.key,
|
||||
);
|
||||
this.previewText = phrase.text;
|
||||
this.requiresPlayerSelection = phrase.requiresPlayer;
|
||||
this.selectedPlayer = null;
|
||||
@@ -219,6 +221,23 @@ export class ChatModal extends LitElement {
|
||||
|
||||
private sendChatMessage() {
|
||||
console.log("Sent message:", this.previewText);
|
||||
console.log("Sender:", this.sender);
|
||||
console.log("Recipient:", this.recipient);
|
||||
console.log("Key:", this.selectedQuickChatKey);
|
||||
|
||||
if (this.sender && this.recipient && this.selectedQuickChatKey) {
|
||||
const variables = this.selectedPlayer ? { P1: this.selectedPlayer } : {};
|
||||
|
||||
this.eventBus.emit(
|
||||
new SendQuickChatEvent(
|
||||
this.sender,
|
||||
this.recipient,
|
||||
this.selectedQuickChatKey,
|
||||
variables,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
this.previewText = null;
|
||||
this.selectedCategory = null;
|
||||
this.requiresPlayerSelection = false;
|
||||
@@ -242,7 +261,24 @@ export class ChatModal extends LitElement {
|
||||
return [...filtered, ...others];
|
||||
}
|
||||
|
||||
public open() {
|
||||
private getFullQuickChatKey(category: string, phraseKey: string): string {
|
||||
return `${category}.${phraseKey}`;
|
||||
}
|
||||
|
||||
public open(sender?: PlayerView, recipient?: PlayerView) {
|
||||
if (sender && recipient) {
|
||||
console.log("Sent message:", recipient);
|
||||
console.log("Sent message:", sender);
|
||||
const alivePlayerNames = this.g
|
||||
.players()
|
||||
.filter((p) => p.isAlive())
|
||||
.map((p) => p.data.name);
|
||||
|
||||
console.log("Alive player names:", alivePlayerNames);
|
||||
this.players = alivePlayerNames;
|
||||
this.recipient = recipient;
|
||||
this.sender = sender;
|
||||
}
|
||||
this.modalEl?.open();
|
||||
}
|
||||
|
||||
@@ -255,9 +291,11 @@ export class ChatModal extends LitElement {
|
||||
this.modalEl?.close();
|
||||
}
|
||||
|
||||
static open() {
|
||||
const modal = new ChatModal();
|
||||
document.body.appendChild(modal);
|
||||
modal.open();
|
||||
public setRecipient(value: PlayerView) {
|
||||
this.recipient = value;
|
||||
}
|
||||
|
||||
public setSender(value: PlayerView) {
|
||||
this.sender = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +133,11 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
});
|
||||
}
|
||||
|
||||
private handleChat(e: Event, sender: PlayerView, other: PlayerView) {
|
||||
this.ctModal.open(sender, other);
|
||||
this.hide();
|
||||
}
|
||||
|
||||
private handleTargetClick(e: Event, other: PlayerView) {
|
||||
e.stopPropagation();
|
||||
this.eventBus.emit(new SendTargetPlayerIntentEvent(other.id()));
|
||||
@@ -286,7 +291,7 @@ export class PlayerPanel extends LitElement implements Layer {
|
||||
<!-- Action buttons -->
|
||||
<div class="flex justify-center gap-2">
|
||||
<button
|
||||
@click=${(e) => this.ctModal.open()}
|
||||
@click=${(e) => this.handleChat(e, myPlayer, other)}
|
||||
class="w-10 h-10 flex items-center justify-center
|
||||
bg-opacity-50 bg-gray-700 hover:bg-opacity-70
|
||||
text-white rounded-lg transition-colors"
|
||||
|
||||
@@ -28,6 +28,7 @@ export type Intent =
|
||||
| TargetTroopRatioIntent
|
||||
| BuildUnitIntent
|
||||
| EmbargoIntent
|
||||
| QuickChatIntent
|
||||
| MoveWarshipIntent;
|
||||
|
||||
export type AttackIntent = z.infer<typeof AttackIntentSchema>;
|
||||
@@ -49,6 +50,7 @@ export type TargetTroopRatioIntent = z.infer<
|
||||
>;
|
||||
export type BuildUnitIntent = z.infer<typeof BuildUnitIntentSchema>;
|
||||
export type MoveWarshipIntent = z.infer<typeof MoveWarshipIntentSchema>;
|
||||
export type QuickChatIntent = z.infer<typeof QuickChatIntentSchema>;
|
||||
|
||||
export type Turn = z.infer<typeof TurnSchema>;
|
||||
export type GameConfig = z.infer<typeof GameConfigSchema>;
|
||||
@@ -269,6 +271,13 @@ export const MoveWarshipIntentSchema = BaseIntentSchema.extend({
|
||||
tile: z.number(),
|
||||
});
|
||||
|
||||
export const QuickChatIntentSchema = BaseIntentSchema.extend({
|
||||
type: z.literal("quick_chat"),
|
||||
recipient: z.string(),
|
||||
quickChatKey: z.string(),
|
||||
variables: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
const IntentSchema = z.union([
|
||||
AttackIntentSchema,
|
||||
CancelAttackIntentSchema,
|
||||
@@ -285,6 +294,7 @@ const IntentSchema = z.union([
|
||||
BuildUnitIntentSchema,
|
||||
EmbargoIntentSchema,
|
||||
MoveWarshipIntentSchema,
|
||||
QuickChatIntentSchema,
|
||||
]);
|
||||
|
||||
export const TurnSchema = z.object({
|
||||
|
||||
@@ -16,6 +16,7 @@ import { EmojiExecution } from "./EmojiExecution";
|
||||
import { FakeHumanExecution } from "./FakeHumanExecution";
|
||||
import { MoveWarshipExecution } from "./MoveWarshipExecution";
|
||||
import { NoOpExecution } from "./NoOpExecution";
|
||||
import { QuickChatExecution } from "./QuickChatExecution";
|
||||
import { RetreatExecution } from "./RetreatExecution";
|
||||
import { SetTargetTroopRatioExecution } from "./SetTargetTroopRatioExecution";
|
||||
import { SpawnExecution } from "./SpawnExecution";
|
||||
@@ -113,6 +114,13 @@ export class Executor {
|
||||
this.mg.ref(intent.x, intent.y),
|
||||
intent.unit,
|
||||
);
|
||||
case "quick_chat":
|
||||
return new QuickChatExecution(
|
||||
playerID,
|
||||
intent.recipient,
|
||||
intent.quickChatKey,
|
||||
intent.variables ?? {},
|
||||
);
|
||||
default:
|
||||
throw new Error(`intent type ${intent} not found`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import quickChatData from "../../../resources/QuickChat.json";
|
||||
import { consolex } from "../Consolex";
|
||||
import { Execution, Game, Player, PlayerID } from "../game/Game";
|
||||
|
||||
export class QuickChatExecution implements Execution {
|
||||
private sender: Player;
|
||||
private recipient: Player;
|
||||
|
||||
private active = true;
|
||||
|
||||
constructor(
|
||||
private senderID: PlayerID,
|
||||
private recipientID: PlayerID,
|
||||
private quickChatKey: string,
|
||||
private variables: Record<string, string>,
|
||||
) {}
|
||||
|
||||
init(mg: Game, ticks: number): void {
|
||||
if (!mg.hasPlayer(this.senderID)) {
|
||||
consolex.warn(`QuickChatExecution: sender ${this.senderID} not found`);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
if (!mg.hasPlayer(this.recipientID)) {
|
||||
consolex.warn(
|
||||
`QuickChatExecution: recipient ${this.recipientID} not found`,
|
||||
);
|
||||
this.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.sender = mg.player(this.senderID);
|
||||
this.recipient = mg.player(this.recipientID);
|
||||
}
|
||||
|
||||
tick(ticks: number): void {
|
||||
const message = this.getMessageFromKey(this.quickChatKey, this.variables);
|
||||
|
||||
this.sender.displayQuickChat(this.sender, this.recipient, message);
|
||||
consolex.log(
|
||||
`[QuickChat] ${this.sender.name} → ${this.recipient.name}: ${message}`,
|
||||
);
|
||||
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
owner(): Player {
|
||||
return this.sender;
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
activeDuringSpawnPhase(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
private getMessageFromKey(
|
||||
fullKey: string,
|
||||
vars: Record<string, string>,
|
||||
): string {
|
||||
const [category, key] = fullKey.split(".");
|
||||
const phrases = quickChatData[category];
|
||||
|
||||
if (!phrases) {
|
||||
consolex.warn(`QuickChat: Unknown category '${category}'`);
|
||||
return `[${fullKey}]`;
|
||||
}
|
||||
|
||||
const phraseObj = phrases.find((p) => p.key === key);
|
||||
if (!phraseObj) {
|
||||
consolex.warn(
|
||||
`QuickChat: Key '${key}' not found in category '${category}'`,
|
||||
);
|
||||
return `[${fullKey}]`;
|
||||
}
|
||||
|
||||
return phraseObj.text.replace(
|
||||
/\[(\w+)\]/g,
|
||||
(_, p1) => vars[p1] || `[${p1}]`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -412,6 +412,7 @@ export interface Player {
|
||||
playerProfile(): PlayerProfile;
|
||||
canBoat(tile: TileRef): boolean;
|
||||
tradingPorts(port: Unit): Unit[];
|
||||
displayQuickChat(sender: Player, recipient: Player, message: string);
|
||||
}
|
||||
|
||||
export interface Game extends GameMap {
|
||||
|
||||
@@ -550,6 +550,14 @@ export class PlayerImpl implements Player {
|
||||
);
|
||||
}
|
||||
|
||||
displayQuickChat(sender: Player, recipient: Player, message: string): void {
|
||||
this.mg.displayMessage(
|
||||
`${sender.name()}: ${message}`,
|
||||
MessageType.INFO,
|
||||
recipient.id(),
|
||||
);
|
||||
}
|
||||
|
||||
hasEmbargoAgainst(other: Player): boolean {
|
||||
return this.embargoes.has(other.id());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user