Files
City10/app/Models/PersonTalk.php
T
marcocosta10000 59963de995 feat(city): add bump-to-talk conversations with persistent context
Implements proximity-based street conversations between player characters and peers (human or agentic NPC). New PersonTalk model, controller, and migrations store daily message threads that persist across sessions and provide context for future interactions. Agent replies include backoff throttling and fallback persona handling to avoid dead-end chats.

Frontend changes refactor the chat panel to support both character and agent peers, opening automatically when the player moves within range, polling for replies, rendering day separators, and displaying rate-limit warnings. Styling updated to a transparent blurred panel. Translations extended with new hint text and rate-limit messages across en, es, and pt_BR.
2026-10-31 17:59:22 -03:00

120 lines
3.4 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* One entry of a daily street conversation.
*
* Exactly one "from" side (character or agent) and one "to" side are set
* per row. The full ordered set of rows between a pair is the persistent
* context used to resume the conversation on later days.
*/
#[Fillable([
'from_character_id', 'from_agent_id',
'to_character_id', 'to_agent_id',
'body', 'talked_on', 'is_fallback',
])]
class PersonTalk extends Model
{
/**
* Peer kind addressing an agentic NPC.
*
* @var string
*/
public const KIND_AGENT = 'agent';
/**
* Peer kind addressing another user's character.
*
* @var string
*/
public const KIND_CHARACTER = 'character';
/**
* Attributes that should be cast to native types.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'talked_on' => 'date',
'is_fallback' => 'boolean',
];
}
/**
* Character that sent the message.
*
* @return BelongsTo<Character, $this>
*/
public function fromCharacter(): BelongsTo
{
return $this->belongsTo(Character::class, 'from_character_id');
}
/**
* Agent that sent the message.
*
* @return BelongsTo<AiAgent, $this>
*/
public function fromAgent(): BelongsTo
{
return $this->belongsTo(AiAgent::class, 'from_agent_id');
}
/**
* Character that received the message.
*
* @return BelongsTo<Character, $this>
*/
public function toCharacter(): BelongsTo
{
return $this->belongsTo(Character::class, 'to_character_id');
}
/**
* Agent that received the message.
*
* @return BelongsTo<AiAgent, $this>
*/
public function toAgent(): BelongsTo
{
return $this->belongsTo(AiAgent::class, 'to_agent_id');
}
/**
* Ordered conversation rows between a character and a peer.
*
* @param int $characterId the local character
* @param string $peerKind PersonTalk::KIND_AGENT or PersonTalk::KIND_CHARACTER
* @param int $peerId agent id or character id of the peer
* @return Builder<self>
*/
public static function conversation(int $characterId, string $peerKind, int $peerId): Builder
{
return self::query()->where(function (Builder $outer) use ($characterId, $peerKind, $peerId): void {
if ($peerKind === self::KIND_AGENT) {
$outer->where(function (Builder $q) use ($characterId, $peerId): void {
$q->where('from_character_id', $characterId)->where('to_agent_id', $peerId);
})->orWhere(function (Builder $q) use ($characterId, $peerId): void {
$q->where('from_agent_id', $peerId)->where('to_character_id', $characterId);
});
return;
}
$outer->where(function (Builder $q) use ($characterId, $peerId): void {
$q->where('from_character_id', $characterId)->where('to_character_id', $peerId);
})->orWhere(function (Builder $q) use ($characterId, $peerId): void {
$q->where('from_character_id', $peerId)->where('to_character_id', $characterId);
});
});
}
}