/** * Main way to create UI components */ module TK.SpaceTac.UI { export type UIText = Phaser.Text export type UIImage = Phaser.Image export type UIButton = Phaser.Button export type UIGroup = Phaser.Group export type UIContainer = Phaser.Group | Phaser.Image export type ShaderValue = number | { x: number, y: number } export type UIOnOffCallback = (on: boolean) => boolean /** * Text style interface */ export interface UITextStyleI { size?: number color?: string shadow?: boolean stroke_width?: number stroke_color?: string bold?: boolean center?: boolean vcenter?: boolean width?: number } /** * Text style */ export class UITextStyle implements UITextStyleI { // Size in points size = 16 // Font color color = "#ffffff" // Shadow under the text shadow = false // Stroke around the letters stroke_width = 0 stroke_color = "#ffffff" // Bold text bold = false // Centering center = true vcenter = true // Word wrapping width = 0 } /** * Button options */ export type UIButtonOptions = { // Centering center?: boolean // Name of the hover picture (by default, the button name, with "-hover" appended) hover_name?: string // Name of the "on" picture (by default, the button name, with "-on" appended) on_name?: string // Whether "hover" picture should stay near the button (otherwise will be on top) hover_bottom?: boolean // Whether "on" picture should stay near the button (otherwise will be on top) on_bottom?: boolean // Text content text?: string text_x?: number text_y?: number // Text content style override text_style?: UITextStyleI // Icon content icon?: string icon_x?: number icon_y?: number } /** * Main UI builder tool */ export class UIBuilder { view: BaseView private game: MainUI private parent: UIContainer private text_style: UITextStyle constructor(view: BaseView, parent: UIContainer | string = "base", text_style = new UITextStyle) { this.view = view; this.game = view.gameui; if (typeof parent == "string") { this.parent = view.getLayer(parent); } else { this.parent = parent; } this.text_style = text_style; } /** * Create a new UIBuilder inside a parent container, or a view layer * * This new builder will inherit the style settings, and will create components in the specified parent */ in(container: UIContainer | string, body?: (builder: UIBuilder) => void): UIBuilder { let result = new UIBuilder(this.view, container, this.text_style); if (body) { body(result); } return result; } /** * Create a new UIBuilder with style changes */ styled(changes: UITextStyleI, body?: (builder: UIBuilder) => void): UIBuilder { let result = new UIBuilder(this.view, this.parent, merge(this.text_style, changes)); if (body) { body(result); } return result; } /** * Clear the current container of all component */ clear(): void { destroyChildren(this.parent); } /** * Internal method to add to the parent */ private add(child: UIText | UIImage | UIButton | UIContainer): void { if (this.parent instanceof Phaser.Group) { this.parent.add(child); } else if (this.parent instanceof Phaser.Button) { // Protect the "on" and "hover" layers let layer = first(this.parent.children, child => child instanceof Phaser.Image && (child.name == "*on*" || child.name == "*hover*")); if (layer) { this.parent.addChildAt(child, this.parent.getChildIndex(layer)); } else { this.parent.addChild(child); } } else { this.parent.addChild(child); } } /** * Add a group of components */ group(name: string, x = 0, y = 0, visible = true): UIGroup { let result = new Phaser.Group(this.game, undefined, name); result.position.set(x, y); result.visible = visible; this.add(result); return result; } /** * Add a text * * Anchor will be defined according to the style centering */ text(content: string, x = 0, y = 0, style_changes: UITextStyleI = {}): UIText { let style = merge(this.text_style, style_changes); let result = new Phaser.Text(this.game, x, y, content, { font: `${style.bold ? "bold " : ""}${style.size}pt SpaceTac`, fill: style.color, align: style.center ? "center" : "left" }); result.anchor.set(style.center ? 0.5 : 0, style.vcenter ? 0.5 : 0); if (style.width) { result.wordWrap = true; result.wordWrapWidth = style.width; } if (style.shadow) { result.setShadow(3, 4, "rgba(0,0,0,0.6)", 3); } if (style.stroke_width) { result.stroke = style.stroke_color; result.strokeThickness = style.stroke_width; } this.add(result); return result; } /** * Add an image */ image(name: string | string[], x = 0, y = 0, centered = false): UIImage { if (typeof name != "string") { name = this.view.getFirstImage(...name); } let info = this.view.getImageInfo(name); let result = this.game.add.image(x, y, info.key, info.frame); result.name = name; if (centered) { result.anchor.set(0.5); } this.add(result); return result; } /** * Add a hoverable and/or clickable button * * If an image with "-hover" suffix is found in atlases, it will be used as hover mask (added as button child) */ button(name: string, x = 0, y = 0, onclick?: Function, tooltip?: TooltipFiller, onoffcallback?: UIOnOffCallback, options: UIButtonOptions = {}): UIButton { let info = this.view.getImageInfo(name); let result = new Phaser.Button(this.game, x, y, info.key, undefined, null, info.frame, info.frame); result.name = name; if (options.center) { result.anchor.set(0.5); } let clickable = bool(onclick); result.input.useHandCursor = clickable; if (clickable) { UIComponent.setButtonSound(result); } let onstatus = false; if (clickable || tooltip || onoffcallback) { // On mask let on_mask: Phaser.Image | null = null; if (onoffcallback) { let on_info = this.view.getImageInfo(options.on_name || (name + "-on")); if (on_info.exists) { on_mask = new Phaser.Image(this.game, 0, 0, on_info.key, on_info.frame); on_mask.name = options.on_bottom ? "on" : "*on*"; on_mask.visible = false; result.addChild(on_mask); } // TODO Find a better way to handle this (extend Button ?) result.data.onoffcallback = (on: boolean): boolean => { onstatus = onoffcallback(on); if (on_mask) { on_mask.anchor.set(result.anchor.x, result.anchor.y); this.view.animations.setVisible(on_mask, onstatus, 100); } return onstatus; } } // Hover mask let hover_info = this.view.getImageInfo(options.hover_name || (name + "-hover")); let hover_mask: Phaser.Image | null = null; if (hover_info.exists) { hover_mask = new Phaser.Image(this.game, 0, 0, hover_info.key, hover_info.frame); hover_mask.name = options.hover_bottom ? "hover" : "*hover*"; hover_mask.visible = false; result.addChild(hover_mask); } this.view.inputs.setHoverClick(result, () => { if (tooltip) { this.view.tooltip.show(result, tooltip); } if (hover_mask) { hover_mask.anchor.set(result.anchor.x, result.anchor.y); this.view.animations.show(hover_mask, 100); } }, () => { if (tooltip) { this.view.tooltip.hide(); } if (hover_mask) { this.view.animations.hide(hover_mask, 100) } }, () => { if (onclick) { onclick(); } else if (onoffcallback) { this.switch(result, !onstatus); } }, 100); } if (options.text) { this.in(result).text(options.text, options.text_x || 0, options.text_y || 0, options.text_style); } if (options.icon) { this.in(result).image(options.icon, options.icon_x || 0, options.icon_y || 0, options.center); } this.add(result); return result; } /** * Add a value bar */ valuebar(name: string, x = 0, y = 0, orientation = ValueBarOrientation.EAST): ValueBar { let result = new ValueBar(this.view, name, orientation, x, y); this.add(result.node); return result; } /** * Add a fragment shader area, with optional fallback image */ shader(name: string, base: string | { width: number, height: number }, x = 0, y = 0, updater?: () => { [name: string]: ShaderValue }): UIImage { let source = this.game.cache.getShader(name); source = "" + source; let uniforms: any = {}; if (updater) { iteritems(updater(), (key, value) => { uniforms[key] = { type: (typeof value == "number") ? "1f" : "2f", value: value }; }); } let filter = new Phaser.Filter(this.game, uniforms, source); let result: Phaser.Image; if (typeof base == "string") { result = this.image(base, x, y); result.filters = [filter]; filter.setResolution(result.width, result.height); } else { result = filter.addToWorld(x, y, base.width, base.height); this.add(result); } if (updater) { result.update = () => { iteritems(updater(), (key, value) => filter.uniforms[key].value = value); filter.update(); } } filter.update(); return result; } /** * Change the content of an component * * If the component is a text, its content will be changed. * If the component is an image or button, its texture will be changed. */ change(component: UIImage | UIButton | UIText, content: string): void { if (component instanceof Phaser.Text) { component.text = content; } else { let info = this.view.getImageInfo(content); component.name = content; if (component instanceof Phaser.Button) { component.loadTexture(info.key); component.setFrames(info.frame, info.frame); } else { component.loadTexture(info.key, info.frame); } } } /** * Change the status on/off on a button * * Return the final effective status */ switch(button: UIButton, on: boolean): boolean { if (button.data.onoffcallback) { return button.data.onoffcallback(on); } else { return false; } } /** * Select a single button inside the container, toggle its "on" status, and toggle all other button to "off" * * This is the equivalent of radio buttons */ select(button: UIButton): void { this.parent.children.forEach(child => { if (child instanceof Phaser.Button && child.data.onoffcallback && child !== button) { child.data.onoffcallback(false); } }); this.switch(button, true); } /** * Evenly distribute the children of this builder along an axis */ distribute(along: "x" | "y", start: number, end: number): void { let sizes = this.parent.children.map(child => { if (child instanceof Phaser.Image || child instanceof Phaser.Sprite || child instanceof Phaser.Group) { return UITools.getScreenBounds(child)[along == "x" ? "width" : "height"]; } else { return 0; } }); let spacing = ((end - start) - sum(sizes)) / (sizes.length + 1); let offset = start; this.parent.children.forEach((child, idx) => { offset += spacing; child[along] = Math.round(offset); offset += sizes[idx]; }); } } }