Adding known word, npcs in locations, auto empty npcs and actions in

locations, etc.
This commit is contained in:
Sergiotarxz 2023-07-09 18:41:21 +02:00
parent c189bb1f08
commit cbecf68f9f
24 changed files with 389 additions and 56 deletions

View File

@ -1,6 +1,8 @@
import * as React from 'react'
import type { Action, ActionHash } from '@lastres/action'
import type { TalkNPCs, TalkNPC } from '@lastres/talk-npc'
import OutputPacketExecuteAction from '@lastres/output-packet/execute_action'
import PresentationItem from '@lastres/components/presentation-item'
@ -9,7 +11,7 @@ import Presentation from '@lastres/components/presentation'
export interface BottomPanelProps {
websocket: WebSocket | null
actionHash: ActionHash | null
talkNPCs: TalkNPCs | null
}
export interface Style {
@ -71,6 +73,45 @@ export default function BottomPanel (props: BottomPanelProps): JSX.Element {
}
</>
}
function printAvatar (npc: TalkNPC): JSX.Element {
if (npc.icon === undefined) {
return <></>
}
return <div className="avatar">
<img src={npc.icon}/><div className="shadow"/>
</div>
}
function printTalkNpcs (): JSX.Element {
const npcs = props.talkNPCs
if (npcs === null) {
return <></>
}
return (
<>
{
Object.keys(npcs).map((identifier) => {
const npc = npcs[identifier]
return <div key={npc.identifier} className="talk-npc">
<div className="detail">
<div className="name-container">
{
printAvatar(npc)
}
<div className="name">
<p>{npc.name}</p>
</div>
</div>
<div className="buttons">
<button>Hablar.</button>
<button>Decir palabra.</button>
</div>
</div>
</div>
})
}
</>
)
}
return (
<Presentation>
<PresentationItem>
@ -79,6 +120,9 @@ export default function BottomPanel (props: BottomPanelProps): JSX.Element {
}
</PresentationItem>
<PresentationItem>
{
printTalkNpcs()
}
</PresentationItem>
<PresentationItem>
</PresentationItem>

View File

@ -4,6 +4,7 @@ import type { PJ } from '@lastres/pj'
import type { Location } from '@lastres/location'
import type { LogLine } from '@lastres/log-line'
import type { ActionHash } from '@lastres/action'
import type { TalkNPCs } from '@lastres/talk-npc'
import UpperPanel from '@lastres/components/upper-panel'
import BottomPanel from '@lastres/components/bottom-panel'
@ -49,13 +50,14 @@ export default function Game (props: GameProps): JSX.Element {
const [movingTo, setMovingTo] = React.useState<Location | null>(null)
const [remainingFrames, setRemainingFrames] = React.useState<number | null>(null)
const [actionHash, setActionHash] = React.useState<ActionHash | null>(null)
const [talkNPCs, setTalkNPCs] = React.useState<TalkNPCs | null>(null)
const logPresentationRef = React.useRef<HTMLDivElement>(null)
const websocket = props.websocket
const setWebsocket = props.setWebsocket
window.setTimeout(() => {
setWebsocket((websocket): WebSocket | null => {
if (websocket === null) {
console.log('Opening websocket');
console.log('Opening websocket')
const locationProtocol = window.location.protocol
if (locationProtocol == null) {
return null
@ -74,7 +76,7 @@ export default function Game (props: GameProps): JSX.Element {
return
}
window.clearInterval(interval)
}, 1000)
}, 100000)
}
const inputPackets = new InputPackets(setTeamPJs,
setEnemyTeamPJs, setIsBattling,
@ -82,7 +84,7 @@ export default function Game (props: GameProps): JSX.Element {
logLines, setLogLines, setError,
setScrollLog, logPresentationRef,
setMovingTo, setRemainingFrames,
setActionHash)
setActionHash, setTalkNPCs)
webSocket.onmessage = (event) => {
const packet = JSON.parse(event.data)
inputPackets.handle(packet)
@ -111,7 +113,9 @@ export default function Game (props: GameProps): JSX.Element {
logPresentationRef={logPresentationRef}
movingTo={movingTo}
remainingFrames={remainingFrames}/>
<BottomPanel actionHash={actionHash} websocket={websocket}/>
<BottomPanel actionHash={actionHash}
websocket={websocket}
talkNPCs={talkNPCs}/>
</>
)
}

View File

@ -3,6 +3,7 @@ import type { Location } from '@lastres/location'
import type InputPacket from '@lastres/input-packet'
import type { LogLine } from '@lastres/log-line'
import type { ActionHash } from '@lastres/action'
import type { TalkNPCs } from '@lastres/talk-npc'
import InputPacketInfo from '@lastres/input-packet/info'
import InputPacketPong from '@lastres/input-packet/pong'
@ -21,6 +22,7 @@ type LogPresentationRef = React.RefObject<HTMLDivElement>
type SetMovingTo = (set: Location | null) => void
type SetRemainingFrames = (set: number | null) => void
type SetActionHash = (set: ActionHash | null) => void
type SetTalkNPCs = (set: TalkNPCs | null) => void
interface Packet {
command: string
@ -44,6 +46,7 @@ export default class InputPackets {
setMovingTo: SetMovingTo
setRemainingFrames: SetRemainingFrames
setActionHash: SetActionHash
setTalkNPCs: SetTalkNPCs
constructor (setTeamPJs: SetTeamPJs,
setEnemyTeamPJs: SetEnemyTeamPJs,
setIsBattling: SetIsBattling,
@ -56,7 +59,8 @@ export default class InputPackets {
logPresentationRef: LogPresentationRef,
setMovingTo: SetMovingTo,
setRemainingFrames: SetRemainingFrames,
setActionHash: SetActionHash) {
setActionHash: SetActionHash,
setTalkNPCs: SetTalkNPCs) {
this.setTeamPJs = setTeamPJs
this.setEnemyTeamPJs = setEnemyTeamPJs
this.setCurrentLocation = setCurrentLocation
@ -70,6 +74,7 @@ export default class InputPackets {
this.setRemainingFrames = setRemainingFrames
this.setIsBattling = setIsBattling
this.setActionHash = setActionHash
this.setTalkNPCs = setTalkNPCs
}
handle (packet: Packet): void {
@ -143,6 +148,9 @@ export default class InputPackets {
this.setMovingTo(null)
}
}
if (data.npcs !== undefined) {
this.setTalkNPCs(data.npcs)
}
if (data.remaining_frames !== undefined) {
this.setRemainingFrames(data.remaining_frames)
}

View File

@ -8,6 +8,8 @@ use utf8;
use Moo;
use List::AllUtils;
use JSON qw/to_json/;
sub identifier {
return 'execute_action';
}

View File

@ -79,6 +79,7 @@ sub handle ( $self, $ws, $session, $data ) {
$self->_enemy_team_pjs($session),
clear => $JSON::true,
$self->_available_actions($pj),
$self->_npcs($pj),
);
$info_packet_to_send->send($ws);
my $redis = LasTres::Redis->new;
@ -104,6 +105,15 @@ sub _enemy_team_pjs ( $self, $session ) {
);
}
sub _npcs ( $self, $pj ) {
my $npcs_hash = $pj->talk_npcs;
for my $identifier ( keys %$npcs_hash ) {
my $npc = $npcs_hash->{$identifier};
$npcs_hash->{$identifier} = $npc->hash;
}
return ( npcs => $npcs_hash );
}
sub _location_data ( $self, $pj ) {
my $connected_places = $self->_get_connected_places($pj);
my $team = $pj->team->get_from_storage;
@ -163,11 +173,12 @@ sub _on_redis_event ( $self, $ws, $session, $message, $topic, $topics ) {
}
if ( $data->{command} eq 'update-actions' ) {
LasTres::Controller::Websocket::OutputPacket::Info->new(
$self->_available_actions($pj) )->send($ws);
$self->_available_actions($pj),
$self->_npcs($pj) )->send($ws);
}
}
sub _available_actions ($self, $pj) {
sub _available_actions ( $self, $pj ) {
return ( available_actions =>
{ map { $_->identifier => $_->hash($pj) } $pj->actions->@* }, );
}

View File

@ -27,6 +27,7 @@ has is_battling => ( is => 'rw' );
has remaining_frames => ( is => 'rw' );
has available_actions => ( is => 'rw' );
has npcs => ( is => 'rw' );
sub identifier {
return 'info';
@ -42,6 +43,7 @@ sub data ($self) {
my $remaining_frames = $self->remaining_frames;
my $is_battling = $self->is_battling;
my $available_actions = $self->available_actions;
my $npcs = $self->npcs;
if ( defined $is_battling ) {
$is_battling = $is_battling ? $JSON::true : $JSON::false;
@ -82,7 +84,11 @@ sub data ($self) {
? ( available_actions => $available_actions )
: ()
),
(
(defined $npcs)
? ( npcs => $npcs)
: ()
)
};
}

View File

@ -8,4 +8,7 @@ use warnings;
sub INTRO_MESSAGE_SENT_FLAG {
return 'INTRO_MESSAGE_FLAG_SENT';
}
sub TALKED_WITH_OLD_MAN_AND_LEARNED_TO_SAY_DEVOTA {
return 'TALKED_WITH_OLD_MAN_AND_LEARNED_TO_SAY_DEVOTA';
}
1;

View File

@ -13,16 +13,31 @@ use Moo::Role;
use JSON qw/to_json from_json/;
use utf8;
requires qw/identifier name description parent actions npcs/;
requires qw/identifier name description parent/;
## Implement action($self, $pj);
## Implement npcs($self, $pj);
## Implement description($self, $pj = undef);
# Will be printed to the user.
## Implement name($self, $pj = undef);
## Implement identifier($self, $pj = undef);
# Must be unique across locations of this area.
my $planets = LasTres::Planets->new;
## OVERRIDE
# The available actions to do in
# this location, must return hashref,
# can be an empty one though.
sub actions($self, $pj) {
return [];
}
## OVERRIDE
# The available persons to talk with
# Must return a hashref, can be a empty
# one though.
sub npcs($self, $pj) {
return [];
}
## OVERRIDE
# Whenever a player can visit this place.
# The player to compute will always be the leader.

View File

@ -90,12 +90,19 @@ sub _start_battle ( $self, $pj ) {
}
]);
my $max_level_rabbit = 3;
if ($pj->level > 7) {
$max_level_rabbit = 4;
}
if ($pj->level > 9) {
$max_level_rabbit = 8;
}
my $battle = LasTres::Battle->start_battle_machine($team,
[
LasTres::EnemyArea->new(
race => 'conejo',
nick => 'Conejo territorial.',
level => LasTres::Util::rand_range_int(1,3),
level => LasTres::Util::rand_range_int(1,$max_level_rabbit),
)
]
);

View File

@ -27,14 +27,6 @@ sub parent {
return LasTres::Planet::Bahdder::BosqueDelHeroe::BosqueDelHeroeI->instance;
}
sub actions {
return [];
}
sub npcs {
return [];
}
sub connected_places {
return [
LasTres::Planet::Bahdder::BosqueDelHeroe::TribuDeLaLima::Entrada->instance,

View File

@ -30,14 +30,6 @@ sub parent {
return LasTres::Planet::Bahdder::BosqueDelHeroe::BosqueDelHeroeI->instance;
}
sub actions {
return [];
}
sub npcs {
return [];
}
sub connected_places {
return [
LasTres::Planet::Bahdder::BosqueDelHeroe::TribuDeLaLima::Entrada->instance,

View File

@ -34,11 +34,11 @@ sub parent {
return LasTres::Planet::Bahdder::BosqueDelHeroe::TribuDeLaLima->instance;
}
sub actions {
sub actions($self, $pj) {
return [ LasTres::PJAction::GolpearArbolCentralTribuDeLaLima->new ];
}
sub npcs {
sub npcs($self, $pj) {
return [ LasTres::TalkingNPC::AncianoTribuLima->new ];
}

View File

@ -42,14 +42,6 @@ sub _build_parent {
return LasTres::Planet::Bahdder::BosqueDelHeroe::TribuDeLaLima->instance;
}
sub actions {
return [];
}
sub npcs {
return [];
}
sub connected_places {
return [
LasTres::Planet::Bahdder::BosqueDelHeroe::BosqueDelHeroeI::TribuDeLaLima->instance,

View File

@ -5,7 +5,7 @@ use v5.36.0;
use strict;
use warnings;
our $VERSION = 12;
our $VERSION = 13;
use feature 'signatures';

View File

@ -6,9 +6,10 @@ use strict;
use warnings;
use feature 'signatures';
use parent 'DBIx::Class::Core';
use utf8;
use parent 'DBIx::Class::Core';
use UUID::URandom qw/create_uuid_string/;
use List::AllUtils;
use Data::Dumper;
@ -122,6 +123,8 @@ __PACKAGE__->has_many( 'logs', 'LasTres::Schema::Result::PJLog', 'owner' );
__PACKAGE__->has_many( 'known_places',
'LasTres::Schema::Result::PJKnownPlaces', 'owner' );
__PACKAGE__->has_many( 'flags', 'LasTres::Schema::Result::PJFlag', 'owner' );
__PACKAGE__->has_many( 'known_words', 'LasTres::Schema::Result::PJKnownWord',
'owner' );
__PACKAGE__->belongs_to( 'born_stats', 'LasTres::Schema::Result::Stats' );
__PACKAGE__->belongs_to( 'training_stats', 'LasTres::Schema::Result::Stats' );
__PACKAGE__->belongs_to( 'inventory', 'LasTres::Schema::Result::Inventory' );
@ -131,6 +134,53 @@ __PACKAGE__->belongs_to( 'equipment', 'LasTres::Schema::Result::Equipment' );
__PACKAGE__->belongs_to( 'team', 'LasTres::Schema::Result::Team' );
__PACKAGE__->belongs_to( 'owner', 'LasTres::Schema::Result::Player' );
sub knows_word ( $self, $word ) {
$self = $self->get_from_storage;
if ( !$word->does('LasTres::Word') ) {
die 'The received word does not implement LasTres::Word.';
}
my @words =
$self->known_words->search( { identifier => $word->identifier } );
if ( !scalar @words ) {
return 0;
}
return 1;
}
sub teach_word ( $self, $word ) {
require LasTres::Schema;
my $schema = LasTres::Schema->Schema;
my $result_set_words = $schema->resultset('PJKnownWord');
$self = $self->get_from_storage;
if ( !$word->does('LasTres::Word') ) {
die 'The received word does not implement LasTres::Word.';
}
if ($self->knows_word($word)) {
return;
}
my $known_word = $result_set_words->new(
{ identifier => $word->identifier, owner => $self->uuid } );
$known_word->insert;
my $team = $self->team;
$team->append_log_line([
{
color => 'green',
text => $self->nick,
},
{
text => ' aprendió la palabra '
},
{
color => 'purple',
text => $word->name,
},
{
text => '.'
}
]);
}
sub knows_location ( $self, $location ) {
require LasTres::Schema;
my $schema = LasTres::Schema->Schema;
@ -470,7 +520,7 @@ sub update_location ($self) {
to_json( { command => 'update-location' } ) );
}
sub update_actions($self) {
sub update_actions ($self) {
require LasTres::Redis;
my $redis = LasTres::Redis->new;
$redis->publish( $redis->pj_subscription($self),
@ -503,7 +553,27 @@ sub actions ($self) {
return \@actions;
}
sub talking_npcs($self) {
sub talk_npcs ($self) {
my @npcs;
$self = $self->get_from_storage;
my $team = $self->team;
my $location = $team->location;
if ( defined $team->battle ) {
return $self->_npc_list_to_hash( \@npcs );
}
if ( $team->is_moving ) {
return $self->_npc_list_to_hash( \@npcs );
}
my $location_npcs = $location->npcs($self);
@npcs = ( @npcs, @$location_npcs );
return $self->_npc_list_to_hash( \@npcs );
}
sub _npc_list_to_hash ( $self, $npcs ) {
return { map { ( $_->identifier => $_ ) } @$npcs };
}
sub talking_npcs ($self) {
return [];
}

View File

@ -0,0 +1,36 @@
package LasTres::Schema::Result::PJKnownWord;
use v5.36.0;
use strict;
use warnings;
use feature 'signatures';
use parent 'DBIx::Class::Core';
use Data::Dumper;
use Moo;
__PACKAGE__->table('player_pjs_known_words');
__PACKAGE__->add_columns(
identifier => {
data_type => 'text',
is_nullable => 0,
},
owner => {
data_type => 'uuid',
is_nullable => 0,
is_foreign_key => 1,
},
);
__PACKAGE__->set_primary_key( 'owner', 'identifier' );
__PACKAGE__->belongs_to( 'owner', 'LasTres::Schema::Result::PJ' );
sub sqlt_deploy_hook ( $self, $sqlt_table ) {
$sqlt_table->add_index( name => 'index_known_word', fields => [qw/owner identifier/] );
}
1;

View File

@ -24,21 +24,34 @@ requires qw/identifier name/;
## IMPLEMENTORS MUST EXTEND
# sub talk($self,$pj,$word);
## DO NOT EXTEND NOT SUPPORTED.
sub hash ($self) {
return {
identifier => $self->identifier,
name => $self->name,
(
( defined $self->icon )
? ( icon => $self->icon )
: ()
),
};
}
## OVERRIDE
sub icon {
return undef;
return;
}
## OVERRIDE
sub color ( $self, $pj ) {
return 'blue',;
return 'blue';
}
## OVERRIDE
# Refer to show_wordlessly_talk_started for
# detail about when it is convenient to
# override.
sub show_told_word($self, $pj, $word) {
sub show_told_word ( $self, $pj, $word ) {
my $team = $pj->team;
$team->append_log_line(
[
@ -85,7 +98,7 @@ sub show_told_word($self, $pj, $word) {
# },
# {
# text => ' ladra a ',
#
#
# },
# {
# text => $pj,
@ -97,7 +110,7 @@ sub show_told_word($self, $pj, $word) {
# ]
# );
# }
sub show_wordlessly_talk_started($self, $pj) {
sub show_wordlessly_talk_started ( $self, $pj ) {
my $team = $pj->team;
$team->append_log_line(
[
@ -133,14 +146,14 @@ sub talk ( $self, $pj, $word ) {
$pj = $pj->get_from_storage;
my $team = $pj->team;
if ( defined $word ) {
$self->show_told_word($pj, $word);
$self->show_told_word( $pj, $word );
return;
}
}
## OVERRIDE
sub verb($self, $pj) {
sub verb ( $self, $pj ) {
return 'dice';
}
@ -154,7 +167,8 @@ sub send_response_dialog ( $self, $pj, $array_text ) {
$team->append_log_line(
[
{ text => $self->name($pj), color => $self->color($pj) },
{ text => " @{[$self->verb($pj)]}.- " }, @$array_text
{ text => " @{[$self->verb($pj)]}.- " },
@$array_text
]
);
}
@ -162,8 +176,9 @@ sub send_response_dialog ( $self, $pj, $array_text ) {
## DO NOT EXTEND NOT SUPPORTED.
{
my %hash;
sub instance($class) {
if (!exists $hash{$class}) {
sub instance ($class) {
if ( !exists $hash{$class} ) {
$hash{$class} = $class->new;
}
return $hash{$class};

View File

@ -9,6 +9,8 @@ use feature 'signatures';
use Moo;
use LasTres::Flags;
with 'LasTres::TalkingNPC';
sub talk ( $self, $pj, $word ) {
@ -22,11 +24,23 @@ sub identifier {
return 'anciano_tribu_de_la_lima';
}
sub icon {
return '/img/anciano.png';
}
sub name {
return 'Anciano';
}
sub wordlessly_talk ( $self, $pj ) {
if ($pj->get_flag(LasTres::Flags::TALKED_WITH_OLD_MAN_AND_LEARNED_TO_SAY_DEVOTA)) {
$self->send_response_dialog([
{
text => '¿A que esperas, ve a hablar con la Devota?'
}
]);
return;
}
$self->send_response_dialog(
[
{
@ -43,5 +57,6 @@ sub wordlessly_talk ( $self, $pj ) {
}
]
);
$pj->set_flag(LasTres::Flags::TALKED_WITH_OLD_MAN_AND_LEARNED_TO_SAY_DEVOTA);
}
1;

View File

@ -17,4 +17,16 @@ requires qw/name identifier/;
# Identifier must be unique across words, failure
# to do so can result in a error or undefined
# behavior.
## DO NOT EXTEND NOT SUPPORTED.
{
my %hash;
sub instance ($class) {
if ( !exists $hash{$class} ) {
$hash{$class} = $class->new;
}
return $hash{$class};
}
}
1;

View File

@ -0,0 +1,16 @@
package LasTres::Word::Devota;
use v5.36.0;
use strict;
use warnings;
use utf8;
use feature 'signatures';
sub name {
return 'Devota';
}
sub identifier {
return 'devota';
}

View File

@ -11,6 +11,48 @@ body {
padding: 0px;
height: 100vh;
background: ghostwhite; }
body div.talk-npc div.detail {
display: flex;
flex-direction: column;
border-radius: 3px;
min-height: 100px;
border: solid 1px black;
align-items: center;
align-content: center;
justify-content: center; }
body div.talk-npc div.name-container {
display: flex;
padding: 10px; }
body div.talk-npc div.name-container div.avatar {
width: 100%;
aspect-ratio: 1/1;
border-radius: 50%;
background: lightgray;
margin-right: 2%;
display: flex;
align-items: center;
justify-content: center;
position: relative; }
body div.talk-npc div.name-container div.avatar img {
animation-name: move-avatar;
animation-duration: 0.5s;
animation-iteration-count: infinite;
width: 80%;
aspect-ratio: 1/1;
z-index: 1; }
body div.talk-npc div.name-container div.avatar div.shadow {
top: 78%;
position: absolute;
width: 50%;
aspect-ratio: 7/2;
background: darkgray;
border-radius: 50%; }
body div.talk-npc div.name-container div.name {
width: 100%;
display: flex;
align-items: center; }
body div.talk-npc div.buttons {
padding: 10px; }
body label.bar-container {
width: 90%; }
body label.bar-container div.bar {

View File

@ -14,6 +14,57 @@ body {
padding: 0px;
height: 100vh;
background: ghostwhite;
div.talk-npc {
div.detail {
display: flex;
flex-direction: column;
border-radius: 3px;
min-height: 100px;
border: solid 1px black;
align-items: center;
align-content: center;
justify-content: center;
}
div.name-container {
display: flex;
padding: 10px;
div.avatar {
width: 100%;
aspect-ratio: 1/1;
border-radius: 50%;
background: lightgray;
margin-right: 2%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
img {
animation-name: move-avatar;
animation-duration: 0.5s;
animation-iteration-count: infinite;
width: 80%;
aspect-ratio: 1/1;
z-index: 1;
}
div.shadow {
top: 78%;
position: absolute;
width: 50%;
aspect-ratio: 7/2;
background: darkgray;
border-radius: 50%;
}
}
div.name {
width: 100%;
display: flex;
align-items: center;
}
}
div.buttons {
padding: 10px;
}
}
label.bar-container {
width: 90%;
div.bar {

BIN
public/img/anciano.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

File diff suppressed because one or more lines are too long