support multiple scenes and passing between them

This commit is contained in:
Rusty Striker 2025-07-04 13:23:22 +03:00
parent 2d46cbb4d0
commit 838c89ac73
Signed by: RustyStriker
GPG key ID: 87E4D691632DFF15
8 changed files with 124 additions and 67 deletions

View file

@ -111,8 +111,11 @@ tavern.onlogin = (s) => {
login.style.display = 'none';
game.style.display = 'flex';
// get last 50 msgs (i think that is enough for now) when we get in
// TODO: Maybe move this into the server itself? that is a lot of stuff that we know are gonna happen...
// For now i'll keep it like that tho
tavern.get_last_msgs(50);
tavern.get_current_scene();
// TODO: Perhaps figure out a way to show a certain scene? maybe on the server it would make more sense
tavern.get_scene_list();
}
else {
alert("Invalid username or password!");
@ -210,6 +213,14 @@ tavern.onshowscene = (show) => {
tavern.onspawntoken(token);
}
}
tavern.onscenelist = (list) => {
console.log(list);
let div = document.getElementById('scene-list');
div.innerHTML = '';
for (let scene of list.scenes) {
div.innerHTML += `<button onclick='tavern.get_scene(${scene[0]});'>${scene[1]}</button>`;
}
}
function onLoginClick() {
let username = document.getElementById('login-username').value;
let pass = document.getElementById('login-pass').value;

View file

@ -36,8 +36,7 @@
</div>
<div id="game-view"
style="display: flex; overflow: hidden; position: relative; top: 0px; left: 0px; flex-grow: 1;">
<div style="position:absolute; top: 10px; left: 5px; background-color: rgb(255, 166, 0); z-index: 5;">
floating<br>stuff
<div id="scene-list">
</div>
<div id="dice-roll-popup"
style="display:none; justify-content: center; width: 100%; height: 100%; background-color: transparent; position: absolute; z-index: 10; top: 0px; left: 0px">

View file

@ -93,3 +93,22 @@ body {
.dice-roll-row p {
margin: 0px;
}
#scene-list {
display: flex;
flex-direction: column;
gap: 2px;
position: absolute;
top: 10px;
left: 5px;
z-index: 5;
border-color: black;
border-width: 2px;
border-style: solid;
background-color: black;
}
#scene-list button {
background-color: wheat;
border-width: 0px;
}

View file

@ -3,7 +3,6 @@ const tavern = {
msgs: [],
connected: false,
loggedIn: false,
currentScene: 0,
call: (f, ...args) => {
if (typeof (f) == "function") {
f(...args);
@ -57,9 +56,11 @@ tavern.socket.onmessage = (m) => {
tavern.call(tavern.onmovetoken, m.move_token);
}
if (m.show_scene) {
tavern.currentScene = m.show_scene.scene;
tavern.call(tavern.onshowscene, m.show_scene);
}
if (m.scene_list) {
tavern.call(tavern.onscenelist, m.scene_list);
}
}
tavern.login = (username, password) => {
if (!tavern.connected || tavern.loggedIn) { return false; }
@ -96,7 +97,11 @@ tavern.action_result = (name, source, targets, results) => {
if (!tavern.connected || tavern.loggedIn) { return false; }
tavern.socket.send(JSON.stringify({ action_result: { name: name, source: source ?? '', targets: targets ?? [], results: results } }));
}
tavern.get_current_scene = () => {
tavern.get_scene = (id) => {
if (!tavern.connected || tavern.loggedIn) { return; }
tavern.socket.send(JSON.stringify('get_current_scene'))
tavern.socket.send(JSON.stringify({ get_scene: { id: id } }))
}
tavern.get_scene_list = () => {
if (!tavern.connected || tavern.loggedIn) { return; }
tavern.socket.send(JSON.stringify('get_scene_list'))
}

View file

@ -48,7 +48,10 @@ pub enum Request {
amount: usize,
},
// Map requests
GetCurrentScene,
GetScene {
id: usize,
},
GetSceneList,
GetTokens {
scene: usize,
},
@ -82,6 +85,9 @@ pub enum Response {
grid_cell_size: Option<f32>,
grid_offset: Option<[f32; 2]>,
},
SceneList {
scenes: Vec<(usize, String)>,
},
MoveToken {
token_id: usize,
x: f32,

View file

@ -5,8 +5,6 @@ use character_sheet::{AccessLevel, Character, CharacterShort, EntryType};
use scene::{Party, Scene, TokenInfo};
use serde::{Deserialize, Serialize};
use crate::game::scene::Map;
pub mod character_sheet;
pub mod chat_message;
pub mod entry;
@ -26,10 +24,7 @@ pub trait GameImpl<'a, C: Character<A> + Serialize + Deserialize<'a>, A: entry::
// Scenes
/// the list of available scenes
fn scenes(&self) -> Vec<usize>;
fn current_scene(&self) -> usize;
/// Gets the map background (file path)
fn scene_characters(&self, scene: usize, character_id: usize) -> Option<Vec<CharacterShort>>;
fn scene_map(&self, scene_id: usize) -> Option<&Map>;
fn get_scene(&self, id: usize) -> Option<&Scene>;
fn create_token(&mut self, scene_id: usize, character: String, img_source: String, x: f32, y: f32) -> usize;
fn move_token(&mut self, scene_id: usize, token_id: usize, x: f32, y: f32) -> bool;
fn token_info(&self, scene: usize, token_id: usize) -> Option<&TokenInfo>;
@ -41,7 +36,6 @@ pub struct Game<C: Character<A> + Serialize, A: entry::GameEntry + Serialize> {
_a: PhantomData<A>,
characters: Vec<(C, CharacterInfo)>,
scenes: HashMap<usize, Scene>,
current_scene: usize,
}
impl<'a, C: Character<A> + Serialize + Deserialize<'a>, A: entry::GameEntry + Serialize + Deserialize<'a>>
GameImpl<'a, C, A> for Game<C, A>
@ -70,6 +64,8 @@ impl<'a, C: Character<A> + Serialize + Deserialize<'a>, A: entry::GameEntry + Se
scenes.insert(
0,
Scene {
title: "Dungeon".to_string(),
visible_to_users: true,
map: Some(scene::Map {
background: "assets/pf2r/maps/testmap.jpg".to_string(),
grid_cell_size: 150.0,
@ -79,11 +75,24 @@ impl<'a, C: Character<A> + Serialize + Deserialize<'a>, A: entry::GameEntry + Se
characters: vec![(0, Party(true))],
},
);
scenes.insert(
1,
Scene {
title: "Basement".to_string(),
visible_to_users: false,
map: Some(scene::Map {
background: "assets/pf2r/maps/testmap.jpg".to_string(),
grid_cell_size: 300.0,
grid_offset: [80.0, 35.0],
tokens: HashMap::new(),
}),
characters: vec![(0, Party(true))],
},
);
Self {
_a: PhantomData,
characters: Vec::new(),
scenes,
current_scene: 0,
}
}
fn create_character(&mut self) -> usize {
@ -175,24 +184,8 @@ impl<'a, C: Character<A> + Serialize + Deserialize<'a>, A: entry::GameEntry + Se
self.scenes.keys().map(|k| *k).collect()
}
fn scene_characters(&self, scene: usize, id: usize) -> Option<Vec<CharacterShort>> {
let party = self.characters.get(id).map(|c| c.1.party).unwrap_or_default();
self.scenes.get(&scene).map(|s| {
s.characters
.iter()
.filter(|c| party.can_see(c.1))
.map(|c| self.characters.get(c.0).map(|e| e.0.short(c.0)))
.flatten()
.collect()
})
}
fn scene_map(&self, scene_id: usize) -> Option<&Map> {
self.scenes.get(&scene_id).map(|s| s.map.as_ref()).flatten()
}
fn current_scene(&self) -> usize {
self.current_scene
fn get_scene(&self, id: usize) -> Option<&Scene> {
self.scenes.get(&id)
}
}

View file

@ -3,6 +3,9 @@ use std::collections::HashMap;
#[derive(Serialize, Deserialize, Debug)]
pub struct Scene {
/// Scene title/name
pub title: String,
pub visible_to_users: bool,
/// Map for the scene, None in case of theater of the mind kinda gameplay
pub map: Option<Map>,
/// List of character ids, and can the party see them (maybe change that to a different thing to allow maybe 2 parties? maybe a bit field)

View file

@ -156,32 +156,52 @@ impl GameServer {
}
}
}
api::Request::GetCurrentScene => {
let scene = self.game.current_scene();
let scene_tokens = self
api::Request::GetScene { id: scene_id } => {
if self
.game
.available_tokens(scene)
.get_scene(scene_id)
.map(|s| s.visible_to_users)
.unwrap_or(false) ||
*self.users.get(&id).unwrap_or(&false)
{
let scene_tokens = self
.game
.available_tokens(scene_id)
.iter()
.map(|&id| self.game.token_info(scene_id, id).map(|info| (id, info)))
.flatten()
.map(|(id, info)| SpawnToken {
token_id: id,
x: info.x,
y: info.y,
img: info.img_source.clone(),
})
.collect::<Vec<_>>();
let map = self.game.get_scene(scene_id).map(|s| s.map.as_ref()).flatten();
_ = broadcast.send((
Some(id.clone()),
api::Response::ShowScene {
scene: scene_id,
tokens: scene_tokens,
background: map.map(|m| m.background.clone()),
grid_cell_size: map.map(|m| m.grid_cell_size),
grid_offset: map.map(|m| m.grid_offset.clone()),
},
));
}
}
api::Request::GetSceneList => {
let admin = *self.users.get(&id).unwrap_or(&false);
let scenes = self
.game
.scenes()
.iter()
.map(|&id| self.game.token_info(scene, id).map(|info| (id, info)))
.map(|id| self.game.get_scene(*id).map(|s| (id, s)))
.flatten()
.map(|(id, info)| SpawnToken {
token_id: id,
x: info.x,
y: info.y,
img: info.img_source.clone(),
})
.filter(|(_, scene)| admin || scene.visible_to_users)
.map(|(id, s)| (*id, s.title.to_string()))
.collect::<Vec<_>>();
let map = self.game.scene_map(scene);
_ = broadcast.send((
Some(id.clone()),
api::Response::ShowScene {
scene: scene,
tokens: scene_tokens,
background: map.map(|m| m.background.clone()),
grid_cell_size: map.map(|m| m.grid_cell_size),
grid_offset: map.map(|m| m.grid_offset.clone()),
},
));
_ = broadcast.send((Some(id.clone()), api::Response::SceneList { scenes: scenes }))
}
api::Request::SpawnToken {
map_id,
@ -190,13 +210,10 @@ impl GameServer {
y,
img_path,
} => {
// TODO: Make sure the user is an admin
let token_id = self.game.create_token(map_id, character, img_path.clone(), x, y);
_ = broadcast.send((
if map_id == self.game.current_scene() {
None
} else {
Some(id.clone())
},
None, // TODO: add the option to spawn the token hidden
api::Response::SpawnToken(SpawnToken {
token_id,
x,
@ -250,11 +267,15 @@ impl GameServer {
_ = broadcast.send((Some(id), api::Response::Shutdown));
}
api::Request::Shutdown => break,
api::Request::CharacterDisplay { id } => todo!(),
api::Request::CharacterInputs { id } => todo!(),
api::Request::CharacterGetField { id, field } => todo!(),
api::Request::CharacterSetField { id, field, val } => todo!(),
api::Request::CharacterAssign { id, user } => todo!(),
api::Request::CharacterDisplay { id: _ } => {}
api::Request::CharacterInputs { id: _ } => todo!(),
api::Request::CharacterGetField { id: _, field: _ } => todo!(),
api::Request::CharacterSetField {
id: _,
field: _,
val: _,
} => todo!(),
api::Request::CharacterAssign { id: _, user: _ } => todo!(),
}
}
_ = broadcast.send((None, api::Response::Shutdown));