From 10fc7aa9eed978169acd8f585df12f594b0cdb31 Mon Sep 17 00:00:00 2001 From: PeenScreeker Date: Sat, 27 Jul 2024 15:06:33 -0400 Subject: [PATCH 1/6] feat: add Zoning UI frame --- images/close-box-outline.svg | 1 + images/pencil-off-outline.svg | 1 + images/pencil-outline.svg | 1 + layout/hud/tab-menu.xml | 8 +- layout/pages/zoning/zoning.xml | 98 ++++++++++- scripts/hud/tab-menu.js | 30 ++++ scripts/pages/zoning/zoning.ts | 302 ++++++++++++++++++++++++++++++++ scripts/types | 2 +- styles/hud/tab-menu.scss | 7 +- styles/pages/_index.scss | 1 + styles/pages/zoning/_index.scss | 1 + styles/pages/zoning/zoning.scss | 152 ++++++++++++++++ 12 files changed, 599 insertions(+), 5 deletions(-) create mode 100644 images/close-box-outline.svg create mode 100644 images/pencil-off-outline.svg create mode 100644 images/pencil-outline.svg create mode 100644 scripts/pages/zoning/zoning.ts create mode 100644 styles/pages/zoning/_index.scss create mode 100644 styles/pages/zoning/zoning.scss diff --git a/images/close-box-outline.svg b/images/close-box-outline.svg new file mode 100644 index 00000000..e052af7a --- /dev/null +++ b/images/close-box-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/pencil-off-outline.svg b/images/pencil-off-outline.svg new file mode 100644 index 00000000..d42b413f --- /dev/null +++ b/images/pencil-off-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/pencil-outline.svg b/images/pencil-outline.svg new file mode 100644 index 00000000..c184d5ef --- /dev/null +++ b/images/pencil-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/layout/hud/tab-menu.xml b/layout/hud/tab-menu.xml index de26117b..7a2eb3aa 100644 --- a/layout/hud/tab-menu.xml +++ b/layout/hud/tab-menu.xml @@ -52,13 +52,19 @@ - + + + diff --git a/layout/pages/zoning/zoning.xml b/layout/pages/zoning/zoning.xml index 9fa54cee..0f4bafce 100644 --- a/layout/pages/zoning/zoning.xml +++ b/layout/pages/zoning/zoning.xml @@ -1,3 +1,99 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/hud/tab-menu.js b/scripts/hud/tab-menu.js index a513c205..bb41b164 100644 --- a/scripts/hud/tab-menu.js +++ b/scripts/hud/tab-menu.js @@ -9,6 +9,12 @@ class HudTabMenu { leaderboardsContainer: $('#LeaderboardsContainer'), /** @type {Panel} @static */ endOfRunContainer: $('#EndOfRunContainer'), + /** @type {Panel} @static */ + zoningContainer: $('#ZoningContainer'), + /** @type {Panel} @static */ + zoningOpenButton: $('#ZoningOpen'), + /** @type {Panel} @static */ + zoningCloseButton: $('#ZoningClose'), /** @type {Image} @static */ gamemodeImage: $('#HudTabMenuGamemodeImage'), /** @type {Panel} @static */ @@ -20,16 +26,40 @@ class HudTabMenu { $.RegisterForUnhandledEvent('HudTabMenu_ForceClose', this.close.bind(this)); $.RegisterForUnhandledEvent('EndOfRun_Show', this.showEndOfRun.bind(this)); $.RegisterForUnhandledEvent('EndOfRun_Hide', this.hideEndOfRun.bind(this)); + $.RegisterForUnhandledEvent('ZoneMenu_Show', this.showZoneMenu.bind(this)); + $.RegisterForUnhandledEvent('ZoneMenu_Hide', this.hideZoneMenu.bind(this)); } static showEndOfRun(_showReason) { this.panels.leaderboardsContainer.AddClass('hud-tab-menu__leaderboards--hidden'); this.panels.endOfRunContainer.RemoveClass('hud-tab-menu__endofrun--hidden'); + this.panels.zoningContainer.AddClass('hud-tab-menu__zoning--hidden'); } static hideEndOfRun() { this.panels.leaderboardsContainer.RemoveClass('hud-tab-menu__leaderboards--hidden'); this.panels.endOfRunContainer.AddClass('hud-tab-menu__endofrun--hidden'); + this.panels.zoningContainer.AddClass('hud-tab-menu__zoning--hidden'); + } + + static showZoneMenu() { + this.panels.tabMenu.AddClass('hud-tab-menu--offset'); + this.panels.leaderboardsContainer.AddClass('hud-tab-menu__leaderboards--hidden'); + this.panels.endOfRunContainer.AddClass('hud-tab-menu__endofrun--hidden'); + + this.panels.zoningContainer.RemoveClass('hud-tab-menu__zoning--hidden'); + this.panels.zoningOpenButton.AddClass('hud-tab-menu__zoning--hidden'); + this.panels.zoningCloseButton.RemoveClass('hud-tab-menu__zoning--hidden'); + } + + static hideZoneMenu() { + this.panels.tabMenu.RemoveClass('hud-tab-menu--offset'); + this.panels.leaderboardsContainer.RemoveClass('hud-tab-menu__leaderboards--hidden'); + this.panels.endOfRunContainer.AddClass('hud-tab-menu__endofrun--hidden'); + + this.panels.zoningContainer.AddClass('hud-tab-menu__zoning--hidden'); + this.panels.zoningOpenButton.RemoveClass('hud-tab-menu__zoning--hidden'); + this.panels.zoningCloseButton.AddClass('hud-tab-menu__zoning--hidden'); } static setMapData(isOfficial) { diff --git a/scripts/pages/zoning/zoning.ts b/scripts/pages/zoning/zoning.ts new file mode 100644 index 00000000..c0ec9843 --- /dev/null +++ b/scripts/pages/zoning/zoning.ts @@ -0,0 +1,302 @@ +/** + * Zoning UI logic + */ + +const TracklistSnippet = { + TRACK: 'tracklist-track', + SEGMENT: 'tracklist-segment', + CHECKPOINT: 'tracklist-checkpoint' +}; + +// future: get this from c++ +const FORMAT_VERSION = 1; + +class ZoneMenu { + static panels = { + zoningMenu: $.GetContextPanel(), + trackList: $('#TrackList')!, + propertiesTrack: $('#TrackProperties')!, + propertiesSegment: $('#SegmentProperties')!, + propertiesZone: $('#ZoneProperties')! + }; + + static selectedZone = { + track: null as MainTrack | BonusTrack | null, + segment: null as Segment | null, + zone: null as Zone | null + }; + static mapZoneData: ZoneDef | null; + static backupZoneData: ZoneDef | null; + static filternameList: string[] | null; + static teleDestList: string[] | null; + static newObjectType: string | null; + + static { + $.RegisterForUnhandledEvent('ZoneMenu_Show', this.showZoneMenu.bind(this)); + $.RegisterForUnhandledEvent('ZoneMenu_Hide', this.hideZoneMenu.bind(this)); + + $.RegisterForUnhandledEvent('LevelInitPostEntity', this.initMenu.bind(this)); + } + + static onLoad() { + //@ts-expect-error API name not recognized + this.mapZoneData = MomentumTimerAPI.GetActiveZoneDefs() as ZoneDef; + + if (!this.mapZoneData) { + const tracks: MapTracks = { + main: { + zones: { + segments: [this.createSegment()], + end: this.createZone() + }, + stagesEndAtStageStarts: true + }, + bonuses: [] as BonusTrack[] + }; + + this.mapZoneData = { formatVersion: FORMAT_VERSION, tracks: tracks } as ZoneDef; + } + } + + static initMenu() { + if (!this.mapZoneData) { + this.onLoad(); + } + + if (!this.mapZoneData) return; + + this.updateSelection(this.mapZoneData.tracks.main, null, null); + + //@ts-expect-error function name not recognized + const entList = $.GetContextPanel().getEntityList(); + this.filternameList = entList.filter ?? []; + this.teleDestList = entList.teleport ?? []; + + this.createTrackEntry(this.panels.trackList, this.mapZoneData.tracks.main, 'Main'); + + if (!this.mapZoneData.tracks.bonuses || this.mapZoneData.tracks.bonuses.length === 0) return; + const tag = $.Localize('#Zoning_Bonus')!; + for (const [i, bonus] of this.mapZoneData.tracks.bonuses.entries()) { + this.createTrackEntry(this.panels.trackList, bonus, `${tag} ${i + 1}`); + } + } + + static showZoneMenu() { + if (!this.mapZoneData || this.panels.trackList.GetChildCount() === 0) { + this.initMenu(); + } + } + + static hideZoneMenu() { + if (this.panels.trackList?.GetChildCount()) { + this.panels.trackList.RemoveAndDeleteChildren(); + } + } + + static toggleCollapse(container: Panel, expandIcon: Image, collapseIcon: Image) { + const shouldExpand = container.HasClass('hide'); + container.SetHasClass('hide', !shouldExpand); + expandIcon.SetHasClass('hide', !shouldExpand); + collapseIcon.SetHasClass('hide', shouldExpand); + const parent = container.GetParent(); + if (parent && parent.HasClass('zoning__tracklist-segment')) { + parent.SetHasClass('zoning__tracklist-segment--dark', shouldExpand); + } + } + + static createTrackEntry(parent: Panel, entry: MainTrack | BonusTrack, name: string) { + const trackChildContainer = this.addTracklistEntry(parent, name, TracklistSnippet.TRACK, { + track: entry, + segment: null, + zone: null + }); + if (trackChildContainer === null) return; + if (entry.zones.segments.length === 0) { + trackChildContainer.RemoveAndDeleteChildren(); + (parent.FindChildTraverse('CollapseButton') as Panel).visible = false; + return; + } + + const trackSegmentContainer = trackChildContainer.FindChildTraverse('SegmentContainer') as Panel; + const segmentTag = $.Localize('#Zoning_Segment')!; + const checkpointTag = $.Localize('#Zoning_Checkpoint')!; + const cancelTag = $.Localize('#Zoning_CancelZone')!; + const endTag = $.Localize('#Zoning_EndZone')!; + for (const [i, segment] of entry.zones.segments.entries()) { + const majorId = segment.name || `${segmentTag} ${i + 1}`; + const segmentChildContainer = this.addTracklistEntry( + trackSegmentContainer, + majorId, + TracklistSnippet.SEGMENT, + { + track: entry, + segment: segment, + zone: null + } + ); + if (segmentChildContainer === null) continue; + if (segment.checkpoints.length === 0 && segment.cancel.length === 0) { + (trackChildContainer.FindChildTraverse('CollapseButton') as Panel).visible = false; + continue; + } + + const segmentCheckpointContainer = segmentChildContainer.FindChildTraverse('CheckpointContainer') as Panel; + for (const [j, zone] of segment.checkpoints.entries()) { + const minorId = j + ? `${checkpointTag} ${j}` + : i + ? $.Localize('#Zoning_Start_Stage')! + : $.Localize('#Zoning_Start_Track')!; + this.addTracklistEntry(segmentCheckpointContainer, minorId, TracklistSnippet.CHECKPOINT, { + track: entry, + segment: segment, + zone: zone + }); + } + if (!segment.cancel || segment.cancel.length === 0) continue; + for (const [j, zone] of segment.cancel.entries()) { + const cancelId = `${cancelTag} ${j + 1}`; + this.addTracklistEntry(segmentCheckpointContainer, cancelId, TracklistSnippet.CHECKPOINT, { + track: entry, + segment: segment, + zone: zone + }); + } + } + + if (entry.zones.end) { + const trackEndZoneContainer = trackChildContainer.FindChildTraverse('EndZoneContainer') as Panel; + this.addTracklistEntry(trackEndZoneContainer, endTag, TracklistSnippet.CHECKPOINT, { + track: entry, + segment: null, + zone: entry.zones.end + }); + } + } + + static addTracklistEntry( + parent: Panel, + name: string, + snippet: string, + selectionObj: { track: MainTrack | BonusTrack; segment: Segment | null; zone: Zone | null }, + setActive: boolean = false + ): Panel | null { + const newTracklistPanel = $.CreatePanel('Panel', parent, name); + newTracklistPanel.LoadLayoutSnippet(snippet); + + const label = newTracklistPanel.FindChildTraverse('Name') as Label; + label.text = name; + + const collapseButton = newTracklistPanel.FindChildTraverse('CollapseButton'); + const childContainer = newTracklistPanel.FindChildTraverse('ChildContainer'); + if (collapseButton && childContainer) { + const expandIcon = newTracklistPanel.FindChildTraverse('TracklistExpandIcon') as Image; + const collapseIcon = newTracklistPanel.FindChildTraverse('TracklistCollapseIcon') as Image; + collapseButton.SetPanelEvent('onactivate', () => + this.toggleCollapse(childContainer as Panel, expandIcon, collapseIcon) + ); + + this.toggleCollapse(childContainer, expandIcon, collapseIcon); + } + + const selectButton = newTracklistPanel.FindChildTraverse('SelectButton') as RadioButton; + selectButton.SetPanelEvent('onactivate', () => + this.updateSelection(selectionObj.track, selectionObj.segment, selectionObj.zone) + ); + + if (setActive) { + selectButton.SetSelected(true); + } + + return childContainer; + } + + static createBonusTrack() { + return { + zones: { + segments: [this.createSegment()], + end: this.createZone() + }, + defragFlags: 0 + } as BonusTrack; + } + + static createSegment() { + return { + limitStartGroundSpeed: false, + checkpointsRequired: true, + checkpointsOrdered: true, + checkpoints: [this.createZone()], + cancel: [] as Zone[], + name: '' + } as Segment; + } + + static createZone(withRegion: boolean = true) { + return { + regions: withRegion ? [this.createRegion()] : ([] as Region[]), + filtername: '' + } as Zone; + } + + static createRegion() { + return { + points: [] as Vec2D[], + bottom: Number.MAX_SAFE_INTEGER, + height: 0, + teleDestTargetname: '' + } as Region; + } + + static updateSelection( + selectedTrack: MainTrack | BonusTrack | null, + selectedSegment: Segment | null, + selectedZone: Zone | null + ) { + if (!selectedTrack) { + this.panels.propertiesTrack.visible = false; + this.panels.propertiesSegment.visible = false; + this.panels.propertiesZone.visible = false; + return; + } + + this.selectedZone.track = selectedTrack as MainTrack | BonusTrack; + this.selectedZone.segment = selectedSegment as Segment; + this.selectedZone.zone = selectedZone as Zone; + + this.panels.propertiesTrack.visible = Boolean(!this.selectedZone.zone && !this.selectedZone.segment); + this.panels.propertiesSegment.visible = Boolean(!this.selectedZone.zone && this.selectedZone.segment); + this.panels.propertiesZone.visible = Boolean(this.selectedZone.zone); + } + + static showAddMenu() { + //show context menu + } + + static showDeletePopup() { + //show context menu + } + + static saveZones() { + if (!this.mapZoneData) return; + this.mapZoneData.dataTimestamp = Date.now(); + //@ts-expect-error API name not recognized + MomentumTimerAPI.SaveZoneDefs(this.mapZoneData); + // reload zones + } + + static cancelEdit() { + this.panels.trackList.RemoveAndDeleteChildren(); + this.mapZoneData = null; + this.initMenu(); + } + + static isSelectionValid() { + const trackValidity = Boolean(this.selectedZone) && Boolean(this.selectedZone.track); + return { + track: trackValidity, + segment: trackValidity && Boolean(this.selectedZone.segment), + zone: trackValidity && Boolean(this.selectedZone.zone) + }; + } +} diff --git a/scripts/types b/scripts/types index 0b545d56..2b9c1fe8 160000 --- a/scripts/types +++ b/scripts/types @@ -1 +1 @@ -Subproject commit 0b545d56f1c4e7882d7b731214d76f9d2e8891c9 +Subproject commit 2b9c1fe8a879f6856e902d905d93c4a4a23dd948 diff --git a/styles/hud/tab-menu.scss b/styles/hud/tab-menu.scss index b5ca4684..5354492b 100644 --- a/styles/hud/tab-menu.scss +++ b/styles/hud/tab-menu.scss @@ -20,8 +20,10 @@ } &--offset { - width: 30%; + width: 560px; + margin: 32px; horizontal-align: left; + vertical-align: top; } &__wrapper { @@ -89,7 +91,7 @@ &__enable-cursor { border-top: 1px solid rgba(0, 0, 0, 0.4); width: 100%; - padding: 12px 24px 12px 0; + padding-right: 24px; vertical-align: bottom; background-color: rgba(0, 0, 0, 0.6); } @@ -97,6 +99,7 @@ &__enable-cursor-tip { font-size: 11px; color: rgba(255, 255, 255, 0.6); + padding: 12px 0; horizontal-align: right; } } diff --git a/styles/pages/_index.scss b/styles/pages/_index.scss index a40cf3c7..e596a5b3 100644 --- a/styles/pages/_index.scss +++ b/styles/pages/_index.scss @@ -9,3 +9,4 @@ @use 'learn'; @use 'loading-screen'; @use 'main-menu'; +@use 'zoning'; diff --git a/styles/pages/zoning/_index.scss b/styles/pages/zoning/_index.scss new file mode 100644 index 00000000..dd2b2a23 --- /dev/null +++ b/styles/pages/zoning/_index.scss @@ -0,0 +1 @@ +@use 'zoning'; diff --git a/styles/pages/zoning/zoning.scss b/styles/pages/zoning/zoning.scss new file mode 100644 index 00000000..3d8973f3 --- /dev/null +++ b/styles/pages/zoning/zoning.scss @@ -0,0 +1,152 @@ +@use '../../config' as *; +@use 'sass:color'; + +$small: 16px; +$medium: 28px; + +.zoning { + width: 100%; + padding: 4px 4px; + flow-children: down; + overflow: squish; + + &__header { + color: white; + font-family: $font-header; + font-size: $medium; + } + + &__button-box { + margin: 2px 0px; + horizontal-align: right; + flow-children: right; + } + + &__menu-section { + width: 100%; + background-color: $dark-200; + margin: 2px 0px; + padding: 6px 0px; + border-radius: 6px; + flow-children: down; + overflow: squish scroll; + } + + &__menu-separator { + width: 100%; + padding: 0px 6px; + border-radius: 4px; + flow-children: down; + + &--dark { + margin: 0px 6px; + padding: 0px; + background-color: $dark-700; + } + } + + &__track-list { + width: 100%; + height: 274px; + padding: 0px 6px; + margin-right: 6px; + flow-children: down; + overflow: squish scroll; + + & > VerticalScrollBar .ScrollThumb { + border-radius: 2px; + } + } + + &__tracklist-button { + padding: 3px 5px; + + &:selected { + padding: 2px 4px; + background-color: $light-200; + border: 1px solid $gray-800; + border-radius: 6px; + } + + &:hover { + padding: 2px 4px; + border: 1px solid $gray-800; + border-radius: 6px; + } + } + + &__tracklist-track { + width: 100%; + margin-bottom: 1px; + padding: 2px 6px; + flow-children: down; + } + + &__tracklist-label { + font-size: $small; + } + + &__tracklist-segment { + width: 100%; + margin-bottom: 1px; + padding: 2px 4px; + border-radius: 6px; + flow-children: down; + + &--dark { + background-color: $dark-700; + } + } + + &__tracklist-checkpoint { + width: 100%; + margin: 2px 0px; + flow-children: down; + } + + &__tracklist-course-entry { + width: 100%; + flow-children: right; + } + + &__list-container { + width: 100%; + margin-left: 12px; + padding-left: 8px; + flow-children: down; + } + + &__collapse-button { + z-index: 2; + horizontal-align: right; + vertical-align: center; + height: 14px; + width: 14px; + + background-color: $light-400; + + margin-right: 4px; + + border-bottom-left-radius: 1px; + border-bottom-right-radius: 1px; + + &:hover { + opacity: 1; + } + } + + &__collapse-icon { + align: center center; + padding: 2px; + opacity: 0.4; + transition: opacity 0.1s ease-in-out 0s; + } + + &__icon-green { + wash-color: $green; + } + + &__icon-red { + wash-color: $red; + } +} From 7dd643d09443ff9ad3410e506dfb375d06c707ca Mon Sep 17 00:00:00 2001 From: PeenScreeker Date: Sat, 27 Jul 2024 15:27:40 -0400 Subject: [PATCH 2/6] feat: add track properties to Zoning UI --- .../modals/context-menus/zoning-df-flags.xml | 28 +++++++ layout/pages/zoning/zoning.xml | 16 ++++ scripts/pages/zoning/zoning.ts | 81 ++++++++++++++++++- styles/pages/zoning/zoning.scss | 45 +++++++++++ 4 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 layout/modals/context-menus/zoning-df-flags.xml diff --git a/layout/modals/context-menus/zoning-df-flags.xml b/layout/modals/context-menus/zoning-df-flags.xml new file mode 100644 index 00000000..e47fb670 --- /dev/null +++ b/layout/modals/context-menus/zoning-df-flags.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/layout/pages/zoning/zoning.xml b/layout/pages/zoning/zoning.xml index 0f4bafce..13b21eb3 100644 --- a/layout/pages/zoning/zoning.xml +++ b/layout/pages/zoning/zoning.xml @@ -81,6 +81,22 @@