MLS for Quick Chat (#686)

## Description:
<img width="842" alt="スクリーンショット 2025-05-09 17 51 27"
src="https://github.com/user-attachments/assets/b9a2cb5b-74d2-4c07-aed2-01d719de6eb4"
/>

MLS
## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors
This commit is contained in:
Aotumuri
2025-05-10 13:50:56 +09:00
committed by GitHub
parent 47f76b5b29
commit d6170f67ab
9 changed files with 272 additions and 93 deletions
-42
View File
@@ -2,222 +2,180 @@
"help": [
{
"key": "troops",
"text": "Please give me troops!",
"requiresPlayer": false
},
{
"key": "gold",
"text": "Please give me gold!",
"requiresPlayer": false
},
{
"key": "no_attack",
"text": "Please don't attack me!",
"requiresPlayer": false
},
{
"key": "sorry_attack",
"text": "Sorry, I didnt mean to attack.",
"requiresPlayer": false
},
{
"key": "alliance",
"text": "Alliance?",
"requiresPlayer": false
},
{
"key": "help_defend",
"text": "Help me defend against [P1]!",
"requiresPlayer": true
},
{
"key": "team_up",
"text": "Lets team up against [P1]!",
"requiresPlayer": true
}
],
"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
},
{
"key": "dont_attack",
"text": "Dont attack [P1]!",
"requiresPlayer": true
},
{
"key": "ally",
"text": "[P1] is my ally!",
"requiresPlayer": true
}
],
"greet": [
{
"key": "hello",
"text": "Hello!",
"requiresPlayer": false
},
{
"key": "good_luck",
"text": "Good luck!",
"requiresPlayer": false
},
{
"key": "have_fun",
"text": "Have fun!",
"requiresPlayer": false
},
{
"key": "gg",
"text": "GG!",
"requiresPlayer": false
},
{
"key": "nice_to_meet",
"text": "Nice to meet you!",
"requiresPlayer": false
},
{
"key": "well_played",
"text": "Well played!",
"requiresPlayer": false
},
{
"key": "hi_again",
"text": "Hi again!",
"requiresPlayer": false
},
{
"key": "bye",
"text": "Bye!",
"requiresPlayer": false
},
{
"key": "thanks",
"text": "Thanks!",
"requiresPlayer": false
},
{
"key": "oops",
"text": "Oops, wrong button!",
"requiresPlayer": false
},
{
"key": "trust_me",
"text": "You can trust me. Promise!",
"requiresPlayer": false
},
{
"key": "trust_broken",
"text": "I trusted you...",
"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
}
],
"warnings": [
{
"key": "strong",
"text": "[P1] is strong.",
"requiresPlayer": true
},
{
"key": "weak",
"text": "[P1] is weak.",
"requiresPlayer": true
},
{
"key": "mirv_soon",
"text": "[P1] can launch a MIRV soon!",
"requiresPlayer": true
},
{
"key": "number1_warning",
"text": "The #1 player will win soon unless we team up!",
"requiresPlayer": false
},
{
"key": "stalemate",
"text": "Let's make peace. This is a stalemate, we will both lose.",
"requiresPlayer": false
},
{
"key": "has_allies",
"text": "[P1] has many allies.",
"requiresPlayer": true
},
{
"key": "no_allies",
"text": "[P1] has no allies.",
"requiresPlayer": true
},
{
"key": "betrayed",
"text": "[P1] betrayed their ally!",
"requiresPlayer": true
},
{
"key": "getting_big",
"text": "[P1] is growing too fast!",
"requiresPlayer": true
},
{
"key": "danger_base",
"text": "[P1] is unprotected!",
"requiresPlayer": true
},
{
"key": "saving_for_mirv",
"text": "[P1] is saving up to launch a MIRV.",
"requiresPlayer": true
},
{
"key": "mirv_ready",
"text": "[P1] has enough gold to launch a MIRV!",
"requiresPlayer": true
}
]
+73
View File
@@ -246,5 +246,78 @@
"move_right_desc": "Move the camera to the right",
"reset": "Reset",
"unbind": "Unbind"
},
"chat": {
"title": "Quick Chat",
"to": "Sent {user}: {msg}",
"from": "From {user}: {msg}",
"category": "Category",
"phrase": "Phrase",
"player": "Player",
"send": "Send",
"search": "Search player...",
"build": "Build your message...",
"cat": {
"help": "Help",
"attack": "Attack",
"defend": "Defend",
"greet": "Greetings",
"misc": "Miscellaneous",
"warnings": "Warnings"
},
"help": {
"troops": "Please give me troops!",
"gold": "Please give me gold!",
"no_attack": "Please don't attack me!",
"sorry_attack": "Sorry, I didnt mean to attack.",
"alliance": "Alliance?",
"help_defend": "Help me defend against [P1]!",
"team_up": "Lets team up against [P1]!"
},
"attack": {
"attack": "Attack [P1]!",
"mirv": "Launch a MIRV at [P1]!",
"focus": "Focus fire on [P1]!",
"finish": "Let's finish off [P1]!"
},
"defend": {
"defend": "Defend [P1]!",
"dont_attack": "Dont attack [P1]!",
"ally": "[P1] is my ally!"
},
"greet": {
"hello": "Hello!",
"good_luck": "Good luck!",
"have_fun": "Have fun!",
"gg": "GG!",
"nice_to_meet": "Nice to meet you!",
"well_played": "Well played!",
"hi_again": "Hi again!",
"bye": "Bye!",
"thanks": "Thanks!",
"oops": "Oops, wrong button!",
"trust_me": "You can trust me. Promise!",
"trust_broken": "I trusted you..."
},
"misc": {
"go": "Lets go!",
"strategy": "Nice strategy!",
"fun": "This game is fun!",
"pr": "When will my PR finally get merged...?"
},
"warnings": {
"strong": "[P1] is strong.",
"weak": "[P1] is weak.",
"mirv_soon": "[P1] can launch a MIRV soon!",
"number1_warning": "The #1 player will win soon unless we team up!",
"stalemate": "Let's make peace. This is a stalemate, we will both lose.",
"has_allies": "[P1] has many allies.",
"no_allies": "[P1] has no allies.",
"betrayed": "[P1] betrayed their ally!",
"getting_big": "[P1] is growing too fast!",
"danger_base": "[P1] is unprotected!",
"saving_for_mirv": "[P1] is saving up to launch a MIRV.",
"mirv_ready": "[P1] has enough gold to launch a MIRV!"
}
}
}
+73
View File
@@ -239,5 +239,78 @@
"continental": "大陸",
"regional": "地域",
"fantasy": "その他"
},
"chat": {
"title": "クイックチャット",
"category": "カテゴリ",
"phrase": "フレーズ",
"player": "プレイヤー",
"to": "{user}に送信: {msg}",
"from": "{user}から着信: {msg}",
"send": "送信",
"search": "プレイヤーを検索...",
"build": "チャットを作成...",
"cat": {
"help": "支援要請",
"attack": "攻撃",
"defend": "防御",
"greet": "挨拶",
"misc": "その他",
"warnings": "警告"
},
"help": {
"troops": "援軍をください!",
"gold": "お金をください!",
"no_attack": "攻撃しないでください!",
"sorry_attack": "ごめん、攻撃するつもりはなかった。",
"alliance": "同盟を組みませんか?",
"help_defend": "[P1] からの防衛を手伝って!",
"team_up": "[P1] に対抗して協力しよう!"
},
"attack": {
"attack": "[P1] を攻撃しよう!",
"mirv": "[P1] にMIRVを撃とう!",
"focus": "[P1] に集中攻撃しよう!",
"finish": "[P1] を仕留めよう!"
},
"defend": {
"defend": "[P1] を守って!",
"dont_attack": "[P1] を攻撃しないで!",
"ally": "[P1] は味方だ!"
},
"greet": {
"hello": "こんにちは!",
"good_luck": "頑張って!",
"have_fun": "楽しもう!",
"gg": "GG",
"nice_to_meet": "よろしく!",
"well_played": "ナイスプレイ!",
"hi_again": "また会ったね!",
"bye": "バイバイ!",
"thanks": "ありがとう!",
"oops": "あっ、間違えた!",
"trust_me": "信じてくれ、本当だ!",
"trust_broken": "信じてたのに..."
},
"misc": {
"go": "行こう!",
"strategy": "いい作戦だ!",
"fun": "このゲーム楽しい!",
"pr": "わたしのPRいつマージされるんだろう...?"
},
"warnings": {
"strong": "[P1] は強い。",
"weak": "[P1] は弱い。",
"mirv_soon": "[P1] がもうすぐMIRVを撃つぞ!",
"number1_warning": "1位のプレイヤーが勝ちそうだ、協力しよう!",
"stalemate": "和平しよう。膠着状態だ、両方負ける。",
"has_allies": "[P1] には味方が多い。",
"no_allies": "[P1] には味方がいない。",
"betrayed": "[P1] は味方を裏切った!",
"getting_big": "[P1] の勢力が急拡大中!",
"danger_base": "[P1] の本拠地が無防備だ!",
"saving_for_mirv": "[P1] はMIRVのために貯金してる。",
"mirv_ready": "[P1] はMIRVを撃てるだけの金を持ってる!"
}
}
}
+35 -21
View File
@@ -7,10 +7,10 @@ import { GameView, PlayerView } from "../../../core/game/GameView";
import quickChatData from "../../../../resources/QuickChat.json";
import { EventBus } from "../../../core/EventBus";
import { SendQuickChatEvent } from "../../Transport";
import { translateText } from "../../Utils";
type QuickChatPhrase = {
key: string;
text: string;
requiresPlayer: boolean;
};
@@ -58,12 +58,12 @@ export class ChatModal extends LitElement {
};
private categories = [
{ id: "help", name: "Help" },
{ id: "attack", name: "Attack" },
{ id: "defend", name: "Defend" },
{ id: "greet", name: "Greetings" },
{ id: "misc", name: "Miscellaneous" },
{ id: "warnings", name: "Warnings" },
{ id: "help" },
{ id: "attack" },
{ id: "defend" },
{ id: "greet" },
{ id: "misc" },
{ id: "warnings" },
];
private getPhrasesForCategory(categoryId: string) {
@@ -83,10 +83,10 @@ export class ChatModal extends LitElement {
const displayPlayers = [...filteredPlayers, ...otherPlayers];
return html`
<o-modal title="Quick Chat">
<o-modal title="${translateText("chat.title")}">
<div class="chat-columns">
<div class="chat-column">
<div class="column-title">Category</div>
<div class="column-title">${translateText("chat.category")}</div>
${this.categories.map(
(category) => html`
<button
@@ -96,7 +96,7 @@ export class ChatModal extends LitElement {
: ""}"
@click=${() => this.selectCategory(category.id)}
>
${category.name}
${translateText(`chat.cat.${category.id}`)}
</button>
`,
)}
@@ -105,13 +105,18 @@ export class ChatModal extends LitElement {
${this.selectedCategory
? html`
<div class="chat-column">
<div class="column-title">Phrase</div>
<div class="column-title">
${translateText("chat.phrase")}
</div>
<div class="phrase-scroll-area">
${this.getPhrasesForCategory(this.selectedCategory).map(
(phrase) => html`
<button
class="chat-option-button ${this
.selectedPhraseText === phrase.text
.selectedPhraseText ===
translateText(
`chat.${this.selectedCategory}.${phrase.key}`,
)
? "selected"
: ""}"
@click=${() => this.selectPhrase(phrase)}
@@ -127,12 +132,14 @@ export class ChatModal extends LitElement {
${this.requiresPlayerSelection || this.selectedPlayer
? html`
<div class="chat-column">
<div class="column-title">Player</div>
<div class="column-title">
${translateText("chat.player")}
</div>
<input
class="player-search-input"
type="text"
placeholder="Search player..."
placeholder="${translateText("chat.search")}"
.value=${this.playerSearchQuery}
@input=${this.onPlayerSearchInput}
/>
@@ -158,7 +165,9 @@ export class ChatModal extends LitElement {
</div>
<div class="chat-preview">
${this.previewText || "Build your message..."}
${this.previewText
? translateText(this.previewText)
: translateText("chat.build")}
</div>
<div class="chat-send">
<button
@@ -166,7 +175,7 @@ export class ChatModal extends LitElement {
@click=${this.sendChatMessage}
?disabled=${!this.previewText}
>
Send
${translateText("chat.send")}
</button>
</div>
</o-modal>
@@ -183,20 +192,24 @@ export class ChatModal extends LitElement {
}
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.selectedPhraseTemplate = translateText(
`chat.${this.selectedCategory}.${phrase.key}`,
);
this.selectedPhraseText = translateText(
`chat.${this.selectedCategory}.${phrase.key}`,
);
this.previewText = `chat.${this.selectedCategory}.${phrase.key}`;
this.requiresPlayerSelection = phrase.requiresPlayer;
this.selectedPlayer = null;
this.requestUpdate();
}
private renderPhrasePreview(phrase: { text: string }) {
return phrase.text.replace("[P1]", "___"); // 仮表示
private renderPhrasePreview(phrase: { key: string }) {
return translateText(`chat.${this.selectedCategory}.${phrase.key}`);
}
private selectPlayer(player: string) {
@@ -270,6 +283,7 @@ export class ChatModal extends LitElement {
this.recipient = recipient;
this.sender = sender;
}
this.requestUpdate();
this.modalEl?.open();
}
@@ -16,6 +16,7 @@ import {
AllianceRequestUpdate,
AttackUpdate,
BrokeAllianceUpdate,
DisplayChatMessageUpdate,
DisplayMessageUpdate,
EmojiUpdate,
GameUpdateType,
@@ -33,6 +34,8 @@ import { onlyImages } from "../../../core/Util";
import { renderTroops } from "../../Utils";
import { GoToPlayerEvent, GoToUnitEvent } from "./Leaderboard";
import { translateText } from "../../Utils";
interface Event {
description: string;
unsafeDescription?: boolean;
@@ -77,6 +80,7 @@ export class EventsDisplay extends LitElement implements Layer {
private updateMap = new Map([
[GameUpdateType.DisplayEvent, (u) => this.onDisplayMessageEvent(u)],
[GameUpdateType.DisplayChatEvent, (u) => this.onDisplayChatEvent(u)],
[GameUpdateType.AllianceRequest, (u) => this.onAllianceRequestEvent(u)],
[
GameUpdateType.AllianceRequestReply,
@@ -187,6 +191,34 @@ export class EventsDisplay extends LitElement implements Layer {
});
}
onDisplayChatEvent(event: DisplayChatMessageUpdate) {
const myPlayer = this.game.playerByClientID(this.clientID);
if (
event.playerID === null ||
!myPlayer ||
myPlayer.smallID() !== event.playerID
) {
return;
}
const baseMessage = translateText(`chat.${event.category}.${event.key}`);
const translatedMessage = baseMessage.replace(
/\[([^\]]+)\]/g,
(_, key) => event.variables?.[key] || `[${key}]`,
);
this.addEvent({
description: translateText(event.isFrom ? "chat.from" : "chat.to", {
user: event.recipient,
msg: translatedMessage,
}),
createdAt: this.game.ticks(),
highlight: true,
type: MessageType.CHAT,
unsafeDescription: false,
});
}
onAllianceRequestEvent(update: AllianceRequestUpdate) {
const myPlayer = this.game.playerByClientID(this.clientID);
if (!myPlayer || update.recipientID !== myPlayer.smallID()) {
+16 -30
View File
@@ -1,6 +1,5 @@
import quickChatData from "../../../resources/QuickChat.json";
import { consolex } from "../Consolex";
import { Execution, Game, MessageType, Player, PlayerID } from "../game/Game";
import { Execution, Game, Player, PlayerID } from "../game/Game";
export class QuickChatExecution implements Execution {
private sender: Player;
@@ -38,16 +37,22 @@ export class QuickChatExecution implements Execution {
tick(ticks: number): void {
const message = this.getMessageFromKey(this.quickChatKey, this.variables);
this.mg.displayMessage(
`${this.sender.name()}: ${message}`,
MessageType.CHAT,
this.mg.displayChat(
message[1],
message[0],
this.variables,
this.recipient.id(),
true,
this.recipient.name(),
);
this.mg.displayMessage(
`You sent to ${this.recipient.name()}: ${message}`,
MessageType.CHAT,
this.mg.displayChat(
message[1],
message[0],
this.variables,
this.sender.id(),
false,
this.recipient.name(),
);
consolex.log(
@@ -72,27 +77,8 @@ export class QuickChatExecution implements Execution {
private getMessageFromKey(
fullKey: string,
vars: Record<string, string>,
): string {
// Key for translation
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}]`,
);
): string[] {
const translated = fullKey.split(".");
return translated;
}
}
+9
View File
@@ -509,6 +509,15 @@ export interface Game extends GameMap {
playerID: PlayerID | null,
): void;
displayChat(
message: string,
category: string,
variables: Record<string, string>,
playerID: PlayerID | null,
isFrom: boolean,
recipient: string,
): void;
// Nations
nations(): Nation[];
+22
View File
@@ -606,6 +606,28 @@ export class GameImpl implements Game {
});
}
displayChat(
message: string,
category: string,
variables: Record<string, string> = {},
playerID: PlayerID | null,
isFrom: boolean | null = null,
recipient: string,
): void {
let id = null;
if (playerID != null) {
id = this.player(playerID).smallID();
}
this.addUpdate({
type: GameUpdateType.DisplayChatEvent,
key: message,
category: category,
variables: variables,
playerID: id,
isFrom: isFrom,
recipient: recipient,
});
}
addUnit(u: Unit) {
this.unitGrid.addUnit(u);
}
+12
View File
@@ -29,6 +29,7 @@ export enum GameUpdateType {
Unit,
Player,
DisplayEvent,
DisplayChatEvent,
AllianceRequest,
AllianceRequestReply,
BrokeAlliance,
@@ -48,6 +49,7 @@ export type GameUpdate =
| BrokeAllianceUpdate
| AllianceExpiredUpdate
| DisplayMessageUpdate
| DisplayChatMessageUpdate
| TargetPlayerUpdate
| EmojiUpdate
| WinUpdate
@@ -157,6 +159,16 @@ export interface DisplayMessageUpdate {
playerID: number | null;
}
export type DisplayChatMessageUpdate = {
type: GameUpdateType.DisplayChatEvent;
key: string;
category: string;
variables?: Record<string, string>;
playerID: number | null;
isFrom: boolean;
recipient: string;
};
export interface WinUpdate {
type: GameUpdateType.Win;
allPlayersStats: AllPlayersStats;