RolemasterUnified Offocial: Tracker significant improvements

Still not really usuable, but it's at a stage people can see
the basic stuff it can do.

- Add tracker back
- Add general "asCharacter" helpers
- Tracker:
  - Can add data using new as character
  - Characters now remember their tracker
  - for All Characters methods to tracker
  - Tracker can set everyones AP/round message
  - Tracker can roll everyones initiative (not to turn tracker though)
  - Track base attributes
- Add option for non-tracker initiative.
- Clean up build so compiled/processed files go to build
This commit is contained in:
Brett Nash
2026-01-12 08:17:37 -08:00
parent 79a3575f00
commit 9cf3f9c812
4 changed files with 398 additions and 28 deletions
@@ -1706,6 +1706,12 @@ div.attacks {
}
img.avatar {
width: 4em;
height: 4em;
}
.repitem:nth-of-type(even) .trackedcharacter {
background-color: var(--fancylightcolor);
}
+375 -27
View File
@@ -151,7 +151,7 @@
</div>
</rolltemplate>
<rolltemplate class='sheet-rolltemplate-rmutrackeradd'>
<rolltemplate class="sheet-rolltemplate-rmutrackeradd">
<div class='sheet-rolltemplate-rmumsg'>
<i>{{whom}}</i> added to template.
</div>
@@ -2617,7 +2617,7 @@ Lists Selected <input type='number' name='comp_cmbaselistcount'> / 6
<option value="13">Leviathan</option>
</select>
<select class='fancyselect' name='attr_type'>
<option value='sl'>Sling</option><option value='sh'>Shield Bash</option><option value='un'>Unarmed Strike</option><option value='sp'>Spear</option><option value='tc'>Two Handed Club</option><option value='sc'>Scimitar</option><option value='rp'>Rapier</option><option value='ra'>Ram</option><option value='cba'>Cold Ball</option><option value='fba'>Fire ball</option><option value='ta'>Thrown Hand Axe</option><option value='all'>Arming Sword</option><option value='ma'>Mace</option><option value='any'>Arming Sword</option><option value='px'>Pole Axe</option><option value='ja'>Javelin</option><option value='ls'>Long Sword</option><option value='bi'>Bite</option><option value='we'>Arming Sword</option><option value='fa'>Falchion</option><option value='be'>Beak</option><option value='bo'>Bola</option><option value='handcrossbow'>Hand Crossbow</option><option value='cu'>Club</option><option value='hc'>Heavy Crossbow</option><option value='da'>Dagger</option><option value='bs'>Broadsword</option><option value='tr'>Trample</option><option value='bx'>Battle Axe</option><option value='si'>Stinger</option><option value='tx'>Two Handed Axe</option><option value='wi'>Whip</option><option value='wbo'>Water Bolt</option><option value='wm'>War Mattock</option><option value='tw'>War Hammer</option><option value='qs'>Quarterstaff</option><option value='fbo'>Fire Bolt</option><option value='ebo'>Earth Ball</option><option value='hx'>Hand Axe</option><option value='tp'>Thrown Spear</option><option value='ts'>Two Handed Sword</option><option value='ibo'>Ice bolt</option><option value='td'>Thrown Dagger</option><option value='rw'>Arming Sword</option><option value='cl'>Claw</option><option value='lc'>Light Crossbow</option><option value='ss'>Short Sword</option><option value='lba'>Lightning Ball</option><option value='sb'>Bow, Short</option><option value='gr'>Grapple</option><option value='lb'>Bow, Long</option><option value='ro'>Thrown Rock</option><option value='rt'>Thrown Rock</option><option value='cr'>Crush</option><option value='as'>Arming Sword</option><option value='ho'>Horn</option><option value='lbo'>Ligtning Bolt</option><option value='hf'>Heavy Flail</option><option value='sw'>Sweep</option>
<option value='ss'>Short Sword</option><option value='fbo'>Fire Bolt</option><option value='qs'>Quarterstaff</option><option value='sw'>Sweep</option><option value='rw'>Arming Sword</option><option value='ibo'>Ice bolt</option><option value='ts'>Two Handed Sword</option><option value='ro'>Thrown Rock</option><option value='hf'>Heavy Flail</option><option value='wi'>Whip</option><option value='any'>Arming Sword</option><option value='si'>Stinger</option><option value='cr'>Crush</option><option value='lbo'>Ligtning Bolt</option><option value='lc'>Light Crossbow</option><option value='ra'>Ram</option><option value='gr'>Grapple</option><option value='ta'>Thrown Hand Axe</option><option value='bx'>Battle Axe</option><option value='tr'>Trample</option><option value='ma'>Mace</option><option value='we'>Arming Sword</option><option value='tc'>Two Handed Club</option><option value='ja'>Javelin</option><option value='hx'>Hand Axe</option><option value='lba'>Lightning Ball</option><option value='wm'>War Mattock</option><option value='fa'>Falchion</option><option value='tx'>Two Handed Axe</option><option value='bi'>Bite</option><option value='all'>Arming Sword</option><option value='ebo'>Earth Ball</option><option value='px'>Pole Axe</option><option value='da'>Dagger</option><option value='cba'>Cold Ball</option><option value='un'>Unarmed Strike</option><option value='rp'>Rapier</option><option value='sp'>Spear</option><option value='tp'>Thrown Spear</option><option value='tw'>War Hammer</option><option value='sl'>Sling</option><option value='cl'>Claw</option><option value='hc'>Heavy Crossbow</option><option value='wbo'>Water Bolt</option><option value='bs'>Broadsword</option><option value='as'>Arming Sword</option><option value='sc'>Scimitar</option><option value='ho'>Horn</option><option value='sb'>Bow, Short</option><option value='rt'>Thrown Rock</option><option value='fba'>Fire ball</option><option value='td'>Thrown Dagger</option><option value='cu'>Club</option><option value='bo'>Bola</option><option value='lb'>Bow, Long</option><option value='sh'>Shield Bash</option><option value='handcrossbow'>Hand Crossbow</option><option value='be'>Beak</option><option value='ls'>Long Sword</option>
</select>
@@ -3002,12 +3002,15 @@ Lists Selected <input type='number' name='comp_cmbaselistcount'> / 6
<!-- Toolbar -->
<div class='fancybox'>
<button type='action' name='act_trackeraddcreature' class='fancybutton'>Add</button>
<!--
<button type='action' name='act_trackerrefreshall' class='fancybutton disabled'>Refresh All</button>
<select class='fancyselect'>
<option>1 AP/Phase</option>
<option>2 AP/Phase</option>
<option>4 AP/Phase</option>
-->
<select class='fancyselect' name="attr_apperphase">
<option value="1">1 AP/Phase</option>
<option value="2">2 AP/Phase</option>
<option value="4">4 AP/Phase</option>
</select>
<button type='action' name='act_trackerrollinitiative' class='fancybutton'>Initiative</button>
</div>
@@ -3015,10 +3018,74 @@ Lists Selected <input type='number' name='comp_cmbaselistcount'> / 6
</div>
<fieldset class='repeating_trackercharacter'>
<div class='trackedcharacter'>
<fieldset class='repeating_trackercharacter'>
<div class="pcframebox">
<img alt="Token" class='avatar' name="attr_character_token">
<span name="attr_character_name"></span>
<span name="attr_hp"></span>
<span name="attr_hp_max"></span>
<div>
<input type="hidden" class="bar-value" name="attr_hp_percent" value="0">
<div class="bar-track"> <div class="bar-progress"></div> </div>
</div>
<span name="attr_pp"></span>
<span name="attr_pp_max"></span>
<div>
<input type="hidden" class="bar-value" name="attr_pp_percent" value="0">
<div class="bar-track"> <div class="bar-progress"></div> </div>
</div>
<select name="attr_actionselect" class="fancyselect">
<option value="melee">Melee</option>
<option value="missile">Missile</option>
<option value="spell">Spell</option>
<option value="perception">Perception</option>
<option value="eat">Eat</option>
<option value="stand">Stand</option>
<option value="pickup">Pickup Item</option>
<option value="mount">Mount</option>
<option value="dismount">Dismount</option>
<option value="stringbow">String Bow</option>
<option value="loadlight">Load Light Xbow</option>
<option value="loadheavy">Load Heavy Xbox</option>
<option value="fulldodge">Full Dodge</option>
<option value="fullblock">Full Block</option>
<option value="picklock ">Pick Lock</option>
<option value="other">Other</option>
</select>
<button type='action' name='act_deletetracked' class='fancybutton fancybuttoninline'>
<span style="font-family:Pictos;">#</span>
</button>
Chan <span name='attr_rr_channeling_bonus'></span>;
Ess <span name='attr_rr_essence_bonus'></span>;
Ment <span name='attr_rr_mentalism_bonus'></span>;
Arcane <span name='attr_rr_arcane_bonus'></span>;
Fear<span name='attr_rr_fear_bonus'></span>;
Phys <span name='attr_rr_physical_bonus'></span>;
AT (DB) <span name="attr_at"></span><span name="attr_activedb"></span>
Parry <span name="attr_parry_mod"></span>
Other <span name="attr_other_mod"></span>
DB Mod <span name="attr_dbmod"></span>
Last Init: <span name='attr_lastinitiative'></span>
</div>
</fieldset>
<fieldset class='repeating_trackercharacterX'>
<div class='trackedcharacter pcframebox'>
<div class='trackedname span2'>
<span name='attr_trackedname'></span>
<span name='attr_tokenid'></span>
<span name='attr_characterid'></span>
<button type='action' name='act_deletetracked' class='fancybutton fancybuttoninline'>
<span style="font-family:Pictos;">#</span>
</button>
@@ -3122,11 +3189,11 @@ Lists Selected <input type='number' name='comp_cmbaselistcount'> / 6
</div>
<div class='span2'>
Chan <span name='attr_trackedchannelingrr'></span>;
Ess <span name='attr_trackedessencerr'></span>;
Ment <span name='attr_trackedmentalismrr'></span>;
Fear<span name='attr_trackedfearrr'></span>;
Phys <span name='attr_trackedphysicalrr'></span>;
Chan <span name='attr_rr_channeling_bonus'></span>;
Ess <span name='attr_rr_essence_bonus'></span>;
Ment <span name='attr_rr_mentalism_bonus'></span>;
Fear<span name='attr_rr_fear_bonus'></span>;
Phys <span name='attr_rr_physical_bonus'></span>;
</div>
<div class='spanall'>
@@ -6683,8 +6750,12 @@ bleed and penalties you need side and location. A Z ends the list.<p>
<h3>Tokens</h3>
<p>Active Token ID: <input type="text" name="attr_activetokenid"> |
Target Character ID: <input type="text" name="attr_target_characterid"></p>
<p>Active Token ID: <input type="text" name="attr_activetokenid"> |
Target Character ID: <input type="text" name="attr_target_characterid"> |
Target Listeners: <input type="text" name="attr_target_listeners"> |
Tracker <span name='attr_trackerid'></span>
Tracker Prefix <span name='attr_trackerprefix'></span>
</p>
<hr>
@@ -6793,7 +6864,7 @@ Sheet Version: <span name='attr_version'></span>
<br>
Revision c87c395f3b03746b0e9462fe1b395a3196849103
Revision bd680cf8d34be84dc8dbebb09ccac98380a3450a
<hr>
@@ -8380,6 +8451,54 @@ function onCheck(str, func) {
on(str, func);
}
/** Sets the character ID and returns the current one.*/
function setActiveCharacterId(id) {
const ocid = getActiveCharacterId();
const data = {
id: '0',
type: 'setActiveCharacter',
data: id,
};
if (self.dispatchEvent) {
const event = new CustomEvent('message');
event.data = data;
self.dispatchEvent(event);
}
else {
self.onmessage({ data });
}
return ocid;
}
function asCharacter(id, func) {
const origid = setActiveCharacterId(id);
const rv = func()
setActiveCharacterId(origid);
return rv;
}
function rmuSetAttrsRemote(characterid, attrs, done) {
// FIXME: Validate the damn args
const myid = setActiveCharacterId(characterid);
if (myid == characterid) {
rmuerror("Set active and current character to the same", characterid);
}
rmuSetAttrs(attrs, done);
setActiveCharacterId(myid);
}
function rmuGetAttrsRemote(characterid, attrs, done) {
// FIXME: Validate the damn args
const myid = setActiveCharacterId(characterid);
if (myid == characterid) {
rmuerror("Set active and current character to the same", characterid);
}
rmuGetAttrs(attrs, done);
setActiveCharacterId(myid);
}
onCheck("clicked:resetworkers", () => {
pendingReset();
});
@@ -8445,6 +8564,7 @@ const knownproperties = {
// Roll20 Meta Stuff
selectedcharacterid: { type: "string" },
target_listeners: { type: "string" },
};
function validateAttributes(attrs) {
@@ -8502,13 +8622,13 @@ function VrmuSetAttrs(arg, fn) {
}
function rmuGetAttrs(toget, handler) {
validateAttributes(toget);
//validateAttributes(toget);
return getAttrs(toget,handler);
}
function rmuSetAttrs(toset, handler) {
validateAttributes(toset);
//validateAttributes(toset);
return setAttrs(toset, handler);
}
@@ -9023,7 +9143,7 @@ function lookupSkillResult(skill, skillinfo, umroll, total) {
update[stat.aname + "_info"] = logmessage;
// Update local cache
statCache_[stat.abbr] = statBonus;
setAttrs(update);
//setAttrs(update);
});
function updateAllSkills() {
@@ -18698,6 +18818,7 @@ function doRollInitiative() {
finishRoll(roll.rollId, {});
const activetokenid = attacks.getDataFromRoll(roll.results.activetokenid.expression);
const selectedcharacterid = attacks.getDataFromRoll(roll.results.selectedcharacterid.expression);
rmuSetAttrs({lastinitiative: roll.results.total.result});
const mycharacterid = getActiveCharacterId()
// So are we the selected token? If so, update the token id
console.log("Selected Char ID/My Char ID/Active Token ID",selectedcharacterid, mycharacterid, activetokenid)
@@ -18706,10 +18827,25 @@ function doRollInitiative() {
rmuSetAttrs({ activetokenid : activetokenid });
console.log("My tokenid is", activetokenid);
}
});
}
function doRollInitiativeNoTracker() {
// roll20 treats floor as round to negative infinity, so ceil since they should be negative
startRoll('@{whispertype} &{template:rmuinit} [[ [[ 2d10 ]] + @{initiative} + [[ceil(@{statuspenalty} / 10)]] ]] ' +
'{\{actor=@{character_name} }} ' +
'{\{bonus=@{initiative} }} ' +
'{\{total=$[[2]]}} {\{roll=$[[0]]}} ' +
'{\{maneuver=$[[1]] }}',
(roll) => {
rmuSetAttrs({lastinitiative: roll.results.total.result});
finishRoll(roll.rollId, {});
});
}
/**/
function updatetokenmarkers() {
console.log("updatetokenmarkers start");
@@ -20544,6 +20680,41 @@ function messageHackery() {
let tracker = {}
tracker.repeatingprefix = "repeating_trackercharacter";
/*
Movement
Attacks
Movement skills
Footwork
Spells.
Initiative
Perception*/
tracker.allAttributes = [
"hp", "hp_max", "pp", "pp_max",
"hp_percent", "pp_percent",
"character_token",
"character_name",
"actionselect",
"aptrack_loaded",
"aptrack",
"lastinitiative",
"rr_channeling_bonus",
"rr_arcane_bonus",
"rr_essence_bonus",
"rr_mentalism_bonus",
"rr_fear_bonus",
"rr_physical_bonus",
"parry_mod",
"other_mod",
"dbmod",
"activedb",
"at"
]
onCheck("clicked:trackedreturntocharacter", () => {
setAttrs({sheetselect: "playersheet"});
});
@@ -20553,29 +20724,114 @@ onCheck("clicked:trackerinit", () => {
setAttrs({sheetselect: "trackersheet"});
});
// Somone clicked the add target button. so lets add them
// We just get the token_id and the character_id first.
// Then we start getting all the other fields.
onCheck("clicked:trackeraddcreature", () =>{
tracker.dataGet('target', generateRowID());
// tracker.dataGet('target', generateRowID());
const roll_string = "&{template:rmutrackeradd} [[1d20]] " +
"{\{who=Tracker}} " +
"{\{targettokenid=[[0[@{target|token_id}]]]}} " +
"{\{targetname=[[0[@{target|character_name}]]]}} " +
"{\{targetcharacterid=[[0[@{target|character_id}]]]}}";
console.log(roll_string);
startRoll(roll_string, roll => {
console.log(roll);
const tokenid = attacks.getDataFromRoll(roll.results.targettokenid.expression);
const characterid = attacks.getDataFromRoll(roll.results.targetcharacterid.expression);
const targetname = attacks.getDataFromRoll(roll.results.targetname.expression);
const updates = { targetname: targetname };
console.log("Got tokenid and characterid", tokenid, characterid, roll);
tracker.addTarget(targetname, characterid, tokenid);
finishRoll(roll.rollId, updates);
});
});
onCheck("clicked:repeating_trackercharacter:trackedrefresh", (ev) =>{
const basename = ev.sourceAttribute.replace("_trackedrefresh", "");
let rowid = basename.match("-.*");
getAttrs([`${basename}_trackedname`], (data) => {
tracker.dataGet(data[`${basename}_trackedname`], rowid);
tracker.appendPrefix = function(prefix, object) {
return Object.fromEntries(
Object.entries(object).map(([key, value]) => [prefix + key, value])
);
}
// So when the AP per phase here triggers, set all the sheets
// This si one way - tracker sets teh other sheets.
onCheck("change:apperphase", (ev) => {
console.log(ev)
tracker.setAllSheets({actionapperphase:ev.newValue});
});
/**
* This is the main call to add items to the tracker.
*/
// FIXME: Should check for duplicates
// FIXME: Should warn if no character id
tracker.addTarget = function(name, characterid, tokenid) {
const rowid = generateRowID();
const prefix = `repeating_trackercharacter_${rowid}_`;
// First pass: Lets just add the basic item
const updates = {}
updates.trackedname = name;
updates.tokenid = tokenid;
updates.characterid = characterid;
console.log(tracker.appendPrefix(prefix, updates));
setAttrs(tracker.appendPrefix(prefix, updates));
const trackerid = getActiveCharacterId();
// So as the character I need to let it know what tracker it has
rmuSetAttrsRemote(characterid, {trackerid: trackerid, trackerprefix: prefix});
rmuGetAttrsRemote(characterid, tracker.allAttributes, (data) => {
// this call back is in the context of the character.
rmuSetAttrsRemote(trackerid, tracker.appendPrefix(prefix, data));
});
}
tracker.setAllSheets = function(values) {
tracker.forAllSheets((id) => {
rmuSetAttrsRemote(id, values);
});
}
tracker.forAllSheets = function(func) {
getSectionIDs(tracker.repeatingprefix, (ids) => {
const toget = ids.map((x) => tracker.repeatingprefix + "_" + x + "_characterid");
getAttrs(toget, (characterids) => {
for (row in characterids) {
func(characterids[row])
}
});
});
}
onCheck("clicked:trackerrollinitiative", (_) => {
tracker.forAllSheets((id) => {
asCharacter(id, () => {doRollInitiativeNoTracker()});
});
});
// old call
onCheck("clicked:repeating_trackercharacter:deletetracked", (ev) => {
const basename = ev.sourceAttribute.replace("_deletetracked", "");
removeRepeatingRow(basename);
});
// old call; now borked
onCheck("clicked:repeating_trackercharacter:selecttarget", (ev) => {
// We are keeping the last '_' as getTargetData doesn;t know about it
const basename = ev.sourceAttribute.replace("selecttarget", "");
attacks.getTargetData("target", basename)
});
});
// old call
const fields = {
// Type is the type of data.
// Field is what we pull on the source character
@@ -20598,6 +20854,7 @@ const fields = {
}
/**/
// old call
tracker.extractResults = function(roll, prefix) {
const results = {}
const rr = roll.results;
@@ -20620,6 +20877,7 @@ tracker.extractResults = function(roll, prefix) {
return results;
}
// old call
tracker.buildString = function(whom) {
let reqs = ["&{template:rmuattacktarget} [[1d20]]"];
for (const field in fields) {
@@ -20636,6 +20894,7 @@ tracker.buildString = function(whom) {
}
// old call
tracker.dataGet = function(whom, rowid) {
const roll_string = tracker.buildString(whom);
startRoll(roll_string, roll => {
@@ -20655,6 +20914,7 @@ tracker.dataGet = function(whom, rowid) {
}
// old call
tracker.handleSkillString = function(string, updates) {
console.log("Handle skill string", string);
if (!string || string == '') return;
@@ -20675,6 +20935,7 @@ tracker.handleSkillString = function(string, updates) {
// updates[`repeat_trackedskill${rowid}_bonus`] = match[2];
}
}
// old call
tracker.handleMoveString = function(string, updates) {
console.log("Handle move string", string);
if (!string || string == '') return;
@@ -20694,6 +20955,7 @@ tracker.handleMoveString = function(string, updates) {
}
}
// old call
tracker.handleAttackString = function(string, updates) {
console.log("Handle attack string", string);
if (!string || string == '') return;
@@ -20706,6 +20968,33 @@ tracker.handleAttackString = function(string, updates) {
}
/**
* Sheet Events
*
* These should all be triggered in the context of the sheet.
* And then send the data to the tracker, if any.
*/
onCheck("change:lastinitiative", () => {
getAttrs(["lastinitiative", "trackerid", "trackerprefix"], (data) => {
if (data.trackerid && data.trackerprefix) {
rmuSetAttrsRemote(data.trackerid,
{ [data.trackerprefix + "lastinitiative"] : data.lastinitiative });
}
});
});
for (attr of tracker.allAttributes) {
onCheck("change:" + attr, (ev) => {
getAttrs(["trackerid", "trackerprefix"], (data) => {
if (data.trackerid && data.trackerprefix) {
rmuSetAttrsRemote(data.trackerid,
{ [data.trackerprefix + ev.sourceAttribute] : ev.newValue }
);
}
});
});
}
@@ -20807,6 +21096,65 @@ onCheck("clicked:initiative-action", (ev) => {
// vim: set sw=2 sts=2 syn=javascript :
/**/
const target = {}
onCheck("change:target_characterid", (change) => {
console.log("Change of target characgter id", change);
if (change.newValue == change.previousValue) {
return;
}
let oldidtarget = change.oldValue;
let newidtarget = change.newValue;
const myid = getActiveCharacterId();
// Remove myself from the old list
/*
rmuGetAttrsRemote(oldidtarget, ["target_listeners"], (remote) => {
const result = remote.target_listeners
.split(",")
.filter(item => item !== myid)
.join(",");
setAttrs({target_listeners: result});
});*/
// Add myself to the new list
rmuGetAttrsRemote(newidtarget, ["target_listeners"], (remote) => {
let result = remote.target_listeners
if (result) {
result += ',' + myid;
} else {
result = myid;
}
setAttrs({target_listeners: result})
});
});
onCheck("change:db", (db) => {
getAttrs(["target_listeners"], (listen) => {
console.log("db change", db, listen);
if (!listen.target_listeners || listen.target_listeners.length < 2) {
console.log("Seems empty; stopping");
return;
}
listen.target_listeners.split(",").forEach((who) => {
rmuSetAttrsRemote(who, {target_db: db.newValue});
});
});
});
/* vim:set sw=4 sts=4 syn=javascript : */
function getStatFromAName(aname) {
+1 -1
View File
@@ -123,5 +123,5 @@
"description": "Set the version of the sheet in use. You typically want latest"
}
],
"version": "1767856195"
"version": "1768234574"
}
+16
View File
@@ -1,6 +1,22 @@
# 2026-01-15
- Add tracker back
- Add general "asCharacter" helpers
- Tracker:
- Can add data using new as character
- Characters now remember their tracker
- for All Characters methods to tracker
- Tracker can set everyones AP/round message
- Tracker can roll everyones initiative (not to turn tracker though)
- Track base attributes
- Add option for non-tracker initiative.
- Clean up build so compiled/processed files go to build
# 2026-01-08
- Fix for creatures max PP
- Fix fetch for targetted creatures
- Partial fix for grapple
# 2026-01-06