package levels import ( "fmt" mgl "github.com/go-gl/mathgl/mgl32" "github.com/inkyblackness/imgui-go/v3" "github.com/inkyblackness/hacked/editor/graphics" "github.com/inkyblackness/hacked/editor/render" "github.com/inkyblackness/hacked/ss1/content/archive/level" "github.com/inkyblackness/hacked/ss1/content/object" "github.com/inkyblackness/hacked/ss1/edit" "github.com/inkyblackness/hacked/ss1/resource" "github.com/inkyblackness/hacked/ss1/world/ids" "github.com/inkyblackness/hacked/ui/input" "github.com/inkyblackness/hacked/ui/opengl" ) const contextMenuName = "MapContextMenu" // MapDisplay renders a level map. type MapDisplay struct { gameObjects *edit.GameObjectsService levelSelection *edit.LevelSelectionService editor *edit.LevelEditorService context render.Context camera *LimitedCamera guiScale float32 background *BackgroundGrid textures *MapTextures colors *MapColors mapGrid *MapGrid highlighter *Highlighter icons *MapIcons moveCapture func(pixelX, pixelY float32) mouseMoved bool positionPopupPos imgui.Vec2 positionValid bool position MapPosition contextMenuRequested bool selectionReference *level.TilePosition hoverItems hoverItems } // NewMapDisplay returns a new instance. func NewMapDisplay(gameObjects *edit.GameObjectsService, levelSelection *edit.LevelSelectionService, editor *edit.LevelEditorService, gl opengl.OpenGL, guiScale float32, textureQuery TextureQuery) *MapDisplay { tilesPerMapSide := float32(64) tileBaseLength := float32(level.FineCoordinatesPerTileSide) tileBaseHalf := tileBaseLength / 2.0 camLimit := tilesPerMapSide*tileBaseLength - tileBaseHalf zoomShift := guiScale - 1.0 zoomLevelMin := float32(-5) + zoomShift zoomLevelMax := float32(1) + zoomShift display := &MapDisplay{ gameObjects: gameObjects, levelSelection: levelSelection, editor: editor, context: render.Context{ OpenGL: gl, ProjectionMatrix: mgl.Ident4(), }, camera: NewLimitedCamera(zoomLevelMin, zoomLevelMax, -tileBaseHalf, camLimit), guiScale: guiScale, moveCapture: func(float32, float32) {}, } display.context.ViewMatrix = display.camera.ViewMatrix() display.background = NewBackgroundGrid(&display.context) display.textures = NewMapTextures(&display.context, textureQuery) display.colors = NewMapColors(&display.context) display.mapGrid = NewMapGrid(&display.context) display.highlighter = NewHighlighter(&display.context) display.icons = NewMapIcons(&display.context) centerX, centerY := (tilesPerMapSide*tileBaseLength)/-2.0, (tilesPerMapSide*tileBaseLength)/-2.0 display.camera.ZoomAt(-3+zoomShift, centerX, centerY) display.camera.MoveTo(centerX, centerY) return display } // Render renders the whole map display. func (display *MapDisplay) Render( paletteTexture *graphics.PaletteTexture, textureRetriever func(resource.Key) (*graphics.BitmapTexture, error), textureDisplay TextureDisplay, colorDisplay ColorDisplay) { lvl := display.editor.Level() columns, rows, _ := lvl.Size() display.hoverItems.validate(lvl) display.background.Render(columns, rows) if lvl.IsCyberspace() { display.renderCyberspaceColors(lvl, columns, rows, paletteTexture, colorDisplay) } else { display.renderRealWorldTextures(lvl, columns, rows, paletteTexture, textureDisplay) display.renderRealWorldShadows(lvl, columns, rows, colorDisplay) } display.mapGrid.Render(columns, rows, lvl) display.renderTileSelection() display.renderObjectBackgrounds(lvl) display.renderObjectIcons(lvl, paletteTexture, textureRetriever) display.renderObjectSelection() display.renderActiveHoverItem() display.renderPositionOverlay(lvl) display.renderContextMenu() } func (display *MapDisplay) renderCyberspaceColors(lvl *level.Level, columns int, rows int, paletteTexture *graphics.PaletteTexture, colorDisplay ColorDisplay) { if paletteTexture == nil { return } var colorQuery ColorQuery palette := paletteTexture.Palette() if colorDisplay == ColorDisplayFloor { colorQuery = display.colorQueryFor(lvl, func(tile *level.TileMapEntry) [4]float32 { rgb := palette[tile.TextureInfo.FloorPaletteIndex()] return [4]float32{float32(rgb.Red) / 255, float32(rgb.Green) / 255, float32(rgb.Blue) / 255, 0.8} }) } else if colorDisplay == ColorDisplayCeiling { colorQuery = display.colorQueryFor(lvl, func(tile *level.TileMapEntry) [4]float32 { rgb := palette[tile.TextureInfo.CeilingPaletteIndex()] return [4]float32{float32(rgb.Red) / 255, float32(rgb.Green) / 255, float32(rgb.Blue) / 255, 0.8} }) } if colorQuery != nil { display.colors.Render(columns, rows, colorQuery) } } func (display *MapDisplay) renderRealWorldTextures(lvl *level.Level, columns int, rows int, paletteTexture *graphics.PaletteTexture, textureDisplay TextureDisplay) { if paletteTexture == nil { return } display.textures.Render(columns, rows, func(pos level.TilePosition) (level.TileType, level.TextureIndex, int) { tile := lvl.Tile(pos) if tile == nil { return level.TileTypeSolid, 0, 0 } atlasIndex, textureRotations := textureDisplay.Func()(tile) atlas := lvl.TextureAtlas() textureIndex := level.TextureIndex(-1) if (int(atlasIndex) >= 0) && (int(atlasIndex) < len(atlas)) { textureIndex = atlas[atlasIndex] } return tile.Type, textureIndex, textureRotations }, paletteTexture) } func (display *MapDisplay) renderRealWorldShadows(lvl *level.Level, columns int, rows int, colorDisplay ColorDisplay) { var colorQuery ColorQuery if colorDisplay == ColorDisplayFloor { colorQuery = display.colorQueryFor(lvl, func(tile *level.TileMapEntry) [4]float32 { return [4]float32{0.0, 0.0, 0.0, float32(tile.Flags.ForRealWorld().FloorShadow()) / 15.0} }) } else if colorDisplay == ColorDisplayCeiling { colorQuery = display.colorQueryFor(lvl, func(tile *level.TileMapEntry) [4]float32 { return [4]float32{0.0, 0.0, 0.0, float32(tile.Flags.ForRealWorld().CeilingShadow()) / 15.0} }) } if colorQuery != nil { display.colors.Render(columns, rows, colorQuery) } } func (display *MapDisplay) renderTileSelection() { selectedTiles := display.levelSelection.CurrentSelectedTiles() tileMapPositions := make([]MapPosition, 0, len(selectedTiles)) for _, pos := range selectedTiles { tileMapPositions = append(tileMapPositions, MapPosition{ X: level.CoordinateAt(pos.X, level.FineCoordinatesPerTileSide/2), Y: level.CoordinateAt(pos.Y, level.FineCoordinatesPerTileSide/2), }) } display.highlighter.Render(tileMapPositions, level.FineCoordinatesPerTileSide, [4]float32{0.0, 0.8, 0.2, 0.5}) } func (display *MapDisplay) renderObjectBackgrounds(lvl *level.Level) { var objects []MapPosition lvl.ForEachObject(func(id level.ObjectID, entry level.ObjectMainEntry) { objects = append(objects, MapPosition{X: entry.X, Y: entry.Y}) }) display.highlighter.Render(objects, level.FineCoordinatesPerTileSide/4, [4]float32{1.0, 1.0, 1.0, 0.3}) } func (display *MapDisplay) renderObjectIcons(lvl *level.Level, paletteTexture *graphics.PaletteTexture, textureRetriever func(resource.Key) (*graphics.BitmapTexture, error)) { if paletteTexture == nil { return } tripleInfo := display.gameObjects.BitmapInfo() var icons []iconData var highlightIcon iconData var highlightID level.ObjectID if display.hoverItems.activeItem != nil { objectItem, isObject := display.hoverItems.activeItem.(objectHoverItem) if isObject { highlightID = objectItem.id } } lvl.ForEachObject(func(id level.ObjectID, entry level.ObjectMainEntry) { triple := entry.Triple() info, cached := tripleInfo[triple] if !cached { return } key := resource.KeyOf(ids.ObjectBitmaps, resource.LangAny, info.Start+info.IconRecommendation) texture, err := textureRetriever(key) if err == nil { icon := iconData{pos: MapPosition{X: entry.X, Y: entry.Y}, texture: texture} if highlightID == id { highlightIcon = icon } else { icons = append(icons, icon) } } }) if (highlightID != 0) && (highlightIcon.texture != nil) { icons = append(icons, highlightIcon) } display.icons.Render(paletteTexture, level.FineCoordinatesPerTileSide/4, icons) } func (display *MapDisplay) renderObjectSelection() { selectedObjects := display.editor.Objects() selectedObjectHighlights := make([]MapPosition, 0, len(selectedObjects)) for _, obj := range selectedObjects { objPos := MapPosition{X: obj.X, Y: obj.Y} selectedObjectHighlights = append(selectedObjectHighlights, objPos) } display.highlighter.Render(selectedObjectHighlights, level.FineCoordinatesPerTileSide/4, [4]float32{0.0, 0.8, 0.2, 0.5}) } func (display *MapDisplay) renderActiveHoverItem() { if display.hoverItems.activeItem == nil { return } display.highlighter.Render( []MapPosition{display.hoverItems.activeItem.Pos()}, display.hoverItems.activeItem.Size(), [4]float32{0.0, 0.2, 0.8, 0.3}) } func (display *MapDisplay) colorQueryFor(lvl *level.Level, tileToColor func(*level.TileMapEntry) [4]float32) func(level.TilePosition) [4]float32 { return func(pos level.TilePosition) [4]float32 { tile := lvl.Tile(pos) if tile == nil { return [4]float32{} } return tileToColor(tile) } } func (display *MapDisplay) renderPositionOverlay(lvl *level.Level) { imgui.SetNextWindowPosV(display.positionPopupPos, imgui.ConditionAlways, imgui.Vec2{X: 1.0, Y: 1.0}) imgui.SetNextWindowSize(imgui.Vec2{X: 140 * display.guiScale, Y: 0}) imgui.SetNextWindowBgAlpha(0.3) if imgui.BeginV("Position", nil, imgui.WindowFlagsNoMove|imgui.WindowFlagsNoTitleBar|imgui.WindowFlagsNoResize|imgui.WindowFlagsAlwaysAutoResize| imgui.WindowFlagsNoSavedSettings|imgui.WindowFlagsNoFocusOnAppearing|imgui.WindowFlagsNoNav) { typeString := "---" hasPos := false var pos MapPosition hasFloor := false var floorRaw int floorString := hintUnknown hasCeiling := false var ceilingRaw int ceilingString := hintUnknown if display.hoverItems.activeItem != nil { pos = display.hoverItems.activeItem.Pos() hasPos = true if _, isTileItem := display.hoverItems.activeItem.(tileHoverItem); isTileItem { pos = display.position // use raw cursor position for this display typeString = "Tile" tile := lvl.Tile(pos.Tile()) if (tile != nil) && (tile.Type != level.TileTypeSolid) { _, _, heightShift := lvl.Size() floorHeight := tile.Floor.AbsoluteHeight() floorHeightInTiles, err := heightShift.ValueFromTileHeight(floorHeight) if err == nil { floorString = fmt.Sprintf("%2.3f", floorHeightInTiles) } floorRaw = int(floorHeight) hasFloor = true ceilingHeight := tile.Ceiling.AbsoluteHeight() ceilingHeightInTiles, err := heightShift.ValueFromTileHeight(ceilingHeight) if err == nil { ceilingString = fmt.Sprintf("%2.3f", ceilingHeightInTiles) } ceilingRaw = int(ceilingHeight) hasCeiling = true } } else if objectItem, isObjectItem := display.hoverItems.activeItem.(objectHoverItem); isObjectItem { _, _, heightShift := lvl.Size() obj := lvl.Object(objectItem.id) if obj != nil { typeString = fmt.Sprintf("%3d = %v", objectItem.id, obj.Triple()) heightInTiles, err := heightShift.ValueFromObjectHeight(obj.Z) if err == nil { floorString = fmt.Sprintf("%2.3f", heightInTiles) } floorRaw = int(obj.Z) hasFloor = true } } } imgui.Text("T: " + typeString) if hasPos { imgui.Text(fmt.Sprintf("X: T %2d F %3d", pos.X.Tile(), pos.X.Fine())) imgui.Text(fmt.Sprintf("Y: T %2d F %3d", pos.Y.Tile(), pos.Y.Fine())) } else { imgui.Text("X: T -- F ---") imgui.Text("Y: T -- F ---") } if hasFloor { imgui.Text(fmt.Sprintf("F: %3d = %s", floorRaw, floorString)) } else { imgui.Text("F: -- = --.---") } if hasCeiling { imgui.Text(fmt.Sprintf("C: %3d = %s", ceilingRaw, ceilingString)) } else { imgui.Text("C: -- = --.---") } imgui.End() } } func (display *MapDisplay) renderContextMenu() { if display.contextMenuRequested { imgui.OpenPopup(contextMenuName) display.contextMenuRequested = false } if imgui.BeginPopupV(contextMenuName, imgui.PopupFlagsMouseButtonRight) { readOnly := display.editor.IsReadOnly() if imgui.BeginMenu("New...") { implicitTriple := display.editor.NewObjectTriple() canCreateImplicitClass := display.canCreateObjectOf(implicitTriple.Class) if imgui.MenuItemV("Object", "Ctrl+2ndClick", false, canCreateImplicitClass) { display.requestCreateNewObject(false, implicitTriple) } if imgui.MenuItemV("Object (at grid)", "Ctrl+Shift+2ndClick", false, canCreateImplicitClass) { display.requestCreateNewObject(true, implicitTriple) } imgui.EndMenu() } imgui.Separator() if imgui.MenuItemV("Delete Objects", "", false, !readOnly && display.editor.HasSelectedObjects()) { _ = display.editor.DeleteObjects() } if imgui.MenuItemV("Clear Tiles", "", false, !readOnly && display.editor.HasSelectedTiles()) { _ = display.editor.ClearTiles() } imgui.EndPopup() } } // WindowResized must be called to notify of a change in window geometry. func (display *MapDisplay) WindowResized(width int, height int) { display.context.ProjectionMatrix = mgl.Ortho2D(0.0, float32(width), float32(height), 0.0) display.camera.SetViewportSize(float32(width), float32(height)) display.positionPopupPos.X = float32(width) - 10.0 display.positionPopupPos.Y = float32(height) - 10.0 } func (display *MapDisplay) unprojectPixel(pixelX, pixelY float32) (x, y float32) { pixelVec := mgl.Vec4{pixelX, pixelY, 0.0, 1.0} invertedView := display.camera.ViewMatrix().Inv() result := invertedView.Mul4x1(pixelVec) return result[0], result[1] } // MouseButtonDown must be called when a button was pressed. func (display *MapDisplay) MouseButtonDown(mouseX, mouseY float32, button uint32) { display.updateMouseWorldPosition(mouseX, mouseY) if button == input.MousePrimary { lastPixelX, lastPixelY := mouseX, mouseY display.mouseMoved = false display.moveCapture = func(pixelX, pixelY float32) { lastWorldX, lastWorldY := display.unprojectPixel(lastPixelX, lastPixelY) worldX, worldY := display.unprojectPixel(pixelX, pixelY) display.camera.MoveBy(worldX-lastWorldX, worldY-lastWorldY) lastPixelX, lastPixelY = pixelX, pixelY display.mouseMoved = true } } } // MouseButtonUp must be called when a button was released. func (display *MapDisplay) MouseButtonUp(mouseX, mouseY float32, button uint32, modifier input.Modifier) { display.updateMouseWorldPosition(mouseX, mouseY) if button == input.MousePrimary { display.moveCapture = func(float32, float32) {} if !display.mouseMoved && display.positionValid { switch { case modifier.Has(input.ModControl): display.toggleSelectionAtActiveHoverItem() case modifier.Has(input.ModShift) && (display.selectionReference != nil): firstPos := *display.selectionReference fromX := int(firstPos.X) fromY := int(firstPos.Y) toX := int(display.position.X.Tile()) toY := int(display.position.Y.Tile()) xIncrement := 1 yIncrement := 1 if fromX > toX { xIncrement = -1 } if fromY > toY { yIncrement = -1 } toX += xIncrement toY += yIncrement var newList []level.TilePosition for y := fromY; y != toY; y += yIncrement { for x := fromX; x != toX; x += xIncrement { newList = append(newList, level.TilePosition{X: byte(x), Y: byte(y)}) } } display.levelSelection.SetCurrentSelectedTiles(newList) display.levelSelection.SetCurrentSelectedObjects(display.objectsInTiles(newList)) default: display.setSelectionByActiveHoverItem() } } } else if button == input.MouseSecondary { if modifier.IsClear() { display.contextMenuRequested = true } else if modifier.Has(input.ModControl) { display.requestCreateNewObject(modifier.Has(input.ModShift), display.editor.NewObjectTriple()) } } } func (display *MapDisplay) canCreateObjectOf(class object.Class) bool { if display.editor.IsReadOnly() { return false } lvl := display.editor.Level() return lvl.HasRoomForObjectOf(class) && display.positionValid } func (display *MapDisplay) requestCreateNewObject(snapToGrid bool, triple object.Triple) { if !display.canCreateObjectOf(triple.Class) { return } pos := display.position if snapToGrid { toGrid := func(value byte) byte { cornerSnapDistance := level.FineCoordinatesPerTileSide / 4 switch { case value < byte(cornerSnapDistance): return 0x00 case value >= byte(level.FineCoordinatesPerTileSide-cornerSnapDistance): return level.FineCoordinatesPerTileSide - 1 default: return level.FineCoordinatesPerTileSide / 2 } } pos.X = level.CoordinateAt(pos.X.Tile(), toGrid(pos.X.Fine())) pos.Y = level.CoordinateAt(pos.Y.Tile(), toGrid(pos.Y.Fine())) } err := display.editor.CreateNewObject(triple, func(obj *level.ObjectMainEntry) { obj.X = pos.X obj.Y = pos.Y }) if err != nil { panic(err) } } func (display *MapDisplay) setSelectionByActiveHoverItem() { var tiles []level.TilePosition var objects []level.ObjectID if display.hoverItems.activeItem != nil { if tileItem, isTile := display.hoverItems.activeItem.(tileHoverItem); isTile { tiles = append(tiles, tileItem.pos.Tile()) } else if objectItem, isObject := display.hoverItems.activeItem.(objectHoverItem); isObject { objects = append(objects, objectItem.id) } } display.levelSelection.SetCurrentSelectedTiles(tiles) if len(tiles) > 0 { display.selectionReference = &tiles[0] display.levelSelection.SetCurrentSelectedObjects(display.objectsInTiles(tiles)) } else { display.levelSelection.SetCurrentSelectedObjects(objects) } } func (display *MapDisplay) toggleSelectionAtActiveHoverItem() { if display.hoverItems.activeItem != nil { if tileItem, isTile := display.hoverItems.activeItem.(tileHoverItem); isTile { wasSelected := display.levelSelection.IsTileSelected(tileItem.pos.Tile()) tiles := []level.TilePosition{tileItem.pos.Tile()} if wasSelected { display.levelSelection.RemoveCurrentSelectedTiles(tiles) display.levelSelection.RemoveCurrentSelectedObjects(display.objectsInTiles(tiles)) } else { display.levelSelection.AddCurrentSelectedTiles(tiles) display.levelSelection.AddCurrentSelectedObjects(display.objectsInTiles(tiles)) } } else if objectItem, isObject := display.hoverItems.activeItem.(objectHoverItem); isObject { display.levelSelection.ToggleObjectSelection([]level.ObjectID{objectItem.id}) } } } func (display *MapDisplay) objectsInTiles(tiles []level.TilePosition) []level.ObjectID { tilesContain := func(pos level.TilePosition) bool { for _, entry := range tiles { if entry == pos { return true } } return false } var objects []level.ObjectID lvl := display.editor.Level() lvl.ForEachObject(func(id level.ObjectID, entry level.ObjectMainEntry) { if tilesContain(entry.TilePosition()) { objects = append(objects, id) } }) return objects } // MouseMoved must be called for a mouse move. func (display *MapDisplay) MouseMoved(mouseX, mouseY float32) { display.updateMouseWorldPosition(mouseX, mouseY) if display.positionValid { display.hoverItems.find(display.editor.Level(), display.position) } else { display.hoverItems.reset() } display.moveCapture(mouseX, mouseY) } // MouseScrolled must be called for a mouse scroll. func (display *MapDisplay) MouseScrolled(mouseX, mouseY float32, deltaX, deltaY float32, modifier input.Modifier) { if modifier.Has(input.ModControl) { display.hoverItems.scroll(deltaY > 0) } else { worldX, worldY := display.unprojectPixel(mouseX, mouseY) if deltaY < 0 { display.camera.ZoomAt(-0.5, worldX, worldY) } if deltaY > 0 { display.camera.ZoomAt(0.5, worldX, worldY) } } } func (display *MapDisplay) updateMouseWorldPosition(mouseX, mouseY float32) { worldX, worldY := display.unprojectPixel(mouseX, mouseY) lvl := display.editor.Level() width, height, _ := lvl.Size() worldWidth := float32(width) * level.FineCoordinatesPerTileSide worldHeight := float32(height) * level.FineCoordinatesPerTileSide display.positionValid = (worldX >= 0.0) && (worldX < worldWidth) && (worldY >= 0.0) && (worldY < worldHeight) if display.positionValid { display.position = MapPosition{X: level.Coordinate(worldX + 0.5), Y: level.Coordinate(worldY + 0.5)} } else { display.position = MapPosition{} } }