This commit is contained in:
Rusty Striker 2025-06-22 20:49:54 +03:00
parent 2a7324b133
commit bb7d3c48ea
Signed by: RustyStriker
GPG key ID: 87E4D691632DFF15
6 changed files with 98 additions and 44 deletions

View file

@ -1,12 +1,22 @@
// init - game view (map) // init - game view (map)
const GRID_SIZE = 200; // Grid size in pixels
var mapScale = 1.0; var mapScale = 1.0;
var mapOffsetX = 0.0; var mapOffsetX = 0.0;
var mapOffsetY = 0.0; var mapOffsetY = 0.0;
var draggedToken = { token: null, offX: 0, offY: 0 }; var draggedToken = { token: null, offX: 0, offY: 0 };
var draggedDiv = { div: null, offX: 0, offY: 0 }; var draggedDiv = { div: null, offX: 0, offY: 0 };
var gridDashType = 0; var gridDashType = 1;
var gridLineWidth = 2;
var gridSize = 200; // Grid size in pixels
var gridOffset = [0, 0]
var gridColor = 'red';
var showGrid = true; var showGrid = true;
const LINE_DASH_TYPES = [
[1.0, 0.0],
[0.1, 0.8, 0.1, 0.0],
[0, 0.1, 0.1, 0.6, 0.1, 0.1],
[0, 0.1, 0.3, 0.2, 0.3, 0.1],
[0.1, 0.8, 0.1],
];
function init() { function init() {
let view = document.getElementById('game-view'); let view = document.getElementById('game-view');
@ -40,34 +50,44 @@ function init() {
} }
document.body.onmousemove = onMoveableDivDrag; document.body.onmousemove = onMoveableDivDrag;
document.body.onmouseup = onMoveableDivMouseUp; document.body.onmouseup = onMoveableDivMouseUp;
let mapBackground = document.getElementById('map-background');
updateGridCanvas(mapBackground.width, mapBackground.height);
} }
function updateGridCanvas(width, height) { function updateGrid() {
// Draw the grid on the grid thing // Draw the grid on the grid thing
let mapBackground = document.getElementById('map-background');
let width = mapBackground.width;
let height = mapBackground.height;
let svg = document.getElementById('map-grid'); let svg = document.getElementById('map-grid');
let lineDashTypes = [
[0.1, 0.8, 0.1, 0.0],
[0, 0.1, 0.1, 0.6, 0.1, 0.1],
[0, 0.1, 0.3, 0.2, 0.3, 0.1],
[0.1, 0.8, 0.1],
];
svg.setAttribute('width', width); svg.setAttribute('width', width);
svg.setAttribute('height', height); svg.setAttribute('height', height);
svg.setAttribute('stroke', gridColor);
svg.setAttribute('stroke-width', gridLineWidth);
svg.setAttribute('stroke-dasharray', LINE_DASH_TYPES[gridDashType].map(v => v * gridSize).join(' '))
svg.innerHTML = ''; svg.innerHTML = '';
svg.setAttribute('stroke', 'black');
svg.setAttribute('stroke-width', '15');
svg.setAttribute('stroke-dash-array', lineDashTypes[gridDashType].join(' '))
let i = 0; let i = 0;
while (i < Math.max(width, height)) { while (i <= Math.max(width, height)) {
i += GRID_SIZE; svg.innerHTML += `<line x1="${gridOffset[0] - gridSize}" x2="${width + gridOffset[0]}" y1="${i + gridOffset[1]}" y2="${i + gridOffset[1]}" vector-effect="non-scaling-stroke" />`;
svg.innerHTML += `<line x1="0" x2="${width}" y1="${i}" y2="${i}" />`; svg.innerHTML += `<line x1="${i + gridOffset[0]}" x2="${i + gridOffset[0]}" y1="${gridOffset[1] - gridSize}" y2="${height + gridOffset[1]}" vector-effect="non-scaling-stroke" />`;
svg.innerHTML += `<line x1="${i}" x2="${i}" y1="0" y2="${height}" />`; i += gridSize;
} }
} }
function updateGridDashType(type) {
gridDashType = Math.max(0, Math.min(LINE_DASH_TYPES.length - 1, type));
let svg = document.getElementById('map-grid');
svg.setAttribute('stroke-dasharray', LINE_DASH_TYPES[gridDashType].map(v => v * gridSize).join(' '))
}
function updateGridColor(color) {
let svg = document.getElementById('map-grid');
svg.setAttribute('stroke', color);
gridColor = color;
}
function updateGridWidth(width) {
let svg = document.getElementById('map-grid');
gridLineWidth = width;
svg.setAttribute('stroke-width', gridLineWidth);
}
tavern.onlogin = (s) => { tavern.onlogin = (s) => {
if (s) { if (s) {
@ -136,11 +156,11 @@ tavern.onspawntoken = (t) => {
let map = document.getElementById('map'); let map = document.getElementById('map');
let token = document.createElement('div'); let token = document.createElement('div');
token.className = 'token token-transition'; token.className = 'token token-transition';
token.style.top = `${t.y * GRID_SIZE}px`; token.style.top = `${t.y * gridSize + gridOffset[1]}px`;
token.style.left = `${t.x * GRID_SIZE}px`; token.style.left = `${t.x * gridSize + gridOffset[0]}px`;
token.token_id = t.token_id; token.token_id = t.token_id;
token.innerHTML = ` token.innerHTML = `
<img src='${t.img}' ondragstart='return false;'> <img src='${t.img}' style="width: ${gridSize}px; height: ${gridSize}px;" ondragstart='return false;'>
` `
token.onmousedown = (e) => { token.onmousedown = (e) => {
token.classList.remove('token-transition'); token.classList.remove('token-transition');
@ -154,8 +174,8 @@ tavern.onspawntoken = (t) => {
tavern.onmovetoken = (m) => { tavern.onmovetoken = (m) => {
let token = Array.from(document.getElementsByClassName('token')).filter(t => t.token_id == m.token_id)[0] let token = Array.from(document.getElementsByClassName('token')).filter(t => t.token_id == m.token_id)[0]
if (token) { if (token) {
token.style.top = `${m.y * GRID_SIZE}px`; token.style.top = `${(m.y * gridSize) + gridOffset[1]}px`;
token.style.left = `${m.x * GRID_SIZE}px`; token.style.left = `${(m.x * gridSize) + gridOffset[0]}px`;
} }
} }
tavern.onshowscene = (show) => { tavern.onshowscene = (show) => {
@ -164,8 +184,13 @@ tavern.onshowscene = (show) => {
Array.from(map.children).filter(c => c.classList.contains('token')).forEach(c => map.removeChild(c)); Array.from(map.children).filter(c => c.classList.contains('token')).forEach(c => map.removeChild(c));
let background = document.getElementById('map-background'); let background = document.getElementById('map-background');
gridOffset = show.grid_offset;
Array.from(document.getElementsByClassName('token')).forEach(t => {
t.children[0].style.width = `${gridSize}px`;
t.children[0].style.height = `${gridSize}px`;
});
gridSize = show.grid_cell_size;
background.src = show.background ?? ''; background.src = show.background ?? '';
updateGridCanvas(background.width, background.height);
for (let token of show.tokens) { for (let token of show.tokens) {
tavern.onspawntoken(token); tavern.onspawntoken(token);
} }
@ -205,8 +230,8 @@ function onGameMouseMove(event) {
function onGameMouseUp() { function onGameMouseUp() {
if (draggedToken.token != null) { if (draggedToken.token != null) {
let t = draggedToken.token; let t = draggedToken.token;
let x = Math.floor(0.5 + parseInt(t.style.left) / GRID_SIZE); let x = Math.floor(0.5 + (-gridOffset[0] + parseInt(t.style.left)) / gridSize);
let y = Math.floor(0.5 + parseInt(t.style.top) / GRID_SIZE); let y = Math.floor(0.5 + (-gridOffset[1] + parseInt(t.style.top)) / gridSize);
let id = t.token_id; let id = t.token_id;
t.classList.add('token-transition'); t.classList.add('token-transition');
t.children[0].style.cursor = ''; t.children[0].style.cursor = '';

View file

@ -64,8 +64,12 @@
<button style="background-color: #ffffd6;" onclick="showHideDiv('initiative-tracker')"><b>i</b></button> <button style="background-color: #ffffd6;" onclick="showHideDiv('initiative-tracker')"><b>i</b></button>
</div> </div>
<div id="map" style="position:absolute; transform-origin: top left; user-select: none;"> <div id="map" style="position:absolute; transform-origin: top left; user-select: none;">
<img id="map-background" src="https://rustystriker.dev/molly.jpg"> <img id="map-background" src="https://rustystriker.dev/molly.jpg" onload="updateGrid()"
<svg id="map-grid" xmlns="http://www.w3.org/2000/svg" vector-effect="non-scaling-stroke" style="position: absolute; top: 0px; left: 0px"></svg> onerror="alert('Failed loading map')">
<svg id="map-grid" xmlns="http://www.w3.org/2000/svg" style="position: absolute; top: 0px; left: 0px">
<g vector-effect="non-scaling-stroke" id="map-grid-horizontal" />
<g vector-effect="non-scaling-stroke" id="map-grid-vertical" />
</svg>
</div> </div>
</div> </div>
</div> </div>

View file

@ -75,11 +75,23 @@ pub enum Response {
Login(login::LoginResult), Login(login::LoginResult),
Message(ChatMessage), Message(ChatMessage),
GetChatHistory(Vec<ChatMessage>), GetChatHistory(Vec<ChatMessage>),
ShowScene { scene: usize, tokens: Vec<SpawnToken>, background: Option<String> }, ShowScene {
MoveToken { token_id: usize, x: f32, y: f32 }, scene: usize,
tokens: Vec<SpawnToken>,
background: Option<String>,
grid_cell_size: Option<f32>,
grid_offset: Option<[f32; 2]>,
},
MoveToken {
token_id: usize,
x: f32,
y: f32,
},
SpawnToken(SpawnToken), SpawnToken(SpawnToken),
CharacterCreated(usize), CharacterCreated(usize),
Quit { id: String }, Quit {
id: String,
},
Shutdown, Shutdown,
} }

View file

@ -5,6 +5,8 @@ use character_sheet::{AccessLevel, Character, CharacterShort, EntryType};
use scene::{Party, Scene, TokenInfo}; use scene::{Party, Scene, TokenInfo};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::game::scene::Map;
pub mod character_sheet; pub mod character_sheet;
pub mod chat_message; pub mod chat_message;
pub mod entry; pub mod entry;
@ -27,7 +29,7 @@ pub trait GameImpl<'a, C: Character<A> + Serialize + Deserialize<'a>, A: entry::
fn current_scene(&self) -> usize; fn current_scene(&self) -> usize;
/// Gets the map background (file path) /// Gets the map background (file path)
fn scene_characters(&self, scene: usize, character_id: usize) -> Option<Vec<CharacterShort>>; fn scene_characters(&self, scene: usize, character_id: usize) -> Option<Vec<CharacterShort>>;
fn scene_map(&self, scene_id: usize) -> Option<String>; fn scene_map(&self, scene_id: usize) -> Option<&Map>;
fn create_token(&mut self, scene_id: usize, character: String, img_source: String, x: f32, y: f32) -> usize; 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 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>; fn token_info(&self, scene: usize, token_id: usize) -> Option<&TokenInfo>;
@ -70,6 +72,8 @@ impl<'a, C: Character<A> + Serialize + Deserialize<'a>, A: entry::GameEntry + Se
Scene { Scene {
map: Some(scene::Map { map: Some(scene::Map {
background: "assets/pf2r/maps/testmap.jpg".to_string(), background: "assets/pf2r/maps/testmap.jpg".to_string(),
grid_cell_size: 150.0,
grid_offset: [80.0, 35.0],
tokens, tokens,
}), }),
characters: vec![(0, Party(true))], characters: vec![(0, Party(true))],
@ -183,11 +187,8 @@ impl<'a, C: Character<A> + Serialize + Deserialize<'a>, A: entry::GameEntry + Se
}) })
} }
fn scene_map(&self, scene_id: usize) -> Option<String> { fn scene_map(&self, scene_id: usize) -> Option<&Map> {
self.scenes self.scenes.get(&scene_id).map(|s| s.map.as_ref()).flatten()
.get(&scene_id)
.map(|s| s.map.as_ref().map(|m| m.background.clone()))
.flatten()
} }
fn current_scene(&self) -> usize { fn current_scene(&self) -> usize {

View file

@ -1,5 +1,5 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Scene { pub struct Scene {
@ -13,6 +13,10 @@ pub struct Scene {
pub struct Map { pub struct Map {
/// Image source for the background of the map /// Image source for the background of the map
pub background: String, pub background: String,
// Cell size of the map grid
pub grid_cell_size: f32,
// Grid offset from top left, [x, y]
pub grid_offset: [f32; 2],
/// Tokens in the current map (should be of characters), maps from token_id to its info /// Tokens in the current map (should be of characters), maps from token_id to its info
pub tokens: HashMap<usize, TokenInfo>, pub tokens: HashMap<usize, TokenInfo>,
} }

View file

@ -171,12 +171,15 @@ impl GameServer {
img: info.img_source.clone(), img: info.img_source.clone(),
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let map = self.game.scene_map(scene);
_ = broadcast.send(( _ = broadcast.send((
Some(id.clone()), Some(id.clone()),
api::Response::ShowScene { api::Response::ShowScene {
scene: scene, scene: scene,
tokens: scene_tokens, tokens: scene_tokens,
background: self.game.scene_map(scene), 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()),
}, },
)); ));
} }
@ -190,7 +193,12 @@ impl GameServer {
let token_id = self.game.create_token(map_id, character, img_path.clone(), x, y); let token_id = self.game.create_token(map_id, character, img_path.clone(), x, y);
_ = broadcast.send(( _ = broadcast.send((
Some(id.clone()), Some(id.clone()),
api::Response::SpawnToken(SpawnToken { token_id, x, y, img: img_path.clone() }), api::Response::SpawnToken(SpawnToken {
token_id,
x,
y,
img: img_path.clone(),
}),
)); ));
} }
api::Request::MoveToken { token_id, x, y } => { api::Request::MoveToken { token_id, x, y } => {