Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import contractRoutes from "./routes/contractRoutes.js";
import testScreeningRoutes from "./routes/testScreeningRoutes.js";
import recordsRoutes from "./routes/recordsRoutes.js";
import insuranceRoutes from "./routes/insuranceRoutes.js";
import facilityRoutes from "./routes/facilityRoutes.js";
import agencyRoutes from "./routes/agencyRoutes.js";


Expand Down Expand Up @@ -136,6 +137,7 @@ app.use("/api/contracts", apiRateLimiter, contractRoutes);
app.use("/api/movies", apiRateLimiter, testScreeningRoutes);
app.use("/api/records", apiRateLimiter, recordsRoutes);
app.use("/api/insurance", apiRateLimiter, insuranceRoutes);
app.use("/api/facilities", apiRateLimiter, facilityRoutes);
app.use("/api/talent-agencies", apiRateLimiter, agencyRoutes);


Expand Down
78 changes: 78 additions & 0 deletions backend/src/controllers/facilityController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import StudioFacility from "../models/StudioFacility.js";
import Studio from "../models/Studio.js";
import { calculateFacilityUpgrade } from "../services/simulation/engines/facilityEngine.js";

export const getStudioFacilities = async (req, res, next) => {
try {
const facilities = await StudioFacility.find({ studioId: req.user.studioId });
return res.status(200).json({ success: true, data: facilities });
} catch (error) {
next(error);
}
};

export const buildFacility = async (req, res, next) => {
try {
const { facilityType } = req.body;

const existing = await StudioFacility.findOne({ studioId: req.user.studioId, facilityType });
const currentTier = existing ? existing.tierLevel : 0;
const upgradeDetails = calculateFacilityUpgrade(facilityType, currentTier || 1);

const studio = await Studio.findById(req.user.studioId);
if (studio.money < upgradeDetails.cost) {
return res.status(400).json({ success: false, message: "Insufficient studio funds to build facility" });
}

studio.money -= upgradeDetails.cost;
await studio.save();

let facility;
if (existing) {
existing.tierLevel = upgradeDetails.nextTier;
existing.qualityBoost = upgradeDetails.qualityBoost;
existing.maintenanceCostPerWeek = upgradeDetails.maintenanceCostPerWeek;
existing.rentalIncomePerWeek = upgradeDetails.rentalIncomePerWeek;
facility = await existing.save();
} else {
facility = await StudioFacility.create({
studioId: req.user.studioId,
facilityType,
tierLevel: 1,
qualityBoost: upgradeDetails.qualityBoost,
maintenanceCostPerWeek: upgradeDetails.maintenanceCostPerWeek,
rentalIncomePerWeek: upgradeDetails.rentalIncomePerWeek,
});
}

return res.status(201).json({
success: true,
message: `Facility ${facilityType} successfully commissioned`,
data: facility,
});
} catch (error) {
next(error);
}
};

export const toggleFacilityRental = async (req, res, next) => {
try {
const { facilityId, isRentedToThirdParty } = req.body;

const facility = await StudioFacility.findOne({ _id: facilityId, studioId: req.user.studioId });
if (!facility) {
return res.status(404).json({ success: false, message: "Facility not found" });
}

facility.isRentedToThirdParty = isRentedToThirdParty;
await facility.save();

return res.status(200).json({
success: true,
message: `Facility third-party rental status updated to ${isRentedToThirdParty}`,
data: facility,
});
} catch (error) {
next(error);
}
};
44 changes: 44 additions & 0 deletions backend/src/models/StudioFacility.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import mongoose from "mongoose";

const studioFacilitySchema = new mongoose.Schema(
{
studioId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Studio",
required: true,
index: true,
},
facilityType: {
type: String,
required: true,
enum: ["SOUNDSTAGE_COMPLEX", "VFX_VIRTUAL_PRODUCTION_LED", "POST_PRODUCTION_SUITE", "BACKLOT_SET"],
},
tierLevel: {
type: Number,
default: 1,
min: 1,
max: 5,
},
maintenanceCostPerWeek: {
type: Number,
required: true,
default: 10000,
},
qualityBoost: {
type: Number,
default: 5,
},
rentalIncomePerWeek: {
type: Number,
default: 15000,
},
isRentedToThirdParty: {
type: Boolean,
default: false,
},
},
{ timestamps: true }
);

const StudioFacility = mongoose.model("StudioFacility", studioFacilitySchema);
export default StudioFacility;
19 changes: 19 additions & 0 deletions backend/src/routes/facilityRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import express from "express";
import { protect } from "../middleware/authMiddleware.js";
import validateRequest from "../middleware/validationMiddleware.js";
import {
getStudioFacilities,
buildFacility,
toggleFacilityRental,
} from "../controllers/facilityController.js";
import { buildFacilitySchema, toggleRentalSchema } from "../validators/facilityValidators.js";

const router = express.Router();

router.use(protect);

router.get("/list", getStudioFacilities);
router.post("/build", validateRequest(buildFacilitySchema), buildFacility);
router.post("/rental", validateRequest(toggleRentalSchema), toggleFacilityRental);

export default router;
45 changes: 45 additions & 0 deletions backend/src/services/simulation/engines/facilityEngine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Facility Engine
* Calculates maintenance costs, production quality boosts, and third-party rental yields for studio real estate.
*/

export function calculateFacilityUpgrade(facilityType, currentTier = 1) {
const baseCosts = {
SOUNDSTAGE_COMPLEX: 500000,
VFX_VIRTUAL_PRODUCTION_LED: 1200000,
POST_PRODUCTION_SUITE: 350000,
BACKLOT_SET: 450000,
};

const nextTier = Math.min(5, currentTier + 1);
const cost = (baseCosts[facilityType] || 500000) * currentTier;
const qualityBoost = nextTier * 4;
const maintenanceCostPerWeek = Math.round(cost * 0.01);
const rentalIncomePerWeek = Math.round(maintenanceCostPerWeek * 1.5);

return {
nextTier,
cost,
qualityBoost,
maintenanceCostPerWeek,
rentalIncomePerWeek,
};
}

export function processWeeklyFacilities(facilities = []) {
let totalMaintenance = 0;
let totalRentalIncome = 0;

facilities.forEach((f) => {
totalMaintenance += f.maintenanceCostPerWeek || 0;
if (f.isRentedToThirdParty) {
totalRentalIncome += f.rentalIncomePerWeek || 0;
}
});

return {
totalMaintenance,
totalRentalIncome,
netFinancialFlow: totalRentalIncome - totalMaintenance,
};
}
10 changes: 10 additions & 0 deletions backend/src/validators/facilityValidators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { z } from "zod";

export const buildFacilitySchema = z.object({
facilityType: z.enum(["SOUNDSTAGE_COMPLEX", "VFX_VIRTUAL_PRODUCTION_LED", "POST_PRODUCTION_SUITE", "BACKLOT_SET"]),
});

export const toggleRentalSchema = z.object({
facilityId: z.string().min(1, "Facility ID is required"),
isRentedToThirdParty: z.boolean(),
});
29 changes: 29 additions & 0 deletions backend/tests/facilityEngine.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
calculateFacilityUpgrade,
processWeeklyFacilities,
} from "../src/services/simulation/engines/facilityEngine.js";

describe("Facility Engine Unit Tests", () => {
it("calculateFacilityUpgrade returns correct cost and quality boost", () => {
const details = calculateFacilityUpgrade("VFX_VIRTUAL_PRODUCTION_LED", 1);

assert.strictEqual(details.nextTier, 2);
assert.strictEqual(details.cost, 1200000);
assert.strictEqual(details.qualityBoost, 8);
});

it("processWeeklyFacilities calculates maintenance costs and rental income", () => {
const facilities = [
{ maintenanceCostPerWeek: 10000, rentalIncomePerWeek: 15000, isRentedToThirdParty: true },
{ maintenanceCostPerWeek: 5000, rentalIncomePerWeek: 7500, isRentedToThirdParty: false },
];

const result = processWeeklyFacilities(facilities);

assert.strictEqual(result.totalMaintenance, 15000);
assert.strictEqual(result.totalRentalIncome, 15000);
assert.strictEqual(result.netFinancialFlow, 0);
});
});
18 changes: 18 additions & 0 deletions docs/studio-vfx-facility-infrastructure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Studio Real Estate & VFX Facility Infrastructure System

## Overview
The Studio Facility Infrastructure module allows movie studios to acquire physical real estate, construct advanced soundstages, build LED virtual production suites, and establish post-production sound facilities.

## Facility Categories & Benefits
1. **SOUNDSTAGE_COMPLEX**: Cuts production delays and improves film shooting capacity.
2. **VFX_VIRTUAL_PRODUCTION_LED**: Provides major quality boosts for sci-fi/action projects.
3. **POST_PRODUCTION_SUITE**: Enhances film editing, color grading, and audio mastering.
4. **BACKLOT_SET**: Provides reusable outdoor sets and lowers production location budgets.

## Rental Yield Monetization
Idle studio facilities can be leased to third-party independent filmmakers to generate weekly recurring rental revenue for the studio treasury.

## API Endpoints
- `GET /api/facilities/list`: Fetch all facilities owned by the studio.
- `POST /api/facilities/build`: Construct or upgrade a studio facility.
- `POST /api/facilities/rental`: Toggle third-party rental status.
80 changes: 80 additions & 0 deletions frontend/src/pages/studio/FacilityManager.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useState, useEffect } from "react";
import api from "../../api/apiClient";

const FacilityManager = () => {
const [facilities, setFacilities] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
fetchFacilities();
}, []);

const fetchFacilities = async () => {
try {
setLoading(true);
const res = await api.get("/facilities/list");
if (res.data.success) {
setFacilities(res.data.data);
}
} catch (err) {
console.error("Failed to load facilities", err);
} finally {
setLoading(false);
}
};

const handleToggleRental = async (facilityId, isRented) => {
try {
const res = await api.post("/facilities/rental", {
facilityId,
isRentedToThirdParty: !isRented,
});
if (res.data.success) {
fetchFacilities();
}
} catch (err) {
console.error("Failed to toggle rental", err);
}
};

return (
<div className="p-6 max-w-6xl mx-auto">
<h1 className="text-3xl font-bold text-white mb-2">Studio Facilities & Infrastructure</h1>
<p className="text-gray-400 mb-6">Manage studio real estate, soundstages, LED virtual production stages, and third-party rentals.</p>

{loading ? (
<div className="text-gray-400">Loading studio facility assets...</div>
) : facilities.length === 0 ? (
<div className="bg-gray-800 p-8 rounded-xl text-center border border-gray-700">
<p className="text-gray-400">No physical facility assets constructed yet.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{facilities.map((f) => (
<div key={f._id} className="bg-gray-800 p-5 rounded-xl border border-gray-700">
<div className="flex justify-between items-center mb-3">
<h3 className="font-bold text-lg text-white">{f.facilityType.replace(/_/g, " ")}</h3>
<span className="text-xs bg-emerald-900 text-emerald-300 font-bold px-2.5 py-1 rounded">
TIER {f.tierLevel}
</span>
</div>
<div className="text-sm space-y-1 text-gray-300 mb-4">
<p>Quality Boost: <span className="text-green-400 font-semibold">+{f.qualityBoost} pts</span></p>
<p>Weekly Maintenance: <span className="text-red-400 font-semibold">${f.maintenanceCostPerWeek?.toLocaleString()}</span></p>
<p>Rental Yield: <span className="text-yellow-400 font-semibold">${f.rentalIncomePerWeek?.toLocaleString()}/wk</span></p>
</div>
<button
onClick={() => handleToggleRental(f._id, f.isRentedToThirdParty)}
className={`w-full py-2 rounded text-sm font-semibold transition ${f.isRentedToThirdParty ? "bg-amber-600 hover:bg-amber-500 text-white" : "bg-emerald-600 hover:bg-emerald-500 text-white"}`}
>
{f.isRentedToThirdParty ? "Stop Rental (Use In-House)" : "Lease Out to Third-Party"}
</button>
</div>
))}
</div>
)}
</div>
);
};

export default FacilityManager;