using System.Collections.Generic;
using UnityEngine;
namespace ServiceSim
{
///
/// 客を「1組ごとに、入ってから出るまで」で動かす(試算の group() と同じ流れ)。
///
/// 入店 → 卓が空くまで待合(あふれたら店の外) → 案内 → 着席 → 料理 → 食事
/// (食事中にドリンクバー・トイレ・呼び出し) → 会計 → 退店 → 片付け
///
/// 座っている人は人として置かない(通路に出ないため)。
/// 立ち上がった人だけを、使い回しの体(Guest.prefab)で歩かせる。
///
/// この script は人だけを扱う。卓の空きと料理の待ち行列は ServiceOps に任せる。
/// 数字は ServiceSpec と PeopleSpec の .asset に置く。
///
public class GroupManager : MonoBehaviour
{
/// 1組の今の状態
public enum GState { Waiting, Guided, Seated, Eating, Paying, Left, Clearing, Done }
/// 1組
public class Group
{
public int id;
public int size;
public GState state = GState.Waiting;
public TableSlot table;
public float arriveS = -1f;
public float guideStartS = -1f;
public float seatedS = -1f;
///
/// その組だけの乱数(依頼 v8b の B)。
/// 種と組の番号から作るので、**いつ引いても同じ組なら同じ値**になる。
/// 人数・料理ができるまで・食べる時間・割れ物の数・呼び出し・
/// ドリンクバー・トイレは、ぜんぶここから決まった順に引く。
/// 動きが1か所ずれても、ほかの組の中身は変わらない
///
public System.Random rng;
/// 食べ始めた時刻(最初の料理が全部届いた時刻, 秒)。まだなら -1(依頼 v8 の 2)
public float eatStartS = -1f;
public float eatEndS = -1f;
public float payS = -1f;
public float leftS = -1f;
public bool visibleWaiting;
public bool warmup; // 慣らしのために最初から座っている組
public readonly List dishes = new List();
public readonly List cookAt = new List(); // 受け渡し口に出す時刻
public readonly List cookFragile = new List();
public readonly List errandAt = new List(); // 立ち上がる時刻
public readonly List errandKind = new List(); // 0=ドリンクバー 1=トイレ
public readonly List callAt = new List();
public int activeErrands;
public int dishesOrdered;
/// 何人目が立ったか。いつも同じ人が同じ椅子を引くようにする(B6)
public int chairTurn;
/// 会計・片付けで止まったままの秒数(B1。長びいたら卓を空ける)
public float stuckS;
///
/// 卓は空いたのに片付けが終わらず待った秒数(v5 3章・blocked_wait_mean)。
/// サイトの試算(haizen_sim.py の _acc)と同じ数え方で、
/// 「片付け待ちの卓の数だけ、待っている列の前から順に、そのコマの時間を足す」。
///
public float blockedWaitS;
public GuestAgent waitingBody; // 待合に立たせている1人
// ---- 会計(レジに立つのは組の1人だけ)----
/// 会計に立つ1人の体。ほかの人は席に座ったまま
public GuestAgent payerBody;
/// 取ったレジの並ぶ点。-1 なら取れていない(席で待つ)
public int payerSpot = -1;
/// 会計の人をレジへ送ったか
public bool payerSent;
/// 会計を始めた時刻(食べ終わって Paying に入った時刻)
public float payStartS = -1f;
public StaffJob job; // いま出している仕事
}
[Header("設定")]
public ServiceSpec spec;
public PeopleSpec people;
public ServiceOps ops;
public StoreBuilder store;
[Tooltip("卓ごとの椅子の出し入れを読む先(B6)。無くても動く")]
public PeopleSpawner spawner;
[Header("部品")]
[Tooltip("歩く客の体。待合の客にも使い回す")]
public GameObject guestPrefab;
[Tooltip("使っていない(段階4の場面の再現用に残してある)")]
public GameObject waitingGuestPrefab;
[Tooltip("歩く人を置く親")]
public Transform bodyRoot;
[Header("いまの様子(読むだけ)")]
[SerializeField] int _groupsArrived;
[SerializeField] int _groupsSeated;
[SerializeField] int _groupsDone;
[SerializeField] int _waitingGroups;
[SerializeField] int _walkingBodies;
readonly List _groups = new List();
readonly List _waiting = new List();
readonly List _pool = new List();
readonly List _live = new List();
PeopleTargets _targets;
///
/// 客の中身でないもの(待つ場所を選ぶなど)に使う乱数。
/// 来客の間隔は _arrivalRng、組ごとのことは Group.rng(依頼 v8b の B)
///
System.Random _rng;
///
/// 来客の間隔だけの乱数(依頼 v8b の B)。
/// ほかの引きと混ぜないので、動きがずれても **来る組と来る時刻は変わらない**
///
System.Random _arrivalRng;
/// 組ごとの乱数を作るもとの種
int _seed;
float _nextArrivalS;
int _nextGroupId = 1;
CrowdLevel _crowd;
public IList Groups { get { return _groups; } }
public int GroupsArrived { get { return _groupsArrived; } }
public int GroupsSeated { get { return _groupsSeated; } }
public int GroupsDone { get { return _groupsDone; } }
public int WaitingGroups { get { return _waitingGroups; } }
/// いま通路を歩いている人の数(スタッフを除く)
public int WalkingBodies { get { return _walkingBodies; } }
// ---------------- 用意 ----------------
///
/// 来客を受け付ける長さ(店の中の秒)。これを過ぎたら新しい組は来ない(v5 3章)。
/// サイトの決まりは「空の店から始め、peak_min だけ来客し、全員が帰るまで回す」。
/// 0 以下なら、これまでどおり止めずに来客し続ける。
///
public float arrivalsEndS;
///
/// 慣らしのために、始めから食事中の組で卓を埋めるか(v5 3章)。
/// サイトと同じ「空の店から」にするときは false。
///
public bool seedWarmupGroups = true;
/// まだ店の中にいる組があるか(全員が帰ったかの判定。v5 3章)
public bool AnyGroupInStore
{
get
{
if (_waiting.Count > 0) return true;
for (int i = 0; i < _groups.Count; i++)
if (_groups[i].state != GState.Done) return true;
return false;
}
}
public void Setup(CrowdLevel crowd, int seed, PeopleTargets targets)
{
_crowd = crowd;
_seed = seed;
_rng = new System.Random(seed * 7919 + 13);
_arrivalRng = new System.Random(seed * 7919 + 101); // 来客だけの列(依頼 v8b の B)
_targets = targets;
_groups.Clear();
_waiting.Clear();
ReturnAllBodies();
_groupsArrived = 0;
_groupsSeated = 0;
_groupsDone = 0;
_nextGroupId = 1;
float rate = spec.ArrivalsPerHour(crowd) / 3600f;
_nextArrivalS = ServiceSpec.ExponentialSec(_arrivalRng, rate);
if (seedWarmupGroups) SeedWarmupGroups();
}
///
/// 慣らしのために、卓の一部を「食事の途中の組」で埋めて始める。
/// 空の店から始めると、はじめの数十分が本番と違う形になるため。
///
void SeedWarmupGroups()
{
if (people == null || ops == null) return;
int tables = ops.Tables.Count;
int want = Mathf.RoundToInt(tables * Mathf.Clamp01(people.warmupOccupancy));
int want0 = want;
int placed = 0;
for (int i = 0; i < tables && want > 0; i++)
{
TableSlot slot = ops.TakeTableAt(i, _nextGroupId);
if (slot == null) continue;
Group g = new Group();
g.id = _nextGroupId++;
g.rng = NewGroupRng(g.id); // その組だけの乱数(依頼 v8b の B)
// 2人席に3人以上を座らせない。卓の席数で頭を切る
g.size = Mathf.Min(spec.PickGroupSize(g.rng), Mathf.Max(1, slot.seatCount));
g.table = slot;
g.warmup = true;
g.arriveS = 0f;
g.seatedS = 0f;
// 食事のどの時点かを決める(B3)。
// 0.00〜0.25:座ったばかりで料理を待っている(1皿めも2皿めもまだ)
// 0.25〜0.50:1皿めは届いたが2皿めを待っている
// 0.50〜1.00:受け取って食べている〜食べ終わりかけ
// でたらめにすると食べ終わりかけが固まり、そのぶんの会計がいちどに来て
// レジが詰まるので、均等にばらす
double phase = (placed + 0.5) / Mathf.Max(1, want0);
if (phase < 0.25) SeedWaitingFirstDishes(g, 2);
else if (phase < 0.5) SeedWaitingFirstDishes(g, 1);
else SeedEating(g, (float)((phase - 0.5) / 0.5));
placed++;
_groups.Add(g);
_groupsArrived++;
_groupsSeated++;
want--;
}
}
///
/// 慣らし:座ったばかりで料理を待っている組(B3)。
/// まだ届いていない皿は、作る時間の残りをでたらめに決めて受け渡し口の待ち行列に入る。
///
void SeedWaitingFirstDishes(Group g, int pending)
{
g.state = GState.Seated;
g.dishesOrdered = 2;
// 1皿め・2皿めの作る時間。pending が 1 なら1皿めは届いた扱いにする
float cook1 = ServiceSpec.TriangularSec(g.rng, spec.firstDish1CookMin);
float cook2 = ServiceSpec.TriangularSec(g.rng, spec.firstDish2CookMin);
if (pending >= 2)
{
g.cookAt.Add((float)g.rng.NextDouble() * cook1);
g.cookFragile.Add(false);
}
else
{
// 1皿めは届いている
g.dishes.Add(ops.SeedArrivedDish(g.id, g.table));
}
g.cookAt.Add((float)g.rng.NextDouble() * cook2);
g.cookFragile.Add(false);
}
///
/// 慣らし:もう食べている組(B3)。
/// done は食事の進み具合(0 なら食べ始め、1 なら食べ終わりかけ)。
/// 均等にばらすので、食べ終わりがいちどに来ない。
///
void SeedEating(Group g, float done)
{
g.state = GState.Eating;
g.dishesOrdered = 2;
// 最初の料理は届いている扱い
for (int k = 0; k < 2; k++) g.dishes.Add(ops.SeedArrivedDish(g.id, g.table));
float eat = ServiceSpec.TriangularSec(g.rng, spec.eatMin);
g.eatEndS = eat * (1f - Mathf.Clamp01(done));
ScheduleDuringMeal(g, g.eatEndS);
}
// ---------------- 毎コマ ----------------
public void Tick(float dt)
{
if (spec == null || ops == null || _rng == null) return;
float now = ops.NowS;
Arrive(now);
CountBlockedWait(dt);
SeatWaiting(now);
for (int i = 0; i < _groups.Count; i++) Advance(_groups[i], now, dt);
_waitingGroups = _waiting.Count;
_walkingBodies = _live.Count;
}
///
/// 「卓は空いたのに片付けが終わらず待った」分を数える(v5 3章・blocked_wait_mean)。
///
/// サイトの試算(haizen_sim.py の _acc)と同じ数え方にそろえた:
/// 片付け待ちの卓の数だけ、待っている列の**前から順に**、そのコマの時間を足す。
/// 片付けが終わっていれば、その卓の数だけ先頭の組が座れていたはず、という数え方。
///
/// full_wait は「席待ち − blocked_wait」で出す(サイトと同じ)。
/// 卓は空いているのに案内が追いつかない分(guide_wait)は、
/// 案内の仕事が板に出てから取られるまでで測る(OnGuided で記録する)。
///
/// サイトの試算には卓の大きさが無いので、ここも席数を見ずに列の前から数える
/// (2人席・4人席は Unity だけのもの)。
///
void CountBlockedWait(float dt)
{
int clearing = ops.ClearingTableCount;
if (clearing <= 0) return;
int n = Mathf.Min(clearing, _waiting.Count);
for (int i = 0; i < n; i++) _waiting[i].blockedWaitS += dt;
}
/// 来客(指数分布。試算の arrivals() と同じ)
void Arrive(float now)
{
// サイトの決まり:peak_min だけ来客し、そのあとは新しい組は来ない(v5 3章)
if (arrivalsEndS > 0f && now >= arrivalsEndS) return;
if (now < _nextArrivalS) return;
float rate = spec.ArrivalsPerHour(_crowd) / 3600f;
_nextArrivalS = now + ServiceSpec.ExponentialSec(_arrivalRng, rate);
Group g = new Group();
g.id = _nextGroupId++;
g.rng = NewGroupRng(g.id); // その組だけの乱数(依頼 v8b の B)
g.size = spec.PickGroupSize(g.rng);
g.arriveS = now;
g.state = GState.Waiting;
_groups.Add(g);
_waiting.Add(g);
_groupsArrived++;
// 待合に立たせるのは上限まで。あふれた組は店の外で待つ扱い(人として置かない)
if (CountVisibleWaiting() < spec.waitingVisibleMax)
{
g.visibleWaiting = true;
g.waitingBody = BorrowBody(_targets.RandomWaitingSpot(_rng), true);
if (g.waitingBody != null)
g.waitingBody.Begin(people, GuestAgent.Errand.Waiting, g.waitingBody.transform.position, 0f,
false, Vector3.zero, null);
}
}
int CountVisibleWaiting()
{
int n = 0;
for (int i = 0; i < _waiting.Count; i++) if (_waiting[i].visibleWaiting) n++;
return n;
}
///
/// その組だけの乱数を作る(依頼 v8b の B)。
///
/// 種と組の番号だけで決まるので、**いつ作っても同じ組なら同じ列**になる。
/// 引く順番も組ごとに決まっている(人数 → 最初の2皿の作る時間 → 食べる時間 →
/// 割れ物 → 呼び出し → ドリンクバー → トイレ)ので、
/// 動きが1か所ずれても、ほかの組の中身は変わらない。
///
/// 引く数の分布・決まり(ServiceSpec の値)は変えていない。
/// 変わったのは「どの乱数の列から引くか」だけ。
///
System.Random NewGroupRng(int groupId)
{
unchecked
{
int h = _seed * 73856093;
h ^= groupId * 19349663;
h = h * 83492791 + 0x5bd1e995;
return new System.Random(h & 0x7fffffff); // 負の種を避ける
}
}
///
/// 空いた卓に、待っている組を先着順で入れる。
/// 2人席と4人席があるので、先頭の組が入れる卓が無いときは、
/// その組を飛ばして次の組を見る(4人の組が先頭にいるあいだ、
/// 2人席が空いているのに誰も座れない、を避ける)。
///
void SeatWaiting(float now)
{
while (_waiting.Count > 0 && ops.FreeTableCount > 0)
{
int pick = -1;
for (int i = 0; i < _waiting.Count; i++)
if (ops.HasFreeTableFor(_waiting[i].size)) { pick = i; break; }
if (pick < 0) break;
Group g = _waiting[pick];
TableSlot slot = ops.TakeFreeTable(g.id, g.size);
if (slot == null) break;
_waiting.RemoveAt(pick);
g.table = slot;
g.guideStartS = now;
g.state = GState.Guided;
ops.RecordSeatWait(g.arriveS);
// スタッフが待合へ迎えに行き、卓まで案内する
Group captured = g;
StaffJob job = ops.PostJob(StaffJobKind.Guide, g.id, slot, null, 1);
job.onDone = delegate { OnGuided(captured); };
g.job = job;
}
}
///
/// その組の卓の椅子を1脚引く(B6)。人が動く瞬間だけ呼ぶ。
/// 何人目かを順に進めて、同じ人がいつも同じ椅子を引くようにする。
///
void PullChair(Group g, string reason)
{
if (spawner == null || g == null || g.table == null) return;
SeatedGuest s = spawner.SeatedAt(g.table.index);
if (s == null) return;
s.PullFor(reason, g.chairTurn++);
}
void OnGuided(Group g)
{
// 入店待ちの内訳を1組ぶん残す(v5 3章。分け方はサイトの試算と同じ)。
// entry … 店に来てから卓に着くまで(案内の歩きと guide_s を含む)
// seat … 店に来てから案内が始まるまで
// blocked … そのうち、卓は空いたのに片付けが終わらず待った分
// guide … 案内の仕事が板に出てから、スタッフが取るまで
// full = seat − blocked、staff = blocked + guide は ServiceOps で出す
if (g.arriveS >= 0f && !g.warmup)
{
float entry = ops.NowS - g.arriveS;
float seat = g.guideStartS >= 0f ? g.guideStartS - g.arriveS : 0f;
float guideWait = g.job != null && g.job.takenS >= 0f
? Mathf.Max(0f, g.job.takenS - g.job.postedS) : 0f;
ops.RecordEntryWait(entry, seat, g.blockedWaitS, guideWait);
}
g.job = null;
g.state = GState.Seated;
g.seatedS = ops.NowS;
// 席に着くので椅子を引く(B6)
PullChair(g, SeatedGuest.ReasonSit);
// 案内された組は席に着く(待合の体を返す)
if (g.waitingBody != null) { ReturnBody(g.waitingBody); g.waitingBody = null; }
g.visibleWaiting = false;
// 最初の注文(試算の first_dishes と同じ数・同じ作る時間)
g.cookAt.Add(g.seatedS + ServiceSpec.TriangularSec(g.rng, spec.firstDish1CookMin));
g.cookFragile.Add(false);
g.cookAt.Add(g.seatedS + ServiceSpec.TriangularSec(g.rng, spec.firstDish2CookMin));
g.cookFragile.Add(false);
g.dishesOrdered = 2;
}
/// 食事中に起きること(デザート・呼び出し・ドリンクバー・トイレ)を先に決める
void ScheduleDuringMeal(Group g, float eatEndS)
{
float now = ops.NowS;
float eat = Mathf.Max(1f, eatEndS - now);
int nFragile = ServiceSpec.PickCount(g.rng, spec.fragilePerGroup);
for (int i = 0; i < nFragile; i++)
{
float at = now + (0.3f + 0.55f * (float)g.rng.NextDouble()) * eat;
g.cookAt.Add(at + ServiceSpec.TriangularSec(g.rng, spec.fragileCookMin));
g.cookFragile.Add(true);
g.dishesOrdered++;
}
// 呼び出し(試算は2回ぶん call_rate/2 で判定)
for (int i = 0; i < 2; i++)
if (g.rng.NextDouble() < spec.callRate / 2f)
g.callAt.Add(now + (float)g.rng.NextDouble() * eat);
// ドリンクバー(1人あたり)とトイレ(1組あたり)
int drinks = ServiceSpec.PickCount(g.rng, spec.drinkTripsPerPerson * g.size);
for (int i = 0; i < drinks; i++)
{
g.errandAt.Add(now + (float)g.rng.NextDouble() * eat);
g.errandKind.Add(0);
}
int toilets = ServiceSpec.PickCount(g.rng, spec.toiletTripsPerGroup);
for (int i = 0; i < toilets; i++)
{
g.errandAt.Add(now + (float)g.rng.NextDouble() * eat);
g.errandKind.Add(1);
}
}
void Advance(Group g, float now, float dt)
{
// できた料理を受け渡し口に出す
for (int i = g.cookAt.Count - 1; i >= 0; i--)
{
if (now < g.cookAt[i]) continue;
if (g.table != null) g.dishes.Add(ops.PutDish(g.id, g.table, g.cookFragile[i]));
g.cookAt.RemoveAt(i);
g.cookFragile.RemoveAt(i);
}
// 呼び出し
for (int i = g.callAt.Count - 1; i >= 0; i--)
{
if (now < g.callAt[i]) continue;
if (g.table != null) ops.PostJob(StaffJobKind.Call, g.id, g.table, null, 1);
g.callAt.RemoveAt(i);
}
// 立ち上がってドリンクバー・トイレへ
for (int i = g.errandAt.Count - 1; i >= 0; i--)
{
if (now < g.errandAt[i]) continue;
StartErrand(g, g.errandKind[i]);
g.errandAt.RemoveAt(i);
g.errandKind.RemoveAt(i);
}
switch (g.state)
{
case GState.Seated:
// 最初の料理が全部届いたら食事が始まる(試算の AllOf(firsts) と同じ)
if (AllFirstDishesArrived(g))
{
g.state = GState.Eating;
g.eatStartS = now; // 食べ始めた時刻(依頼 v8 の 2)
g.eatEndS = now + ServiceSpec.TriangularSec(g.rng, spec.eatMin);
ScheduleDuringMeal(g, g.eatEndS);
}
break;
case GState.Eating:
if (now < g.eatEndS) break;
if (g.cookAt.Count > 0 || !AllDishesArrived(g) || g.activeErrands > 0) break;
g.state = GState.Paying;
g.stuckS = 0f;
g.payStartS = now;
g.payerSent = false;
g.payerSpot = -1;
break;
case GState.Paying:
// 会計に立つのは組の1人だけ。レジの並ぶ点が空くまでは席で待つ
SendPayerIfPossible(g, now);
break;
case GState.Left:
// 退店したら、スタッフが片付けに行く(優先はいちばん後)
g.state = GState.Clearing;
g.stuckS = 0f;
if (g.table != null) g.table.clearing = true;
{
Group captured = g;
StaffJob job = ops.PostJob(StaffJobKind.Clear, g.id, g.table, null, 2);
job.onDone = delegate { OnCleared(captured); };
g.job = job;
}
break;
}
// 座っている客の形を、いま席にいる人数に合わせる(見た目だけ)
UpdateSeatedBodies(g);
// 会計・片付けで止まって卓が空かないままなら、時間を見て卓を空ける(B1)
if (g.state == GState.Paying || g.state == GState.Clearing)
{
g.stuckS += dt;
if (spec.tableStuckFreeSec > 0f && g.stuckS >= spec.tableStuckFreeSec)
ForceFreeTable(g);
}
}
///
/// 会計・片付けが進まないまま時間が過ぎた組の卓を、強引に空ける(B1)。
/// これが多いときは、人が通れない店になっているしるし。
///
///
/// 座っている客の形を、いま席にいる人数に合わせる(見た目だけ)。
/// 席にいるのは「組の人数 − 立って歩いている人 − 会計に立っている人」。
/// 席を立つ・卓を空けると 0 になる。
///
void UpdateSeatedBodies(Group g)
{
if (spawner == null || g.table == null) return;
SeatedGuest seated = spawner.SeatedAt(g.table.index);
if (seated == null) return;
bool sitting = g.state == GState.Seated || g.state == GState.Eating ||
g.state == GState.Paying;
int n = 0;
if (sitting)
{
n = g.size - g.activeErrands - (g.payerBody != null ? 1 : 0);
if (n < 0) n = 0;
}
seated.ShowSeated(n);
}
void ForceFreeTable(Group g)
{
// 卓を空ける前に、座っている客の形を消す
if (spawner != null && g.table != null)
{
SeatedGuest seated = spawner.SeatedAt(g.table.index);
if (seated != null) seated.ShowSeated(0);
}
Vector3 at = g.table != null ? g.table.deliveryPoint : Vector3.zero;
ops.AddGiveUp("組", g.state == GState.Paying ? "会計" : "片付け",
g.table != null ? g.table.tableId : 0,
at, at, g.stuckS, "卓を空けた");
// 出していた仕事を板から外す
if (g.job != null) { ops.FinishJobSilently(g.job); g.job = null; }
ReleasePayer(g);
if (g.leftS < 0f) g.leftS = ops.NowS;
ops.ReleaseTable(g.table, g.leftS);
g.table = null;
g.state = GState.Done;
g.stuckS = 0f;
_groupsDone++;
}
///
/// 会計に立つ1人をレジへ送る。
/// ・レジの並ぶ点が空いていなければ、席に座ったまま待つ(レジに団子にならないように)
/// ・送れたときだけ、スタッフの会計の仕事を板に出す(誰も居ないレジで会計しないように)
/// 残りの人は会計が終わってから、全員で立って店を出る(OnLeft)。
///
void SendPayerIfPossible(Group g, float now)
{
if (g.payerSent) return;
// 前のとおり(誰もレジに立たず、スタッフだけで会計)に戻せるようにしておく。
// レジの前の人数を直す前・直したあとで比べるため
if (!spec.payAtRegisterByOne)
{
g.payerSent = true;
PullChair(g, SeatedGuest.ReasonPay);
Group old = g;
StaffJob j = ops.PostJob(StaffJobKind.Pay, g.id, g.table, null, 1);
j.onDone = delegate { OnPaid(old); };
g.job = j;
return;
}
Vector3 spot;
int index = ops.TakeRegisterQueueSpot(out spot);
if (index < 0) return; // 並ぶ点が埋まっている。席で待つ
g.payerSpot = index;
g.payerSent = true;
// 会計で立ち上がるので椅子を引く(B6)
PullChair(g, SeatedGuest.ReasonPay);
Vector3 from = g.table != null ? g.table.deliveryPoint : _targets.entrance;
g.payerBody = BorrowBody(from, false);
if (g.payerBody != null)
{
g.payerBody.SetTable(g.table != null ? g.table.tableId : 0);
g.payerBody.Begin(people, GuestAgent.Errand.Pay, spot, float.MaxValue,
false, from, null);
}
Group captured = g;
StaffJob job = ops.PostJob(StaffJobKind.Pay, g.id, g.table, null, 1);
job.onDone = delegate { OnPaid(captured); };
g.job = job;
}
/// 会計の人を席へ戻し、レジの並ぶ点を空ける
void ReleasePayer(Group g)
{
if (g.payerBody != null) { ReturnBody(g.payerBody); g.payerBody = null; }
if (g.payerSpot >= 0) { ops.ReleaseRegisterQueueSpot(g.payerSpot); g.payerSpot = -1; }
g.payerSent = false;
}
void OnPaid(Group g)
{
ReleasePayer(g);
g.job = null;
g.payS = ops.NowS;
g.leftS = ops.NowS;
g.state = GState.Left;
g.stuckS = 0f;
// 退店するので椅子を引く(B6)
PullChair(g, SeatedGuest.ReasonLeave);
// 組の全員が立って入口から出る
int tableId = g.table != null ? g.table.tableId : 0;
for (int i = 0; i < g.size; i++)
{
Vector3 from = g.table != null ? g.table.deliveryPoint : _targets.entrance;
GuestAgent a = BorrowBody(from, false);
if (a == null) break;
GuestAgent captured = a;
a.SetTable(tableId);
a.Begin(people, GuestAgent.Errand.Leave, _targets.entrance, 0f, false, Vector3.zero,
delegate { ReturnBody(captured); });
}
}
void OnCleared(Group g)
{
g.job = null;
ops.ReleaseTable(g.table, g.leftS);
g.state = GState.Done;
_groupsDone++;
}
void StartErrand(Group g, int kind)
{
if (g.table == null) return;
Vector3 home = g.table.deliveryPoint;
Vector3 target = kind == 1 ? _targets.toilet : _targets.drinkBar;
float dwell = kind == 1 ? spec.toiletSec : spec.drinkBarSec;
// ドリンクバーは、空いている機械の前へ行く。埋まっていたら端に並ぶ。
// それも埋まっていたら、立ち上がらずにあきらめる(B4)
int token = -1;
if (kind == 0 && ops.HasDrinkBarSpots)
{
Vector3 spot;
bool queued;
token = ops.TakeDrinkBarSpot(out spot, out queued);
if (token < 0)
{
ops.AddGiveUp("客", "ドリンクバー", g.table.tableId, target, home, 0f, "あきらめた");
return;
}
target = spot;
}
GuestAgent a = BorrowBody(home, false);
if (a == null) { ops.ReleaseDrinkBarSpot(token, 0f); return; }
// 立ち上がるので椅子を引く(B6)
PullChair(g, kind == 1 ? SeatedGuest.ReasonToilet : SeatedGuest.ReasonDrinkBar);
g.activeErrands++;
Group cg = g;
GuestAgent ca = a;
int ct = token;
float startS = ops.NowS;
a.SetTable(g.table.tableId);
a.Begin(people, kind == 1 ? GuestAgent.Errand.Toilet : GuestAgent.Errand.DrinkBar,
target, dwell, true, home,
delegate
{
cg.activeErrands--;
ops.ReleaseDrinkBarSpot(ct, ops.NowS - startS - dwell);
PullChair(cg, SeatedGuest.ReasonSitBack); // 戻って座り直す(B6)
ReturnBody(ca);
});
}
bool AllFirstDishesArrived(Group g)
{
if (g.cookAt.Count > 0) return false;
if (g.dishes.Count < 2) return false;
for (int i = 0; i < 2 && i < g.dishes.Count; i++)
if (g.dishes[i].arrivedS < 0f) return false;
return true;
}
bool AllDishesArrived(Group g)
{
for (int i = 0; i < g.dishes.Count; i++)
if (g.dishes[i].arrivedS < 0f) return false;
return true;
}
// ---------------- 歩く体の使い回し ----------------
GuestAgent BorrowBody(Vector3 at, bool waitingKind)
{
// 待合の客も同じ体を使う(GuestAgent が「待っている」用事を持っている)
GameObject prefab = guestPrefab;
if (prefab == null || bodyRoot == null) return null;
GuestAgent a = null;
for (int i = _pool.Count - 1; i >= 0; i--)
{
GuestAgent c = _pool[i];
_pool.RemoveAt(i);
if (c == null) continue;
a = c;
break;
}
if (a == null)
{
GameObject go = Instantiate(prefab, bodyRoot);
a = go.GetComponent();
if (a == null) { Destroy(go); return null; }
}
a.gameObject.SetActive(true);
a.ops = ops; // あきらめたことを残す先(B1)
// 人がロボットをよける仕掛け(prefab に無くても付ける)
if (spawner != null) spawner.AttachRobotYield(a.gameObject, _nextGroupId * 31 + _live.Count);
a.PlaceAt(at);
_live.Add(a);
return a;
}
void ReturnBody(GuestAgent a)
{
if (a == null) return;
_live.Remove(a);
a.Stop();
a.gameObject.SetActive(false);
_pool.Add(a);
}
void ReturnAllBodies()
{
for (int i = _live.Count - 1; i >= 0; i--) ReturnBody(_live[i]);
_live.Clear();
}
public void ClearAll()
{
ReturnAllBodies();
for (int i = 0; i < _pool.Count; i++)
if (_pool[i] != null) Destroy(_pool[i].gameObject);
_pool.Clear();
_groups.Clear();
_waiting.Clear();
}
}
}