diff --git a/src/events/achievements/AchievementsUIEvent.ts b/src/events/achievements/AchievementsUIEvent.ts new file mode 100644 index 00000000..55954b54 --- /dev/null +++ b/src/events/achievements/AchievementsUIEvent.ts @@ -0,0 +1,8 @@ +import { NitroEvent } from 'nitro-renderer'; + +export class AchievementsUIEvent extends NitroEvent +{ + public static SHOW_ACHIEVEMENTS: string = 'AE_SHOW_ACHIEVEMENTS'; + public static HIDE_ACHIEVEMENTS: string = 'AE_HIDE_ACHIEVEMENTS'; + public static TOGGLE_ACHIEVEMENTS: string = 'AE_TOGGLE_ACHIEVEMENTS'; +} diff --git a/src/events/achievements/index.ts b/src/events/achievements/index.ts new file mode 100644 index 00000000..264ce6ad --- /dev/null +++ b/src/events/achievements/index.ts @@ -0,0 +1 @@ +export * from './AchievementsUIEvent'; diff --git a/src/views/Styles.scss b/src/views/Styles.scss index 0a172311..db1592c7 100644 --- a/src/views/Styles.scss +++ b/src/views/Styles.scss @@ -16,3 +16,4 @@ @import './toolbar/ToolbarView'; @import './wired/WiredView'; @import './mod-tools/ModToolsView'; +@import './achievements/AchievementsView'; diff --git a/src/views/achievements/AchievementsMessageHandler.tsx b/src/views/achievements/AchievementsMessageHandler.tsx new file mode 100644 index 00000000..d29009f0 --- /dev/null +++ b/src/views/achievements/AchievementsMessageHandler.tsx @@ -0,0 +1,70 @@ +import { AchievementEvent, AchievementsEvent, AchievementsScoreEvent } from 'nitro-renderer'; +import { FC, useCallback } from 'react'; +import { CreateMessageHook } from '../../hooks/messages'; +import { IAchievementsMessageHandlerProps } from './AchievementsMessageHandler.types'; +import { useAchievementsContext } from './context/AchievementsContext'; +import { AchievementsActions } from './reducers/AchievementsReducer'; +import { AchievementCategory } from './utils/AchievementCategory'; + +export const AchievementsMessageHandler: FC = props => +{ + const { achievementsState = null, dispatchAchievementsState = null } = useAchievementsContext(); + + const onAchievementEvent = useCallback((event: AchievementEvent) => + { + const parser = event.getParser(); + + console.log(parser); + + }, [ dispatchAchievementsState ]); + + const onAchievementsEvent = useCallback((event: AchievementsEvent) => + { + const parser = event.getParser(); + + const categories: AchievementCategory[] = []; + + for(const achievement of parser.achievements) + { + const categoryName = achievement.category; + + const existing = categories.find(category => category.name === categoryName); + + if(existing) + { + existing.achievements.push(achievement); + continue; + } + + const category = new AchievementCategory(categoryName); + category.achievements.push(achievement); + categories.push(category); + } + + dispatchAchievementsState({ + type: AchievementsActions.SET_CATEGORIES, + payload: { + categories: categories + } + }); + }, [ dispatchAchievementsState ]); + + const onAchievementsScoreEvent = useCallback((event: AchievementsScoreEvent) => + { + const parser = event.getParser(); + + dispatchAchievementsState({ + type: AchievementsActions.SET_SCORE, + payload: { + score: parser.score + } + }); + + }, [ dispatchAchievementsState ]); + + CreateMessageHook(AchievementEvent, onAchievementEvent); + CreateMessageHook(AchievementsEvent, onAchievementsEvent); + CreateMessageHook(AchievementsScoreEvent, onAchievementsScoreEvent); + + return null; +}; diff --git a/src/views/achievements/AchievementsMessageHandler.types.ts b/src/views/achievements/AchievementsMessageHandler.types.ts new file mode 100644 index 00000000..3de1326b --- /dev/null +++ b/src/views/achievements/AchievementsMessageHandler.types.ts @@ -0,0 +1,2 @@ +export interface IAchievementsMessageHandlerProps +{} diff --git a/src/views/achievements/AchievementsView.scss b/src/views/achievements/AchievementsView.scss new file mode 100644 index 00000000..902968b8 --- /dev/null +++ b/src/views/achievements/AchievementsView.scss @@ -0,0 +1,52 @@ +.nitro-achievements { + width: 650px; + height: 376px; + + .score { + border-color: $grid-border-color !important; + background-color: $grid-bg-color; + } + + .category { + border-color: $grid-border-color !important; + background-color: $grid-bg-color; + cursor: pointer; + + &.active { + border-color: $grid-active-border-color !important; + background-color: $grid-active-bg-color; + } + + .category-score { + margin-top: 43.5px; + } + } + + .achievements { + height: 230px; + overflow-y: auto; + overflow-x: hidden; + + .achievement { + border-color: $grid-border-color !important; + background-color: $grid-bg-color; + cursor: pointer; + + &.active { + border-color: $grid-active-border-color !important; + background-color: $grid-active-bg-color; + } + + &.gray { + div { + filter: grayscale(1); + opacity: .5; + } + } + + div { + height: 40px; + } + } + } +} diff --git a/src/views/achievements/AchievementsView.tsx b/src/views/achievements/AchievementsView.tsx new file mode 100644 index 00000000..88789d2b --- /dev/null +++ b/src/views/achievements/AchievementsView.tsx @@ -0,0 +1,67 @@ +import { FC, useCallback, useEffect, useReducer, useState } from 'react'; +import { AchievementsUIEvent } from '../../events/achievements'; +import { useUiEvent } from '../../hooks/events'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../layout'; +import { LocalizeText } from '../../utils/LocalizeText'; +import { AchievementsMessageHandler } from './AchievementsMessageHandler'; +import { AchievementsViewProps } from './AchievementsView.types'; +import { AchievementsContextProvider } from './context/AchievementsContext'; +import { AchievementsReducer, initialAchievements } from './reducers/AchievementsReducer'; +import { AchievementCategoryView } from './views/category/AchievementCategoryView'; +import { AchievementsListView } from './views/list/AchievementsListView'; + +export const AchievementsView: FC = props => +{ + const [ isVisible, setIsVisible ] = useState(false); + const [ achievementsState, dispatchAchievementsState ] = useReducer(AchievementsReducer, initialAchievements); + const { score = null } = achievementsState; + + const onAchievementsEvent = useCallback((event: AchievementsUIEvent) => + { + switch(event.type) + { + case AchievementsUIEvent.SHOW_ACHIEVEMENTS: + setIsVisible(true); + return; + case AchievementsUIEvent.HIDE_ACHIEVEMENTS: + setIsVisible(false); + return; + case AchievementsUIEvent.TOGGLE_ACHIEVEMENTS: + setIsVisible(value => !value); + return; + } + }, []); + + useUiEvent(AchievementsUIEvent.SHOW_ACHIEVEMENTS, onAchievementsEvent); + useUiEvent(AchievementsUIEvent.HIDE_ACHIEVEMENTS, onAchievementsEvent); + useUiEvent(AchievementsUIEvent.TOGGLE_ACHIEVEMENTS, onAchievementsEvent); + + useEffect(() => + { + if(!isVisible) return; + + }, [ isVisible ]); + + return ( + + + { isVisible && + + setIsVisible(false) } /> + +
+
+ +
+ { LocalizeText('achievements.categories.score', ['score'], [score.toString()]) } +
+
+
+ +
+
+
+
} +
+ ); +}; diff --git a/src/views/achievements/AchievementsView.types.ts b/src/views/achievements/AchievementsView.types.ts new file mode 100644 index 00000000..821d5c20 --- /dev/null +++ b/src/views/achievements/AchievementsView.types.ts @@ -0,0 +1,2 @@ +export class AchievementsViewProps +{} diff --git a/src/views/achievements/context/AchievementsContext.tsx b/src/views/achievements/context/AchievementsContext.tsx new file mode 100644 index 00000000..c1162f4d --- /dev/null +++ b/src/views/achievements/context/AchievementsContext.tsx @@ -0,0 +1,14 @@ +import { createContext, FC, useContext } from 'react'; +import { AchievementsContextProps, IAchievementsContext } from './AchievementsContext.types'; + +const AchievementsContext = createContext({ + achievementsState: null, + dispatchAchievementsState: null +}); + +export const AchievementsContextProvider: FC = props => +{ + return { props.children } +} + +export const useAchievementsContext = () => useContext(AchievementsContext); diff --git a/src/views/achievements/context/AchievementsContext.types.ts b/src/views/achievements/context/AchievementsContext.types.ts new file mode 100644 index 00000000..320cd787 --- /dev/null +++ b/src/views/achievements/context/AchievementsContext.types.ts @@ -0,0 +1,13 @@ +import { Dispatch, ProviderProps } from 'react'; +import { IAchievementsAction, IAchievementsState } from '../reducers/AchievementsReducer'; + +export interface IAchievementsContext +{ + achievementsState: IAchievementsState; + dispatchAchievementsState: Dispatch; +} + +export interface AchievementsContextProps extends ProviderProps +{ + +} diff --git a/src/views/achievements/reducers/AchievementsReducer.tsx b/src/views/achievements/reducers/AchievementsReducer.tsx new file mode 100644 index 00000000..1224a86b --- /dev/null +++ b/src/views/achievements/reducers/AchievementsReducer.tsx @@ -0,0 +1,84 @@ +import { Reducer } from 'react'; +import { AchievementCategory } from '../utils/AchievementCategory'; + +export interface IAchievementsState +{ + categories: AchievementCategory[], + score: number, + selectedCategoryName: string, + selectedAchievementId: number +} + +export interface IAchievementsAction +{ + type: string; + payload: { + categories?: AchievementCategory[], + score?: number, + selectedCategoryName?: string, + selectedAchievementId?: number + } +} + +export class AchievementsActions +{ + public static SET_CATEGORIES: string = 'AA_SET_CATEGORIES'; + public static SET_SCORE: string = 'AA_SET_SCORE'; + public static SELECT_CATEGORY: string = 'AA_SELECT_CATEGORY'; + public static SELECT_ACHIEVEMENT: string = 'AA_SELECT_ACHIEVEMENT'; +} + +export const initialAchievements: IAchievementsState = { + categories: null, + score: null, + selectedCategoryName: null, + selectedAchievementId: null +} + +export const AchievementsReducer: Reducer = (state, action) => +{ + switch(action.type) + { + case AchievementsActions.SET_CATEGORIES: { + const categories = (action.payload.categories || state.categories || null); + + let selectedCategoryName = null; + + if(categories.length > 0) + { + selectedCategoryName = categories[0].name; + } + + return { ...state, categories, selectedCategoryName }; + } + case AchievementsActions.SET_SCORE: { + const score = (action.payload.score || state.score || null); + + return { ...state, score }; + } + case AchievementsActions.SELECT_CATEGORY: { + const selectedCategoryName = (action.payload.selectedCategoryName || state.selectedCategoryName || null); + + let selectedAchievementId = null; + + if(selectedCategoryName) + { + const category = state.categories.find(category => category.name === selectedCategoryName); + + if(category && category.achievements.length > 0) + { + selectedAchievementId = category.achievements[0].achievementId; + } + } + + return { ...state, selectedCategoryName, selectedAchievementId }; + } + case AchievementsActions.SELECT_ACHIEVEMENT: { + const selectedAchievementId = (action.payload.selectedAchievementId || state.selectedAchievementId || null); + + return { ...state, selectedAchievementId }; + } + default: + return state; + } +} diff --git a/src/views/achievements/utils/AchievementCategory.ts b/src/views/achievements/utils/AchievementCategory.ts new file mode 100644 index 00000000..bcf4f7f6 --- /dev/null +++ b/src/views/achievements/utils/AchievementCategory.ts @@ -0,0 +1,33 @@ +import { AchievementData } from 'nitro-renderer'; + +export class AchievementCategory +{ + private _name: string; + private _achievements: AchievementData[]; + + constructor(name: string) + { + this._name = name; + this._achievements = []; + } + + public get name(): string + { + return this._name; + } + + public set name(name: string) + { + this._name = name; + } + + public get achievements(): AchievementData[] + { + return this._achievements; + } + + public set achievements(achievements: AchievementData[]) + { + this._achievements = achievements; + } +} diff --git a/src/views/achievements/views/category/AchievementCategoryView.tsx b/src/views/achievements/views/category/AchievementCategoryView.tsx new file mode 100644 index 00000000..91ba95e0 --- /dev/null +++ b/src/views/achievements/views/category/AchievementCategoryView.tsx @@ -0,0 +1,82 @@ +import classNames from 'classnames'; +import { AchievementData } from 'nitro-renderer'; +import { FC, useCallback } from 'react'; +import { LocalizeText } from '../../../../utils/LocalizeText'; +import { BadgeImageView } from '../../../shared/badge-image/BadgeImageView'; +import { useAchievementsContext } from '../../context/AchievementsContext'; +import { AchievementsActions } from '../../reducers/AchievementsReducer'; +import { AchievementCategoryViewProps } from './AchievementCategoryView.types'; + +export const AchievementCategoryView: FC = props => +{ + const achievementsContext = useAchievementsContext(); + + const { achievementsState = null, dispatchAchievementsState = null } = achievementsContext; + const { categories = null, selectedCategoryName = null, selectedAchievementId = null } = achievementsState; + + const getSelectedCategory = useCallback(() => + { + return categories.find(category => category.name === selectedCategoryName); + }, [ categories, selectedCategoryName ]); + + const getAchievementImage = useCallback((achievement: AchievementData) => + { + if(!achievement) return null; + + let badgeId = achievement.badgeId; + + if(achievement.levelCount > 1) + { + badgeId = badgeId.replace(/[0-9]/g, ''); + badgeId = (badgeId + (((achievement.level - 1) > 0) ? (achievement.level - 1) : achievement.level)); + } + + return badgeId; + }, []); + + const getSelectedAchievement = useCallback(() => + { + if(!getSelectedCategory()) return null; + + return getSelectedCategory().achievements.find(achievement => achievement.achievementId === selectedAchievementId); + }, [ getSelectedCategory, selectedAchievementId ]); + + const selectAchievement = useCallback((id: number) => + { + dispatchAchievementsState({ + type: AchievementsActions.SELECT_ACHIEVEMENT, + payload: { + selectedAchievementId: id + } + }); + }, [ dispatchAchievementsState ]); + + + return ( +
+
+
{ LocalizeText('quests.' + selectedCategoryName + '.name') }
+
IMAGE
+
+
+
+ +
+
+
+
+ { getSelectedCategory().achievements.map((achievement, index) => + { + return ( +
+
selectAchievement(achievement.achievementId)}> + +
+
+ ) + }) } +
+
+
+ ); +} diff --git a/src/views/achievements/views/category/AchievementCategoryView.types.ts b/src/views/achievements/views/category/AchievementCategoryView.types.ts new file mode 100644 index 00000000..d6c378d4 --- /dev/null +++ b/src/views/achievements/views/category/AchievementCategoryView.types.ts @@ -0,0 +1,2 @@ +export class AchievementCategoryViewProps +{} diff --git a/src/views/achievements/views/list/AchievementListView.types.ts b/src/views/achievements/views/list/AchievementListView.types.ts new file mode 100644 index 00000000..047bd157 --- /dev/null +++ b/src/views/achievements/views/list/AchievementListView.types.ts @@ -0,0 +1,2 @@ +export class AchievementListViewProps +{} diff --git a/src/views/achievements/views/list/AchievementsListView.tsx b/src/views/achievements/views/list/AchievementsListView.tsx new file mode 100644 index 00000000..a2615ab8 --- /dev/null +++ b/src/views/achievements/views/list/AchievementsListView.tsx @@ -0,0 +1,70 @@ +import classNames from 'classnames'; +import { FC, useCallback } from 'react'; +import { GetConfiguration } from '../../../../api'; +import { useAchievementsContext } from '../../context/AchievementsContext'; +import { AchievementsActions } from '../../reducers/AchievementsReducer'; +import { AchievementCategory } from '../../utils/AchievementCategory'; +import { AchievementListViewProps } from './AchievementListView.types'; + +export const AchievementsListView: FC = props => +{ + const achievementsContext = useAchievementsContext(); + + const { achievementsState = null, dispatchAchievementsState = null } = achievementsContext; + const { categories = null, selectedCategoryName = null } = achievementsState; + + const getCategoryImage = useCallback((category: AchievementCategory) => + { + let level = 0; + + for(const achievement of category.achievements) + { + level = (level + ((achievement.finalLevel) ? achievement.level : (achievement.level - 1))); + } + + const isActive = ((level > 0) ? 'active' : 'inactive'); + + return GetConfiguration('achievements.images.url', GetConfiguration('achievements.images.url') + `quests/achcategory_${category.name}_${isActive}.png`).replace('%image%',`achcategory_${category.name}_${isActive}`); + }, []); + + const getCategoryProgress = useCallback((category: AchievementCategory) => + { + let completed = 0; + let total = 0; + + for(const achievement of category.achievements) + { + if(!achievement) continue; + + if(achievement.finalLevel) completed = completed + 1 + achievement.level; + + total = (total + achievement.scoreLimit); + } + + return (completed + ' / ' + total); + }, []); + + const selectCategory = useCallback((name: string) => + { + dispatchAchievementsState({ + type: AchievementsActions.SELECT_CATEGORY, + payload: { + selectedCategoryName: name + } + }); + }, [ dispatchAchievementsState ]); + + return ( +
+ { categories && categories.map((category, index) => + { + return
+
selectCategory(category.name)}> + +
{ getCategoryProgress(category) }
+
+
+ }) } +
+ ); +}; diff --git a/src/views/main/MainView.tsx b/src/views/main/MainView.tsx index 96cacefb..92f222f5 100644 --- a/src/views/main/MainView.tsx +++ b/src/views/main/MainView.tsx @@ -1,6 +1,7 @@ import { Nitro, RoomSessionEvent } from 'nitro-renderer'; import { FC, useCallback, useEffect, useState } from 'react'; import { useRoomSessionManagerEvent } from '../../hooks/events/nitro/session/room-session-manager-event'; +import { AchievementsView } from '../achievements/AchievementsView'; import { AvatarEditorView } from '../avatar-editor/AvatarEditorView'; import { CatalogView } from '../catalog/CatalogView'; import { FriendListView } from '../friend-list/FriendListView'; @@ -51,6 +52,7 @@ export const MainView: FC = props => + diff --git a/src/views/toolbar/ToolbarView.tsx b/src/views/toolbar/ToolbarView.tsx index 726baa1a..a807308d 100644 --- a/src/views/toolbar/ToolbarView.tsx +++ b/src/views/toolbar/ToolbarView.tsx @@ -1,10 +1,10 @@ import { Dispose, DropBounce, EaseOut, JumpBy, Motions, NitroToolbarAnimateIconEvent, Queue, UserFigureEvent, UserInfoDataParser, UserInfoEvent, Wait } from 'nitro-renderer'; import { FC, useCallback, useState } from 'react'; import { AvatarEditorEvent, CatalogEvent, FriendListEvent, InventoryEvent, NavigatorEvent, RoomWidgetCameraEvent } from '../../events'; +import { AchievementsUIEvent } from '../../events/achievements'; import { UnseenItemTrackerUpdateEvent } from '../../events/inventory/UnseenItemTrackerUpdateEvent'; import { ModToolsEvent } from '../../events/mod-tools/ModToolsEvent'; -import { useRoomEngineEvent } from '../../hooks'; -import { dispatchUiEvent, useUiEvent } from '../../hooks/events/ui/ui-event'; +import { dispatchUiEvent, useRoomEngineEvent, useUiEvent } from '../../hooks'; import { CreateMessageHook } from '../../hooks/messages/message-event'; import { TransitionAnimation } from '../../layout/transitions/TransitionAnimation'; import { TransitionAnimationTypes } from '../../layout/transitions/TransitionAnimation.types'; @@ -117,6 +117,10 @@ export const ToolbarView: FC = props => case ToolbarViewItems.MOD_TOOLS_ITEM: dispatchUiEvent(new ModToolsEvent(ModToolsEvent.TOGGLE_MOD_TOOLS)); return; + case ToolbarViewItems.ACHIEVEMENTS_ITEM: + dispatchUiEvent(new AchievementsUIEvent(AchievementsUIEvent.TOGGLE_ACHIEVEMENTS)); + setMeExpanded(false); + return; } }, []); diff --git a/src/views/toolbar/ToolbarView.types.ts b/src/views/toolbar/ToolbarView.types.ts index d9ab106a..8ab78b75 100644 --- a/src/views/toolbar/ToolbarView.types.ts +++ b/src/views/toolbar/ToolbarView.types.ts @@ -12,4 +12,5 @@ export class ToolbarViewItems public static CLOTHING_ITEM: string = 'TVI_CLOTHING_ITEM'; public static CAMERA_ITEM: string = 'TVI_CAMERA_ITEM'; public static MOD_TOOLS_ITEM: string = 'TVI_MOD_TOOLS_ITEM'; + public static ACHIEVEMENTS_ITEM: string = 'TVI_ACHIEVEMENTS_ITEM'; } diff --git a/src/views/toolbar/me/ToolbarMeView.tsx b/src/views/toolbar/me/ToolbarMeView.tsx index 31cc71ab..e77e7489 100644 --- a/src/views/toolbar/me/ToolbarMeView.tsx +++ b/src/views/toolbar/me/ToolbarMeView.tsx @@ -38,7 +38,7 @@ export const ToolbarMeView: FC = props =>
-
+
handleToolbarItemClick(ToolbarViewItems.ACHIEVEMENTS_ITEM) }>