import "phaser"; import FightScene from "./fight_scene"; import * as Types from "../../../backend/src/types"; // const HYPERASCENDERS = /[ÄÖÜ]/; const ASCENDERS = /[ABCDEFGHIJKLMNOPQRSTUVWXYZbdfhijklstäöüß]/; const DESCENDERS = /[AFHJPQYZÄfghjpqsyzß]/; const CONCEAL_TINT = 0xaaaaaa; class Clue extends Phaser.GameObjects.Sprite { word: Types.Word; scene: FightScene; textureKey: string; body: Phaser.Physics.Arcade.Body; baseHeight: number; constructor(scene: FightScene, word: Types.Word) { // TODO: set positions super(scene, 0, 0, "__MISSING"); scene.add.existing(this); this.setAlpha(0); this.scene = scene; this.word = word; this.baseHeight = Math.max(this.scene.cameras.main.width * 0.035, 25); // max(3.5vw,25px) // TODO: we could be smarter and fully leverage caching, but meh. this.textureKey = `${word.id}-${Date.now()}`; this.loadTexture(); } loadTexture() { // this.scene.textures.remove() this.scene.textures.addBase64(this.textureKey, this.word.image); this.scene.textures.once( "addtexture", this.showTexture.bind(this), this.scene, ); } estimateWordHeight() { let height = 1.0; // if (this.word.ocr_transcript.match(HYPERASCENDERS)) height += 0.2; if (this.word.ocr_transcript.match(ASCENDERS)) height += 0.2; if (this.word.ocr_transcript.match(DESCENDERS)) height += 0.2; return height; } applyTexture() { this.setTexture(this.textureKey); const scale = (this.estimateWordHeight() * this.baseHeight) / this.texture.getSourceImage().height; this.setScale(scale); } showTexture() { if (!this.scene.scene.isActive()) return; this.applyTexture(); this.body = new Phaser.Physics.Arcade.Body(this.scene.physics.world, this); this.scene.physics.world.add(this.body); this.scene.cluesGroup.add(this); this.setPositionForDrop(); this.fadeIn(); } setPositionForDrop() { const bounds = this.body.customBoundsRectangle; const x = this.findBestDropPosition(bounds, this.scene.cluesGroup.children); // const x = this.findRandDropPosition(bounds); const y = bounds.top + this.displayHeight * 0.5; this.setPosition(x, y); } findRandDropPosition(bounds: Phaser.Geom.Rectangle) { return ( bounds.left + this.displayWidth * 0.5 + Math.random() * (bounds.width - this.displayWidth) ); } findBestDropPosition( bounds: Phaser.Geom.Rectangle, siblings: Phaser.Structs.Set<Phaser.GameObjects.GameObject>, pad = 10, ) { const minX = Math.ceil(bounds.left); const maxX = Math.floor(bounds.right); const xCount = maxX - minX + 1; const xScores = Array(xCount).fill(0); xScores.forEach((_, i, xs) => { (siblings as Phaser.Structs.Set<Clue>).each((clue) => { if (clue == this) return; const clueBounds = clue.getBounds(); const x = i + minX; let intersect = true; intersect &&= clueBounds.left - pad < x; intersect &&= x < clueBounds.right + pad; const pileHeight = bounds.bottom - clueBounds.bottom; xs[i] += (intersect ? 1 : 0) * pileHeight; }); }); const boxWidth = Math.ceil(this.displayWidth); const boxPosScores = Array(xCount - boxWidth).fill(0); boxPosScores.forEach((_, i, ps) => { ps[i] = xScores.slice(i, i + boxWidth).reduce((a, b) => a + b, 0); }); const bestBoxPos = boxPosScores.indexOf(Math.min(...boxPosScores)); return bounds.left + 0.5 * this.displayWidth + bestBoxPos; } delete() { this.fadeOut(() => { this.texture.destroy(); this.destroy(); }); } fadeIn(onComplete?: Phaser.Types.Tweens.TweenOnCompleteCallback) { this.scene.tweens.add({ targets: this, alpha: 1, ease: "Linear", delay: 0, duration: 100, onComplete: onComplete, }); } fadeOut(onComplete?: Phaser.Types.Tweens.TweenOnCompleteCallback) { this.scene.tweens.add({ targets: this, alpha: 0, ease: "Linear", delay: 0, duration: 100, onComplete: onComplete, }); } uncover() { this.clearTint(); } conceal() { this.setTintFill(CONCEAL_TINT); } } export default Clue;