258 lines
9.0 KiB
Go
258 lines
9.0 KiB
Go
package edit
|
|
|
|
import (
|
|
"github.com/inkyblackness/hacked/ss1/content/archive/level"
|
|
"github.com/inkyblackness/hacked/ss1/content/object"
|
|
"github.com/inkyblackness/hacked/ss1/edit/undoable/cmd"
|
|
"github.com/inkyblackness/hacked/ss1/world"
|
|
)
|
|
|
|
// LevelEditorService provides level editing functionality based on the currently selected level and content.
|
|
type LevelEditorService struct {
|
|
registry cmd.Registry
|
|
|
|
gameObjects *GameObjectsService
|
|
levels *EditableLevels
|
|
levelSelection *LevelSelectionService
|
|
|
|
newObjectTriple object.Triple
|
|
}
|
|
|
|
// NewLevelEditorService returns a new instance.
|
|
func NewLevelEditorService(registry cmd.Registry,
|
|
gameObjects *GameObjectsService,
|
|
levels *EditableLevels,
|
|
levelSelection *LevelSelectionService) *LevelEditorService {
|
|
return &LevelEditorService{
|
|
registry: registry,
|
|
gameObjects: gameObjects,
|
|
levels: levels,
|
|
levelSelection: levelSelection,
|
|
}
|
|
}
|
|
|
|
// IsReadOnly returns true if the currently selected level cannot be modified.
|
|
func (service *LevelEditorService) IsReadOnly() bool {
|
|
return service.levels.IsLevelReadOnly(service.levelSelection.CurrentLevelID())
|
|
}
|
|
|
|
// Level returns the currently selected level.
|
|
func (service *LevelEditorService) Level() *level.Level {
|
|
return service.levels.Level(service.levelSelection.CurrentLevelID())
|
|
}
|
|
|
|
// Tiles returns the list of currently selected tiles of the current level.
|
|
func (service *LevelEditorService) Tiles() []*level.TileMapEntry {
|
|
lvl := service.Level()
|
|
positions := service.levelSelection.CurrentSelectedTiles()
|
|
tiles := make([]*level.TileMapEntry, len(positions))
|
|
for i, pos := range positions {
|
|
tiles[i] = lvl.Tile(pos)
|
|
}
|
|
return tiles
|
|
}
|
|
|
|
// HasSelectedTiles returns true if Tiles() returns at least one entry.
|
|
func (service *LevelEditorService) HasSelectedTiles() bool {
|
|
return len(service.levelSelection.CurrentSelectedTiles()) > 0
|
|
}
|
|
|
|
// ClearTiles resets the selected tiles.
|
|
func (service *LevelEditorService) ClearTiles() error {
|
|
return service.ChangeTiles(func(tile *level.TileMapEntry) {
|
|
index := tile.FirstObjectIndex
|
|
tile.Reset()
|
|
tile.FirstObjectIndex = index
|
|
})
|
|
}
|
|
|
|
// ChangeTiles performs a modification on all currently selected tiles and commits these changes to the repository.
|
|
func (service *LevelEditorService) ChangeTiles(modifier func(*level.TileMapEntry)) error {
|
|
positions := service.levelSelection.CurrentSelectedTiles()
|
|
if len(positions) == 0 {
|
|
return nil
|
|
}
|
|
lvl := service.Level()
|
|
for _, pos := range positions {
|
|
modifier(lvl.Tile(pos))
|
|
}
|
|
levelID := lvl.ID()
|
|
return service.registry.Register(cmd.Named("ChangeTiles"),
|
|
cmd.Forward(service.levelSelection.SetCurrentLevelIDTask(levelID)),
|
|
cmd.Forward(service.setSelectedTilesTask(positions)),
|
|
cmd.Nested(func() error { return service.levels.CommitLevelChanges(levelID) }),
|
|
cmd.Reverse(service.setSelectedTilesTask(positions)),
|
|
cmd.Reverse(service.levelSelection.SetCurrentLevelIDTask(levelID)),
|
|
)
|
|
}
|
|
|
|
// Objects returns the list of currently selected objects of the current level.
|
|
func (service *LevelEditorService) Objects() []*level.ObjectMainEntry {
|
|
lvl := service.Level()
|
|
ids := service.levelSelection.CurrentSelectedObjects()
|
|
objects := make([]*level.ObjectMainEntry, len(ids))
|
|
for i, id := range ids {
|
|
objects[i] = lvl.Object(id)
|
|
}
|
|
return objects
|
|
}
|
|
|
|
// HasSelectedObjects returns true if a call to Objects() returns at least one item.
|
|
func (service *LevelEditorService) HasSelectedObjects() bool {
|
|
return len(service.levelSelection.CurrentSelectedObjects()) > 0
|
|
}
|
|
|
|
// NewObjectTriple returns the identifier of implicitly created objects.
|
|
func (service *LevelEditorService) NewObjectTriple() object.Triple {
|
|
return service.newObjectTriple
|
|
}
|
|
|
|
// SetNewObjectTriple sets the identifier for implicitly created objects.
|
|
func (service *LevelEditorService) SetNewObjectTriple(value object.Triple) {
|
|
service.newObjectTriple = value
|
|
}
|
|
|
|
// CreateNewObject adds a new object to the level and selects it.
|
|
func (service *LevelEditorService) CreateNewObject(triple object.Triple, modifier level.ObjectMainEntryModifier) error {
|
|
lvl := service.Level()
|
|
id, err := lvl.NewObject(triple.Class)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
obj := lvl.Object(id)
|
|
obj.Subclass = triple.Subclass
|
|
obj.Type = triple.Type
|
|
service.resetHitpointsToDefault(obj)
|
|
modifier(obj)
|
|
service.placeObject(lvl, obj, service.atFloorLevelIn(lvl))
|
|
lvl.UpdateObjectLocation(id)
|
|
return service.patchLevelObjects(lvl, service.levelSelection.CurrentSelectedObjects(), []level.ObjectID{id})
|
|
}
|
|
|
|
// PlaceObjectsOnFloor puts all selected objects to sit on the floor.
|
|
func (service *LevelEditorService) PlaceObjectsOnFloor() error {
|
|
lvl := service.Level()
|
|
atHeight := service.atFloorLevelIn(lvl)
|
|
return service.ChangeObjects(func(obj *level.ObjectMainEntry) { service.placeObject(lvl, obj, atHeight) })
|
|
}
|
|
|
|
// PlaceObjectsOnEyeLevel puts all selected objects to be at eye level (approximately).
|
|
func (service *LevelEditorService) PlaceObjectsOnEyeLevel() error {
|
|
lvl := service.Level()
|
|
atHeight := service.atEyeLevelIn(lvl)
|
|
return service.ChangeObjects(func(obj *level.ObjectMainEntry) { service.placeObject(lvl, obj, atHeight) })
|
|
}
|
|
|
|
// PlaceObjectsOnCeiling puts all selected objects to hang from the ceiling.
|
|
func (service *LevelEditorService) PlaceObjectsOnCeiling() error {
|
|
lvl := service.Level()
|
|
atHeight := service.atCeilingLevelIn(lvl)
|
|
return service.ChangeObjects(func(obj *level.ObjectMainEntry) { service.placeObject(lvl, obj, atHeight) })
|
|
}
|
|
|
|
// ChangeObjects modifies properties of selected objects. The modifier is called for each selected object.
|
|
func (service *LevelEditorService) ChangeObjects(modifier level.ObjectMainEntryModifier) error {
|
|
objectIDs := service.levelSelection.CurrentSelectedObjects()
|
|
if len(objectIDs) == 0 {
|
|
return nil
|
|
}
|
|
lvl := service.Level()
|
|
for _, id := range objectIDs {
|
|
obj := lvl.Object(id)
|
|
oldPosition := obj.TilePosition()
|
|
modifier(obj)
|
|
if oldPosition != obj.TilePosition() {
|
|
lvl.UpdateObjectLocation(id)
|
|
}
|
|
}
|
|
return service.patchLevelObjects(lvl, objectIDs, objectIDs)
|
|
}
|
|
|
|
// DeleteObjects deletes all currently selected objects, clearing the selection afterwards.
|
|
func (service *LevelEditorService) DeleteObjects() error {
|
|
objectIDs := service.levelSelection.CurrentSelectedObjects()
|
|
if len(objectIDs) == 0 {
|
|
return nil
|
|
}
|
|
lvl := service.Level()
|
|
for _, id := range objectIDs {
|
|
lvl.DelObject(id)
|
|
}
|
|
return service.patchLevelObjects(lvl, objectIDs, nil)
|
|
}
|
|
|
|
func (service *LevelEditorService) patchLevelObjects(
|
|
lvl *level.Level,
|
|
reverseObjectIDs []level.ObjectID,
|
|
forwardObjectIDs []level.ObjectID) error {
|
|
levelID := lvl.ID()
|
|
|
|
return service.registry.Register(cmd.Named("PatchLevelObjects"),
|
|
cmd.Forward(service.levelSelection.SetCurrentLevelIDTask(levelID)),
|
|
cmd.Reverse(service.setSelectedObjectsTask(reverseObjectIDs)),
|
|
cmd.Nested(func() error { return service.levels.CommitLevelChanges(levelID) }),
|
|
cmd.Forward(service.setSelectedObjectsTask(forwardObjectIDs)),
|
|
cmd.Reverse(service.levelSelection.SetCurrentLevelIDTask(levelID)),
|
|
)
|
|
}
|
|
|
|
func (service *LevelEditorService) setSelectedTilesTask(positions []level.TilePosition) cmd.Task {
|
|
return func(world.Modder) error {
|
|
service.levelSelection.SetCurrentSelectedTiles(positions)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (service *LevelEditorService) setSelectedObjectsTask(ids []level.ObjectID) cmd.Task {
|
|
return func(world.Modder) error {
|
|
service.levelSelection.SetCurrentSelectedObjects(ids)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
type heightCalculatorFunc func(tile *level.TileMapEntry, pos level.FinePosition, objPivot float32) level.HeightUnit
|
|
|
|
func (service *LevelEditorService) placeObject(lvl *level.Level, obj *level.ObjectMainEntry, atHeight heightCalculatorFunc) {
|
|
var objPivot float32
|
|
prop, err := service.gameObjects.PropertiesFor(obj.Triple())
|
|
if err == nil {
|
|
objPivot = object.Pivot(prop.Common)
|
|
}
|
|
tile := lvl.Tile(obj.TilePosition())
|
|
if tile != nil {
|
|
obj.Z = atHeight(tile, obj.FinePosition(), objPivot)
|
|
}
|
|
}
|
|
|
|
func (service *LevelEditorService) atFloorLevelIn(lvl *level.Level) heightCalculatorFunc {
|
|
_, _, height := lvl.Size()
|
|
return func(tile *level.TileMapEntry, pos level.FinePosition, objPivot float32) level.HeightUnit {
|
|
floorHeight := tile.FloorTileHeightAt(pos, height)
|
|
return height.ValueToObjectHeight(floorHeight + objPivot)
|
|
}
|
|
}
|
|
|
|
func (service *LevelEditorService) atEyeLevelIn(lvl *level.Level) heightCalculatorFunc {
|
|
_, _, height := lvl.Size()
|
|
return func(tile *level.TileMapEntry, pos level.FinePosition, objPivot float32) level.HeightUnit {
|
|
floorHeight := tile.FloorTileHeightAt(pos, height)
|
|
return height.ValueToObjectHeight(floorHeight + 0.75 - objPivot)
|
|
}
|
|
}
|
|
|
|
func (service *LevelEditorService) atCeilingLevelIn(lvl *level.Level) heightCalculatorFunc {
|
|
_, _, height := lvl.Size()
|
|
return func(tile *level.TileMapEntry, pos level.FinePosition, objPivot float32) level.HeightUnit {
|
|
floorHeight := tile.CeilingTileHeightAt(pos, height)
|
|
return height.ValueToObjectHeight(floorHeight - objPivot)
|
|
}
|
|
}
|
|
|
|
func (service *LevelEditorService) resetHitpointsToDefault(obj *level.ObjectMainEntry) {
|
|
prop, err := service.gameObjects.PropertiesFor(obj.Triple())
|
|
if err != nil {
|
|
return
|
|
}
|
|
obj.Hitpoints = prop.Common.Hitpoints
|
|
}
|