all repos — felt @ e7081a53201fe48f7741119c3f475371aad013e2

virtual tabletop for dungeons and dragons (and similar) using Go, MongoDB, and websockets

implemented tokens; alpha testing go
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iQIzBAABCAAdFiEEkFh6dA+k/6CXFXU4O3+8IhROY5gFAmSqWDMACgkQO3+8IhRO
Y5jKOg/8C3gniYWLM81arW/oFj8gEmacMed6MdIHAARv+8cpc1KluBu6ZNG1fSDE
coyM1CW3JvqQJ9C8Zx80FNbCK2OsqlINjkFCBCoboyNdvxS2nRKfaCxf8gQMU3l6
KXVtySnjFW5mp2V1GHbhMeUfhYtF9woFtW/J7B+uVkQujiq1x8Hz34UZ7Ad62A4j
1yiMw6MMxCoeX3zZWqQpjPnd8oxwWggit5xf4hpnDTIr4eVH0dwQ/DJL2Qj3JdQQ
PNX4tbAzX+nlGD40zZYgSvgK5R/L/fTGYHZxIBtISWbVQbeayxuYO7wonJ5I/xk4
DaK0z0HGuebLomMUnXyD/Hc07zQ27NppTVGNO79kvBzc0Xb4Qk6p6t6kk0Tn/b35
FBP3pFnHU1kR5kSOpGX67iRXF3AmaN7HK1IOtaMiZ5in3JkGXSREFw+x+aSbx6Nh
vhItwsK9nPAMGjsquRpsXiswL1YPLS6b5uUNBBYXxkqYcnRpOTtYAkCRAcS4aGgj
3TJ9xmci80IALy83PUhRA7qDag86pkLijAPDN3dfuFF9CwKdbAGR0q0nkOXxvQDU
YZof6iNlhq40TE4JkJpSNzf2kd5RV2QucNoY141IKxC+n9xNLCfOHhIQhN7c3lnm
MKdbzbzIqVg7JiFvW64fFn5AGllbyzCvbXnFe9hS2D2EhTrOYuA=
=Bcif
-----END PGP SIGNATURE-----
commit

e7081a53201fe48f7741119c3f475371aad013e2

parent

4ba6c9315fa06c4b0b01cd16b0629d37ef6e2a54

M gametable/server.gogametable/server.go

@@ -231,8 +231,6 @@ err := self.dbAdapter.ActivateToken(key, *t.Id, false)

if err != nil { return err } - tableMsg.Token.X = nil - tableMsg.Token.Y = nil } else if t.X != nil && t.Y != nil { err := self.dbAdapter.MoveToken(key, t) if err != nil {

@@ -273,11 +271,6 @@ }

} if tableMsg.Token != nil { t := tableMsg.Token - strId := "" - if t.Id != nil { - strId = *t.Id - } - fmt.Println(strId +"::" + t.Name + "::" + t.Sprite) if t.Id == nil { id, err := self.dbAdapter.CreateToken(key, *t) t.Id = &id
M models/models.gomodels/models.go

@@ -18,16 +18,16 @@ Timestamp time.Time `json:"timestamp"`

} type Token struct { - Id *string `json:"id" bson:"_id"` - Name string `json:"name"` - Sprite string `json:"sprite"` - W int `json:"w"` - H int `json:"h"` - OX int `json:"oX"` - OY int `json:"oY"` - X *int `json:"x"` - Y *int `json:"y"` - Active bool `json:"active"` + Id *string `json:"id" bson:"_id"` + Name string `json:"name"` + Sprite string `json:"sprite"` + W int `json:"w"` + H int `json:"h"` + OX int `json:"oX"` + OY int `json:"oY"` + X *float64 `json:"x"` + Y *float64 `json:"y"` + Active bool `json:"active"` } type Table struct {

@@ -42,8 +42,8 @@

type TableMessage struct { Auth *string `json:"auth,omitempty"` Key *TableKey `json:"key"` - DiceRoll *DiceRoll `json:"diceRoll"` - Token *Token `json:"token"` - MapImg *string `json:"mapImg"` - AuxMsg *string `json:"auxMsg"` + DiceRoll *DiceRoll `json:"diceRoll,omitempty"` + Token *Token `json:"token,omitempty"` + MapImg *string `json:"mapImg,omitempty"` + AuxMsg *string `json:"auxMsg,omitempty"` }
M mongodb/adapter.gomongodb/adapter.go

@@ -290,24 +290,21 @@ return "", errors.New(fmt.Sprintf(errNoCollection, "tables"))

} func (self *DbEngine) CheckToken(table models.TableKey, tokenId string) (bool, bool) { - mongoId, err := primitive.ObjectIDFromHex(tokenId) - if err != nil { - return false, false - } tables := self.db.Collection("tables") if tables != nil { result := models.Table{} err := tables.FindOne(self.mkCtx(10), bson.D{ {"name", table.Name}, {"passcode", table.Passcode}, - {"tokens", bson.E{"_id", mongoId}}, + {"tokens._id", tokenId}, }).Decode(&result) if err != nil { + fmt.Printf("%v", err) return false, false } else { active := false for _, t := range result.Tokens { - if *t.Id == tokenId && t.Active { + if t.Id != nil && *t.Id == tokenId && t.Active { active = true } }

@@ -344,10 +341,6 @@

} func (self *DbEngine) ActivateToken(table models.TableKey, tokenId string, active bool) error { - mongoId, err := primitive.ObjectIDFromHex(tokenId) - if err != nil { - return err - } tables := self.db.Collection("tables") if tables != nil { var result models.Table

@@ -356,12 +349,10 @@ self.mkCtx(10),

bson.D{ {"name", table.Name}, {"passcode", table.Passcode}, - {"tokens", bson.E{"_id", mongoId}}, + {"tokens._id", tokenId}, }, bson.D{ - {"$set", bson.D{{"tokens.$", bson.D{ - {"active", active}, - }}}}, + {"$set", bson.D{{"tokens.$.active", active}}}, }, ).Decode(&result) return err

@@ -370,10 +361,6 @@ return errors.New(fmt.Sprintf(errNoCollection, "tables"))

} func (self *DbEngine) MoveToken(table models.TableKey, token models.Token) error { - mongoId, err := primitive.ObjectIDFromHex(*token.Id) - if err != nil { - return err - } tables := self.db.Collection("tables") if tables != nil { var result models.Table

@@ -382,13 +369,10 @@ self.mkCtx(10),

bson.D{ {"name", table.Name}, {"passcode", table.Passcode}, - {"tokens", bson.E{"_id", mongoId}}, + {"tokens._id", token.Id}, }, bson.D{ - {"$set", bson.D{{"tokens.$", bson.D{ - {"x", token.X}, - {"y", token.Y}, - }}}}, + {"$set", bson.D{{"tokens.$.x", token.X}, {"tokens.$.y", token.Y}}}, }, ).Decode(&result) return err

@@ -397,10 +381,7 @@ return errors.New(fmt.Sprintf(errNoCollection, "tables"))

} func (self *DbEngine) DestroyToken(table models.TableKey, tokenId string) error { - mongoId, err := primitive.ObjectIDFromHex(tokenId) - if err != nil { - return err - } + tables := self.db.Collection("tables") if tables != nil { var result models.Table

@@ -411,7 +392,7 @@ {"name", table.Name},

{"passcode", table.Passcode}, }, bson.D{ - {"$pull", bson.D{{"tokens", bson.D{{"_id", mongoId}}}}}, + {"$pull", bson.D{{"tokens", bson.D{{"_id", tokenId}}}}}, }, ).Decode(&result) return err
M static/admin.jsstatic/admin.js

@@ -43,17 +43,17 @@ document.getElementById("input_table_name").value = name;

document.getElementById("input_table_pass").value = pass; dial(); const table = await res.json() - infoHtml = "<a href='#' onclick='getTables()'>&larr; table list</a><br>"; + infoHtml = "<a href='#' onclick='getTables();return false;'>&larr; table list</a><br>"; infoHtml += `<textarea id='auxMsgZone'>${table.auxMsg}</textarea><br><button onclick='publishAuxMsg()'>Set Status</button>` infoHtml += "<button onclick='destroyTable()'>Destroy Table</button><br/>"; infoHtml += "<input id='map_img_upload' type='file'/><button onclick='uploadMapImg()'>Upload Map</button><br/>" if (mapImgs.ok) { infoHtml += "<label>Available Maps</label>"; const imgs = await mapImgs.json(); - infoHtml += "<ul>"; + infoHtml += "<ul class='two_btn_list'>"; for (const i of imgs) { const parts = i.split("/"); - infoHtml += `<li>${parts[parts.length - 1]} <a href="${i}" target="_blank">view</a> <button onclick="sendMapImg('${i}');">Set</button> <button onclick="deleteImg('${i}')">Delete</button></li>\n`; + infoHtml += `<li><a href="${i}" target="_blank">${parts[parts.length - 1]}</a> <button onclick="sendMapImg('${i}');">Set</button> <button onclick="deleteImg('${i}')">Delete</button></li>\n`; } infoHtml += "</ul>"; } else {

@@ -65,10 +65,10 @@ let tokenListHTML = "<input id='token_img_upload' type='file'/><button onclick='uploadTokenImg()'>Upload Sprite</button><br/>";

if (tokenImgs.ok) { tokenListHTML += "<label>Available Sprites</label>"; const tokens = await tokenImgs.json(); - tokenListHTML += "<ul>"; + tokenListHTML += "<ul class='single_btn_list'>"; for (const t of tokens) { const parts = t.split("/"); - tokenListHTML += `<li>${parts[parts.length - 1]} <a href="${t}" target="_blank">view</a> <button onclick="deleteImg('${t}')">Delete</button></li>\n` + tokenListHTML += `<li><a href="${t}" target="_blank">${parts[parts.length - 1]}</a> <button onclick="deleteImg('${t}')">Delete</button></li>\n` } tokenListHTML += "</ul>"; fillSpriteDropdown(tokens);

@@ -156,6 +156,7 @@ }

} function drawTokenOrigin() { + if (tokenSpriteDropdown.selectedIndex >= 0) { const img = previewZone.children[0]; const x = Number(tokenWidth.value) / Number(tokenCX.value); const y = Number(tokenHeight.value) / Number(tokenCY.value);

@@ -173,6 +174,7 @@ previewZone.replaceChild(originImg, previewZone.children[1]);

} else { previewZone.appendChild(originImg); } + } } function reinitializeSpritePreview() {

@@ -189,11 +191,8 @@ tokenWidth.value = w;

tokenHeight.value = h; scaleSpritePreview(); } - if (previewZone.children.length) { - previewZone.replaceChild(img, previewZone.children[0]); - } else { - previewZone.appendChild(img); - } + previewZone.innerHTML = ""; + previewZone.appendChild(img); } function createToken() {

@@ -233,6 +232,46 @@ }

setErr("All token fields are required"); } +function destroyToken(id) { + const existing = tokens.find(t=>t.t.id == id); + if (existing) { + const self = Object.assign({}, existing.t); + self.active = false; + self.x = null; + self.y = null; + sendToken(self); + } +} + +function previewExistingToken(id) { + const existing = tokens.find(t=>t.t.id == id); + if (existing) { + tokenWidth.value = existing.t.w; + tokenHeight.value = existing.t.h; + tokenCX.value = existing.t.oX; + tokenCY.value = existing.t.oY; + tokenName.value = existing.t.name; + for (let i = 0; i < tokenSpriteDropdown.options.length; i++) { + if (tokenSpriteDropdown.options[i].value == existing.t.sprite) { + tokenSpriteDropdown.selectedIndex = i; + break; + } + } + reinitializeSpritePreview(); + } +} + +function renderTokenMasterList() { + if (tokenZone) { + let tokenMasterListHTML = "<label>Available Tokens</label><br/><ul class='single_btn_list'>"; + for (const t of tokens) { + tokenMasterListHTML += `<li><a href="#" onclick="previewExistingToken('${t.t.id}');return false">${t.t.name}</a><button onclick="destroyToken('${t.t.id}')">Destroy</button></li>\n`; + } + tokenMasterListHTML += "</ul>"; + tokenZone.innerHTML = tokenMasterListHTML; + } +} + function publishAuxMsg() { const txtArea = document.getElementById("auxMsgZone"); if (txtArea != null) {

@@ -246,12 +285,6 @@ }

function sendToken(t) { publish({token: t, auth: adminToken.access_token}); -} - -function revokeToken(t) { - t.x = null; - t.y = null; - sendToken(t); } async function uploadMapImg() {

@@ -391,6 +424,7 @@ if (adminToken) {

getTables(); adminWrapper.style.display="inline"; adminZone.style.display = "block"; + closeErr(); } else { setErr("Incorrect credentials"); }

@@ -430,11 +464,19 @@ }

} function setTokenCreateFormVisible(v) { - if (createTokenForm) { + if (createTokenForm && tokenZone) { + if (v) { + // clear the form when displaying because we may have values from a preview of an existing token + tokenWidth.value = ""; + tokenHeight.value = ""; + tokenCX.value = ""; + tokenCY.value = ""; + tokenName.value = ""; + tokenSpriteDropdown.selectedIndex = 0; + } createTokenForm.style.display = v ? "block" : "none"; - } - if (!v) { - // clear the form + tokenZone.style.display = v ? "none" : "inline"; + reinitializeSpritePreview(); } }
M static/index.htmlstatic/index.html

@@ -106,8 +106,8 @@ <label>cX<input type="number" id="token_cx" min="0" max="9999" onchange="previewSprite(this)"/></label><br/>

<label>cY<input type="number" id="token_cy" min="0" max="9999" onchange="previewSprite(this)"/></label><br/> <button type="submit" onclick="createToken()">Create</button> <button onclick="setTokenCreateFormVisible(false)">Cancel</button> - <div id="tokenPreview_zone"></div> </form> + <div id="tokenPreview_zone"></div> <div id="tokenZone"></div> </details><br/> <details id="admin_sprite_win" class="ui_win admin_win"><summary>sprites</summary>
M static/map.jsstatic/map.js

@@ -19,10 +19,8 @@ map.setMaxBounds(worldBounds);

if (init) { map.setView([0,0], 2); } - while (tokens.some(t=>t)) { - tokens[0].m.removeFrom(map); - tokens.shift(); - } + /* +*/ } // this works but assumes the map is square (reasonable limitation I think)

@@ -30,11 +28,58 @@ function resizeMarkers() {

tokens.forEach(t=>{ const icon = t.m.options.icon; const scaleFactor = mapImg._image.clientWidth / mapImg._image.naturalWidth; - icon.options.iconSize = [scaleFactor * t.sz[0], scaleFactor * t.sz[1]]; + icon.options.iconSize = [scaleFactor * t.t.w, scaleFactor * t.t.h]; + icon.options.iconAnchor = [scaleFactor * t.t.oX, scaleFactor * t.t.oY]; t.m.setIcon(icon); }); } +function processTokens(tokenChanges) { + for (const t of tokenChanges) { + const i = tokens.findIndex(tk=>tk.t.id == t.id); + if (i >= 0) { + const self = tokens[i]; + // token was made active + if (t.x != null && t.y != null && !self.t.active && t.active) { + self.t.active = true; + self.m.addTo(map); + // token was made inactive + } else if (t.x != null && t.y != null && self.t.active && !t.active) { + self.t.active = false; + self.m.removeFrom(map); + // token was destroyed + } else if (t.x == null && t.y == null) { + self.m.removeFrom(map); + tokens.splice(i, 1); + // token was moved + } else { + self.t.x = t.x; + self.t.y = t.y; + self.m.setLatLng([t.y, t.x]); + } + } else { + if (t.x != null && t.y != null) { + const self = NewToken(t); + tokens.push(self); + if (t.active) { + self.m.addTo(map); + } + } + } + } + resizeMarkers(); +} + +function toggleActive(tokenId) { + const existing = tokens.find(t=>t.t.id == tokenId); + if (existing) { + const self = Object.assign({}, existing.t); + self.active = !self.active; + console.log(self); + publish({token: self}); + } +} + function getCascadingPos() { const topLeft = [0,0]; const n = tokens.length;

@@ -43,19 +88,36 @@ topLeft[0] -= (n)*Math.random()*10 - 5;

return topLeft; } -function NewToken(w, h, oX, oY, img, name, x, y) { +function moveToken(id) { + const existing = tokens.find(t=>t.t.id == id); + if (existing) { + const self = Object.assign({}, existing.t); + const realPos = existing.m.getLatLng(); + self.x = realPos.lng; + self.y = realPos.lat; + console.log(self); + publish({token: self}); + } +} + +function NewToken(token) { + const marker = L.marker([token.y,token.x], { + icon: L.icon({ + iconUrl: token.sprite, + iconSize: [token.w,token.h], + iconAnchor: [token.oX, token.oY] + }), + title: token.name, + draggable: true, + autoPan: true + }); + + marker.on("moveend", ()=>{moveToken(token.id)}); + return { - sz: [w, h], - m: L.marker((x && y) ? [y,x] : getCascadingPos(), { - icon: L.icon({ - iconUrl: img, - iconSize: [w,h], - }), - title: name, - draggable: true, - autoPan: true - }), - }; + t: token, + m: marker, + }; } function addToken(token) {
M static/socket.jsstatic/socket.js

@@ -33,9 +33,9 @@

return p; } -function logDice(dice, many) { +function logDice(dice) { const diceLog = document.getElementById("dice_log"); - if (!many) { + if (!Array.isArray(dice)) { dice = [ dice ]; } else { if (diceLog) {

@@ -56,21 +56,47 @@ const auxDiv = document.getElementById("aux");

if (auxDiv) { auxDiv.innerText = msg; } +} + +function updateTokens(tokens) { + // update internal token array and map + processTokens(tokens); + // update token select window + renderTokenSelect(); + // if admin, update token master list + renderTokenMasterList(); +} + +function renderTokenSelect() { + const tokenSelect = document.getElementById("token_select"); + let tokenSelectHTML = "<ul class='single_btn_list'>"; + for (const t of tokens) { + tokenSelectHTML += `<li><a target="_blank" href="${t.t.sprite}">${t.t.name}</a><button onclick="toggleActive('${t.t.id}')">${(t.t.active ? "Deactivate" : "Activate")}</button></li>\n`; + } + tokenSelectHTML += "</ul>"; + tokenSelect.innerHTML = tokenSelectHTML; } function makeUpToDate(table) { if (table) { - if (table.diceRolls) { - logDice(table.diceRolls, true); - } else if (table.diceRoll) { - logDice(table.diceRoll, false); - } + // map image has to be set before tokens can be handled! if (table.mapImg) { setMapImg(table.mapImg); } if (table.auxMsg) { setAuxMsg(table.auxMsg); } + if (table.diceRolls) { + logDice(table.diceRolls); + } else if (table.diceRoll) { + logDice([table.diceRoll]); + } + if (table.tokens) { + updateTokens(table.tokens); + } else if (table.token) { + updateTokens([table.token]); + } + } }

@@ -109,6 +135,10 @@ if (tabletop) {

tabletop.style.display = "none"; } table = null; + while (tokens.some(t=>t)) { + tokens[0].m.removeFrom(map); + tokens.shift(); + } } }); conn.addEventListener("open", e => {

@@ -126,7 +156,7 @@

conn.addEventListener("message", e => { const data = JSON.parse(e.data); if (table == null) { - // first fetch comes from mongo, so the rolls array in each diceRoll is a byte array and needs to be decoded + // dicerolls are treated as a byte array when marshalling to json, so we have to decode them data.diceRolls.forEach(r=>{ r.roll = Uint8Array.from(atob(r.roll), c => c.charCodeAt(0)) })
M static/style.cssstatic/style.css

@@ -24,6 +24,7 @@ body {

background: url('./bg.png'); background-repeat: repeat; background-attachment: fixed; + font-family: sans-serif; } label {

@@ -150,6 +151,7 @@ }

.ui_win ul { max-height: 10em; + overflow: auto; } #admin_section {

@@ -159,6 +161,9 @@

.admin_win { } +.admin_win summary { + text-align: right; +} #map { position:fixed;

@@ -182,4 +187,14 @@ }

#tokenPreview_zone { position: relative; +} + +.single_btn_list li { + display: grid; + grid-template-columns: 1fr auto; +} + +.two_btn_list li { + display: grid; + grid-template-columns: 1fr auto auto; }
M static/util.jsstatic/util.js

@@ -20,7 +20,6 @@ function saveName() {

console.log("saving username"); const username = document.getElementById("name_entry"); if (username) { - console.log(username.value + "input found"); document.cookie = "username=" + username.value; } }