Skip to content

Commit

Permalink
Merge pull request #166 from mardanbeigi/main
Browse files Browse the repository at this point in the history
40k: v1 of reorderable units
  • Loading branch information
rweyrauch committed Apr 26, 2024
2 parents 613f8d0 + 26dd0cc commit ce080c3
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 101 deletions.
40 changes: 37 additions & 3 deletions css/prettyscribe.css
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ label {
vertical-align: top;
}

.wh40k_unit_sheet > table > * > * > td {
.wh40k_unit_sheet table > * > * > td {
padding: 0.1rem 0.25rem;
}

Expand Down Expand Up @@ -351,10 +351,36 @@ label {
border-right: 1px solid black;
}

.wh40k_unit_sheet > table > * > * > td.subTableTd > table {
.wh40k_unit_sheet > table > * > * > td.subTableTd > div {
display: flex;
align-items: flex-start;
margin-top: -1px; /* not sure why this is necessary */
}

body.single_column .wh40k_unit_sheet > table > * > * > td.subTableTd > div {
flex-direction: column;
}


.wh40k_unit_sheet > table > * > * > td.subTableTd > div > table {
margin-bottom: 0;
}

body:not(.single_column) .wh40k_unit_sheet > table > * > * > td.subTableTd > div > table:first-child {
border-right: 1px solid black;
width: 60%;
}

body.single_column .wh40k_unit_sheet > table > * > * > td.subTableTd > div > table:last-child {
border-top: 1px solid black;
}

body:not(.single_column) .wh40k_unit_sheet > table > * > * > td.subTableTd > div > table:last-child {
border-left: 1px solid black;
margin-left: -1px; /* counter overlap between left and right columns' shared border */
width: 40%;
}

.wh40k_rules > div > p {
white-space: pre-wrap;
line-height: 1;
Expand Down Expand Up @@ -539,7 +565,7 @@ label {

.hide_enabled .hide_able:hover {
background-color: rgba(128, 128, 128, .4);
cursor: pointer;
cursor: cell;
top: 0;
}

Expand All @@ -557,3 +583,11 @@ label {
.page_break {
break-before: page;
}

.draggable {
cursor: grab;
}

.draggable_drop_target_top {
border-top: 2px solid green;
}
2 changes: 1 addition & 1 deletion dist/prettyscribe.js

Large diffs are not rendered by default.

50 changes: 25 additions & 25 deletions spec/40k10th/01.LoV.Leviathan.v0102Spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,73 +163,73 @@ describe("CreateRoster", function() {
"Abilities": mapWithKeys(["Oathband Bodyguard", "Teleport crest"]),
}}),
jasmine.objectContaining({
'_name': "Hekaton Land Fortress",
'_cost': jasmine.objectContaining({_points: 225}),
'_name': "Hernkyn Pioneers",
'_cost': jasmine.objectContaining({_points: 90}),
'_profileTables': {
"Unit": jasmine.objectContaining({
'_headers': ["Unit","M","T","SV","W","LD","OC"],
'_contents': [
jasmine.arrayContaining(["Hekaton Land Fortress"]),
jasmine.arrayContaining(["Hernkyn Pioneer"]),
jasmine.arrayContaining(["Hernkyn Pioneer w/ ion beamer"]),
],
}),
"Ranged Weapons": jasmine.objectContaining({
'_headers': ["Ranged Weapons","Range","A","BS","S","AP","D","Keywords"],
'_contents': [
jasmine.arrayContaining(["MATR autocannon"]),
jasmine.arrayContaining(["SP heavy conversion beamer"]),
jasmine.arrayContaining(["Twin bolt cannon"]),
jasmine.arrayContaining(["Bolt revolver"]),
jasmine.arrayContaining(["Bolt shotgun"]),
jasmine.arrayContaining(["Magna-coil autocannon"]),
jasmine.arrayContaining(["Ion beamer"]),
],
}),
"Melee Weapons": jasmine.objectContaining({
'_headers': ["Melee Weapons","Range","A","WS","S","AP","D","Keywords"],
'_contents': [
jasmine.arrayContaining(["Armoured wheels"]),
jasmine.arrayContaining(["Plasma knife"]),
],
})
},
'_modelList': [
"Hekaton Land Fortress (Armoured wheels, MATR autocannon, Pan spectral scanner, SP heavy conversion beamer, 2x Twin bolt cannon)"
"Hernkyn Pioneer w/ ion beamer (Bolt revolver, Bolt shotgun, Ion beamer, Magna-coil autocannon, Plasma knife)",
"Hernkyn Pioneer w/ pan-spectral scanner (Bolt revolver, Bolt shotgun, Magna-coil autocannon, Pan-spectral scanner, Plasma knife)",
"Hernkyn Pioneer w/ searchlight (Bolt revolver, Bolt shotgun, Magna-coil autocannon, Plasma knife, Rollbar searchlight)"
],
'_rules': mapWithKeys(["Deadly Demise D6", "Eye of the Ancestors", "Ruthless Efficiency"]),
'_rules': mapWithKeys(["Eye of the Ancestors", "Ignores Cover", "Ruthless Efficiency", "Scouts 9\""]),
'_abilities': {
"Abilities": mapWithKeys(["Damaged: 1-5 wounds remaining", "Fire Support", "Pan spectral scanner"]),
"Transport": mapWithKeys(["Hekaton Land Fortress"]),
"Abilities": mapWithKeys(["Outflanking Mag-Riders", "Pan-spectral scanner", "Rollbar searchlight"]),
}}),
jasmine.objectContaining({
'_name': "Hernkyn Pioneers",
'_cost': jasmine.objectContaining({_points: 90}),
'_name': "Hekaton Land Fortress",
'_cost': jasmine.objectContaining({_points: 225}),
'_profileTables': {
"Unit": jasmine.objectContaining({
'_headers': ["Unit","M","T","SV","W","LD","OC"],
'_contents': [
jasmine.arrayContaining(["Hernkyn Pioneer"]),
jasmine.arrayContaining(["Hernkyn Pioneer w/ ion beamer"]),
jasmine.arrayContaining(["Hekaton Land Fortress"]),
],
}),
"Ranged Weapons": jasmine.objectContaining({
'_headers': ["Ranged Weapons","Range","A","BS","S","AP","D","Keywords"],
'_contents': [
jasmine.arrayContaining(["Bolt revolver"]),
jasmine.arrayContaining(["Bolt shotgun"]),
jasmine.arrayContaining(["Magna-coil autocannon"]),
jasmine.arrayContaining(["Ion beamer"]),
jasmine.arrayContaining(["MATR autocannon"]),
jasmine.arrayContaining(["SP heavy conversion beamer"]),
jasmine.arrayContaining(["Twin bolt cannon"]),
],
}),
"Melee Weapons": jasmine.objectContaining({
'_headers': ["Melee Weapons","Range","A","WS","S","AP","D","Keywords"],
'_contents': [
jasmine.arrayContaining(["Plasma knife"]),
jasmine.arrayContaining(["Armoured wheels"]),
],
})
},
'_modelList': [
"Hernkyn Pioneer w/ ion beamer (Bolt revolver, Bolt shotgun, Ion beamer, Magna-coil autocannon, Plasma knife)",
"Hernkyn Pioneer w/ pan-spectral scanner (Bolt revolver, Bolt shotgun, Magna-coil autocannon, Pan-spectral scanner, Plasma knife)",
"Hernkyn Pioneer w/ searchlight (Bolt revolver, Bolt shotgun, Magna-coil autocannon, Plasma knife, Rollbar searchlight)"
"Hekaton Land Fortress (Armoured wheels, MATR autocannon, Pan spectral scanner, SP heavy conversion beamer, 2x Twin bolt cannon)"
],
'_rules': mapWithKeys(["Eye of the Ancestors", "Ignores Cover", "Ruthless Efficiency", "Scouts 9\""]),
'_rules': mapWithKeys(["Deadly Demise D6", "Eye of the Ancestors", "Ruthless Efficiency"]),
'_abilities': {
"Abilities": mapWithKeys(["Outflanking Mag-Riders", "Pan-spectral scanner", "Rollbar searchlight"]),
"Abilities": mapWithKeys(["Damaged: 1-5 wounds remaining", "Fire Support", "Pan spectral scanner"]),
"Transport": mapWithKeys(["Hekaton Land Fortress"]),
}}),
jasmine.objectContaining({
'_name': "Sagitaur",
Expand Down
125 changes: 83 additions & 42 deletions src/renderer40k10th.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,71 @@ export class Wh40kRenderer implements Renderer {
});
forceTitle.appendChild(table);

let body = document.createElement('tbody');
table.appendChild(body);
for (let unit of force._units) {
const tbody = table.appendChild(document.createElement('tbody'));
for (let i = 0; i < force._units.length; i++) {
const unit = force._units[i];
const tr = document.createElement('tr');
tr.id = `unit_summary_${i}`;
tr.appendChild(document.createElement('td')).appendChild(document.createTextNode(unit.nameWithExtraCosts()));
tr.appendChild(document.createElement('td')).appendChild(document.createTextNode(Wh40k.UnitRoleToString[unit._role]));
const models = tr.appendChild(document.createElement('td'));
this.renderModelList(models, unit);
tr.appendChild(document.createElement('td')).appendChild(document.createTextNode(unit._cost._points.toString()));
body.appendChild(tr);
tbody.appendChild(tr);
}

this.makeForceSummaryListItemsDraggable(tbody);
}
}

/** Make the table rows re-orderable via drag-and-drop. */
private makeForceSummaryListItemsDraggable(tbody: HTMLElement) {
for (const child of tbody.children) {
(child as HTMLElement).draggable = true;
child.classList.add('draggable');
}

let draggedItem: Element | null;
let dropTarget: Element | null;
tbody.addEventListener("dragstart", ev => {
draggedItem = (ev.target as Element).closest('[draggable]');
ev.dataTransfer!.effectAllowed = 'move';
});
tbody.addEventListener('dragover', ev => {
ev.preventDefault();
const target = (ev.target as Element).closest('[draggable]');
if (dropTarget === target) return;

dropTarget?.classList.remove('draggable_drop_target_top')
target?.classList.add('draggable_drop_target_top');
dropTarget = target;
});
tbody.addEventListener("drop", ev => {
ev.preventDefault();
dropTarget?.classList.remove('draggable_drop_target_top')
const target = (ev.target as Element).closest('[draggable]');
if (!draggedItem || draggedItem === target) return;

// TODO: drop the item after target if dropped on lower half of target
const container = draggedItem.parentElement!;
container.insertBefore(draggedItem, target);

// Reorder all the datasheets.
// NB: this is the only part of this function that's not generic; we
// could separate it out, and reuse the rest of this function as a
// general function under ./html/draggable.
// TODO: Fix behavior when identical datasheets have been merged.
const children = container.children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
const originalIndex = child.id.match(/unit_summary_(\d+)/)?.[1];
const datasheet = document.getElementById(`unit_details_${originalIndex}`);
if (!datasheet) continue;
datasheet.style.order = String(i);
}
});
}

private renderOptionsDiv(title: HTMLElement) {
const optionsDiv = title.appendChild(document.createElement('div'));
optionsDiv.classList.add('wh40k_options_div', 'd-print-none');
Expand Down Expand Up @@ -162,16 +213,10 @@ export class Wh40kRenderer implements Renderer {
});
renderCheckboxOption(optionsDiv, 'singleColumnDatasheets', 'Single-Column Datasheets',
(e: Event) => {
const singleColumnSheeets = document.getElementById('single_column_sheets');
const doubleColumnSheets = document.getElementById('double_column_sheets');
if (!singleColumnSheeets || !doubleColumnSheets) return;

if ((e.target as HTMLInputElement).checked) {
singleColumnSheeets.classList.remove('d-none')
doubleColumnSheets.classList.add('d-none')
document.body.classList.add('single_column');
} else {
singleColumnSheeets.classList.add('d-none')
doubleColumnSheets.classList.remove('d-none')
document.body.classList.remove('single_column');
}
});

Expand Down Expand Up @@ -216,7 +261,6 @@ export class Wh40kRenderer implements Renderer {
});
}


private renderAbilitiesByPhase(list: HTMLElement) {
if (!this._roster) return;

Expand Down Expand Up @@ -326,12 +370,8 @@ export class Wh40kRenderer implements Renderer {

const catalogueRules: Map<string, Map<string, string | null>> = new Map<string, Map<string, string | null>>();
const subFactionRules: Map<string, Map<string, string | null>> = new Map<string, Map<string, string | null>>();
const doubleColumnSheets = forces.appendChild(document.createElement('div'));
doubleColumnSheets.id = 'double_column_sheets';
const singleColumnSheets = forces.appendChild(document.createElement('div'));
singleColumnSheets.id = 'single_column_sheets';
singleColumnSheets.style.pageBreakBefore = "always";
singleColumnSheets.classList.add('d-none');
const dataSheetsDiv = forces.appendChild(document.createElement('div'));
dataSheetsDiv.classList.add('page_break');

for (const force of this._roster._forces) {
if (this._roster._forces.length > 1) {
Expand All @@ -348,11 +388,12 @@ export class Wh40kRenderer implements Renderer {

let h3 = document.createElement('h3');
h3.appendChild(forceTitle)
doubleColumnSheets.appendChild(h3);
dataSheetsDiv.appendChild(h3);
}

this.renderDatasheets(doubleColumnSheets, force._units);
this.renderDatasheets(singleColumnSheets, force._units, true);
dataSheetsDiv.style.display = 'flex';
dataSheetsDiv.style.flexDirection = 'column';
this.renderDatasheets(dataSheetsDiv, force._units);

mergeRules(catalogueRules, force._catalog, force._rules);
mergeRules(subFactionRules, force._faction, force._factionRules);
Expand All @@ -365,22 +406,24 @@ export class Wh40kRenderer implements Renderer {
forces.appendChild(rules);
}

private renderDatasheets(forces: HTMLElement, units: Wh40k.Unit[], singleColumn = false) {
private renderDatasheets(forces: HTMLElement, units: Wh40k.Unit[]) {
let numIdenticalUnits = 0;
for (let i = 0; i < units.length; i++) {
numIdenticalUnits++;
const unit = units[i];
const nextUnit = units[i + 1];
if (unit.equal(nextUnit)) continue;

this.renderUnitHtml(forces, unit, numIdenticalUnits, singleColumn);
this.renderUnitHtml(forces, unit, numIdenticalUnits, i);
numIdenticalUnits = 0
}
}

private renderUnitHtml(forces: HTMLElement, unit: Wh40k.Unit, unitCount: number, singleColumn = false) {
private renderUnitHtml(forces: HTMLElement, unit: Wh40k.Unit, unitCount: number, index: number) {
const statsDiv = forces.appendChild(document.createElement('div'));
statsDiv.classList.add('wh40k_unit_sheet');
statsDiv.id = `unit_details_${index}`;
statsDiv.style.order = String(index);
const statsTable = document.createElement('table');
statsTable.classList.add('table', 'table-sm', 'table-striped');
statsDiv.appendChild(statsTable);
Expand Down Expand Up @@ -413,35 +456,33 @@ export class Wh40kRenderer implements Renderer {
const tbody = statsTable.appendChild(document.createElement('thead'));
const tr = tbody.appendChild(document.createElement('tr'));

let profilesTable = statsTable;
if (!singleColumn) {
const profilesTd = tr.appendChild(document.createElement('td'));
profilesTd.colSpan = 12;
profilesTd.classList.add('subTableTd');
profilesTable = profilesTd.appendChild(document.createElement('table'));
profilesTable.classList.add('table', 'table-sm', 'table-striped');
}
// Create a new cell that, with CSS, can be displayed in one or two columns.
const singleOrDoubleColumnTd = tr.appendChild(document.createElement('td'));
singleOrDoubleColumnTd.colSpan = 20;
singleOrDoubleColumnTd.classList.add('subTableTd');
const singleOrDoubleColumnDiv = singleOrDoubleColumnTd.appendChild(document.createElement('div'));

// Tabular profile data, like model stats and weapons.
// Sort by unit, then weapons, then other stuff.
const profilesTable = singleOrDoubleColumnDiv.appendChild(
document.createElement('div').appendChild(
document.createElement('table')));
profilesTable.classList.add('table', 'table-sm', 'table-striped');

const typeNames = Object.keys(unit._profileTables).sort(Wh40k.CompareProfileTableName);
for (const typeName of typeNames) {
const table = unit._profileTables[typeName];
const widths = typeName === 'Unit' ? this._unitLabelWidthsNormalized : this._weaponLabelWidthNormalized;
this.renderSubTable(profilesTable, table._headers, table._contents, widths, 'Notes', [table]);
}

let abilitiesTable = statsTable;
if (!singleColumn) {
const abilitiesTd = tr.appendChild(document.createElement('td'));
abilitiesTd.colSpan = 8;
abilitiesTd.classList.add('subTableTd');
abilitiesTable = abilitiesTd.appendChild(document.createElement('table'));
abilitiesTable.classList.add('table', 'table-sm', 'table-striped');
}

// unit abilities and rules; rules are shared across units, with their
// descriptions printed in bulk later, but show up with unit 'Abilities'
const abilitiesTable = singleOrDoubleColumnDiv.appendChild(
document.createElement('div').appendChild(
document.createElement('table')));
abilitiesTable.classList.add('table', 'table-sm', 'table-striped');

if (!unit._abilities['Abilities'] && unit._rules.size > 0) {
this.renderUnitAbilitiesAndRules(abilitiesTable, 'Abilities', new Map(), unit._rules);
}
Expand Down
Loading

0 comments on commit ce080c3

Please sign in to comment.