added chat sys

This commit is contained in:
Aotumuri
2025-04-03 16:54:25 +09:00
parent 74a8393c15
commit a3e964b205
10 changed files with 251 additions and 30 deletions
+39
View File
@@ -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": "Lets 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
}
]
}
+20
View File
@@ -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",
+8
View File
@@ -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),
+67 -29
View File
@@ -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: "Lets 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;
}
}
+6 -1
View File
@@ -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"
+10
View File
@@ -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({
+8
View File
@@ -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`);
}
+84
View File
@@ -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}]`,
);
}
}
+1
View File
@@ -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 {
+8
View File
@@ -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());
}