using System.Collections.Generic;
using System.Text;
using UnityEngine;
namespace ServiceSim
{
///
/// キッチンのモニター(2026-10-23 エイイチと決めた。設計書 v0 の 0 章)。
/// ロボット・スタッフのいまの仕事と止まりの理由、店の様子を1つにまとめて出す。
///
/// 出し方は **Scene ビューの画面に貼り付けるパネル1つ**(描くのは ServiceSimLabels)。
/// カメラを回してもズームしても、画面の上の位置は変わらない。
/// 屋根の上に板を置く作り・店の上に浮かべる吹き出しは、どちらもやめた
/// (読めなくなる/通路やほかの札に重なるため)。
///
/// この script は **文を作るだけ**で、見た目は持たない。
/// ・当たり判定・NavMesh・ロボットのセンサー・人がよける処理には入らない
/// ・言葉と数字は、記録に使っているものだけ(LABEL_RULES.md の表)
/// ・文を作り直すのは店の中の 0.5 秒ごと(毎コマ作らない)
/// ・頭の上の字幕は測る回では切る(B11)が、このパネル1つだけは出す
///
public class KitchenMonitor : MonoBehaviour
{
[Header("参照(Setup が入れる)")]
public StoreBuilder store;
public ServiceOps ops;
public GroupManager groups;
public PeopleSpawner people;
[Header("出し方")]
[Tooltip("出すか。ExperimentPlan.showKitchenMonitor が入れる")]
public bool show = true;
[Tooltip("文を作り直す間隔(店の中の秒)。毎コマ作らない")]
public float refreshSec = 0.5f;
[Header("いまの様子(読むだけ)")]
[SerializeField] int _lines;
float _nextAtS;
string _text = "";
readonly StringBuilder _sb = new StringBuilder();
/// この部品を置く親の名前
public const string RootName = "KitchenMonitor";
/// いま出す文(ServiceSimLabels が読む)。空なら出さない
public string Text { get { return _text; } }
/// 行の数(読むだけ)
public int Lines { get { return _lines; } }
void Update()
{
if (!show) { _text = ""; _lines = 0; return; }
if (ops == null) return; // 回していないあいだは見本を出す(描く側が決める)
float now = ops.NowS;
if (now < _nextAtS) return;
_nextAtS = now + Mathf.Max(0.05f, refreshSec);
_text = Compose();
_lines = CountLines(_text);
}
///
/// 店を建てたあと、まだ回していないときに出す見本(ServiceSimLabels が読む)。
/// 場所と大きさを確かめられるようにするため
///
public static string SampleText()
{
return "ロボット×2\n" +
" 1号機 配膳 卓19 / 一時停止:人が立ち止まっていた\n" +
" 2号機 戻る\n" +
"スタッフ×4 案内 1/配膳 1/片付け 1/待機 1\n" +
"ロボット待ち スタッフ 0 人 / 客 1 人(回り道 0 人)\n" +
"組 0(席待ち 0・帰った 0) 皿 0\n" +
"受け渡し口でロボットを待っている皿 0\n" +
"あきらめた 0 / 移した 0 / 卓を空けた 0\n" +
"立ち往生 0 回 ※見本(回すと本物の数字になります)";
}
static int CountLines(string s)
{
if (string.IsNullOrEmpty(s)) return 0;
int n = 1;
for (int i = 0; i < s.Length; i++) if (s[i] == '\n') n++;
return n;
}
///
/// 前の作り(屋根の上の板)で置いた子を片付ける。
/// 吹き出しに変えたので、板と文字は要らない
///
public void RemoveOldBoards()
{
RemoveChild("Board");
RemoveChild("Backing");
}
void RemoveChild(string name)
{
Transform t = transform.Find(name);
if (t == null) return;
if (Application.isPlaying) Destroy(t.gameObject);
else DestroyImmediate(t.gameObject);
}
// ---------------- 文を作る ----------------
string Compose()
{
_sb.Length = 0;
// ---- ロボット(台ごとに仕事と、止まっているときは理由)----
IList rs = ops.Robots;
_sb.Append("ロボット×").Append(rs.Count).Append('\n');
for (int i = 0; i < rs.Count; i++)
{
ServingRobotController r = rs[i];
if (r == null) continue;
_sb.Append(" ").Append(r.robotId).Append("号機 ").Append(RobotJob(r));
string why = StopText(r);
if (why.Length > 0) _sb.Append(" / ").Append(why);
_sb.Append('\n');
}
// ---- スタッフ(仕事ごとの人数)----
AppendStaff();
// ---- ロボット待ち(スタッフと客の人数)----
int waitStaff, waitGuest, detours;
CountYielding(out waitStaff, out waitGuest, out detours);
_sb.Append("ロボット待ち スタッフ ").Append(waitStaff)
.Append(" 人 / 客 ").Append(waitGuest).Append(" 人")
.Append("(回り道 ").Append(detours).Append(" 人)").Append('\n');
// ---- 店の様子 ----
if (groups != null)
_sb.Append("組 ").Append(groups.GroupsArrived)
.Append("(席待ち ").Append(groups.WaitingGroups)
.Append("・帰った ").Append(groups.GroupsDone).Append(")");
_sb.Append(" 皿 ").Append(ops.DishesWaiting).Append('\n');
// ---- 受け渡し口でロボットを待っている皿(サイトの試算 v2.7)----
// 料理ができてから robot_first_wait_s のあいだは、ロボットだけが運ぶ。
// そのあいだ、まだ誰も持っていない皿の数(記録と同じ数え方)
_sb.Append("受け渡し口でロボットを待っている皿 ")
.Append(ops.DishesWaitingForRobot).Append('\n');
_sb.Append("あきらめた ").Append(ops.gaveUpCount)
.Append(" / 移した ").Append(ops.warpedCount)
.Append(" / 卓を空けた ").Append(ops.tableFreedCount).Append('\n');
_sb.Append("立ち往生 ").Append(ops.rescueCalls).Append(" 回");
return _sb.ToString();
}
/// ロボットの仕事(LABEL_RULES の言葉)
static string RobotJob(ServingRobotController r)
{
switch (r.CurrentPhase)
{
case ServingRobotController.Phase.ToTable: return "配膳 卓" + r.TableId.ToString("00");
case ServingRobotController.Phase.Dwell: return "配膳 卓" + r.TableId.ToString("00");
case ServingRobotController.Phase.ToPass: return "戻る";
case ServingRobotController.Phase.StuckWait: return "立ち往生";
default: return "待機";
}
}
/// 止まっているときの理由(動いていれば空)
static string StopText(ServingRobotController r)
{
if (r.CurrentPhase == ServingRobotController.Phase.StuckWait) return ""; // 仕事の側に出ている
if (r.spec != null && r.SpeedMps > r.spec.stoppedSpeedEpsilon) return "";
string why = r.StopCauseNow;
if (why.Length == 0) return "";
return "一時停止:" + why;
}
void AppendStaff()
{
IReadOnlyList staff = StaffAgent.All;
_sb.Append("スタッフ×").Append(staff.Count);
Dictionary byJob = new Dictionary();
for (int i = 0; i < staff.Count; i++)
{
if (staff[i] == null) continue;
string job = MonitorJobName(staff[i]);
int n;
byJob.TryGetValue(job, out n);
byJob[job] = n + 1;
}
bool first = true;
foreach (KeyValuePair kv in byJob)
{
_sb.Append(first ? " " : "/").Append(kv.Key).Append(' ').Append(kv.Value);
first = false;
}
_sb.Append('\n');
}
///
/// スタッフの仕事の名前。記録(staff_time_s)の言葉は過去の記録と比べるため変えないので、
/// ここだけ LABEL_RULES の表の言葉に直して出す(料理 → 配膳)。
///
static string MonitorJobName(StaffAgent a)
{
if (a.CurrentJob == null) return "待機";
switch (a.CurrentJob.kind)
{
case StaffJobKind.Guide: return "案内";
case StaffJobKind.Serve: return "配膳";
case StaffJobKind.Call: return "呼び出し";
case StaffJobKind.Pay: return "会計";
case StaffJobKind.Clear: return "片付け";
case StaffJobKind.Rescue: return "立ち往生";
}
return "待機";
}
///
/// いまロボットをよけている人を数える。
/// ロボット待ち … 入口で待つ・通路の中で下がる(止まっている)
/// 回り道 … 別の通路へまわっている(止まっていない)
///
void CountYielding(out int waitStaff, out int waitGuest, out int detours)
{
waitStaff = 0; waitGuest = 0; detours = 0;
IReadOnlyList all = PersonBody.All;
for (int i = 0; i < all.Count; i++)
{
PersonBody b = all[i];
if (b == null || b.kind == PersonKind.Seated) continue;
PersonRobotYield y = b.GetComponent();
if (y == null || !y.Intervening) continue;
if (y.CurrentMode == PersonRobotYield.Mode.Detour) { detours++; continue; }
if (b.kind == PersonKind.Staff) waitStaff++;
else waitGuest++;
}
}
}
}