【Day21】会話が大切である事は自明である【QAC24】


# いうほど自明なのだろうか...。

`say`というクラスを作りましょう。

```typescript:src/screen/say.ts
import { Screen } from "../lib/screen";

export class Say extends Screen {
  public screenId = "say";

  constructor() {
    super();
  }
}
```

まぁまずはこんな感じでしょうか。
`screenId`については`abstract`なので定義する必要があります。

では少しずつデザインをくみ上げて行きましょう。

# メッセージ表示部分

まずはメッセージ表示部分を作ります。

```typescript:src/screen/say.ts
export class Say extends Screen {
  public screenId = "say";

  constructor() {
    super();
    const screen = super.build();
    this.hide();

    const messageElement = document.createElement("div");
    messageElement.id = "say-message";
    screen.appendChild(messageElement);
  }

  public say(message: string) {
    const messageElement = this.getElementById<HTMLDivElement>("say-message");
    if (!this.showing) this.show();
    if (messageElement) {
      messageElement.textContent = message;
    }
  }
}
```

`say`メソッドをはやしてみました。
これを使うことでメッセージを表示できます。
（メッセージ毎にクラスを作っていると SDGs に反するので使いまわしましょう。）

ですが、これだけだとテキストが表示されるだけです。
もう少しデザインを加えてみましょう。

```typescript:src/screen/say.ts
export class Say extends Screen {
  ...

  constructor() {
    super();
    const screen = super.build();
    this.hide();

    const messageElement = document.createElement("div");
    messageElement.id = "say-message";
    screen.appendChild(messageElement);

    const style = document.createElement("style");
    style.textContent = `
      #say-message {
        background-color: #000;
        border-radius: 10px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
        padding: 10px;
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        color: #fff;
      }
    `;
    screen.appendChild(style);
  }

  ...
}
```

このようにするだけで、メッセージ表示部分がデザインされます。

(Copilot がこの CSS を生み出したが Say ではないなこれ。)

## メッセージだけ出せばいい？

そう思うあなたは一回なんかしらのゲームをやってきてください。
セリフには、セリフを話している話者の名前が表示されることが多いです。

後は、右下の三角マークみたいなのがあるといいですね。

では、名前を表示する部分を作りましょう。

```typescript:src/screen/say.ts
export class Say extends Screen {
  ...

  constructor() {
    super();
    const screen = super.build();
    this.hide();

    const whome = document.createElement("div");
    whome.id = "say-whome";
    whome.style.display = "none";
    screen.appendChild(whome);

    const messageElement = document.createElement("div");
    messageElement.id = "say-message";
    screen.appendChild(messageElement);

    const pagenationElement = document.createElement("div");
    pagenationElement.id = "say-pagenation";
    pagenationElement.innerHTML = "▼";
    screen.appendChild(pagenationElement);

    const style = document.createElement("style");
    style.innerHTML = `
    #say {
      position: absolute;
      bottom: 0%;
      left: 50%;
      width: 80%;
      height: 20%;
      transform: translate(-50%, -100%);
      margin: 0;
      background-color: rgb(0, 0, 0, 0.8);
      border-radius: 10px;
      border: 2px solid #fff;
      display: flex;
      flex-direction: column;
      overflow-y: auto;
      cursor: pointer;
    }

    #say-whome {
      padding: 10px;
      color: #fff;
      border-radius: 10px 10px 0 0;
      border-bottom: 2px solid #fff;
    }

    #say-message {
      padding: 10px;
      color: #fff;
    }

    #say-pagenation {
      position: absolute;
      bottom: 0;
      right: 0;
      padding: 10px;
      color: #fff;
      text-align: right;
    }
    `;
    screen.appendChild(style);
  }

  ...
}
```

こんなデザインになるでしょうか。
ちゃんと動かすために`say`プロパティを調整しましょう。

```typescript:src/screen/say.ts
/// 以下は別ファイルにまとめてもいいかもしれない (ex: src/lib/sleep.ts)
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export class Say extends Screen {
  ...

  public async say(
    message: string,
    name?: string,
    hide: boolean = false,
    speed: number = 50
  ) {
    const messageElement = this.getElementById<HTMLDivElement>("say-message");
    if (!messageElement) {
      throw new Error("Message element not found");
    }
    if (name) {
      const whomeElement = this.getElementById<HTMLDivElement>("say-whome");
      if (!whomeElement) {
        throw new Error("Whome element not found");
      }
      whomeElement.innerHTML = name;
      whomeElement.style.display = "block";
    }
    messageElement.innerHTML = "";
    if (!this.showing) this.show();
    return new Promise<void>(async (resolve) => {
      let skip = false;
      this.element.onclick = () => {
        skip = true;
      };
      for (const char of message) {
        messageElement.innerHTML += char;
        if (speed <= 0) continue;
        if (skip) {
          messageElement.innerHTML = message;
          break;
        } else {
          await sleep(speed);
        }
      }
      this.element.onclick = () => {
        if (hide) {
          this.hide();
          messageElement.innerHTML = "";
          if (name) {
            const whomeElement =
              this.getElementById<HTMLDivElement>("say-whome");
            if (!whomeElement) {
              throw new Error("Whome element not found");
            }
            whomeElement.style.display = "none";
          }
        }
        this.element.onclick = null;
        resolve();
      };
    });
  }

  ...
}
```

一気に進化させました。
セリフが表示されるときのあの 1 文字ずつ出てくるあれとかです。

これで、セリフを表示する部分は完成です。

`say`だけは、よく使われる&使いまわす想定なので、`GameMap`の中にも埋め込ませておきましょう。
例えばプロパティとして`say`を持たせるとか。

# まとめ

これで、セリフを表示する部分が完成しました。
（というかスクリーンの使い方は分かりましたよね！）

でもこれだけだとセリフの表示をそもそもどうやってやるんだって話ですよね。
次回はイベント系の話をしましょうか。