Skip to content

Commit

Permalink
Feat: Undo/redo ride painting
Browse files Browse the repository at this point in the history
  • Loading branch information
ltsSmitty committed Aug 7, 2022
1 parent b26e0a5 commit 8158178
Show file tree
Hide file tree
Showing 6 changed files with 288 additions and 83 deletions.
51 changes: 51 additions & 0 deletions src/Components/RidePaintSection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { button, compute, horizontal } from "openrct2-flexui";
import FeatureController from "../controllers/FeatureController";
import ColourChange from "../themeSettings/ColourChange";

const ridePaintSection = (fc: FeatureController) => {
const rc = fc.rideController;
const { rideHistory } = rc;

const layout = horizontal({
// height:80,
content: [
// undo button
button({
text: "Undo",
padding: "5px",
disabled: compute(rideHistory.undoPointer, (pointer) => pointer <= 0),
onClick: () => rideHistory.undoLastPaint(),
}),

// ride paint button
button({
height: 30,
padding: "5px",
// width: "80%",
text: "6. Paint selected rides",
disabled: compute(
fc.rideController.selectedRides,
(rides) => (rides?.length || -1) <= 0
),
onClick: () => ColourChange.colourRides(fc),
tooltip: `Nothing changing?
Make sure to enable 'Allow repainting of already painted rides'`,
}),

// paint redo button
button({
text: "Redo",
padding: "5px",
disabled: compute(
rideHistory.undoPointer,
(pointer) => pointer >= rideHistory.ridePaintHistory.get().length - 1
),
onClick: () => rideHistory.redoPaint(),
}),
],
});

return layout;
};

export default ridePaintSection;
136 changes: 66 additions & 70 deletions src/controllers/RideController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,90 +2,86 @@ import { Store, store, ArrayStore, arrayStore } from "openrct2-flexui";
import BaseController from "./BaseController";
import { RideType } from "../helpers/RideType";
import { debug } from "../helpers/logger";
import RideHistory from "../helpers/RideHistory";

export default class RideController extends BaseController<Ride> {
selectedRides: ArrayStore<Ride>;
selectedRides: ArrayStore<Ride>;

selectedText: Store<string>;
selectedText: Store<string>;

paintedRides: ArrayStore<Ride | null>;
paintedRides: ArrayStore<Ride | null>;

allRideTypes!: ArrayStore<RideType>;
allRideTypes!: ArrayStore<RideType>;

paintToggle: Store<number>;
paintToggle: Store<number>;

constructor() {
const allRides = map.rides.filter(
(ride) => ride.classification === "ride"
);
super(allRides);
rideHistory: RideHistory;

debug(
`RideController, all: ${this.all.get().map((ride) => ride.name)}`
);
// set the ride types
this.allRideTypes = arrayStore<RideType>([]);
this.updateAllRideTypes();
this.selected = store<Ride | null>(null);
this.selectedRides = arrayStore<Ride>([]);
this.paintedRides = arrayStore<Ride | null>([]);
this.selectedText = store<string>("");
this.paintToggle = store<number>(0);
}
constructor() {
const allRides = map.rides.filter((ride) => ride.classification === "ride");
super(allRides);

/**
* Update the model's values for rideController.all and rideController.allRideTypes
*/
updateRideModel() {
this.updateAllRides();
this.updateAllRideTypes();
}
debug(`RideController, all: ${this.all.get().map((ride) => ride.name)}`);
// set the ride types
this.allRideTypes = arrayStore<RideType>([]);
this.updateAllRideTypes();
this.selected = store<Ride | null>(null);
this.selectedRides = arrayStore<Ride>([]);
this.paintedRides = arrayStore<Ride | null>([]);
this.selectedText = store<string>("");
this.paintToggle = store<number>(0);
this.rideHistory = new RideHistory(this);
}

private updateAllRides() {
const allRides = map.rides.filter(
(ride) => ride.classification === "ride"
);
this.all.set(allRides);
}
/**
* Update the model's values for rideController.all and rideController.allRideTypes
*/
updateRideModel() {
this.updateAllRides();
this.updateAllRideTypes();
}

private updateAllRideTypes() {
const allRideTypes = this.all.get().map((ride) => ride.type);
const uniqueRideTypes = allRideTypes
// get the unique ride types
.filter(onlyUnique)
// get only non-zero/truthy values
.filter((n) => n);
this.allRideTypes.set(uniqueRideTypes);
// debug(`<Controller>rc.allRideTypes updated: ${uniqueRideTypes}`)
return uniqueRideTypes;
/**
* Helper to get unique ride types
*/
function onlyUnique(value: any, index: any, self: any) {
return self.indexOf(value) === index;
}
}
private updateAllRides() {
const allRides = map.rides.filter((ride) => ride.classification === "ride");
this.all.set(allRides);
}

private updateAllRideTypes() {
const allRideTypes = this.all.get().map((ride) => ride.type);
const uniqueRideTypes = allRideTypes
// get the unique ride types
.filter(onlyUnique)
// get only non-zero/truthy values
.filter((n) => n);
this.allRideTypes.set(uniqueRideTypes);
// debug(`<Controller>rc.allRideTypes updated: ${uniqueRideTypes}`)
return uniqueRideTypes;
/**
* Set this.selectedText to display which rides are selected, e.g. "3/10 rides selected"
* Helper to get unique ride types
*/
setSelectedRidesText() {
const selectedRides = this.selectedRides.get() || [];
this.selectedText.set(
`{BLACK}${selectedRides.length}/${
this.all.get().length
} rides selected`
);
function onlyUnique(value: any, index: any, self: any) {
return self.indexOf(value) === index;
}
}

override getActive() {
return {
all: this.all,
selected: this.selected,
selectedIndex: this.selectedIndex,
allRideTypes: this.allRideTypes,
selectedRides: this.selectedRides,
selectedText: this.selectedText,
};
}
/**
* Set this.selectedText to display which rides are selected, e.g. "3/10 rides selected"
*/
setSelectedRidesText() {
const selectedRides = this.selectedRides.get() || [];
this.selectedText.set(
`{BLACK}${selectedRides.length}/${this.all.get().length} rides selected`
);
}

override getActive() {
return {
all: this.all,
selected: this.selected,
selectedIndex: this.selectedIndex,
allRideTypes: this.allRideTypes,
selectedRides: this.selectedRides,
selectedText: this.selectedText,
};
}
}
125 changes: 125 additions & 0 deletions src/helpers/RideHistory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { ArrayStore, Store, arrayStore, store } from "openrct2-flexui";
import RideController from "../controllers/RideController";
import { debug } from "./logger";
import { convertRideToProxy, RideProxy } from "./RideProxy";
import ColourChange from "../themeSettings/ColourChange";

class RideHistory {
/**
* Version control history. Adds a new entry each time a ride/rides are painted.
*/
ridePaintHistory: ArrayStore<RideProxy[]>;

/**
* Version control pointer to track the place in the undo/redo history
*/
undoPointer: Store<number>;

rc: RideController;

constructor(rc: RideController) {
this.ridePaintHistory = arrayStore<RideProxy[]>([]);
this.undoPointer = store<number>(0);
this.rc = rc;
}

/**
* Add the rides being painted to the paint version control.
*/
pushRidesToPaintHistory = (rides: Ride[]) => {
// convert to a shallower version of ride for cheaper storage
const ridesProxy = rides.map((ride) => convertRideToProxy(ride));
const pointer = this.undoPointer;
const startingIndex = pointer.get();
// used for the splice to know where to start inserting/deleting.
const deletionPoint = this.ridePaintHistory.get().length - pointer.get();

// if the user has undone some paints (and therefore is not at the top of the stack),
// those paint histories will be lost
const numPaints = this.ridePaintHistory.splice(
startingIndex,
deletionPoint,
ridesProxy
);
// move the pointer up by 1 to point at the newest paint job
pointer.set(pointer.get() + 1);

debug(
`\nPushed ${rides.length} rides to paint history in this batch.
${numPaints.length} paint histories were discarded.`
);
};

public undoLastPaint = () => {
const pointer = this.undoPointer;

// if there have been no paints
// or if you've undone all the way back to the beginning
if (pointer.get() <= 0) {
debug(`there are no paints to undo`);
return;
}

// if it's on the most recent paint, add it to the array right before undoing
debug(`\nUndoing paint.`);

if (pointer.get() === this.ridePaintHistory.get().length) {
debug(`At stack header, so adding current paint to the stack.`);
this.pushRidesToPaintHistory(this.rc.selectedRides.get());
// Compensation for pointer being incremented during pushRidesToPaintHistory()
pointer.set(pointer.get() - 1);
}
pointer.set(pointer.get() - 1);
const ridesToUndoPaint = this.ridePaintHistory.get()[pointer.get()];
ridesToUndoPaint.map((ride) =>
ColourChange.setRideColour(
ride as Ride,
...this.getColoursFromProxy(ride)
)
);
// refresh selected rides to update right column
this.rc.selectedRides.set([...this.rc.selectedRides.get()]);
};

public redoPaint = () => {
const pointerNumber = this.undoPointer.get();
// make sure pointer isn't already at the head
if (pointerNumber >= this.ridePaintHistory.get().length) {
debug(`there are no paints to redo`);
return;
}

this.undoPointer.set(pointerNumber + 1);
// get the rides to redo painting
const ridesToUndoPaint =
this.ridePaintHistory.get()[this.undoPointer.get()];
debug(`\nRedoing paintjob.`);
debug(
`${ridesToUndoPaint.length} rides which will be brought back to redone state.`
);

if (!ridesToUndoPaint) {
debug(`no rides to undo`);
return;
}
ridesToUndoPaint.map((ride) =>
ColourChange.setRideColour(
ride as Ride,
...this.getColoursFromProxy(ride)
)
);
// refresh selected rides to update right column
this.rc.selectedRides.set([...this.rc.selectedRides.get()]);
};

private getColoursFromProxy = (ride: RideProxy) => [
ride.colourSchemes[0].main,
ride.colourSchemes[0].additional,
ride.colourSchemes[0].supports,
ride.vehicleColours[0].body,
ride.vehicleColours[0].trim,
ride.vehicleColours[0].tertiary,
];
}

export default RideHistory;
28 changes: 28 additions & 0 deletions src/helpers/RideProxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { debug } from "./logger";

export type RideProxy = {
id: number;
name: string;
vehicleColours: VehicleColour[];
colourSchemes: TrackColour[];
stationStyle: number;
};

export const convertRideToProxy = (ride: Ride): RideProxy => {
const { id, name, vehicleColours, colourSchemes, stationStyle } = ride;
return {
id,
name,
vehicleColours: [vehicleColours[0]],
colourSchemes: [colourSchemes[0]],
stationStyle,
};
};

export const hydrateRideProxy = (rideProxy: RideProxy): Ride => {
debug(`prepping to rehydrate ${JSON.stringify(rideProxy)}`);
return map.rides.filter((ride) => ride.id === rideProxy.id)[0];
};

// map all rides into proxies
// save into an array
3 changes: 3 additions & 0 deletions src/themeSettings/ColourChange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ export default class ColourChange {

const ridesToTheme = filterRidesToTheme(ridesToPaint);

// add the final colour to
rideController.rideHistory.pushRidesToPaintHistory(ridesToTheme);

// group rides together so they're painted identically
const groupedRides = currentGrouping.applyGrouping(ridesToTheme);

Expand Down
Loading

0 comments on commit 8158178

Please sign in to comment.