diff options
| author | Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> | 2024-11-21 20:06:15 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-11-21 20:06:15 +0300 |
| commit | 53337bad0623a5f32c87d760bc03efb3cfe4eab6 (patch) | |
| tree | 7a542d6ba9c453c8df60ef1d05781cf1ac75b6a0 /frontend | |
| parent | feat/backend: gdrive to backblaze migration, improve create record (#237) (diff) | |
| download | lphub-53337bad0623a5f32c87d760bc03efb3cfe4eab6.tar.gz lphub-53337bad0623a5f32c87d760bc03efb3cfe4eab6.tar.bz2 lphub-53337bad0623a5f32c87d760bc03efb3cfe4eab6.zip | |
feat/frontend: remove map select from upload run dialog (#239)
Diffstat (limited to 'frontend')
| -rw-r--r-- | frontend/src/App.tsx | 6 | ||||
| -rw-r--r-- | frontend/src/api/Api.ts | 2 | ||||
| -rw-r--r-- | frontend/src/api/Maps.ts | 10 | ||||
| -rw-r--r-- | frontend/src/components/UploadRunDialog.tsx | 113 | ||||
| -rw-r--r-- | frontend/src/types/Content.ts | 1 | ||||
| -rw-r--r-- | frontend/src/types/MapNames.ts | 127 |
6 files changed, 185 insertions, 74 deletions
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e4bde75..a02779b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx | |||
| @@ -66,12 +66,6 @@ const App: React.FC = () => { | |||
| 66 | _fetch_games(); | 66 | _fetch_games(); |
| 67 | }, []); | 67 | }, []); |
| 68 | 68 | ||
| 69 | if (!games) { | ||
| 70 | return ( | ||
| 71 | <></> | ||
| 72 | ) | ||
| 73 | }; | ||
| 74 | |||
| 75 | return ( | 69 | return ( |
| 76 | <> | 70 | <> |
| 77 | <UploadRunDialog token={token} open={uploadRunDialog} onClose={(updateProfile) => { | 71 | <UploadRunDialog token={token} open={uploadRunDialog} onClose={(updateProfile) => { |
diff --git a/frontend/src/api/Api.ts b/frontend/src/api/Api.ts index 2e55ab4..4a3f907 100644 --- a/frontend/src/api/Api.ts +++ b/frontend/src/api/Api.ts | |||
| @@ -35,7 +35,7 @@ export const API = { | |||
| 35 | 35 | ||
| 36 | post_map_discussion: (token: string, map_id: string, content: MapDiscussionContent) => post_map_discussion(token, map_id, content), | 36 | post_map_discussion: (token: string, map_id: string, content: MapDiscussionContent) => post_map_discussion(token, map_id, content), |
| 37 | post_map_discussion_comment: (token: string, map_id: string, discussion_id: number, comment: string) => post_map_discussion_comment(token, map_id, discussion_id, comment), | 37 | post_map_discussion_comment: (token: string, map_id: string, discussion_id: number, comment: string) => post_map_discussion_comment(token, map_id, discussion_id, comment), |
| 38 | post_record: (token: string, run: UploadRunContent) => post_record(token, run), | 38 | post_record: (token: string, run: UploadRunContent, map_id: number) => post_record(token, run, map_id), |
| 39 | 39 | ||
| 40 | delete_map_discussion: (token: string, map_id: string, discussion_id: number) => delete_map_discussion(token, map_id, discussion_id), | 40 | delete_map_discussion: (token: string, map_id: string, discussion_id: number) => delete_map_discussion(token, map_id, discussion_id), |
| 41 | 41 | ||
diff --git a/frontend/src/api/Maps.ts b/frontend/src/api/Maps.ts index 89657b5..3832a2e 100644 --- a/frontend/src/api/Maps.ts +++ b/frontend/src/api/Maps.ts | |||
| @@ -73,9 +73,9 @@ export const delete_map_discussion = async (token: string, map_id: string, discu | |||
| 73 | return response.data.success; | 73 | return response.data.success; |
| 74 | }; | 74 | }; |
| 75 | 75 | ||
| 76 | export const post_record = async (token: string, run: UploadRunContent): Promise<[boolean, string]> => { | 76 | export const post_record = async (token: string, run: UploadRunContent, map_id: number): Promise<[boolean, string]> => { |
| 77 | if (run.partner_demo) { | 77 | if (run.partner_demo) { |
| 78 | const response = await axios.postForm(url(`maps/${run.map_id}/record`), { | 78 | const response = await axios.postForm(url(`maps/${map_id}/record`), { |
| 79 | "host_demo": run.host_demo, | 79 | "host_demo": run.host_demo, |
| 80 | "partner_demo": run.partner_demo, | 80 | "partner_demo": run.partner_demo, |
| 81 | }, { | 81 | }, { |
| @@ -83,16 +83,16 @@ export const post_record = async (token: string, run: UploadRunContent): Promise | |||
| 83 | "Authorization": token, | 83 | "Authorization": token, |
| 84 | } | 84 | } |
| 85 | }); | 85 | }); |
| 86 | return [ response.data.success, response.data.message ]; | 86 | return [response.data.success, response.data.message]; |
| 87 | } else { | 87 | } else { |
| 88 | const response = await axios.postForm(url(`maps/${run.map_id}/record`), { | 88 | const response = await axios.postForm(url(`maps/${map_id}/record`), { |
| 89 | "host_demo": run.host_demo, | 89 | "host_demo": run.host_demo, |
| 90 | }, { | 90 | }, { |
| 91 | headers: { | 91 | headers: { |
| 92 | "Authorization": token, | 92 | "Authorization": token, |
| 93 | } | 93 | } |
| 94 | }); | 94 | }); |
| 95 | return [ response.data.success, response.data.message ]; | 95 | return [response.data.success, response.data.message]; |
| 96 | } | 96 | } |
| 97 | } | 97 | } |
| 98 | 98 | ||
diff --git a/frontend/src/components/UploadRunDialog.tsx b/frontend/src/components/UploadRunDialog.tsx index 118b589..c02fdb8 100644 --- a/frontend/src/components/UploadRunDialog.tsx +++ b/frontend/src/components/UploadRunDialog.tsx | |||
| @@ -4,12 +4,12 @@ import { ScoreboardTempUpdate, SourceDemoParser, NetMessages } from '@nekz/sdp'; | |||
| 4 | 4 | ||
| 5 | import '@css/UploadRunDialog.css'; | 5 | import '@css/UploadRunDialog.css'; |
| 6 | import { Game } from '@customTypes/Game'; | 6 | import { Game } from '@customTypes/Game'; |
| 7 | import { Map } from '@customTypes/Map'; | ||
| 8 | import { API } from '@api/Api'; | 7 | import { API } from '@api/Api'; |
| 9 | import { useNavigate } from 'react-router-dom'; | 8 | import { useNavigate } from 'react-router-dom'; |
| 10 | import useMessage from '@hooks/UseMessage'; | 9 | import useMessage from '@hooks/UseMessage'; |
| 11 | import useConfirm from '@hooks/UseConfirm'; | 10 | import useConfirm from '@hooks/UseConfirm'; |
| 12 | import useMessageLoad from "@hooks/UseMessageLoad"; | 11 | import useMessageLoad from "@hooks/UseMessageLoad"; |
| 12 | import { MapNames } from '@customTypes/MapNames'; | ||
| 13 | 13 | ||
| 14 | interface UploadRunDialogProps { | 14 | interface UploadRunDialogProps { |
| 15 | token?: string; | 15 | token?: string; |
| @@ -27,19 +27,11 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 27 | const navigate = useNavigate(); | 27 | const navigate = useNavigate(); |
| 28 | 28 | ||
| 29 | const [uploadRunContent, setUploadRunContent] = React.useState<UploadRunContent>({ | 29 | const [uploadRunContent, setUploadRunContent] = React.useState<UploadRunContent>({ |
| 30 | map_id: 0, | ||
| 31 | host_demo: null, | 30 | host_demo: null, |
| 32 | partner_demo: null, | 31 | partner_demo: null, |
| 33 | }); | 32 | }); |
| 34 | 33 | ||
| 35 | const [currentMap, setCurrentMap] = React.useState<string>(""); | ||
| 36 | |||
| 37 | const _set_current_map = (game_name: string) => { | ||
| 38 | setCurrentMap(game_name); | ||
| 39 | } | ||
| 40 | |||
| 41 | const [selectedGameID, setSelectedGameID] = React.useState<number>(0); | 34 | const [selectedGameID, setSelectedGameID] = React.useState<number>(0); |
| 42 | const [selectedGameMaps, setSelectedGameMaps] = React.useState<Map[]>([]); | ||
| 43 | const [selectedGameName, setSelectedGameName] = React.useState<string>(""); | 35 | const [selectedGameName, setSelectedGameName] = React.useState<string>(""); |
| 44 | 36 | ||
| 45 | // dropdowns | 37 | // dropdowns |
| @@ -50,6 +42,7 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 50 | 42 | ||
| 51 | const [dragHightlight, setDragHighlight] = React.useState<boolean>(false); | 43 | const [dragHightlight, setDragHighlight] = React.useState<boolean>(false); |
| 52 | const [dragHightlightPartner, setDragHighlightPartner] = React.useState<boolean>(false); | 44 | const [dragHightlightPartner, setDragHighlightPartner] = React.useState<boolean>(false); |
| 45 | |||
| 53 | const fileInputRef = React.useRef<HTMLInputElement>(null); | 46 | const fileInputRef = React.useRef<HTMLInputElement>(null); |
| 54 | const fileInputRefPartner = React.useRef<HTMLInputElement>(null); | 47 | const fileInputRefPartner = React.useRef<HTMLInputElement>(null); |
| 55 | 48 | ||
| @@ -102,14 +95,6 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 102 | 95 | ||
| 103 | const _handle_game_select = async (game_id: string, game_name: string) => { | 96 | const _handle_game_select = async (game_id: string, game_name: string) => { |
| 104 | setLoading(true); | 97 | setLoading(true); |
| 105 | const gameMaps = await API.get_game_maps(game_id); | ||
| 106 | setSelectedGameMaps(gameMaps); | ||
| 107 | setUploadRunContent({ | ||
| 108 | map_id: gameMaps.find((map) => !map.is_disabled)!.id, //gameMaps[0].id, | ||
| 109 | host_demo: null, | ||
| 110 | partner_demo: null, | ||
| 111 | }); | ||
| 112 | _set_current_map(gameMaps.find((map) => !map.is_disabled)!.name); | ||
| 113 | setSelectedGameID(parseInt(game_id) - 1); | 98 | setSelectedGameID(parseInt(game_id) - 1); |
| 114 | setSelectedGameName(game_name); | 99 | setSelectedGameName(game_name); |
| 115 | setLoading(false); | 100 | setLoading(false); |
| @@ -158,6 +143,20 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 158 | await message("Error", "Error while processing demo: Unable to get scoreboard result. Either there is a demo that is corrupt or haven't been recorded in challenge mode.") | 143 | await message("Error", "Error while processing demo: Unable to get scoreboard result. Either there is a demo that is corrupt or haven't been recorded in challenge mode.") |
| 159 | return | 144 | return |
| 160 | } | 145 | } |
| 146 | |||
| 147 | if (!demo.mapName || !MapNames[demo.mapName]) { | ||
| 148 | await message("Error", "Error while processing demo: Invalid map name.") | ||
| 149 | return | ||
| 150 | } | ||
| 151 | |||
| 152 | if (selectedGameID === 0 && MapNames[demo.mapName] > 60) { | ||
| 153 | await message("Error", "Error while processing demo: Invalid cooperative demo in singleplayer submission.") | ||
| 154 | return | ||
| 155 | } else if (selectedGameID === 1 && MapNames[demo.mapName] <= 60) { | ||
| 156 | await message("Error", "Error while processing demo: Invalid singleplayer demo in cooperative submission.") | ||
| 157 | return | ||
| 158 | } | ||
| 159 | |||
| 161 | const { portalScore, timeScore } = scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {}; | 160 | const { portalScore, timeScore } = scoreboard.userMessage?.as<ScoreboardTempUpdate>() ?? {}; |
| 162 | 161 | ||
| 163 | const userConfirmed = await confirm("Upload Record", `Map Name: ${demo.mapName}\nPortal Count: ${portalScore}\nTicks: ${timeScore}\n\nAre you sure you want to upload this demo?`); | 162 | const userConfirmed = await confirm("Upload Record", `Map Name: ${demo.mapName}\nPortal Count: ${portalScore}\nTicks: ${timeScore}\n\nAre you sure you want to upload this demo?`); |
| @@ -167,10 +166,14 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 167 | } | 166 | } |
| 168 | 167 | ||
| 169 | messageLoad("Uploading..."); | 168 | messageLoad("Uploading..."); |
| 170 | const [success, response] = await API.post_record(token, uploadRunContent); | 169 | const [success, response] = await API.post_record(token, uploadRunContent, MapNames[demo.mapName]); |
| 171 | messageLoadClose(); | 170 | messageLoadClose(); |
| 172 | await message("Upload Record", response); | 171 | await message("Upload Record", response); |
| 173 | if (success) { | 172 | if (success) { |
| 173 | setUploadRunContent({ | ||
| 174 | host_demo: null, | ||
| 175 | partner_demo: null, | ||
| 176 | }); | ||
| 174 | onClose(success); | 177 | onClose(success); |
| 175 | navigate("/profile"); | 178 | navigate("/profile"); |
| 176 | } | 179 | } |
| @@ -179,7 +182,6 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 179 | 182 | ||
| 180 | React.useEffect(() => { | 183 | React.useEffect(() => { |
| 181 | if (open) { | 184 | if (open) { |
| 182 | |||
| 183 | setDragHighlightPartner(false); | 185 | setDragHighlightPartner(false); |
| 184 | setDragHighlight(false); | 186 | setDragHighlight(false); |
| 185 | _handle_game_select("1", "Portal 2 - Singleplayer"); // a different approach?. | 187 | _handle_game_select("1", "Portal 2 - Singleplayer"); // a different approach?. |
| @@ -203,37 +205,20 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 203 | <div className='dropdown-cur'>{selectedGameName}</div> | 205 | <div className='dropdown-cur'>{selectedGameName}</div> |
| 204 | <i style={{ rotate: "-90deg", transform: "translate(-5px, 10px)" }} className="triangle"></i> | 206 | <i style={{ rotate: "-90deg", transform: "translate(-5px, 10px)" }} className="triangle"></i> |
| 205 | </div> | 207 | </div> |
| 206 | <div style={{top: "110px"}} className={dropdown1Vis ? "upload-run-dropdown" : "upload-run-dropdown hidden"}> | 208 | <div style={{ top: "110px" }} className={dropdown1Vis ? "upload-run-dropdown" : "upload-run-dropdown hidden"}> |
| 207 | {games.map((game) => ( | 209 | {games.map((game) => ( |
| 208 | <div onClick={() => { _handle_game_select(game.id.toString(), game.name); _handle_dropdowns(1) }} key={game.id}>{game.name}</div> | 210 | <div onClick={() => { _handle_game_select(game.id.toString(), game.name); _handle_dropdowns(1) }} key={game.id}>{game.name}</div> |
| 209 | ))} | 211 | ))} |
| 210 | </div> | 212 | </div> |
| 211 | {!loading && ( | 213 | </div> |
| 212 | <> | ||
| 213 | <div style={{ padding: "25px 0px" }}> | ||
| 214 | <h3 style={{ margin: "0px 0px" }}>Select Map</h3> | ||
| 215 | <div onClick={() => _handle_dropdowns(2)} style={{ display: "flex", alignItems: "center", cursor: "pointer", justifyContent: "space-between", margin: "10px 0px" }}> | ||
| 216 | <span style={{ userSelect: "none" }}>{currentMap}</span> | ||
| 217 | <i style={{ rotate: "-90deg", transform: "translate(-5px, 10px)" }} className="triangle"></i> | ||
| 218 | </div> | ||
| 219 | </div> | ||
| 220 | <div style={{top: "220px"}} id='dropdown2' className={dropdown2Vis ? "upload-run-dropdown" : "upload-run-dropdown hidden"}> | ||
| 221 | {selectedGameMaps && selectedGameMaps.filter(gameMap => !gameMap.is_disabled).map((gameMap) => ( | ||
| 222 | <div onClick={() => { setUploadRunContent({ ...uploadRunContent, map_id: gameMap.id }); _set_current_map(gameMap.name); _handle_dropdowns(2); }} key={gameMap.id}>{gameMap.name}</div> | ||
| 223 | ))} | ||
| 224 | </div> | ||
| 225 | </> | ||
| 226 | |||
| 227 | )} | ||
| 228 | </div> | ||
| 229 | 214 | ||
| 230 | { | 215 | { |
| 231 | !loading && | 216 | !loading && |
| 232 | ( | 217 | ( |
| 233 | <> | 218 | <> |
| 234 | 219 | ||
| 235 | <div> | 220 | <div> |
| 236 | <h3 style={{margin: "10px 0px"}}>Host Demo</h3> | 221 | <h3 style={{ margin: "10px 0px" }}>Host Demo</h3> |
| 237 | <div onClick={() => { _handle_file_click(true) }} onDragOver={(e) => { _handle_drag_over(e, true) }} onDrop={(e) => { _handle_drop(e, true) }} onDragLeave={(e) => { _handle_drag_leave(e, true) }} className={`upload-run-drag-area ${dragHightlight ? "upload-run-drag-area-highlight" : ""} ${uploadRunContent.host_demo ? "upload-run-drag-area-hidden" : ""}`}> | 222 | <div onClick={() => { _handle_file_click(true) }} onDragOver={(e) => { _handle_drag_over(e, true) }} onDrop={(e) => { _handle_drop(e, true) }} onDragLeave={(e) => { _handle_drag_leave(e, true) }} className={`upload-run-drag-area ${dragHightlight ? "upload-run-drag-area-highlight" : ""} ${uploadRunContent.host_demo ? "upload-run-drag-area-hidden" : ""}`}> |
| 238 | <input ref={fileInputRef} type="file" name="host_demo" id="host_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, true)} /> | 223 | <input ref={fileInputRef} type="file" name="host_demo" id="host_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, true)} /> |
| 239 | {!uploadRunContent.host_demo ? | 224 | {!uploadRunContent.host_demo ? |
| @@ -252,38 +237,44 @@ const UploadRunDialog: React.FC<UploadRunDialogProps> = ({ token, open, onClose, | |||
| 252 | games[selectedGameID].is_coop && | 237 | games[selectedGameID].is_coop && |
| 253 | ( | 238 | ( |
| 254 | <> | 239 | <> |
| 255 | <div> | 240 | <div> |
| 256 | <h3 style={{margin: "10px 0px"}}>Partner Demo</h3> | 241 | <h3 style={{ margin: "10px 0px" }}>Partner Demo</h3> |
| 257 | <div onClick={() => { _handle_file_click(false) }} onDragOver={(e) => { _handle_drag_over(e, false) }} onDrop={(e) => { _handle_drop(e, false) }} onDragLeave={(e) => { _handle_drag_leave(e, false) }} className={`upload-run-drag-area ${dragHightlightPartner ? "upload-run-drag-area-highlight-partner" : ""} ${uploadRunContent.partner_demo ? "upload-run-drag-area-hidden" : ""}`}> | 242 | <div onClick={() => { _handle_file_click(false) }} onDragOver={(e) => { _handle_drag_over(e, false) }} onDrop={(e) => { _handle_drop(e, false) }} onDragLeave={(e) => { _handle_drag_leave(e, false) }} className={`upload-run-drag-area ${dragHightlightPartner ? "upload-run-drag-area-highlight-partner" : ""} ${uploadRunContent.partner_demo ? "upload-run-drag-area-hidden" : ""}`}> |
| 258 | <input ref={fileInputRefPartner} type="file" name="partner_demo" id="partner_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, false)} /> {!uploadRunContent.partner_demo ? | 243 | <input ref={fileInputRefPartner} type="file" name="partner_demo" id="partner_demo" accept=".dem" onChange={(e) => _handle_file_change(e.target.files, false)} /> {!uploadRunContent.partner_demo ? |
| 259 | <div> | ||
| 260 | <span>Drag and drop</span> | ||
| 261 | <div> | 244 | <div> |
| 262 | <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> | 245 | <span>Drag and drop</span> |
| 263 | <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button> | 246 | <div> |
| 247 | <span style={{ fontFamily: "BarlowSemiCondensed-Regular" }}>Or click here</span><br /> | ||
| 248 | <button style={{ borderRadius: "24px", padding: "5px 8px", margin: "5px 0px" }}>Upload</button> | ||
| 249 | </div> | ||
| 264 | </div> | 250 | </div> |
| 265 | </div> | 251 | : null} |
| 266 | : null} | ||
| 267 | 252 | ||
| 268 | <span className="upload-run-demo-name">{uploadRunContent.partner_demo?.name}</span> | 253 | <span className="upload-run-demo-name">{uploadRunContent.partner_demo?.name}</span> |
| 254 | </div> | ||
| 269 | </div> | 255 | </div> |
| 270 | </div> | ||
| 271 | </> | 256 | </> |
| 272 | ) | 257 | ) |
| 273 | } | 258 | } |
| 274 | </div> | 259 | </div> |
| 275 | <div className='search-container'> | 260 | <div className='search-container'> |
| 261 | |||
| 262 | </div> | ||
| 276 | 263 | ||
| 277 | </div> | ||
| 278 | |||
| 279 | </> | 264 | </> |
| 280 | ) | 265 | ) |
| 281 | } | 266 | } |
| 282 | </div> | 267 | </div> |
| 283 | <div className='upload-run-buttons-container'> | 268 | <div className='upload-run-buttons-container'> |
| 284 | <button onClick={_upload_run}>Submit</button> | 269 | <button onClick={_upload_run}>Submit</button> |
| 285 | <button onClick={() => onClose(false)}>Cancel</button> | 270 | <button onClick={() => { |
| 286 | </div> | 271 | onClose(false); |
| 272 | setUploadRunContent({ | ||
| 273 | host_demo: null, | ||
| 274 | partner_demo: null, | ||
| 275 | }); | ||
| 276 | }}>Cancel</button> | ||
| 277 | </div> | ||
| 287 | </div> | 278 | </div> |
| 288 | </div> | 279 | </div> |
| 289 | </> | 280 | </> |
diff --git a/frontend/src/types/Content.ts b/frontend/src/types/Content.ts index 42a6917..775fab4 100644 --- a/frontend/src/types/Content.ts +++ b/frontend/src/types/Content.ts | |||
| @@ -18,7 +18,6 @@ export interface MapDiscussionCommentContent { | |||
| 18 | }; | 18 | }; |
| 19 | 19 | ||
| 20 | export interface UploadRunContent { | 20 | export interface UploadRunContent { |
| 21 | map_id: number; | ||
| 22 | host_demo: File | null; | 21 | host_demo: File | null; |
| 23 | partner_demo: File | null; | 22 | partner_demo: File | null; |
| 24 | }; | 23 | }; |
diff --git a/frontend/src/types/MapNames.ts b/frontend/src/types/MapNames.ts new file mode 100644 index 0000000..b6313e7 --- /dev/null +++ b/frontend/src/types/MapNames.ts | |||
| @@ -0,0 +1,127 @@ | |||
| 1 | export const MapNames: { [key: string]: number } = { | ||
| 2 | "sp_a1_intro1": 1, | ||
| 3 | "sp_a1_intro2": 2, | ||
| 4 | "sp_a1_intro3": 3, | ||
| 5 | "sp_a1_intro4": 4, | ||
| 6 | "sp_a1_intro5": 5, | ||
| 7 | "sp_a1_intro6": 6, | ||
| 8 | "sp_a1_intro7": 7, | ||
| 9 | "sp_a1_wakeup": 8, | ||
| 10 | "sp_a2_intro": 9, | ||
| 11 | |||
| 12 | "sp_a2_laser_intro": 10, | ||
| 13 | "sp_a2_laser_stairs": 11, | ||
| 14 | "sp_a2_dual_lasers": 12, | ||
| 15 | "sp_a2_laser_over_goo": 13, | ||
| 16 | "sp_a2_catapult_intro": 14, | ||
| 17 | "sp_a2_trust_fling": 15, | ||
| 18 | "sp_a2_pit_flings": 16, | ||
| 19 | "sp_a2_fizzler_intro": 17, | ||
| 20 | |||
| 21 | "sp_a2_sphere_peek": 18, | ||
| 22 | "sp_a2_ricochet": 19, | ||
| 23 | "sp_a2_bridge_intro": 20, | ||
| 24 | "sp_a2_bridge_the_gap": 21, | ||
| 25 | "sp_a2_turret_intro": 22, | ||
| 26 | "sp_a2_laser_relays": 23, | ||
| 27 | "sp_a2_turret_blocker": 24, | ||
| 28 | "sp_a2_laser_vs_turret": 25, | ||
| 29 | "sp_a2_pull_the_rug": 26, | ||
| 30 | |||
| 31 | "sp_a2_column_blocker": 27, | ||
| 32 | "sp_a2_laser_chaining": 28, | ||
| 33 | "sp_a2_triple_laser": 29, | ||
| 34 | "sp_a2_bts1": 30, | ||
| 35 | "sp_a2_bts2": 31, | ||
| 36 | |||
| 37 | "sp_a2_bts3": 32, | ||
| 38 | "sp_a2_bts4": 33, | ||
| 39 | "sp_a2_bts5": 34, | ||
| 40 | "sp_a2_core": 35, | ||
| 41 | |||
| 42 | "sp_a3_01": 36, | ||
| 43 | "sp_a3_03": 37, | ||
| 44 | "sp_a3_jump_intro": 38, | ||
| 45 | "sp_a3_bomb_flings": 39, | ||
| 46 | "sp_a3_crazy_box": 40, | ||
| 47 | "sp_a3_transition01": 41, | ||
| 48 | |||
| 49 | "sp_a3_speed_ramp": 42, | ||
| 50 | "sp_a3_speed_flings": 43, | ||
| 51 | "sp_a3_portal_intro": 44, | ||
| 52 | "sp_a3_end": 45, | ||
| 53 | |||
| 54 | "sp_a4_intro": 46, | ||
| 55 | "sp_a4_tb_intro": 47, | ||
| 56 | "sp_a4_tb_trust_drop": 48, | ||
| 57 | "sp_a4_tb_wall_button": 49, | ||
| 58 | "sp_a4_tb_polarity": 50, | ||
| 59 | "sp_a4_tb_catch": 51, | ||
| 60 | "sp_a4_stop_the_box": 52, | ||
| 61 | "sp_a4_laser_catapult": 53, | ||
| 62 | "sp_a4_laser_platform": 54, | ||
| 63 | "sp_a4_speed_tb_catch": 55, | ||
| 64 | "sp_a4_jump_polarity": 56, | ||
| 65 | |||
| 66 | "sp_a4_finale1": 57, | ||
| 67 | "sp_a4_finale2": 58, | ||
| 68 | "sp_a4_finale3": 59, | ||
| 69 | "sp_a4_finale4": 60, | ||
| 70 | |||
| 71 | "mp_coop_start": 61, | ||
| 72 | "mp_coop_lobby_3": 62, | ||
| 73 | |||
| 74 | "mp_coop_doors": 63, | ||
| 75 | "mp_coop_race_2": 64, | ||
| 76 | "mp_coop_laser_2": 65, | ||
| 77 | "mp_coop_rat_maze": 66, | ||
| 78 | "mp_coop_laser_crusher": 67, | ||
| 79 | "mp_coop_teambts": 68, | ||
| 80 | |||
| 81 | "mp_coop_fling_3": 69, | ||
| 82 | "mp_coop_infinifling_train": 70, | ||
| 83 | "mp_coop_come_along": 71, | ||
| 84 | "mp_coop_fling_1": 72, | ||
| 85 | "mp_coop_catapult_1": 73, | ||
| 86 | "mp_coop_multifling_1": 74, | ||
| 87 | "mp_coop_fling_crushers": 75, | ||
| 88 | "mp_coop_fan": 76, | ||
| 89 | |||
| 90 | "mp_coop_wall_intro": 77, | ||
| 91 | "mp_coop_wall_2": 78, | ||
| 92 | "mp_coop_catapult_wall_intro": 79, | ||
| 93 | "mp_coop_wall_block": 80, | ||
| 94 | "mp_coop_catapult_2": 81, | ||
| 95 | "mp_coop_turret_walls": 82, | ||
| 96 | "mp_coop_turret_ball": 83, | ||
| 97 | "mp_coop_wall_5": 84, | ||
| 98 | |||
| 99 | "mp_coop_tbeam_redirect": 85, | ||
| 100 | "mp_coop_tbeam_drill": 86, | ||
| 101 | "mp_coop_tbeam_catch_grind_1": 87, | ||
| 102 | "mp_coop_tbeam_laser_1": 88, | ||
| 103 | "mp_coop_tbeam_polarity": 89, | ||
| 104 | "mp_coop_tbeam_polarity2": 90, | ||
| 105 | "mp_coop_tbeam_polarity3": 91, | ||
| 106 | "mp_coop_tbeam_maze": 92, | ||
| 107 | "mp_coop_tbeam_end": 93, | ||
| 108 | |||
| 109 | "mp_coop_paint_come_along": 94, | ||
| 110 | "mp_coop_paint_redirect": 95, | ||
| 111 | "mp_coop_paint_bridge": 96, | ||
| 112 | "mp_coop_paint_walljumps": 97, | ||
| 113 | "mp_coop_paint_speed_fling": 98, | ||
| 114 | "mp_coop_paint_red_racer": 99, | ||
| 115 | "mp_coop_paint_speed_catch": 100, | ||
| 116 | "mp_coop_paint_longjump_intro": 101, | ||
| 117 | |||
| 118 | "mp_coop_separation_1": 102, | ||
| 119 | "mp_coop_tripleaxis": 103, | ||
| 120 | "mp_coop_catapult_catch": 104, | ||
| 121 | "mp_coop_2paints_1bridge": 105, | ||
| 122 | "mp_coop_paint_conversion": 106, | ||
| 123 | "mp_coop_bridge_catch": 107, | ||
| 124 | "mp_coop_laser_tbeam": 108, | ||
| 125 | "mp_coop_paint_rat_maze": 109, | ||
| 126 | "mp_coop_paint_crazy_box": 110, | ||
| 127 | }; | ||