Skip to content
Open
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
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ kaiju/
└── AGENTS.md # This file
```

## UI & Engine Interoperability Guidelines (CRITICAL)

When creating or modifying User Interfaces (HUDs, Menus, Overlays) within Kaiju Engine, AI agents must strict adhere to the following axioms:
1. **Always preserve HTML/CSS layout sovereignty.** The document parser (`markup.DocumentFromHTMLAsset`) uses the bounding boundaries defined in the CSS (`position`, `top`, `width`, `height`) to anchor UI meshes correctly to the screen canvas viewport. DO NOT strip structural styling from HTML placeholders to reconstruct bounding boxes exclusively via Go transforms (`Transform.SetLocalPosition`). Removing these HTML anchors will cause render meshes to collapse to `(0,0)` or visually disappear.
2. **Mandatory Explicit Dirty Flags**: Kaiju UI rendering does not redraw structural changes automatically via setters in the loop. If you hook into an element via Go and modify its inner text (`label.SetText()`) or dynamic coloring, you MUST explicitly tell the GPU pipeline to redraw that interface by firing `element.UI.SetDirty(ui.DirtyTypeLayout)` before the frame ends.
3. **Hierarchical Panels vs Labels Nature**: A standard block like `<div>Text</div>` implicitly generates a `UIPanel` wrapper hosting a pure `UILabel` child. Background colors must be painted strictly on the outer Panel (`UIPanel.SetBGColor`), while internal Font styling applies exclusively to the `InnerLabel()`. Mixing these commands results in UI rendering glitches.

## CRITICAL: Custom Math Library

**DO NOT use external math libraries (e.g., gonum, mathgl).** The Kaiju Engine has a complete custom math library at `kaijuengine.com/matrix`.
Expand Down
34 changes: 33 additions & 1 deletion docs/engine/ui/writing.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,36 @@ data := struct{
doc, err := markup.DocumentFromHTMLAsset(host, "ui/tests/binding.html", data, nil)
```

This will load up the HTML document and any of the CSS it references and build out your UI. The returned `doc` will contain the document and all the elements/panels/labels. This UI is immediately loaded into the `host` so you don't need to worry about doing that yourself. *The last argument is a funcmap used for inline template functions*
This will load up the HTML document and any of the CSS it references and build out your UI. The returned `doc` will contain the document and all the elements/panels/labels. This UI is immediately loaded into the `host` so you don't need to worry about doing that yourself. *The last argument is a funcmap used for inline template functions*
## Dynamic UI Updates (Go Interoperability)
While HTML/CSS provides the initial structural foundation and styling (the canvas initial geometric bounding box), you will commonly need to update the UI dynamically during runtime from your game's Go logic (e.g., updating a timer, health bar, or coordinate tracker).

### 1. Element Hierarchy (Panel vs Label)
When declaring a simple markup with text in HTML:
`html
<div id="fps-val">FPS: --</div>
`
The parsing engine instantiates a ui.UIPanel base structural wrapper, and creates an attached hierarchical child ui.UILabel for the string text logic.
To retrieve and modify properties dynamically in the Go loop:
* **Backgrounds & Solid Borders**: Are applied to the parent pane (element.UIPanel.SetBGColor(...)).
* **Text Strings & Typography Colors**: Are applied to the inner textual label (element.InnerLabel().SetText(...) or element.InnerLabel().SetColor(...)).

### 2. Dirty Flags (Reactivity is NOT Automatic)
Unlike standard web browsers, the Kaiju Engine does NOT automatically redraw the UI canvas frame when you modify an element's property via backend Go scripts. You **must explicitly** flag the element as "dirty" in your game or system's update loop to force the Vulkan render pipeline to redraw the updated geometry on the screen:
`go
if e, ok := doc.GetElementById("fps-val"); ok {
if lbl := e.InnerLabel(); lbl != nil {
lbl.SetText(fmt.Sprintf("FPS: %d", fps))
}
// Critical: Signal the renderer that this specific element layout has mutated!
e.UI.SetDirty(ui.DirtyTypeLayout)
}
`

### 3. Layout Control Pitfalls (The Invisible Canvas Bug)
**Never strip foundational HTML positioning anchors in an attempt to control the Screen Layout purely via Go Transform functions.**

The document parser deeply relies on CSS properties (position: absolute, op, ottom, ight, width, height) to anchor the UI primitives to the Screen Viewport properly.

If you declare empty/naked <div> tags in the HTML and attempt to explicitly align and mathematically scale them exclusively via Transform.SetLocalPosition(...) or manual offset matrices natively in Go, the engine's internal UI-Layout System will fight the DOM logic. This typically cascades in elements infinitely collapsing to coordinates (0,0), or losing their mesh dimension entirely, making your UI visually disappear from the screen logic.
* **Golden Rule**: HTML/CSS solely owns the global layout spatial anchors, responsivity bounds, and z-index ordering. Go owns dynamic text injection, loop data binding, and situational state-color updates.
Expand Down
23 changes: 23 additions & 0 deletions src/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"window_width": 1280,
"window_height": 720,
"window_title": "FortressVision",
"fullscreen": false,
"target_fps": 60,
"dfhack_host": "localhost",
"dfhack_port": 5000,
"server_url": "ws://127.0.0.1:8080/ws",
"draw_distance": 10,
"view_levels": 5,
"mesher_threads": 4,
"fov": 60,
"draw_range_down": 5,
"draw_range_up": 1,
"draw_range_side": 4,
"camera_speed": 10,
"camera_sensitivity": 0.3,
"zoom_speed": 5,
"show_debug_info": true,
"show_grid": false,
"wireframe_mode": false
}
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,5 @@
float weight = clamp(pow(min(1.0, unWeightedColor.a * 10.0) + 0.01, 3.0) * 1e8 * pow(1.0 - gl_FragCoord.z * 0.9, 3.0), 1e-2, 3e3);
outColor = vec4(unWeightedColor.rgb * unWeightedColor.a, unWeightedColor.a) * weight;
#else
if (unWeightedColor.a < (1.0 - 0.001)) {
discard;
}
outColor = unWeightedColor;
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ void main(void) {
processAxis(normUV.y, fragNineSliceEdgeLen.y / fragSize2D.w, fragSize2D.w / fragSize2D.y)
);
vec2 newUV = fragUvs.xy + scaledNormUV * fragUvs.zw;
vec4 unWeightedColor = texture(texSampler, newUV) * fragColor;
vec4 texColor = texture(texSampler, newUV);
// Lógica Premium de Cor de Fundo:
// Misturamos fragBGColor (fundo sólido) com (texColor * fragColor) baseado no alpha da textura.
vec4 unWeightedColor = mix(fragBGColor, texColor * fragColor, texColor.a);
// Border
{
vec2 dimensions = fragSize2D.xy;
Expand Down
4 changes: 3 additions & 1 deletion src/engine/entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ func NewEntity(workGroup *concurrent.WorkGroup) *Entity {
func (e *Entity) Init(workGroup *concurrent.WorkGroup) {
e.isActive = true
e.Children = make([]*Entity, 0)
e.Transform.Initialize(workGroup)
if e != nil {
e.Transform.Initialize(workGroup)
}
e.namedData = make(map[string][]interface{})
e.name = "Entity"
}
Expand Down
4 changes: 4 additions & 0 deletions src/engine/ui/markup/css/properties/css_background_color.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import (
"kaijuengine.com/engine/ui/markup/css/rules"
"kaijuengine.com/engine/ui/markup/document"
"kaijuengine.com/matrix"
"kaijuengine.com/rendering"
)

func setChildTextBackgroundColor(elm *document.Element, color matrix.Color) {
Expand Down Expand Up @@ -91,7 +92,10 @@ func (p BackgroundColor) Process(panel *ui.Panel, elm *document.Element, values
}
if color, err = matrix.ColorFromHexString(hex); err == nil {
if applyPanelColor || panel.Base().Type() == ui.ElementTypeImage {
tex, _ := host.TextureCache().Texture(assets.TextureSquare, rendering.TextureFilterLinear)
panel.SetBackground(tex)
panel.SetColor(color)
panel.SetBGColor(color)
}
if panel.Base().IsType(ui.ElementTypeInput) {
panel.Base().ToInput().SetBGColor(color)
Expand Down
7 changes: 6 additions & 1 deletion src/engine/ui/markup/css/properties/css_font_family.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,14 @@ import (

func setChildrenFontFace(elm *document.Element, face rendering.FontFace) {
defer tracing.NewRegion("properties.setChildrenFontFace").End()
if elm.UI == nil {
return
}
if elm.IsText() {
lbl := elm.UI.ToLabel()
lbl.SetFontFace(face)
if lbl != nil {
lbl.SetFontFace(face)
}
} else if elm.UI.IsType(ui.ElementTypeInput) {
elm.UI.ToInput().SetFontFace(face)
} else {
Expand Down
10 changes: 6 additions & 4 deletions src/engine/ui/markup/css/properties/css_font_size.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,16 @@ import (
)

func setChildrenFontSize(elm *document.Element, size string, host *engine.Host) {
if elm.Stylizer.HasRule("font-size") {
if elm.Stylizer.HasRule("font-size") || elm.UI == nil {
return
}
if elm.IsText() {
lbl := elm.UI.ToLabel()
size := helpers.NumFromLengthWithFont(size, host.Window,
host.FontCache().EMSize(lbl.FontFace()))
lbl.SetFontSize(size)
if lbl != nil {
size := helpers.NumFromLengthWithFont(size, host.Window,
host.FontCache().EMSize(lbl.FontFace()))
lbl.SetFontSize(size)
}
} else {
for _, child := range elm.Children {
setChildrenFontSize(child, size, host)
Expand Down
9 changes: 7 additions & 2 deletions src/engine/ui/markup/css/properties/css_font_style.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,16 @@ import (
)

func setChildrenFontStyle(elm *document.Element, style string) {
if elm.UI == nil {
return
}
if elm.IsText() {
lbl := elm.UI.ToLabel()
lbl.SetFontStyle(style)
if lbl != nil {
lbl.SetFontStyle(style)
}
} else if elm.UI.IsType(ui.ElementTypeInput) {
elm.UI.ToInput().SetFontWeight(style)
elm.UI.ToInput().SetFontStyle(style)
} else {
for _, child := range elm.Children {
setChildrenFontStyle(child, style)
Expand Down
7 changes: 6 additions & 1 deletion src/engine/ui/markup/css/properties/css_font_weight.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,14 @@ import (
)

func setChildrenFontWeight(elm *document.Element, weight string) {
if elm.UI == nil {
return
}
if elm.IsText() {
lbl := elm.UI.ToLabel()
lbl.SetFontWeight(weight)
if lbl != nil {
lbl.SetFontWeight(weight)
}
} else if elm.UI.IsType(ui.ElementTypeInput) {
elm.UI.ToInput().SetFontWeight(weight)
} else {
Expand Down
3 changes: 2 additions & 1 deletion src/engine/ui/markup/css/properties/css_left.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"errors"
"kaijuengine.com/engine"
"kaijuengine.com/engine/ui"
"kaijuengine.com/engine/ui/markup/css/helpers"
"kaijuengine.com/engine/ui/markup/css/rules"
"kaijuengine.com/engine/ui/markup/document"
"strings"
Expand All @@ -63,7 +64,7 @@ func (p Left) Process(panel *ui.Panel, elm *document.Element, values []rules.Pro
offsetX += elm.Parent.Value().UI.Layout().Offset().X()
}
default:
val := values[0].Num
val := helpers.NumFromLength(values[0].Str, host.Window)
if strings.HasSuffix(values[0].Str, "%") {
l := panel.Base().Layout()
if l.Ui().Entity().IsRoot() {
Expand Down
12 changes: 7 additions & 5 deletions src/engine/ui/markup/css/properties/css_line_height.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,17 @@ import (
)

func setChildrenLineHeight(elm *document.Element, size string, host *engine.Host) {
if elm.Stylizer.HasRule("line-height") {
if elm.Stylizer.HasRule("line-height") || elm.UI == nil {
return
}
if elm.IsText() {
lbl := elm.UI.ToLabel()
height := helpers.NumFromLengthWithFont(size, host.Window,
host.FontCache().EMSize(lbl.FontFace()))
height = elm.Parent.Value().UI.Layout().PixelSize().Y() * height
lbl.SetLineHeight(height)
if lbl != nil {
size := helpers.NumFromLengthWithFont(size, host.Window,
host.FontCache().EMSize(lbl.FontFace()))
height := elm.Parent.Value().UI.Layout().PixelSize().Y() * size
lbl.SetLineHeight(height)
}
} else {
for _, child := range elm.Children {
setChildrenLineHeight(child, size, host)
Expand Down
1 change: 1 addition & 0 deletions src/engine/ui/markup/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"kaijuengine.com/engine"
"kaijuengine.com/engine/ui"
"kaijuengine.com/engine/ui/markup/css"
_ "kaijuengine.com/engine/ui/markup/css/properties"
"kaijuengine.com/engine/ui/markup/css/rules"
"kaijuengine.com/engine/ui/markup/document"
"log/slog"
Expand Down
11 changes: 8 additions & 3 deletions src/engine/ui/panel.go
Original file line number Diff line number Diff line change
Expand Up @@ -600,12 +600,17 @@ func (p *Panel) UnEnforceColor() {

func (p *Panel) Color() matrix.Color { return p.shaderData.FgColor }

func (p *Panel) SetColor(bgColor matrix.Color) {
func (p *Panel) SetColor(color matrix.Color) {
if p.HasEnforcedColor() {
p.PanelData().enforcedColorStack[0] = bgColor
p.PanelData().enforcedColorStack[0] = color
return
}
p.setColorInternal(bgColor)
p.setColorInternal(color)
}

func (p *Panel) SetBGColor(color matrix.Color) {
p.shaderData.BgColor = color
p.Base().SetDirty(DirtyTypeColorChange)
}

func (p *Panel) SetScrollX(value float32) {
Expand Down
3 changes: 3 additions & 0 deletions src/matrix/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ func (t *Transform) setup() {
}

func (t *Transform) Initialize(workGroup *concurrent.WorkGroup) {
if t == nil {
return
}
defer tracing.NewRegion("matrix.Initialize").End()
t.workGroup = workGroup
t.setup()
Expand Down
21 changes: 17 additions & 4 deletions src/rendering/font.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func (cache *FontCache) TransparentMaterial(target *Material) *Material {
} else if target.SelectRoot() == cache.textOrthoMaterial.SelectRoot() {
return cache.textOrthoMaterialTransparent
} else if target.SelectRoot() == cache.textMaterialTransparent.SelectRoot() ||
target == cache.textOrthoMaterialTransparent.SelectRoot() {
target.SelectRoot() == cache.textOrthoMaterialTransparent.SelectRoot() {
return target
}
slog.Error("invalid material used for getting transparent text material", "material", target.Id)
Expand Down Expand Up @@ -265,6 +265,9 @@ func (cache *FontCache) cachedMeshLetter(font fontBin, letter rune, isOrtho bool

func (cache *FontCache) createLetterMesh(font fontBin, key rune, c fontBinChar, meshCache *MeshCache) {
defer tracing.NewRegion("FontCache.createLetterMesh").End()
if font.cachedLetters == nil || font.cachedOrthoLetters == nil {
return
}
mat := cache.textMaterial
oMat := cache.textOrthoMaterial

Expand Down Expand Up @@ -307,12 +310,15 @@ func (cache *FontCache) createLetterMesh(font fontBin, key rune, c fontBinChar,
func (cache *FontCache) initFont(face FontFace, adb assets.Database) bool {
defer tracing.NewRegion("FontCache.initFont").End()
bin := fontBin{}
bin.texture, _ = cache.renderCaches.TextureCache().Texture(face.string()+".png", TextureFilterLinear)
bin.texture.MipLevels = 1
bin.cachedLetters = make(map[rune]*cachedLetterMesh)
bin.cachedOrthoLetters = make(map[rune]*cachedLetterMesh)
bin.texture, _ = cache.renderCaches.TextureCache().Texture(face.string()+".png", TextureFilterLinear)
if bin.texture == nil {
return false
}
bin.texture.MipLevels = 1
out, _ := adb.Read(face.string() + ".bin")
if bin.texture == nil || out == nil || len(out) == 0 {
if out == nil || len(out) == 0 {
return false
}
read := bytes.NewReader(out)
Expand Down Expand Up @@ -421,6 +427,10 @@ func (cache *FontCache) RenderMeshes(caches RenderCaches,
material = cache.textOrthoMaterial
}
}
if material == nil {
slog.Error("FontCache.RenderMeshes: material de texto nulo", "transparência", fgColor.A() < 1 || bgColor.A() < 1, "is3D", is3D)
return nil
}
// Iterate through all characters
runes := []rune(text)
textLen := len(runes)
Expand Down Expand Up @@ -548,6 +558,9 @@ func (cache *FontCache) RenderMeshes(caches RenderCaches,
Scissor: matrix.Vec4{-matrix.FloatMax, -matrix.FloatMax, matrix.FloatMax, matrix.FloatMax},
}
shaderData.SetModel(model)
if material == nil || fontFace.texture == nil {
continue
}
drawing := Drawing{
Material: material.CreateInstance([]*Texture{fontFace.texture}),
Mesh: m,
Expand Down
16 changes: 16 additions & 0 deletions src/rendering/gpu_device_drawing_vulkan.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,17 @@ func (g *GPUDevice) blitTargetsImpl(passes []*RenderPass) {
cmd.Begin()
defer cmd.End()
g.Painter.forceQueueCommand(*cmd, false)

frame := g.Painter.currentFrame
idxSF := g.Painter.imageIndex[frame]
swapChain := g.LogicalDevice.SwapChain

if img == nil {
g.TransitionImageLayout(&swapChain.Images[idxSF], GPUImageLayoutPresentSrc,
GPUImageAspectColorBit, GPUAccessTransferWriteBit, cmd)
return
}

g.TransitionImageLayout(&swapChain.Images[idxSF],
GPUImageLayoutTransferDstOptimal, GPUImageAspectColorBit,
GPUAccessTransferWriteBit, cmd)
Expand Down Expand Up @@ -370,6 +378,9 @@ func (g *GPUDevice) combineTargets() *TextureId {
cmd.Begin()
defer cmd.End()
g.Painter.forceQueueCommand(*cmd, false)
if len(g.Painter.combinedDrawings.renderPassGroups) == 0 {
return nil
}
// There is only one render pass in combined, so we can just grab the first one
draws := g.Painter.combinedDrawings.renderPassGroups[0].draws
for i := range draws[0].instanceGroups {
Expand All @@ -386,6 +397,11 @@ func (g *GPUDevice) combineTargets() *TextureId {

func (g *GPUDevice) cleanupCombined(cmd *CommandRecorder) {
defer tracing.NewRegion("Vulkan.cleanupCombined").End()

if len(g.Painter.combinedDrawings.renderPassGroups) == 0 {
return
}

// There is only one render pass in combined, so we can just grab the first one
groups := g.Painter.combinedDrawings.renderPassGroups[0].draws[0].instanceGroups
for i := range groups {
Expand Down
4 changes: 4 additions & 0 deletions src/rendering/material.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ func (m *Material) CreateInstance(textures []*Texture) *Material {
defer tracing.NewRegion("Material.CreateInstance").End()
instanceKey := strings.Builder{}
for i := range textures {
if textures[i] == nil {
instanceKey.WriteString("nil;")
continue
}
instanceKey.WriteString(textures[i].Key)
instanceKey.WriteRune(';')
}
Expand Down