forked from GitHub/virtualtabletop
Add scoreboard widget (#1381)
* First working version of scoreboard * Fix computation of scores in some cases. * Add horizontal player display. Rewrite table building code, rewrite css. * Sort player columns/rows, default values for round names. * Allow sorting on any attribute of seat, as well as 'total'. * Set the width/height widget properties instead of DOM property. * First implementation of scrolling. * Add scrolling w/fixed row, column. * Sticky bottom row, allow unseated seats * Fix z-index of 'Total', customize round display. * Actually make showAllRounds work. Doh. * Fix empty scoreboard, customize Round text, fix sortField. * Implement `currentRound`. * Change default for `sortField` and add JSON editor support. * Add read-only property _totals * Fix sorting crash in certain circumstances. * Implement SCORE, with JSON edit support. * Add `"round": "total"` to `SCORE`. * Add teams processing. * Change `addTotals` to `showTotals` * Fix crash for empty score array. * Fix a number of issues. * Make sure that each team is an array. * Oops. * Revised definition of SCORE. * Clean up handling of teams. * Fix reported crash. * Make scoreboard corners rounded. * Change JSON editor for new SCORE parameters. * Change rounding strategy for scoreboard in css. * Fix children removed from DOM * store table in `this.tableDOM` and clear that element instead of everything * Change ScoreBoard to Scoreboard * Move table height computation into CSS. * Fix sort functions. * Move sticky rows/cols into CSS; determine header text color based on background. * Erase table on invalid 'seats' parameter; also allow "seats": "Seat1" * Fix read-only property processing for scoreboard. * Fix foreground color computation for named colors. * Fix sort routine to work with unnamed players, some other code reorg. * includeAllSeats -> showAllSeats, usePlayerColors -> showPlayerColors, 'None' -> '-' * Add 'autosizeColumns' to allow automatic equalization of column sizes. This is on by default. * Change scoreboard css to make autosizing work better. * Various code cleanup to respond to comments. * Remove display: flex in the scoreboard css. * Fix firstColWidth problem in css * Fix error in team score computation. * Now non-numeric scores are treated as zero but are shown in the display. * Oops. * inserted an intermediate div so that children are not hidden * Fix issue with non-array scores. * Remove height setting on table to allow white space at bottom. * added manual input overlay when clicking a scoreboard * catch needs a variable apparently * removed unnecessary CSS properties * removed cellType parameter from addRowToTable * renamed getHexColor to toRGB * empty the table first to avoid duplicated code * moved scoreboard update to updateLinkedWidgets * made intermediate div its own stacking context * fixed indentation * turn the score into a number * Apply background-color to root widget and inherit border-radius. * Don't use applyDeltaToDOM(null) to update score table. * Minor code quality changes. * Now team-based scoreboards will list all players on each team when editing the scoreboard. * Seats are now ordered in editor as they are in scoreboard. Also seats inside teams are ordered by index. Finally, scalar scores are no longer converted to arrays. * Fix totals-only scoreboards. Also move click routine to the end. * Use new contrastAnyColor function * Remove getFgColor, replace calls by calls to contrastAnyColor. * Add Hearts with new scoreboard * Add Scoreboard tutorial with 3 variants * Add SCORE tutorial with 1 variant * Update Tittle with scoreboard * Tittle-add reset scoreboard function to restart button * Add scoreboard to Wordy * puts click in correct place * Wordy-add turn capability into scoreboard * Tittle-add turn capability to scoreboard * Fix typo in Tittle * Fix typo in Wordy Co-authored-by: 96LawDawg <76912527+96LawDawg@users.noreply.github.com> Co-authored-by: MICHAEL W TAYLOR <pafbadc@gmail.com> Co-authored-by: ArnoldSmith86 <arnold.smith.86@mail.ru> Co-authored-by: RaphaelAlvez <73476656+RaphaelAlvez@users.noreply.github.com> Co-authored-by: Robert Flack <flackr@gmail.com>
This commit is contained in:
parent
8c5c65415c
commit
fa0ad654ee
8
.gitconfig
Normal file
8
.gitconfig
Normal file
@ -0,0 +1,8 @@
|
||||
[user]
|
||||
name = 96LawDawg
|
||||
email = 76912527+96LawDawg@users.noreply.github.com
|
||||
[filter "lfs"]
|
||||
clean = git-lfs clean -- %f
|
||||
smudge = git-lfs smudge -- %f
|
||||
process = git-lfs filter-process
|
||||
required = true
|
68
client/css/widgets/scoreboard.css
Normal file
68
client/css/widgets/scoreboard.css
Normal file
@ -0,0 +1,68 @@
|
||||
.scoreboard {
|
||||
background: #fff;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.scoreboard > div.scoreboardIntermediate {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
border: 2px solid black;
|
||||
box-sizing: border-box;
|
||||
border-radius: inherit;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scoreboard > div.scoreboardIntermediate > table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scoreboard > div.scoreboardIntermediate > table tbody td {
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-right: 1px solid #ddd;
|
||||
padding: 5px;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.scoreboard.equalWidth > div.scoreboardIntermediate > table td {
|
||||
white-space: normal;
|
||||
width: calc((100% - var(--firstColWidth)) / (var(--columns) - 1));
|
||||
}
|
||||
|
||||
.scoreboard > div.scoreboardIntermediate > table tbody tr:nth-child(1) td:nth-child(1) {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.scoreboard > div.scoreboardIntermediate > table tbody tr:nth-child(1) td {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
background-color: #ddd;
|
||||
word-break: break-all;
|
||||
text-align:center;
|
||||
}
|
||||
|
||||
.scoreboard > div.scoreboardIntermediate > table tbody tr td:nth-child(1) {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
background-color: #ddd;
|
||||
word-break: break-all;
|
||||
text-align: center;
|
||||
min-width: var(--firstColWidth);
|
||||
}
|
||||
|
||||
.scoreboard > div.scoreboardIntermediate > table .currentRound {
|
||||
background-color: #fdf5e6;
|
||||
}
|
||||
|
||||
.scoreboard > div.scoreboardIntermediate > table .totalsLine {
|
||||
background-color: #ddd;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
}
|
||||
|
@ -786,6 +786,8 @@ function populateAddWidgetOverlay() {
|
||||
x: 1000,
|
||||
y: 600
|
||||
});
|
||||
|
||||
/* Note that the button to add a scoreboard is in room.html */
|
||||
}
|
||||
// end of JSON generators
|
||||
|
||||
@ -1194,6 +1196,15 @@ onLoad(function() {
|
||||
showOverlay();
|
||||
});
|
||||
|
||||
on('#addScoreboard', 'click', async function() {
|
||||
await addWidgetLocal({
|
||||
type: 'scoreboard',
|
||||
x: 1000,
|
||||
y:660
|
||||
});
|
||||
showOverlay();
|
||||
});
|
||||
|
||||
on('#uploadBoard', 'click', _=>uploadWidget('board'));
|
||||
on('#uploadToken', 'click', _=>uploadWidget('token'));
|
||||
|
||||
|
@ -877,6 +877,7 @@ function jeAddCommands() {
|
||||
widgetTypes.push(jeAddWidgetPropertyCommands(new Holder()));
|
||||
widgetTypes.push(jeAddWidgetPropertyCommands(new Label()));
|
||||
widgetTypes.push(jeAddWidgetPropertyCommands(new Pile()));
|
||||
widgetTypes.push(jeAddWidgetPropertyCommands(new Scoreboard()));
|
||||
widgetTypes.push(jeAddWidgetPropertyCommands(new Seat()));
|
||||
widgetTypes.push(jeAddWidgetPropertyCommands(new Spinner()));
|
||||
widgetTypes.push(jeAddWidgetPropertyCommands(new Timer()));
|
||||
@ -898,6 +899,7 @@ function jeAddCommands() {
|
||||
jeAddRoutineOperationCommands('MOVEXY', { count: 1, face: null, from: null, x: 0, y: 0, snapToGrid: true });
|
||||
jeAddRoutineOperationCommands('RECALL', { owned: true, holder: null });
|
||||
jeAddRoutineOperationCommands('ROTATE', { count: 1, angle: 90, mode: 'add', holder: null, collection: 'DEFAULT' });
|
||||
jeAddRoutineOperationCommands('SCORE', { mode: 'set', property: 'score', seats: null, round: null, value: null });
|
||||
jeAddRoutineOperationCommands('SELECT', { type: 'all', property: 'parent', relation: '==', value: null, max: 999999, collection: 'DEFAULT', mode: 'set', source: 'all', sortBy: '###SEE jeAddRoutineOperation###'});
|
||||
jeAddRoutineOperationCommands('SET', { collection: 'DEFAULT', property: 'parent', relation: '=', value: null });
|
||||
jeAddRoutineOperationCommands('SHUFFLE', { holder: null, collection: 'DEFAULT' });
|
||||
@ -949,6 +951,7 @@ function jeAddCommands() {
|
||||
jeAddEnumCommands('^.*\\(LABEL\\) ↦ mode', [ 'set', 'dec', 'inc', 'append' ]);
|
||||
jeAddEnumCommands('^.*\\(ROTATE\\) ↦ angle', [ 45, 60, 90, 135, 180 ]);
|
||||
jeAddEnumCommands('^.*\\(ROTATE\\) ↦ mode', [ 'set', 'add' ]);
|
||||
jeAddEnumCommands('^.*\\(SCORE\\) ↦ mode', [ 'set', 'inc', 'dec' ]);
|
||||
jeAddEnumCommands('^.*\\(SELECT\\) ↦ mode', [ 'set', 'add', 'remove', 'intersect' ]);
|
||||
jeAddEnumCommands('^.*\\(SELECT\\) ↦ relation', [ '<', '<=', '==', '!=', '>', '>=', 'in' ]);
|
||||
jeAddEnumCommands('^.*\\(SELECT\\) ↦ type', widgetTypes);
|
||||
@ -961,6 +964,7 @@ function jeAddCommands() {
|
||||
jeAddEnumCommands('^.*\\((CLICK|COUNT|DELETE|FLIP|GET|LABEL|ROTATE|SET|SORT|SHUFFLE|TIMER)\\) ↦ collection', collectionNames.slice(1));
|
||||
jeAddEnumCommands('^.*\\(CLONE\\) ↦ source', collectionNames.slice(1));
|
||||
jeAddEnumCommands('^.*\\((SELECT|TURN)\\) ↦ source', collectionNames);
|
||||
jeAddEnumCommands('^scoreboard ↦ sortField',['index', 'player', 'total']);
|
||||
|
||||
jeAddNumberCommand('increment number', '+', x=>x+1);
|
||||
jeAddNumberCommand('decrement number', '-', x=>x-1);
|
||||
|
@ -58,6 +58,8 @@ export function addWidget(widget, instance) {
|
||||
w = new Label(id);
|
||||
} else if(widget.type == 'pile') {
|
||||
w = new Pile(id);
|
||||
} else if(widget.type == 'scoreboard') {
|
||||
w = new Scoreboard(id);
|
||||
} else if(widget.type == 'seat') {
|
||||
w = new Seat(id);
|
||||
} else if(widget.type == 'spinner') {
|
||||
|
366
client/js/widgets/scoreboard.js
Normal file
366
client/js/widgets/scoreboard.js
Normal file
@ -0,0 +1,366 @@
|
||||
import { $, $a, removeFromDOM, asArray, escapeID } from '../domhelpers.js';
|
||||
import { Widget } from './widget.js';
|
||||
|
||||
class Scoreboard extends Widget {
|
||||
constructor(object, surface) {
|
||||
super(object, surface);
|
||||
|
||||
this.addDefaults({
|
||||
movable: true,
|
||||
width: 300,
|
||||
height: 200,
|
||||
layer: -1,
|
||||
typeClasses: 'widget scoreboard',
|
||||
playersInColumns: true,
|
||||
rounds: null,
|
||||
roundLabel: 'Round',
|
||||
scoreProperty: 'score',
|
||||
seats: null,
|
||||
showAllRounds: false,
|
||||
showAllSeats: false,
|
||||
showPlayerColors: true,
|
||||
showTotals: true,
|
||||
sortField: 'index',
|
||||
sortAscending: true,
|
||||
currentRound: null,
|
||||
autosizeColumns: true,
|
||||
borderRadius: 8,
|
||||
editPaneTitle: 'Set score'
|
||||
});
|
||||
}
|
||||
|
||||
applyDeltaToDOM(delta) {
|
||||
super.applyDeltaToDOM(delta);
|
||||
this.updateTable();
|
||||
}
|
||||
|
||||
classes() {
|
||||
let className = super.classes();
|
||||
|
||||
if(this.get('autosizeColumns'))
|
||||
className += ' equalWidth';
|
||||
|
||||
return className;
|
||||
}
|
||||
|
||||
classesProperties() {
|
||||
const p = super.classesProperties();
|
||||
p.push('autosizeColumns');
|
||||
return p;
|
||||
}
|
||||
|
||||
async click(mode='respect') {
|
||||
if(!await super.click(mode)) {
|
||||
const scoreProperty = this.get('scoreProperty');
|
||||
const seats = this.getIncludedSeats();
|
||||
let players = [];
|
||||
if(Array.isArray(seats))
|
||||
players = seats.map(function(s) { return { value: s.get('id'), text: s.get('player') || '-' }; });
|
||||
else { // Teams
|
||||
for (const team in seats)
|
||||
players = players.concat(seats[team].map(function(s) { return { value: s.get('id'), text: `${s.get('player') || '-'} (${team})` } }))
|
||||
}
|
||||
|
||||
let rounds = this.getRounds(seats, scoreProperty, 1).map(function(r, i) { return { text: r, value: i+1 }; });
|
||||
|
||||
if(this.totalsOnly)
|
||||
rounds = [{text: 'total', value: 0}];
|
||||
|
||||
if(!players.length || !rounds.length)
|
||||
return;
|
||||
|
||||
try {
|
||||
const result = await this.showInputOverlay({
|
||||
header: this.get('editPaneTitle'),
|
||||
fields: [
|
||||
{
|
||||
type: 'select',
|
||||
label: 'Player',
|
||||
options: players,
|
||||
variable: 'player'
|
||||
},
|
||||
{
|
||||
type: 'select',
|
||||
label: this.get('roundLabel'),
|
||||
options: rounds,
|
||||
variable: 'round'
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
label: 'Value',
|
||||
variable: 'score'
|
||||
}
|
||||
]
|
||||
});
|
||||
const seat = widgets.get(result.player);
|
||||
let scores = seat.get(scoreProperty);
|
||||
if(!this.totalsOnly) {
|
||||
scores = [...scores];
|
||||
scores[result.round-1] = +result.score;
|
||||
} else
|
||||
scores = +result.score;
|
||||
await seat.set(scoreProperty, scores);
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
get(property) {
|
||||
if(property != '_totals')
|
||||
return super.get(property)
|
||||
else {
|
||||
// First get total score for each relevant seat
|
||||
const totals = [];
|
||||
const seats = this.getIncludedSeats();
|
||||
if(Array.isArray(seats)) {// Getting seat totals
|
||||
for (const seat of seats) {
|
||||
const score = seat.get(this.get('scoreProperty'));
|
||||
const index = seat.get('index');
|
||||
totals[index] = this.getTotal(score);
|
||||
}
|
||||
return totals
|
||||
} else if (typeof seats == 'object') { // Getting team totals
|
||||
const teamTotals = [null];
|
||||
for (const team in seats) {
|
||||
const seatsInTeam = widgetFilter(w => w.get('type') == 'seat' && seats[team].includes(w));
|
||||
const seatsScores = seats[team].map(w => w.get(this.get('scoreProperty')));
|
||||
const seatsTotals = asArray(seatsScores).map( s => this.getTotal(s) );
|
||||
teamTotals.push(this.getTotal(seatsTotals));
|
||||
}
|
||||
return teamTotals;
|
||||
}
|
||||
return null; // Neither array nor object, return null.
|
||||
}
|
||||
}
|
||||
|
||||
readOnlyProperties() {
|
||||
return new Set([...super.readOnlyProperties(), '_totals']);
|
||||
}
|
||||
|
||||
// Return a modified array or object, structured as with the 'seats' property,
|
||||
// including the seat widgets (not just the seat ids) to actually be used.
|
||||
// The returned array will be sorted as requested by the widget. For teams, players
|
||||
// will be sorted by player name within each team, and the teams will be sorted as
|
||||
// shown in the scoreboard (i.e., as given in the scoreboard's property).
|
||||
getIncludedSeats() {
|
||||
const showTotals = this.get('showTotals');
|
||||
const scoreProperty = this.get('scoreProperty');
|
||||
let sortField = this.get('sortField');
|
||||
|
||||
let seats = this.get('seats');
|
||||
if(typeof seats == 'string') // Allow "seats": "Seat1"
|
||||
seats = asArray(seats);
|
||||
if(Array.isArray(seats) || seats === null) { // Scoreboard just using seats
|
||||
const seatList = [...widgetFilter(w => w.get('type') == 'seat' && (this.get('showAllSeats') || w.get('player')) && (!seats || seats.includes(w.get('id'))))];
|
||||
// Sort player scores as requested
|
||||
if(sortField == 'total' && !showTotals) // Use default sort if no totals
|
||||
sortField = 'index';
|
||||
if(sortField == 'total')
|
||||
seatList.sort((a,b) => this.getTotal(a.get(scoreProperty)) - this.getTotal(b.get(scoreProperty)))
|
||||
else
|
||||
seatList.sort((a,b) => {
|
||||
const pa = a.get(sortField);
|
||||
const pb = b.get(sortField);
|
||||
return pa < pb ? -1 : pa > pb ? 1 : 0; // These need not be numeric
|
||||
});
|
||||
if(!this.get('sortAscending'))
|
||||
seatList.reverse();
|
||||
return seatList;
|
||||
} else if(typeof seats == 'object') { // Scoreboard using teams
|
||||
const teamList = {};
|
||||
for (const team in seats) {
|
||||
teamList[team] = [... widgetFilter(w => w.get('type') == 'seat' && (this.get('showAllSeats') || w.get('player')) && asArray(seats[team]).includes(w.get('id')))];
|
||||
teamList[team].sort((a,b) => a.get('index') - b.get('index'));
|
||||
if (!this.get('sortAscending'))
|
||||
teamList[team].reverse()
|
||||
}
|
||||
return teamList;
|
||||
} else // 'seats' property is not array or object, return null to do nothing further
|
||||
return null;
|
||||
}
|
||||
|
||||
// Compute number of scoring rounds to show and create round names table
|
||||
getRounds(seats, scoreProperty, addEmptyRounds=0) {
|
||||
let rounds = this.get('rounds'); // User-supplied round names
|
||||
let numRounds=0;
|
||||
this.totalsOnly = true;
|
||||
const arrayOfSeats = Array.isArray(seats) ? seats : Object.keys(seats).reduce((union,key) => union.concat(seats[key]), []);
|
||||
for (let i=0; i < arrayOfSeats.length; i++) {
|
||||
const score = arrayOfSeats[i].get(scoreProperty);
|
||||
if(Array.isArray(score) && score.length > numRounds)
|
||||
numRounds = score.length;
|
||||
if(Array.isArray(score))
|
||||
this.totalsOnly = false;
|
||||
}
|
||||
if(this.get('showAllRounds') && Array.isArray(rounds))
|
||||
numRounds = Math.max(rounds.length, numRounds);
|
||||
else if (!this.totalsOnly)
|
||||
numRounds += addEmptyRounds;
|
||||
|
||||
if(Array.isArray(rounds))
|
||||
rounds = rounds.concat(Array(numRounds).fill('')).slice(0,numRounds);
|
||||
else
|
||||
rounds = [...Array(numRounds).keys()].map(i => i+1);
|
||||
return rounds;
|
||||
}
|
||||
|
||||
getTotal(x) {
|
||||
return asArray(x).reduce((partialSum, a) => partialSum + (parseFloat(a) || 0), 0)
|
||||
}
|
||||
|
||||
addRowToTable(parent, values) {
|
||||
const tr = parent.insertRow();
|
||||
const v = asArray(values);
|
||||
tr.innerHTML = Array(values.length).fill('<td></td>').join('');
|
||||
for (let i=0; i < values.length; i++)
|
||||
$a('td', tr)[i].innerText = values[i];
|
||||
return tr;
|
||||
}
|
||||
|
||||
updateTable() {
|
||||
/* This routine creates the HTML table for display in the scoreboard. It is
|
||||
* complicated by the fact that the `seats` property can be either an array of
|
||||
* seat IDs or an object whose keys are team names and each of whose values is an
|
||||
* array of seat IDs.
|
||||
* There are two major sections: the first computes the pScores array, which contains
|
||||
* the array of scores, either for seats or for teams. The second section uses the
|
||||
* pScores array to construct the HTML table.
|
||||
* There are lots of other things going on, to get the totals line, round names, etc
|
||||
* correct.
|
||||
*/
|
||||
|
||||
const seats = this.getIncludedSeats();
|
||||
// First, empty the table
|
||||
if(!this.tableDOM) {
|
||||
this.tableDOM = document.createElement('table');
|
||||
const intermediateDiv = document.createElement('div');
|
||||
intermediateDiv.className = 'scoreboardIntermediate';
|
||||
this.domElement.appendChild(intermediateDiv);
|
||||
intermediateDiv.appendChild(this.tableDOM);
|
||||
} else {
|
||||
this.tableDOM.innerHTML = '';
|
||||
}
|
||||
|
||||
// Just return if no seats were specified.
|
||||
// We choose here to regard a result of [] or {} as a valid set of seats/teams with no entries.
|
||||
if(seats===null)
|
||||
return
|
||||
|
||||
const showTotals = this.get('showTotals');
|
||||
const scoreProperty = this.get('scoreProperty');
|
||||
let sortField = this.get('sortField');
|
||||
|
||||
// Compute number of scoring rounds to show and create round names table
|
||||
let rounds = this.getRounds(seats, scoreProperty);
|
||||
let numRounds = rounds.length;
|
||||
if(showTotals)
|
||||
rounds.push('Totals');
|
||||
rounds.unshift(this.get('roundLabel'));
|
||||
|
||||
// Fill scores array. pScores[i][0] is player name or team name, last is total
|
||||
// (or last score if showTotals is false)
|
||||
let pScores = [];
|
||||
if(Array.isArray(seats)) { // Show individual seats
|
||||
// Fill player score array, totals array. This will work properly for totals-only.
|
||||
for (let i=0; i < seats.length; i++) {
|
||||
const score = seats[i].get(scoreProperty);
|
||||
pScores[i] = Array.isArray(score) ? [...score] : [];
|
||||
pScores[i] = pScores[i].concat(Array(numRounds).fill('')).slice(0,numRounds);
|
||||
// Add totals if requested, and player name.
|
||||
if(showTotals)
|
||||
pScores[i].push(this.getTotal(score)); // Use 'score' instead of 'pScores[i]' here b/c of scalars.
|
||||
pScores[i].unshift(seats[i].get('player') || '-');
|
||||
}
|
||||
|
||||
} else if(typeof seats == 'object') { // Display team scores
|
||||
let i = 0;
|
||||
for (const team in seats) {
|
||||
if (this.totalsOnly) {
|
||||
pScores[i] = [this.getTotal(seats[team].map(w => w.get(scoreProperty)))];
|
||||
} else {
|
||||
// Get array of (arrays of) seat scores.
|
||||
const seatScores = seats[team].map(w => asArray(w.get(scoreProperty)));
|
||||
|
||||
// Make all score arrays for this team the same length, then add them element-by-element
|
||||
const n = seatScores.reduce((max, xs) => Math.max(max, xs.length), 0);
|
||||
pScores[i] = Array(n).fill(0).map((_,i) => this.getTotal(seatScores.map(xs => xs[i])));
|
||||
pScores[i] = pScores[i].concat(Array(numRounds).fill('')).slice(0,numRounds);
|
||||
|
||||
// Add totals and team name
|
||||
if(showTotals)
|
||||
pScores[i].push(this.getTotal(pScores[i].slice(0,n)));
|
||||
}
|
||||
pScores[i].unshift(team || '-');
|
||||
i++
|
||||
}
|
||||
} else { // Should never happen.
|
||||
console.log('Internal error: invalid seats in updateTable');
|
||||
return
|
||||
}
|
||||
|
||||
let numCols;
|
||||
let numRows;
|
||||
// Do not use player colors if team scores are being shown.
|
||||
let showPlayerColors = this.get('showPlayerColors') && Array.isArray(seats);
|
||||
|
||||
let currentRound = parseInt(this.get('currentRound'));
|
||||
if (isNaN(currentRound) || currentRound < 1 || currentRound > numRounds)
|
||||
currentRound = null;
|
||||
|
||||
if(this.get('playersInColumns')) { // Scores are in columns
|
||||
// Compute total number of rows and columns in table
|
||||
numCols = pScores.length + 1;
|
||||
numRows = numRounds + 1 + (showTotals ? 1 : 0);
|
||||
|
||||
// Add header row
|
||||
const names = pScores.map(x => x[0]);
|
||||
names.unshift(this.get('roundLabel'));
|
||||
this.tableDOM.innerHTML += '<tbody></tbody>';
|
||||
const tr = this.addRowToTable($('tbody', this.tableDOM), names);
|
||||
const defaultColor = window.getComputedStyle(tr.cells[0]).getPropertyValue('background-color');
|
||||
// Get player colors if needed
|
||||
if(showPlayerColors)
|
||||
for (let c=0; c<pScores.length; c++ ) {
|
||||
const bgColor = pScores[c][0]=='-' ? defaultColor : seats.filter(x=> x.get('player') == pScores[c][0])[0].get('color');
|
||||
tr.cells[c+1].style.backgroundColor = bgColor;
|
||||
tr.cells[c+1].style.color = contrastAnyColor(bgColor, 1);
|
||||
}
|
||||
// Add remaining rows
|
||||
for( let r=1; r < numRows; r++ ) {
|
||||
const pRow = pScores.map(x => x[r]);
|
||||
pRow.unshift(rounds[r]);
|
||||
const tr = this.addRowToTable($('tbody',this.tableDOM), pRow);
|
||||
if(r == currentRound)
|
||||
for(let c=1; c < numCols; c++)
|
||||
tr.cells[c].classList.add('currentRound');
|
||||
}
|
||||
if(showTotals)
|
||||
for(let c=0; c < numCols; c++)
|
||||
this.tableDOM.rows[numRows-1].cells[c].classList.add('totalsLine');
|
||||
} else { // Scores are in rows
|
||||
// Compute total number of rows and columns in table
|
||||
numCols = numRounds + 1 + (showTotals ? 1 : 0);
|
||||
numRows = pScores.length + 1;
|
||||
|
||||
// First row contains round names
|
||||
const tr = this.addRowToTable(this.tableDOM, rounds);
|
||||
const defaultColor = window.getComputedStyle(tr.cells[0]).getPropertyValue('background-color');
|
||||
// Remaining rows are one row per player.
|
||||
for( let r=0; r < pScores.length; r++) {
|
||||
const tr = this.addRowToTable(this.tableDOM, pScores[r]);
|
||||
if(showPlayerColors) {
|
||||
const bgColor = pScores[r][0]=='-' ? defaultColor : seats.filter(x=> x.get('player') == pScores[r][0])[0].get('color');
|
||||
tr.cells[0].style.backgroundColor = bgColor;
|
||||
tr.cells[0].style.color = contrastAnyColor(bgColor, 1);
|
||||
}
|
||||
}
|
||||
for(let r=1; r < numRows; r++) {
|
||||
if(currentRound)
|
||||
this.tableDOM.rows[r].cells[currentRound].classList.add('currentRound');
|
||||
if(showTotals)
|
||||
this.tableDOM.rows[r].cells[numCols-1].classList.add('totalsLine');
|
||||
}
|
||||
}
|
||||
this.domElement.style.setProperty('--firstColWidth', '50px');
|
||||
this.domElement.style.setProperty('--columns', numCols);
|
||||
}
|
||||
}
|
@ -33,13 +33,13 @@ class Seat extends Widget {
|
||||
displayedText = displayedText.replaceAll('playerName',this.get('player'))
|
||||
setText(this.domElement, displayedText);
|
||||
}
|
||||
if(delta.player !== undefined)
|
||||
this.updateLinkedWidgets();
|
||||
|
||||
this.updateLinkedWidgets(delta.player !== undefined);
|
||||
}
|
||||
|
||||
applyInitialDelta(delta) {
|
||||
super.applyInitialDelta(delta);
|
||||
this.updateLinkedWidgets();
|
||||
this.updateLinkedWidgets(true);
|
||||
}
|
||||
|
||||
classes() {
|
||||
@ -92,7 +92,12 @@ class Seat extends Widget {
|
||||
}
|
||||
}
|
||||
|
||||
updateLinkedWidgets() {
|
||||
widgetFilter(w=>w.get('onlyVisibleForSeat') || w.get('linkedToSeat') || w.get('type') == 'seat').forEach(wc=>wc.updateOwner());
|
||||
updateLinkedWidgets(playerChanged) {
|
||||
const scoreboard = widgetFilter(w => w.get('type') == 'scoreboard');
|
||||
for(const board of scoreboard)
|
||||
board.updateTable();
|
||||
|
||||
if(playerChanged)
|
||||
widgetFilter(w=>w.get('onlyVisibleForSeat') || w.get('linkedToSeat') || w.get('type') == 'seat').forEach(wc=>wc.updateOwner());
|
||||
}
|
||||
}
|
||||
|
@ -1310,6 +1310,49 @@ export class Widget extends StateManaged {
|
||||
}
|
||||
}
|
||||
|
||||
if(a.func == 'SCORE') {
|
||||
setDefaults(a, { mode: 'set', property: 'score', seats: null, round: null, value: null});
|
||||
if([ 'set', 'inc', 'dec' ].indexOf(a.mode) == -1) {
|
||||
problems.push(`Warning: Mode ${a.mode} interpreted as set.`);
|
||||
a.mode = 'set'
|
||||
}
|
||||
|
||||
if(a.value === null)
|
||||
a.value = a.mode=='set' ? 0 : 1;
|
||||
if(isNaN(parseFloat(a.value))) {
|
||||
problems.push(`value ${a.value} must be a number, assuming 0.`);
|
||||
a.value = 0;
|
||||
}
|
||||
a.value = parseFloat(a.value);
|
||||
|
||||
let round = a.round ? parseInt(a.round) : null;
|
||||
if(round !== null && (isNaN(parseInt(round)) || round < 1)) {
|
||||
problems.push(`round ${a.round} must be null or a positive integer, assuming null.`);
|
||||
round = null;
|
||||
}
|
||||
|
||||
const seats = widgetFilter(w => w.get('type')=='seat' && (a.seats===null || asArray(a.seats).includes(w.get('id'))));
|
||||
|
||||
const relation = (a.mode == 'set') ? '=' : (a.mode == 'dec' ? '-' : '+');
|
||||
for(let i=0; i < seats.length; i++) {
|
||||
let newScore = [...asArray(seats[i].get(a.property) || 0)];
|
||||
const seatRound = a.round === null ? newScore.length + 1 : a.round;
|
||||
if(a.round > newScore.length)
|
||||
newScore = newScore.concat(Array(a.round - newScore.length).fill(0));
|
||||
newScore[seatRound-1] = compute(relation, null, newScore[seatRound-1] || 0, a.value);
|
||||
await seats[i].set(a.property, newScore);
|
||||
}
|
||||
|
||||
if(jeRoutineLogging) {
|
||||
const phrase = round===null ? 'new round' : `round ${a.round}`;
|
||||
const seatIds = seats.map(w => w.get('id'));
|
||||
if(a.mode == 'inc' || a.mode == 'dec')
|
||||
jeLoggingRoutineOperationSummary(`${a.mode} ${phrase} in seats ${JSON.stringify(seatIds)} by ${a.value}`)
|
||||
else
|
||||
jeLoggingRoutineOperationSummary(`set ${phrase} in seats ${JSON.stringify(seatIds)} to ${a.value}`)
|
||||
}
|
||||
}
|
||||
|
||||
if(a.func == 'SELECT') {
|
||||
setDefaults(a, { type: 'all', property: 'parent', relation: '==', value: null, max: 999999, collection: 'DEFAULT', mode: 'set', source: 'all' });
|
||||
let source;
|
||||
@ -1375,8 +1418,6 @@ export class Widget extends StateManaged {
|
||||
}
|
||||
if((a.property == 'parent' || a.property == 'deck') && a.value !== null && !widgets.has(a.value)) {
|
||||
problems.push(`Tried setting ${a.property} to ${a.value} which doesn't exist.`);
|
||||
} else if (readOnlyProperties.has(a.property)) {
|
||||
problems.push(`Tried setting read-only property ${a.property}.`);
|
||||
} else if (collection = getCollection(a.collection)) {
|
||||
if (a.property == 'id') {
|
||||
for(const oldWidget of collections[collection]) {
|
||||
@ -1397,6 +1438,11 @@ export class Widget extends StateManaged {
|
||||
}
|
||||
} else {
|
||||
for(const w of collections[collection]) {
|
||||
if (w.readOnlyProperties().has(a.property)) {
|
||||
problems.push(`Tried setting read-only property ${a.property}.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(a.relation == '+' && w.get(String(a.property)) == null)
|
||||
a.relation = '=';
|
||||
if(a.relation == '+' && a.value == null)
|
||||
@ -1891,6 +1937,10 @@ export class Widget extends StateManaged {
|
||||
}
|
||||
}
|
||||
|
||||
readOnlyProperties() {
|
||||
return readOnlyProperties;
|
||||
}
|
||||
|
||||
async rotate(degrees, mode) {
|
||||
if(!mode || mode == 'add')
|
||||
await this.set('rotation', (this.get('rotation') + degrees) % 360);
|
||||
|
@ -175,6 +175,7 @@
|
||||
</div>
|
||||
<div class="category">
|
||||
<h2>Decorative</h2>
|
||||
<button class="ui-button" id="addScoreboard">Add Scoreboard</button>
|
||||
</div>
|
||||
<div class="category">
|
||||
<h2>Custom</h2>
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1216,226 +1216,6 @@
|
||||
"movableInEdit": false,
|
||||
"image": "/assets/-753447363_159340"
|
||||
},
|
||||
"p2ScoreMinus": {
|
||||
"type": "button",
|
||||
"id": "p2ScoreMinus",
|
||||
"x": 4,
|
||||
"y": -2,
|
||||
"width": 36,
|
||||
"height": 36,
|
||||
"layer": 15,
|
||||
"movableInEdit": false,
|
||||
"backgroundColor": "#00a2e8",
|
||||
"backgroundColorOH": "#96BFE1",
|
||||
"borderColor": "black",
|
||||
"borderColorOH": "white",
|
||||
"clickRoutine": [
|
||||
{
|
||||
"func": "LABEL",
|
||||
"label": "p1Score",
|
||||
"mode": "inc",
|
||||
"value": -1
|
||||
}
|
||||
],
|
||||
"text": "-",
|
||||
"parent": "p1Score"
|
||||
},
|
||||
"p2ScorePlus": {
|
||||
"type": "button",
|
||||
"id": "p2ScorePlus",
|
||||
"x": 100,
|
||||
"y": -2,
|
||||
"width": 36,
|
||||
"height": 36,
|
||||
"layer": 15,
|
||||
"movableInEdit": false,
|
||||
"backgroundColor": "#00a2e8",
|
||||
"backgroundColorOH": "#96BFE1",
|
||||
"borderColor": "black",
|
||||
"borderColorOH": "white",
|
||||
"clickRoutine": [
|
||||
{
|
||||
"func": "LABEL",
|
||||
"label": "p1Score",
|
||||
"mode": "inc",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"text": "+",
|
||||
"parent": "p1Score"
|
||||
},
|
||||
"p1ScoreMinus": {
|
||||
"type": "button",
|
||||
"id": "p1ScoreMinus",
|
||||
"x": 4,
|
||||
"y": -2,
|
||||
"width": 36,
|
||||
"height": 36,
|
||||
"layer": 15,
|
||||
"movableInEdit": false,
|
||||
"backgroundColor": "#ed1c24",
|
||||
"backgroundColorOH": "#F7C8C8",
|
||||
"borderColor": "black",
|
||||
"borderColorOH": "white",
|
||||
"clickRoutine": [
|
||||
{
|
||||
"func": "LABEL",
|
||||
"label": "p2Score",
|
||||
"mode": "inc",
|
||||
"value": -1
|
||||
}
|
||||
],
|
||||
"text": "-",
|
||||
"parent": "p2Score"
|
||||
},
|
||||
"p1ScorePlus": {
|
||||
"type": "button",
|
||||
"id": "p1ScorePlus",
|
||||
"x": 100,
|
||||
"y": -2,
|
||||
"width": 36,
|
||||
"height": 36,
|
||||
"layer": 15,
|
||||
"movableInEdit": false,
|
||||
"backgroundColor": "#ed1c24",
|
||||
"backgroundColorOH": "#F7C8C8",
|
||||
"borderColor": "black",
|
||||
"borderColorOH": "white",
|
||||
"clickRoutine": [
|
||||
{
|
||||
"func": "LABEL",
|
||||
"label": "p2Score",
|
||||
"mode": "inc",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"text": "+",
|
||||
"parent": "p2Score"
|
||||
},
|
||||
"p3Score": {
|
||||
"type": "label",
|
||||
"id": "p3Score",
|
||||
"x": 10,
|
||||
"y": 950,
|
||||
"width": 140,
|
||||
"height": 44,
|
||||
"layer": 15,
|
||||
"z": 2481,
|
||||
"css": "font-size: 30px",
|
||||
"editable": true,
|
||||
"text": 0,
|
||||
"linkedToSeat": "seat4"
|
||||
},
|
||||
"p3ScoreMinus": {
|
||||
"type": "button",
|
||||
"id": "p3ScoreMinus",
|
||||
"parent": "p3Score",
|
||||
"x": 4,
|
||||
"y": -2,
|
||||
"width": 36,
|
||||
"height": 36,
|
||||
"layer": 15,
|
||||
"movableInEdit": false,
|
||||
"backgroundColor": "#22b14c",
|
||||
"backgroundColorOH": "#CEFBD8",
|
||||
"borderColor": "black",
|
||||
"borderColorOH": "white",
|
||||
"clickRoutine": [
|
||||
{
|
||||
"func": "LABEL",
|
||||
"label": "p3Score",
|
||||
"mode": "inc",
|
||||
"value": -1
|
||||
}
|
||||
],
|
||||
"text": "-"
|
||||
},
|
||||
"p3ScorePlus": {
|
||||
"type": "button",
|
||||
"id": "p3ScorePlus",
|
||||
"parent": "p3Score",
|
||||
"x": 100,
|
||||
"y": -2,
|
||||
"width": 36,
|
||||
"height": 36,
|
||||
"layer": 15,
|
||||
"movableInEdit": false,
|
||||
"backgroundColor": "#22b14c",
|
||||
"backgroundColorOH": "#CEFBD8",
|
||||
"borderColor": "black",
|
||||
"borderColorOH": "white",
|
||||
"clickRoutine": [
|
||||
{
|
||||
"func": "LABEL",
|
||||
"label": "p3Score",
|
||||
"mode": "inc",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"text": "+"
|
||||
},
|
||||
"p4Score": {
|
||||
"type": "label",
|
||||
"id": "p4Score",
|
||||
"x": 10,
|
||||
"y": 815,
|
||||
"width": 140,
|
||||
"height": 44,
|
||||
"layer": 15,
|
||||
"z": 2494,
|
||||
"css": "font-size: 30px",
|
||||
"editable": true,
|
||||
"text": 0,
|
||||
"linkedToSeat": "seat3"
|
||||
},
|
||||
"p4ScoreMinus": {
|
||||
"type": "button",
|
||||
"id": "p4ScoreMinus",
|
||||
"parent": "p4Score",
|
||||
"x": 4,
|
||||
"y": -2,
|
||||
"width": 36,
|
||||
"height": 36,
|
||||
"layer": 15,
|
||||
"movableInEdit": false,
|
||||
"backgroundColor": "#ffc90e",
|
||||
"backgroundColorOH": "#FBFBCE",
|
||||
"borderColor": "black",
|
||||
"borderColorOH": "white",
|
||||
"clickRoutine": [
|
||||
{
|
||||
"func": "LABEL",
|
||||
"label": "p4Score",
|
||||
"mode": "inc",
|
||||
"value": -1
|
||||
}
|
||||
],
|
||||
"text": "-"
|
||||
},
|
||||
"p4ScorePlus": {
|
||||
"type": "button",
|
||||
"id": "p4ScorePlus",
|
||||
"parent": "p4Score",
|
||||
"x": 100,
|
||||
"y": -2,
|
||||
"width": 36,
|
||||
"height": 36,
|
||||
"layer": 15,
|
||||
"movableInEdit": false,
|
||||
"backgroundColor": "#ffc90e",
|
||||
"backgroundColorOH": "#FBFBCE",
|
||||
"borderColor": "black",
|
||||
"borderColorOH": "white",
|
||||
"clickRoutine": [
|
||||
{
|
||||
"func": "LABEL",
|
||||
"label": "p4Score",
|
||||
"mode": "inc",
|
||||
"value": 1
|
||||
}
|
||||
],
|
||||
"text": "+"
|
||||
},
|
||||
"l2jq": {
|
||||
"type": "label",
|
||||
"z": 1,
|
||||
@ -1522,20 +1302,21 @@
|
||||
"func": "CLICK"
|
||||
},
|
||||
{
|
||||
"func": "SELECT",
|
||||
"property": "id",
|
||||
"relation": "in",
|
||||
"func": "SET",
|
||||
"collection": [
|
||||
"seat1",
|
||||
"seat2",
|
||||
"seat3",
|
||||
"seat4"
|
||||
],
|
||||
"property": "score",
|
||||
"value": [
|
||||
"p1Score",
|
||||
"p2Score",
|
||||
"p3Score",
|
||||
"p4Score"
|
||||
0
|
||||
]
|
||||
},
|
||||
{
|
||||
"func": "SET",
|
||||
"property": "text",
|
||||
"value": 0
|
||||
"func": "TURN",
|
||||
"turnCycle": "random"
|
||||
}
|
||||
],
|
||||
"color": "white",
|
||||
@ -1573,10 +1354,38 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"scoreGlobalUpdateRoutine": [
|
||||
{
|
||||
"func": "SELECT",
|
||||
"type": "seat",
|
||||
"property": "id",
|
||||
"relation": "!=",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"func": "SET",
|
||||
"property": "turn",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"func": "SELECT",
|
||||
"source": "DEFAULT",
|
||||
"property": "player",
|
||||
"value": "${playerName}"
|
||||
},
|
||||
{
|
||||
"func": "SET",
|
||||
"property": "turn",
|
||||
"value": true
|
||||
},
|
||||
{
|
||||
"func": "TURN"
|
||||
}
|
||||
]
|
||||
},
|
||||
"_meta": {
|
||||
"version": 6,
|
||||
"version": 9,
|
||||
"info": {
|
||||
"name": "Tittle",
|
||||
"image": "/assets/371528430_9397",
|
||||
@ -1585,15 +1394,20 @@
|
||||
"year": "2012",
|
||||
"mode": "vs",
|
||||
"time": "30",
|
||||
"players": "2-4",
|
||||
"language": "en-US",
|
||||
"variant": "",
|
||||
"attribution": "The game layout and images (including the library image) were done by LawDawg and are released to the Public Domain under CC0.\n\nThe background picture is by Dawn Hudson (cropped and colors modified) and released to the public domain under the CC0 license at https://publicdomainpictures.net/en/view-image.php?image=71643&picture=colourful-flame-background.",
|
||||
"similarName": "IOTA",
|
||||
"description": "Place cards in tile pattern on board matching 4 shapes and 4 colors. Player with most points wins.",
|
||||
"attribution": "The game layout and images (including the library image) were done by LawDawg and are released to the Public Domain under CC0.\n\nThe background picture is by Dawn Hudson (cropped and colors modified) and released to the public domain under the CC0 license at https://publicdomainpictures.net/en/view-image.php?image=71643&picture=colourful-flame-background.",
|
||||
"lastUpdate": 1672794002688,
|
||||
"id": "z7eg",
|
||||
"showName": true,
|
||||
"helpText": "Players take seats. Move 4 cards to hand on right of screen using +4 button. Play cards and keep score manually. Replace cards using +1 button. The black \"dead end\" squares are optional. You can use them to indicate a line that is impossible to complete because a card has already been played elsewhere.",
|
||||
"lastUpdate": 1659321088000
|
||||
"skill": "",
|
||||
"description": "Place cards in tile pattern on board matching 4 shapes and 4 colors. Player with most points wins.",
|
||||
"similarImage": "",
|
||||
"similarName": "IOTA",
|
||||
"similarAwards": "2012/Fall Parents' Choice Recommended\n2012 Mensa Select Winner",
|
||||
"ruleText": "",
|
||||
"helpText": "Players take seats. Move 4 cards to hand on right of screen using +4 button. Play cards and keep score by clicking on the scoreboard and entering score. Replace cards using +1 button. The black \"dead end\" squares are optional. You can use them to indicate a line that is impossible to complete because a card has already been played elsewhere.",
|
||||
"players": "",
|
||||
"language": "",
|
||||
"variant": ""
|
||||
}
|
||||
},
|
||||
"plus7": {
|
||||
@ -1842,12 +1656,14 @@
|
||||
"collection": "thisButton",
|
||||
"mode": "ignoreClickRoutine"
|
||||
}
|
||||
],
|
||||
"score": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"seat2": {
|
||||
"type": "seat",
|
||||
"id": "seat2",
|
||||
"y": 140,
|
||||
"z": 2083,
|
||||
"movableInEdit": false,
|
||||
"hideWhenUnused": true,
|
||||
@ -1864,12 +1680,15 @@
|
||||
"collection": "thisButton",
|
||||
"mode": "ignoreClickRoutine"
|
||||
}
|
||||
],
|
||||
"y": 75,
|
||||
"score": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"seat3": {
|
||||
"type": "seat",
|
||||
"id": "seat3",
|
||||
"y": 750,
|
||||
"z": 2083,
|
||||
"movableInEdit": false,
|
||||
"hideWhenUnused": true,
|
||||
@ -1885,12 +1704,15 @@
|
||||
"collection": "thisButton",
|
||||
"mode": "ignoreClickRoutine"
|
||||
}
|
||||
],
|
||||
"y": 150,
|
||||
"score": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"seat4": {
|
||||
"type": "seat",
|
||||
"id": "seat4",
|
||||
"y": 890,
|
||||
"z": 2083,
|
||||
"movableInEdit": false,
|
||||
"index": 4,
|
||||
@ -1906,36 +1728,12 @@
|
||||
"collection": "thisButton",
|
||||
"mode": "ignoreClickRoutine"
|
||||
}
|
||||
],
|
||||
"y": 225,
|
||||
"score": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"p2Score": {
|
||||
"type": "label",
|
||||
"id": "p2Score",
|
||||
"x": 10,
|
||||
"y": 200,
|
||||
"width": 140,
|
||||
"height": 44,
|
||||
"layer": 15,
|
||||
"z": 10365,
|
||||
"css": "font-size: 30px",
|
||||
"editable": true,
|
||||
"text": 0,
|
||||
"linkedToSeat": "seat2"
|
||||
},
|
||||
"p1Score": {
|
||||
"type": "label",
|
||||
"id": "p1Score",
|
||||
"x": 10,
|
||||
"y": 65,
|
||||
"width": 140,
|
||||
"height": 44,
|
||||
"layer": 15,
|
||||
"z": 2474,
|
||||
"css": "font-size: 30px",
|
||||
"editable": true,
|
||||
"text": 0,
|
||||
"linkedToSeat": "seat1"
|
||||
},
|
||||
"next": {
|
||||
"type": "button",
|
||||
"id": "next",
|
||||
@ -1950,5 +1748,17 @@
|
||||
],
|
||||
"text": "Next Player",
|
||||
"movableInEdit": false
|
||||
},
|
||||
"scoreboard": {
|
||||
"type": "scoreboard",
|
||||
"id": "scoreboard",
|
||||
"y": 800,
|
||||
"z": 2101,
|
||||
"css": {
|
||||
".scoreboard td:first-child": {
|
||||
"--firstColWidth": "10px"
|
||||
}
|
||||
},
|
||||
"sortField": "player"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
1145
library/tutorials/Functions - SCORE/0.json
Normal file
1145
library/tutorials/Functions - SCORE/0.json
Normal file
File diff suppressed because it is too large
Load Diff
13
library/tutorials/Functions - SCORE/assets/770478567_4518
Normal file
13
library/tutorials/Functions - SCORE/assets/770478567_4518
Normal file
@ -0,0 +1,13 @@
|
||||
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500" width="500" height="500">
|
||||
<title>Buttons Blue</title>
|
||||
<style>
|
||||
.s0 { fill: #1f5ca6;stroke: #0d2f5e;stroke-linejoin: round;stroke-width: 12 }
|
||||
.s1 { fill: #ffffff }
|
||||
</style>
|
||||
<path id="Forma 1" class="s0" d="m250 490c-132.7 0-240-107.3-240-240 0-132.7 107.3-240 240-240 132.7 0 240 107.3 240 240 0 132.7-107.3 240-240 240z"/>
|
||||
<path id="Functions: FOREACH " class="s1" aria-label="Functions:
|
||||
SCORE
|
||||
" d="m120.6 205.7v5.3h-20.9v22h-6.5v-49.8h30.8v5.4h-24.3v17.1zm32.5 27.3l-0.1-3.7q-3.7 4.4-10.9 4.4-5.9 0-9-3.4-3.1-3.5-3.1-10.2v-24.1h6.3v23.9q0 8.4 6.8 8.4 7.3 0 9.7-5.4v-26.9h6.3v37zm15.6-37h6l0.2 4.7q4.3-5.4 11.1-5.4 11.7 0 11.8 13.3v24.4h-6.3v-24.5q0-4-1.9-5.9-1.7-1.9-5.5-1.9-3.1 0-5.4 1.6-2.4 1.7-3.7 4.3v26.4h-6.3zm53.5 32.5q3.4 0 5.9-2 2.5-2.1 2.8-5.2h6q-0.2 3.2-2.2 6.1-2 2.8-5.4 4.6-3.4 1.7-7.1 1.7-7.6 0-12-5.1-4.5-5-4.5-13.8v-1q0-5.4 2-9.6 2-4.2 5.7-6.6 3.7-2.3 8.7-2.3 6.3 0 10.4 3.7 4.1 3.8 4.4 9.7h-6q-0.3-3.6-2.7-5.9-2.5-2.3-6.1-2.3-4.8 0-7.4 3.5-2.7 3.5-2.7 10v1.2q0 6.4 2.7 9.9 2.6 3.4 7.5 3.4zm24-41.5h6.4v9h6.9v4.9h-6.9v22.9q0 2.2 0.9 3.4 0.9 1.1 3.1 1.1 1.1 0 3-0.4v5.1q-2.5 0.7-4.8 0.7-4.2 0-6.4-2.6-2.2-2.6-2.2-7.3v-22.9h-6.7v-4.9h6.7zm27.5 9v36.9h-6.3v-36.9zm-6.8-9.9q0-1.5 0.9-2.6 0.9-1 2.8-1 1.8 0 2.8 1 1 1.1 1 2.6 0 1.6-1 2.6-1 1-2.8 1-1.9 0-2.8-1-0.9-1-0.9-2.6zm15.2 28.4v-0.4q0-5.4 2.2-9.8 2.1-4.3 5.9-6.7 3.8-2.3 8.7-2.3 7.6 0 12.2 5.2 4.7 5.2 4.7 13.9v0.5q0 5.4-2.1 9.7-2 4.2-5.9 6.6-3.8 2.4-8.8 2.4-7.5 0-12.2-5.2-4.7-5.2-4.7-13.9zm6.4 0.4q0 6.1 2.8 9.8 2.9 3.8 7.7 3.8 4.8 0 7.7-3.8 2.8-3.8 2.8-10.6 0-6.1-2.9-9.8-2.9-3.8-7.7-3.8-4.7 0-7.5 3.7-2.9 3.7-2.9 10.7zm35.2-18.9h6l0.2 4.6q4.3-5.3 11.1-5.3 11.7 0 11.8 13.2v24.4h-6.3v-24.4q0-4-1.8-5.9-1.8-2-5.6-2-3.1 0-5.4 1.7-2.3 1.6-3.6 4.3v26.3h-6.4zm60.2 27.2q0-2.6-1.9-4-2-1.4-6.8-2.4-4.8-1-7.6-2.5-2.8-1.4-4.2-3.4-1.3-2-1.3-4.7 0-4.6 3.8-7.7 3.9-3.1 9.9-3.1 6.3 0 10.1 3.2 4 3.2 4 8.3h-6.4q0-2.6-2.2-4.5-2.2-1.9-5.5-1.9-3.5 0-5.4 1.6-2 1.5-2 3.9 0 2.3 1.8 3.4 1.8 1.2 6.6 2.3 4.7 1 7.6 2.5 3 1.5 4.4 3.5 1.4 2.1 1.4 5.1 0 4.9-3.9 7.9-4 3-10.3 3-4.5 0-7.9-1.6-3.4-1.5-5.4-4.3-1.9-2.9-1.9-6.2h6.4q0.1 3.2 2.5 5.1 2.4 1.8 6.3 1.8 3.6 0 5.7-1.4 2.2-1.5 2.2-3.9zm14.4 6.5q0-1.6 0.9-2.7 1-1.1 3-1.1 1.9 0 2.9 1.1 1 1.1 1 2.7 0 1.6-1 2.6-1 1.1-2.9 1.1-2 0-3-1.1-0.9-1-0.9-2.6zm0-30.2q0-1.6 0.9-2.7 1-1.1 3-1.1 1.9 0 2.9 1.1 1 1.1 1 2.7 0 1.6-1 2.7-1 1-2.9 1-2 0-3-1-0.9-1.1-0.9-2.7zm-237.9 95.3q-8.4-2.4-12.3-5.9-3.8-3.6-3.8-8.8 0-5.8 4.6-9.7 4.8-3.8 12.3-3.8 5.1 0 9.1 2 4 1.9 6.2 5.4 2.3 3.5 2.3 7.6h-6.6q0-4.5-2.9-7-2.9-2.6-8.1-2.6-4.9 0-7.6 2.1-2.7 2.1-2.7 5.9 0 3.1 2.6 5.2 2.6 2.1 8.7 3.8 6.3 1.8 9.8 3.9 3.5 2.1 5.1 4.9 1.8 2.8 1.8 6.6 0 6-4.8 9.7-4.7 3.6-12.6 3.6-5.1 0-9.5-1.9-4.5-2-6.9-5.4-2.4-3.5-2.4-7.8h6.6q0 4.5 3.3 7.1 3.4 2.6 8.9 2.6 5.2 0 8-2.1 2.8-2.1 2.8-5.8 0-3.6-2.6-5.6-2.6-2-9.3-4zm56.9 6.4h6.6q-1 7.9-5.9 12.2-4.9 4.3-13 4.3-8.8 0-14.1-6.3-5.3-6.3-5.3-16.9v-4.8q0-7 2.4-12.2 2.5-5.3 7.1-8.1 4.5-2.8 10.5-2.8 7.9 0 12.7 4.4 4.8 4.4 5.6 12.3h-6.6q-0.9-6-3.8-8.7-2.8-2.6-7.9-2.6-6.3 0-9.9 4.6-3.5 4.7-3.5 13.2v4.9q0 8.1 3.3 12.9 3.4 4.7 9.5 4.7 5.5 0 8.4-2.4 2.9-2.5 3.9-8.7zm53.8-10.6v3.1q0 7.4-2.5 12.8-2.4 5.5-6.9 8.3-4.5 2.9-10.6 2.9-5.8 0-10.4-2.9-4.5-2.9-7.1-8.2-2.5-5.4-2.5-12.4v-3.6q0-7.2 2.5-12.7 2.5-5.5 7-8.4 4.6-2.9 10.5-2.9 6 0 10.5 2.9 4.6 2.8 7 8.4 2.5 5.4 2.5 12.7zm-6.5 3.6v-3.7q0-8.8-3.6-13.5-3.5-4.8-9.9-4.8-6.2 0-9.8 4.8-3.6 4.7-3.7 13.1v3.6q0 8.6 3.6 13.5 3.6 4.9 9.9 4.9 6.4 0 9.9-4.6 3.5-4.6 3.6-13.3zm45.4 22.8l-10.8-20.1h-11.7v20.1h-6.6v-49.7h16.5q8.4 0 12.9 3.8 4.6 3.8 4.6 11.1 0 4.7-2.6 8.1-2.5 3.5-6.9 5.2l11.6 21.1v0.4zm-22.4-44.3v18.8h10q4.9 0 7.8-2.5 2.9-2.5 2.9-6.8 0-4.6-2.8-7.1-2.7-2.4-7.9-2.4zm64.7 16v5.3h-21.6v17.7h25.1v5.3h-31.7v-49.7h31.3v5.4h-24.7v16z"/>
|
||||
<path id="Layer" class="s1" d="m227.1 65.6c0-12.6 10.3-22.9 22.9-22.9 12.6 0 22.9 10.3 22.9 22.9 0 12.6-10.3 22.8-22.9 22.8-12.6 0-22.9-10.2-22.9-22.8zm62.9 52.5v7.5c0 4.7-3.8 8.5-8.6 8.5h-62.8c-4.8 0-8.6-3.8-8.6-8.5v-7.5c0-13.2 10.8-24 24-24h3c4 1.9 8.4 2.9 13 2.9 4.6 0 9.1-1 13-2.9h3c13.2 0 24 10.8 24 24z"/>
|
||||
<path id="Layer" class="s1" d="m212.4 51.3c-3.2-0.8-3.2-4.9 0-5.7l34.1-8.2c2.3-0.5 4.7-0.5 7.1 0l34 8.2c3.2 0.8 3.2 4.9 0 5.7l-17.2 4.1c1.5 3.1 2.5 6.5 2.5 10.2 0 12.6-10.3 22.8-22.9 22.8-12.6 0-22.9-10.2-22.9-22.8 0-3.7 1-7.1 2.5-10.2l-11.7-2.8v9.3c1.2 0.8 2.1 2.1 2.1 3.7 0 1.5-0.8 2.7-2 3.5l2.8 11.1c0.3 1.3-0.4 2.5-1.4 2.5h-7.4c-1 0-1.7-1.2-1.4-2.5l2.8-11.1c-1.1-0.8-2-2-2-3.5 0-1.6 0.9-2.9 2.2-3.7v-10.4l-1.2-0.2z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 4.4 KiB |
549
library/tutorials/Scoreboard/0.json
Normal file
549
library/tutorials/Scoreboard/0.json
Normal file
@ -0,0 +1,549 @@
|
||||
{
|
||||
"_meta": {
|
||||
"version": 9,
|
||||
"info": {
|
||||
"name": "Scoreboard",
|
||||
"image": "/assets/-751946941_5803",
|
||||
"rules": "",
|
||||
"bgg": "",
|
||||
"year": "2022",
|
||||
"mode": "Tutorial",
|
||||
"time": "",
|
||||
"attribution": "",
|
||||
"id": "4ebx",
|
||||
"showName": true,
|
||||
"skill": "",
|
||||
"description": "",
|
||||
"variantImage": "",
|
||||
"variant": "Basic",
|
||||
"language": "en-US",
|
||||
"players": "1",
|
||||
"similarImage": "",
|
||||
"similarName": "",
|
||||
"similarAwards": "",
|
||||
"ruleText": "",
|
||||
"helpText": ""
|
||||
}
|
||||
},
|
||||
"seat1": {
|
||||
"type": "seat",
|
||||
"id": "seat1",
|
||||
"z": 6,
|
||||
"scale": 0.5,
|
||||
"x": -37,
|
||||
"y": 10,
|
||||
"color": "red",
|
||||
"score": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6
|
||||
],
|
||||
"bids": [
|
||||
3,
|
||||
4
|
||||
],
|
||||
"movableInEdit": false,
|
||||
"player": "Player 1"
|
||||
},
|
||||
"seat3": {
|
||||
"type": "seat",
|
||||
"id": "seat3",
|
||||
"z": 2,
|
||||
"scale": 0.5,
|
||||
"x": -37,
|
||||
"y": 103,
|
||||
"index": 3,
|
||||
"color": "yellow",
|
||||
"player": "Player 3",
|
||||
"score": [
|
||||
1,
|
||||
3,
|
||||
5,
|
||||
11
|
||||
],
|
||||
"movableInEdit": false,
|
||||
"bids": [
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
"seat5": {
|
||||
"type": "seat",
|
||||
"id": "seat5",
|
||||
"z": 2,
|
||||
"scale": 0.5,
|
||||
"x": -37,
|
||||
"y": 203,
|
||||
"index": 5,
|
||||
"color": "blue",
|
||||
"player": "Player 5",
|
||||
"movableInEdit": false,
|
||||
"bids": [
|
||||
5,
|
||||
2
|
||||
],
|
||||
"score": [
|
||||
"x"
|
||||
]
|
||||
},
|
||||
"seat2": {
|
||||
"type": "seat",
|
||||
"id": "seat2",
|
||||
"index": 2,
|
||||
"x": -37,
|
||||
"y": 53,
|
||||
"scale": 0.5,
|
||||
"z": 2,
|
||||
"color": "orange",
|
||||
"player": "Player 2",
|
||||
"score": [
|
||||
0,
|
||||
-2,
|
||||
1,
|
||||
-3.5,
|
||||
4,
|
||||
1
|
||||
],
|
||||
"movableInEdit": false,
|
||||
"bids": [
|
||||
0,
|
||||
5
|
||||
]
|
||||
},
|
||||
"seat4": {
|
||||
"type": "seat",
|
||||
"id": "seat4",
|
||||
"index": 4,
|
||||
"x": -37,
|
||||
"y": 153,
|
||||
"scale": 0.5,
|
||||
"z": 2,
|
||||
"color": "green",
|
||||
"player": "Player 4",
|
||||
"score": [
|
||||
1,
|
||||
2,
|
||||
"",
|
||||
"Foo",
|
||||
4,
|
||||
1
|
||||
],
|
||||
"movableInEdit": false,
|
||||
"bids": [
|
||||
4,
|
||||
1
|
||||
]
|
||||
},
|
||||
"comment1": {
|
||||
"id": "comment1",
|
||||
"x": 100,
|
||||
"y": 485,
|
||||
"width": 300,
|
||||
"z": 10075,
|
||||
"movable": false,
|
||||
"text": "This is the default scoreboard widget with no changes made to any of the default properties. This scoreboard is also moveable so you can drag it around the room, but the others are not.",
|
||||
"layer": -2
|
||||
},
|
||||
"overview": {
|
||||
"id": "overview",
|
||||
"x": 108,
|
||||
"y": 114,
|
||||
"width": 1400,
|
||||
"height": 150,
|
||||
"layer": -3,
|
||||
"z": 125,
|
||||
"css": "font-size: 25px; ",
|
||||
"text": "Scoreboard uses a property in seats to display numerical information in table form and total those numbers. Working in conjunction with routines and the SCORE function (shown in a different tutorial), the property on the seat can be updated and cause real-time changes to the scoreboard data. This room shows most of the options available when using scoreboards. The seats along the left side are pre-filled with player and score information to populate the data on the scoreboards in this room. Use the JSON Editor (ctrl-J) to look in the seats and scoreboards to see how missing or non-numeric data is handled.",
|
||||
"movable": false,
|
||||
"movableInEdit": false
|
||||
},
|
||||
"header": {
|
||||
"id": "header",
|
||||
"x": 400,
|
||||
"y": -2,
|
||||
"width": 800,
|
||||
"height": 60,
|
||||
"z": 74,
|
||||
"movable": false,
|
||||
"movableInEdit": false,
|
||||
"css": "font-size: 60px;text-align:center",
|
||||
"text": "Scoreboard: Basic"
|
||||
},
|
||||
"comment2": {
|
||||
"id": "comment2",
|
||||
"x": 425,
|
||||
"y": 485,
|
||||
"width": 300,
|
||||
"z": 10076,
|
||||
"movable": false,
|
||||
"text": "This scoreboard has 'showTotals' set to false so it does not show the sum of all the data for each player. It also sets 'showPlayerColors' to false to change the appearance of the header row.",
|
||||
"layer": -2
|
||||
},
|
||||
"scoreboard1": {
|
||||
"type": "scoreboard",
|
||||
"id": "scoreboard1",
|
||||
"x": 100,
|
||||
"y": 275,
|
||||
"z": 11,
|
||||
"movableInEdit": false
|
||||
},
|
||||
"scoreboard2": {
|
||||
"type": "scoreboard",
|
||||
"id": "scoreboard2",
|
||||
"x": 425,
|
||||
"y": 275,
|
||||
"z": 5,
|
||||
"movableInEdit": false,
|
||||
"movable": false,
|
||||
"showTotals": false,
|
||||
"showPlayerColors": false,
|
||||
"clickable": false
|
||||
},
|
||||
"scoreboard3": {
|
||||
"type": "scoreboard",
|
||||
"id": "scoreboard3",
|
||||
"x": 750,
|
||||
"y": 275,
|
||||
"z": 5,
|
||||
"movableInEdit": false,
|
||||
"movable": false,
|
||||
"showAllSeats": true,
|
||||
"autosizeColumns": false,
|
||||
"clickable": false
|
||||
},
|
||||
"comment3": {
|
||||
"id": "comment3",
|
||||
"x": 750,
|
||||
"y": 485,
|
||||
"width": 300,
|
||||
"z": 10076,
|
||||
"movable": false,
|
||||
"text": "This scoreboard has 'showAllSeats' set to true. It shows Player 6 data (header is '-') even though that seat is not occupied. It also has 'autosizeColumns' set to false so you scroll across to see all player data.",
|
||||
"layer": -2
|
||||
},
|
||||
"seat6": {
|
||||
"type": "seat",
|
||||
"id": "seat6",
|
||||
"index": 6,
|
||||
"x": -37,
|
||||
"y": 253,
|
||||
"scale": 0.5,
|
||||
"z": 2,
|
||||
"score": [
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"movableInEdit": false,
|
||||
"bids": []
|
||||
},
|
||||
"scoreboard4": {
|
||||
"type": "scoreboard",
|
||||
"id": "scoreboard4",
|
||||
"x": 1075,
|
||||
"y": 275,
|
||||
"z": 5,
|
||||
"movableInEdit": false,
|
||||
"movable": false,
|
||||
"playersInColumns": false,
|
||||
"width": 500
|
||||
},
|
||||
"comment4" |