package levels import ( "fmt" "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" "github.com/inkyblackness/hacked/ss1/content/archive/level" "github.com/inkyblackness/hacked/ss1/content/text" "github.com/inkyblackness/hacked/ss1/edit" "github.com/inkyblackness/hacked/ss1/edit/undoable/cmd" "github.com/inkyblackness/hacked/ss1/resource" "github.com/inkyblackness/hacked/ss1/world" "github.com/inkyblackness/hacked/ss1/world/ids" "github.com/inkyblackness/hacked/ui/gui" ) // ControlView is the core view for level editing. type ControlView struct { levels *edit.EditableLevels levelSelection *edit.LevelSelectionService editor *edit.LevelEditorService guiScale float32 registry cmd.Registry textCache *text.Cache textureCache *graphics.TextureCache model controlViewModel } // NewControlView returns a new instance. func NewControlView(levels *edit.EditableLevels, levelSelection *edit.LevelSelectionService, editor *edit.LevelEditorService, guiScale float32, textCache *text.Cache, textureCache *graphics.TextureCache, registry cmd.Registry) *ControlView { view := &ControlView{ levels: levels, levelSelection: levelSelection, editor: editor, guiScale: guiScale, registry: registry, textCache: textCache, textureCache: textureCache, model: freshControlViewModel(), } return view } // WindowOpen returns the flag address, to be used with the main menu. func (view *ControlView) WindowOpen() *bool { return &view.model.windowOpen } // Render renders the view. func (view *ControlView) Render() { if view.model.restoreFocus { imgui.SetNextWindowFocus() view.model.restoreFocus = false view.model.windowOpen = true } if view.model.windowOpen { imgui.SetNextWindowSizeV(imgui.Vec2{X: 400 * view.guiScale, Y: 300 * view.guiScale}, imgui.ConditionFirstUseEver) title := "Level Control" readOnly := view.editor.IsReadOnly() if readOnly { title += hintReadOnly } if imgui.BeginV(title+"###Level Control", view.WindowOpen(), imgui.WindowFlagsNoCollapse) { view.renderContent(view.editor.Level(), readOnly) } imgui.End() } } var levelHeights = []string{ "32 Tiles", "16 Tiles", "8 Tiles", "4 Tiles", "2 Tiles", "1 Tile", "1/2 Tile", "1/4 Tile", } func (view *ControlView) renderContent(lvl *level.Level, readOnly bool) { imgui.PushItemWidth(-200 * view.guiScale) selectedLevel := view.levelSelection.CurrentLevelID() if gui.StepSliderInt("Active Level", &selectedLevel, 0, archive.MaxLevels-1) { view.levelSelection.SetCurrentLevelID(selectedLevel) } imgui.Separator() levelType := "Real World" if lvl.IsCyberspace() { levelType = "Cyberspace" } imgui.LabelText("Type", levelType) view.renderLevelHeight(lvl, readOnly) if !lvl.IsCyberspace() { view.renderTextureAtlas(lvl, readOnly) view.renderSurveillanceObjects(lvl, readOnly) view.renderHazards(lvl, readOnly) view.renderTextureAnimations(lvl, readOnly) } imgui.PopItemWidth() } func (view *ControlView) renderLevelHeight(lvl *level.Level, readOnly bool) { _, _, currentShift := lvl.Size() if readOnly { imgui.LabelText("Level Height", levelHeights[currentShift]) } else if imgui.BeginCombo("Level Height", levelHeights[currentShift]) { for shift, height := range levelHeights { if imgui.SelectableV(height, shift == int(currentShift), 0, imgui.Vec2{}) { view.requestSetZShift(lvl, shift) } } imgui.EndCombo() } } func (view *ControlView) textureName(index int) string { key := resource.KeyOf(ids.TextureNames, resource.LangDefault, index) name, err := view.textCache.Text(key) suffix := "" if err == nil { suffix = ": " + name } return fmt.Sprintf("%3d", index) + suffix } func (view *ControlView) renderTextureAtlas(lvl *level.Level, readOnly bool) { imgui.Separator() atlas := lvl.TextureAtlas() { render.TextureSelector("Level Textures", -200, view.guiScale, len(atlas), view.model.selectedAtlasIndex, view.textureCache, func(index int) resource.Key { return resource.KeyOf(ids.LargeTextures.Plus(int(atlas[index])), resource.LangAny, 0) }, func(index int) string { textureType := "(F/C/W)" if index >= level.FloorCeilingTextureLimit { textureType = "(walls only)" } return fmt.Sprintf("Atlas Index %2d ", index) + textureType + "\n" + view.textureName(int(atlas[index])) }, func(newIndex int) { view.model.selectedAtlasIndex = newIndex }) imgui.SameLine() imgui.Text("Level Textures") } gameTextureIndex := -1 if (view.model.selectedAtlasIndex >= 0) && (view.model.selectedAtlasIndex < len(atlas)) { gameTextureIndex = int(atlas[view.model.selectedAtlasIndex]) } if !readOnly { render.TextureSelector("Game Textures", -200, view.guiScale, world.MaxWorldTextures, gameTextureIndex, view.textureCache, func(index int) resource.Key { return resource.KeyOf(ids.LargeTextures.Plus(index), resource.LangAny, 0) }, view.textureName, func(newIndex int) { view.requestSetLevelTexture(lvl, view.model.selectedAtlasIndex, newIndex) }) imgui.SameLine() imgui.Text("Game Textures") } } func (view *ControlView) renderSurveillanceObjects(lvl *level.Level, readOnly bool) { imgui.Separator() if imgui.BeginCombo("Surveillance Object", fmt.Sprintf("Object %d", view.model.selectedSurveillanceObjectIndex)) { for i := 0; i < level.SurveillanceObjectCount; i++ { if imgui.SelectableV(fmt.Sprintf("Object %d", i), i == view.model.selectedSurveillanceObjectIndex, 0, imgui.Vec2{}) { view.model.selectedSurveillanceObjectIndex = i } } imgui.EndCombo() } sources := lvl.SurveillanceSources() surrogates := lvl.SurveillanceSurrogates() capacity := lvl.ObjectCapacity() view.renderSliderInt(readOnly, "Surveillance Source", int(sources[view.model.selectedSurveillanceObjectIndex]), func(int) string { return "%d" }, 0, capacity, func(newValue int) { view.requestSetSurveillanceSource(lvl, view.model.selectedSurveillanceObjectIndex, level.ObjectID(newValue)) }) view.renderSliderInt(readOnly, "Surveillance Surrogate", int(surrogates[view.model.selectedSurveillanceObjectIndex]), func(int) string { return "%d" }, 0, capacity, func(newValue int) { view.requestSetSurveillanceSurrogate(lvl, view.model.selectedSurveillanceObjectIndex, level.ObjectID(newValue)) }) } func (view *ControlView) renderHazards(lvl *level.Level, readOnly bool) { imgui.Separator() parameters := lvl.Parameters() currentCeiling := currentCeilingHazard(parameters) if readOnly { imgui.LabelText("Ceiling Hazard", currentCeiling.title) } else if imgui.BeginCombo("Ceiling Hazard", currentCeiling.title) { for _, info := range ceilingHazards { if imgui.SelectableV(info.title, info.title == currentCeiling.title, 0, imgui.Vec2{}) { view.requestSetCeilingHazard(lvl, info) } } imgui.EndCombo() } view.renderSliderInt(readOnly, "Ceiling Hazard Level", int(parameters.CeilingHazardLevel), currentCeiling.formatter, 0, 255, func(newValue int) { view.requestSetCeilingHazardLevel(lvl, byte(newValue)) }) currentFloor := currentFloorHazard(parameters) if readOnly { imgui.LabelText("Floor Hazard", currentFloor.title) } else if imgui.BeginCombo("Floor Hazard", currentFloor.title) { for _, info := range floorHazards { if imgui.SelectableV(info.title, info.title == currentFloor.title, 0, imgui.Vec2{}) { view.requestSetFloorHazard(lvl, info) } } imgui.EndCombo() } view.renderSliderInt(readOnly, "Floor Hazard Level", int(parameters.FloorHazardLevel), currentFloor.formatter, 0, 255, func(newValue int) { view.requestSetFloorHazardLevel(lvl, byte(newValue)) }) } func (view *ControlView) renderTextureAnimations(lvl *level.Level, readOnly bool) { imgui.Separator() animations := lvl.TextureAnimations() selectedText := "" if (view.model.selectedTextureAnimationIndex >= 1) && (view.model.selectedTextureAnimationIndex < len(animations)) { selectedText = fmt.Sprintf("%d", view.model.selectedTextureAnimationIndex) } if imgui.BeginCombo("Texture Animation Group", selectedText) { for i := 1; i < len(animations); i++ { if imgui.SelectableV(fmt.Sprintf("%2d", i), view.model.selectedTextureAnimationIndex == i, 0, imgui.Vec2{}) { view.model.selectedTextureAnimationIndex = i } } imgui.EndCombo() } if (view.model.selectedTextureAnimationIndex >= 1) && (view.model.selectedTextureAnimationIndex < len(animations)) { animation := animations[view.model.selectedTextureAnimationIndex] view.renderSliderInt(readOnly, "Texture Animation Time", int(animation.FrameTime), func(int) string { return "%d msec" }, 0, 1000, func(newValue int) { view.requestSetTextureAnimationTime(lvl, view.model.selectedTextureAnimationIndex, uint16(newValue)) }) view.renderSliderInt(readOnly, "Texture Animation Frame Count", int(animation.FrameCount), func(int) string { return "%d" }, 0, 10, func(newValue int) { view.requestSetTextureAnimationFrameCount(lvl, view.model.selectedTextureAnimationIndex, byte(newValue)) }) if readOnly { imgui.LabelText("Texture Animation Loop Type", animation.LoopType.String()) } else if imgui.BeginCombo("Texture Animation Loop Type", animation.LoopType.String()) { loopTypes := level.TextureAnimationLoopTypes() for _, loopType := range loopTypes { if imgui.SelectableV(loopType.String(), loopType == animation.LoopType, 0, imgui.Vec2{}) { view.requestSetTextureAnimationType(lvl, view.model.selectedTextureAnimationIndex, loopType) } } imgui.EndCombo() } } } func (view *ControlView) renderSliderInt(readOnly bool, label string, selectedValue int, formatter func(int) string, min, max int, changeHandler func(int)) { selectedString := formatter(selectedValue) labelValue := fmt.Sprintf(selectedString, selectedValue) if readOnly { imgui.LabelText(label, labelValue) } else if gui.StepSliderIntV(label, &selectedValue, min, max, selectedString) { changeHandler(selectedValue) } } func (view *ControlView) requestSetZShift(lvl *level.Level, newValue int) { lvl.SetHeightShift(level.HeightShift(newValue)) view.patchLevelResources(lvl, func() {}) } func (view *ControlView) requestSetLevelTexture(lvl *level.Level, atlasIndex, worldTextureIndex int) { lvl.SetTextureAtlasEntry(atlasIndex, level.TextureIndex(worldTextureIndex)) view.patchLevelResources(lvl, func() { view.model.selectedAtlasIndex = atlasIndex }) } func (view *ControlView) requestSetSurveillanceSource(lvl *level.Level, objectIndex int, objectID level.ObjectID) { lvl.SetSurveillanceSource(objectIndex, objectID) view.patchLevelResources(lvl, func() { view.model.selectedSurveillanceObjectIndex = objectIndex }) } func (view *ControlView) requestSetSurveillanceSurrogate(lvl *level.Level, objectIndex int, objectID level.ObjectID) { lvl.SetSurveillanceSurrogate(objectIndex, objectID) view.patchLevelResources(lvl, func() { view.model.selectedSurveillanceObjectIndex = objectIndex }) } func (view *ControlView) requestSetCeilingHazard(lvl *level.Level, info ceilingHazardInfo) { parameters := lvl.Parameters() parameters.RadiationRegister = 0 if info.radiationRegister { parameters.RadiationRegister = 2 } view.patchLevelResources(lvl, func() {}) } func (view *ControlView) requestSetCeilingHazardLevel(lvl *level.Level, value byte) { lvl.Parameters().CeilingHazardLevel = value view.patchLevelResources(lvl, func() {}) } func (view *ControlView) requestSetFloorHazard(lvl *level.Level, info floorHazardInfo) { parameters := lvl.Parameters() parameters.BiohazardRegister = 0 parameters.FloorHazardIsGravity = 0 if info.isGravity { parameters.FloorHazardIsGravity = 1 } else if info.biohazardRegister { parameters.BiohazardRegister = 2 } view.patchLevelResources(lvl, func() {}) } func (view *ControlView) requestSetFloorHazardLevel(lvl *level.Level, value byte) { lvl.Parameters().FloorHazardLevel = value view.patchLevelResources(lvl, func() {}) } func (view *ControlView) requestSetTextureAnimationTime(lvl *level.Level, index int, value uint16) { lvl.TextureAnimations()[index].FrameTime = value view.patchLevelResources(lvl, func() { view.model.selectedTextureAnimationIndex = index }) } func (view *ControlView) requestSetTextureAnimationFrameCount(lvl *level.Level, index int, value byte) { lvl.TextureAnimations()[index].FrameCount = value view.patchLevelResources(lvl, func() { view.model.selectedTextureAnimationIndex = index }) } func (view *ControlView) requestSetTextureAnimationType(lvl *level.Level, index int, value level.TextureAnimationLoopType) { lvl.TextureAnimations()[index].LoopType = value view.patchLevelResources(lvl, func() { view.model.selectedTextureAnimationIndex = index }) } func (view *ControlView) patchLevelResources(lvl *level.Level, extraRestoreState func()) { oldLevelID := view.levelSelection.CurrentLevelID() extraTask := func(world.Modder) error { extraRestoreState() return nil } err := view.registry.Register(cmd.Named("PatchLevel"), cmd.Forward(view.restoreFocusTask()), cmd.Forward(view.levelSelection.SetCurrentLevelIDTask(lvl.ID())), cmd.Forward(extraTask), cmd.Nested(func() error { return view.levels.CommitLevelChanges(lvl.ID()) }), cmd.Reverse(extraTask), cmd.Reverse(view.levelSelection.SetCurrentLevelIDTask(oldLevelID)), cmd.Reverse(view.restoreFocusTask())) if err != nil { panic(err) } } func (view *ControlView) restoreFocusTask() cmd.Task { return func(modder world.Modder) error { view.model.restoreFocus = true return nil } }