A minimal MMORPG started a long time ago:
I found my old RPG graphics on my desktop, so my plan is to create an RPG game on the React stack, maybe with PWA output, as easy as possible.
Rogmor - Code Architecture & Gameplay Summary
Game Type
A minimal browser-based MMORPG with turn-based tactical combat, designed mobile-first as a Progressive Web App.
Technical Stack
- Frontend: Next.js 15 + React 19 + TypeScript
- Styling: Tailwind CSS + SCSS
- State: Redux-Saga for complex combat orchestration
- Backend: GraphQL (Apollo) + MongoDB
- Build: Next.js with SWC compiler
Architecture
src/
├── rpg/ # Game logic (stats, professions, skill parser)
├── gui/ # UI components (hero cards, windows, battle UI)
├── components/ # Page-level components (CombatZone, CreateHero)
├── lib/ # mainSaga.ts (combat engine orchestration)
├── graphql/ # API schema & resolvers
└── pages/ # Next.js routes
Core Gameplay
7 Character Attributes: Body, Reflex, Soul, Aura, Stamina, Focus, Morale
Combat Mechanics:
- Speed-based turn order (reflex + modifiers)
- 3 interaction types: Physical, Soul, Social
- Team-based battles (PLAYER vs BAD)
- Auto-fight or manual skill selection (3 slots: Alfa/Beta/Delta)
Unique Features:
- Custom skill language (slash.ts) - Tailwind-like syntax for defining abilities
- 50+ character classes with unique stat distributions
- Leveling formula: 0.5 × level^4 + (2 × level)
- Redux-Saga manages complex combat state flows
This is a sophisticated tactical RPG framework emphasizing modularity and extensibility!
The project has been modernized with latest dependencies and security fixes:
Major Version Updates:
- ✅ Next.js 14.2 → 15.5.12 (fixes DoS vulnerabilities)
- ✅ React 18.3 → 19.2.4
- ✅ TypeScript 4.7 → 5.9.3
- ✅ MongoDB driver 4.x → 6.x
- ✅ Node.js requirement: 18+ (previously 16)
Security Improvements:
- ✅ Removed hardcoded credentials and API keys from source code
- ✅ Added input validation to API routes (
/api/ai,/api/ai-image) - ✅ Fixed middleware authentication with proper error handling
- ✅ Migrated from deprecated
publicRuntimeConfigtoenvvariables - ✅ pnpm audit: No known vulnerabilities found
Code Quality:
- ✅ ESLint: Switched to
@typescript-eslintparser - ✅ Removed Jest and Cypress test dependencies (cleanup)
- ✅ Build successfully compiles with no errors
- Create
.env.localfile with your API keys (see.env.local.exampleif available) - Never commit API keys or secrets to version control
- For production: Use Vercel Environment Variables
In 2023, AI and ChatGPT gave me many ideas about how to improve my development process. I posted about this here:
2026 Update: Modernized with GitHub Copilot CLI
Using ChatGPT for Complex Fantasy Story and Programming Creation
Reworking this project to TypeScript and Next.js built with SWC.
This is the next iteration of my vision because it focuses on being realized as a Next.js project. Rogmor development started as a StackBlitz application, then became a React application, and now we're focusing on going into the server side.
Question: What is the minimal requirement for a usable mobile MMORPG?
- 2d images
- basic interface
- minimal animation
export function * fightSaga(a, b, fallenOne = p => p) {
yield `\n`;
yield `${a.name} - ${a.profession} : ${a.level} : ${a.reflex}`;
yield 'vs.';
yield `${b.name} - ${b.profession} : ${b.level} : ${b.reflex}`;
yield '-'.repeat(20);
yield `${a.reflex} : ${b.reflex}`;
const astart = improved(a.reflex)
const bstart = improved(b.reflex)
const [atk, def] = astart > bstart ? [a, b] : [b, a];
yield `Attacker is: ${atk.name} ${astart} vs ${bstart}`;
let round = 1;
while (def.staminaState > 0 && atk.staminaState > 0 && stayInCombat(a) && stayInCombat(b)) {
let [striker, target] = round % 2 ? [atk, def] : [def, atk];
let dmg = improved(striker.physique / 2);
yield `round: ${round} - ${striker.name} - strike ${dmg}`;
round ++;
target.staminaState -= Math.min(dmg, target.staminaState);
yield `${target.name} ${target.stamina}/${target.staminaState}`;
}
yield `${atk.staminaState <= 0 ? atk.name : '' } knocked out`;
yield `${def.staminaState <= 0 ? def.name : '' } knocked out`;
if (a.staminaState <= 0)
{fallenOne(a);}
else if (b.staminaState <= 0)
{fallenOne(b)}
else
{fallenOne(null)};
}body : It is physic of character including strength.
reflex : It is the reflex capability including first thinking and the reflex.
soul : Spiritual harmony, logical thinking and wisdom.
aura : This is the character presence, social and impact to other.
stamina : This is the stamina and Longevity. if value reduced to 0 then hero unable to do anything.
focus : Hero mind stability. If value reduce to 0 then hero is fall down.
morale : This is the fossil energy very helpful at any social interaction.
This program focus be refact to the nextjs I hope with that step I reach my goal to single typescript MMORPG focus be a reachable goal.
The following mistakes I made it when I wrote this program the first one are used exports define default solutions so that each component has no name and that is terrible resident and development tool: Component.
// ./components/NoNameComponent.tsx
export default () => (
<pre>
<p>Component whitout name</p>
<p>react component devtool isn't handle well this</p>
</pre>
);
// use
import IGaveAnotherName from './components/NoNameComponent';The big question is which database is perfect for this application?
- mongoDB - already selected mongoDB cloud one
- firebase
- supabase
system design with arrows
MATCH path0 = (Talk)<--(NPC)-->()<--(Story)<--()<--(Hero)-->(Experience)<--(Story)<--(Skillset),
path1 = (`Level `)-->()-->(Hero)-->(Inventory)-->()-->()-->(Hero)-->(Skillset),
path2 = (NPC)-->()<--(Story)-->(Talk),
path3 = (Login)-->(Play)-->(Hero)-->(`Level `)<--(Experience),
path4 = (Story)-->()-->(Inventory),
path5 = (Login)-->()-->(Play),
path6 = (:instance)-->(Login),
path7 = ()
RETURN path0, path1, path2, path3, path4, path5, path6, path7
I take few hour to solve the automatic vercel deployment from my gith repo.
On Build & Development Settings drop down you can select nextjs instead of try output library set to .next/
export function * encounterSaga() {
const {payload:{heroTeam, opponentTeam, area, isAuto}} = yield take(ENCOUNTER_BEGIN);
const {type} = yield take([LETS_TALK, AMBUSH, CHARGE]);
if (type === CHARGE) {
let sequence = speedOrder([
chargeOrder(heroTeam),
awarnesOrder(opponentTeam),
]);
while (sequence.length) {
const [next] = sequence;
const nextMove = heroTeam.includes(next)
? yield waitUserInteraction() // >> take([STRIKE, SKILL])
: yield figureOutOppositesMove()
}
}
}Small script language with tailwind like syntax for simplify skill programming. Easy compose functionality
# simple strong strike with weapon
target
body-hit
power-2
target-all
body-hit
weak-2
fill-4 # need a four round to refill
target-2
soul-hit-4
fill-3
target-ally
soul-heal-2
fill-2
target-ally-all
reflex-shield-4
instant # this means ready for use instant
fill-4
target-random
soul-hit-percent-5
fill-6
body-sacrifice-5
target-ally-dead
resurrection
soul-heal-4Under Slash development, the TDD is a great help. Step by step implement the whole action process, maybe this code is not the optimum, but looks it is working. And TS also great help to solve this complex process.
Seems it is works fine in reality. This way the whole progress can be followed. And shows some missing feature after the first implementation of slash the inner script language.
export function * combatZoneSaga() {
while (true) {
yield take(ENCOUNTER_BEGIN);
const {hero}:MainState = yield select();
const pickProf = () => pickOne(Object.keys(skillForProf))
const testTeams = yield getTeams();
const combatSetupMobList = testTeams.map(
([lvl, type, team, avatar]:[number, ProfessionKey, Team, number]) =>
makeMob(lvl, type, team, avatar)
);
yield putAction(SET_MOB_LIST, combatSetupMobList)
let mobList = combatSetupMobList;
_CombatIsOver_: while (true) {
const order = actionOrder(mobList);
while (order.length) {
const [actor]:OrderOfSeed = order.shift();
yield putAction(PLAY_FLOW, {who: actor.uid});
const skillList = getSkillObject(actor);
const {isAutoFight} = yield select();
const subList= yield call(userChiceTheSkillSaga, skillList, actor, isAutoFight)
_NextActor_: while (true) {
if (subList.length < 1) break _NextActor_;
const [, command] = yield race([
delay(BATTLE_SPEED),
take([HEART_BEAT, ENCOUNTER_OUTCOME]),
])
if (command?.type === ENCOUNTER_OUTCOME) break _CombatIsOver_;
const skill = subList.shift();
const [aiTargetting, skillResult] = getSkillResult(actor, skill, mobList);
yield putAction(PLAY_FLOW, aiTargetting);
yield putAction(PLAY_FLOW, skillResult);
mobList = yield call(skillReducer, mobList, skillResult);
yield putAction(SET_MOB_LIST, mobList);
const isTwoTeam = mobList
.filter(isCapableToAction)
.map(mob => mob.team)
.find((item, _, arr) => arr.indexOf(item) !== 0)
;
if(!isTwoTeam) {
yield take([HEART_BEAT, ENCOUNTER_OUTCOME]);
yield putAction(FOCUS_ON, null);
break _CombatIsOver_;
}
}
}
}
yield putAction(SET_MOB_LIST, [])
}
}
function * userChiceTheSkillSaga(skillList:Partial<SlashObject>[][], actor:Mob, isAutoFight: boolean) {
if (actor.team !== Team.GOOD || isAutoFight) return skillList.at(0)
const {payload: skillIndex} = yield take([USE_SKILL, SET_AUTO_FIGHT]);
return skillList.at(skillIndex);
}My imagine is I made whole game on gird 5 x 7, so this is fine for vertical mobile setup. With this single css attribute I can setup the size of item.
Goal is: really fast and easy UI module in react.
.container {
display: grid;
grid: repeat( 7, 7em) / repeat( 5, 5em);
}
.item { gridArea: 4 / 1 / 7 / 6; }Image generation with DALL-E AI api endpoints
const getHeader = key => new Headers({
'Authorization': `Bearer ${key}`,
'Content-Type': 'application/json'
});
// get from :: https://platform.openai.com/account/api-keys
const headers = getHeader('< YOUR-API-KEY >')
const ai = seek => fetch(
"https://api.openai.com/v1/completions",
{
method: 'POST',
headers,
body: JSON.stringify({
"model": "text-davinci-003",
"prompt": seek,
"max_tokens": 2000,
"temperature": 1.0
})
}
)
.then(res => res.json())
.then(res => res.choices[0].text)
.then(console.log)
.catch(console.error)
;



