Adding initial actions and some documentation.

This commit is contained in:
Sergiotarxz 2023-06-30 19:59:18 +02:00
parent f8020241cb
commit 01a098d926
18 changed files with 478 additions and 49 deletions

289
README.md
View File

@ -1,2 +1,291 @@
# LasTres
## Installation instructions. (Prod)
This is an example, you can configure the server
as you please, use this guide as a reference.
This guide uses Debian stable 12, this server
requires at least a system compatible with
Redis such as Linux, Windows is not compatible
with Redis, but you can use it virtualized or
with WSL2.
We recommend Linux or at least something Unix-like
to deploy both in server and in development this
project.
That said features that improve OS compatibility
will always be appreciated.
### Postgresql notes.
We are going to use the peer authentication that
comes preconfigured in Debian, if you are using
other OS you should care of `pg_hba.conf` to
be configured as peer for empty address.
This is how it looks on the Debian I used.
You can also configure it with password if the database
server is not the same host that the webserver, it
is supported by the software.
```
local all postgres peer
# TYPE DATABASE USER ADDRESS METHOD
# "local" is for Unix domain socket connections only
local all all peer
# IPv4 local connections:
host all all 127.0.0.1/32 scram-sha-256
# IPv6 local connections:
host all all ::1/128 scram-sha-256
# Allow replication connections from localhost, by a user with the
# replication privilege.
local replication all peer
host replication all 127.0.0.1/32 scram-sha-256
host replication all ::1/128 scram-sha-256
```
### Creating user
```shell
sudo useradd -m las_tres -d /var/lib/las_tres
```
### Installing dependencies.
```shell
sudo apt update && \
sudo apt install git postgresql redis nginx pwgen liblocal-lib-perl libpq-dev
```
### Starting an enable dependency services
```shell
sudo systemctl daemon-reload && \
sudo systemctl enable redis-server && \
sudo systemctl start redis-server && \
sudo systemctl enable postgresql && \
sudo systemctl start postgresql && \
sudo systemctl enable nginx && \
sudo systemctl start nginx
```
### Configuring the database
```shell
( cat << 'EOF'
create user "las_tres";
create database "las_tres";
grant all privileges on database "las_tres" to "las_tres";
alter database "las_tres" owner to "las_tres";
EOF
) | sudo -u postgres psql
```
### Cloning project.
```shell
sudo -u las_tres bash -c "$(cat <<'EOF'
cd && \
git clone https://git.owlcode.tech/sergiotarxz/LasTres
EOF
)"
```
### If you are going to change the JS. (Only once.)
```shell
sudo apt update && sudo apt install nodejs npm && \
sudo -u las_tres bash -c "$(cat <<'EOF'
cd ~/LasTres && \
npm install
EOF
)"
```
### Deploying your js changes
```shell
sudo -u las_tres bash -c "$(cat <<'EOF'
cd ~/LasTres && \
npx webpack
EOF
)"
```
### If you are going to change the css (Only once.)
```shell
sudo apt update && sudo apt install sassc
```
### Deploying the css changes
```shell
sudo -u las_tres bash -c "$(cat <<'EOF'
cd ~/LasTres && \
bash build_styles.sh
EOF
)"
```
### Configuring NGINX.
#### (TLS is out of the scope of this tutorial, but you must do it if you want to deploy this server to the public world.)
```shell
( cat << 'EOF'
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream backend_las_tres {
server 127.0.0.1:3000 fail_timeout=0;
}
server {
listen 80;
listen [::]:80;
server_name las_tres.example.com;
location /.well-known/acme-challenge/ { allow all; }
keepalive_timeout 70;
sendfile on;
client_max_body_size 80m;
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml image/x-icon;
location / {
add_header Strict-Transport-Security "max-age=31536000" always;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Proxy "";
proxy_pass_header Server;
proxy_pass http://backend_las_tres;
proxy_buffering on;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
tcp_nodelay on;
}
}
EOF
) | sudo tee /etc/nginx/sites-enabled/las_tres.conf && \
sudo systemctl reload nginx
```
### Configuring the app
For non standard configurations you should peek the
config in `las_tres.example.yml` copy to `las_tres.yml`
and modify the copy until it suits your needs, patches
and issues are welcome if you need more options than
example allows you to do.
```shell
sudo -u las_tres bash -c "$( cat << 'EOF'
cd ~/LasTres && \
cat << EOF1 > las_tres.yml
secrets:
- $(pwgen -s 512 1)
database:
dbname: las_tres
hypnotoad:
listen:
# Here we have changed it to only listen localhost.
- http://127.0.0.1:3000
EOF1
EOF
)"
```
### Installing Perl deps
```shell
sudo -u las_tres bash -c "$( cat << EOF
source ~/.profile && \
if ! grep PERL5LIB ~/.profile; then
perl -Mlocal::lib 2>/dev/null >> ~/.profile ;
fi && \
rm -fr ~/.cpan && \
( echo y && echo reload cpan && echo o conf commit ) | perl -MCPAN -Mlocal::lib -e shell && \
cd ~/LasTres && \
perl Build.PL && \
./Build installdeps
EOF
)"
```
### Creating Systemd service
```shell
( cat << 'EOF'
[Unit]
Description=LasTres the web text game
After=network.target postgresql.service redis.service
[Service]
User=las_tres
Group=las_tres
WorkingDirectory=/var/lib/las_tres/LasTres/
ExecStart=/bin/bash -c "source /var/lib/las_tres/.profile && hypnotoad -f script/las_tres"
User=las_tres
Group=las_tres
Restart=on-failure
[Install]
WantedBy=multi-user.target
Alias=las_tres.service
EOF
) | sudo tee /etc/systemd/system/las_tres.service
```
### Deploying the database
#### If you are installing for the first time.
```shell
sudo -u las_tres bash -c "$( cat << EOF
cd ~/LasTres && \
source ~/.profile && \
perl script/install.pl
EOF
)"
```
#### If you are migrating from a older version
```shell
sudo -u las_tres bash -c "$( cat << EOF
cd ~/LasTres && \
source ~/.profile && \
perl script/upgrade.pl
EOF
)"
```
### Enabling and starting service
```shell
sudo systemctl daemon-reload && \
sudo systemctl restart las_tres && \
sudo systemctl enable las_tres
```

8
js-src/action.ts Normal file
View File

@ -0,0 +1,8 @@
export interface Action {
name: string
identifier: string
icon: string | null
is_disabled: boolean
disabled_reason: string | null
};
export type ActionHash = Record<string, Action>

View File

@ -1,18 +1,72 @@
import * as React from 'react'
import type { Action, ActionHash } from '@lastres/action'
import PresentationItem from '@lastres/components/presentation-item'
import Presentation from '@lastres/components/presentation'
export interface BottomPanelProps {
websocket: WebSocket | null
actionHash: ActionHash | null
}
export default function BottomPanel (): JSX.Element {
export interface Style {
background?: string
}
export default function BottomPanel (props: BottomPanelProps): JSX.Element {
const actionHash = props.actionHash
function printListActions(): JSX.Element {
if (actionHash === null) {
return <></>
}
const listOfActionKeys = Object.keys(actionHash).sort((a, b) => {
const isDisabledComparisionValue: number = +actionHash[a].is_disabled - +actionHash[b].is_disabled
if (isDisabledComparisionValue !== 0) {
return isDisabledComparisionValue
}
if (actionHash[a].name < actionHash[b].name) {
return -1
}
if (actionHash[a].name > actionHash[b].name) {
return 1
}
return 0
})
function printDisabledReason (action: Action): JSX.Element {
if (!action.is_disabled || action.disabled_reason === null) {
return <></>
}
return (
<p className="disabled-reason" style={{ color: 'red' }}>{action.disabled_reason}</p>
)
}
return <>
<p>Acciones disponibles.</p>
{
listOfActionKeys.map((key) => {
const style: Style = {}
const action = actionHash[key]
if (action.is_disabled) {
style.background = 'lightgray'
}
return <div className="action" style={style} key={action.identifier}>
<p>{action.name}</p>
{
printDisabledReason(action)
}
</div>
})
}
</>
}
return (
<Presentation>
<PresentationItem>
</PresentationItem>
<PresentationItem>
{
printListActions()
}
</PresentationItem>
<PresentationItem>
</PresentationItem>

View File

@ -3,6 +3,7 @@ import * as React from 'react'
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 UpperPanel from '@lastres/components/upper-panel'
import BottomPanel from '@lastres/components/bottom-panel'
@ -47,6 +48,7 @@ export default function Game (props: GameProps): JSX.Element {
const [scrollLog, setScrollLog] = React.useState<number | null>(null)
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 logPresentationRef = React.useRef<HTMLDivElement>(null)
const websocket = props.websocket
const setWebsocket = props.setWebsocket
@ -79,7 +81,8 @@ export default function Game (props: GameProps): JSX.Element {
setCurrentLocation, setConnectedLocations,
logLines, setLogLines, setError,
setScrollLog, logPresentationRef,
setMovingTo, setRemainingFrames)
setMovingTo, setRemainingFrames,
setActionHash)
webSocket.onmessage = (event) => {
const packet = JSON.parse(event.data)
inputPackets.handle(packet)
@ -108,7 +111,7 @@ export default function Game (props: GameProps): JSX.Element {
logPresentationRef={logPresentationRef}
movingTo={movingTo}
remainingFrames={remainingFrames}/>
<BottomPanel/>
<BottomPanel actionHash={actionHash} websocket={websocket}/>
</>
)
}

View File

@ -2,6 +2,7 @@ import type { PJ } from '@lastres/pj'
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 InputPacketInfo from '@lastres/input-packet/info'
import InputPacketPong from '@lastres/input-packet/pong'
@ -19,6 +20,7 @@ type SetScrollLog = (set: number | null | SetScrollLogCallback) => void
type LogPresentationRef = React.RefObject<HTMLDivElement>
type SetMovingTo = (set: Location | null) => void
type SetRemainingFrames = (set: number | null) => void
type SetActionHash = (set: ActionHash | null) => void
interface Packet {
command: string
@ -41,6 +43,7 @@ export default class InputPackets {
logPresentationRef: LogPresentationRef
setMovingTo: SetMovingTo
setRemainingFrames: SetRemainingFrames
setActionHash: SetActionHash
constructor (setTeamPJs: SetTeamPJs,
setEnemyTeamPJs: SetEnemyTeamPJs,
setIsBattling: SetIsBattling,
@ -52,7 +55,8 @@ export default class InputPackets {
setScrollLog: SetScrollLog,
logPresentationRef: LogPresentationRef,
setMovingTo: SetMovingTo,
setRemainingFrames: SetRemainingFrames) {
setRemainingFrames: SetRemainingFrames,
setActionHash: SetActionHash) {
this.setTeamPJs = setTeamPJs
this.setEnemyTeamPJs = setEnemyTeamPJs
this.setCurrentLocation = setCurrentLocation
@ -65,6 +69,7 @@ export default class InputPackets {
this.setMovingTo = setMovingTo
this.setRemainingFrames = setRemainingFrames
this.setIsBattling = setIsBattling
this.setActionHash = setActionHash
}
handle (packet: Packet): void {
@ -164,6 +169,9 @@ export default class InputPackets {
applyScroll()
}, 10)
}
if (data.available_actions !== undefined) {
this.setActionHash(data.available_actions)
}
})
this.cachedArray = [infoPacket, pongPacket]
}

View File

@ -1,4 +1,3 @@
---
secrets:
- change_me_for_a_proper_secret_generated_with_pwgen
database:

View File

@ -74,10 +74,11 @@ sub handle ( $self, $ws, $session, $data ) {
LasTres::Controller::Websocket::OutputPacket::Info->new(
set_log => [ $pj->last_50_log ],
$self->_location_data($pj),
team_pjs => $team->combat_members_serializable($pj),
team_pjs => $team->combat_members_serializable($pj),
is_battling => defined $team->battle,
$self->_enemy_team_pjs($session),
clear => $JSON::true,
$self->_available_actions($pj),
);
$info_packet_to_send->send($ws);
my $redis = LasTres::Redis->new;
@ -155,11 +156,20 @@ sub _on_redis_event ( $self, $ws, $session, $message, $topic, $topics ) {
if ( $data->{command} eq 'update-team-sprites' ) {
my $team = $pj->team;
LasTres::Controller::Websocket::OutputPacket::Info->new(
team_pjs => $team->combat_members_serializable($pj),
team_pjs => $team->combat_members_serializable($pj),
is_battling => defined $team->battle,
$self->_enemy_team_pjs($session)
)->send($ws);
}
if ( $data->{command} eq 'update-actions' ) {
LasTres::Controller::Websocket::OutputPacket::Info->new(
$self->_available_actions($pj) )->send($ws);
}
}
sub _available_actions ($self, $pj) {
return ( available_actions =>
{ map { $_->identifier => $_->hash($pj) } $pj->actions->@* }, );
}
sub _get_connected_places ( $self, $pj ) {

View File

@ -25,21 +25,23 @@ has set_log => ( is => 'rw', );
has append_log => ( is => 'rw' );
has is_battling => ( is => 'rw' );
has remaining_frames => ( is => 'rw' );
has remaining_frames => ( is => 'rw' );
has available_actions => ( is => 'rw' );
sub identifier {
return 'info';
}
sub data ($self) {
my $clear = $self->clear;
my $team_pjs = $self->team_pjs;
my $enemy_team_pjs = $self->enemy_team_pjs;
my $location_data = $self->location_data;
my $set_log = $self->set_log;
my $append_log = $self->append_log;
my $remaining_frames = $self->remaining_frames;
my $is_battling = $self->is_battling;
my $clear = $self->clear;
my $team_pjs = $self->team_pjs;
my $enemy_team_pjs = $self->enemy_team_pjs;
my $location_data = $self->location_data;
my $set_log = $self->set_log;
my $append_log = $self->append_log;
my $remaining_frames = $self->remaining_frames;
my $is_battling = $self->is_battling;
my $available_actions = $self->available_actions;
if ( defined $is_battling ) {
$is_battling = $is_battling ? $JSON::true : $JSON::false;
@ -74,7 +76,13 @@ sub data ($self) {
( defined $remaining_frames )
? ( remaining_frames => $remaining_frames )
: ()
)
),
(
( defined $available_actions )
? ( available_actions => $available_actions )
: ()
),
};
}

View File

@ -102,8 +102,8 @@ sub on_pj_arrival ( $self, $pj ) {
$pj->set_known_location($self);
}
$self->show_intro($pj);
$redis->publish( $redis->pj_subscription($pj),
to_json( { command => 'update-location' } ) );
$pj->update_location;
$pj->update_actions;
}
## DO NOT EXTEND NOT SUPPORTED.
@ -168,8 +168,8 @@ sub on_pj_moving ( $self, $pj ) {
{ color => 'green', text => $self->name($pj) },
]
);
$redis->publish( $redis->pj_subscription($pj),
to_json( { command => 'update-location' } ) );
$pj->update_location;
$pj->update_actions;
}
## DO NOT EXTEND NOT SUPPORTED.

View File

@ -6,19 +6,31 @@ use warnings;
use feature 'signatures';
use JSON qw/to_json from_json/;
use Moo::Role;
requires( 'identifier', 'callback', 'name' );
## IMPLEMENTORS MUST IMPLEMENT
#
# sub callback($self, $pj);
# What to do when this action is invoked by the PJ.
#
# sub identifier;
# The unique identifier of this action in the game.
#
# sub name($self, $pj);
# The name of the action, possibly variable for diferent PJs.
## OVERRIDE
# This should be a square icon, ideally of 400x400px
# If not set the frontend should attempt to show the
# action with as much dignity as possible.
#
# Should return nothing or a string that is a absolute url
# Should return undef or a string that is a absolute url
# to the resource.
sub icon ( $self, $pj ) {
return;
return undef;
}
## OVERRIDE
@ -38,20 +50,20 @@ sub is_disabled ( $self, $pj ) {
# the pj cannot use this action but they still see it as
# something possible to be done.
#
# Should return nothing if the pj can do the action and a string
# Should return undef if the pj can do the action and a string
# with the reason otherwise.
sub disabled_reason ( $self, $pj ) {
return;
return undef;
}
## DO NOT EXTEND NOT SUPPORTED.
sub hash ( $self, $pj ) {
return {
identifier => $self->identifier,
icon => $self->icon,
name => $self->name,
is_disabled => $self->is_disabled,
disabled_reason => $self->disabled_reason,
icon => $self->icon($pj),
name => $self->name($pj),
is_disabled => $self->is_disabled($pj) ? $JSON::true : $JSON::false,
disabled_reason => $self->disabled_reason($pj),
};
}
1;

View File

@ -4,6 +4,7 @@ use v5.36.0;
use strict;
use warnings;
use utf8;
use Moo;
@ -40,7 +41,7 @@ sub disabled_reason($self, $pj) {
if (!$self->_check_if_someone_needs_to_restore($pj)) {
return 'Todo tu equipo esta lleno de vitalidad y listo para la aventura.';
}
return;
return undef;
}
sub _check_if_someone_needs_to_restore($self, $pj) {

View File

@ -1,8 +1,9 @@
package LasTres::Action::GolpearArbolCentralTribuDeLaLima;
package LasTres::PJAction::GolpearArbolCentralTribuDeLaLima;
use v5.36.0;
use strict;
use warnings;
use utf8;
use feature 'signatures';
@ -31,7 +32,7 @@ sub disabled_reason ( $self, $pj ) {
if ($pj->health < 1) {
return "Estás debilitado.";
}
return;
return undef;
}
sub _chance_enemy ( $self, $pj ) {

View File

@ -12,6 +12,7 @@ use utf8;
use Moo;
use LasTres::Planet::Bahdder::BosqueDelHeroe::TribuDeLaLima;
use LasTres::PJAction::GolpearArbolCentralTribuDeLaLima;
with 'LasTres::Location';
@ -32,7 +33,9 @@ sub parent {
}
sub actions {
return [];
return [
LasTres::PJAction::GolpearArbolCentralTribuDeLaLima->new
];
}
sub npcs {

View File

@ -13,8 +13,8 @@ use LasTres::Schema;
our $VERSION = $LasTres::Schema::VERSION;
{
my $self;
sub new {
my $self;
my $class = shift;
if (!defined $self) {
$self = $class->SUPER::new(@_);

View File

@ -470,6 +470,13 @@ sub update_location ($self) {
to_json( { command => 'update-location' } ) );
}
sub update_actions($self) {
require LasTres::Redis;
my $redis = LasTres::Redis->new;
$redis->publish( $redis->pj_subscription($self),
to_json( { command => 'update-actions' } ) );
}
sub actions ($self) {
my @actions;
$self = $self->get_from_storage;
@ -477,13 +484,13 @@ sub actions ($self) {
my $location = $team->location;
if ( defined $team->battle ) {
# There should go the battle actions.
return;
# Here should go the battle actions.
return [];
}
if ( $team->is_moving ) {
# Probably there should go the actions still doable when moving.
return;
return [];
}
# TODO: Handle explore when implemented.

View File

@ -9,7 +9,7 @@
body {
margin: 0px;
padding: 0px;
min-height: 100%;
height: 100vh;
background: ghostwhite; }
body label.bar-container {
width: 90%; }
@ -20,6 +20,18 @@ body {
body label.bar-container div.bar div.filled {
background: lightgreen;
height: 100%; }
body div.action {
border: solid 1px black;
text-decoration: underline;
display: flex;
min-height: 100px;
align-content: center;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%; }
body div.action div.disabled-reason {
text-decoration: none; }
body div.pj-list-item {
display: flex;
align-items: center; }
@ -64,7 +76,7 @@ body {
body div.width-max-content {
width: max-content; }
body div#game-container {
min-height: 100%; }
height: 100vh; }
body div#game-container nav.menu-bar {
width: 100%;
background: grey;

View File

@ -10,6 +10,10 @@
}
}
body {
margin: 0px;
padding: 0px;
height: 100vh;
background: ghostwhite;
label.bar-container {
width: 90%;
div.bar {
@ -22,6 +26,20 @@ body {
}
}
}
div.action {
border: solid 1px black;
text-decoration: underline;
display: flex;
min-height: 100px;
align-content: center;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
div.disabled-reason {
text-decoration: none;
}
}
div.pj-list-item {
display: flex;
align-items: center;
@ -75,15 +93,11 @@ body {
height: 50vh;
}
margin: 0px;
padding: 0px;
min-height: 100%;
background: ghostwhite;
div.width-max-content {
width: max-content;
}
div#game-container {
min-height: 100%;
height: 100vh;
nav.menu-bar {
width: 100%;
background: grey;

File diff suppressed because one or more lines are too long