@@ -17,12 +23,12 @@ export const ConvertEventsPage = () => {
window.api.convertRMXPEventsToStudioEvents(
- { events: [], map: JSON.stringify(map), projectPath },
+ { events, map: JSON.stringify(map), projectPath },
() => setResult(`Success! (Map id: ${map.id})`),
({ errorMessage }) => {
console.error(errorMessage);
setResult(`Error! (Map id: ${map.id}) Read the console log`);
- }
+ },
)
}
>
diff --git a/src/utils/ModelUtils.ts b/src/utils/ModelUtils.ts
index 3d85963f..865659a5 100644
--- a/src/utils/ModelUtils.ts
+++ b/src/utils/ModelUtils.ts
@@ -1,3 +1,4 @@
+import { StudioEvent } from '@modelEntities/event/event';
import { StudioTextInfo } from '@modelEntities/textInfo';
import { StudioTrainerAdditionalDialogs } from '@modelEntities/trainer';
import { ProjectData } from '@src/GlobalStateProvider';
@@ -8,7 +9,7 @@ import { ProjectData } from '@src/GlobalStateProvider';
* @returns The text id
*/
export const findFirstAvailableTextId = (
- allData: ProjectData['abilities'] | ProjectData['types'] | StudioTextInfo[] | StudioTrainerAdditionalDialogs[]
+ allData: ProjectData['abilities'] | ProjectData['types'] | StudioTextInfo[] | StudioTrainerAdditionalDialogs[],
) => {
const textIdSet = Object.values(allData)
.map(({ textId }) => textId) // Fetch all ids
@@ -109,3 +110,37 @@ export const findFirstAvailableCustomObjectiveTextId = (allQuests: ProjectData['
return textIdSet[holeIndex - 1] + 1;
};
+
+export const findFirstAvailableCsvFileId = (allData: Record, startId: number) => {
+ const values = Object.values(allData);
+ if (values.length === 0) return startId;
+
+ const idSet = values
+ .map(({ csvFileId }) => csvFileId) // Fetch all csvFileIds
+ .filter((id, index, array) => index === array.indexOf(id)) // reject all duplicates
+ .sort((a, b) => a - b); // sort id by ascending order
+ // Since ids are ordered, if the first isn't the startId that means we need to fill the beginning of the list ;)
+ if (idSet[0] > startId) return startId;
+
+ const holeIndex = idSet.findIndex((id, index) => id !== index + startId);
+ if (holeIndex === -1) return idSet[idSet.length - 1] + 1;
+
+ return idSet[holeIndex - 1] + 1;
+};
+
+export const findFirstAvailableTextIdEvent = (event: StudioEvent, startId: number) => {
+ const commands = Object.values(event.commands).filter((command) => !!command && command.type === 'show_message');
+ if (commands.length === 0) return { messageId: startId, narratorId: startId + 1 };
+
+ const idSet = commands
+ .reduce((prev, { message, narrator }) => [...prev, message, narrator], [])
+ .filter((id, index, array) => index === array.indexOf(id)) // reject all duplicates
+ .sort((a, b) => a - b); // sort id by ascending order
+ // Since ids are ordered, if the first isn't the startId that means we need to fill the beginning of the list ;)
+ if (idSet[0] > startId && idSet[1] > startId + 1) return { messageId: startId, narratorId: startId + 1 };
+
+ const holeIndex = idSet.findIndex((id, index) => id !== index + startId);
+ if (holeIndex === -1) return { messageId: idSet[idSet.length - 1] + 1, narratorId: idSet[idSet.length - 1] + 2 };
+
+ return { messageId: idSet[holeIndex - 1] + 1, narratorId: idSet[holeIndex - 1] + 2 };
+};
diff --git a/src/utils/entityCreation.ts b/src/utils/entityCreation.ts
index e8bc97b8..466afbd2 100644
--- a/src/utils/entityCreation.ts
+++ b/src/utils/entityCreation.ts
@@ -5,8 +5,11 @@ import { DEX_DEFAULT_NAME_TEXT_ID, StudioDex, StudioDexCreature } from '@modelEn
import type { StudioCustomGroupCondition, StudioGroup, StudioGroupSystemTag, StudioGroupTool, StudioGroupVsType } from '@modelEntities/group';
import { createExpandPokemonSetup, StudioGroupEncounter } from '@modelEntities/groupEncounter';
import type { StudioItem, StudioItemStatusCondition } from '@modelEntities/item';
+import type { StudioMap, StudioMapAudio } from '@modelEntities/map';
+import type { StudioMapInfo, StudioMapInfoMap } from '@modelEntities/mapInfo';
import type { StudioMapLink } from '@modelEntities/mapLink';
import type { StudioMove, StudioMoveCategory, StudioMoveCondition } from '@modelEntities/move';
+import type { StudioNature } from '@modelEntities/nature';
import type {
StudioCreatureQuestCondition,
StudioCreatureQuestConditionType,
@@ -17,21 +20,24 @@ import type {
StudioQuestObjectiveType,
StudioQuestResolution,
} from '@modelEntities/quest';
+import type { StudioTextInfo } from '@modelEntities/textInfo';
import type { StudioTrainer, StudioTrainerVsType } from '@modelEntities/trainer';
import type { StudioType } from '@modelEntities/type';
import type { StudioZone } from '@modelEntities/zone';
import { ProjectData } from '@src/GlobalStateProvider';
+import { CommandId, StudioEventCommand } from '../models/entities/event/command';
+import { EVENT_START_CSV_FILE_ID, StudioEvent } from '../models/entities/event/event';
import { assertUnreachable } from './assertUnreachable';
-import { findFirstAvailableCustomObjectiveTextId, findFirstAvailableFormTextId, findFirstAvailableId, findFirstAvailableTextId } from './ModelUtils';
-import { padStr } from './PadStr';
-import type { StudioTextInfo } from '@modelEntities/textInfo';
-import type { StudioMap, StudioMapAudio } from '@modelEntities/map';
-import type { StudioMapInfo, StudioMapInfoMap } from '@modelEntities/mapInfo';
-import type { StudioNature } from '@modelEntities/nature';
-import { mapInfoFindFirstAvailableId, mapInfoFindFirstAvailableTextId } from './MapInfoUtils';
import { cloneEntity } from './cloneEntity';
-import { CommandId, StudioEventCommand } from '../models/entities/event/command';
-import { CustomEvent, StudioEvent } from '../models/entities/event/event';
+import { mapInfoFindFirstAvailableId, mapInfoFindFirstAvailableTextId } from './MapInfoUtils';
+import {
+ findFirstAvailableCsvFileId,
+ findFirstAvailableCustomObjectiveTextId,
+ findFirstAvailableFormTextId,
+ findFirstAvailableId,
+ findFirstAvailableTextId,
+} from './ModelUtils';
+import { padStr } from './PadStr';
/**
* Create a new ability with default values
@@ -615,10 +621,15 @@ export const createNature = (allNatures: ProjectData['natures'], dbSymbol: DbSym
};
};
-export const createEvent = (dbSymbol: DbSymbol, id: number): StudioEvent => {
+export const createEvent = (allEvents: ProjectData['events']): StudioEvent => {
+ const id = findFirstAvailableId(allEvents, 1);
+ const dbSymbol = `event_${id}` as DbSymbol;
+ const csvFileId = findFirstAvailableCsvFileId(allEvents, EVENT_START_CSV_FILE_ID);
+
return {
dbSymbol,
id,
+ csvFileId,
klass: 'Event',
type: 'custom',
triggers: [],
diff --git a/src/utils/eventCommandCreation.ts b/src/utils/eventCommandCreation.ts
index d7601508..622ab5ff 100644
--- a/src/utils/eventCommandCreation.ts
+++ b/src/utils/eventCommandCreation.ts
@@ -1,79 +1,101 @@
-import type { StudioEventCommandType, StudioEventCommand, StudioEventCommandData } from '@modelEntities/event/command';
+import type { StudioEventCommand, StudioEventCommandData, StudioEventCommandType } from '@modelEntities/event/command';
+import { StudioEvent } from '@modelEntities/event/event';
+import { findFirstAvailableTextIdEvent } from './ModelUtils';
-const insertScriptCommand = { script: '' };
-
-export const EventCommandCreation: Record, 'type'>> = {
- show_message: {},
- narrator_settings: {},
- manage_message_box: {},
- show_choice: {},
- wait_key_press: {},
- record_key_press: {},
- input_creature_name: {},
- input_character_name: {},
- ask_player_for_number: {},
- create_loop: {},
- exit_loop: {},
- manage_conditions: {},
- go_to: {},
- wait_for_set_time: {},
- stop_event_execution: {},
- call_event: {},
- trigger_event: {},
- change_event_parameters: {},
- move_event: {},
- teleport_event: {},
- teleport_player: {},
- wait_move_completion: {},
- manage_event_reappearance: {},
- manage_path_finding: {},
- manage_follow_me: {},
- manage_variables: {},
- manage_event_variables: {},
- manage_timer: {},
- change_character_name: {},
- start_trainer_battle: {},
- start_wild_encounter: {},
- start_scripted_battle: {},
- manage_random_encounters: {},
- manage_player_items: {},
- manage_player_money: {},
- manage_dex: {},
- set_active_dex: {},
- give_badge: {},
- manage_access_save_menu: {},
- open_save_menu: {},
- manage_autosave: {},
- force_autosave: {},
- force_save: {},
- open_scene: {},
- open_shop: {},
- open_custom_scene: {},
- manage_access_main_menu: {},
- trigger_game_over: {},
- return_to_title_screen: {},
- open_creature_shop: {},
- start_quest: {},
- display_hidden_objective: {},
- validate_quest_objectives: {},
- display_quest_progress: {},
- complete_quest: {},
- play_sound: {},
- stop_current_sound: {},
- change_default_sound: {},
- memorize_background_sounds: {},
- restore_background_sounds: {},
- play_creature_cry: {},
- change_screen_tone: {},
- display_animation: {},
- display_screen_animation: {},
- display_emotion: {},
- manage_image: {},
- manage_camera: {},
- manage_dynamic_light: {},
- change_weather: {},
- manage_map_fog: {},
- manage_map_panorama: {},
- change_battle_background: {},
- insert_script: insertScriptCommand,
+const createShowMessageCommand = (event: StudioEvent) => {
+ const { messageId, narratorId } = findFirstAvailableTextIdEvent(event, 0);
+ return {
+ message: messageId,
+ allowSkipping: false,
+ narrator: narratorId,
+ nameColor: '#000000',
+ showMessageBox: true,
+ messageBoxPosition: 'bottom',
+ messageBoxAppearance: '',
+ lookAtThisEvent: false,
+ lookToOtherEvent: '__undef__',
+ minimap: '',
+ portraits: [],
+ };
};
+
+const insertScriptCommand = () => ({ script: '' });
+
+const dummy = () => ({});
+
+export const EventCommandCreation: Record Omit, 'type'>> =
+ {
+ show_message: createShowMessageCommand,
+ narrator_settings: dummy,
+ manage_message_box: dummy,
+ show_choice: dummy,
+ wait_key_press: dummy,
+ record_key_press: dummy,
+ input_creature_name: dummy,
+ input_character_name: dummy,
+ ask_player_for_number: dummy,
+ create_loop: dummy,
+ exit_loop: dummy,
+ manage_conditions: dummy,
+ go_to: dummy,
+ wait_for_set_time: dummy,
+ stop_event_execution: dummy,
+ call_event: dummy,
+ trigger_event: dummy,
+ change_event_parameters: dummy,
+ move_event: dummy,
+ teleport_event: dummy,
+ teleport_player: dummy,
+ wait_move_completion: dummy,
+ manage_event_reappearance: dummy,
+ manage_path_finding: dummy,
+ manage_follow_me: dummy,
+ manage_variables: dummy,
+ manage_event_variables: dummy,
+ manage_timer: dummy,
+ change_character_name: dummy,
+ start_trainer_battle: dummy,
+ start_wild_encounter: dummy,
+ start_scripted_battle: dummy,
+ manage_random_encounters: dummy,
+ manage_player_items: dummy,
+ manage_player_money: dummy,
+ manage_dex: dummy,
+ set_active_dex: dummy,
+ give_badge: dummy,
+ manage_access_save_menu: dummy,
+ open_save_menu: dummy,
+ manage_autosave: dummy,
+ force_autosave: dummy,
+ force_save: dummy,
+ open_scene: dummy,
+ open_shop: dummy,
+ open_custom_scene: dummy,
+ manage_access_main_menu: dummy,
+ trigger_game_over: dummy,
+ return_to_title_screen: dummy,
+ open_creature_shop: dummy,
+ start_quest: dummy,
+ display_hidden_objective: dummy,
+ validate_quest_objectives: dummy,
+ display_quest_progress: dummy,
+ complete_quest: dummy,
+ play_sound: dummy,
+ stop_current_sound: dummy,
+ change_default_sound: dummy,
+ memorize_background_sounds: dummy,
+ restore_background_sounds: dummy,
+ play_creature_cry: dummy,
+ change_screen_tone: dummy,
+ display_animation: dummy,
+ display_screen_animation: dummy,
+ display_emotion: dummy,
+ manage_image: dummy,
+ manage_camera: dummy,
+ manage_dynamic_light: dummy,
+ change_weather: dummy,
+ manage_map_fog: dummy,
+ manage_map_panorama: dummy,
+ change_battle_background: dummy,
+ insert_script: insertScriptCommand,
+ };
diff --git a/src/utils/events/EventUtils.ts b/src/utils/events/EventUtils.ts
index 0e49d661..ea8ea4a0 100644
--- a/src/utils/events/EventUtils.ts
+++ b/src/utils/events/EventUtils.ts
@@ -31,7 +31,7 @@ export const initCommandNodes = (event: StudioEvent, dialogsRef?: CommandDialogs
id,
type: command?.type,
position: { x: command?.studioData.x || 0, y: command?.studioData.y || 0 },
- data: { dialogsRef, command, comments: command?.studioData.comments },
+ data: { dialogsRef, command, comments: command?.studioData.comments, csvFileId: event.csvFileId },
}));
};
diff --git a/src/utils/inputAttrs.ts b/src/utils/inputAttrs.ts
index 89302e64..91682e32 100644
--- a/src/utils/inputAttrs.ts
+++ b/src/utils/inputAttrs.ts
@@ -10,7 +10,7 @@ type InputProps = Pick<
export const inputAttrsSingle = (
singleAttributeValidator: z.ZodFirstPartySchemaTypes,
name: string,
- defaults?: Record
+ defaults?: Record,
): InputProps => {
if (singleAttributeValidator instanceof z.ZodBranded) {
return inputAttrsSingle(singleAttributeValidator.unwrap(), name, defaults);
@@ -88,20 +88,27 @@ export const inputAttrsSingle = (
return attributes;
};
+const unwrapValidator = (validator: z.ZodFirstPartySchemaTypes): z.ZodFirstPartySchemaTypes => {
+ if (validator instanceof z.ZodDefault) return unwrapValidator(validator._def.innerType as z.ZodFirstPartySchemaTypes);
+ if (validator instanceof z.ZodOptional) return unwrapValidator(validator._def.innerType as z.ZodFirstPartySchemaTypes);
+ if (validator instanceof z.ZodNullable) return unwrapValidator(validator._def.innerType as z.ZodFirstPartySchemaTypes);
+ return validator;
+};
+
const getValidatorFromSchema = (schema: z.ZodObject, schemaKey: string) => {
const keys = schemaKey.split('.');
let validator: z.ZodFirstPartySchemaTypes = schema;
for (const key of keys) {
+ const unwrapped = unwrapValidator(validator);
if (isStringPositiveInteger(key)) {
- if (validator instanceof z.ZodArray) {
- const newValidator = validator._def.type as z.ZodFirstPartySchemaTypes;
- validator = newValidator;
+ if (unwrapped instanceof z.ZodArray) {
+ validator = unwrapped._def.type as z.ZodFirstPartySchemaTypes;
} else {
throw new Error('Cannot have extract type from non array object with numeric key');
}
} else {
- if (validator instanceof z.ZodObject) {
- validator = validator.shape[key];
+ if (unwrapped instanceof z.ZodObject) {
+ validator = unwrapped.shape[key];
if (!validator) throw new Error(`Failed to extract ${key} from schema (${schemaKey})`);
} else {
throw new Error('Expect simple Zod object with string key, consider giving a non-opaque schema with a schemaKey instead.');
diff --git a/src/views/components/buttons/DarkButtonWithPlusIcon.tsx b/src/views/components/buttons/DarkButtonWithPlusIcon.tsx
index fea200a7..f895a4fa 100644
--- a/src/views/components/buttons/DarkButtonWithPlusIcon.tsx
+++ b/src/views/components/buttons/DarkButtonWithPlusIcon.tsx
@@ -1,9 +1,11 @@
-import React from 'react';
-import PlusIcon from '@assets/icons/global/plus-icon.svg';
+import EditIcon from '@assets/icons/global/edit-icon.svg';
import ImportIcon from '@assets/icons/global/import-icon.svg';
+import PlusIcon from '@assets/icons/global/plus-icon.svg';
+import QuestionMarkIcon from '@assets/icons/global/question-mark-icon.svg';
import ReOrderIcon from '@assets/icons/global/reorder.svg';
-import { SecondaryButton, DarkButton } from './GenericButtons';
+import React from 'react';
import styled from 'styled-components';
+import { DarkButton, SecondaryButton } from './GenericButtons';
type DarkButtonWithPlusIconProps = Omit[0], 'theme'>;
@@ -59,3 +61,17 @@ export const DarkButtonReOrderResponsive = ({ children, disabled, breakpoint, ..
{children}
);
+
+export const DarkButtonEditResponsive = ({ children, disabled, ...props }: DarkButtonWithPlusIconResponsiveProps) => (
+
+
+ {children}
+
+);
+
+export const DarkButtonQuestionMarkResponsive = ({ children, disabled, ...props }: DarkButtonWithPlusIconResponsiveProps) => (
+
+
+ {children}
+
+);
diff --git a/src/views/components/database/type/editors/TypeFrameEditor.tsx b/src/views/components/database/type/editors/TypeFrameEditor.tsx
index 4adc0233..d244cdea 100644
--- a/src/views/components/database/type/editors/TypeFrameEditor.tsx
+++ b/src/views/components/database/type/editors/TypeFrameEditor.tsx
@@ -67,7 +67,7 @@ export const TypeFrameEditor = forwardRef((_, ref) => {
- setColor(event.target.value)} />
+ setColor(event.target.value)} />
diff --git a/src/views/components/editor/Editor.tsx b/src/views/components/editor/Editor.tsx
index 9a71a467..26c110a0 100644
--- a/src/views/components/editor/Editor.tsx
+++ b/src/views/components/editor/Editor.tsx
@@ -1,3 +1,5 @@
+import ClearIcon from '@assets/icons/global/clear-tag-icon.svg';
+import { DarkButton } from '@components/buttons';
import { PaginationWithTitle, PaginationWithTitleProps } from '@components/PaginationWithTitle';
import React, { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
@@ -38,6 +40,31 @@ export const EditorTitle = styled.div`
}
`;
+export const EditorTitleContainer = styled.div`
+ display: flex;
+ gap: 8px;
+ justify-content: space-between;
+
+ ${DarkButton} {
+ padding: 0;
+ min-width: 32px;
+ height: 32px;
+ }
+
+ ${EditorTitle} {
+ padding: 0;
+
+ & > h3 {
+ padding: 0;
+ border: none;
+ }
+ }
+
+ padding: 0 0 12px 0;
+ border-bottom: 1px solid ${({ theme }) => theme.colors.dark20};
+ margin-bottom: 16px;
+`;
+
type EditorProps = {
type:
| 'edit'
@@ -55,31 +82,46 @@ type EditorProps = {
| 'combo_moves';
title: string;
children: ReactNode;
+ onClose?: () => void;
};
-export const Editor = ({ type, title, children }: EditorProps) => {
+export const Editor = ({ type, title, children, onClose }: EditorProps) => {
const { t } = useTranslation();
return (
-
- {t(type)}
- {title}
-
+
+
+ {t(type)}
+ {title}
+
+ {onClose && (
+
+
+
+ )}
+
{children}
);
};
-export const EditorWithCollapse = ({ type, title, children }: EditorProps) => {
+export const EditorWithCollapse = ({ type, title, children, onClose }: EditorProps) => {
const { t } = useTranslation();
return (
-
- {t(type)}
- {title}
-
+
+
+ {t(type)}
+ {title}
+
+ {onClose && (
+
+
+
+ )}
+
{children}
);
@@ -87,7 +129,7 @@ export const EditorWithCollapse = ({ type, title, children }: EditorProps) => {
type EditorWithPaginationProps = {
paginationProps?: PaginationWithTitleProps;
-} & EditorProps;
+} & Omit;
export const EditorWithPagination = ({ type, title, children, paginationProps }: EditorWithPaginationProps) => {
const { t } = useTranslation();
diff --git a/src/views/components/editor/TranslationEditorWithCloseHandling.tsx b/src/views/components/editor/TranslationEditorWithCloseHandling.tsx
index 083f12a5..b67de173 100644
--- a/src/views/components/editor/TranslationEditorWithCloseHandling.tsx
+++ b/src/views/components/editor/TranslationEditorWithCloseHandling.tsx
@@ -1,17 +1,17 @@
+import ClearIcon from '@assets/icons/global/clear-tag-icon.svg';
import { DarkButton } from '@components/buttons';
+import { Input, InputContainer, InputWithTopLabelContainer, Label, MultiLineInput } from '@components/inputs';
+import { SecondaryTag } from '@components/Tag';
+import { getProjectMultiLanguageTextChange } from '@hooks/updateProjectText';
+import { useGlobalState } from '@src/GlobalStateProvider';
+import { getText, useGetProjectText } from '@utils/ReadingProjectText';
+import { SavingTextMap } from '@utils/SavingUtils';
import React, { forwardRef, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
-import { EditorTitle } from './Editor';
+import { EditorTitle, EditorTitleContainer } from './Editor';
import { EditorContainer } from './EditorContainer';
-import ClearIcon from '@assets/icons/global/clear-tag-icon.svg';
-import { useGlobalState } from '@src/GlobalStateProvider';
-import { Input, InputContainer, InputWithTopLabelContainer, Label, MultiLineInput } from '@components/inputs';
-import { SecondaryTag } from '@components/Tag';
-import { getText, useGetProjectText } from '@utils/ReadingProjectText';
import { EditorHandlingClose, useEditorHandlingClose } from './useHandleCloseEditor';
-import { getProjectMultiLanguageTextChange } from '@hooks/updateProjectText';
-import { SavingTextMap } from '@utils/SavingUtils';
const TranslationEditorContainer = styled(EditorContainer)`
position: absolute;
@@ -31,31 +31,6 @@ const TranslationEditorContainer = styled(EditorContainer)`
}
`;
-const TranslateEditorTitleContainer = styled.div`
- display: flex;
- gap: 8px;
- justify-content: space-between;
-
- ${DarkButton} {
- padding: 0;
- min-width: 32px;
- height: 32px;
- }
-
- ${EditorTitle} {
- padding: 0;
-
- & > h3 {
- padding: 0;
- border: none;
- }
- }
-
- padding: 0 0 12px 0;
- border-bottom: 1px solid ${({ theme }) => theme.colors.dark20};
- margin-bottom: 16px;
-`;
-
type TranslationInputProps = {
defaultValue: string;
name: string;
@@ -83,6 +58,8 @@ export type TranslationEditorTitle =
| 'translation_additional_dialog'
| 'translation_form_name'
| 'translation_form_description'
+ | 'translation_message'
+ | 'translation_narrator'
| 'translation_custom_objective';
type InputRefsType = Record;
@@ -111,16 +88,16 @@ const TranslationEditor = ({ title, name, textId, fileId, onClose, isMultiline,
state.projectStudio.languagesTranslation
.map<[string, number]>(({ code }, index) => [code, index])
.filter(([code]) => code !== defaultLanguageCode),
- [state.projectStudio.languagesTranslation, defaultLanguageCode]
+ [state.projectStudio.languagesTranslation, defaultLanguageCode],
);
const defaultLanguageName = useMemo(
() => state.projectStudio.languagesTranslation.find(({ code }) => code === defaultLanguageCode)?.name || '???',
- [defaultLanguageCode, state.projectStudio.languagesTranslation]
+ [defaultLanguageCode, state.projectStudio.languagesTranslation],
);
return (
-
+
{t('translation')}
{t(title, { name })}
@@ -128,7 +105,7 @@ const TranslationEditor = ({ title, name, textId, fileId, onClose, isMultiline,
-
+