in share/src/components/GameModder.tsx [502:781]
data: imageLiteralToBitmap(blank),
name: name,
callToAction: CALL_TO_ACTION[name],
default: textToBitmap(def)
};
})
this.state = {
userImages: imgs,
currentImg: 0,
currentBackground: 12
}
Object.assign(gameModderState, this.state)
}
this.tabImages = Object.keys(moddableImages)
.map(k => moddableImages[k])
.map(textToBitmap)
if (!(gameModderState as GameModderState).alertShown) this.alertTimeout = setTimeout(this.alertPlay, 5000);
}
// async renderExperiments() {
// let tabBar = this.refs["tab-bar"] as TabBar
// let dummyImg = createPngImg(20, 20, 64, 64)
// tabBar.TabBarSvg.appendChild(dummyImg)
// setInterval(() => {
// updatePngImg(dummyImg, this.spriteEditor.bitmap().image)
// }, 500)
// function getImages(ts: string) {
// let imgRegex = /img`([\d\s\.a-f]*)`/gm
// let match = imgRegex.exec(ts);
// let res: string[] = []
// while (match != null) {
// res.push(match[1])
// match = imgRegex.exec(ts);
// }
// return res
// }
// // HACK:
// let mainTs = bunny_hop_main_ts;
// // let mainTs = await getTxtFile("games/bunny_hop/main.ts")
// // TODO: find images
// let imgs = getImages(mainTs)
// // console.dir(imgs)
// let imgsAsBmps = imgs.map(textToBitmap)
// // console.dir(imgsAsBmps)
// }
private alertPlay = () => {
this.save();
(gameModderState as GameModderState).alertShown = true;
this.setState({ pulse: true });
}
private clearTimers = () => {
clearTimeout(this.alertTimeout);
}
private updateCurrentUserImage(bmp: Bitmap) {
// TODO: set image bug somehow?
function updateUserImage(old: UserImage, nw: Bitmap): UserImage {
return {
data: nw,
name: old.name,
callToAction: old.callToAction,
default: old.default
}
}
let newState = {
userImages: this.state.userImages.map((m, i) =>
i === this.state.currentImg
? updateUserImage(m, bmp)
: m)
}
this.setState(newState)
Object.assign(gameModderState, newState)
}
private save() {
if (this.spriteEditor && this.spriteEditor.editor) {
this.spriteEditor.editor.commit()
let newImg = this.spriteEditor.editor.bitmap().image
this.updateCurrentUserImage(newImg)
}
}
onTabChange(idx: number) {
this.save()
this.setState({ currentImg: idx })
if (IsGameModderState(gameModderState))
gameModderState.currentImg = idx
tickEvent("shareExperiment.mod.tabChange", { "tab": idx });
}
onBackgroundColorChanged(idx: number) {
this.setState({ currentBackground: idx })
tickEvent("shareExperiment.mod.changeBackground", { "color": idx });
if (IsGameModderState(gameModderState))
gameModderState.currentBackground = idx
}
onSpriteGalleryPick(bmp: Bitmap, idx?: number) {
tickEvent("shareExperiment.mod.galleryPick", { "tab": this.state.currentImg, "item": idx });
this.updateCurrentUserImage(bmp)
}
render() {
let currImg = this.state.userImages[this.state.currentImg]
let isBackgroundTab = this.state.currentImg === 3
let body = document.getElementsByTagName('body')[0]
// const MARGIN = 20
const HEADER_HEIGHT = 50
let actualWidth = body.clientWidth
let actualHeight = body.clientHeight - HEADER_HEIGHT
let refWidth = 539.0
let refHeight = SE.TOTAL_HEIGHT
let wScale = actualWidth / refWidth
let hScale = actualHeight / refHeight
this.scale = Math.min(wScale, hScale)
const SPRITE_GALLERY_HEIGHT = 100
let spriteGalleryHeight = SPRITE_GALLERY_HEIGHT * this.scale
let colorPickerHeight = (SE.TOTAL_HEIGHT + SPRITE_GALLERY_HEIGHT) * this.scale
// TODO
let samples = [
SAMPLE_CHARACTERS,
SAMPLE_OBSTACLES,
SAMPLE_OBSTACLES2
]
let spriteGalleryOptions =
(samples[this.state.currentImg] || SAMPLE_CHARACTERS)
.map(i => imageLiteralToBitmap(i))
let startImg = this.state.userImages[this.state.currentImg].data
let galKey = `tab${this.state.currentImg}__` + spriteGalleryOptions.map(b => b.buf.toString()).join("_")
let galProps: SpriteGalleryProps = {
height: spriteGalleryHeight,
options: spriteGalleryOptions,
onClick: this.onSpriteGalleryPick.bind(this)
}
return (
<div className="game-modder">
<h1 ref="header" className="what-to-do-header">{currImg.callToAction}</h1>
<TabBar ref="tab-bar" tabImages={this.tabImages}
tabChange={this.onTabChange.bind(this)} startTab={this.state.currentImg} />
{isBackgroundTab
?
<ColorPicker selectionChanged={this.onBackgroundColorChanged.bind(this)}
selected={this.state.currentBackground} colors={SE.COLORS}
height={colorPickerHeight}></ColorPicker>
:
<SpriteEditorComp ref="sprite-editor" startImage={startImg}
onPlay={this.onPlay} scale={this.scale} galleryProps={galProps}></SpriteEditorComp>
}
{/* <div ref="sprite-gallery" className="sprite-gallery">
</div> */}
<button ref="play-btn" className={`play-btn ${this.state.pulse ? "shake" : ""}`}>
<span>Play</span>
<i className="icon play"></i>
</button>
</div>
)
}
async componentDidMount() {
this.playBtn = this.refs["play-btn"] as HTMLButtonElement;
this.spriteEditor = this.refs["sprite-editor"] as SpriteEditorComp;
this.header = this.refs['header'] as HTMLHeadingElement
// events
this.playBtn.addEventListener('click', this.onPlay.bind(this))
// HACK: Disable scrolling in iOS
document.ontouchmove = function (e) {
e.preventDefault();
}
}
componentDidUpdate() {
this.spriteEditor = this.refs["sprite-editor"] as SpriteEditorComp;
}
componentWillUnmount() {
this.playBtn = undefined;
this.spriteEditor = undefined;
this.header = undefined;
this.clearTimers();
}
async onPlay() {
this.save();
(gameModderState as GameModderState).alertShown = true;
const toReplace = this.state.userImages.filter(ui => !isEmptyBitmap(ui.data));
function modBackground(bin: string, newColor: number): string {
const originalColor = 13
const template = (color: number) => `scene_setBackgroundColor__P935_mk(s);s.tmp_0.arg0=${color}`
let old = template(originalColor)
let newIdx = newColor + 1 // arcade function is 1-based b/c 0 is transparent
let nw = template(newIdx)
return bin.replace(old, nw)
}
function modBackgroundTs(bin: string, newColor: number): string {
const originalColor = 13
const template = (color: number) => `scene.setBackgroundColor(${color})`
let old = template(originalColor)
let newIdx = newColor + 1 // arcade function is 1-based b/c 0 is transparent
let nw = template(newIdx)
return bin.replace(old, nw)
}
function modImg(bin: string, img: UserImage): string {
// HACK: for some reason the compiler emits image prefixes that look like:
// 8704100010000000
// whereas ours look like:
// e4101000
const MOD_PREFIX_LEN = "e4101000".length
const BIN_PREFIX_LEN = "8704100010000000".length
let newHex = bitmapToBinHex(img.data)
const oldToFind = bitmapToBinHex(img.default)
.slice(MOD_PREFIX_LEN)
let oldStartIncl = bin.indexOf(oldToFind) - BIN_PREFIX_LEN
if (oldStartIncl < 0)
return bin;
let oldEndExcl = bin.indexOf(`"`, oldStartIncl)
let oldHex = bin.slice(oldStartIncl, oldEndExcl)
return bin.replace(oldHex, newHex)
}
let gameBinJs = bunny_hop_bin_js
let gameMainTs = bunny_hop_main_ts
let gameMainBlocks = bunny_hop_main_blocks;
for (let i of toReplace) {
const def = bitmapToText(i.default);
const user = bitmapToText(i.data);
gameBinJs = modImg(gameBinJs, i)
gameMainTs = replaceImages(gameMainTs, def, user);
gameMainBlocks = replaceImages(gameMainBlocks, def, user);
}
gameBinJs = modBackground(gameBinJs, this.state.currentBackground)
gameMainTs = modBackgroundTs(gameMainTs, this.state.currentBackground);
const screenshot = await mkScreenshotAsync(this.state.currentBackground + 1, this.state.userImages.map(u => isEmptyBitmap(u.data) ? u.default : u.data));
this.props.playHandler({
binJs: gameBinJs,
mainTs: gameMainTs,
mainBlocks: gameMainBlocks,
screenshot
});
}
}
function replaceImages(sourceFile: string, toReplace: string, userImage: string) {
const sourceLines = sourceFile.split(/\n/).map(l => l.trim());
const replaceLines = toReplace.split(/\n/).map(l => l.trim()).slice(1, -1);
userImage = userImage.replace("img`", "").replace("`", "");
let foundMatch = false;
for (let i = 0; i < sourceLines.length; i++) {
if (sourceLines[i] === replaceLines[0]) {
foundMatch = true;
for (let j = 1; j < replaceLines.length; j++) {
if (sourceLines[i + j] != replaceLines[j]) {