aether-shards/src/ui/dialogue-overlay.js

222 lines
5 KiB
JavaScript
Raw Normal View History

import { LitElement, html, css } from "lit";
import { narrativeManager } from "../../systems/NarrativeManager.js";
export class DialogueOverlay extends LitElement {
static get styles() {
return css`
:host {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: 80%;
max-width: 800px;
z-index: 100;
pointer-events: auto;
font-family: "Courier New", monospace;
}
.dialogue-box {
background: rgba(10, 10, 20, 0.95);
border: 2px solid #00ffff;
box-shadow: 0 0 20px rgba(0, 255, 255, 0.2);
padding: 20px;
display: flex;
gap: 20px;
animation: slideUp 0.3s ease-out;
}
.portrait {
width: 100px;
height: 100px;
background: #222;
border: 1px solid #555;
flex-shrink: 0;
}
.portrait img {
width: 100%;
height: 100%;
object-fit: cover;
}
.content {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.speaker {
color: #00ffff;
font-weight: bold;
font-size: 1.2rem;
margin-bottom: 5px;
}
.text {
color: white;
font-size: 1.1rem;
line-height: 1.5;
min-height: 3em;
}
.choices {
margin-top: 15px;
display: flex;
gap: 10px;
}
button {
background: #333;
color: white;
border: 1px solid #555;
padding: 8px 16px;
cursor: pointer;
font-family: inherit;
text-transform: uppercase;
}
button:hover {
background: #444;
border-color: #00ffff;
}
.next-indicator {
align-self: flex-end;
font-size: 0.8rem;
color: #888;
margin-top: 10px;
animation: blink 1s infinite;
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes blink {
50% {
opacity: 0;
}
}
/* Tutorial Style Override */
.type-tutorial {
border-color: #00ff00;
}
.type-tutorial .speaker {
color: #00ff00;
}
`;
}
static get properties() {
return {
activeNode: { type: Object },
isVisible: { type: Boolean },
};
}
constructor() {
super();
this.activeNode = null;
this.isVisible = false;
}
connectedCallback() {
super.connectedCallback();
// Subscribe to Manager Updates
narrativeManager.addEventListener(
"narrative-update",
this._onUpdate.bind(this)
);
narrativeManager.addEventListener("narrative-end", this._onEnd.bind(this));
// Allow clicking/spacebar to advance
window.addEventListener("keydown", this._handleInput.bind(this));
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("keydown", this._handleInput.bind(this));
}
_onUpdate(e) {
this.activeNode = e.detail.node;
this.isVisible = e.detail.active;
}
_onEnd() {
this.isVisible = false;
this.activeNode = null;
}
_handleInput(e) {
if (!this.isVisible) return;
if (e.code === "Space" || e.code === "Enter") {
// Only advance if no choices
if (!this.activeNode.choices) {
narrativeManager.next();
}
}
}
render() {
if (!this.isVisible || !this.activeNode) return html``;
return html`
<div
class="dialogue-box ${this.activeNode.type === "TUTORIAL"
? "type-tutorial"
: ""}"
@click="${() => !this.activeNode.choices && narrativeManager.next()}"
>
${this.activeNode.portrait
? html`
<div class="portrait">
<img
src="${this.activeNode.portrait}"
alt="${this.activeNode.speaker}"
/>
</div>
`
: ""}
<div class="content">
<div class="speaker">${this.activeNode.speaker}</div>
<div class="text">${this.activeNode.text}</div>
${this.activeNode.choices
? html`
<div class="choices">
${this.activeNode.choices.map(
(choice, index) => html`
<button
@click="${(e) => {
e.stopPropagation();
narrativeManager.makeChoice(index);
}}"
>
${choice.text}
</button>
`
)}
</div>
`
: html`<div class="next-indicator">
Press SPACE to continue...
</div>`}
</div>
</div>
`;
}
}
customElements.define("dialogue-overlay", DialogueOverlay);