Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 90 additions & 1 deletion src/rpg/Actor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Container, Sprite, Texture } from "pixi.js";
import { Container, Graphics, Sprite, Text, Texture } from "pixi.js";
import { HealthBar } from "./HealthBar.ts";

export abstract class Actor extends Container {
Expand Down Expand Up @@ -124,6 +124,10 @@ export abstract class Actor extends Container {
private twitchDistance: number = 30;
private twitchDirection: number = 1; // 1 = forward, -1 = backward

private speechBubble: Container | null = null;
private speechBubbleTimeout: ReturnType<typeof setTimeout> | null = null;
private resolveSpeechBubble: (() => void) | null = null;

runLeft(): Promise<void> {
this.sprite.scale.x = this.sprite.scale.x * -1;
this.isRunningLeft = true;
Expand Down Expand Up @@ -255,6 +259,91 @@ export abstract class Actor extends Container {
this.healthBar.update(delta);
this.healthBar.y = -this.sprite.height + offset - 20 + shakeY;
this.healthBar.x = shakeX;

if (this.speechBubble) {
this.speechBubble.y = -this.sprite.height + offset - 40 + shakeY;
this.speechBubble.x = shakeX;
}
}

async showSpeechBubble(text: string, duration: number): Promise<void> {
this.hideSpeechBubble();

const bubble = new Container();

const textObj = new Text({
text,
style: {
fontSize: 14,
fill: 0x000000,
wordWrap: true,
wordWrapWidth: 150,
fontFamily: "Arial",
},
});

const padding = 10;
const tailHeight = 10;
const bubbleWidth = textObj.width + padding * 2;
const bubbleHeight = textObj.height + padding * 2;
const r = 8;
const tw = 8;
const cx = bubbleWidth / 2;

const bg = new Graphics();
bg.moveTo(r, 0);
bg.lineTo(bubbleWidth - r, 0);
bg.arcTo(bubbleWidth, 0, bubbleWidth, r, r);
bg.lineTo(bubbleWidth, bubbleHeight - r);
bg.arcTo(bubbleWidth, bubbleHeight, bubbleWidth - r, bubbleHeight, r);
bg.lineTo(cx + tw, bubbleHeight);
bg.lineTo(cx, bubbleHeight + tailHeight);
bg.lineTo(cx - tw, bubbleHeight);
bg.lineTo(r, bubbleHeight);
bg.arcTo(0, bubbleHeight, 0, bubbleHeight - r, r);
bg.lineTo(0, r);
bg.arcTo(0, 0, r, 0, r);
bg.closePath();
bg.fill({ color: 0xffffff });
bg.stroke({ width: 2, color: 0x333333 });

textObj.x = padding;
textObj.y = padding;

bubble.addChild(bg);
bubble.addChild(textObj);

bubble.pivot.set(cx, bubbleHeight + tailHeight);

this.speechBubble = bubble;
this.addChild(bubble);

if (duration === -1) {
return;
}

return new Promise<void>((resolve) => {
this.resolveSpeechBubble = resolve;
this.speechBubbleTimeout = setTimeout(() => {
this.hideSpeechBubble();
}, duration * 1000);
});
}

hideSpeechBubble(): void {
if (this.speechBubbleTimeout !== null) {
clearTimeout(this.speechBubbleTimeout);
this.speechBubbleTimeout = null;
}
if (this.speechBubble) {
this.removeChild(this.speechBubble);
this.speechBubble.destroy();
this.speechBubble = null;
}
if (this.resolveSpeechBubble) {
this.resolveSpeechBubble();
this.resolveSpeechBubble = null;
}
}

async attack(targets: Actor[]): Promise<{
Expand Down
32 changes: 32 additions & 0 deletions src/rpg/actors.html
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,38 @@ <h3>Animations</h3>
<button id="btn-heal" disabled>Heal Max</button>
<button id="btn-reset" disabled>Reset Position</button>
</div>
<div class="row" style="margin-top: 8px">
<input
type="text"
id="bubble-text"
value="Hello!"
placeholder="Bubble text"
style="
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
color: #e0e0e0;
font-family: inherit;
font-size: 14px;
padding: 6px 10px;
width: 160px;
"
/>
<label
>Duration
<input
type="number"
id="bubble-duration"
value="2"
step="0.5"
style="width: 70px"
/></label>
<button id="btn-show-bubble" disabled>Show Bubble</button>
<button id="btn-show-bubble-forever" disabled>
Show Bubble (-1)
</button>
<button id="btn-hide-bubble" disabled>Hide Bubble</button>
</div>
<div id="log"></div>
</div>
</div>
Expand Down
27 changes: 27 additions & 0 deletions src/rpg/actors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ function setButtons(enabled: boolean) {
"btn-level-up",
"btn-heal",
"btn-reset",
"btn-show-bubble",
"btn-show-bubble-forever",
"btn-hide-bubble",
];
for (const id of ids) {
($(id) as HTMLButtonElement).disabled = !enabled;
Expand Down Expand Up @@ -339,6 +342,30 @@ async function init() {
log("Reset actor position & state");
});

$("btn-show-bubble").addEventListener("click", async () => {
if (!currentActor) return;
const text = ($("bubble-text") as HTMLInputElement).value || "Hello!";
const duration =
parseFloat(($("bubble-duration") as HTMLInputElement).value) || 2;
log(`Show speech bubble (${duration}s): "${text}"`);
await currentActor.showSpeechBubble(text, duration);
log("Speech bubble hidden");
});

$("btn-show-bubble-forever").addEventListener("click", async () => {
if (!currentActor) return;
const text = ($("bubble-text") as HTMLInputElement).value || "Hello!";
log(`Show speech bubble (forever): "${text}"`);
await currentActor.showSpeechBubble(text, -1);
log("Speech bubble shown (persistent)");
});

$("btn-hide-bubble").addEventListener("click", () => {
if (!currentActor) return;
currentActor.hideSpeechBubble();
log("Speech bubble hidden");
});

// Create wizard by default
await createActor("wizard", 0);
}
Expand Down