thepeach's Forum Posts

  • Is it possible to determine when a tagged master keyframe has been reached in a timeline using the scripting interfaces?

    In event sheets, this feature is equivalent to Timeline Controller’s "on keyframe reached" event.

  • Looks like a fix is on the way (#7998). Apologies for the duplicate report.

  • In r390, this error message is logged to the console when destroying a Sprite object with a behavior (e.g. the Solid behavior).

    TypeError: this._iScriptInterface._release is not a function at BehaviorInstance.Release (behaviorInstance.js:2:401) ...

    The error does not occur in r388 (stable), and can be reproduced with the following code (where the instance returned from runtime.objects.Sprite.getFirstInstance() is a Sprite object with a behavior):

    main.js

    runOnStartup(async runtime => {
    	runtime.addEventListener('beforeprojectstart', () => {
    		runtime.objects.Sprite.getFirstInstance()?.destroy();
    	});
    });
    
  • Glad to help. Really appreciate you getting the fix out so quickly!

  • Is the TypeScript definition for IWorldInstance's testOverlapSolid() method correct? The docs say:

    This returns the instance interface class for the first instance with the solid behavior that was found to overlap this instance, or null if none.

    However, the type definition in IWorldInstance.d.ts is testOverlapSolid(): boolean;.

    I'm currently working around this by using the ICollisionEngine interface directly (e.g. runtime.collisions.testOverlapSolid(myInstance)).

  • Gotcha. Yeah, those would be nice performance optimizations to make. Thanks again for the suggestions.

  • Thanks for the feedback! That's definitely worth considering.

    With that said, unless the current character is a whitespace, I don't believe line 69 tests if the next word will wrap (since the logical AND expression is a short-circuit operator).

  • If anyone needs a workaround, here's my quick implementation (in TypeScript). There's certainly room for improvement, but it solves my issue for now:

    Example Usage

    import Typewriter from './Typewriter.js';
    
    // ...
    
    // Create a new Typewriter instance.
    const textInst = runtime.objects.Text.getFirstInstance()!;
    const typewriter = new Typewriter(textInst);
    
    // Callback executed when a character is added.
    typewriter.onTypeHandler = (char, _) => {
    	console.log(`Typed character: ${char}`);
    }
    
    // Callback executed when typing finishes.
    typewriter.onFinishedHandler = text => {
    	console.log(`Finished typing text: ${text}`);
    }
    
    // Start the effect with the specified arguments.
    const text = 'Hello, World! This is a typewriter effect with callbacks.';
    const charsPerSecond = text.length / 30;
    typewriter.start(text, charsPerSecond);
    

    Typewriter.ts

    export default class Typewriter {
    	readonly textInstance: ITextInstance;
    
    	onTypeHandler: ((char: string, text: string) => void) | null = null;
    	onFinishedHandler: ((text: string) => void) | null = null;
    
    	get isTyping() { return this.charIndex > -1; }
    
    	private text = '';
    	private charIndex = -1;
    	private charsPerSecond = 0;
    	private timeSinceLastType = 0;
    
    	constructor(textInstance: ITextInstance) {
    		this.textInstance = textInstance;
    		this.onTick = this.onTick.bind(this);
    		this.textInstance.runtime.addEventListener('tick', this.onTick);
    	}
    
    	destroy() {
    		this.textInstance.runtime.removeEventListener('tick', this.onTick);
    	}
    
    	start(text: string, duration: number) {
    		if (this.isTyping) this.finish();
    
    		this.text = text;
    
    		if (text.length > 0 && duration > 0) {
    			this.charIndex = 0;
    			this.charsPerSecond = text.length / duration;
    			this.timeSinceLastType = 0;
    			this.textInstance.text = '';
    		} else {
    			this.finish();
    		}
    	}
    
    	finish() {
    		this.charIndex = -1;
    		this.textInstance.text = this.text;
    		this.onFinishedHandler?.(this.text);
    	}
    
    	private onTick() {
    		if (!this.isTyping) return;
    
    		while (this.isTyping && this.timeSinceLastType >= 1 / this.charsPerSecond) {
    			this.type();
    			this.timeSinceLastType -= 1 / this.charsPerSecond;
    		}
    
    		this.timeSinceLastType += this.textInstance.runtime.dt;
    	}
    
    	private type() {
    		if (this.charIndex === this.text.length - 1) {
    			this.finish();
    			return;
    		}
    
    		// Type the next character.
    		const char = this.text.charAt(this.charIndex);
    		this.textInstance.text += char;
    		this.onTypeHandler?.(char, this.textInstance.text);
    		this.charIndex++;
    
    		// Add a line break if the next word would exceed the text width.
    		if (char === ' ' && this.textInstance.wordWrapMode === 'word' && this.nextWordWillWrap()) {
    			this.textInstance.text =
    				this.textInstance.text.substring(0, this.charIndex - 1) + '\n' +
    				this.textInstance.text.substring(this.charIndex);
    		}
    	}
    
    	private nextWordWillWrap(): boolean {
    		const lines = this.textInstance.text.split('\n');
    		const currentLine = lines[lines.length - 1] || '';
    		const nextSpaceCharIndex = this.text.indexOf(' ', this.charIndex);
    		const nextWordEndIndex = (nextSpaceCharIndex === -1) ? undefined : nextSpaceCharIndex;
    		const nextWord = this.text.substring(this.charIndex, nextWordEndIndex);
    		const lineWidth = this.getLineWidth(currentLine + nextWord);
    
    		return lineWidth > this.textInstance.width;
    	}
    
    	private getLineWidth(text: string): number {
    		// Cache the text instance's state.
    		const originalText = this.textInstance.text;
    		const originalWidth = this.textInstance.width;
    
    		// Measure the full width of the line.
    		this.textInstance.text = text;
    		this.textInstance.width = Infinity;
    		const lineWidth = this.textInstance.textWidth;
    
    		// Restore the text instance's state.
    		this.textInstance.text = originalText;
    		this.textInstance.width = originalWidth;
    
    		return lineWidth;
    	}
    }
    
  • Using the typewriterText(str, duration) method on ITextInstance, is there a way to track added characters or know when typing ends? The instance's text property always reflects the value of the str argument, not the current text state. Here's a simplified code example:

    runOnStartup(async runtime => {
    	runtime.addEventListener('beforeprojectstart', () => onBeforeProjectStart(runtime));
    });
    
    async function onBeforeProjectStart(runtime: IRuntime) {
    	const str = 'Hello, World! This is a typewriter effect.';
    	const duration = str.length / 30;
    	const textInst = runtime.objects.Text.getFirstInstance()!;
    	textInst.typewriterText(str, duration);
    
    	runtime.addEventListener('tick', () => {
    		console.log(textInst.text === str); // Always true
    	});
    }
    
  • Ah, makes sense. Thanks very much for the clarification.

  • Works great for the “Simple” mode!

    Should this solution also work for the “Advanced” mode? I'm running into an issue:

    [JSC_UNDEFINED_VARIABLE] variable InstanceType is undeclared

    Main.ts

    import MySprite from './MySprite.js';
    
    runOnStartup(async runtime => {
     runtime.objects.MySprite.setInstanceClass(MySprite);
    });
    

    MySprite.ts

    export default class MySprite extends InstanceType.MySprite {
    }
    
  • Thanks for the info, Ashley. I suspected that might be the case. I will minify after exporting for now.

  • Try Construct 3

    Develop games in your browser. Powerful, performant & highly capable.

    Try Now Construct 3 users don't see these ads
  • I encountered an issue when trying to minify my TypeScript project for web using "Simple" or "Advanced" mode:

    [JSC_LANGUAGE_FEATURE] This language feature is only supported for UNSTABLE mode or better: Public class fields.

    The error is from using public class fields, which are necessary for TypeScript, but also nice to have in vanilla JavaScript projects.

    Are there any recommended pipelines or solutions out there for working around this issue?

  • Here's the solution for anyone else that overlooked Ashley's article on using TypeScript in Construct:

    When using TypeScript, Construct generates a special class representing an instance for every object type and family in the project. This includes type definitions for things like the instance variables, behaviors and effects specific to that object. These classes are all in the InstanceType namespace with the name of the object.

    So, the solution was to extend DragItem from InstanceType.DragItem.

    export default class DragItem extends InstanceType.DragItem {
    	// ...
    }
    
  • How do we get behaviors on an ISpriteInstance subclass in TypeScript?

    When attempting to get the behaviors property of my subclass, I see the following error:

    Property 'behaviors' does not exist on type 'DragItem'.

    Here's my truncated code:

    main.ts

    import DragItem from './DragItem.js';
    
    runOnStartup(async runtime => {
    	runtime.objects.DragItem.setInstanceClass(DragItem);
    	// ...
    });
    

    DragItem.ts

    export default class DragItem extends ISpriteInstance {
    	constructor() {
    		super();
    		const dragDrop = this.behaviors.DragDrop; // Error
    	}
    }
    

    instanceTypes.d.ts

    (Generated)

    class __DragItemBehaviors<InstType> {
    	DragDrop: IDragDropBehaviorInstance<InstType>;
    }